简体   繁体   中英

How to declare generic multitype collection of generic handlers

I always have hard time using generics with collections and wildcards.

So here is the following map. I want to keep collection of handlers for a specific type of packet class.

private ConcurrentHashMap<Class<? extends Packet>, List<PacketListener<? extends Packet>>> listeners = new ConcurrentHashMap<>();

And the PacketListener

public interface PacketListener<T extends Packet> {

    public void onOutgoingPacket(Streamer streamer, T packet);

    public void onIncomingPacket(Streamer streamer, T packet);
}

now what I would like to do is to get listeners depending on incoming packet class like this:

public <T extends Packet> void addPacketListener(Class<T> clazz, PacketListener<T> listener) {
    if (listeners.containsKey(clazz) == false) {
        listeners.putIfAbsent(clazz, new LinkedList<PacketListener<T>>());  // ERROR
    }
    List<PacketListener<? extends Packet>> list = listeners.get(clazz);
    list.add(listener);
}

public <T extends Packet> List<PacketListener<T>> getPacketListeners(Class<T> clazz) {
    List<PacketListener<T>> list = listeners.get(clazz);// ERROR
    if (list == null || list.isEmpty()) {
        return null;
    } else {
        return new ArrayList<>(list);
    }
}

And finally I would like to perform such invocation

private <T extends Packet> void notifyListeners(T packet) {
    List<PacketListener<T>> listeners = streamer.getPacketListeners(packet.getClass());
    if (listeners != null) {
        for (PacketListener<? extends Packet> packetListener : listeners) {
            packetListener.onIncomingPacket(streamer, packet);
        }
    }
}

All I am getting are just lot of errors. Is it because of wildcards in collection declaration? Is it possible to achieve such solution?

There is a nice image: 佩奇 In one of the other answers which can explain you this problem.

The thing is called PECS which stands for

Producer extends and Consumer super .

TL;DR: you can only both add and get from/to a collection with a concrete type ( T ). You can get any T (and its possible subtypes) with T extends Something and you can add any Something to a Collection with T super Something but you can't go both ways: thus your errors.

Your issue starts here:

private ConcurrentHashMap<Class<? extends Packet>, List<PacketListener<? extends Packet>>> listeners = new ConcurrentHashMap<>();

You are expecting (or perhaps just hoping) for a way to bind the two ? together so that a lookup with a key of type Class<T> will result in a value of type List<PacketListener<T>> . Sadly there is no way to tell Java that the two ? are the same but can take different (but constrained) types.

This issue is usually solved using the covariance/contravariance methods mentioned elsewhere but in your case you need to both write and read from your collection. You therefore must use an invariance .

I believe a solution to your problem is to bind the two objects into one helper class and therefore introduce the invariance there. This way you can maintain their equality while still letting them vary under restrictions.

Some of this is a little hacky IMHO (ie there are some casts) but at least you can achieve your aim and you are still type safe. The casts are provably valid.

public interface PacketListener<T extends Packet> {

    public void onOutgoingPacket(Streamer streamer, T packet);

    public void onIncomingPacket(Streamer streamer, T packet);
}

/**
 * Binds the T's of Class<T> and PacketListener<T> so that we CAN assume they are the same type.
 *
 * @param <T> The type of Packet we listen to.
 */
private static class Listeners<T extends Packet> {

    final Class<T> packetClass;
    final List<PacketListener<T>> listenerList = new LinkedList<>();

    public Listeners(Class<T> packetClass) {
        this.packetClass = packetClass;
    }

    public List<PacketListener<T>> getListenerList() {
        return listenerList;
    }

    private void addListener(PacketListener<T> listener) {
        listenerList.add(listener);
    }

}
/**
 * Now we have bound the T of Class<T> and List<PacketListener<T>> by using the Listeners class we do not need to key on the Class<T>, we just need to key on Class<?>.
 */
private final ConcurrentMap<Class<?>, Listeners<?>> allListeners = new ConcurrentHashMap<>();

public <T extends Packet> List<PacketListener<T>> getPacketListeners(Class<T> clazz) {
    // Now we can confidently cast it.
    Listeners<T> listeners = (Listeners<T>) allListeners.get(clazz);
    if (listeners != null) {
        // Return a copy of the list so they cannot change it.
        return new ArrayList<>(listeners.getListenerList());
    } else {
        return Collections.EMPTY_LIST;
    }
}

