Java 嵌套泛型

Java nested generics

我正在尝试为 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>

出于某种奇怪的原因,将 class 变量更改为 ActionEvent.class.asSubclass(Button.class) 确实可以编译,为我提供了 event.component 的正确类型,但它当然会因稍后的 ClassCastException 而崩溃。 .

编辑: 这是所有涉及的 class 定义的列表:

基础class 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

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);
    }
}
...

好的,通过这个模型和解释进行筛选,我认为问题的症结在于:

  • 事件本身是在源组件类型上键入的。
  • ActionEvent.class 不足以表示这一点(因为它没有通知 runtime/compiler 事件的组件类型)。您需要传递一个 Class<ActionEvent<Button>> 但使用 class 文字是不可能的。

在这种情况下,典型的方法是使用更丰富的类型捕获机制。 Guava's 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
}

既然你没有遵循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)

事件通知机制必须携带两个值而不是一个值,但我认为这只会影响框架中心部分内的一小段代码。在您必须重新定位事件的情况下,它甚至会使事情变得更容易,因为您不必克隆事件来适应不同的源引用……