Java里局部变量和成员变量的隐式初始化

注:本文是对另一篇文档( https://blog.csdn.net/duke_ding2/article/details/142365872 )的补充。

环境

  • Windows 11 专业版
  • Java 21

初始化

局部变量(栈)

局部变量不会自动初始化,所以必须要先赋值,再使用。

    public static void main(String[] args) {
    
    
        int n1 = 0;
        int n2;
        System.out.println("n1: " + n1);
        System.out.println("n2: " + n2); // 编译错误:变量没有初始化

        boolean b1 = false;
        boolean b2;
        System.out.println("b1: " + b1);
        System.out.println("b2: " + b2); // 编译错误:变量没有初始化

        Object o1 = null;
        Object o2;
        System.out.println("o1: " + o1);
        System.out.println("o2: " + o2); // 编译错误:变量没有初始化
    }

注意:在Java 21之前(比如Java 17),对于引用类型的局部变量(比如上例中的 o2 ),是会自动初始化为null的。Java 21引入了更加严格的限制,所有局部变量都必须先赋值,再使用

成员变量(堆)

成员变量会自动初始化为 0falsenull 等。

public class Test1212_31 {
    
    
    private int n1;
    private boolean b1;
    private Object o1;

    public static void main(String[] args) {
    
    
        var obj = new Test1212_31();
        System.out.println(obj.n1);
        System.out.println(obj.b1);
        System.out.println(obj.o1);
    }
}

输出结果如下:

0
false
null

其它

数组

在Java里,数组是对象,所以,创建数组后,数组里的元素可以隐式的初始化( 0falsenull 等)。

数组对象是在堆上创建的,可以把数组里的元素想象成类似数组对象的成员变量。

(注意:Java的数组是定长的,一旦创建了数组对象,其长度就不能变化了。当然,对于多维数组,只有最高维是不能变化的。)

    public static void main(String[] args) {
    
    
        int[] arr1 = new int[3];
        for (int n : arr1)
            System.out.println(n);

        boolean[] arr2 = new boolean[3];
        for(boolean b : arr2)
            System.out.println(b);
        
        Object[] arr3 = new Object[3];
        for (Object o : arr3)
            System.out.println(o);
    }

输出结果如下:

0
0
0
false
false
false
null
null
null

分析

局部变量不会自动初始化,成员变量会自动初始化。为什么要如此设计呢?

安全性

“先赋值,再使用”是一个通用的法则,通常我们也是这么做的,很少出现不赋值就使用的情况。

如果不清楚语言特性,那么“先赋值,再使用”显然是一个good practice。如果不赋值就使用,那么假如没有隐式初始化,则可能会出现无法预料的结果(因为变量的值无法预料)。通过强制程序员初始化变量,Java编译器可以在编译阶段就发现这些问题,提高代码的质量和安全性。

性能

我们知道,局部变量是在栈(stack)里的,其特点是生命周期较短,而调用较频繁,比如:

    public void foo(int arg1) {
    
    
        int n1;
        n1 = arg1 * 2;
        ......
    }

foo() 方法可能会被频繁调用,如果 n1 隐式初始化为 0 ,显然在频繁调用 foo() 方法时,其开销会更大(而且一般也没有必要)。

再看另外一个例子:

    public void bar() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            int temp;
            temp = i * 2;
            ......
        }
    }

temp 变量是在循环内部定义的,和上一个例子类似,在循环次数很多时,隐式初始化的开销会更大(而且一般也没有必要)。

成员变量 VS. 局部变量

我们知道,对象是在堆(heap)上创建,所以,对象的成员变量也是在堆上的。对象的特点(相对于局部变量)是:

  • 数量较少
  • 生命周期较长

可能是为了帮助程序员确保对象的一致性和完整性,编译器对成员变量进行了隐式初始化。由于数量相对比较少,隐式初始化的开销也不算特别大。

如果没有隐式初始化,程序员很可能需要在对象的构造方法里对成员变量做初始化。隐式初始化使得代码更加简洁,程序员可以集中精力在更重要的事情上面。

总结

  • 局部变量(在栈上创建)没有隐式初始化,必须先赋值,再使用
  • 成员变量(在堆上创建)会隐式初始化( 0falsenull 等),可以直接使用