Genericity and Inherited Methods – Generics

Genericity and Inherited Methods

The subsignature requirement for overriding means that the signature of the subtype method must be the same as that of the supertype method, or it must be the same as the erasure of the signature of the supertype method. Note the implication of the last sentence: The signature of the subtype method must be the same as the erasure of the supertype method, not the other way around. The converse is neither overloading nor overriding, but a name clash and is reported as an error.

The subsignature requirement also implies that a generic subtype method cannot override a non-generic supertype method. In other words, genericity cannot be added to an inherited method. This case is illustrated in Example 11.16. It is the erasures of the signatures of the generic methods in the subtype that are the same as the signatures of the non-generic methods in the supertype. Overriding requires the converse. A name clash is generally the reason why neither overriding nor overloading is permitted.

Example 11.16 Genericity Cannot be Added to Inherited Methods

Click here to view code image

class SupJ {
  public void set(Object obj) {/*…*/}          // (1)
  public Object get()    {return null;}          // (2)
}
class SubJ extends SupJ {
  @Override public <T> void set(T t) {/*…*/}   // (1a) Error: same erasure
  @Override public <S> S get()  {return null;}   // (2a) Error: same erasure
}

Overriding Abstract Methods from Multiple Superinterfaces

The code below illustrates the simple case where the subinterface TaskAB at (3) extends the two superinterfaces, TaskA and TaskB, whose abstract methods are override-equivalent—that is, they have the same signature. The abstract method declared at (4) in the subinterface TaskAB overrides the two abstract method declarations from the superinterfaces. This method represents the two abstract method declarations from the superinterfaces. The compiler can also infer it from the inherited methods, and its declaration at (4) can be omitted.

Click here to view code image

interface TaskA { Object compute(Integer iRef1); }   // (1)
interface TaskB { Object compute(Integer iRef2); }   // (2)
interface TaskAB extends TaskA, TaskB {              // (3)
  @Override Object compute(Integer iRef3);           // (4) Can be omitted.
}
interface TaskC { String compute(Integer iRef4); }   // (5)
interface TaskABC extends TaskA, TaskB, TaskC {      // (6)
  @Override String compute(Integer iRef5);           // (7a) Can be omitted.
//@Override Object compute(Integer iRef6);           // (7b) Compile-time error!
}

The subinterface TaskABC at (6) extends the superinterfaces TaskA, TaskB, and TaskC. The abstract methods of the three superinterfaces are override-equivalent, since they have the same method signature: compute(Integer). The abstract method at (5) in TaskC can override the other two methods with its covariant return, as String type is a subtype of Object type. The compiler infers that the abstract method at (5) can represent the methods from the superinterfaces in the subinterface TaskABC. Again we can omit the abstract method declaration at (7a), as the compiler can infer it. A client of course must make sure that it implements the correct abstract method when implementing the interface TaskABC.

If the abstract method declaration at (7a) is replaced with the abstract method declaration at (7b) where the return type is Object, the code will not compile, as this method cannot override the abstract method declaration at (5) declared with the return type String. This is an example of a name clash—that is, the inherited methods from the superinterfaces are override-equivalent, but the method at (7b) cannot override them all, thus resulting in a compile-time error.

The next example shows that the multiple abstract methods inherited by the ContractZ are not override-equivalent—in fact they are overloaded, and both are inherited from their respective interfaces by the subinterface.

Click here to view code image

interface ContractX { void doIt(int i); }
interface ContractY { void doIt(double d); }
interface ContractZ extends ContractX, ContractY {
  @Override void doIt(int d);                        // Can be omitted.
  @Override void doIt(double d);                     // Can be omitted.
}

So far all examples have involved methods without generics, where a subtype method could override a supertype method that had the same signature.

A subtype method without generics can override supertype methods with generics that have the same signature after type erasure. After type erasure of the bake() methods at (1) and (2) in the code below, all three bake() methods at (1), (2), and (3) are override-equivalent with the signature bake(List). However, it is the bake() method at (3) that overrides the bake() methods at (1) and (2). This bake() method without generics at (3) logically represents all the inherited bake() methods in the Bakeable subinterface. The implementation of the Bakeable subinterface thus requires implementation of the bake() method without generics declared at (3).

Click here to view code image

class Ingredient { }
interface PizzaBakeable {
  void bake(List<Ingredient> ingredients);  // (1) After type erasure: bake(List)
}
interface CalzoneBakeable {
  void bake(List<Ingredient> ingredients);  // (2) After type erasure: bake(List)
}
interface SimplyBakeable {
  void bake(List ingredients);              // (3) Signature: bake(List)
}
interface Bakeable extends SimplyBakeable, PizzaBakeable, CalzoneBakeable {
  @Override void bake(List ingredients);    // Can be omitted.
}

In the code above, the bake() method without generics at (3) that overrides the other two inherited bake() methods, and thus represents all the inherited bake() methods in the subinterface, can be inferred by type erasure of any of the inherited bake() methods with generics, since they are all override-equivalent.

From the examples in this subsection, we see that override-equivalent methods in superinterfaces can be legally inherited by a subinterface, if a method can be inferred that overrides all inherited override-equivalent methods (after any type erasure), and which then represents these methods in the subinterface. In the case of override-equivalent methods with generics from multiple superinterfaces, the type erasure of any of the override-equivalent methods is always a candidate that can override and represent these methods in the subinterface.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *