简体   繁体   中英

Correct use of generics in Java

I'm trying to set my custom event management system - just for learning.

I have created a structure of:

  • an Event - which is the simplest component that will carry any information
  • a Listener - which will accept one or more Event s
  • an EventManager which will be responsible to submit any Events to its registered Listeners

I have used generics in order to make use of Java's strong types but I've hit a dead-end as I can't get past by a compiler "Unchecked" warning

The classes are as below:

Event:

// no functionality, just a placeholder for events
interface Event { }

Listener:

interface EventListener<T extends Event> {
    boolean accept(Event event);

    void submit(T event);
}

EventManager:

public class EventManager {

    private final List<EventListener<? extends Event>> listeners;

    public EventManager(List<EventListener<? extends Event>> listeners) {
        this.listeners = listeners;
    }

    public void sendEvent(Event event) {
        this.listeners.stream()
                .filter(l -> l.accept(event))
                .forEach(l -> l.submit(getEvent(event)));
    }

    private <T extends Event> T getEvent(Event event) {
        return (T) event;  // <-- warning
    }
}

I'm expecting a client to call:

ConcreteEvent event = new ConcreteEvent(); // <-- That implements Event
eventManager.sendEvent(event);

The warning is in the last method, getEvent , which makes sense.

My question is:

  • Is there any alternative design that wouldn't raise a compilation warning and make correct use of java generics? I'd like to submit any event through the EventManager and that event would be processed by one or many listeners

Generics link things at compile (ie 'as you write the code') time. For example, in:

private <T extends Event> T getEvent(Event event) {
  return (T) event;  // <-- warning
}

This isn't accomplishing anything meaningful, you're just dressing up a cast: You have declared a T for this method, but whenever you declare a typevar, you must use it in at least 2 places or it is either a pointless exercise or (as is the case here), a hack. You are using it in 2 places here, but the second place is in a cast to T which does absolutely nothing: That is an assertion: "Compiler, shut up, I know what I am doing, trust me, it will be a T". Neither at write time, nor at runtime , does that (T) do anything. No check is performed, and this line cannot possibly throw a ClassCastException. Instead, the code of the event handler does an invisible cast and that one would fail: You'd get a CCE on a line with zero casts on it, which is confusing. That is what the warning is trying to tell you: The compiler cannot actually guarantee type safety here.

Your problem is that this 'link things at compile time' job is impossible here: You have a list of eventlisteners of who-knows-what (literally: ? extends Event -? = who knows), and there is thus no linking 2 usages in your code. There is no way to add such a compile-time linkage.

Okay, so what do I do?

IF you are on board with limiting the kinds of events an event handler can handle to solely typevar-less types (so no NewDataUploadedEvent<X> - only concrete types that themselves has no generics), there is a solution: Reify the type by explicitly passing it along.

Before I dive into the code, a note:

Your accept method accepts any event, but your submit method only accepts a T. This is bad design and you should fix it. What does it mean if a non-T Event type is nevertheless accepted by your boolean-returning method? The only way you could possibly pass it to the submit method is by breaking type safety, which makes no sense. More generally, why do you have an accept method? You're just forcing somebody writing an event handler to duplicate code and create bizarre questions ("What do I do if an event is passing to my submit method that I cannot handle?"). Why not just have a submit method which can throw an exception to indicate it doesn't want it, or returning a boolean to indicate this? I'll address this in the coming examples; I don't like writing bad code style in answers; makes me feel like I'm teaching bad habits.

On that note, the forEach terminal on a stream op, especially if no actual stream ops are done (no mapping or filtering or whatnot) is usually a code smell: You lose control flow, checked exception, and local variable transparency, and you gain absolutely nothing in return. Just write a for(:) loop. I know, I know. You have a new hammer, it is very shiny, and it is making you think everything is a nail. But, it's not a nail. You CAN butter toast with a hammer, but, the butterknife is the better tool for it. So it goes here. I'll fix that too.

  1. Add a Class<T> returning method to the interface:
interface EventListener<T extends Event> {
    Class<T> getType();
    void submit(T event) throws EventNotAcceptedException;
}

Now you can write your event loop like so:

public void sendEvent(Event event) {
  for (EventListener<?> listener : listeners) tryListener(listener, event);
}

private <T extends Event> boolean tryListener(EventListener<T> listener, T event) {
  Class<T> type = listener.getType();
  T obj;
  try {
    obj = type.cast(event);
  } catch (ClassCastException e) {
    return false;
  }

  try {
    listener.submit(obj);
    return true;
  } catch (EventNotAcceptedException e) {
    return false;
  }
}
  1. Go on a reflection spree

It is technically possible to use reflection to fetch the <X> in a class Foo extends Bar<X> . However, you get the literal X, straight from the source file, and not what it actually is. That's great if you write:

class UploadHandler extends EventHandler<UploadEvent> {}

as it means you can get UploadEvent.class out of that. It's not so good if you have:

class GeneralizedHandler<T> extends EventHandle<T> {
   private final Consumer<T> consumer;
   // boilerplate here
}

as you'll just be getting T which isn't what you need and you can't use that to do type checks and casts. You'd have to runtime error out if someone tries to pull this stunt on you, you can't catch this at compile time. If you're willing, well, doable, but complicated. the jlClass API has the getGenericSuperclass() method, which returns a Type , which is useless (it has no methods), but you'd check if gGS returns a java.lang.reflect.ParameterizedType , and if it does, call its getActualTypeArguments() method, whose first arg should, presumably, be a java.lang.Class , and that's the one. You can then cast and the like from there with guarantees that it's 'the right type', so to speak. However, there is no way to write that without getting the compiler warning. You solve that by isolating the code in a single method as you did in your getEvent method in your question, and sticking a @SuppressWarnings on that.

You can extract the proper type as an event listener is registered, storing your event listeners not as a List<EventListener> but, to make a container class: private static class EventContainer<T> { EventListener<T> listener; Class<T> eventType; } private static class EventContainer<T> { EventListener<T> listener; Class<T> eventType; } private static class EventContainer<T> { EventListener<T> listener; Class<T> eventType; } - this lets you throw an exception if someone passes an eventlistener impl that isn't defined to extends EventListener<SomethingConcrete> , as soon as possible (it's always good to throw sooner rather than later), and you can now use that eventType to call cast and friends - and there won't be any warnings.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM