Using generics and Class of an object in map

Viewed 102

I have following class/interface structure (can't modify the source):

public interface Car {}

public class CarA implements Car{
    public String getASpecifics() {...}
}

public class CarB implements Car{
    public String getBSpecifics() {...}
}

public class Summary {...}

I want to have a generic way of creating Summray for concrete implementations of Car interface which will be open to adding new implementations. My approach is following:

public CarSummarizer {
    public interface SummaryGenerator<T extends Car> {
        Summary generateSummary(T car);
    }

    static {
        SummaryGenerator<CarA> aGen = c -> {... c.getASpecifics(); ...}
        SummaryGenerator<CarB> bGen = c -> {... c.getBSpecifics(); ...}
    }
}

Now I'd like to store aGen and bgen in a Map. I want to parametrize it so that I can offer one only public static method which accepts Car car and based on it's class object (car.getClass()) uses correct SummaryGenerator implemenation. That should look something like following:

public static Summary getSummaryForCar(Car car) {
     return map.get(car.getClass()).generateSummary(car);
}

I don't know how to declare and instantiate that Map so that it's fully type-safe (i.e. doesn't allow inserting pair (CarC.class, SummaryGenerator<CarD>)). I'd like something like this:

public static <T extends Car> Map<Class<T>, SummaryGenerator<T>> map = new LinkedHashMap<>();

static {
    // after instantiation
    map.put(CarA.class, aGen);
    map.put(CarB.class, BGen);
}

// also support following
public static <T extends Car> void addSummaryGenerator(T car, SummaryGenerator<T> sg) {
    map.put(car.getClass(), sg);
}

That doesn't work because generics can't be declared on variables like they can be on functions. I guess I could define new class public class SummarizerStorage<T extends Car> and the place map inside and just delegate calls. That seems like an overkill and ugly. I feel like it should be done somehow directly.

Declaring map like Map<Class<? extends Car>, SummaryGenerator<? extends Car>> would allow paring of Class<> and SummaryGenerator<> of sibling types. I want to allow only same type pairs.

1 Answers

You can do this with compile-time safety if you wrap your map and only allow to put entries of which the value SummaryGenerator<T> matches a key of Class<T>. To get the SummaryGenerator you need to cast but since you ensured that you only added entries that actually can be cast to SummaryGenerator<T>, this is safe to do. If you try to add an incompatible SummaryGenerator for a given implementation of Car, it would result in a compile error.

public class SummaryGeneratorStorage {

    private static final Map<Class<? extends Car>, CarSummarizer.SummaryGenerator<? extends Car>> map = new HashMap<>();

    // provides type safety to only add a SummaryGenerator<T> for a key of Class<T>
    public static <T extends Car> void add(Class<T> clazz, CarSummarizer.SummaryGenerator<T> sg) {
        map.put(clazz, sg);
    }

    @SuppressWarnings("unchecked")
    public static <T extends Car> CarSummarizer.SummaryGenerator<T> get(T car) {
        // this cast is safe since the add-method only
        // allows a SummaryGenerator<T> to be added for a key of Class<T>
        return (CarSummarizer.SummaryGenerator<T>) map.get(car.getClass());
    }

    private SummaryGeneratorStorage() { }
}

In order to get a Summary, you retrieve the registered SummaryGenerator for the Car and call the implemented generateSummary method. If no SummaryGenerator is found for the passed implementation of Car, just throw an exception or handle it a different way. Note that the signature of add takes a Class and not a Car object since we don't need an instance at this point to make the reference to an implementation of Car.

public class CarSummarizer {

    public interface SummaryGenerator<T extends Car> {
        Summary generateSummary(T car);
    }

    static {
        // here you cannot pass incompatible implementations of Car (compile-time safety)
        SummaryGenerator<CarA> aGen = c -> new Summary(c.getASpecifics());
        SummaryGeneratorStorage.add(CarA.class, aGen); // with variable
        SummaryGeneratorStorage.add(CarB.class, c -> new Summary(c.getBSpecifics())); // direct
    }

    public static <T extends Car> Summary getSummary(T car) {
        SummaryGenerator<T> generator = SummaryGeneratorStorage.get(car);
        if (generator != null) {
            return generator.generateSummary(car);
        } else {
            throw new IllegalArgumentException("no summary generator found");
        }
    }
}

Here is a test class for the above code. A simple Summary containing a name-field was used and the implementations of Car return A, B or C in their getXSpecifics() methods:

public class CarSummarizerTest {

    @Test
    public void testCarA() {
        CarA car = new CarA();
        Summary summary = CarSummarizer.getSummary(car);
        Assert.assertEquals(car.getASpecifics(), summary.getName());
    }

    @Test
    public void testCarB() {
        CarB car = new CarB();
        Summary summary = CarSummarizer.getSummary(car);
        Assert.assertEquals(car.getBSpecifics(), summary.getName());
    }

    @Test
    public void testCarC() {
        CarC car = new CarC();
        Assert.assertThrows(IllegalArgumentException.class, () -> {
            CarSummarizer.getSummary(car);
        });
    }
}

For completeness the other classes:

public class CarA implements Car {
    public String getASpecifics() { return "A"; }
}

public class CarB implements Car {
    public String getBSpecifics() { return "B"; }
}

public class CarC implements Car {
    public String getCSpecifics() { return "C"; }
}

public class Summary {
    private final String name;
    public Summary(String name) { this.name = name; }
    public String getName() { return name; }
}
Related