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 Consumersuper
.
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 (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>
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With