Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare generic multitype collection of generic handlers

Tags:

java

generics

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?

like image 298
Antoniossss Avatar asked May 22 '15 08:05

Antoniossss


2 Answers

There is a nice image: PECS 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.

like image 159
Adam Arold Avatar answered Nov 18 '22 21:11

Adam Arold


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 (i.e. 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>.

like image 30
OldCurmudgeon Avatar answered Nov 18 '22 22:11

OldCurmudgeon