I have a class where listeners can listen for certain events.
To make it easier to call, this method is statically bound to the generic parameter T:
static public <T> void addListener(final Class<T> pTriggerOnClass, final JcIEventListener<T> pListener) {
so I can call
JcUEventQueue.addListener(String.class, e -> System.out.println("GOT INCOMING EVENT: " + e));
and have e
as type that I'm expecting back (in this case e is of Class String
). [ as opposed to receiving an Object
when not using type bounding ].
JcUEventQueue
for every possible type, (I want to keep using one the one central static Class)so inside JcUEventQueue
I work with Object as type.
When an event occurs, I have to notify the according listeners.
listener.receiveInternalMessage(pMessage);
But Java will not accept this, because the types are incompatible. pMessage
is of another type than required by the method receiveInternalMessage
. This is obvious, because of the follwing:
Now, internally, pMessage
is an Object:
static private ArrayList<Throwable> notifyListeners(final Class<?> pClazz, final Object pMessage) {
And even if I defined the event-raising with static type parameter T pMessage
:
static private <T> ArrayList<Throwable> notifyListeners(final Class<?> pClazz, final T pMessage) {
the T passed into the method would not match the T that the called method receiveInternalMessage
would accept (different definition sources).
I also cannot cast the variable pMessage
into anything useful in order to call
listener.receiveInternalMessage(pMessage);
So far, I have only found Reflection as a workable tool for this problem:
final String methodName = "receiveInternalMessage";
sListenerMethod = JcIEventListener.class.getDeclaredMethod(methodName, Object.class);
sListenerMethod.invoke(listener, pMessage); // use reflection to we can force an object into a T
Do you guys know of any other possible solutions to this problem, without creating and managing listst for each possible type?
Here's my full source code:
public interface JcIEventListener<T> {
void receiveInternalMessage(final T pMessage);
}
and
package jc.lib.observer.events;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import jc.lib.gui.window.dialog.JcUDialog;
public class JcUEventQueue {
static private final ConcurrentHashMap<Class<?>, List<JcIEventListener<?>>> sClass2Listeners = new ConcurrentHashMap<>();
static private Method sListenerMethod;
static {
final String methodName = "receiveInternalMessage";
try {
sListenerMethod = JcIEventListener.class.getDeclaredMethod(methodName, Object.class);
} catch (NoSuchMethodException | SecurityException e) {
System.err.println("Critical shutdown while initializing " + JcUEventQueue.class.getCanonicalName() + ": Method not found: " + methodName);
JcUDialog.showError(e);
System.exit(1);
}
}
static public <T> void addListener(final Class<T> pTriggerOnClass, final JcIEventListener<T> pListener) {
if (pTriggerOnClass == null) throw new IllegalArgumentException("Triggering Class cannot be null!");
if (pListener == null) throw new IllegalArgumentException("Listener cannot be null!");
// T gets lost here
final List<JcIEventListener<?>> list = sClass2Listeners.computeIfAbsent(pTriggerOnClass, p -> Collections.synchronizedList(new ArrayList<>()));
if (!list.contains(pListener)) list.add(pListener);
}
static public <T> void submitMessage(final T pMessage) {
Class<?> clazz = pMessage.getClass();
while (true) { // trigger on class and all of its super-classes
// I removed the code to also cover all interfaces, as this would bloat this question up
notifyListeners(clazz, pMessage);
clazz = clazz.getSuperclass();
if (clazz == null) break;
}
}
static private <T> ArrayList<Throwable> notifyListeners(final Class<?> pClazz, final T pMessage) {
if (pClazz == null) return null;
// if (pMessage == null) return null; // explicitly allow sending of null message Objects
final List<JcIEventListener<?>> listeners = sClass2Listeners.get(pClazz);
if (listeners == null || listeners.size() < 1) return null;
ArrayList<Throwable> errors = null;
for (final JcIEventListener<?> listener : listeners) {
try {
// Problem:
// listener.receiveInternalMessage(pMessage); // cannot cast Object into T; Do not have T here, as it's registered
// Solution:
sListenerMethod.invoke(listener, pMessage); // use reflection to we can force an object into a T
// The conversion Object -> T is type-safe, due to the inner workings of this class: only
// See static initializer above for getting mListenerMethod
} catch (final Throwable e) {
if (errors == null) errors = new ArrayList<>();
errors.add(e);
}
}
return errors;
}
}
and its use
package jc.lib.observer.events;
public class JcUEventQueue_Test {
public static void main(final String... args) {
/*
* the contract of method signature
* - static public <T> void addListener(final Class<T> pTriggerOnClass, final JcEventListener<T> pListener) { ... }
* is advantageous, because the 'e' parameter in the lambdas is already typed-bound to the Class<T> (first argument), and not just an Object
*/
JcUEventQueue.addListener(String.class, e -> System.out.println("GOT INCOMING EVENT: " + e));
JcUEventQueue.addListener(CharSequence.class, e -> System.out.println("GOT INCOMING EVENT on CS: " + e));
JcUEventQueue.addListener(Object.class, e -> System.out.println("GOT INCOMING EVENT on Object: " + e));
JcUEventQueue.submitMessage("Peter");
/*
* Will show only
*
* GOT INCOMING EVENT: Peter
* GOT INCOMING EVENT on Object: Peter
*
* because we left out the interface casts+calls for brevity
*/
}
}
You can cast the listener to the appropriate type and call the method directly inside notifyListeners
. Just replace reflection invoke:
// sListenerMethod.invoke(listener, pMessage);force an object into a T
JcIEventListener<T> l2 = (JcIEventListener<T>)listener;
l2.receiveInternalMessage(pMessage);