I think that the answer provided by @RyanTheLeach addresses your problem quite nicely, via a registry based approach. However, in comments to your question the visitor pattern was brought up. I mentioned "if you create the instances yourself", and... it's not currently elaborated on by any of the existing answers. I find that subtle variations of the problem you've encountered here tend to be annoyingly frequent. Since implementing a wrapper based visitor pattern in Java can be a bit confusing, I thought I'd add some details about that particular approach.
First off, this seems like it should be extremely simple. Just create a visitor interface, a wrapper for the base type, and a wrapper method accepting visitor implementations.
public interface AnimalVisitor {
public void visit(Animal instance);
public void visit(Cat instance);
public void visit(Dog instance);
// ...
}
public class AnimalWrapper {
public final Animal instance;
// ...
public void acceptVisitor(AnimalVisitor visitor) {
visitor.visit(instance);
}
}
Method overloading will take care of the rest, right? It turns out that this won't work, because Java determines the method overload to call based on the statically determined type, as opposed to the actual type at runtime. Note that this is different from inheritance, where dynamic dispatch is used to look up the concrete implementation of a virtual method at runtime.
Perhaps we could tackle this with generics?
public class AnimalWrapper <T extends Animal> {
public final T instance;
// ...
public void acceptVisitor(AnimalVisitor visitor) {
visitor.visit(instance);
}
}
Nope. It might seem like the call to visitor.visit(instance)
should statically determine the subtype, but in reality the static type of a generic is the upper bound of its capture (in this case, Animal
).
Perhaps we could get around this by leveraging the usual Java pattern for obtaining runtime type information for generics?
public class AnimalWrapper <T extends Animal> {
public final T instance;
private final Class<T> type;
// ...
public void acceptVisitor(AnimalVisitor visitor) {
visitor.visit(type.cast(instance));
}
}
Unfortunately this won't work either, for the same reason as before; Class<T> type
is also generic, so it statically resolves to the basetype Animal
.
So how can we make this work? This answer illustrates quite clearly how to go about this using a separate wrapper class for every subtype you want to wrap. Wouldn't it be nice if we could pull this off without needing so many different unique wrapper classes though? You said you're using Java 8, so lets combine lambdas and static factory methods to achieve all of this more or less transparently (and with only a single wrapper class).
The Third-Party Code
public abstract class Animal {
private final String instanceName;
private Animal(String instanceName) {
this.instanceName = instanceName;
}
public abstract String typeName();
public String instanceName() {
return instanceName;
}
public static class Cat extends Animal {
public Cat(String instanceName) {
super(instanceName);
}
@Override
public String typeName() {
return "Cat";
}
}
public static class Dog extends Animal {
public Dog(String instanceName) {
super(instanceName);
}
@Override
public String typeName() {
return "Dog";
}
}
public static class Fox extends Animal {
public Fox(String instanceName) {
super(instanceName);
}
@Override
public String typeName() {
return "Fox";
}
}
}
Our Code
/*
* The Wrapper Class
*/
public class AnimalWrapper {
private final Animal instance;
private final Consumer<AnimalVisitor> visitResolver;
private AnimalWrapper(Animal instance, Consumer<AnimalVisitor> visitResolver) {
this.instance = instance;
this.visitResolver = visitResolver;
}
public Animal getInstance() {
return instance;
}
public void acceptVisitor(AnimalVisitor visitor) {
visitResolver.accept(visitor);
}
public static AnimalWrapper create(Animal instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
public static AnimalWrapper create(Animal.Cat instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
public static AnimalWrapper create(Animal.Dog instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
public static AnimalWrapper create(Animal.Fox instance) {
final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
return new AnimalWrapper(instance, visitResolver);
}
}
/*
* The Visitor Interface
*/
public interface AnimalVisitor {
public default void visit(Animal instance) {
printMessage("Default implementation", "Animal (base type)", instance);
}
public static void visitResolver(AnimalVisitor visitor, Animal instance) {
visitor.visit(instance);
}
public default void visit(Animal.Cat instance) {
printMessage("Default implementation", "Cat", instance);
}
public static void visitResolver(AnimalVisitor visitor, Animal.Cat instance) {
visitor.visit(instance);
}
public default void visit(Animal.Dog instance) {
printMessage("Default implementation", "Dog", instance);
}
public static void visitResolver(AnimalVisitor visitor, Animal.Dog instance) {
visitor.visit(instance);
}
public static void printMessage(String implementation, String signature, Animal instance) {
System.out.println();
System.out.println(implementation);
System.out.println("\tSignature: " + signature);
System.out.println("\tInstance type: " + instance.typeName());
System.out.println("\tInstance name: " + instance.instanceName());
}
}
/*
* The Visitor Implementation
*/
public class AnimalVisitorImpl implements AnimalVisitor {
@Override
public void visit(Animal instance) {
AnimalVisitor.printMessage("Specialized implementation", "Animal (base type)", instance);
}
@Override
public void visit(Animal.Cat instance) {
AnimalVisitor.printMessage("Specialized implementation", "Cat", instance);
}
}
/*
* Actual Usage
*/
public static void main(String[] args) {
final List<AnimalWrapper> wrappedAnimals = new ArrayList<>();
wrappedAnimals.add(AnimalWrapper.create(new Animal.Cat("A normal cat.")));
wrappedAnimals.add(AnimalWrapper.create((Animal) new Animal.Cat("A stealthy cat.")));
wrappedAnimals.add(AnimalWrapper.create(new Animal.Dog("A dog (only default support).")));
wrappedAnimals.add(AnimalWrapper.create(new Animal.Fox("A fox (no support).")));
final AnimalVisitor visitor = new AnimalVisitorImpl();
for (AnimalWrapper w : wrappedAnimals)
w.acceptVisitor(visitor);
}
Output
Specialized implementation
Signature: Cat
Instance type: Cat
Instance name: A normal cat.
Specialized implementation
Signature: Animal (base type)
Instance type: Cat
Instance name: A stealthy cat.
Default implementation
Signature: Dog
Instance type: Dog
Instance name: A dog (only default support).
Specialized implementation
Signature: Animal (base type)
Instance type: Fox
Instance name: A fox (no support).
Other Thoughts
- The library ought to have provided a method accepting a visitor in the first place!
- All of this will only work if we are the ones instantiating the individual instances. As soon as we have to deal with receiving something opaque (such as a
Collection<Animal>
) from the third party code, this approach breaks down and we find ourselves back at a check-and-cast (or reflection) based approach. This is why the registry based approach outlined by @RyanTheLeach really shines; animalInstance.getClass()
returns the unique static class object that is shared by all instances of that particular subtype, thereby allowing us to retrieve the correct lambda even when presented with a basetype instance.