'Using interface Class<T> as keys to get concrete instance values?

I have the following test case which fails to retrieve the values from the Map:

package tests;

import java.util.HashMap;
import java.util.Map;

public class ClassTest {

    interface A {}
    interface B extends A {}
    interface C extends A {}

    class D implements B {}
    class E implements C {}

    public ClassTest() {
        Map<Class<? extends A>, A> map = new HashMap<>();

        A d = new D();
        A e = new E();

        map.put(d.getClass(), d);
        map.put(e.getClass(), e);

        System.out.println(B.class.getSimpleName() + ": " + map.get(B.class));
        System.out.println(C.class.getSimpleName() + ": " + map.get(C.class));
    }

    public static void main(String[] args) {
        new ClassTest();
    }

}

The expected output was:

B: D
C: E

The actual output is:

B: null
C: null

From what I understand, the case is "expected" to fail because B.class will not be equal to D.class, even though D class is an implementation of the B interface... so the map.get(...) fails to find the instance value for the associated key. (Correct me if I'm wrong on this.) The case above hopefully shows the intention and "spirit" behind what I want to accomplish.

Is there a good/elegant alternative for this that works but also preserves the spirit of what I was trying to do?

I'm currently updating code to replace the enum-types that are being used as 'open sets' for Class<T> as type tokens, somewhat similar to Effective Java, 2nd Ed., Item 29.


As requested by @CKing in a comment, the part of the book that motivated my approach is quoted below.

The client presents a Class object when setting and getting favorites. Here is the API:

// Typesafe heterogeneous container pattern - API
public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}

Here is a sample program that exercises the Favorites class, storing, retrieving, and printing a favorite String, Integer, and Class instance:

// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
    Favorites f = new Favorites();

    f.putFavorite(String.class, "Java");
    f.putFavorite(Integer.class, 0xcafebabe);
    f.putFavorite(Class.class, Favorites.class);

    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    Class<?> favoriteClass = f.getFavorite(Class.class);
    System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}

As you might expect, this program prints Java cafebabe Favorites.

Please understand that I know the book's example works because it's using the specific concrete class of the value (e.g. String.class for an actual String, and not some hypothetical derived type from String, etc.) As stated, this simply motivated my approach to see if my test case would work, and now I'm looking for a solution or alternative that respects the "spirit" of what I intended to do on the test case.



Solution 1:[1]

Maybe not so elegant, but you can use reflection to get all values assignable from Key.class:

System.out.println(B.class.getSimpleName() + ": " + getMapEntries(map, B.class));
System.out.println(C.class.getSimpleName() + ": " + getMapEntries(map, C.class));

....

private <T extends A> List<T> getMapEntries(Map<Class<? extends A>, A> map, Class<T> clazz) {
    List<T> result = new ArrayList<>();
    for (Map.Entry<Class<? extends A>, A> entry : map.entrySet()) {
        if (clazz.isAssignableFrom(entry.getKey())) {
            result.add((T) entry.getValue());
        }
    }
    return result;
}

Solution 2:[2]

If you want interface class keys to map to concrete instances, then you need to explicitly add those to your map.

package tests;

import java.util.HashMap;
import java.util.Map;

public class ClassTest {

    interface A {}
    interface B extends A {}
    interface C extends A {}

    class D implements B {}
    class E implements C {}

    public ClassTest() {
        Map<Class<? extends A>, A> map = new HashMap<>();

        A d = new D();
        A e = new E();

        map.put(B.class,d);
        map.put(C.class,e);
        map.put(d.getClass(), d);
        map.put(e.getClass(), e);

        System.out.println(B.class.getSimpleName() + ": " + map.get(B.class));
        System.out.println(C.class.getSimpleName() + ": " + map.get(C.class));
    }

    public static void main(String[] args) {
        new ClassTest();
    }

}

Solution 3:[3]

You can override your Map.put() so that whenever anyone puts an a D.class, it keys it under B.class. Like this:

