Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compilation error while overriding abstract enum method with generic return type

Here is a code that compiles properly

public enum SupportedConversions {
    INTEGER {
        @Override
        public Integer convert( String ion, Object[]  aa) {
            return null;
        }
    },
    LONG {
        @Override
        public Long convert(final String ion, Object[] aa) {
            return null;
        }
    };

    public abstract <T> T convert(String val, Object[] aa);
}

But when i change the abstract function parameter to a List of objects instead of an array i get compilation error saying "Method does not override from super class". and this happens only if the return type is generic

example bad code

public enum SupportedConversions {
    INTEGER {
        @Override
        public Integer convert( String ion, List<Object>  aa) {
            return null;
        }
    },
    LONG {
        @Override
        public Long convert(final String ion, List<Object> aa) {
            return null;
        }
    };

    public abstract <T> T convert(String val, List<Object> aa);
}

Is there a reason for this to not work. Seems more like a bug in Java

like image 812
Sparky Avatar asked Oct 30 '22 11:10

Sparky


1 Answers

The question should be “why can the first be compiled” rather than “why does the second fail”.
Both are broken.

A method signature like

<T> T convert(String val, Object[] aa)

says “whatever the caller substitutes for T, this method will return a compatible result”. This isn’t very useful as the only valid return value is null, but at least, the compiler will tell you, when you try to return an incompatible result within a method declared that way.

But the subclasses override this method like

Long convert(final String ion, Object[] aa)

in other words, override a method that promises to return whatever the caller wishes with a method that always returns Long. That should feel wrong in the first place… The result is still compatible when you return null, but not when you return a non-null Long value and the compiler won’t even warn you about that, as the Long value is compatible with the declared return type of Long.

The compiler should have issued a warning about the method declaration itself, however. To demonstrate the problem, with that declaration, you can write

String s = SupportedConversions.LONG.convert("bla", null);

and the compiler won’t object. As said, the base type declaration <T> T convert(…) promises to return whatever the caller assumes for T and here, T has been inferred to be String. That obviously will break at runtime, when the implementation returns a Long instance.


The reason why this can be compiled at all, is the compatibility with pre-Generics code. The intention was to allow libraries with different “generification” state to interact. E.g. you can compile Java 1.4 application code with a recent jdk, even if some classes override now-Generic methods.

So the convert method in your subclass not using Generics is allowed to override convert method of the base class. In contrast, a method declaration like

Long convert(final String ion, List<Object> aa)

is using Generics, hence, is not allowed to bypass the Generic type system. If you use the raw type List instead, you have again a non-Generic declaration which gets away with bypassing Generics.


If you are now tempted to say, it’s illogical to assume pre-Generics behavior here, you are not alone. Not only because the overriding method is within the same compilation unit (enum declaration) than the Generic declaration that is overridden, both are inside an enum declaration, a syntactical construct that didn’t exist prior to Java 5 (where Generics were introduced).

Further, the overriding methods utilize covariant return types, Long resp. Integer where the erasure of the method declaration has a return type of Object, which also can’t appear in code prior to Java 5.

But these (still) are the rules. You should care for the compiler warnings here. If you didn’t get warnings (I know that the Netbeans IDE has sloppy defaults), you should try to enable them.


There is no fix for this code. What you are trying to do, is impossible with enums. You could remove the type parameter T and let the base type’s method declaration return Object, but the covariant return types in the enum constants are irrelevant as they are not part of the public API. The best alternative would be:

public interface SupportedConversions<T> {
    SupportedConversions<Integer> INTEGER = (String ion, Object[] aa) -> {
        return null;
    };
    SupportedConversions<Long> LONG = (String ion, Object[] aa) -> {
        return null;
    };
    public abstract T convert(String val, Object[] aa);
}

resp.

public interface SupportedConversions<T> {
    SupportedConversions<Integer> INTEGER = (ion, aa) -> {
        return null;
    };
    SupportedConversions<Long> LONG = (ion, aa) -> {
        return null;
    };
    public abstract T convert(String val, List<Object> aa);
    // we can support both variants
    public default T convert(String val, Object[] aa) {
        return convert(val, Arrays.asList(aa));
    }
}
like image 103
Holger Avatar answered Nov 13 '22 07:11

Holger