Java嵌套泛型

3
我正在尝试为Java创建一个GUI库,并计划使用Java 8 lambda表达式使其高度可扩展,从而使其成为事件驱动。
目前我有两种类型的事件。第一种是GuiEvent,它不是泛型的。然而,第二种指定了一个泛型参数:ComponentEvent<T extends GuiComponent<?, ?>>,以便以后可以使用以下lambda捕获事件:
button.onEvent((event) -> {
    // event.component is supposed to be of the type ComponentEvent<Button>
    System.out.println("Test button pressed! " + ((Button) event.component).getText());
}, ActionEvent.class);

"onEvent" 看起来像下面这样:
public <EVENT extends ComponentEvent<?>> O onEvent(EventListener<EVENT> listener, Class<EVENT> clazz) {
    return onEvent(listener, clazz, Side.CLIENT);
}

它是 GuiComponent<O extends GuiComponent<O, T>, T extends NativeGuiComponent> 的一部分...在我们的情况下,由于我们监听的组件是按钮,因此可以简化为 Button<Button, NativeButton>,因此派生类型 OButton
如预期所示,它不会对 ComponentEvent<?> 进行参数化,因此 event.component 的类型正确地是 GuiComponent<?, ?>,这使得强制转换成为必需。
现在我尝试了几种方法来参数化 ComponentEvent,从以下方式开始:
public <EVENT extends ComponentEvent<O>> O onEvent(EventListener<EVENT> listener, Class<EVENT> clazz) {
    return onEvent(listener, clazz, Side.CLIENT);
}

这使情况变得更糟,因为它现在显示了编译警告:
Type safety: Unchecked invocation onEvent(EventListener<ComponentEvent.ActionEvent>,
Class<ComponentEvent.ActionEvent>) of the generic method
onEvent(EventListener<EVENT>, Class<EVENT>) of type 
GuiComponent<Button,NativeButton>

由于某种奇怪的原因,将类变量更改为ActionEvent.class.asSubclass(Button.class)可以编译,给我event.component的正确类型,但是当然后来由于ClassCastException而崩溃......

编辑:以下是涉及到的所有类定义的列表:

基类GuiComponent:

