从Java访问元组的奇怪行为

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

我将用一个简单的测试来展示奇怪的行为。 我创建了这个Scala类:

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

然后我运行这个Java程序:

 Tuple2 t = (new Foo()).intsNullTuple(); t._1(); // returns 0 ! t._1; // return null Tuple2 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的重载Tuple2 ,而Tuple2则没有。 你可以从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 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."":(II)V 12: areturn public scala.Tuple2 intAndStringNullTuple(); Code: 0: new #27 // class scala/Tuple2 3: dup 4: aconst_null 5: ldc #29 // String 2 7: invokespecial #32 // Method scala/Tuple2."":(Ljava/lang/Object;Ljava/lang/Object;)V 10: areturn public com.testing.Foo(); Code: 0: aload_0 1: invokespecial #35 // Method java/lang/Object."":()V 4: return } 

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

使用-Xprint:jvm标志进行编译时, -Xprint:jvm也可以查看scalac

 λ 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 (): com.testing.Foo = { Foo.super.(); () } } } 

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

 public scala.Tuple2 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."":(Ljava/lang/Object;Ljava/lang/Object;)V 16: areturn 

产量:

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

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

编辑:

在与Lightbend的相关人员交谈之后,发生了这种情况,原因是在2.12中对字节码生成器(后端)进行了返工(更多信息请参见https://github.com/scala/scala/pull/5176 )。

首先,需要调用,在Scala中 ,一切都是Object ,没有原始类型 (对于你的代码,它是Int )不像Java ,但是Scala需要编译到Java Bytecode以在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 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."":(II)V 12: areturn 

正如您可以看到上面的代码,编译器决定通过BoxesRunTime.unboxToInt将对象解包为int ,这将返回一个基本类型int. so it's actually will invoke int. so it's actually will invoke Tuple2 $ mcII $ sp(int _1,int _2)`。

对于intAndStringNullTuple ,请参阅字节码:

  public scala.Tuple2 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."":(Ljava/lang/Object;Ljava/lang/Object;)V 16: areturn 

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

并且为什么_1()返回0但是_1返回null ,因为Javagenerics只支持Object类型, Tuple2 ,当你调用_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 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 ,所以它应该为null

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

最后,所以对于我的理解,由于boxunbox都是专门的 ,我们需要总是尝试调用_1()而不是_1