Short answer
I'm afraid there is no really clean way. Since the types are erased, you need to keep the type info somewhere and your third suggestion using the getAnimalClass()
is one way to do it (however it's unclear in your question how you put it to use later on).
I would personally get rid of the lambda's and add a canFeed(animal)
to the Feeder
to delegate the decision (instead of adding the getAnimalClass()
). That way, the Feeder
is responsible for knowing what animals it can feed.
So the type info will be kept in the Feeder class, for example using a Class
instance passed with construction (or by overriding a getAnimalClass()
as you presumably did):
final Class<T> typeParameterClass;
public Feeder(Class<T> typeParameterClass){
typeParameterClass = typeParameterClass;
}
So that it can be used by the canFeed
method:
public boolean canFeed(Animal animal) {
return typeParameterClass.isAssignableFrom(animal.getClass());
}
This will keep the code in the PetStore
pretty clean:
private Feeder feederFor(Animal animal) {
return feeders.stream()
.filter(feeder -> feeder.canFeed(animal))
.findFirst()
.orElse((Feeder) unknownAnimal -> {});
}
Alternative Answer
As you said, storing the type info makes it clumsy. You could however pass the type info in another, less strict way, without cluttering the Feeders, for example by relying on the names of the Spring beans you inject.
Let's say you inject all the Feeders in the PetStore
in a Map
:
@Autowired
Map<String, Feeder<? extends Animal>> feederMap;
Now you have a map that contains the feeder name (and implicit it's type) and the corresponding Feeder. The canFeed
method is now simply checking the substring:
private boolean canFeed(String feederName, Animal animal) {
return feederName.contains(animal.getClass().getTypeName());
}
And you can use it to fetch the correct Feeder
from the map:
private Feeder feederFor(Animal animal) {
return feederMap.entrySet().stream()
.filter(entry -> canFeed(entry.getKey(), animal))
.map(Map.Entry::getValue)
.findFirst()
.orElse((Feeder) unknownAnimal -> {});
}
Deep dive
The lambda's are clean and concise, so what if we want to keep the lambda expressions in the config. We need the type information, so the first try can be to add the canFeed
method as a default
method on the Feeder<T>
interface:
default <A extends Animal> boolean canFeed(A animalToFeed) {
return A == T;
}
Of course we can't do A == T, and because of type erasure, there is no way to compare the generic types A and T. Normally, there is the trick you referred to, that only works when using generic supertypes. You mentioned the Spring toolbox method, but let's look at the Java implementation:
this.entityBeanType = ((Class) ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0]);
Since we have multiple Feeder implementations with a Feeder<T>
superinterface, you might think we can adopt this strategy. However, we can't, because we are dealing with lambda's here and lambda's are not implemented as anonymous inner classes (they are not compiled to a class but use an invokedynamic
instruction) and we loose the typing info.
So back to the drawing board, what if we make it an abstract class instead, and use it as a lambda. This however is not possible and Brian Goetz, chief language architect, explains why on the mailinglist:
Before throwing this use case under the bus, we did some corpus analysis to found how often abstract class SAMs are used compared to
interface SAMs. We found that in that corpus, only 3% of the lambda
candidate inner class instances had abstract classes as their target.
And most of them were amenable to simple refactorings where you added
a constructor/factory that accepted a lambda that was
interface-targeted.
We could go and create factory methods to work around this, but that again would be clumsy and get us too far.
Since we got this far, let's take our lambda's back and try to fetch the type info in some other way. Spring's GenericTypeResolver
did not bring the expected result Animal
, but we could get hacky and take advantage of the way the Type info is stored in bytecode.
When compiling a lambda, the compiler inserts a dynamic invoke instruction that points to a LambdaMetafactory and a synthetic method with the body of the lambda. The method handle in the constant pool contains the generic type, since the generics in our Config
our explicit. At runtime, ASM generates a class that implements the functional interface. Unfortunately, this specific generated class does not store the generic signatures and you can not use reflection to get around the erasure, since it is defined using Unsafe.defineAnonymousClass
. There is a a hack to get the info out of the Class.getConstantPool
that uses the ASM to parse and return the argument types, but this hack relies on undocumented methods and classes and is vulnerable to code changes in the JDK. You could hack it in yourself (by coping pasting the code from the reference) or use a library that implements this approach such as TypeTools. Other hacks could work too, such as adding Serialization
support to the lambda and try to fetch the method signature of the instantiated interface from the serialized form. Unfortunately, I did not find a way yet to resolve the type info with the new Spring api's.
If we take this approach to add the default method in our interface, you can keep all your code (e.g. the config) and the actual Feeder
-hack hack reduces to:
default <A extends Animal> boolean canFeed(A animalToFeed) {
Class<?> feederType = TypeResolver.resolveRawArgument(Feeder.class, this.getClass());
return feederType.isAssignableFrom(animalToFeed.getClass());
}
The PetStore
stays clean:
@Autowired
private List<Feeder<? extends Animal>> feeders;
public void feed(Animal animal) {
feederFor(animal).feed(animal);
}
private Feeder feederFor(Animal animal) {
return feeders.stream()
.filter(feeder -> feeder.canFeed(animal))
.findFirst()
.orElse(unknownAnimal -> {});
}
So unfortunately, there is no straightforward approach and I guess we can safely conclude we checked all (or at least more than a few basic) options.