public <T extends Packet> void addPacketListener(Class<T> clazz, PacketListener<T> listener) {
    // Now we can confidently cast it.
    Listeners<T> listeners = (Listeners<T>) allListeners.get(clazz);
    if (listeners == null) {
        // Make one.
        Listeners<T> newListeners = new Listeners<>();
        if ((listeners = (Listeners<T>) allListeners.putIfAbsent(clazz, newListeners)) == null) {
            // It was added - use that one.
            listeners = newListeners;
        }
    }
    // Add the listener.
    listeners.addListener(listener);
}

Note that although it is generally assumed that if you need to cast something while using generics you are doing something wrong - in this case we can be safe because of the run-time assurance that all Listeners<T> objects in the map are keyed by their Class<T> and therefore the enclosed list is indeed a List<PacketListener<T> .

The following is some kind of similiar to the answer of @OldCurmudgeon.

The keypoint is also the listeners field. But I declare it as this:

private final Map<Class<?>, DelegatingPacketListener> listeners

The point here is that we get rid of the list as the map value type. DelegatingPacketListener is declared as follows:

public class DelegatingPacketListener implements PacketListener<Packet> {

    private final List<PacketListener<Packet>> packetListeners;

    public DelegatingPacketListener(List<? extends PacketListener<Packet>> packetListeners) {
        super();
        this.packetListeners = new ArrayList<PacketListener<Packet>>(packetListeners);
    }

    @Override
    public void onOutgoingPacket(Streamer streamer, Packet packet) {
        for(PacketListener<Packet> packetListener : packetListeners) {
            packetListener.onOutgoingPacket(streamer, packet);
        }
    }

    @Override
    public void onIncomingPacket(Streamer streamer, Packet packet) {
        for(PacketListener<Packet> packetListener : packetListeners) {
            packetListener.onIncomingPacket(streamer, packet);
        }
    }

    public List<PacketListener<Packet>> getPacketListeners() {
        return Collections.unmodifiableList(packetListeners);
    }
}

Now that DelegatingPacketListener only supports listeners of type Packet we need one more specific implementation of PacketListener :

public class WrappingPacketListener<T extends Packet> implements PacketListener<Packet> {

    private final Class<T> packetClass;
    private final PacketListener<T> wrapped;

    public WrappingPacketListener(Class<T> packetClass, PacketListener<T> delegate) {
        super();
        this.packetClass = packetClass;
        this.wrapped = delegate;
    }

    @Override
    public void onOutgoingPacket(Streamer streamer, Packet packet) {
        if(packetClass.isInstance(packet)) {
            T genericPacket = packetClass.cast(packet);
                wrapped.onOutgoingPacket(streamer, genericPacket);
        }
    }

    @Override
    public void onIncomingPacket(Streamer streamer, Packet packet) {
        if(packetClass.isInstance(packet)) {
            T genericPacket = packetClass.cast(packet);
                wrapped.onIncomingPacket(streamer, genericPacket);
        }
    }
}

Please note that the type parameter T is not used in the implements clause. It is only for the implementation used. We will wrap every PacketListener passed to the API in a WrappingPacketListener . So the implementation is like this:

public List<PacketListener<Packet>> getPacketListeners(Class<?> clazz) {
    return Collections.<PacketListener<Packet>>singletonList(listeners.get(clazz));
}

public <T extends Packet> void addPacketListener(Class<T> clazz, PacketListener<T> listener) {
    if (listeners.containsKey(clazz) == false) {
        listeners.put(clazz, new DelegatingPacketListener(Collections.singletonList(new WrappingPacketListener<T>(clazz, listener))));
        return;
    }
    DelegatingPacketListener existing = listeners.get(clazz);
    List<PacketListener<Packet>> newListeners = new ArrayList<PacketListener<Packet>>(existing.getPacketListeners());
    newListeners.add(new WrappingPacketListener<T>(clazz, listener));
    listeners.put(clazz, new DelegatingPacketListener(newListeners));        
}

private <T extends Packet> void notifyListeners(T packet) {
    List<PacketListener<Packet>> listeners = streamer.getPacketListeners(packet.getClass());
    if (listeners != null) {
        for (PacketListener<Packet> packetListener : listeners) {
            packetListener.onIncomingPacket(streamer, packet);
        }
    }
}

The API has slightly changed for getPacketListeners which doesnt use a generic type anymore.

In comparison with OldCurmudgeon's solution, this one sticks with the already existing PacketListener interface and doesn't require an unchecked cast to be applied.

Note that the implementation is not thread safe, because of the implemention of addPacketListener needs synchronization on the map key (as the original code in question does need too). However encapsulating the list of packet listeners in immutable DelegatingPacketListener is probably better suited for concurrency purposes.

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