当我们在java中创建final时,它保证即使在运行时也不能更改,因为JVM保证了这一点。
Java类:
public class JustATest {
public final int x = 10;
}
Javap反编译:
编译自"JustATest.java"
public class JustATest {
public final int x;
public JustATest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field x:I
10: return
}
但是在scala中,如果我们声明一个val
,它会被编译成一个普通的整数,var和val在反编译输出方面没有区别。
原始Scala类:
class AnTest {
val x = 1
var y = 2
}
反编译输出:
Compiled from "AnTest.scala"
public class AnTest {
public int x();
Code:
0: aload_0
1: getfield #14 // Field x:I
4: ireturn
public int y();
Code:
0: aload_0
1: getfield #18 // Field y:I
4: ireturn
public void y_$eq(int);
Code:
0: aload_0
1: iload_1
2: putfield #18 // Field y:I
5: return
public AnTest();
Code:
0: aload_0
1: invokespecial #25 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #14 // Field x:I
9: aload_0
10: iconst_2
11: putfield #18 // Field y:I
14: return
}
有了这些信息,val
的不变性概念仅在编译时由scala编译器控制?在运行时如何保证这一点?
在Scala中,通过val
传递不变性是一个编译时强制执行,它与发出的字节码无关。在Java中,当字段为final
时,为了使其不被重新分配,您可以声明它,而在Scala中,用val
声明变量只意味着它不能被重新分配,但它可以被覆盖。如果您想要一个字段为final
,您需要像在Java中那样指定它:
class AnTest {
final val x = 10
}
收益率:
public class testing.ReadingFile$AnTest$1 {
private final int x;
public final int x();
Code:
0: bipush 10
2: ireturn
public testing.ReadingFile$AnTest$1();
Code:
0: aload_0
1: invokespecial #19 // Method java/lang/Object."<init>":()V
4: return
}
这相当于您在Java中看到的字节码,除了编译器为x
发出了一个getter。
真正的简单的答案是:有些Scala特性可以用JVM字节码编码,而有些则不能。
特别是,有一些约束不能在JVM字节码中编码,例如sealed
或private[this]
或val
。这意味着,如果您获得了Scala源文件的编译JVM字节码,那么您可以通过非Scala语言与代码交互来完成在Scala中无法完成的任务。
这不是JVM后端特有的,Scala.js也有类似的甚至更明显的问题,因为这里的编译目标(ECMAScript)提供的表达约束的方式比JVM字节码更少。
但实际上,这只是一个普遍的问题:我可以采用像Haskell这样安全纯净的语言,将其编译为本机代码,如果我得到编译后的二进制代码,所有的安全性都将失去。事实上,大多数Haskell编译器执行(几乎)完整的类型擦除,因此编译后实际上没有类型,也没有类型约束。