Reputation: 2961
The SCCE below shows 2 classes (B and C) implementing the interface Marker. For each class that implements Marker there is a corresponding class implementing the generic Handler interface (B_Handler, C_Handler). A map is used to associate the Class type of Pair.second to it's associated Handler. The code executes as anticipated; however, I get a compile-time warning:
warning: [unchecked] unchecked cast Handler h1 = (Handler) (dispatch.get(p1.second.getClass())); required: Handler found: Handler where CAP#1 is a fresh type-variable: CAP#1 extends Marker from capture of ? extends Marker
What's the cleanest way to resolve this besides @SuppressWarnings(value = "unchecked")?
package genericpair;
import java.util.HashMap;
import java.util.Map;
import javax.swing.SwingUtilities;
public class GenericPair
{
public class A
{
}
public interface Marker
{
}
public class B implements Marker
{
}
public class C implements Marker
{
}
public Pair<A, Marker> getTarget()
{
A a = new A();
C c = new C();
return new Pair<>(a, c);
}
public interface Handler<T extends Marker>
{
void handle(Pair<A, T> target);
}
public class B_Handler implements Handler<B>
{
@Override
public void handle(Pair<A, B> target)
{
System.out.println("B");
}
}
public class C_Handler implements Handler<C>
{
@Override
public void handle(Pair<A, C> target)
{
System.out.println("C");
}
}
public class Pair<F, S>
{
public final F first;
public final S second;
public Pair(F first, S second)
{
this.first = first;
this.second = second;
}
}
private void executeSCCE()
{
// register a handler for each Marker type
Map<Class, Handler<? extends Marker>> dispatch = new HashMap<>();
dispatch.put(B.class, new B_Handler());
dispatch.put(C.class, new C_Handler());
// get a target (e.g., Pair<A,C>)
Pair<A, Marker> p1 = getTarget();
// select handler based on the class type of the second parameter
Handler<Marker> h1 = (Handler<Marker>) (dispatch.get(p1.second.getClass()));
h1.handle(p1);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(() -> new GenericPair().executeSCCE());
}
}
Upvotes: 3
Views: 1304
Reputation: 122429
There are several issues.
The first is that your Map
is not able to express the type relationship between each key and its value. So if you pass a Class<T>
to dispatch.get()
, you only get a Handler<? extends Marker>
back, not Handler<T>
. In fact, there is no type you can give dispatch
to make that work. Instead, you have to make a wrapper class to enforce this relationship via its API:
public class ClassToHandlerMap
{
private final Map<Class<?>, Handler<?>> map = new HashMap<>();
public <T extends Marker> void put(Class<T> clazz, Handler<T> handler) {
map.put(clazz, handler);
}
@SuppressWarnings("unchecked")
public <T extends Marker> Handler<T> get(Class<T> clazz) {
return (Handler<T>)map.get(clazz);
}
}
Note that you do still have to suppress unchecked warnings inside this class, but at least here you know it's provably correct, based on how things are allowed to be put into the map. The unchecked cast is just an implementation detail that the user of this class doesn't need to know about.
The second issue is that getTarget()
should probably return Pair<A, ? extends Marker>
instead of Pair<A, Marker>
. You don't ever have a Handler
s of Marker
; rather, you have Handler
s of particular types of Marker
. So it makes sense that you only use Pair
s of particular types of Marker
too.
public Pair<A, ? extends Marker> getTarget()
{
A a = new A();
C c = new C();
return new Pair<>(a, c);
}
The last part of your function basically is using p1
to operate on itself, so we need to use a capture helper to "capture" the ?
in the type of p1
into a useful type variable for what we need to do.
However, this is more complicated in this case, because you are using .getClass()
. foo.getClass()
has the type Class<? extends |X|>
where |X|
is the erasure of the compile-time type of foo
. So no matter if p1
had the type Pair<A, ?>
or Pair<A, T>
, p1.second.getClass()
would still return the type Class<? extends Marker>
. So capturing on the ?
in Pair<A, ?>
is not enough; instead, we should capture on the ?
in the return of .getClass()
:
@SuppressWarnings("unchecked")
private static <T extends Marker> void captureHelper(Class<T> clazz,
Pair<A, ? extends Marker> p, ClassToHandlerMap dispatch) {
Pair<A, T> p1 = (Pair<A, T>)p;
Handler<T> h1 = dispatch.get(clazz);
h1.handle(p1);
}
Unfortunately, we will have to do an unchecked cast here also. Due to the peculiar return type of .getClass()
we are unable to connect the types of the return of .getClass()
and the expression it is called on. And we can't use runtime casting like .cast()
to cast between parameterized types (we could use .cast()
to get rid of unchecked casts if we were taking an instance of the given class as an argument, but not here). There may be some edge cases in which this is incorrect, but as long as you always use Pair
with the second type argument being a final implementing class, it should be correct.
And finally the primary method looks like this:
private void executeSCCE()
{
// register a handler for each Marker type
ClassToHandlerMap dispatch = new ClassToHandlerMap();
dispatch.put(B.class, new B_Handler());
dispatch.put(C.class, new C_Handler());
// get a target (e.g., Pair<A,C>)
Pair<A, ? extends Marker> p1 = getTarget();
// select handler based on the class type of the second parameter
captureHelper(p1.second.getClass(), p1, dispatch);
}
Upvotes: 1
Reputation: 23614
Consider the following example:
List<? extends List> test1 = new ArrayList<>();
List<List> test2 = (List<List>) test1;
Here we get the warning:
warning: [unchecked] unchecked cast
List<List> test2 = (List<List>) test1;
^
required: List<List>
found: List<CAP#1>
where CAP#1 is a fresh type-variable:
CAP#1 extends List from capture of ? extends List
This happens because there is no way to ensure that the generic constraint of List<List>
will match List<? extends List>
. Imagine that we rewrite this example to the following:
List<? extends List> test1 = new ArrayList<ArrayList>();
List<List> test2 = (List<List>) test1;
test1.add(new LinkedList<>());//ERROR no suitable method found for add(LinkedList<Object>)
test2.add(new LinkedList<>());//Will work fine!!
Here it is more obvious that the initial contract is broken. The list defined to contain ArrayList
now contains a LinkedList
. This is unsafe, and is why you are getting this warning. So there is no way to cast from Handler<? extends Marker>
to Handler<Marker>
safely.
Upvotes: 5