Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java nested generics

I'm trying to create a GUI library for java and planning to make it highly extensible by making it event driven using java 8 lambda expressions.

I have two types of events currently. The first one, GuiEvent, is not generic. The second one however does specify a generic parameter: ComponentEvent<T extends GuiComponent<?, ?>> so that later on, you can use the following lambda to catch the event:

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 looks like the following:

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

and it is part of GuiComponent<O extends GuiComponent<O, T>, T extends NativeGuiComponent> ... which in our case, as the component we are listening to is a button, can be simplified to Button<Button, NativeButton> and so the derived type O is Button.

As expected, it doesn't parameterize ComponentEvent<?> and therefore the type of event.component is rightfully GuiComponent<?, ?> which makes a cast mandatory.

Now I tried several ways to parameterize ComponentEvent starting out with this:

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

which worsens the situations as it is now showing a compile warning:

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>

For some weird reason changing the class variable to ActionEvent.class.asSubclass(Button.class) does compile, giving me the right type for event.component but it of course crashes due to a ClassCastException later on...

EDIT: Here is a list of all the class definitions involved:

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

Button, parameterized

public class Button extends GuiComponent<Button, NativeButton> {

Example 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);

Component & ActionEvent:

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);
    }
}
...
like image 216
Vic Avatar asked Sep 19 '26 15:09

Vic


1 Answers

Ok, so sifting through this model and explanation, I think the crux of the issue is:

  • The event itself is typed on the source component type.
  • ActionEvent.class is not rich enough to represent this (since it does not inform the runtime/compiler of the component type of the event). You need to pass a Class<ActionEvent<Button>> but that is not possible with class literals.

In situations like this, the typical approach is to use a richer type capture mechanism. Guava's TypeToken is one implementation:

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

Where onEvent is:

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

This pattern captures the fully resolved generic type of the Event, and makes it available to be inspected at runtime.

Edit

If you're worried about the burden of creating the TypeToken for callers, there are ways to move a lot of the complexity to your library code. Here's an example that does that:

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

Edit 2

Of course, the easiest route would be to stop typing your events on the component type in the first place. You could do this simply by passing two parameters to the handler:

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
}
like image 184
Mark Peters Avatar answered Sep 21 '26 03:09

Mark Peters



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!