javascalainner-classesscala-java-interop

How to instantiate a nested generic class defined in scala from java?


I'm trying to instantiate a nested generic Scala class from Java and running into this compilation error. Can someone help? Thanks

outer.scala:

class Outer {
    class Inner[A]
}

sctest.java:

public class sctest{
    public static void main(String[] args) {
        Outer o = new Outer();
        Outer.Inner<String> a = o.new Inner<String>();
    }
}

$ javac sctest.java

sctest.java:4: error: constructor Inner in class Outer.Inner cannot be applied to given types;
    Outer.Inner a = o.new Inner();
                              ^
  required: Outer
  found: no arguments
  reason: actual and formal argument lists differ in length
  where A is a type-variable:
    A extends Object declared in class Outer.Inner
 1 error

Solution

  • I don't see how this can be done from Java. See appendix for details. I'll jump directly to a workaround proposal.


    Whenever you are facing problems with Java-Scala interop while trying to call Scala code from Java, there is a simple, while maybe somewhat heavyweight workaround, that works in essentially every situation.

    ###TL;DR

    If instantiating Scala entities in Java code does not work, hide everything behind a Java-interface, and implement this interface in Scala.

    ###Workaround proposal

    If you are using some mainstream Scala framework, it probably has a completely separate Java API (e.g. Spark, Akka, Play), then please use that!

    If it is a less known Scala software package without a separate Java API, do the following:

    1. Write down a minimal, clean user-facing API in pure Java
    2. Provide framework-facing interfaces in Java
    3. Implement the Java interfaces in Scala
    4. Provide an entry point to the Java API implementation
    5. Use the pure-Java API in your Java code

    If you don't want to implement a full Java API, you can use this approach locally, to deal with those parts of Scala code that don't work seamlessly with your Java project.

    Why it should work: Scala can easily express everything that Java can express, whereas Java has no mechanisms for interpreting most of the Scala constructs (the entire metaprogramming subsystem - givens/using/derives/inline etc., as well as the whole type-inference machinery, simply have no corresponding counterparts on the javac side). Therefore, it's much easier to implement Java-interfaces in Scala than using any Scala code from Java.

    Here is how this pattern can be applied to your example (I extended it a bit to make it non-trivial):

    Note that class names are somewhat ugly, I did that only to omit the package declarations, so that all the files can be dumped in src/main/{scala,java}; Don't do this in real implementation.

    Step 0: Look at the third-party Scala library

    Suppose that this is our third-party Scala library, that has superImportantMethods and computes superImportantThings:

    /* Suppose that this is the external Scala library 
     * that you cannot change.
     * A = Outer
     * B = Inner
     * 
     * + I added at least one member and one
     * method, so that it's not so boring and trivial
     */
    
    class A {
      var superImportantMemberOfA: Int = 42
      class B[T](t: T) {
        def superImportantMethod: String = t + "" + superImportantMemberOfA
      }
    }
    

    Step 1/2: Minimal Java API for use in your Java project

    We will use these interfaces in our java project, and implement them using Scala directly:

    interface A_j {
      <X> B_j<X> createB(X x);
    }
    
    interface B_j<X> {
      String superImportantMethod();
    }
    

    Step 3: Implement the interfaces in Scala

    /** Implementation of the java api in 
      * Scala
      */
    class A_javaApiImpl extends A_j {
      private val wrappedA: A = new A
    
      private class B_javaApiImpl[X](val x: X) extends B_j[X] {
        private val wrappedB: wrappedA.B[X] = new wrappedA.B[X](x)
        def superImportantMethod: String = wrappedB.superImportantMethod
      }
    
      def createB[X](x: X): B_j[X] = new B_javaApiImpl[X](x)
    }
    

    Step 4: provide an entry point to the API

    /** Some kind of entry point to the 
      * java API.
      */
    object JavaApi {
      def createA: A_j = new A_javaApiImpl
    }
    

    Step 5: Use the java-only API in your Java code:

    public class JavaMain {
      public static void main(String[] args) {
    
        // Use the Java API in your Java application
        // Notice that now all A_j's and B_j's are
        // pure Java interfaces, so that nothing 
        // should go wrong.
        A_j a = JavaApi.createA(); // the only call of a Scala-method.
        B_j<String> b = a.createB("foobarbaz");
        System.out.println(b.superImportantMethod());
      }
    }
    

    Now nothing should go wrong, because Java (almost) never calls any Scala methods or constructors, and by defining a clean API you also have a guarantee that you will not run into any problems because some Scala concepts cannot be represented in Java. Indeed, it does compile and run:

     [info] Running (fork) JavaMain 
     [info] foobarbaz42
    

    ###Appendix I ###(Failed) attempt to instantiate generic inner Scala classes from Java

    I started with these definitions (slightly abbreviated code from your question):

    .scala:

    class A {
      class B[T]
    }
    

    .java:

    public class JavaMain {
      public static void main(String[] args) {
        A a = new A();
        A.B<String> b = a.new B<String>(a);
      }
    }
    

    Notice that the javac compiler required a parameter of type A for the B constructor, which already was somewhat suspicious, and not really intuitive.

    It compiled, but when I tried to run it, I got the following cryptic error message:

    [error] Exception in thread "main" java.lang.NoSuchMethodError: A$B.(LA;LA;)V [error] at JavaMain.main(JavaMain.java:4)

    I had absolutely no clue what this is supposed to mean, so I decompiled the generated ".class"-files:

    Decompiled A.class:

    public class A {
    
        public class B<T> {
            public /* synthetic */ A A$B$$$outer() {
                return A.this;
            }
    
            public B() {
                if (A.this == null) {
                    throw null;
                }
            }
        }
    
    }
    

    Decompiled A$B.class:

    public class A.B<T> {
        public /* synthetic */ A A$B$$$outer() {
            return A.this;
        }
    
        public A.B() {
            if (A.this == null) {
                throw null;
            }
        }
    }
    

    Decompiled JavaMain.class:

    public class JavaMain {
        public static void main(String[] arrstring) {
            A a;
            A a2 = a = new A();
            a2.getClass();
            A.B b = new A.B(a2, a);
        }
    }
    

    The new A.B(a2, a) part doesn't even look like valid java to me (and neither to the javac). So, what I'm essentially trying to say: everything crashes and burns and I don't know why. Therefore, I would simply advise to implement the workaround described above.

    Hope that helps.