user1889540
user1889540

Reputation: 73

Java generics: put() on Map<String,capture#3-of ? extends AbstractClass> is not applicable for the arguments (String, AbstractClass)

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

Answers (3)

newacct
newacct

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

VGR
VGR

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

Vikdor
Vikdor

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

Related Questions