javagenericsboundstype-erasurejava-bridge-method

Bridge methods in Java generics. Is this example correct?


Let's say I have this generic class:

class Item<T> {
  private T item;
  public void set(T item) {
    this.item = item;
  }
  public T get() {
    return item;
  } 
}

If I create 2 instances like this:

Item<Integer> intItem = new Item<Integer>();
Item<String> stringItem = new Item<String>();

The 2 instances share this same raw class:

  class Item {
    private Object item;
    public void set(Object item) {
      this.item = item;
    }
    public Object get() {
      return item;
    } 
  }

Now, if I extends the class Item like this:

class IntItem extends Item<Integer>{
  private Integer item;
  public void set(Integer item) {
    this.item = item;
  }
  public Integer get() {
   return item;
  } 
}

These bridge methods are created:

  class IntItem extends Item<Integer>{
      private Integer item;
      //Bridge method 1
      public void set(Object item) {
        this.item = (Integer) item;
      }
      public void set(Integer item) {
        this.item = item;
      }
      //Bridge method 2
      public Object get() {
       return item;
      }
      public Integer get() {
       return item;
      } 
    }

Have I got it right until here? My question is, why and when are bridge methods necessary? Can you make some example using this Item class?

I already read other answers, but I still do not get it completely without a concrete example.


Solution

  • You almost got it correct. Almost, because bridge methods bridge methods calls and do not duplicate method implementations. Your IntItem class would look like the following desugared version (you can verify this using for example javap):

    class IntItem extends Item<Integer> {
    
      private Integer item;
    
      // Bridge method 1
      public void set(Object item) {
        set((Integer) item);
      }
    
      public void set(Integer item) {
        this.item = item;
      }
    
      //Bridge method 2
      public Object get() {
       return <Integer>get(); // pseudosyntax
      }
    
      public Integer get() {
       return item;
      } 
    }
    

    Within Java byte code, it is allowed to define two methods that only differ by their return type. This is why there can be two methods get which you could not define explicitly using the Java language. As a matter of fact, you need to name the parameter types and the return type on any method invocation within the byte code format.

    And this is why you need bridge methods in the first place. The Java compiler applies a type erasure on generic types. This means, generic types are not considered by the JVM which sees all occurences of Item<Integer> as a raw Item. However, this does not work well with the explicit naming of types. In the end, ItemInt is itself not longer generic as it overrides all methods with explicitly typed versions which would be visible to the JVM with these explicit types. Thus, IntItem would in its sugared version not even override any methods of Item because the signatures are not compatible. In order to make generic types transparent to the JVM, the Java compiler needs to insert these bridge methods which factually override the original implementations in order to bridge the invocations to the methods defined in IntItem. This way, you indirectly override the methods and get the behavior you expect. Consider the following scenario:

    IntItem nonGeneric = new IntItem();
    nonGeneric.set(42);
    

    As mentioned, the IntItem::set(Integer) method is not generic because it is overridden with a non-generically typed method. Thus, there is no type erasure involved in calling this method. The Java compiler simply compiles the above method call to invoke the method with the byte code signature set(Integer): void. Just like you expected it.

    However, when looking at the following code:

    Item<Integer> generic = ...;
    generic.set(42);
    

    the compiler can not know for sure that the generic variable contains an instance of IntItem or of Item (or any other compatible class). Thus, there is no guarantee that a method with the byte code signature set(Integer): void even exists. This is why the Java compiler applies a type erasure and looks at Item<Integer> as if it was a raw type. By looking at the raw type, the method set(Object): void is invoked which is defined on Item itself and therefore always exists.

    As a consequence, the IntItem class can not be sure if its methods are invoked using the method with the erased type (which it inherits from Item) or the methods with the explicit type. Bridge methods are therefore implemented implicitly to create a single version of truth. By dynamically dispatching the bridge, you can override set(Integer) in a subclass of IntItem and the bridge methods still work.

    Bridge methods can also be used for implementing a covariant return type of a method. This feature was added when generics were introduced because bridge methods already existed as a concept. This feature came for free, so to speak. There is a third application of bridge methods where they implement a visibility bridge. This is necessary to overcome an access restriction of the reflection engine.