Austin Hyde
Austin Hyde

Reputation: 27436

Need some help using Java Generics

I am trying to make a system for responding to events that happen in my application, similar to the Observer pattern. In my system, EventProducers trigger events and EventConsumers respond to those events, and the two are connected through a central hub:

For the moment, I'm going to ignore EventProducer and focus on EventHub and EventConsumer:

interface EventConsumer<E extends Event> {
    void respondToEvent(E event);
}

class EventHub {
    private HashMap<Class</*event type*/>, HashSet<EventConsumer</*event type*/>>> subscriptions;
    public <E extends Event> void fireEvent(E event) {
        /* For every consumer in the set corresponding to the event type {
            consumer.respondToEvent(event);
        } */
    }
    public <E extends Event> void subscribeToEvent(EventConsumer<E> consumer) {
        /* Insert consumer into the set corresponding to E */
    }
}

The problem lies in the declaration of the HashMap: I want to be able to do something like

HashMap<Class<E extends Event>, HashSet<EventConsumer<E>>>
// or
<E extends Event> HashMap<Class<E>, HashSet<EventConsumer<E>>>

So that the EventConsumer is parameterized by the same type the Class is, but the closest I can get is

HashMap<Class<? extends Event>, HashSet<EventConsumer<? extends Event>>>

But then this would allow things like a HashSet<EventConsumer<MouseClickEvent>> being assigned to Class<KeyPressEvent>, assuming both KeyPressEvent and MouseClickEvent subclass Event.

A second problem is in subscribeToEvent: I need to be able to store the consumer in the correct set corresponding to its event, like in

subscriptions.get(E.class).put(consumer)

but I cannot get the class of E at run-time.

How can I solve these problems? Am I going about this the wrong way?

Upvotes: 5

Views: 156

Answers (5)

Ravi Bhatt
Ravi Bhatt

Reputation: 3163

Why don't you parameterise your EventHub class? any challanges?

interface EventConsumer<E extends Event> {
void respondToEvent(E event);
}

class EventHub<E extends Event> {
 private HashMap<Class<E>, HashSet<EventConsumer<E>>> subscriptions;

 public void fireEvent(E event) {
/*
 * For every consumer in the set corresponding to the event type {
 * consumer.respondToEvent(event); }
 */
  }

  public void subscribeToEvent(EventConsumer<E> consumer) {
  /* Insert consumer into the set corresponding to E */
}
}

And then use E in all your function signatures.

EDIT 1: Okay, since i understand your question more clearly, here we go:

your EventConsumer Class can keep an EventType(s) that it supports/handles. EvenType is an Enum. In your Map you store Consumers against a EventType.

Upvotes: 0

Michael
Michael

Reputation: 35341

You could remove the generics from the EventConsumer class. But you'd have to cast the Event object in each implementation of EventConsumer.

interface EventConsumer {
    void respondToEvent(Event event);
}

class ClickEventConsumer implements EventConsumer {
   public void respondToEvent(Event event){
     ClickEvent ce = (ClickEvent)event;
     //...
   }
}

class EventHub {
  private HashMap<Class<? extends Event>, HashSet<EventConsumer>> subscriptions;

  public void fireEvent(Event event) {
    HashSet<EventConsumer> consumers = subscriptions.get(event.getClass());
    if (consumers != null){
      for (EventConsumer ec : consumers){
        ec.respondToEvent(event);
      }
    }
  }

  public void subscribeToEvent(Class<? extends Event> clazz, EventConsumer consumer) {
    HashSet<EventConsumer> consumers = subscriptions.get(clazz);
    if (consumers == null){
      consumers = new HashSet<EventConsumer>();
      subscriptions.put(clazz, consumers);
    }
    consumers.add(consumer);
  }
}

Upvotes: 1

alf
alf

Reputation: 8513

Well, starting from the end:

First, you can actually obtain generic type parameter type at runtime. It's just that you can only do it in a special case:

static class A<E extends EventObject> {
}

static class B extends A<MouseEvent> {
}

public static void main(String[] args) {
    System.out.println(B.class.getGenericSuperclass());
}

Note B is non-generic, but is inherited from a generic parent.

If the superclass is a parameterized type, the Type object returned must accurately reflect the actual type parameters used in the source code.

Whether you can put it in any use is a different question. I didn't try. Most of the code I saw explicitly pass a Class instance as a workaround.

Second, the Map here cannot (well, as far as I know) be fixed with generics. You can implement a typesafe map of course, but I think there's one more interesting thing to consider: when you fire an event, you probably want to send it to all who subscribed to this particular event class and all who subscribed to its parents. So just event.getClass() would be just a little bit less than enough.

Upvotes: 0

krakover
krakover

Reputation: 3029

What you can do is to wrap the Map with it's own parameterized class. given the parameter to the class - you can use it in the map. something like that:

public class EventsMap<E extends Event> {
    HashMap<Class<E>, HashSet<E>> map;
}

As for subscribing - I'll use ty1824's answer..

Upvotes: 2

Gandalf
Gandalf

Reputation: 2348

As for the Map, I'd leave it as follows:

HashMap<Class<? extends Event>, Set<EventConsumer<? extends Event>>> subscriptions;

And then use parameterized accessor methods like:

<E extends Event> void addSubscription(Class<E> eventClass, EventConsumer<? super E> eventConsumer)

<E extends Event> Set<EventConsumer<? super E>> getSubscriptions(Class<E> eventClass)

As you already pointed out you cannot obtain the event class at runtime, so you'll need to have it provided by your API users as for example with the method signature of addSubscription provided above.

Upvotes: 1

Related Questions