Search code examples
javagenerics

Problems when a non-generic method overrides a generic method


Before generic was added to the Java language, there was a class Super, which was defined as:

class Super {
  public Object transform(Object t) { ... }
  public Object get() { ... }
}

As a user of this class, I can derive a subclass Sub from Super:

class Sub extends Super {
  public Object transform(Object t) { ... } // override
  // The following cases are also considered as method override.
  // public String transform(Object t) { ... }
  // public List transform(Object t) { ... }
  // and so on.
  public Object get() { ... } // override
}

The transform and get in Sub override the transform and get in Super. One day, generic was added to the Java language, the author of the Super class wanted to re-engineering, he changed Super to a new form using generic:

class Super {
  public <T> T transform(T t) { ... }
  public <T> T get() { ... }
}

For backward compatibility, Java allows a non-generic method in subclass to override a generic method in superclass as long as they have the same erasure. The signature of transform in Sub and the signature's erasure of transform in Super are identical. In this way, regardless of whether it is before or after the Super's re-engineering, users of Super do not need to modify the code using Super.

If Super is refactored into a generic type, users of Super actually also do not need to modify any code. However, in this case, the raw type is used in Sub extends Super.

class Super<T> {
  public T transform(T t) { ... }
  public T get() { ... }
}

Now define a subclass of Super<T> -- Sub<T>:

class Sub<T> extends Super<T> {
  public Object transform(Object t) { ... } // override
  // The following cases are also considered as method override.
  // public String transform(Object t) { ... }
  // public List<Integer> transform(Object t) { ... }
  // and so on.
}

The transform in Sub<T> has the same signature as the signature's erasure of the transform in Super<T>. This prompts me to judge: If a get() method is added to Sub<T>, it should also override the get() method in Super<T>. However, during compilation, I found that not only does it fail to override, but it also results in an incompatible return type error. Why, in this case, can't the get() method in Sub<T> apply the same erasure rule to achieve overriding?

// after adding get() method to Sub<T>
class Sub<T> extends Super<T> {
  public Object transform(Object t) { ... } // override
  public Object get() { ... } // error: incompatible return type
}

After all, in the following form of code, the get() method in the subclass does override the get() method in the superclass. Why can't this rule be applied in the case where I encountered the error?

class Super {
  public <T> T transform(T t) { ... }
  public <T> T get() { ... }
}

class Sub extends {
  public Object transform(Object t) { ... } // override
  public Object get() { ... } // override!
}

Solution

  • The subclass method having a signature that is the erasure of the superclass method is indeed a necessary condition for overriding methods. In the words of the Java Language Specification, if m1 overrides m2, the signature of m1 must be a subsignature of the signature of m2.

    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.

    The signature of a method includes its name, type parameters, and formal parameter types. Notably, the signature does not include the return type.

    However, having a subsignature is not sufficient for overriding. There are additional requirements. In particular, the Object get() method fails to satisfy the following:

    If a method declaration d1 with return type R1 overrides or hides the declaration of another method d2 with return type R2, then d1 must be return-type-substitutable (§8.4.5) for d2, or a compile-time error occurs.

    For R1 and R2 that are reference types, fulfilling any of these will make the method declarations return-type-satisfiable:

    • R1, adapted to the type parameters of d2 (§8.4.4), is a subtype of R2.
    • R1 can be converted to a subtype of R2 by unchecked conversion (§5.1.9).
    • d1 does not have the same signature as d2 (§8.4.2), and R1 = |R2|.

    Object get() is not return-type-substitutable for T get() because

    • Object is not a subtype of T
    • Object cannot be converted to T using an unchecked conversion. (It is possible to convert from Object to T using a narrowing reference conversion that is unchecked, but this is different from the "unchecked conversions" specified in §5.1.9)
    • Object get() has the same signature as T get(). Remember that method signatures do not include the return type.

    Object transform(Object t) is return-type-substitutable for T transform(T t), because they have different signatures, and the erasure of T is Object.

    Object get() is return-type-substitutable for <T> T get() as well, because these have different signatures, and the erasure of T is Object. Recall that method signature includes the generic type parameters too. <T> T get() has one type parameter, and Object get() has none.