javagenericscastingtype-erasure

How to pass an object into a method that accepts a generic type


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 ].

Problem

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);

Possible Solution

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

Question

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
         */
    }

}

Solution

  • 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);