简体   繁体   中英

Java Generics Wildcards Question

I'm facing some problems with Generics when using Google Guava's excellent Multimap. I have a type Handler defined as such

public interface Handler<T extends Serializable> {
    void handle(T t);
} 

In another class I've defined a multimap that maps a String to a collection of Handlers.

private Multimap<String, Handler<? extends Serializable>> multimap = 
    ArrayListMultimap.create();

Now when I try to do stuff with the multimap, I'm getting compiler errors. My first attempt looked like this:

public <T extends Serializable> void doStuff1(String s, T t)  {
    Collection<Handler<T>> collection = multimap.get(s);
    for (Handler<T> handler : collection) {
        handler.handle(t);
    }
}

which resulted in the following error.

Type mismatch: cannot convert from Collection<Handler<? extends Serializable>> to Collection<Handler<T>>

Afterwards, I tried to code it like this

public void doStuff2(String s, Serializable serializable)  {
    Collection<Handler<? extends Serializable>> collection = multimap.get(s);
    for (Handler<? extends Serializable> handler : collection) {
        handler.handle(serializable); 
    }
}

which unfortunately failed as well:

The method handle(capture#1-of ? extends Serializable) in the type Handler<capture#1-of ? extends Serializable> is not applicable for the arguments (Serializable)

Any help would be greatly appreciated. Thanks.

Update:

The only way I have managed to fix this is by suppressing compiler warnings. Given the following handler:

public interface Handler<T extends Event> {
    void handle(T t);

    Class<T> getType();
}

I can write the event bus as such.

public class EventBus {

    private Multimap<Class<?>, Handler<?>> multimap = ArrayListMultimap.create();

    public <T extends Event> void subscribe(Handler<T> handler) {
        multimap.put(handler.getType(), handler);
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void publish(Event event)  {
        Collection<Handler<?>> collection = multimap.get(event.getClass());
        for (Handler handler : collection) {
            handler.handle(event);
        }
    }
}

I guess there's no way to handle this with less or even without @SuppressWarnings?

It will work better if you define:

private Multimap<String, Handler<Serializable>> multimap = 
    ArrayListMultimap.create();

Update: Explaination of your problem.

When you have something like ..

private Multimap<String, Handler<? extends Serializable>> multimap;

It means that multimap can accept ANY Handler<N> where N Extends Serializable . Let's consider that it will contain a value of type Handler<Foo> and a value of type Handler<Bar> . Foo and Bar are unrelated and do not extend from each other.

When in your function you want to use a type to represent the type of all the possible values of Handler<? extends Serializable> Handler<? extends Serializable> , you are trying to express a type which is at the same time Foo and Bar , but there is no such a type.

This explains your problem with the compiler. Now remove this "-1" and vote for my answer if you think I am correct.

The problem is that the types might be different:

private Multimap<String, Handler<? extends Serializable>> multimap = 
ArrayListMultimap.create();

wouldn't allow you to add anything to the multimap, since you don't know what ? actually stands for. You could for example have a Multimap<String, Handler<String>> and try to add an Integer because both implement Serializable .

Edit: Actually the above paragraph is slightly wrong. You should be able to add handlers to the multimap, but since the type parameters of the handlers are not known, you wouldn't be able to use the handlers, see below.

In your doStuff1 method you define a concrete parameter T which might be something completely different. Thus the compiler can't determine if this assignment would be correct: Collection<Handler<T>> collection = multimap.get(s); (is T really the type of the handler you get from the multimap? - The compiler doesn't know).

Your second approach does get the assignment right, however the handle() method won't work, since you pass in a Serializable which could be anything ( String , Integer , something else) and the compiler still doesn't know if the handler's type matches that (imagine it's a Handler<Number> and you pass a String to doStuff2 ).

You have several alternatives to fix that, each with it's own drawbacks:

  1. Just use Multimap<String, Handler<Serializable>> , which would allow you to pass any Serializable object to the handler
  2. Use a concrete type, eg Multimap<String, Handler<String>> , which would limit you to string handlers only
  3. Get the type parameter of the handler at runtime and cast, which might be error prone if you don't get it right

I modified your updated code a bit. Now it works without @SuppressWarnings.

I've made some assumptions, of course.

But I hope it will be helpful.

public interface Handler<T extends Event> {
    //void handle(T t);
    // It seems you won't need dependency of T in this method implementation.
    // So, you can use generic method here.
    <S extends Event> void handle(S s);

    Class<T> getType();
}

Modified EventBus.

public class EventBus {
    //private Multimap<Class<?>, Handler<?>> multimap = ArrayListMultimap.create();
    // It seems you don't need anything except Event here.
    // So. Logicaly more correct to use the bounded wildcard.
    private Multimap<Class<? extends Event>, Handler<? extends Event>> 
        multimap = ArrayListMultimap.create();

    //public <T extends Event> void subscribe(Handler<T> handler) {
    // You don't need to use generic method here.
    // Because T is never used in method implementation.
    // Wildcard fits simpler.
    public void subscribe(Handler<? extends Event> handler) {
        multimap.put(handler.getType(), handler);
    }

    //@SuppressWarnings({ "rawtypes", "unchecked" })
    public void publish(Event event)  {
        //Collection<Handler<?>> collection = multimap.get(event.getClass());
        // The bounded wildcard again.
        Collection<Handler<? extends Event>> 
            collection = multimap.get(event.getClass());
        //for (Handler handler : collection) {
        for (Handler<? extends Event> handler : collection) {
            handler.handle(event);
        }
    }
}

And some more code, just to complete the example.

public class Main {

    public static void main(String[] args) {
        EventBus bus = new EventBus();

        bus.subscribe(new Handler<MyEvent> () {

            public <S extends Event> void handle(S s) {
                System.out.println(s);
            }

            public Class<MyEvent> getType() {
                return MyEvent.class;
            }
        });

        bus.publish(new MyEvent());
    }
}

class MyEvent implements Event {

// Event implementation ...

}

The program output looks like this:

MyEvent@12276af2

Two part answer. Firts, your method "consumes" objects, so you can't use "extends"... you need to use "super" (PECS: Producer Extends, Consumer Super!).

Without changing your handler, this compiles with without warnings for me:

private Multimap<String, Handler<? super Serializable>> multimap = ArrayListMultimap.create();

public void doStuff1(String s, Serializable t) {
    Collection<Handler<? super Serializable>> collection = multimap.get(s);
    for (Handler<? super Serializable> handler : collection) {
        handler.handle(t);
    }
}

This way you define a multimap from string to a handler that consume at least Serializable.

Second, I have often used something similar to your construct:

Map<Class<?>, Handler<?>> 

and is where the handler returned is a consumer of Class. The main problem is that you have no way to "add to the generic type" when you know more... If you need to, always declare a new variable can put the @SuppressWarning at the declaration:

@SuppressWarnings("unchecked") 
Handler<String> myHandler = (Handler<String>) getHandler();

This only work if you are casting the whole generic type. If you are given Handler and you know that what you have is really Handler>, the only way you can cast it is by going through the raw type:

Handler<List> myLessSpecifiedHandler = getHandler();
@SuppressWarnings("unchecked") 
Handler<List<String>> myHandler = (Handler<List<String>>) (Handler) myLessSpecifiedHandler;

If you don't, you get an error instead of a warning...

Yes, generics are kind of messy.. :-/

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