Java中Lambda访问私有final成员的编译异常及原因问询
在OpenJDK 11环境下,当类的私有Lambda成员直接访问构造方法中初始化的私有final成员时,会出现编译错误:variable num might not have been initialized,错误代码示例如下:
public class Main { private final int num; public Main() {num = 7;} private java.util.function.Supplier getNum = () -> num; public void printNum() { System.out.println(getNum.get()); } public static void main(String[] args) { Main main = new Main(); main.printNum(); } }
但以下几种代码写法可以正常编译运行,下面逐一解释每种方案的原理:
可行方案及原理
i. 将Lambda定义移至方法内部
public class Main { private final int num; public Main() {num = 7;} private java.util.function.Supplier getNum; public void printNum() { getNum = () -> num; System.out.println(getNum.get()); } public static void main(String[] args) { Main main = new Main(); main.printNum(); } }
原理:类成员变量的初始化顺序是先初始化所有非static成员(包括Lambda成员),再执行构造方法。原错误代码中,Lambda成员getNum在构造方法执行前就已初始化,此时num还未被赋值,编译器无法保证Lambda访问num时它已完成初始化。而把Lambda的赋值移到方法内部后,方法调用必然发生在构造方法执行完成之后,此时num已经被正确初始化,编译器可以确认num的赋值状态,因此不会报错。
ii. 方法内定义局部Lambda
public class Main { private final int num; public Main() {num = 7;} public void printNum() { java.util.function.Supplier getNum = () -> num; System.out.println(getNum.get()); } public static void main(String[] args) { Main main = new Main(); main.printNum(); } }
原理:和方案i逻辑一致,局部Lambda是在对象构造完成后调用方法时才定义的,此时num已经通过构造方法完成初始化。编译器可以明确判断,Lambda访问num时这个final成员已经被赋值,不存在未初始化的风险,因此允许编译。
iii. 在类成员前添加this并将this强转为类名
public class Main { private final int num; public Main() {num = 7;} private java.util.function.Supplier getNum = () -> ((Main) this).num; public void printNum() { System.out.println(getNum.get()); } public static void main(String[] args) { Main main = new Main(); main.printNum(); } }
原理:这种写法通过强制类型转换改变了编译器的分析逻辑。原错误场景中,编译器直接分析num的初始化时机,发现Lambda初始化时num还未赋值;而通过((Main) this).num的写法,编译器会将其视为访问当前对象的成员变量,而对象的final成员在构造完成后必然会被初始化(语言规范要求final成员必须在构造方法结束前完成赋值)。这种转换让编译器跳过对num初始化时机的直接检查,转而依赖对对象构造完整性的判断,因此通过编译。
iv. 在构造方法中初始化Lambda
public class Main { private final int num; private final java.util.function.Supplier getNum; public Main() {num = 7; getNum = () -> num;} public void printNum() { System.out.println(getNum.get()); } public static void main(String[] args) { Main main = new Main(); main.printNum(); } }
原理:构造方法内的代码执行顺序是先给num赋值,再初始化Lambda。此时Lambda被创建时,num已经完成赋值,编译器可以明确看到num的初始化发生在Lambda之前,因此确认Lambda访问num时它已经是有效状态,不会触发未初始化的错误。同时这种方式还能保证Lambda作为类成员,可在多个方法间共享。
该问题是否为编译器特定问题?
这不是编译器特定问题,而是符合Java语言规范的行为。Java语言规范要求,final成员变量必须在构造方法结束前完成初始化;而类成员变量的初始化顺序是在构造方法执行之前。当Lambda作为类成员初始化时,它访问的final成员此时还未被构造方法赋值,编译器必须严格检查这种情况并报错,这是语言规范的要求,而非特定编译器的实现问题。
内容的提问来源于stack exchange,提问作者user1589188

