在Java中,static和final是使用频率非常高的关键字,之前也简单地用过static和final,但是始终觉得没有从深层次上理解这两个关键字。本文将从class文件和类的初始化机制出发理解这两个关键字。
一、编译时常量:
很多人包括我最开始有这种误解,是不是被final修饰的变量就是编译时常量呢,非也。顾名思义,编译时常量是指变量在编译期间就可以确定了,下面我举两个例子说明一下什么是编译时常量。
- public class Test{
- public finalint a=100;
- }
public class Test{
public final int a=100;
}
看一下,编译之后的class文件:
此时,变量a是编译时常量,在构造方法中,对变量a分配内存空间之后,将常量111赋值给a。
- public class Test{
- public finalint a;
- public Test(int a){
- this.a=a;
- }
- }
public class Test{
public final int a;
public Test(int a){
this.a=a;
}
}
编译之后的class文件如下:
此时,变量a在编译时期并不知道要赋什么值,只有在运行期间实例化Test对象时,去调用构造方法的时候才能知道a的值。这时候a就不是编译时常量。
二、static变量
static修饰的变量为类(静态)变量,static成员变量是对于整个类而不是类实例而存在的。一个static变量对于每个类来说都只有一份存储空间。而非static变量(实例变量)对于每个对象都有一份存储空间。
在我们实例化一个Java对象之前,这个对象的类型信息必须已经存放在了Java虚拟机的方法区。Java虚拟机通过装载、连接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。其中装载就是把二进制的class文件读入Java虚拟机中;而连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时数据区中,连接分为三个子步骤:验证、准备和解析。验证工作是确认class文件符合Java语言的语义规范。在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。解析过程是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用。最后一个步骤就是初始化,也就是为类变量赋予正确的初始值。
下面我们来看一下类(静态)变量的初始化过程。
所有的类变量初始化语句和类型的静态初始化器都被Java编译器收集在一起,放到一个特殊的方法。在class文件中,这个方法被称为"<clinit>"。通常的java程序时无法调用这个<clinit>方法。这个方法只能被java虚拟机调用,专门用把类型的静态变量设置为它们的正确初始值。以下两种方式产生的字节码文件时相同的。
- public class Test{
- public staticint a=111;
- }
public class Test{
public static int a=111;
}
- public class Test{
- public staticint a;
- static{
- a=111;
- }
- }
public class Test{
public static int a;
static{
a=111;
}
}
并非所有的类都在class文件中拥有一个<clinit>()方法。如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有<clinit>()方法。若果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句初始化它们,那么类不会有<clinit>()方法。如果类仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有<clinit>()方法。在编译的时候,java编译器把编译时常量(用final声明以及编译时已知的值初始化的类变量)则和一般的类变量的处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码中。也就是Java虚拟机会将常量直接保存到类文件中。
- public class Test{
- public staticfinal int a=11;
- }
public class Test{
public static final int a=11;
}