简体   繁体   中英

Reuniting similar classes with different templates

Context

I want to create color maps for my JavaFX application in order to display a grid with different colours according to their value. Two types are defined: DiscreteColorMap which uses integer keys and ContinuousColorMap with double keys. Both must implement the interface ColorMap , so that it can be called like that:

ColorMap palette1 = new DiscreteColorMap();
ColorMap palette2 = new ContinuousColorMap();

Problem

As both classes rely on the same interface, I specify a template ( public interface ColorMap<T> ) in order to adapt to each of them:

ColorMap<Integer> palette1 = new DiscreteColorMap();
ColorMap<Double> palette2 = new ContinuousColorMap();

I want the simplest syntax for color maps so I need to get rid of the <Integer> and <Double> strings. What is the most elegant way to do that?

Source

The complete code can be found in this GitHub project .

EDIT

My English is not perfect ^^ I used "to get rid of" but this is not clear: when I instantiate my color maps, I want to make <Integer> and <Double> disappear, so I could write ColorMap palette... instead of ColorMap<Integer> palette... .

TL/DR:

There are three ways to remove the type parameters from the type of the reference variable:

  1. Use var . This is simply a syntactic shorthand, and var palette = new DiscreteColorMap(); is identical at both runtime and compile time to DiscreteColorMap palette = new DiscreteColorMap();. This is covered in another answer.
  2. Use wildcards: ColorMap<?> palette = new DiscreteColorMap(); . This tells the compiler to "forget" the type being used as the parametrized type. This means you won't be able to invoke any methods expecting parameters of type T , because the compiler can't check the type is correct. This is covered in detail below.
  3. (Don't do this.) Use raw types: ColorMap palette = new DiscreteColorMap(); . This tells the compiler to ignore the types of the parameters (it effectively treats T as Object ). Any errors because of incompatible types are thrown at runtime and not caught at compile time, and for this reason this approach is strongly not recommended.

The rest of this answer describes in detail the second option, using wildcards.


The purpose of Java generics is to allow the flexibility to create classes which can work with any type of object (or a specific "range" of types of object), while preserving the ability of the compiler to perform compile-time type checking. The canonical example of this is the Collections API that is part of the java.util package.

In your case, you've defined a ColorMap interface which is generic, and I'm guessing from the name of the interface that you are mapping values of the parametrized type T to colors. So you probably have something like this:

public interface ColorMap<T> {
    public Color get(T value);
}

And then some implementations. I'm going to use very basic implementations which are not production-level, just to demonstrate the idea. There is one for values of Integer type:

public class DiscreteColorMap implements ColorMap<Integer> {

    private final Color[] colors ;

    public DiscreteColorMap(Color... colors) {
        this.colors = colors ;
    }

    @Override
    public Color get(Integer value) {
        return colors[value];
    }
}

and one of type Double :

public class ContinuousColorMap implements ColorMap<Double> {

    private final Color start ;
    private final Color end ;

    public ContinuousColorMap(Color start, Color end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Color get(Double value) {
        return start.interpolate(end, value);
    }
}

Note that as far as the compiler is concerned , ColorMap<Integer> and ColorMap<Double> are distinct types.

As far as I can tell, your question seems to be "Can I create a DiscreteColorMap and ContinuousColorMap and assign them to references of the same type". The answer is "yes", and you can do this in a non-trivial way (ie not just assigning them to Object references) using wildcards:

ColorMap<?> cm1 = new DiscreteColorMap(Color.RED, Color.GREEN, Color.BLUE);
ColorMap<?> cm2 = new ContinuousColorMap(Color.RED, Color.BLUE);

The reference type ColorMap<?> can be thought of as "A ColorMap of some specific, but unknown, type". (I think of ColorMap<Integer> as "A ColorMap of type Integer ", etc.)

You can also use bounded wild cards. Since both Integer and Double are subclasses of Number , you can specify that in the types:

ColorMap<? extends Number> cm1 = new DiscreteColorMap(Color.RED, Color.GREEN, Color.BLUE);
ColorMap<? extends Number> cm2 = new ContinuousColorMap(Color.RED, Color.BLUE);

The way to think of ColorMap<? extends Number> ColorMap<? extends Number> is "A ColorMap of some specific type that is Number or a subclass of Number ". Here Number is an upper bound for the parametrized type.

With the current interface and class definitions you have, this is the most specific way to "unite" the types of the two different color maps: they are both ColorMap s of some type that is a subclass of Number .

Whether or not any of this is useful depends on what you want to do with the ColorMap s. You can certainly do

List<ColorMap<? extends Number>> colorMaps = List.of(cm1, cm2);

The problem here is that the only method you have in ColorMap consumes values of the parametrized type (ie the get(...) method expects a parameter of type T ). Since the actual type for each ColorMap in our list is unknown (we only know it is some specific subclass of Number ), the compiler cannot infer that we are passing the correct value to any given instance of a ColorMap<? extends Number> ColorMap<? extends Number> . One of our instances specifically needs to be passed an Integer , the other specifically needs to be passed a Double . Since there's no value that can be both of these things, we can't write any code like:

Number value = 1;
for (ColorMap<? extends Number> cm : colorMaps) {
    // this won't compile, because cm expects some specific type of Number:
    Color c = cm.get(value);
}

This next part is a little artificial in this case, but if ColorMap had a method that produced (ie returns) values of type T , then this list might be useful. It's not clear how you would implement this, but if you added a method to the interface:

public interface ColorMap<T> {
    public Color get(T value);
    public T getValue(Color c);
}

Then you could do:

List<ColorMap<? extends Number>> colorMaps = List.of(cm1, cm2);
Color c = Color.BLUE;
for (ColorMap<? extends Number> cm : colorMaps) {
    Number value = cm.getValue(c);
}

This will compile. The complier is assured that each ColorMap in our list has a specific value of T that is Number or a subclass of Number . Therefore each getValue() method returns some kind of Number , and the assignment Number value = cm.getValue(c); is guaranteed to succeed.

If you change the definition of ContinuousColorMap a little, then there may be a nice way to use a common type without the artificial getValue() method:

public interface ColorMap<T> {
    public Color get(T value);
}
public class DiscreteColorMap implements ColorMap<Integer> {

    private final Color[] colors ;

    public DiscreteColorMap(Color... colors) {
        this.colors = colors ;
    }

    @Override
    public Color get(Integer value) {
        return colors[value];
    }
}

This time, make ContinuousColorMap a ColorMap<Number> :

public class ContinuousColorMap implements ColorMap<Number> {

    private final Color start ;
    private final Color end ;

    public ContinuousColorMap(Color start, Color end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Color get(Number value) {
        return start.interpolate(end, value.doubleValue());
    }
}

Now we can do

ColorMap<? super Integer> cm1 = new DiscreteColorMap(Color.RED, Color.GREEN, Color.BLUE);
ColorMap<? super Integer> cm2 = new ContinuousColorMap(Color.RED, Color.BLUE);
List<ColorMap<? super Integer>> colorMaps = List.of(cm1, cm2);

Here Integer is a lower bound for the parametrized type, and we can interpret ColorMap<? super Integer> ColorMap<? super Integer> as "A ColorMap of some specific type which is an Integer or a superclass of Integer ". Since Number is a superclass of integer, the assignment for cm2 compiles.

For every element in the list, the get(...) method expects some specific type, but we know that specific type must be either an Integer or a superclass of Integer . So if we pass in an Integer , that call is guaranteed to succeed. Consequently, we can do

Integer value = 1;
for (ColorMap<? super Integer> cm : colorMaps) {
    // this line will compile and retrieve the correct color when executed:
    Color c = cm.get(value);
}

This is probably now way beyond the scope of the question, but if you want you can even write a class that keeps track of ColorMap instances according to their type, and given a Number will return a color from the color map for the specific type of number provided. To do this, you use the Class<T> class as a "type token":

@SuppressWarnings("unchecked")
public class ColorMaps {

    private final Map<Class<? extends Number>, ColorMap<? extends Number>> colorMaps = new HashMap<>();

    public <N extends Number> void registerColorMap(Class<N> type, ColorMap<N> map) {
        colorMaps.put(type, map);
    }

    public <N extends Number> ColorMap<N> getColorMap(Class<N> type) {
        return (ColorMap<N>) colorMaps.get(type);
    }

    public <N extends Number> Color getColor(N n) {
        Class<N> type = (Class<N>) n.getClass();
        return getColorMap(type).get(n);
    }
}

And then you can do fun things like:

ColorMaps colorMaps = new ColorMaps();
colorMaps.registerColorMap(Integer.class, 
    new DiscreteColorMap(Color.RED, Color.GREEN, Color.BLUE));
colorMaps.registerColorMap(Double.class,
    new ContinuousColorMap(Color.RED, Color.BLUE));

List<Number> numbers = List.of(0, 0.5, 1, 1.0, 2);
for (Number n : numbers) {
    System.out.println(n.getClass());
    System.out.println(colorMaps.getColor(n));
}

This code will use the DiscreteColorMap to map the integers in the list ( 0 , 1 , and 2 ), and the ContinuousColorMap to map the doubles in the list ( 0.5 and 1.0 ).

If you want to get rid of variable type declaration, then you use var :

var palette1 = new DiscreteColorMap();
var palette2 = new ContinuousColorMap();

As openjdk says:

The role of var in a local variable declaration is to stand in for the type, so that the name and initializer stand out: var person = new Person(); The compiler infers the type of the local variable from the initializer. This is especially worthwhile if the type is parameterized with wildcards, or if the type is mentioned in the initializer. Using var can make code more concise without sacrificing readability, and in some cases it can improve readability by removing redundancy.

UPDATE:

Special thanks to jewelsea and Slaw :

  • var cannot be used for field declarations
  • var will infer a type of the subclass, not the superclass.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM