Reputation: 33
I'm trying to instantiate a nested generic Scala class from Java and running into this compilation error. Can someone help? Thanks
class Outer {
class Inner[A]
}
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
Upvotes: 3
Views: 704
Reputation: 44918
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:
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 - given
s/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 superImportantMethod
s 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.
Upvotes: 2
Reputation: 3863
This looks like a bug to me. The code compiles fine without the type parameter, but when you add it, compilation just fails unexpectedly. For some reason, with the type parameter present, javac starts to think that the constructor of Outer.Inner
requires two parameters of type Outer
. And for that reason the code that actually compiles is o.new Inner<String>(o)
, which fails at runtime because of the wrong method type.
Anyway, I couldn't think of any other method to do it in Java besides reflectively calling the constructor. It's not very elegant but that way we can at least pass the correct parameters to the <init>
method:
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);
Seems to be caused by the signature attribute that stores the generic method type information. It's used at compile-time to check that methods are called with correct generic arguments, but it has no use at runtime as far as I know. Java compiler seems to leave the implicit outer
parameter out from this signature so it gets confused when Scala compiler does the opposite.
Equivalent pieces of code in Java and Scala produce different signatures while having the same descriptor (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
}}
Found some related discussion: Java inner class inconsistency between descriptor and signature attribute? (class file)
Quote from 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.
This might not be the most consistent answer but seems like this is specified behavior.
Upvotes: 1
Reputation: 13473
Can you add a generic method to Outer
to instantiate Inner
instances?
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();
}
}
Upvotes: 0