package com.matt.tester;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Tester {

        interface A {}
        interface B extends A {}
        interface C extends A {}
        interface Q {};

        class D implements B, Q {}
        class E implements C, Q {}


        // New method to get the object's interface that derives from A.
        private Class<? extends A> getHierarchyClass(Class cls) {
            List<Class<?>> classList = Arrays.asList(cls.getInterfaces()).stream().filter(i -> A.class.isAssignableFrom(i)).collect(Collectors.toList());
            if ( classList.isEmpty() ) {
                return null;
            }
            return (Class<? extends A>) classList.get(0);

        }


        public Tester() {
            Map<Class<? extends A>, A> map = new HashMap<Class<? extends A>,A>() {
                @Override
                public A put(Class<? extends A> key, A value) {
                    // Whenever anyone puts a value in, actually put it in for whatever interface it implements that derives from A
                    Class<? extends A> newKey = getHierarchyClass(key);
                    if ( newKey != null ) {
                        return super.put(newKey, value);
                    }
                    return super.put(key, value);
                }
            };

            A d = new D();
            A e = new E();

            map.put(d.getClass(), d);
            map.put(e.getClass(), e);

            System.out.println(B.class.getSimpleName() + ": " + map.get(B.class));
            System.out.println(C.class.getSimpleName() + ": " + map.get(C.class));
        }



        public static void main(String[] args) {
            new Tester();
        }

}

Output:

B: com.matt.tester.Tester$D@404b9385
C: com.matt.tester.Tester$E@6d311334

Solution 4:[4]

For sure, the following is not an elegant solution but it works considering your "simplistic" type hierarchy.

What it does is populating an entry in the map for each class in the class hierarchy of the values' classes. Then, you can get values from the map using any interface or class that the values implement.

If several values implements the same class/interface, entries get overwritten but your design already has this kind of limitation.

public ClassTest()
{
    final Map<Class<? extends A>, A> map = new HashMap<>();

    final A d = new D();
    final A e = new E();

    getSuperClassesAndInterfaces(A.class, d.getClass()).forEach(clazz -> map.put(clazz, d));
    getSuperClassesAndInterfaces(A.class, e.getClass()).forEach(clazz -> map.put(clazz, e));

    System.out.println(B.class.getSimpleName() + ": " + map.get(B.class));
    System.out.println(C.class.getSimpleName() + ": " + map.get(C.class));
    System.out.println(B.class.getSimpleName() + ": " + map.get(D.class));
    System.out.println(C.class.getSimpleName() + ": " + map.get(E.class));
}

/**
 * Gets all the super classes and interfaces of first argument that are subclasses of the second argument.
 * 
 * @param <T> Type of the limit class.
 * @param aClass Any class.
 * @param limit We do not want anything higher in the hierarchy than this class.
 * @return A collection of classes and interfaces that 'aClass' implements and that are subclasses of 'limit'.
 */
// Did not find any elegant way to achieve this, but it works.
public <T> Collection<Class<? extends T>> getSuperClassesAndInterfaces(final Class<T> aClass, final Class<? extends A> limit)
{
    final Collection<Class<? extends T>> result = new HashSet<>();
    // Classes
    Class<?> rawClass = limit;
    while (rawClass != null && aClass.isAssignableFrom(rawClass))
    {
        result.add((Class<T>) rawClass);
        rawClass = rawClass.getSuperclass();
    }
    // Interfaces
    final Class<?>[] interfaces = limit.getInterfaces();
    for (final Class<?> itf : interfaces)
    {
        if (aClass.isAssignableFrom(itf))
        {
            result.add((Class<T>) itf);
        }
    }
    return result;
}

Output :

B: tests.ClassTest$D@41629346
C: tests.ClassTest$E@404b9385
B: tests.ClassTest$D@41629346
C: tests.ClassTest$E@404b9385

Frankly, this code is awful. Do you really need such a feature ?

Solution 5:[5]

What i understood from the information given here is that you want to fetch all the subclasses of the key class, for example if we write map.get(B.class) then all of the B's implementation classes (or child classes) must be given. If we keep things simple, there is one way to keep a collection and while putting into a map, decide which key class it will be added to.

Here is the code example

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ClassTest {

    interface A {}
    interface B extends A {}
    interface C extends A {}

    class D implements B {
       @Override
       public String toString(){
           return "D";
       }
    }
    class E implements C {
       @Override
       public String toString(){
           return "E";
       }
    }

    public ClassTest() {
        Map<Class<? extends A>, List<A>> map = new HashMap();
        map.computeIfAbsent(B.class, s-> new ArrayList());
        map.computeIfAbsent(C.class, s-> new ArrayList());
    
        A d = new D();
        A e = new E();
    
        map.get(B.class).add(d);
        map.get(C.class).add(e);

        map.get(B.class).stream().forEach(System.out :: println);
        map.get(C.class).stream().forEach(System.out :: println);
    }

    public static void main(String[] args) {
        new ClassTest();
    }

}

Output:

D
E

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Vlad Bochenin
Solution 2 Robert The Rebuilder
Solution 3
Solution 4 Emmanuel Guiton
Solution 5 Yogesh Sharma