I am looking for explanation and/or versioning details (if possible) about a very strange behavior I found in Java accessing tuples created in Scala.
I will show the weird behavior with an easy test I did. I created this Scala class:
class Foo {
def intsNullTuple = (null.asInstanceOf[Int], 2)
def intAndStringNullTuple = (null.asInstanceOf[Int], "2")
}
and then I run this Java program:
Tuple2<Object, Object> t = (new Foo()).intsNullTuple();
t._1(); // returns 0 !
t._1; // return null
Tuple2<Object, String> t2 = (new Foo()).intAndStringNullTuple();
t._1(); // returns null
t._1; // return null
does anybody have any explanation on the reason of this? Moreover, in my tests I am using Java 1.8 and Scala 2.11.8. Can anyone provide any suggestion about the compatibility of using _1
from Java code also with older Scala 2.11 and 2.10 versions and Java 1.7? I read that _1
is not accessible from Java, but I can access it in my tests. Thus I am looking for the versions which support it.
Thanks.
does anybody have any explanation on the reason of this?
This is due to the fact that Scala has a specialization for the overload of Tuple2<Int, Int>
, while Tuple2<Int, String>
doesn't. You can see it from the signature of Tuple2
:
case class Tuple2[@specialized(Int, Long, Double, Char, Boolean/*, AnyRef*/) +T1, @specialized(Int, Long, Double, Char, Boolean/*, AnyRef*/) +T2](_1: T1, _2: T2)
This means that the Scala compiler emits a class for the special case where T1
and T2
are one of the specialized tuple types, in our example there is a special class taking two ints, roughly like this:
class Tuple2Special(i: Int, j: Int)
We can see this when looking at the decompiled byte code:
Compiled from "Foo.scala"
public class com.testing.Foo {
public scala.Tuple2<java.lang.Object, java.lang.Object> intsNullTuple();
Code:
0: new #12 // class scala/Tuple2$mcII$sp
3: dup
4: aconst_null
5: invokestatic #18 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
8: iconst_2
9: invokespecial #22 // Method scala/Tuple2$mcII$sp."<init>":(II)V
12: areturn
public scala.Tuple2<java.lang.Object, java.lang.String> intAndStringNullTuple();
Code:
0: new #27 // class scala/Tuple2
3: dup
4: aconst_null
5: ldc #29 // String 2
7: invokespecial #32 // Method scala/Tuple2."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
10: areturn
public com.testing.Foo();
Code:
0: aload_0
1: invokespecial #35 // Method java/lang/Object."<init>":()V
4: return
}
In the case of intsNullTuple
, you see that the new
opcode calls Tuple2$mcII$sp
, which is the specialized version. That is the reason your call to _1()
yields 0
, because that's the default value for the value type Int
, while _1
isn't specialized and calls the overload returning an Object
, not Int
.
This can also be viewed by scalac
when compiling with the -Xprint:jvm
flag:
λ scalac -Xprint:jvm Foo.scala
[[syntax trees at end of jvm]] // Foo.scala
package com.testing {
class Foo extends Object {
def intsNullTuple(): Tuple2 = new Tuple2$mcII$sp(scala.Int.unbox(null), 2);
def intAndStringNullTuple(): Tuple2 = new Tuple2(scala.Int.box(scala.Int.unbox(null)), "2");
def <init>(): com.testing.Foo = {
Foo.super.<init>();
()
}
}
}
Another interesting fact is that Scala 2.12 changed the behavior, and makes intAndStringNullTuple
print 0
instead:
public scala.Tuple2<java.lang.Object, java.lang.String> intAndStringNullTuple();
Code:
0: new #27 // class scala/Tuple2
3: dup
4: aconst_null
5: invokestatic #18 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
8: invokestatic #31 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
11: ldc #33 // String 2
13: invokespecial #36 // Method scala/Tuple2."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
16: areturn
Yields:
t1 method: 0
t1 field: null
t2 method: 0
t2 field: 0
Since now null
gets transformed to 0
via unboxToInt
and wrapped inside an Integer
instance via boxToInteger
.
Edit:
After talking to the relevant people at Lightbend, this happened due to the rework done in 2.12 for the bytecode generator (backend) (see https://github.com/scala/scala/pull/5176 for more).