Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Groovy method overloading: selection of method prefers interfaces over subclasses?

Hello Groovy & Java Experts

We've come across a peculiar Groovy behaviour that seems to us like a limitation (or bug) in the language. Our long post boils down to this question:

Is method selection in Groovy intentionally preferring interfaces over subclassing when method overloading is at play?

We created a simple example to illustrate the case:

interface A {}
interface B {}

class C implements A, B {}
class D extends C {}

class Foo {
    void add(A a) { System.out.println("A"); }
    void add(B b) { System.out.println("B"); }
    void add(C c) { System.out.println("C"); }
}

D d = new D();
new Foo().add(d);

What we would have expected is that method Foo#add(C c) is being called, however, the following exception is thrown:

groovy.lang.GroovyRuntimeException: Ambiguous method overloading for method Foo#add.
Cannot resolve which method to invoke for [class D] due to overlapping prototypes between: [interface A] {interface B]

This seems unexpected as Foo#add(C c) clearly is the best candidate. So, we tested this exact code in Java and there it worked as expected: method Foo#add(C c) gets called.

We then went on to investigate a bit further and debugged through the source code. Specifically, there is a method choosing which methods gets invoked: groovy.lang.MetaClassImpl#chooseMostSpecificParams

In there, a distance is calculated among all 3 (in our case) #add methods - in the end - here: org.codehaus.groovy.runtime.MetaClassHelper#calculateParameterDistance(java.lang.Class, org.codehaus.groovy.reflection.CachedClass)

The algorithm then successively adds to the distance. First, because in our case, parameter d (instance of D) is not an interface and and not a primitive type, a distance of 17 is added. Second, and only then, it is checked whether types C and D are the same or whether D inherits from C. For every inheritance level that C is above D, a distance of 3 is added. Hence, we end up with a distance of 20.

This distance of 20 (after some additional shifting like a penalty for object param types) then compares to a distances of 2 for both the add-methods with only interfaces in their signatures, which results in method #add(C c) not being chosen/considered. The exception occurs because, indeed, there are 2 methods now (#add(A a) and #add(B b)) which have the same distance and the runtime can't know which method to choose.

Maybe someone can explain to us why this is handled differently in Groovy as compared to Java?

like image 253
Christof Avatar asked Oct 31 '22 16:10

Christof


1 Answers

This sounds like a more specific case of this (unsolved) bug. I'd suggest filling a JIRA about it.


Groovy's method selection works at runtime with multiple dispatch, or multimethods, due to being a dynamic language with optional typing, whereas Java uses single dispatch, where the method which will be called is defined at compile time.

The following code works on Java, but fails with an assertion error in Groovy:

public class SingleMult {
  public static void main(String[] args) {
    A a = new B();
    assert(new SingleMult().method(a) == "A");
  }

  String method(A a) { return "A"; }
  String method(B b) { return "B"; }
}

interface A {}

class B implements A {}
like image 60
Will Avatar answered Nov 08 '22 05:11

Will