public abstract class GuiComponent<O extends GuiComponent<O, T>, 
T extends NativeGuiComponent> implements Identifiable, 
EventListener<GuiEvent>, PacketHandler {

private SidedEventBus<ComponentEvent<?>> eventListenerList = new SidedEventBus<ComponentEvent<?>>(this::dispatchNetworkEvent);    

...
public <EVENT extends ComponentEvent<?>> O onEvent(EventListener<EVENT> listener, Class<EVENT> clazz, Side side) {
    eventListenerList.add(listener, clazz, side);
    return (O) this;
}

public <EVENT extends ComponentEvent<?>> O onEvent(EventListener<EVENT> listener, Class<EVENT> clazz) {
    return onEvent(listener, clazz, Side.CLIENT);
}
...

按钮,带参数化的。
public class Button extends GuiComponent<Button, NativeButton> {

示例图形用户界面
Gui testGUI = new Gui("testgui")
        .add(new Button("testbutton2", "I'm EAST")
            .setMaximumSize(Integer.MAX_VALUE, 120)

            .onEvent((event) -> {
                System.out.println("Test button pressed! " + Side.get());
            }, ActionEvent.class), Anchor.EAST)

        .add(new Button("testbutton3", "I'm CENTER"))
        .add(new Button("testbutton4", "I'm SOUTH"), Anchor.SOUTH)

        .add(new GuiContainer("container").setLayout(new FlowLayout())
            .add(new Button("testbutton5", "I'm the FIRST Button and need lots of space"))
            .add(new Label("testlabel1", "I'm some label hanging around").setBackground(new Background(Color.white)))
            .add(new Button("testbutton7", "I'm THIRD"))
            .add(new Button("testbutton8", "I'm FOURTH"))
        , Anchor.NORTH)

        .onGuiEvent((event) -> {
            System.out.println("Test GUI initialized! " + event.player.getDisplayName() + " " + event.position);
        }, BindEvent.class)

        .onGuiEvent((event) -> {
            System.out.println("Test GUI closed!");
        }, UnBindEvent.class);

    guiFactory.registerGui(testGUI, id);

组件和动作事件:
public abstract class ComponentEvent<T extends GuiComponent<?, ?>> extends CancelableEvent implements SidedEvent {

public final T component;

public ComponentEvent(T component) {
    this.component = component;
}

public static class ActionEvent<T extends GuiComponent<?, ?>> extends ComponentEvent<T> {

    public ActionEvent(T component) {
        super(component);
    }
}
...

2
很难理解你的问题。我迷失了理解所有那些用文字解释的类之间关系的方法。也许写下类定义会有所帮助... - Rohit Jain
请问您能分享一下您遇到的ClassCastException吗?我对这个问题很感兴趣(我喜欢泛型!),但我并不完全明白为什么<EVENT extends ComponentEvent<O>>会失败。您是否理解了“未经检查的调用”警告,并且即使它发出警告,它是否仍然有效?虽然上次类似的事情发生在我身上时,我在模板方法中失去了我的泛型(http://stackoverflow.com/questions/28234960/java-generics-and-enum-loss-of-template-parameters this)。 - EpicPandaForce
java.lang.ClassCastException: class nova.core.gui.ComponentEvent$ActionEvent我无法理解这个警告...根据我的理解,使用O应该可以工作...完整的代码可以在这里找到:https://github.com/NOVAAPI/NovaCore/tree/master/src/main/java/nova/core/gui,但是可能很难阅读所有内容。 - Vic
请不要把GitHub链接直接扔给我们,而是请提供一个最小完整示例,来说明问题。 - Radiodef
ActionEvent 是从哪里来的?它的类型声明是什么?为什么 ComponentEvent 的类型参数是 T extends GuiComponent<?, ?>?难道你不能根据 T 缩小 GuiComponent 的类型参数,而不是使用 <?, ?> 吗? - fps
2个回答

5

好的,浏览了这个模型和解释后,我认为问题的关键在于:

  • 事件本身是在源组件类型上进行分类的。
  • ActionEvent.class 不够丰富,无法表示此类情况(因为它不能告诉运行时/编译器事件的组件类型)。您需要传递一个 Class<ActionEvent<Button>> 但使用类字面量不可能实现。

在这种情况下,典型的方法是使用更丰富的类型捕获机制。 Guava 的 TypeToken 是其中一种实现:

button.onEvent((event) -> {
    System.out.println("Test button pressed! " + event.component.getText());
}, new TypeToken<ActionEvent<Button>>(){});

onEvent在哪里:

public <E extends ComponentEvent<O>> O onEvent(EventListener<E> listener, TypeToken<E> eventType) {
    //register listener
}

这个模式捕获Event的完全解析泛型类型,并使其在运行时可以检查。

编辑

如果您担心为调用者创建TypeToken的负担,有办法将大部分复杂性移动到库代码中。以下是一个示例:

button.onEvent((event) -> {
    System.out.println("Test button pressed! " + event.component.getText());
}, ActionEvent.type(Button.class));

//then in ActionEvent
public static <C> TypeToken<ActionEvent<C>> type(Class<C> componentType) {
   return new TypeToken<ActionEvent<C>>(){}.where(new TypeParameter<C>(){}, componentType);
}

编辑 2

当然,最简单的方法是一开始就不要在组件类型中输入事件。你只需要通过将两个参数传递给处理程序即可实现:

button.onEvent((event, component) -> {
    System.out.println("Test button pressed! " + component.getText());
}, ActionEvent.class);

public <E extends ComponentEvent> O onEvent(EventListener<E, O> listener, Class<E> eventType) {
    //register listener
}

问题在于这将被我的API使用,所以我不想强制传递一个复杂的类型令牌,转换是较小的恶……而且你总是可以传递一个像 onClick(ActionEvent<Button>> button) 这样的方法。 - Vic
1
@Victorious3:我认为类型安全始终优于容易出错的运行时检查。我不认为强制转换是更小的恶。我也不认为在API中引入“TypeToken”会有问题。这是一个非常通用的概念,涵盖了核心Java API中的缺陷。这与接受“Class”没有什么区别。 - Mark Peters
我不知道你的答案是否正确,但我相信它是可行的。无论如何,我会点赞的,因为你在首先解密问题方面付出了辛勤的努力。 - fps
3
即使它不起作用,这也很有用并且很棒。直到现在我才真正明白TypeTokens的目的所在。 - EpicPandaForce

3

由于您没有遵循Bean事件模式,您可以考虑另一种修改方式:放弃监听器必须具有一个参数的限制。通过将源从事件中分离并提供源和实际事件作为两个参数,您可以摆脱事件类型的通用签名。

public interface EventListener<E,S> {
    void processEvent(E event, S source);
}
public abstract class ComponentEvent extends CancelableEvent implements SidedEvent {
    // not dealing with source anymore…
}

 

public abstract class
GuiComponent<O extends GuiComponent<O, T>, T extends NativeGuiComponent> {
    public <EV extends ComponentEvent> O onEvent(
                                         EventListener<EV,O> listener, Class<EV> clazz) {
        return onEvent(listener, clazz, Side.CLIENT);
    }
    // etc
}

没有通用事件类型,使用Class令牌将满足通用监听器注册而不产生警告。
您的侦听器只会变得稍微复杂一些:
.onEvent((event,source) -> {
                System.out.println("Test button pressed! " + Side.get());
            }, ActionEvent.class)

或者

.onGuiEvent((event,source) -> {
            System.out.println("Test GUI initialized! " + event.player.getDisplayName() + " " + event.position);
        }, BindEvent.class)

事件通知机制需要携带两个值而不是一个,但我认为这只影响了框架中的一小部分代码。如果您必须重新定位事件,则它甚至会使事情变得更加容易,因为您不必克隆事件以适应不同的源引用...

这正是我最终所做的。 - Vic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接