I'm trying to combine this 3 features of Guice: inject, multibinding, generics. I create a prototype of production project, so here it is:
First, this is a little hierarchy for generics(in production case there is hierarchy of N entities):
public interface Type {
}
public class Type1 implements Type{
}
public class Type2 implements Type {
}
Next, classes ToCreate1 and ToCreate2 I want to create by Factory.
Base class:
public abstract class AbstractToCreate<T extends Type> {
public T type;
public Integer param;
public AbstractToCreate(T type, Integer param){
this.type = type;
this.param = param;
}
}
It's inheritors:
public class ToCreate1 extends AbstractToCreate<Type1>{
@Inject
public ToCreate1(Type1 type, @Assisted Integer param) {
super(type, param);
}
}
public class ToCreate2 extends AbstractToCreate<Type2> {
@Inject
public ToCreate2(Type2 type, @Assisted Integer param) {
super(type, param);
}
}
Then, the Factory itself:
public interface Factory<T extends Type> {
AbstractToCreate<T> create(Integer param);
}
So, now I want to Inject a map, containing Factory<Type1> and Factory<Type2> to create ToInject1 and ToInject2 respectively.
So, I create Guice's AbstractModule with configure method:
protected void configure() {
install(new FactoryModuleBuilder()
.implement(new TypeLiteral<AbstractToCreate<Type1>>(){}, ToCreate1.class)
.build(new TypeLiteral<Factory<Type1>>(){}));
install(new FactoryModuleBuilder()
.implement(new TypeLiteral<AbstractToCreate<Type2>>(){}, ToCreate2.class)
.build(new TypeLiteral<Factory<Type2>>(){}));
MapBinder<String, Factory> mapBinder = MapBinder.newMapBinder(binder(), String.class, Factory.class);
mapBinder.addBinding("type1").to(new TypeLiteral<Factory<Type1>>(){});
mapBinder.addBinding("type2").to(new TypeLiteral<Factory<Type2>>(){});
}
So, I inject it @Inject public Map<String, Factory> map;
and all is Ok:
Factory<Type1> factory1 = main.map.get("type1");
Factory<Type2> factory2 = main.map.get("type2");
AbstractToCreate<Type1> create1 = factory1.create(1);//create1 is ToCreate1 instance
AbstractToCreate<Type2> create2 = factory2.create(2);//create2 is ToCreate2 instance
As I mentioned before, there is much more Types in my production system, so the AbstractModule becomes too cumbersome. I tried to avoid duplicate code and modified configure method:
@Override
protected void configure() {
this.<Type1>inst(ToCreate1.class);
this.<Type2>inst(ToCreate2.class);
}
private <V extends Type> void inst(Class<? extends AbstractToCreate<V>> clazz) {
install(new FactoryModuleBuilder()
.implement(new TypeLiteral<AbstractToCreate<V>>(){}, clazz)
.build(new TypeLiteral<Factory<V>>(){}));
}
And it doesn't work! Guice says:
1) ru.test.genericassistedinject.AbstractToCreate<V> cannot be used as a key; It is not fully specified.
What's wrong?
The problem here is with type erasure. In particular, this code:
can't work because it's relying on type parameter
V
to help make a runtime decision (what binding to use), but the type parameterV
has no runtime representation, so its value can never directly affect runtime. Another way of thinking about this: Java can't "read" a type parameter's value in a generic;new TypeLiteral<Factory<V>>(){}
is always the same value regardless of whatV
is instantiated with in the caller.As is often the case when you run into erasure-related problems, the trick is to add a runtime value that represents the type you want. In this case that's especially tricky, since what you want to do is represent a value for a type parameter to a larger type.
There are a few ways to get runtime values that represent static types.
TypeToken
is one andClass
is another, but neither of them will allow you to represent a type with a parameter and then programmatically fill that value. Luckily, Google Guava contains another representation,com.google.common.reflect.TypeToken
, that will work for us.TypeToken
s can represent a type with a variable and support programmatically "filling in" that variable with a concrete representation, e.g:represents the type
List<Integer>
at runtime.Using
TypeToken
we can build our types, like so:Now we can call
inst
with:and everything will work as desired.
This is pretty fancy stuff, though, and understanding it hinges on having a pretty good understanding of the difference between compile-time and runtime representations of types. If it were me, I wouldn't do this if it was something you only expected to use once or twice, since the confusion burden is pretty high; I'd only do it if this was part of a library or something and you could save some work for every caller.