0

I am trying to refactor some code, moving common code shared among a few caches to a base class. Here's a simplified concept of it:

public abstract class DBObject {
    public void copyTo(DBObject other) {
       other.setId(this.id);
    }
} 

public class Person extends DBObject {
    public void copyTo(Person other) {
       super.copyTo(other);
       other.setName(this.name);
    }
} 

public class PersonCache extends Cache<Person> {
}

public abstract class Cache<T extends DBObject>  {
    Map<Long, T> idToCachedMap;
    private Class<T> tObjectClass;

    public void initialize() {
        // does stuff to populate the idToCachedMap
    }

    public void updateCache(T cachedObjToUpdate) {
        T cachedObj = idToCachedMap.get(cachedObjToUpdate.getId());
        T oldCachedObj = tObjectClass.newInstance();;

        cachedObj.copyTo(oldCachedObj); // PROBLEM HERE
        // do other stuff...
    }
}

The problem I'm running into is that when I call updateCache(Person) on a PersonCache, the copyTo methods that get invoked on the objects are that in DBObject, as opposed to the one in Person. As a result, only some of the data is actually copied (in this example case, the ID, but not the name).

It seems to me that since both cachedObj and oldCachedObjs are guaranteed to be Person objects if it's a PersonCache, the copyTo method that should be called is the one on the Person class.

I feel like there must be something about how generics work that I'm missing that is causing this behavior. I know if I override copyTo in the Person class to be a signature of copyTo(DBObject other) rather than copyTo(Person other) it then does call the copyTo on the person class - but that'd be a sloppy way to rewrite it and I think I'm missing something that might be cleaner.

2 Answers2

3

You are not overwriting the copyTo method because you change the signature. And you invoke the method which exist in T.

Try this:

public abstract class DBObject<T extends DBObject> {
  public void copyTo(T other) {
    other.setId(this.id);
  }
} 

public class Person extends DBObject<Person> {
  @Override
  public void copyTo(Person other) {
   super.copyTo(other);
   other.setName(this.name);
  }
} 
nrainer
  • 2,383
  • 1
  • 18
  • 33
2

You state It seems to me that since both cachedObj and oldCachedObjs are guaranteed to be Person objects if it's a PersonCache, the copyTo method that should be called is the one on the Person class.

Because of type erasure this is an incorrect assumption, at runtime all it knows is DBObject and obviously Object as well.

It knows nothing about T at runtime, it is erased and not available at runtime.

copyTo(T other) is not equivlent to copyTo(Person other) they are overloaded not overridden because of the type erasure. copyTo(T other) actually becomes copyTo(DBObject other) as your behavior is showing that it matches copyTo(DBObject other). This is the expected behavior.

Type Erasure behavior is very well documented here on SO and on the internet in general.