引言

在说说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_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》

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