浅谈java中final数据

来源:互联网 发布:装多客软件多少钱 编辑:程序博客网 时间:2024/06/05 15:26

引言

在说说final之前,我们先了解下类被加载到内存中所需要的几个步骤,一个类被加载到内存中需要经过如下几个阶段:

  • 编译: java文件必须编译成Class文件(也称为字节码文件)才可以被JVM识别,JVM并不关心Class的来源是什么语言,只要它符合一定的结构,就可以在Java中运行。

  • 装载:查找和导入必要的Class文件,在Java虚拟机执行过程中,只有他需要一个类的时候,才会调用类加载器来加载这个类,并不会在开始运行时加载所有的类。

  • 链接: 检查载入Class文件数据的正确性、静态变量分配内存空间,并设置默认值、将符号引用转成直接引用

  • 初始化:对类的静态变量,静态代码块执行初始化操作

了解完成上面后,我们进入final,一个被final修饰的数据会有两种情况的存在:

  1. 永不改变的编译时常量
  2. 运行时被初始化的值

一、编译时的常量

对于编译时常量这种情况就在于编译成Class文件后,常量的值就已经存在于编译时常量池中了。当我们使用该常量时,类就不需要为它进行任何初始化操作,我们直接拿来用就行了。要成为编译时的常量必须要满足以下这点:

  1. 必须关键字final修饰
  2. 数据类型必须基本数据类型或String类型
  3. 对这个常量进行定义的时候,必须对其进行赋值

下面的例子在编译的时候就会生成编译时的常量:

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_SUPERConstant 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_SUPERConstant 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 0