Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding a method with a generic return type fails after adding a parameter

I wonder why this is a valid override:

public abstract class A {

    public abstract <X> Supplier<X> getSupplier();

    public static class B extends A {

        @Override
        public Supplier<String> getSupplier() {
            return String::new;
        }
    }
}

Whereas this is not:

public abstract class A {

    public abstract <X> Supplier<X> getSuppliers(Collection<String> strings);

    public static class B extends A {

        @Override
        public Supplier<String> getSuppliers(Collection<String> strings) {
            return String::new;
        }
    }
}

According to JLS §8.4.8.1, B.getSupplier must be a subsignature A.getSupplier:

An instance method mC declared in or inherited by class C, overrides from C another method mA declared in class A, iff all of the following are true:

  • ...
  • The signature of mC is a subsignature (§8.4.2) of the signature of mA.
  • ...

Subsignatures are defined in JLS §8.4.2:

Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the the type parameters of M, the same formal parameter types.

The signature of a method m1 is a subsignature of the signature of a method m2 if either:

  • m2 has the same signature as m1, or
  • the signature of m1 is the same as the erasure (§4.6) of the signature of m2.

So it seems like B.getSupplier is a subsignature of A.getSupplier but B.getSuppliers is not a subsignature of A.getSuppliers.

I wonder how it can be the case.

If B.getSupplier is a subsignature of A.getSupplier because it has the same erasure, then B.getSuppliers must also have the same erasure as A.getSuppliers. This should suffice for overriding getSuppliers to be legal - but it does not.

If B.getSupplier is a subsignature of A.getSupplier because it has the same signature, then I wonder what "the same type parameters (if any)" exactly means.

If type parameters are considered, then they should have different type parameters: A.getSupplier has type parameter X, B.getSupplier has none.
If type parameters are not considered then how's getSuppliers different?

This is more of an academic question about overrides and generics so please don't suggest refactoring code (like moving type parameter X to the class etc.).

I am looking for a formal, JLS-based answer.

From my point of view B.getSupplier should not be able override A.getSupplier as they don't have the same type parameters. This makes the following code (which produces ClassCastException) legal:

A b = new B();
URL url = b.<URL>getSupplier().get();
like image 686
lexicore Avatar asked May 11 '18 16:05

lexicore


Video Answer


2 Answers

According to the compiler output, the method signatures are different in both examples (compile the code with -Xlint:unchecked option to confirm it):

<X>getSupplier() in A (m2)
                                 1st snippet
getSupplier()    in B (m1)


<X>getSuppliers(Collection<String> strings) in A (m2)
                                                           2nd snippet
getSuppliers(Collection<String> strings)    in B (m1)

According to the JLS specification, the signature of a method m1 is a subsignature of the signature of a method m2 if either:

  • m2 has the same signature as m1, or

  • the signature of m1 is the same as the erasure of the signature of m2.

The first statement is out of the game - method signatures are different. But what about the second statement and erasure?

Valid Override

B.getSupplier() (m1) is a subsignature of A.<X>getSupplier() (m2), because:

  • the signature of m1 is the same as the erasure of the signature of m2

<X>getSupplier() after erasure is equal to getSupplier().

Invalid Override

B.getSuppliers(...) (m1) is not a subsignature of A.<X>getSuppliers(...) (m2), because:

  • the signature of m1 is not the same as the erasure of the signature of m2

The signature of m1:

getSuppliers(Collection<String> strings);

Erasure of the signature of m2:

getSuppliers(Collection strings);

Changing m1 argument from Collection<String> to the raw Collection eliminates an error, in this case m1 becomes a subsignature of m2.

Conclusion

1st  code snippet (valid override): the method signatures in the parent and child classes are different initially. But, after applying the erasure to the parent method the signatures becomes the same.

2nd code snippet (invalid override): the method signatures are different initially and remains different after applying the erasure to the parent method.

like image 52
Oleksandr Pyrohov Avatar answered Oct 27 '22 15:10

Oleksandr Pyrohov


The moment you added the parameter it ceased to be an override and became an overload.

Generics have nothing to do with it.

like image 37
user207421 Avatar answered Oct 27 '22 17:10

user207421