amirkr
amirkr

Reputation: 183

Java Map with variable generics as values

So here's a slightly tricky question (for me).

I have a generic object. Call it MyObject. This object has a method which returns something of the type T:

public class MyObject<T>
{
    private T _t;

    public MyObject(T t)
    {
        _t = t;
    }

    //...

    public T get()
    {
        return _t;
    }
}

(Obviously my "MyObject" does a bit more but that's the gist).

Now, I want to have a map of this type:

Map<String, MyObject<?>> m = new HashMap<>();

I want to be able to fetch maps using some predefined string name, and these maps can be of any MyObject. For example, I could call:

m.put("map_1", new MyObject<String>("String"));
m.put("map_2", new MyObject<Integer>(new Integer(3));
m.put("map_3", new MyObject<Long>(new Long(5));

etc.

But - and here's the tricky part - I want the map to "remember" the parameterized type of MyObject when I fetch some value from the map. Using

m.get("map_1");

would return a

MyObject<Object> 

type, since the map was defined as containing

MyObject<?> 

values. Thus:

m.get("map_1").get() // <-- This is an Object, not a String! 

What modification (if any) is possible, in order to be able to get the correct - full - information regarding the MyObject fetched object, such that invoking the last line (m.get("map_1")) would return a

MyObject<String>

Thanks :)

Amir.

Upvotes: 1

Views: 4479

Answers (3)

meriton
meriton

Reputation: 70574

The type system only knows about types, not objects, and therefore can not distinguish "key1" from "key2", because both are of type String.

If keys have different types, the easiest way is to encapsulate a weakly typed map, and use reflective casts to prove to the compiler the types are correct:

class Favorites {

    private Map<Class<?>,?> map = new HashMap<>();

    <V> V get(Class<V> clazz) {
        return clazz.cast(map.get(clazz));
    }

    <V> void put(Class<V> clazz, V value) {
        map.put(clazz, value);
    }
}

Favorites favs = new Favorites();
favs.put(String.class, "hello");
favs.put(Integer.class, 42);
favs.get(String.class).charAt(1); 

Upvotes: 0

markspace
markspace

Reputation: 11030

Typesafe Heterogeneous Containers from Joshua Bloch's Effective Java might work here. Basically you add a Class object to represent the type.

public class MyObject<T>
{
    private T _t;
    private Class<T> type;

    public MyObject( Class<T> type, T t)
    {
        _t = t;
        this.type = type;
    }

    //...

    public T get()
    {
        return _t;
    }

    public Class<T> getType() { return type; }
}

Then you could do something like this:

public <T> T get( Map<String, MyObject<?>> map, String key, Class<T> type ) {
   return type.cast( m.get( key ).get() );
}

Which is safe and will compile, but will throw a runtime error if you get the type wrong.

(Note I didn't actually compile that, so I might have syntax errors floating around. But most folks don't know how to use Class to cast objects.)

Upvotes: 6

Jose Martinez
Jose Martinez

Reputation: 12022

You can get the class.

Class c = m.get("map_1").get().getClass();
if (String.class.equals(c)) {
    System.out.println("its a String");
}

Here is a full test.

public class GenericsTest {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {

        Map<String, MyObject<?>> map = new HashMap<>();
        MyObject<String> obj = new MyObject<>("hello");

        map.put("greeting", obj);

        Class c = map.get("greeting").get().getClass();
        if (String.class.equals(c)) {
            System.out.println("its a String");
        }

    }

    static class MyObject<T> {

        T t;

        public MyObject(T t) {
            this.t = t;
        }

        T get() {
            return t;
        }

    }

}

Upvotes: 0

Related Questions