@[toc]


1. 引入

Java中被final关键字修饰的变量表示表示最终的,不可改变。常见的用法有四种:

  • 修饰一个类:一个类如果被final修饰,当前类不能有子类,而且其中的所有成员方法都无法进行覆盖重写。格式:
    1. public final class 类名称{
    2. ...
    3. }
  • 修饰一个方法,当final关键字用来修饰一个方法的时候,这个方法就是最终方法,无法被覆盖重写。格式:
    1. 修饰符 final 返回值类型 方法名称(参数列表){
    2. ...
    3. }

对于类、方法来说,abstract关键字和final关键字不能同时使用,因为矛盾

  • 修饰一个局部变量,当变量被final修饰,一次赋值终生不变。对于基本类型来说,不可变说的是变量当中的数据不可变;对于引用类型来说,不可变说的是变量当中的地址值不可变

  • 修饰一个成员变量,当成员变量被final关键字修饰,变量照样不可变

  • 由于成员变量具有默认值,所以使用了final之后必须手动赋值,不会再给默认值
  • 对于被final修饰的成员变量,要么使用直接赋值,要么通过构造方法进行赋值,二者选其一
  • 必须保证类中所有的重载的构造方法都最终会对final的成员变量进行赋值

知道了如何使用final关键字来保证方法或是变量的不可变性,那么为什么使用了final关键字就可以做到这些呢?下面我们看一下它的底层实现原理。


2. 原理

假设此时代码如下所示:

  1. /**
  2. * @Author dyliang
  3. * @Date 2020/8/27 15:01
  4. * @Version 1.0
  5. */
  6. public class FinalDemo {
  7. public final int number = 10;
  8. }

如果此时在主方法中为number重新赋值,那么就会看到编译错误如下所示:
浅析Java中的final关键字的使用和底层原理 - 图1

编译程序后,查看方法对应的字节码指令,如下所示:

  1. 0 aload_0
  2. 1 invokespecial #1 <java/lang/Object.<init>>
  3. 4 aload_0
  4. 5 bipush 10
  5. 7 putfield #2 <keyWords/FinalDemo.number>
  6. 10 return

通过上面的字节码指令可以看出,被final修饰的变量的赋值使用了putfiled指令,操作系统会在该指令后加入写屏障,保证其他线程读到它的值时不会出现为0的情况。那为什么使用了putfield指令后,就可以保证其他线程读到的一定是它初始化后的结果呢?下面,我们从内存层面继续往下看一下它的原理。

我们知道写屏障和读屏障一个重要的特性就是禁止指令间的重排序,特别是对于final域来说,编译器和处理器都需要遵从如下的两条规则:

  • 任意构造函数中对一个final域的写入,与随后把这个构造对象的引用赋值给另一个引用变量,这两个操作不能重排序

    言外之意:在对象引用为任意线程可见之前,对象的final域已经被正确的初始化过了。

  • 初次读一个包含final域对象的引用,与之后初次读这个final域,这两个操作之间不能重排序

    言外之意:只有得到了包含final域对象的引用,才能后读到final域的值。

final关键字通过上述的两条规则,保证了在写final域和读final域时的正确性。


3. 优点

  • final关键字提高了性能,JVM和Java应用都会缓存final变量

  • final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销

  • 使用final关键字,JVM会对方法、变量及类进行优化