Issue
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...
.
Solution
TL/DR:
There are three ways to remove the type parameters from the type of the reference variable:
- Use
var
. This is simply a syntactic shorthand, andvar palette = new DiscreteColorMap();
is identical at both runtime and compile time toDiscreteColorMap palette = new DiscreteColorMap();
. This is covered in another answer. - 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 typeT
, because the compiler can't check the type is correct. This is covered in detail below. - (Don't do this.) Use raw types:
ColorMap palette = new DiscreteColorMap();
. This tells the compiler to ignore the types of the parameters (it effectively treatsT
asObject
). 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 (i.e. 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>
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 (i.e. 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>
. 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 (i.e. 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>
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
).
Answered By - James_D
Answer Checked By - David Marino (JavaFixing Volunteer)