★ 类常量
Java类常量(final修饰)的值在编译阶段就已经写入了class文件的常量池中。
该类在JVM中运行的任何阶段都不可能改变这个常量值。这也就形成了Java常量定义的两个语法规定:(1) Java类常量必须在类中声明的时候就赋给特定的值。(2) Java类常量绝对不允许赋第二次值。
下面是Java类常量的初始化代码:
1 //代码1: 类常量的初始化
2 public class Init{
3 //定义类常量ITEM,声明的同时必须赋值
4 public static final int ITEM=100;
5
6 public static void main(String[] args){
7 //Init.ITEM=200; //编译错误, 常量值不允许在程序运行中改变
8 }
9 }
如果你了解JVM。就知道上面的Init类被JVM加载之后,会在方法区(内存)中开辟一块存放Init类信息的空间。其中的常量池数据结构中就存放了ITEM常量值100。因此,被final修饰的常量并不是存放于某个对象在堆中的内存
其中的常量池数据结构中就存放了ITEM常量值100。因此,被final修饰的常量并不是存放于某个对象在堆中的内存。
记住, final类常量在编译阶段就已经有确定的值,并不需要在JVM虚拟机加载之后才初始化。这也是唯一一种不需要运行就已经初始化的Java数据值。
★ 类静态变量/类静态初始化块
类静态变量是用static修饰的类变量(不用final)。在编译源代码阶段,任何对类静态变量定义的值都不会作为常量值存在于常量池中。看看下面两句定义。
public static final int ITEM=100;
public static int itemI=200;
常量池中只会存储整型值100,而不会记录200。因此在编译阶段,鬼才知道itemI的值是多少。只有当JVM装载class文件的完成之后,会为所有的类静态变量开辟内存空间(在方法区中),然后在这些内存空间中赋予默认值
(比如int默认值是0,boolean默认值是false,引用类型默认值是null等)。注意,此时类静态变量的内存空间里是默认值,而并非我们在源代码中的赋值。这也就是为什么下面的代码是正确的:
1 // 类静态变量的默认值
2 public class Init{
3 //类整型静态变量
4 public static int itemI;
5
6 public static void main(String[] args){
7 System.out.println(Init.itemI); //打印结果: 0
8 }
9 }
赋予默认值之后,接下来就是对类静态变量的初始化我们想要的值了(如下面代码2)。
1 //代码2:类静态变量的初始化
2 public class Init{
3 //将类静态变量itemI初始化为200
4 public static int itemI=200;
5
6 public static void main(String[] args){}
7 }
对于虚拟机而言,对类静态变量的初始化实质上是执行一段名字叫做“<clinit>”的方法。该方法字节码指令如下:
1 0 bipush 200 //将整型值200压入操作数栈
2 2 putstatic itemI : int [10] //解析常量池中符号引用"itemI"的入口地址为直接地址(JVM为itemI类静态变量开辟的在方法区的内存空间的地址)。然后弹出操作数栈的栈顶数据100,存储在这个直接地址上。
3 5 return
另外,类静态变量只初始化一次,换句话说就是只在方法区中开辟一次空间。且只在JVM首次主动使用类型的时候才初始化。我们认为只有6种活动属于主动使用类型:
(1) 指定的类作为JVM启动时的初始化类。也就是JVM开始运行程序时,最先加载的包含main()方法的类。
(2) 创建类的实例,比如最常见的new方法等。
(3) 调用类中声明的静态方法。
(4) 操作类或接口中声明的非常量静态字段。
(5) 调用Java API中特定的反射方法。
(6) 初始化指定类的子类。实际上,在初始化当前类之前,JVM会将这个类的所有父类全部加载、链接和初始化。然后再初始化当前类。
★ 编译时常量
对于final类型的静态变量,如果在编译时就能计算出变量的取值,那么这种变量被看做编译时常量。java程序中对类的编译时常量的使用,被看做是对类的被动使用,不会导致类的初始化。
1 public class TestTwo
2 {
3
4 public static void main(String[] args)
5 {
6 System.out.println(Tester.a); //打印6
7 }
8
9 }
10 class Tester{
11 public static final int a = 2*3; //变量a是编译时常量
12 // public static final int a = int(Math.random()*5); //变量a不是编译时常量
13 static{
14 System.out.println("init tester");
15 }
16 }
当java编译器生成TestTwo类的.class文件时,它不会再main()方法的字节码流中保存一个表示“Tester.a”的符号引用,而是直接在字节码流中嵌入常量值6.因此在程序访问“Tester.a”时,客观上无须初始化Tester类。
当java虚拟机加载并连接Tester类时,不会再方法区内为它的编译时常量a分配内存。
记住: 类静态变量只初始化一次,且在首次主动使用类的时候才被初始化。JVM初始化类静态变量实际上是调用了<clinit>方法(在编译阶段偷偷生成,并不是源代码中的某个方法)。类静态变量在内存中的存储空间属于方法区,并不属于某个对象的堆空间内。变量值的改变会影响所有类的对象。
事实上类静态初始化块的初始化过程和类静态变量基本一样。对于类静态初始化块中的语句,编译器也会将其编译成<clinit>方法中的指令。
★ 类对象的初始化(类实例化)
类静态变量的初始化完成也标志着该类已经被JVM成功的装载、连接和类初始化了。接下来这个类可以随时被调用,程序可以访问它的静态字段和方法,也可以创建它的对象 —— 类实例化。
在Java程序中,类实例化有四种途径:
(1) new 指令;
(2) 调用Class对象的newInstance()方法;
(3) 调用任何现有对象的clone()方法;
(4) 通过ObjectInputStream类的getObject()方法反序列化。
除此四种初始化方法,JVM在装载类的同时还会隐含的创建两种对象:(1) 实例化一个Class对象代表类型信息;(2) 装载常量池中的CONSTANT_String_info入口的类的时候,创建拘留字符串对象。
当JVM创建一个类对象时,首先在堆中分配一个内存空间用来存放对象所属类和超类的实例变量(类中非常量非静态变量),然后对这些变量赋默认值(与类静态变量类似),最后初始化我们需要的值。我们有三种方法对类对象的实例变量进行初始化:(1) 在声明中赋值,(2) 在构造器中设置值,(3) 在初始化块中赋值。
JVM初始化这些变量都是通过调用一个名叫<init>的方法。实际上针对每一个类的构造器,都会产生一个对应的<init>()方法。