18

Update: Thanks for everyone who helped out - the answer to this one lay in what I wasn't noticing in my more complex code and what I didn't know about the Java5 covariant return types.

Original Post:

I've been playing around with something this morning. While I know that I could tackle this whole problem differently, I'm finding myself obsessed with figuring out why it isn't working the way that I was expecting. After spending some time reading around on this, I find I'm no closer to understanding, so I offer it up as a question to see if I'm just being stupid or if there is really something I don't understand going on here.

I have created a custom Event hierarchy like so:

public abstract class AbstractEvent<S, T extends Enum<T>>
{
    private S src;
    private T id;

    public AbstractEvent(S src, T id)
    {
        this.src = src;
        this.id = id;
    }

    public S getSource()
    {
        return src;
    }

    public T getId()
    {
        return id;
    }
}

With a concrete implementation like so:

public class MyEvent
extends AbstractEvent<String, MyEvent.Type>
{
    public enum Type { SELECTED, SELECTION_CLEARED };

    public MyEvent(String src, Type t)
    {
        super(src, t);
    }
}

And then I create an event like so:

fireEvent(new MyEvent("MyClass.myMethod", MyEvent.Type.SELECTED));

Where my fireEvent is defined as:

protected void fireEvent(MyEvent event)
{
    for(EventListener l : getListeners())
    {
        switch(event.getId())
        {
            case SELECTED:
                l.selected(event);
                break;
            case SELECTION_CLEARED:
                l.unselect(event);
                break;
         }
    }
}

So I thought that this would be pretty straightforward but it turns out that the call to event.getId() results in the compiler telling me that I cannot switch on Enums, only convertible int values or enum constants.

It is possible to add the following method to MyEvent:

public Type getId()
{
    return super.getId();
}

Once I do this, everything works exactly as I expected it to. I'm not just interested in finding a workaround for this (because I obviously have one), I'm interested in any insight people might have as to WHY this doesn't work as I expected it to right off the bat.

3
  • Chris, can you show the class declaration of class containing fireEvent? Commented Jul 23, 2009 at 18:13
  • The problem was really that my AbstractEvent is much more complicated than what I posted here. I should have created the toy implementation and tested it before posting! Thank for everyone who helped out! Than answer was that my getId() method was a 'covariant return type' - I had another implementation buried in the class that was defined as Enum<T> return type. When I ditched that, the switch statement started compiling. Commented Jul 23, 2009 at 18:33
  • -0 for at least coming clean... Commented Jul 23, 2009 at 19:27

5 Answers 5

11

Yishai is right, and the magic phrase is "covariant return types" which is new as of Java 5.0 -- you can't switch on Enum, but you can switch on your Type class which extends Enum. The methods in AbstractEvent that are inherited by MyEvent are subject to type erasure. By overriding it, you're redirecting the result of getId() towards your Type class in a way that Java can handle at run-time.

Sign up to request clarification or add additional context in comments.

3 Comments

