Search code examples
javaclassgenericsinterfacetreemap

TreeMap with Classes as Key


I am trying to program a kind of registry for objects of different classes.

I have the following:

public interface DbObject{
    void setId(long id);
    Long getId();
}

A prototypic class implementing this interface would be the following:

public class BasicDbObject implements DbObject{
    private long id=null;
    void setId(long id){
        this.id=id;
    }
    Long getId(){
        return id;
    }
}

I want to build various different Implementations of this Interface. And I want to be able to have a Map object, that maps from each implementing class to a Map of instances.

Something like this:

Map <Class<C implements DbObject> , Map<Long, C>> registry = new TreeMap/HashMap/SomeOtherKindOfMap (...)

I know I could do something like

Map <String,Map<Long,DbObjects>> registry = new ...

But this way I would have to write some more code for determining names, comparing classes and so on. Is there an easier way to accomplish this?

So what I want to know: is it possible to have class objects as keys in a tree map?

What would be the syntax to declare a map object, that maps from implementing classes C to a map objects each mapping from a long object (the id) to instances of C?

I want to be able to do requests like the following:

BasicObject bo = registry.get(BasicObject.class).get(42);

assuing id did

BasicObject bo=new BasicObject(...);
innerMap = new SomeMap<Long,BasicObject>();
innerMap.put(42,bo);
registry.put(BasicObject.class,innerMap);

before.

Please tell me, if this still is not clear, I have difficulties to explain, since english is not my mother tongue.

Thank you in advance.


Edit:

It turns out, i can do something very close to what I want, when defining a generic class around the map:

public class ObjectRegistry <T extends DbObject>{

    private HashMap<Class<T>, TreeMap<Long,T>> registry=null;

    ObjectRegistry(){
        registry=new HashMap<Class<T>, TreeMap<Long,T>>();
    }
    public void register(T dbObject){
        TreeMap<Long, T> map = registry.get(dbObject.getClass());
        if (map==null){
            map=new TreeMap<Long,T>();
            registry.put((Class<T>) dbObject.getClass(),map);
        }
        map.put(dbObject.getId(),dbObject);
    }

    public <T extends DbObject>T get(Class<T> objectClass,long id){
        TreeMap<Long, T> map = (TreeMap<Long, T>) registry.get(objectClass);
        if (map != null){
            return map.get(id);
        }
        return null;
    }

    public TreeMap<Long,T> getAll(Class<T> dbObjectClass) {
        return registry.get(dbObjectClass);
    }
}

I use a TreeMap for the inner mappings since I want to easily return Class instances sorted by id.

So the refined question is: Is there a way to do this, without the <T extends DbObject> clause in the Class head?


Edit 2:

Thinking through it again, it turns out that John's answer is exactly the solution to this.

Here is my final code:

HashMap<Class<? extends DbObject>, TreeMap<Long, ? extends DbObject>> registry = null;

public <T extends DbObject> T get(Class<T> clazz, long id) {
    TreeMap<Long, T> map = (TreeMap<Long, T>) registry.get(clazz);
    if (map != null) {
        return map.get(id);
    }
    return null;
}

public <T extends DbObject> void register(T dbObject) {
    TreeMap<Long, T> map = (TreeMap<Long, T>) registry.get(dbObject.getClass());
    if (map == null) {
        map = new TreeMap<Long, T>();
        registry.put((Class<T>) dbObject.getClass(), map);
    }
    map.put(dbObject.getId(), dbObject);
}


public <T extends DbObject> TreeMap<Long, T> getAll(Class<T> dbObjectClass) {
    return (TreeMap<Long, T>) registry.get(dbObjectClass);
}

It does not need the <T extends DbObject> clause in the Class head.


Solution

  • So what I want to know: is it possible to have class objects as keys in a tree map?

    TreeMap depends on there being a total order over the key space, as established by the key type having a natural order (by implementing Comparable) or by a separate Comparator object that you provide. Classes do not have a natural order. It is conceivable that you could write a suitable Comparator, but that seems very contrived to me.

    But why do you specifically need a TreeMap? You didn't describe any requirement that would not be at least as well addressed by any other kind of Map. In particular, I almost always find HashMap to be a better choice, and I don't see any reason why it would be unsuitable in this one. It can certainly have objects of type Class as keys.

    Moreover, if indeed you don't need any particular implementation, then you are best off declaring the type simply as a Map. That way you can actually provide any Map implementation, and even change which one you do provide if you ever discover a reason to do so.

    What would be the syntax to declare a map object, that maps from implementing classes C to a map objects each mapping from a long object (the id) to instances of C?

    You ask that the constraints on the type of each value be dependent on the type of the associated key, but there is no way to declare a type that enforces such a relationship. Whether a particular key or a particular value is appropriate for the Map is a function of the type of the map alone, not of each others' type.

    You can write generic methods around access to your map that provide the appearance of what you want, but the data retrieval methods will need to cast. For example:

    Map<Class<? extends DbObject>, Map<Long, ? extends DbObject>> registry = /*...*/;
    
    <T extends DbObject> Map<Long, T> getRegistryMap(Class<T> clazz) {
        return (Map<Long, T>) registry.get(clazz);
    }
    
    <T extends DbObject> T get(Class<T> clazz, Long id) {
        Map<Long, T> map = getRegistryMap(clazz);
    
        return (map == null) ? null : map.get(id);
    }
    
    <T extends DbObject> T put(Class<T> clazz, Long id, T obj) {
        Map<Long, T> map = getRegistryMap(clazz);
    
        if (map == null) {
            map = new HashMap<>();
            registry.put(clazz, map);
        }
        return map.put(id, obj);
    }
    

    Updated to add:

    So the refined question is: Is there a way to do this, without the <T extends DbObject> clause in the Class head?

    Yes, what I already wrote. Just slap a plain class declaration around it. You do not need a generic class to have generic methods. In fact, the two are orthogonal. Regular methods of a generic class can use that class's type parameters. That does not make them generic methods. A method is generic if it declares its own type parameter(s), as mine above do. Your get() method also does that, and it is important to understand that the type parameter <T> you declare explicitly in the method signature shadows the class's type parameter of the same name: it is a different T.