Reputation: 29497
I am designing an event-driven system and am running into some basic API problems regarding generics.
I woud like all events to extend BaseEvent
:
// Groovy pseudo-code
abstract BaseEvent {
Date occurredOn
BaseEvent() {
super()
this.occurredOn = new Date() // Now
}
}
And I would like all event listeners to implement some basal interface:
interface EventListener<EVENT extends BaseEvent> {
void onEvent(EVENT event)
}
So this works great for simple listeners that only handle a single type of event:
class FizzEvent extends BaseEvent { ... }
class FizzEventListener implements EventListener<FizzEvent> {
@Override
void onEvent(FizzEvent fizzEvent) {
...
}
}
But I will have some listeners that need to handle multiple types of events:
class BuzzEvent extends BaseEvent { ... }
// So then, ideally:
class ComplexListener implements EventListener<FizzEvent>,
EventListener<BuzzEvent> {
@Override
void onEvent(FizzEvent fizzEvent) {
...
}
@Override
void onEvent(BuzzEvent buzzEvent) {
...
}
}
But this produces compiler errors:
Name clash: The method onEvent(EVENT) of type EventListener has the same erasure as onEvent(EVENT) of type EventListener but does not override it
Any ideas what the solution is for handling multiple events?
Upvotes: 3
Views: 1393
Reputation: 19682
In java 8
public class ComplexListener
{
public final EventListener<FizzEvent> fizzListener = fizzEvent ->
{
...
}
...
use complexListener.fizzListener
whenever an EventListener<FizzEvent>
is needed.
(Without java8, you can use anonymous class for the same effect, just more verbose.)
Another way in java8 is through method reference
public class ComplexListener
{
public void handleFizzEvent(FizzEvent fizzListener)
{
...
}
use complexListener::handleFizzEvent
whenever an EventListener<FizzEvent>
is needed.
In java generics, it is explicitly forbidden that an object can be both Foo<A>
and Foo<B>
(A!=B); i.e. Foo<A>
and Foo<B>
are mutually exclusive. Many reasons can be raised, but the most important one I think is because of capture conversion -- given a Foo<?>
object, the compiler assumes it is a Foo<X>
of a unique X
. Therefore no object can be Foo<A> & Foo<B>
(irrespective of reification).
Upvotes: 0
Reputation: 6085
The problem you're running into is called Type Erasure, which is how Java implements generics. This means that, for Java, the following lines of code:
@Override
void onEvent(FizzEvent fizzEvent) {
...
}
@Override
void onEvent(BuzzEvent buzzEvent) {
...
}
really look like this:
@Override
void onEvent(BaseEvent fizzEvent) {
...
}
@Override
void onEvent(BaseEvent buzzEvent) {
...
}
Notice that the type information has been 'erased' and only the super type BaseEvent
remains as the type parameter for both methods, which causes ambiguity and won't work.
If the extends
keyword had not been used, it would only see Object
instead, but would still run into the same problem.
This is in contrast to C#, which uses Type Reification to implement generics and can know the difference of types at runtime.
In other words, if you ask Java whether a List<Dog>
is the same kind of list as a List<Car>
, Java would say "yes" because it doesn't know any better at runtime, while C# would say "no" because it retains type information.
Any ideas what the solution is for handling multiple events?
You will need to use different method names or signatures if you want to use the same listener interface (e.g. onDogBarkEvent(Dog d)
, onCatMeowEvent(Cat c)
or perhaps create separate listener interfaces for different kinds of events (e.g. DogBarkListener
, CatMeowListener
).
This should point you in the right direction with a few Java options.
That aside, if you really feel strongly about your choice and are also free to choose your programming language, then you could consider taking C# for a spin and see if it works better for you.
Upvotes: 4
Reputation: 2769
a possible solution would be to skip generics and have an explicit "supports" method:
public FooListener implements Listener {
public <T extends BaseEvent> boolean supports(Class<T> clazz) {
//decide
}
public void handle(BaseEvent baseEvent) {
//handle
}
}
this, in combination with some abstract classes with generics for the "simple" cases, should do the trick:
private Class<S> clazz;
public Class<S> getClazz() {
if(clazz==null) {
ParameterizedType superclass =
(ParameterizedType)getClass().getGenericSuperclass();
clazz = (Class<S>) superclass.getActualTypeArguments()[0];
}
return clazz;
}
public boolean supports(Class clazz) {
return clazz!=null && clazz == getClazz();
Upvotes: 0