从 Java 访问元组的奇怪行为



我正在寻找关于我在 Java 中发现的访问在 Scala 中创建的元组的非常奇怪的行为的解释和/或版本控制细节(如果可能的话)。

我将通过我所做的简单测试来展示奇怪的行为。 我创建了这个 Scala 类:

class Foo {
def intsNullTuple = (null.asInstanceOf[Int], 2)
def intAndStringNullTuple =  (null.asInstanceOf[Int], "2")
}

然后我运行这个 Java 程序:

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

有人对此有任何解释吗?此外,在我的测试中,我使用的是Java 1.8和Scala 2.11.8。任何人都可以提供任何关于使用Java代码中的_1与较旧的Scala 2.11和2.10版本以及Java 1.7的兼容性的建议吗?我读到_1无法从 Java 访问,但我可以在测试中访问它。因此,我正在寻找支持它的版本。

谢谢。

有人对此

有任何解释吗?

这是因为Scala对Tuple2<Int, Int>的重载有专门的研究,而Tuple2<Int, String>则没有。你可以从Tuple2的签名中看到它:

case class Tuple2[@specialized(Int, Long, Double, Char, Boolean/*, AnyRef*/) +T1, @specialized(Int, Long, Double, Char, Boolean/*, AnyRef*/) +T2](_1: T1, _2: T2)

这意味着 Scala 编译器在特殊情况下发出一个类,其中T1T2是专门的元组类型之一,在我们的示例中,有一个特殊的类需要两个整数,大致如下所示:

class Tuple2Special(i: Int, j: Int)

我们在查看反编译字节码时可以看到这一点:

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
}

intsNullTuple的情况下,你可以看到new操作码调用Tuple2$mcII$sp,这是专用版本。这就是你调用_1()产生0的原因,因为这是值类型Int的默认值,而_1不是专用的,调用重载返回一个Object,而不是Int

这也可以通过scalac在使用-Xprint:jvm标志进行编译时查看:

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

另一个有趣的事实是,Scala 2.12 改变了行为,并intAndStringNullTuple打印0

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

收益 率:

t1 method: 0
t1 field: null
t2 method: 0
t2 field: 0

从现在开始,null通过unboxToInt转换为0,并通过boxToInteger包装在Integer实例中。

编辑:

在与Lightbend的相关人员交谈后,这是由于在2.12中对字节码生成器(后端)进行了返工(有关更多信息,请参阅 https://github.com/scala/scala/pull/5176)。

首先,需要指出,在 Scala 中,一切都是对象,没有不像 Java 那样的原始类型(对于你的代码来说,它是Int),但 Scala 需要编译成Java 字节码才能在JVM中运行,因为Object基元类型消耗更多的内存,所以Scala专门解决了这个问题,这意味着生成基元类型参数方法(当用类型specialized批注时)。

所以对于你的代码,它是Tuple2,它是专门为Int, Long, Double, Char, Boolean的。 这将生成相应的基元类型构造函数,如下所示:

Tuple2(int _v1, int _v2) --> `Tuple2$mcII$sp`
Tuple2(long _v1, long _v2) 
...

还有一件事需要清除,那就是BoxUnBox,这意味着编译器将决定变量是否需要在编译时将其转换为基元类型或将变量转换为Object,找到更多 BoxesRunTime

有关intsNullTuple,请参阅字节码:

scala>:javap -c Foo
public scala.Tuple2<java.lang.Object, java.lang.Object> intsNullTuple();
Code:
0: new           #17                 // class scala/Tuple2$mcII$sp
3: dup
4: aconst_null
5: invokestatic  #23                 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
8: iconst_2
9: invokespecial #27                 // Method scala/Tuple2$mcII$sp."<init>":(II)V
12: areturn

正如你所看到的上面的代码,编译器已经决定通过BoxesRunTime.unboxToInt将对象拆箱int,这返回了一个原始类型int. so it's actually will invokeTuple2$mcII$sp(int _1, int _2)'。

有关intAndStringNullTuple,请参阅字节码:

public scala.Tuple2<java.lang.Object, java.lang.String> intAndStringNullTuple();
Code:
0: new           #32                 // class scala/Tuple2
3: dup
4: aconst_null
5: invokestatic  #23                 // Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
8: invokestatic  #36                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
11: ldc           #38                 // String 2
13: invokespecial #41                 // Method scala/Tuple2."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
16: areturn

你也可以看到最后它已经boxToInteger到一个Object,它实际上会调用Tuple2(Object _1, Object _2)

以及为什么_1()返回0_1返回null,因为Java泛型只支持Object类型,Tuple2<Object, Object>,当你调用_1()时,它是实际调用java.lang.Object _1(),它等于调用public int _1$mcI$sp();

scala> :javap -c scala.Tuple2$mcII$sp
Compiled from "Tuple2.scala"
public final class scala.Tuple2$mcII$sp extends scala.Tuple2<java.lang.Object, java.lang.Object> implements scala.Product2$mcII$sp {
public final int _1$mcI$sp;
public final int _2$mcI$sp;
public int _1$mcI$sp();
Code:
0: aload_0
1: getfield      #14                 // Field _1$mcI$sp:I
4: ireturn
...
public java.lang.Object _1();
Code:
0: aload_0
1: invokevirtual #33                 // Method _1:()I
4: invokestatic  #56                 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer;
7: areturn

所以_1()会返回0.

对于直接_1,它是Tuple2<Object, Object>字段的实际访问,因为它是Object,所以它应该是null

scala> :javap -c scala.Tuple2
Compiled from "Tuple2.scala"
public class scala.Tuple2<T1, T2> implements scala.Product2<T1, T2>, scala.Serializable {
public final T1 _1;
public final T2 _2;

最后,所以据我了解,既然盒子拆盒子都有专门的,我们需要始终尝试调用_1()而不是_1

最新更新