views:

3387

answers:

9

Consider this example (typical in OOP books): I have an Animal class, where each Animal can have many friends. And subclasses like Dog, Duck, Mouse etc which add specific behavior like bark(), quack() etc.

Here's the Animal class:

public class Animal {
    private Map<String,Animal> friends = new HashMap<String,Animal>();

    public void addFriend(String name, Animal animal){
        friends.put(name,animal);
    }

    public Animal callFriend(String name){
        return friends.get(name);
    }
}

And here's some code snippet with lots of typecasting:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

((Dog) jerry.callFriend("spike")).bark();
((Duck) jerry.callFriend("quacker")).quack();

Is there any way i can use Generics for the return type to get rid of the typecasting so that i can say

jerry.callFriend("spike").bark();
jerry.callFriend("quacker").quack();

Here's some initial code with Return type conveyed to the method as a parameter thats never used.

public<T extends Animal> T callFriend(String name, T unusedTypeObj){
    return (T)friends.get(name);        
}

Is there a way to figure out the Return type at runtime without the extra parameter using instanceof. Or atleast by passing a class of the Type instead of a dummy instance. I understand Generics is for compile time typechecking, but is there a workaround for this?

+11  A: 

No. The compiler can't know what type jerry.callFriend("spike") would return. Also, your implementation just hides the cast in the method without any additional type safety. Consider this:

jerry.addFriend("quaker", new Duck());
jerry.callFriend("quaker", /* unused */ new Dog()); // dies with illegal cast

In this specific case, creating an abstract talk() method and overriding it appropriately in the subclasses would server you much better:

Mouse jerry = new Mouse();
jerry.addFriend("spike", new Dog());
jerry.addFriend("quacker", new Duck());

jerry.callFriend("spike").talk();
jerry.callFriend("quacker").talk();
David Schmitt
While mmyers method may work, I think this method is better OO programming and will save you some trouble in the future.
James McMahon
A: 

Not really, because as you say, the compiler only knows that callFriend() is returning an Animal, not a Dog or Duck.

Can you not add an abstract makeNoise() method to Animal that would be implemented as a bark or quack by its subclasses?

sk
what if the animals have multiple methods which do not even fall under a common action that can be abstracted? I need this for communication between subclasses with different actions where im okay with passing the Type, not an instance.
Sathish
You've really just answered your own question - if an animal has a unique action, then you have to cast to that specific animal. If an animal has an action that can be grouped with other animals, than you can define an abstract or virtual method in a base class and use that.
Matt Jordan
+2  A: 

You could implement it like this:

@SuppressWarnings("unchecked")
public <T extends Animal> T callFriend(String name){
    return (T)friends.get(name);
}

(Yes, this is legal code; see this question.)

The return type will be inferred from the caller. However, note the @SuppressWarnings annotation: that tells you that this code isn't typesafe. You have to verify it yourself, or you could get ClassCastExceptions at runtime.


EDIT: Unfortunately, the way you're using it (without assigning the return value to a temporary variable), the only way to make the compiler happy is to call it like this:

jerry.<Dog>callFriend("spike").bark();

While this may be a little nicer than casting, you are probably better off giving the Animal class an abstract talk() method, as David Schmitt said.

Michael Myers
Oh that is interesting. OP let me know if this works for you.
James McMahon
Method chaining was not really an intention. i don't mind assigning the value to a Subtyped variable and using it. Thanks for the solution.
Sathish
+1  A: 

Not possible. How is the Map supposed to know which subclass of Animal it's going to get, given only a String key?

The only way this would be possible is if each Animal accepted only one type of friend (then it could be a parameter of the Animal class), or of the callFriend() method got a type parameter. But it really looks like you're missing the point of inheritance: it's that you can only treat subclasses uniformly when using exclusively the superclass methods.

Michael Borgwardt
+8  A: 

You could define callFriend this way:

public <T extends Animal> T callFriend(String name, Class<T> type) {
    return type.cast(friends.get(name));
}

Then call it as such:

jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();

This code has the benefit of not generating any compiler warnings.

laz
... but still has no compile time type checking between the parameters of the callFriend() call.
David Schmitt
This is the best answer so far - but you ought to change addFriend in the same way. It makes it harder to write bugs since you need that class literal in both places.
Craig P. Motlin
+1  A: 

As you said passing a class would be OK, you could write this:

public <T extends Animal> T callFriend(String name, Class<T> clazz) {
   return (T) friends.get(name);
}

And then use it like this:

jerry.callFriend("spike", Dog.class).bark();
jerry.callFriend("quacker", Duck.class).quack();

Not perfect, but this is pretty much as far as you get with Java generics. There is a way to implement Typesafe Heterogenous Containers (THC) using Super Type Tokens, but that has its own problems again.

Fabian Steeg
I am sorry but this is the exact same answer laz has, so you are either copying him or he is copying you.
James McMahon
Thats a neat way to pass the Type. But still it aint type safe like Schmitt said. I could still pass a different class and the typecasting will bomb. mmyers 2nd reply to set the Type in Return type seems better
Sathish
Nemo, if you check the posting time you'll see we posted them pretty much exactly at the same moment. Also, they are not exactly the same, only two lines.
Fabian Steeg
@Fabian I posted a similar answer, but there's an important difference between Bloch's slides and what was published in Effective Java. He uses Class<T> instead of TypeRef<T>. But this is still a great answer.
Craig P. Motlin
+2  A: 

This question is very similar to Item 29 in Effective Java - "Consider typesafe heterogeneous containers." Laz's answer is the closest to Bloch's solution. However, both put and get should use the Class literal for safety. The signatures would become:

public <T extends Animal> void addFriend(String name, Class<T> type, T animal);
public <T extends Animal> Animal callFriend(String name, Class<T> type);

Inside both methods you should check that the parameters are sane. See Effective Java and the Class javadoc for more info.

Craig P. Motlin
A: 

Based on the same idea as Super Type Tokens, you could create a typed id to use instead of a string:

public abstract class TypedID<T extends Animal> {
  public final Type type;
  public final String id;

  protected TypedID(String id) {
    this.id = id;
    Type superclass = getClass().getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    }
    this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
  }
}

But I think this may defeat the purpose, since you now need to create new id objects for each string and hold on to them (or reconstruct them with the correct type information).

Mouse jerry = new Mouse();
TypedID<Dog> spike = new TypedID<Dog>("spike") {};
TypedID<Duck> quacker = new TypedID<Duck>("quacker") {};

jerry.addFriend(spike, new Dog());
jerry.addFriend(quacker, new Duck());

But you can now use the class in the way you originally wanted, without the casts.

jerry.callFriend(spike).bark();
jerry.callFriend(quacker).quack();

This is just hiding the type parameter inside the id, although it does mean you can retrieve the type from the identifier later if you wish.

You'd need to implement the comparison and hashing methods of TypedID too if you want to be able to compare two identical instances of an id.

Mike Houston
A: 

I've written an article which contains a proof of concept, support classes and a test class which demonstrates how Super Type Tokens can be retrieved by your classes at runtime. In a nutshell, it allows you to delegate to alternative implementations depending on actual generic parameters passed by the caller. Example:

TimeSeries delegates to a private inner class which uses double[];
TimeSeries delegates to a private inner class which uses ArrayList;

See:
http://www.jquantlib.org/index.php/Using_TypeTokens_to_retrieve_generic_parameters

Thanks

Richard Gomes
http://www.jquantlib.org/index.php/User:RichardGomes

Richard Gomes