42. Exemplify erasure vs. overloading

Before we join them in an example, let’s quickly tackle erasure and overloading separately.

Erasure in a nutshell

Java uses type erasure at compile time in order to enforce type constraints and backward compatibility with old bytecode. Basically, at compilation time, all type arguments are replaced by Object (any generic must be convertible to Object) or type bounds (extends or super). Next, at runtime, the type erased by the compiler will be replaced by our type. A common case of type erasure implies generics.

Erasure of generic types

Practically, the compiler erases the unbound types (such as E, T, U, and so on) with the bounded Object. This enforces type safety as in the following example of class type erasure:

public class ImmutableStack<E> implements Stack<E> {
  private final E head;
  private final Stack<E> tail;
  …

The compiler applies type erasure to replace E with Object:

public class ImmutableStack<Object> implements Stack<Object> {
  private final Object head;
  private final Stack<Object> tail;
  …

If the E parameter is bound then the compiler uses the first bound class. For instance, in a class as class Node<T extends Comparable<T>> {…}, the compiler will replace T with Comparable. In the same manner, in a class as class Computation<T extends Number> {…} all occurrences of T would be replaced by the compiler with the upper bound Number.Check out the following case which is a classical case of method type erasure:

public static <T, R extends T> List<T> listOf(T t, R r) {
     
  List<T> list = new ArrayList<>();
  list.add(t);
  list.add(r);     
     
  return list;
}
// use this method
List<Object> list = listOf(1, “one”);

How does this work? When we call listOf(1, “one”), we are actually passing two different types to the generic parameters T and R. The compiler type erasure has replaced T with Object. This way, we can insert different types in the ArrayList and the code works just fine.

Erasure and bridge-methods

The bridge-methods are created by the compiler to cover corner cases. Specifically, when the compiler encounters an implementation of a parameterized interface or an extension of a parameterized class, it may need to generate a bridge-method (also known as a synthetic method), as part of the type erasure phase. For instance, let’s consider the following parameterized class:

public class Puzzle<E> {
  public E piece;
  public Puzzle(E piece) {
    this.piece = piece;
  }
  public void setPiece(E piece) {       
    this.piece = piece;
  }
}

And, an extension of this class:

public class FunPuzzle extends Puzzle<String> {
  public FunPuzzle(String piece) {
    super(piece);
  }
  @Override
  public void setPiece(String piece) {      
    super.setPiece(piece);
  }
}

Type erasure modifies the Puzzle.setPiece(E) as Puzzle.setPiece(Object). This means that FunPuzzle.setPiece(String) method does not override the Puzzle.setPiece(Object) method. Since the signatures of the methods are not compatible, the compiler must accommodate the polymorphism of generic types via a bridge (synthetic) method meant to guarantee that sub-typing works as expected. Let’s highlight this method in the code:

/* Decompiler 8ms, total 3470ms, lines 18 */
package modern.challenge;
public class FunPuzzle extends Puzzle<String> {
   public FunPuzzle(String piece) {
      super(piece);
   }
   public void setPiece(String piece) {
      super.setPiece(piece);
   }
   // $FF: synthetic method
   // $FF: bridge method
   public void setPiece(Object var1) {
      this.setPiece((String)var1);
   }
}

Now, whenever you see a bridge-method in the stack trace, you know what it is and why it is there.

Leave a Reply

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