【问题标题】:Map of generics: read and write泛型映射:读写
【发布时间】:2019-12-13 12:21:55
【问题描述】:

有许多不同的事件,都实现相同的接口:

interface Event {}
class FooEvent implements Event {}
class BarEvent implements Event {}

每个事件都有一个专用的处理程序:

interface EventHandler<T extends Event> {
    void handle(T event);
}
class FooEventHandler implements EventHandler<FooEvent> {
    @Override
    public void handle(FooEvent event) { }
}
class BarEventHandler implements EventHandler<BarEvent> {
    @Override
    public void handle(BarEvent event) { }
}

所有事件处理程序都创建一次并添加到地图中。每当发生事件时,都应使用此映射来找到合适的事件处理程序。

class Main {
    Map<Class<? extends Event>, EventHandler<? extends Event>> eventHandlerRegistry = Map.of(
            FooEvent.class, new FooEventHandler(),
            BarEvent.class, new BarEventHandler()
    );

    void handleEvent(Event event) {
        EventHandler<? extends Event> handler = this.eventHandlerRegistry.get(event.getClass());
        handler.handle(event); // DOES NOT COMPILE: needed=capture<? extends Event>, given=Event
    }
}

不幸的是,最后一行无法编译。我可以通过省略 EventHandler 的类型参数来编译它:

EventHandlerhandler = this.eventHandlerRegistry.get(event.getClass());
handler.handle(event); // WARNING: unchecked call to 'handle(T)' as a member of raw type 'EventHandler'

但这感觉不太对劲…… 我知道PECS,但我感觉有点被困住了,因为我生产和消费了我的事件处理程序。

我怎样才能干净利落地实现这个?

【问题讨论】:

  • 这就是您的事件处理程序的全部用途吗?如果是这样,为什么EventHandler&lt;T extends Event&gt; 是通用的?换句话说,handle(FooEvent event) 所增加的价值是什么,而 handle(Event event) 中不存在?
  • 每个事件都可以有不同的属性。例如。 FooEvent 可以有一个属性String myStringBarEvent 可以有一个属性List&lt;String&gt; myBars。具体的处理程序确切地知道他们得到什么类型的事件,并且他们需要该事件的属性来处理它。显然我可以省略EventHandler 的通用参数,但是我需要在每个handle() 方法中强制转换event,这也感觉不干净......
  • Event 是单身人士吗?你能有许多相同类型的事件吗?如果没有,您可以尝试使用Map&lt;Event, EventHandler&lt;? extends Event&gt;&gt; eventHandlerRegistry 来映射事件和处理程序。否则,我建议为每种类型的事件提供常量(或枚举)并在地图中使用它们,而不是 Class&lt;? extends Event&gt;
  • 能否分享调用void handleEvent(Event event)的代码(这个方法可以通用吗?)
  • 不,Event 不是单例,可以有任意数量的相同类型的事件(例如,对于三明治店,可以有 100 个 OrderSandwichEvent)。但我不能使用Event 作为映射键,因为并非所有OrderSandwichEvents 都是相等的(例如,一个有boolean ham=true,另一个可以有boolean ham=false)。对于使用常量,我看不出这将如何解决我的问题?在我看来,使用Class&lt;? extends Event&gt; 作为映射键不是问题,但通用映射值是......

标签: java generics events collections event-handling


【解决方案1】:

如果您要将(通用)处理程序混合在同一个映射中,则不能具有类型安全性。据我所知,使代码类型安全的方法是去掉EventHandler 上的泛型类型参数;但这是您要避免的一件事。

如果您可能会牺牲类型安全,知道您的处理程序将始终与指定的类匹配,那么您可以尝试以下操作:

private <T extends Event> EventHandler<T> getHandler(Class<?> eventClass) {
    return (EventHandler<T>) 
              this.eventHandlerRegistry.get(eventClass); //Unchecked cast
}

然后让你的 handleEvent 方法通用:

<T extends Event> void handleEvent(T event) {
    EventHandler<T> handler = this.getHandler(event.getClass());
    handler.handle(event);
}

然后此方法将成功编译,而不会发出警告。你唯一需要确定的是eventHandlerRegistry 永远不会被这样的东西污染:

put(FooEvent.class, new BarEventHandler())); //this can happen

【讨论】:

  • 谢谢,看来这确实可行。我并不真正担心eventHandlerRegistry 被污染,因为它只创建一次,我不妨让它不可变并为它编写一个测试以确保它没有被污染。但是,您的解决方案有一个未经检查的演员表,而我的解决方案有一个未经检查的电话...不确定哪个更好/更差...?
  • @Tagas 您的代码使用的是原始类型,如果您正在处理通用代码,这是首先要避免的事情之一。但是,正如我所说,我的代码也不是类型安全的,但您确实保证警告得到处理(通过确保 eventHandlerRegistry 仅具有正确的 mapping
  • 还在思考... 将handleEvent() 设为通用有什么好处?我还不如直接将this.eventHandlerRegistry.get() 的返回值转换为EventHandler&lt;Event&gt;,而不是使handleEvent() 方法泛型。这也只会导致 1 个未经检查的强制转换警告。
  • 使handleEvent 泛型的唯一原因是在方法内部支持EventHandler&lt;T&gt;。没有参数T,你必须将handler声明为EventHandler&lt;Event&gt;,这比EventHandler&lt;T&gt;差,即使T不安全使用;它至少坚持TEvent 的子类型,而不是Event 本身(或者更糟的是原始类型EventHandler,如原始代码中的)。所以,是的,您可以将该方法声明为void handleEvent(Event event),但这会破坏具有参数化EventHandler 的意义(这让我在cmets 中提出了第一个问题)
【解决方案2】:

这是一个使用新 EventType 枚举可以做什么的示例:

声明枚举:

public enum EventType {
    FOO_EVENT, BAR_EVENT
}

声明Event接口:

interface Event {
    EventType getType();
}

class FooEvent implements Event {
    EventType getType() {
        return FOO_EVENT;
    }
}

class BarEvent implements Event {
    EventType getType() {
        return BAR_EVENT;
    }
}

EventHandlers:

interface EventHandler {
    void handle(Event event);
}

class FooEventHandler implements EventHandler {
    @Override
    public void handle(Event event) {
        //cast Event to FooEvent when processing
    }
}

class BarEventHandler implements EventHandler {
    @Override
    public void handle(Event event) {
        //cast Event to BarEvent when processing
    }
}

EventTypeEventHandler 之间声明一个Map

Map<EventType, EventHandler> eventHandlerRegistry = //... fill the map here

最后,当事件发生时,只需执行以下操作:

eventHandlerRegistry.get(event.getType()).handle(event);

P.S.注意,对于枚举,最好使用java.util.EnumMap

【讨论】:

  • 感谢您的建议。但不幸的是它并没有真正解决我的问题,因为当我使用EventType 作为通用参数时,我仍然需要根据该类型在handle() 方法中转换我收到的Event,这就是我一开始就试图避免......
  • @Tagas,更新了答案。恐怕你不能不投射,但你能做的是确保投射是安全的(受到EventType和地图的保护)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-08-11
  • 2015-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-25
相关资源
最近更新 更多