浅谈java中final数据
引言在说说final之前,我们先了解下类被加载到内存中所需要的几个步骤,一个类被加载到内存中需要经过如下几个阶段:编译:java文件必须编译成Class文件(也称为字节码文件)才可以被JVM识别,JVM并不关心Class的来源是什么语言,只要它符合一定的结构,就可以在Java中运行。装载:查找和导入必要的Class文件,在Java虚拟机执行过程中,只有他需要一个类的时候,才会调用类加载器来加
引言
在说说final之前,我们先了解下类被加载到内存中所需要的几个步骤,一个类被加载到内存中需要经过如下几个阶段:
编译: java文件必须编译成Class文件(也称为字节码文件)才可以被JVM识别,JVM并不关心Class的来源是什么语言,只要它符合一定的结构,就可以在Java中运行。
装载:查找和导入必要的Class文件,在Java虚拟机执行过程中,只有他需要一个类的时候,才会调用类加载器来加载这个类,并不会在开始运行时加载所有的类。
链接: 检查载入Class文件数据的正确性、静态变量分配内存空间,并设置默认值、将符号引用转成直接引用
初始化:对类的静态变量,静态代码块执行初始化操作
了解完成上面后,我们进入final,一个被final修饰的数据会有两种情况的存在:
- 永不改变的编译时常量
- 在运行时被初始化的值
一、编译时的常量
对于编译时常量这种情况就在于编译成Class文件后,常量的值就已经存在于编译时常量池中了。当我们使用该常量时,类就不需要为它进行任何初始化操作,我们直接拿来用就行了。要成为编译时的常量必须要满足以下这点:
- 必须关键字final修饰
- 数据类型必须基本数据类型或String类型
- 对这个常量进行定义的时候,必须对其进行赋值
下面的例子在编译的时候就会生成编译时的常量:
package test;
public class Test {
private final String valueOne = "aaaaaaa";
public final static String VALUE_TWO = "bbbbbb";
}
我们通过javap -c -v -p Test 对Class文件进行反汇编,并只列出了关键的部分:
public class test.Test
SourceFile: "Test.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
....
#21 = Utf8 aaaaaaa
....
#25 = Utf8 bbbbbb
{
private final java.lang.String valueOne;
flags: ACC_PRIVATE, ACC_FINAL
ConstantValue: String aaaaaaa
public static final java.lang.String VALUE_TWO;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String bbbbbb
....
}
我们可以看出编译后常量的值aaaaaaa 和 bbbbbb已经存在于Constant pool中。以后在类装载的时候就不需要为这两个变量进行初始化的操作了。
注意,我们发现这两个常量字段被ConstantValue属性所修饰,根据深入理解JVM这本书中所描述:
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。
也就是说static修饰的变量才可以使用ConstantValue的属性,可是我们上面valueOne变量明明是非static修饰,难道java7以后对ConstantValue属性进行了重新定义也允许非static的final变量使用该属性? 这点暂时还没搞懂,后再查查资料。
编译时的常量还有个特点:常量在编译阶段会存入调用它的类的常量池中,所以不会触发定义常量的类的初始化。
在Test类中定义了一个常量:
public class Test {
public final static String VALUE_TWO = "bbbbbb";
}
在Main类中使用Test类的常量:
public class Main {
public static void main(String[] args) {
System.out.println(Test.VALUE_TWO);
}
}
我们知道一个类要使用另一个类时,必须要把该类装载进来,并进行初始操作,方可以使用,但是如果调用编译时的常量则不需要。在编译阶段会将此常量的值“bbbbbb”存储到了调用它的类Main 的常量池中,对常量Test.VALUE_TWO 的引用实际上转化为了Main 类对自身常量池的引用。也就是说,实际上Main 的Class文件之中并没有Test 类的符号引用入口,这两个类在编译成Class文件后就不存在任何联系了。
我们通过javap 反汇编Main字节码文件就可以直观的看出:
public class test.Main
SourceFile: "Main.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
.....
#4 = String #25 // bbbbbb
......
#25 = Utf8 bbbbbb
{
public test.Main();
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String bbbbbb
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
bbbbbb值直接存储在了Main的常量池中了,所以跟Test 类没有任何关系。
二、运行时被初始化
我们并不能因为某数据是final的就认为在编译的时候可以知道它的值了,就如下面:
public class Test {
public static final int INT_1 = new Random().nextInt();
public final int i1 = new Random().nextInt();
}
我们说过要成为编译时常量的条件类型必须基本类型或String,很明显上面的例子不符合这个条件。上面的例子只有当在运行时才会进行初始化。 INT_1 变量在类被装载时已经被初始化,每次调用该变量数值都是一样的。而i1的初始化的阶段则在于创建实例对象时,所以每个对象的i1值都是不一样的:
public static void main(String[] args) {
Test test=new Test();
System.out.println(test.i1);
test=new Test();
System.out.println(test.i1);
test=new Test();
System.out.println(test.i1);
}
打印后的值分别是2145916241 、 465177656 、 -680509643。
空白final
java 允许生成空白final,空白final是指被声明为final但又未给定初始化值的域。 无论什么情况,编译器都要确保空白final在使用前必须被初始化,所以我们可以不必在声明final变量时就给定值,可以在后面再进行赋值,这给关键字final的使用上提供了更大的灵活性。
- 对于static修饰的空白final字段,我们只能在static代码块中进行赋值:
public class Test {
public static final String INT_1;
static {
INT_1 = "aaaaaaaa";
}
}
- 对于非static修饰的空白final字段,我们可以在动态代码块 或 构造器中进行赋值:
public class Test {
public final String i1;
{
i1="bbbbbbbbbb";
}
}
或
public class Test {
public final String i1;
public Test(){
i1="bbbbbbbb";
}
}
- 对于局部变量,编译器也允许先声明后赋值:
public static void main(String[] args) {
final int a;
a=4;
System.out.println(a);
}
不管哪种情况编译器总要保证被final修饰的变量,在使用前必须先进行赋值操作,才可编译通过。
参考:
《Thinking in Java》
更多推荐
所有评论(0)