javacavaj
javacavaj

Reputation: 2961

Java Generics Wildcard Capture Warning

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

Answers (2)

newacct
newacct

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 Handlers of Marker; rather, you have Handlers of particular types of Marker. So it makes sense that you only use Pairs 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

flakes
flakes

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

Related Questions