This is great - I missed this feature of Java5 entirely thanks for the pointer!
After going back and combing through the code, I hadn't noticed that I'd defined a second getId() method in my abstract class (obviously it was more complex than the example I'd posted here). It was this second getId() that returned an Enum<T> was being called by the switch statement and messing me up!
The link seems to be dead.
8

This is not related to generics. Switch statement for enum in java can only use values of that particular enum, thus it's prohibited to actually specify enum name. This should work:

switch(event.getId()) {
   case SELECTED:
         l.selected(event);
         break;
   case SELECTION_CLEARED:
         l.unselect(event);
         break;
}

Update: Ok, here's an actual code (which I had to change a little bit to get it to compile with no dependencies) that I've copy / pasted, compiled and ran - no errors:

AbstractEvent.java

public abstract class AbstractEvent<S, T extends Enum<T>> {
    private S src;
    private T id;

    public AbstractEvent(S src, T id) {
        this.src = src;
        this.id = id;
    }

    public S getSource() {
        return src;
    }

    public T getId() {
        return id;
    }
}

MyEvent.java

public class MyEvent extends AbstractEvent<String, MyEvent.Type> {
    public enum Type { SELECTED, SELECTION_CLEARED };

    public MyEvent(String src, Type t) {
        super(src, t);
    }
}

Test.java

public class Test {
  public static void main(String[] args) {
      fireEvent(new MyEvent("MyClass.myMethod", MyEvent.Type.SELECTED));
  }

  private static void fireEvent(MyEvent event) {
        switch(event.getId()) {
            case SELECTED:
                System.out.println("SELECTED");
                break;
            case SELECTION_CLEARED:
                System.out.println("UNSELECTED");
                break;
         }
    }
}

This compiles and runs under Java 1.5 just fine. What am I missing here?

5 Comments

No, I'm afraid that doesn't work. If that were the case then it would be complaining about my case : lines and not my event.getId() in a switch statement. Besides, if I override getId() in my concrete class and only return the super's getId() it all works.
I apologize: I typed in the example wrong. You are of course correct about the qualification of the case labels, however the real problem that is puzzling me is why event.getId() isn't a valid switch-able parameter.
It most certainly works for me with your code - no overrides. Now if fireEvent() was declared with AbstractEvent as parameter type, that would have been a different story.
Nope, fireEvent() uses the concrete class.
I don't know... I'm using JDK 1.6, but I don't imagine there should be a difference...
2

Works for me.

Complete cut-out-and-paste test program:

enum MyEnum {
    A, B
}
class Abstract<E extends Enum<E>> {
    private final E e;
    public Abstract(E e) {
        this.e = e;
    }
    public E get() {
        return e;
    }
}
class Derived extends Abstract<MyEnum> {
    public Derived() {
        super(MyEnum.A);
    }
    public static int sw(Derived derived) {
        switch (derived.get()) {
            case A: return 1;
            default: return 342;
        }
    }
}

Are you using some peculiar compiler?

1 Comment

I'm using a Java 6 compiler (r14) through Eclipse
2

In very short, because T is erased to an Enum class, not an Enum constant, so the compiled statement looks like you are switching on the result of getID being Enum, as in this signature:

Enum getId();

When you override it with a specific type, then you are changing the return value and can switch on it.

EDIT: The resistance made me curious, so I whipped up some code:

public enum Num {
    ONE,
    TWO
}
public abstract class Abstract<T extends Enum<T>> {
    public abstract T getId();
}

public abstract class Real extends Abstract<Num> {

}

public static void main(String[] args) throws Exception {
    Method m = Real.class.getMethod("getId");
    System.out.println(m.getReturnType().getName());
}

The result is java.lang.Enum, not Num. T is erased to Enum at compile time, so you can't switch on it.

EDIT:

I tested the code samples everyone is working with, and they work for me as well, so although I suspect that this underlies the issue in the more complex real code, the sample as posted actually compiles and runs fine.

5 Comments

This would only matter if we have a raw reference to AbstractEvent.
That was what I was thinking - that the auto-boxing isn't working for some reason - but WHY is the important question.
Nope, fireEvent only works with the concrete MyEvent class. No one is dealing with the AbstractEvent - it is just a convenience for me.
@Yishai, it's true that types are erased at runtime and the getName() would return Enum, but at compile time, the compiler has access to the parameterized types.
@Yishai - ah, but you've tested the whole different issue. Chris's code explicitly passes MyEvent.Type instance to his MyEvent constructor, so his switch works just fine.
1

I just tried this (copy-pasted your code) and I can't duplicate a compiler error.

public abstract class AbstractEvent<S, T extends Enum<T>>
{
    private S src;
    private T id;

    public AbstractEvent(S src, T id)
    {
        this.src = src;
        this.id = id;
    }

    public S getSource()
    {
        return src;
    }

    public T getId()
    {
        return id;
    }
}

and

public class MyEvent extends AbstractEvent<String, MyEvent.Type>
{

    public enum Type
    {
        SELECTED, SELECTION_CLEARED
    };

    public MyEvent( String src, Type t )
    {
        super( src, t );
    }


}

and

public class TestMain
{
    protected void fireEvent( MyEvent event )
    {
        switch ( event.getId() )
        {
            case SELECTED:
            break;
            case SELECTION_CLEARED:
            break;
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.