3

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);
    }
}
...
Vic
  • 139
  • 8
  • 2
    It is very difficult to understand your question. I lost my way to understand the relation between all those classes explained in words. Probably writing down the class definition will help a bit.. – Rohit Jain Mar 02 '15 at 20:19
  • can you please share the `ClassCastException` you get? I'm curious about this question (I like generics!) but I don't entirely see why `>` fails. Did you make sense of the `unchecked invocation` warning, and whether it still works even if it gives a warning? Although last time something like this happened to me, I lost my generics within the template method ( http://stackoverflow.com/questions/28234960/java-generics-and-enum-loss-of-template-parameters this ). – EpicPandaForce Mar 02 '15 at 20:28
  • `java.lang.ClassCastException: class nova.core.gui.ComponentEvent$ActionEvent` I can't really make sense of the warning... Up to my understanding using `O` there should work... The full code can be found here: https://github.com/NOVAAPI/NovaCore/tree/master/src/main/java/nova/core/gui but it might be quite hard to read through all of it. – Vic Mar 02 '15 at 20:42
  • Please don't dump GitHub links on us. Please instead include a [minimal, complete example](http://stackoverflow.com/help/mcve) that illustrates the problem. – Radiodef Mar 02 '15 at 21:04
  • Where does `ActionEvent` come from? And what's its type declaration? Why `ComponentEvent`'s type parameter is `T extends GuiComponent, ?>`? Can't you narrow down the type parameters of GuiComponent based on T instead of using `, ?>`? – fps Mar 02 '15 at 21:07

2 Answers2

5

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
}
Mark Peters
  • 80,126
  • 17
  • 159
  • 190
  • The problem is that this will be used by my API so I don't really want to make it mandatory to pass a complicated type token there, casts are the smaller evil... And you can always pass a method like `onClick(ActionEvent – Vic Mar 02 '15 at 20:49
  • 1
    @Victorious3: I would argue that type safety is always preferable to error-prone runtime checks. I don't see how casts are the lesser evil here. I also don't really think there's a problem in introducing `TypeToken` into your API. It's a very generic concept that covers a deficiency in the core Java APIs. It's really no different than accepting a `Class`. – Mark Peters Mar 02 '15 at 20:51
  • I don't know if your answer is right or not, I believe it works. But I'll upvote it anyway due to the hard work you've done to decipher the question in the first place. – fps Mar 02 '15 at 21:09
  • 3
    Even if it doesn't work, this is informative and awesome. I never really got the purpose of TypeTokens until now. – EpicPandaForce Mar 02 '15 at 21:11
3

Since you are not following the Bean event pattern, you may consider another modification: just abandon the restriction that a listener has to have exactly one parameter. By splitting the source off the event and providing source and actual event as two parameters you can get rid of the generic signature of the even type.

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
}

Without the event type being generic, using Class tokens will satisfy the generic listener registration without generating warnings.

Your listeners become only slightly more complicated:

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

or

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

The event notification mechanism has to carry two values instead of one but I assume that this affects only a small piece of code within a central part of your framework. In the case that you have to retarget an event, it will even make things easier as you don’t have to clone the event to accommodate the different source reference…

Holger
  • 285,553
  • 42
  • 434
  • 765