Simple Message Handler in Java with generics


I can easily put together a message handling set of generic apis in C# with the following quick and dirty code:

void Main()
{
    var messageClient = new MessageClient(new TestMessageHandler(), new AnotherTestMessageHandler());

    messageClient.Send(new TestMessage { Information = "Hello TestMessageHandler" });
    messageClient.Send(new AnotherTestMessage { Information = "Hello AnotherTestMessageHandler" });
}


public interface IMessageHandler<TMessage>
{
    void Handle(TMessage message);
}

public class TestMessage
{
    public string Information { get; set; }
}

public class TestMessageHandler : IMessageHandler<TestMessage>
{
    public void Handle(TestMessage message) => 
        Console.WriteLine($"I have handled {message.GetType()} and message is '{message.Information}'");
}

public class AnotherTestMessage
{
    public string Information { get; set; }
}

public class AnotherTestMessageHandler : IMessageHandler<AnotherTestMessage>
{
    public void Handle(AnotherTestMessage message) =>
        Console.WriteLine($"I have handled {message.GetType()} and message is '{message.Information}'");
}

public class MessageClient
{
    private object() receivers;
    
    public MessageClient(params object() receivers)
    {
        this.receivers = receivers;
    }
    
    public void Send<TMessage>(TMessage message)
    {
        var messageReceivers = receivers.OfType<IMessageHandler<TMessage>>();
        
        foreach (var messageReceiver in messageReceivers)
        {
            messageReceiver.Handle(message);
        }
    }
}

The purpose being to decouple the handling from the message and also the handling from the message initiators.

Question

What are the best ways to do this in Java without having to fight against type erasure? I have spent last few days reading around generics in java, and still have a lot to learn.

Thoughts

I have considered the following implementation which is based around a Map, however the compiler throws up a warning:

Warning:(67, 53) Unchecked cast: 'thoughts.dictionarythought.Handles<capture<?>>' to 'thoughts.dictionarythought.Handles<TMessage>'

package thoughts;

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class dictionarythought {

    @Test
    public void testReceive() {
        var message = new MyMessage();

        var registrations = new MessageHandlerRegistrations();
        var client = new MessageClient(registrations);

        MyMessageHandler handler = new MyMessageHandler();
        registrations.register(MyMessage.class, handler);


        client.send(message);
        client.send(new MyMessage2());

        Assert.assertEquals(message, handler.lastReceived);
    }



    public class MyMessage {

    }

    public class MyMessage2 {

    }

    public interface Handles<TMessage> {
        void handle(TMessage message);
    }

    public class MyMessageHandler implements Handles<MyMessage> {
        public MyMessage lastReceived;

        @Override
        public void handle(MyMessage message) {
            System.out.println("Received");
            lastReceived = message;
        }
    }

    public class MessageClient {

        private final MessageHandlerRegistrations registrations;

        MessageClient(MessageHandlerRegistrations registrations) {
            this.registrations = registrations;
        }

        public <TMessage> void send(TMessage message) {

            List<Handles<?>> registeredHandlers = registrations.getHandlersRegisteredForMessage(message);

            for (Handles<?> handler : registeredHandlers) {
                // compiler warning here
                Handles<TMessage> concreteHandler = (Handles<TMessage>)handler;
                concreteHandler.handle(message);
            }
        }
    }

    public class MessageHandlerRegistrations {
        Map<Class<?>, List<Handles<?>>> handlers;

        MessageHandlerRegistrations() {
            handlers = new HashMap<>();
        }

        public void register(Class<?> messageType, Handles<?> handler) {
            List<Handles<?>> messageHandlers = handlers.getOrDefault(messageType, null);

            if (messageHandlers == null) {
                messageHandlers = new ArrayList<>();
                handlers.put(messageType, messageHandlers);
            }

            messageHandlers.add(handler);
        }

        public <TMessage> List<Handles<?>> getHandlersRegisteredForMessage(TMessage message) {
            List<Handles<?>> registeredHandlers = handlers.get(message.getClass());

            if (registeredHandlers == null) {
                registeredHandlers = new ArrayList<>();
            }

            return registeredHandlers;
        }
    }
}