Reputation: 73
I'm trying to implement a sort of intern factory for multiple classes that extend from a common parent. Much of the logic is identical, but it can't really be inherited because the lookups need to be static. The desired syntax is something like:
Car c = AbstractClass.valueOf(Car.class, "Ford");
with Car having specific methods related to cars, but the instances are stored in a common cache. Here's what I have so far. My compile error is on the put in the constructor:
"The method put(String, capture#3-of ? extends AbstractClass) in the type Map is not applicable for the arguments (String, AbstractClass)"
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public abstract class AbstractClass {
private static Map<Class<? extends AbstractClass>, LinkedHashMap<String, ? extends AbstractClass>> map = new HashMap<Class<? extends AbstractClass>, LinkedHashMap<String, ? extends AbstractClass>>();
private static synchronized <T extends AbstractClass> Map<String, T> getNameMap(Class<T> clazz) {
LinkedHashMap<String, T> nameToEnum = (LinkedHashMap<String, T>) map.get(clazz);
if (nameToEnum == null) {
nameToEnum = new LinkedHashMap<String, T>();
map.put(clazz, nameToEnum);
}
return nameToEnum;
}
public static <T extends AbstractClass> T valueOf(Class<T> clazz, String name) {
return getNameMap(clazz).get(name);
}
public static <T extends AbstractClass> Collection<T> VALUES(Class<T> clazz) {
return getNameMap(clazz).values();
}
public static <T extends AbstractClass> Set<T> SORTED_VALUES(Class<T> clazz) {
return new TreeSet<T>(getNameMap(clazz).values());
}
AbstractClass(String name) {
AbstractClass.getNameMap(this.getClass()).put(name, this);
}
}
Upvotes: 5
Views: 13030
Reputation: 122499
Your problem can basically be boiled down to this:
Given a method with this signature:
public static <T> void foo(T x, Class<T> y);
and a variable of any reference type:
<any reference type> bar;
it is impossible to pass bar
and bar.getClass()
to this method:
foo(bar, bar.getClass()); // error
even though it is provable that there always exists some T
for which it is correct (i.e. T
= the actual runtime type of bar
).
It is due to the special case in the language for the type of .getClass()
that causes this problem.
I can think of two ways to solve this:
1) Cast the class object to be parameterized by the same type as the reference (even though this is technically not true):
AbstractClass(String name) {
AbstractClass.getNameMap((Class<AbstractClass>)this.getClass()).put(name, this);
}
2) Cast the object to the same type as the parameter of the class method. This will require a capture helper due to the wildcard in the class's type:
private static <T> void helper(Class<T> clazz, String name, Object obj) {
AbstractClass.getNameMap(clazz).put(name, (T)obj);
}
AbstractClass(String name) {
helper(this.getClass(), name, this);
}
(if you don't want that unchecked cast you can do AbstractClass.getNameMap(clazz).put(name, clazz.cast(obj));
)
Upvotes: 0
Reputation: 44414
According to the javadoc for Object.getClass()
, the returned type is a wildcard based compile-time type of the expression. Since the compiler only knows that this
returns an AbstractClass instance, this.getClass()
returns Class<? extends AbstractClass>
.
This means your call to getNameMap
in the constructor will return a Map<String, ? extends AbstractClass>
. Which means that, while the returned Map has values of a specific (non-wildcard) type, that exact type isn't known at compile-time; the compiler only knows the Map's values are required to be either AbstractClass or something that inherits from AbstractClass. So the compiler can't safely add this
as a value, since it isn't known at compile-time which subtype of AbstractClass this
represents.
To use a simpler example: if a method returned Map<String, ? extends Number>
then the compiler wouldn't know whether it was safe to add an Integer to the Map, because the Map's actual, non-wildcard type might be Map<String, Double>
, Map<String, Short>
, etc.
As for a solution: I don't think there is a way to have a Map use generics to match each individual key's type with its corresponding value's type. I would forget about using bounded types on the inner Maps' values, and use dynamic casting instead:
private static Map<Class<? extends AbstractClass>, Map<String, AbstractClass>> map = new HashMap<>();
private static synchronized Map<Class<? extends AbstractClass>, Map<String, AbstractClass>> getNameMap(Class<T> clazz) {
// same as before
}
public static <T extends AbstractClass> T valueOf(Class<T> clazz, String name) {
return clazz.cast(getNameMap(clazz).get(name));
}
Upvotes: 3
Reputation: 24134
If you just want to store anything that is an AbstractClass, just declare your map as
private static Map<Class<? extends AbstractClass>, LinkedHashMap<String, AbstractClass>> map =
new HashMap<Class<? extends AbstractClass>, LinkedHashMap<String, AbstractClass>>();
This would allow you to store any instance of AbstractClass or its subclasses in the inner map, against AbstractClass or one of its sub class.
Upvotes: 1