Search code examples
javajvmbytecodejvm-bytecode

In the Java bytecode/class format, what determines if a method overrides another?


I know that the bytecode specification allows classes to have methods with the same signature, differing only in the return type, unlike in the Java language. Some languages even make use of that under certain circumstances. My question is related to reflection:

if in a class I find a (non-private) method with the same name and parameter types as (a non final, non private) one its superclass , and with a return type equal or being a subtype of the return type of the said method in the superclass, when can I assume that code invoking the 'supermethod' statically will always result in the execution of the 'overriding(?)' method (naturally assuming the call is made on an object which is of that class)? Even in cases of other languages compiled to the JVM bytecode, or if runtime code generation is involved, or in hacked synthetic classes like the lambda forwarders?

My question was brought about from noticing how in the Scala standard library, an Iterable[E] has a method:

def map[O](f :E => O) :Iterable[E]

while a Map[K, V], a subclass of Iterable[(K, V)] declares another method:

def map[A, B](f :((K, V)) => (A, B)) :Map[A, B]

The actual signatures are more complex than here, but the principle is the same: after erasure, the method in Map could override the method in Iterable.


Solution

  • The fact of overriding is determined by the JVM by the exact equality of method descriptors (which include both parameter types and the return type). See JVMS §5.4.5, §5.4.6.

    Therefore, on the JVM level, a method returning Map does not override a method returning Iterable. Compilers typically generate a bridge method returning Iterable, implemented with a simple delegation to a method returning Map.

    Example:

    class A<T extends Iterable> {
        T map() {
            return null;
        }
    }
    
    class B extends A<Collection> {
        @Override
        Collection map() {
            return null;
        }
    }
    

    Here B.map overrides A.map, even though the return types differ. To make such hierarchy possible and valid, compiler generates (on the bytecode level) another method B.map that returns Iterable:

    class B extends A<java.util.Collection> {
      B();
           0: aload_0
           1: invokespecial #1                  // Method A."<init>":()V
           4: return
    
    
      java.util.Collection map();
           0: aconst_null
           1: areturn
    
    
      java.lang.Iterable map();
           0: aload_0
           1: invokevirtual #7                  // Method map:()Ljava/util/Collection;
           4: areturn
    

    When a virtual method A.map is invoked on an instance of B, a method B.map:Iterable is always called, which in turns calls B.map:Collection.