javascalareflectionscala-generics

"ambiguous reference to overloaded definition" when using Java class in Scala


I am using a Java class in my Scala which generates ambiguous reference to overloaded definition. Here is the code to explain this problem.

IComponent.java

package javascalainterop;

import java.util.Map;

public interface IComponent {
    public void callme(Map<String, Object> inputMap);
}

AComponent.java

package javascalainterop;

import java.util.Map;

public class AComponent implements IComponent {
     String message;
     public AComponent(String message) {
        this.message = message;
     }

     @Override
     public void callme(Map inputMap) {
        System.out.println("Called AComponent.callme with " + message);
    }
}

BComponent.scala

package javascalainterop

import java.util.{Map => JMap}

class BComponent(inputMessage: String) extends AComponent(inputMessage) {
    override def callme(inputMap: JMap[_, _]) {
        println(s"Called BComponent.callme with $inputMessage")
    }
}

ComponentUser.scala

package javascalainterop

import java.util.{HashMap => JHashMap}

object ComponentUser extends App {
    val bComponent = new BComponent("testmessage")
    val javaMap = new JHashMap[String, AnyRef]
    bComponent.callme(javaMap)
}

When I try to compile BComponent.scala and ComponentUser.scala the compilation fails with message below.

javascalainterop/ComponentUser.scala:8: error: ambiguous reference to overloaded definition,
both method callme in class BComponent of type (inputMap: java.util.Map[_, _])Unit
and  method callme in trait IComponent of type (x$1: java.util.Map[String,Object])Unit
match argument types (java.util.HashMap[String,AnyRef])
    bComponent.callme(javaMap)
                   ^
one error found

The Java classes represent a library which I have no control over. I have considered using reflection but it doesn't quite serve the use case. super[AComponent].callme also doesn't resolve the issue. How can the situation be resolved so that the code compiles and AComponent.callme is invoked at runtime?


Solution

  • EDITED

    I've edited this answer significantly to resolve earlier confusion and to be more correct.

    I think the original library that you're working with is broken, and not doing what it appears to be doing.

    IComponent declares a method void callme(java.util.Map<String, Object> inputMap) (which is equivalent to callme(inputMap: java.util.Map[String, AnyRef]: Unit in Scala), while AComponent declares void callme(java.util.Map inputMap) (callme(inputMap: java.util.Map[_, _]): Unit in Scala).

    That is, IComponent.callme accepts a Java Map whose key is a String and whose value is an AnyRef, while AComponent.callme accepts a Java Map whose key is any type and whose value is any type.

    By default, the Java compiler accepts this without complaint. However, if compiled with the -Xlint:all option, the Java compiler will issue the warning:

    javascalainterop/AComponent.java:12:1: found raw type: java.util.Map
      missing type arguments for generic class java.util.Map<K,V>
      public void callme(Map inputMap) {
    

    However, there's a far bigger problem in this specific case than merely omitting Map's type arguments.

    Because the compile time signature of the AComponent.callme method differs from that of the IComponent.callme method, it now appears that AComponent provides two different callme methods (one that takes a Map<String, Object> argument, and one that takes a Map argument). Yet, at the same time, type erasure (the removal of generic type information at runtime) means that the two methods also look identical at runtime (and when using Java reflection). So, AComponent.callme overrides IComponent.callme (thereby fulfilling the IComponent interface's contract), while also making any subsequent calls to Acomponent.callme with a Map<String, Object> instance ambiguous.

    We can verify this as follows: comment out the callme definition in BComponent and change the contents of the ComponentUser.scala file as follows:

    package javascalainterop
    
    import java.util.{HashMap => JHashMap}
    
    object ComponentUser extends App {
      //val bComponent = new BComponent("testmessage")
      val javaMap = new JHashMap[String, AnyRef]
      //bComponent.callme(javaMap)
    
      // Test what happens when calling callme through IComponent reference.
      val aComponent = new AComponent("AComponent")
      val iComponent: IComponent = aComponent
      iComponent.callme(javaMap)
    }
    

    When run, the program now outputs:

    Called AComponent.callme with AComponent
    

    Great! We created an AComponent instance, converted it to an IComponent reference, and when we called callme, it was unambiguous (an IComponent only has a single method named callme) and executed the overridden version provided by AComponent.

    However, what happens if we try to call callme on the original aComponent?

    package javascalainterop
    
    import java.util.{HashMap => JHashMap}
    
    object ComponentUser extends App {
      //val bComponent = new BComponent("testmessage")
      val javaMap = new JHashMap[String, AnyRef]
      //bComponent.callme(javaMap)
    
      // Test what happens when calling callme through each reference.
      val aComponent = new AComponent("AComponent")
      val iComponent: IComponent = aComponent
      iComponent.callme(javaMap)
      aComponent.callme(javaMap)
    }
    

    Uh oh! This time, we get an error from the Scala compiler, which is a little more rigorous regarding type parameters than Java:

    javascalainterop/ComponentUser.scala:14:14: ambiguous reference to overloaded definition,
     both method callme in class AComponent of type (x$1: java.util.Map[_, _])Unit
     and  method callme in trait IComponent of type (x$1: java.util.Map[String,Object])Unit
     match argument types (java.util.HashMap[String,AnyRef])
       aComponent.callme(javaMap)
                  ^
    

    Note that we haven't even looked at BComponent yet.

    So far as I can tell , there is no way to workaround this ambiguity in Scala, since the two ambiguous functions are actually one and the same, and have the exact same signature at runtime (making reflection useless too). (If anyone knows otherwise, please feel free to add a comment!)

    Since Java is happier with this generic nonsense than Scala, you might need to write all of the relevant code that uses this library in Java.

    Otherwise, it looks like your only options are to:

    UPDATE

    Having thought about it a little more, you might be able to achieve what you're looking to do with a little sleight of hand. Clearly, it will depend upon what you're trying to do, and the complexity of the actual IComponent and AComponent classes, but you might find it useful to change BComponent to implement IComponent, while using an AComponent instance in its implementation. This should work provided that BComponent doesn't have to be derived from AComponent (in BComponent.scala):

    package javascalainterop
    
    import java.util.{Map => JMap}
    
    class BComponent(inputMessage: String) extends IComponent {
    
      // Create an AComponent instance and access it as an IComponent.
      private final val aComponent: IComponent = new AComponent(inputMessage)
    
      // Implement overridden callme in terms of AComponent instance.
      override def callme(inputMap: JMap[String, AnyRef]): Unit = {
        println(s"Called BComponent.callme with $inputMessage")
        aComponent.callme(inputMap)
      }
    }