为什么非静态内部类中不能有 static 成员变量却可以有 static final 属性的编译期常量

转载自微信公众号 码农每日一题

问:为什么非静态内部类中不能有 static 成员变量却可以有 static final 属性的编译期常量?
答:这是一个陷阱题,看起来似乎很简单,实际是一箭双雕,即考察了非静态内部类相关知识,还考察了 final 的各种常量分类细则(很多人回答时会疏忽这个点)。

由于 Java 中非静态内部类默认持有了外部类引用,也就是说可以将它看成是其外部类的一个成员,所以其必须跟外部类实例相关联才能初始化。而静态成员是属于类层次的,是不需要类实例就可以初始化访问的。此时如果假设让一个非静态内部类拥有了静态变量,则其应该可以不依托于任何外部类实例就能访问,而非静态内部类却没法做到不实例化其外部类而使用,所以这种设计从语法层面就是互斥的。

而对于 static final 成员来说就不一样了,至于为什么不一样我们需要先切换补充一些知识点再说。我们先看下面程序段:

public class Outer {
    class Inner {
        static int t1 = 100; //编译错误
        static final int t2 = 100; //编译成功
        static final int t3 = new Integer(100); //编译错误
    }

    public static void main(String[] args) {
        System.out.println(Outer.Inner.t1);
        System.out.println(Outer.Inner.t2);
        System.out.println(Outer.Inner.t3);
  }
}

上面程序片段分别编译运行的结果在注释部分已经给出了,在我们挨个解释上面语句现象前先说说 Java 对常量的一些定义和处理机制。

对于 Java 中的常量其实可以分为编译期常量和非编译期常量。编译期常量指的是在程序编译阶段(不需要加载类的字节码)就可以确定具体值的常量,其中会涉及到编译期常量折叠(编译器可以语法分析计算出值的常量表达式进行计算取值)。非编译期常量(运行期常量)指的是在程序运行阶段(需要加载类的字节码)才可以确定具体值的常量(编译期无法折叠,编译器能做的只是对所有可能修改它的地方进行检查和报错)。

当我们通过类名访问被 static final 修饰的常量时,如果该常量是编译期常量则该类不会被 JVM 加载,如果该常量是非编译期常量则该类会被 JVM 加载。当通过类名访问被 static 修饰的变量时都会触该类被 JVM 加载。

有了上面这个概念我们再来分析上面代码段的原因。由于 Java 类属性的初始化顺序为(静态变量、静态初始化块)>(变量、初始化块)> 构造器,所以 JVM 要求所有的静态属性必须在类对象创建之前完成初始化。故对于 t1 属性调用处来说,必须要等到外部类 Outer 实例化之后(即有创建一个外部类对象)JVM 才能加载其内部类 Inner 的字节码,而我们调用处并没有对外部类进行实例化,所以内部类 Inner 也不会被加载,而 t1 是 static 属性,要初始化 t1 就必须先加载内部类的字节码;而 t3 是非编译期常量,要初始化它们也必须加载内部类的字节码才能确定它们的值;只有 t2 是编译时常量,所以不会触发内部类加载机制;故而有了上面代码段的结果。

所以说非静态内部类中不能有 static 成员变量却可以有 static final 属性的编译期常量而不能有 static final 属性的运行期常量。

由此又引出了一个新的面试题和代码优化小技巧。

为什么我们在类中定义 static 修饰的 String 常量时经常会在它前面加上 final 修饰符?因为这样可以使 JVM 不必加载该类的类体就能直接使用其值从而节省了内存空间。

猜你喜欢

转载自blog.csdn.net/tianhongyan1122/article/details/81836867
今日推荐