2

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.

Turin
  • 2,208
  • 15
  • 23

2 Answers2

4

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.

apangin
  • 92,924
  • 10
  • 193
  • 247
1

It all eventually depends on the JVM instruction used:

  • invokespecial would invoke the method without doing dynamic resolution based on the type of current object.

  • invokevirtual would dispatch based on the class.

Related: Why invokeSpecial is needed when invokeVirtual exists

So the answer is it depends on the generated bytecode.

M A
  • 71,713
  • 13
  • 134
  • 174
  • I edited my question with a motivating example. It also excluded final/private methods for that reason - I was interested in potentially two 'lines' of *virtual* methods. In other words, is the virtual table of a class something that the compiler creates and has some leaway with, or is it created by JVM itself based on the class bytecode? – Turin Sep 08 '21 at 12:06
  • 1
    The front-end compiler used to have more control, but this was taken away. In particular, take a look at the ACC_SUPER modifier, which is no longer generated and has been ignored by the JVM for many years. The driving factor is security, and this also makes it difficult to implement other languages that target the JVM but which have different virtual dispatch rules. – boneill Sep 08 '21 at 15:20