请注意,对于作为编译时常量的变量的任何普通 Java 访问,都会发生常量值,因此,与某些人声称的不同,它不受初始化顺序问题的影响。
我们可以通过下面的例子来证明这一点:
abstract class Base {
Base() {
// bad coding style don't do this in real code
printValues();
}
void printValues() {
System.out.println("var1 read: "+getVar1());
System.out.println("var2 read: "+getVar2());
System.out.println("var1 via lambda: "+supplier1().get());
System.out.println("var2 via lambda: "+supplier2().get());
}
abstract String getVar1();
abstract String getVar2();
abstract Supplier<String> supplier1();
abstract Supplier<String> supplier2();
}
public class ConstantInitialization extends Base {
final String realConstant = "a constant";
final String justFinalVar; { justFinalVar = "a final value"; }
ConstantInitialization() {
System.out.println("after initialization:");
printValues();
}
@Override String getVar1() {
return realConstant;
}
@Override String getVar2() {
return justFinalVar;
}
@Override Supplier<String> supplier1() {
return () -> realConstant;
}
@Override Supplier<String> supplier2() {
return () -> justFinalVar;
}
public static void main(String[] args) {
new ConstantInitialization();
}
}
打印出来:
var1 read: a constant
var2 read: null
var1 via lambda: a constant
var2 via lambda: null
after initialization:
var1 read: a constant
var2 read: a final value
var1 via lambda: a constant
var2 via lambda: a final value
因此,如您所见,在执行超级构造函数时尚未发生对 realConstant 字段的写入这一事实,即使通过访问它,也不会看到真正的编译时常量的未初始化值拉姆达表达式。从技术上讲,因为该字段实际上并没有被读取。
此外,出于同样的原因,讨厌的反射黑客对普通 Java 对编译时常量的访问没有影响。读取这种修改后的值的唯一方法是通过反射:
public class TestCapture {
static class MyClass {
final String foo = "foo";
private Consumer<String> getFn() {
//final String localFoo = foo;
return bar -> System.out.println("lambda: " + bar + foo);
}
}
public static void main(String[] args) throws ReflectiveOperationException {
final MyClass obj = new MyClass();
Consumer<String> fn = obj.getFn();
// change the final field obj.foo
Field foo=obj.getClass().getDeclaredFields()[0];
foo.setAccessible(true);
foo.set(obj, "bar");
// prove that our lambda expression doesn't read the modified foo
fn.accept("");
// show that it captured obj
Field capturedThis=fn.getClass().getDeclaredFields()[0];
capturedThis.setAccessible(true);
System.out.println("captured obj: "+(obj==capturedThis.get(fn)));
// and obj.foo contains "bar" when actually read
System.out.println("via Reflection: "+foo.get(capturedThis.get(fn)));
// but no ordinary Java access will actually read it
System.out.println("ordinary field access: "+obj.foo);
}
}
打印出来:
lambda: foo
captured obj: true
via Reflection: bar
ordinary field access: foo
这向我们展示了两件事,
- 反射对编译时常量也没有影响
- 周围的物体已被捕获,尽管它不会被使用
我很乐意找到这样的解释,“对实例字段的任何访问都需要 lambda 表达式来捕获该字段的实例(即使该字段实际上并未被读取)” ,但不幸的是,我在当前的 Java 语言规范中找不到关于捕获值或 this 的 任何 语句,这有点可怕:
我们已经习惯了这样一个事实:不访问 lambda 表达式中的实例字段将创建一个没有对 this 的引用的实例,但即使这样实际上也不能保证当前规范。重要的是要尽快修复这个遗漏……