如何从 java 实例化在 scala 中定义的嵌套泛型 class?

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

我正在尝试将 Java 和 运行 中的嵌套通用 Scala class 实例化到此编译错误中。有人可以帮忙吗?谢谢

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

您可以向 Outer 添加通用方法来实例化 Inner 个实例吗?

outer.scala:

class Outer {
  class Inner[A]
  def inner[A](): Inner[A] = new Inner[A]
}

sctest.java:

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

我不明白如何从 Java 中做到这一点。详见附录。我将直接跳转到解决方法建议。


每当您在尝试从 Java 调用 Scala 代码时遇到 Java-Scala 互操作问题时,有一个简单但可能有点重量级的解决方法,基本上适用于所有情况。

TL;DR

如果在 Java 代码中实例化 Scala 实体不起作用,请将所有内容隐藏在 Java-interface 后面,并在 Scala 中实现此接口。

变通方案

如果你正在使用一些主流的 Scala 框架,它可能 有一个完全独立的 Java API (例如 Spark、Akka、Play),然后 请使用那个!

如果是鲜为人知的Scala软件包,没有单独的 Java API,执行以下操作:

  1. 用纯粹的Java
  2. 写下一个最小的、干净的user-facingAPI
  3. 在Java
  4. 中提供framework-facing接口
  5. 在 Scala 中实现 Java 接口
  6. 提供 Java API 实现的入口点
  7. 在您的 Java 代码中使用 pure-Java API

如果您不想实现完整的 Java API,您可以在本地使用此方法,以处理 Scala 代码中无法与您的 Java项目。

为什么它应该工作:Scala 总是试图适应 Java。 Java 完全忽略 Scala。因此,容易得多 从 Scala 调用 Java 代码,并在其中实现 Java 接口 Scala,而不是反过来。

以下是如何将此模式应用于您的示例(我对其进行了一些扩展以使其成为 non-trivial):

注意 class 名字有点难看,我这样做只是为了省略包声明,这样所有文件都可以转储到 src/main/{scala, java};不要在实际实施中这样做。

第 0 步: 查看 third-party Scala 库

假设这是我们的 third-party Scala 库,那 有 superImportantMethods 并计算 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
  }
}

第 1/2 步: 最小 Java API 用于您的 Java 项目

我们将在我们的 java 项目中使用这些接口,并实现它们 直接使用 Scala:

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

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

步骤 3:在 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)
}

第 4 步: 提供 API

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

第 5 步: 在您的 Java 代码中使用 java-only API:

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());
  }
}

现在应该不会出错,因为 Java(几乎)从不调用任何 Scala 方法或构造函数,通过定义一个干净的 API 你也 保证你不会 运行 陷入任何问题,因为 某些 Scala 概念无法在 Java 中表示。确实如此 编译并 运行:

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

附录一

(失败)尝试从 Java

实例化通用内部 Scala classes

我从这些定义开始(您问题中的代码略有缩写):

.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);
  }
}

请注意,javac 编译器需要 A 类型的参数用于 B 构造函数, 这已经有点可疑了,而且不是很直观。

它编译了,但是当我尝试 运行 它时,我收到了以下神秘的错误消息:

[错误] 线程异常 "main" java.lang.NoSuchMethodError: A$B.(LA;LA;)V [错误] JavaMain.main(JavaMain.java:4)

我完全不知道这是什么意思,所以我反编译了生成的 “.class”-文件:

反编译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;
            }
        }
    }

}

反编译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;
        }
    }
}

反编译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);
    }
}

new A.B(a2, a) 部分对我来说甚至看起来都不像有效的 java (对我来说也不是 javac).所以,我基本上想说的是:一切 崩溃和燃烧,我不知道为什么。因此,我会 简单地建议实施上述解决方法。

希望对您有所帮助。

我觉得这像是一个错误。代码在没有类型参数的情况下编译得很好,但是当你添加它时,编译就会意外失败。出于某种原因,在存在类型参数的情况下,javac 开始认为 Outer.Inner 的构造函数需要两个 Outer 类型的参数。因此,实际编译的代码是 o.new Inner<String>(o),由于方法类型错误,它在运行时失败。

反正在Java中我想不出还有什么方法可以做到,除了反射调用构造函数。这不是很优雅,但这样我们至少可以将正确的参数传递给 <init> 方法:

MethodHandle constructor = MethodHandles.lookup().findConstructor(Outer.Inner.class,
        MethodType.methodType(void.class, Outer.class));

Outer outer = new Outer();
Outer.Inner<String> inner = (Outer.Inner<String>) constructor.invokeExact(outer);

好像是存储泛型方法类型信息的signature属性引起的。它用于 compile-time 以检查是否使用正确的泛型参数调用方法,但据我所知它在运行时没有用。 Java 编译器似乎将隐式 outer 参数从该签名中移除,因此当 Scala 编译器执行相反操作时它会感到困惑。

Java 和 Scala 中的等效代码片段在具有相同描述符的同时产生不同的签名 (Ltest/Outer;Ljava/lang/Object;)V:

// Java
package test;
class Outer { class Inner<A> {
    public Inner(A a) {}
    // Signature: (TA;)V
}}

// Scala
package test
class Outer { class Inner[A](a: A) {
  // Signature: (Ltest/Outer;TA;)V
}}

找到一些相关的讨论:Java inner class inconsistency between descriptor and signature attribute? (class file)


引自 JVMS 4.7.9.1.:

A method signature encoded by the Signature attribute may not correspond exactly to the method descriptor in the method_info structure (§4.3.3). In particular, there is no assurance that the number of formal parameter types in the method signature is the same as the number of parameter descriptors in the method descriptor. The numbers are the same for most methods, but certain constructors in the Java programming language have an implicitly declared parameter which a compiler represents with a parameter descriptor but may omit from the method signature. See the note in §4.7.18 for a similar situation involving parameter annotations.

这可能不是最一致的答案,但似乎是指定的行为。