• 主页
  • 归档
所有文章 关于我

  • 主页
  • 归档

了解 final 关键字的特性、使用方法以及原理

2020-12-12

final 使用

final 变量有成员变量 和 本地变量(方法内的局部变量),在类成员中 final 经常和static 一起使用,作为类常量使用。其中类常量必须在声明时初始化,final 成员常量可以在构造函数初始化。

修饰类常量

1
2
3
public class Test{
public static final int i = 1;
}

修饰基本数据类型变量和引用

1
2
3
4
5
6
7
8
public void test(){
final int i = 1;
final int[] j = new {1};
final int[] k = new {1};
// j = k; 会报错
j[0] = 2;
// i = 2; 会报错
}

final 修饰方法表示该方法不能被子类方法重写,将方法声明为 final,在编译时就已经静态绑定,不需要在运行是动态绑定。final 方法调用时使用的是 invokespecial 指令。

final 修饰类

final 类不能被继承,final 类中的方法默认也会是final 类型,java 中的String 类和 Integer 类都是 final 类型。


final关键字知识点

  1. final 成员变量必须在声明时初始化或者在构造器中初始化,否则就会报编译错误。final 变量一旦被初始化后不能再次赋值。
  2. 本地变量必须在声明时赋值。因为没有初始化的过程
  3. 在匿名类中所有变量都必须是final 变量
  4. final方法不能被重写, final类不能被继承
  5. 接口中声明的所有变量本身是final的。类似于匿名类
  6. final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
  7. final方法在编译阶段绑定,称为静态绑定(static binding)。
  8. 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。

final 方法的好处

  1. 提高了性能,JVM在常量池中会缓存final变量
  2. final变量在多线程中并发安全,无需额外的同步开销
  3. final方法是静态编译的,提高了调用速度
  4. final类创建的对象是只可读的,在多线程可以安全共享

final 关键字的最佳实践

对于 final 修饰的常量、或者一些基本变量来说,值不能变,但是修饰的引用,标识的是这个引用不能被改变,而引用内的变量是可以被改变的。

关于空白final

final 修饰变量有三种:静态变量、实例变量和局部变量,分别表示三种类型常量。

另,final 变量定义时,可以先声明,而不给初值,这种变量称为 final 空白,无论什么情况,编译器都确保空白final 在使用前必须被初始化。

final 空白在 final 关键字的使用上提供了更大的灵活性,为此,一个类中 final 数据成员就可以实现根据依对象而有所不同。

final 内存分配

一般地调用一个函数除了函数本身的执行时间之外,还需要额外的时间去寻找这个函数(类内部有一个函数签名和函数地址的映射表)。所以减少函数调用次数就等于降低了性能消耗。

final 修饰的函数会被编译器优化,优化的结果减少了函数的调用次数。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test{ 
final void func(){
System.out.println("g");
}
public void main(String[] args){
for(int j=0;j<1000;j++){
func();
}
}
}
//经过编译器优化之后,这个类变成了相当于这样写:
public class Test{
final void func(){
System.out.println("g");
}
public void main(String[] args){
for(int j=0;j<1000;j++) {
System.out.println("g");
}
}
}

编译器直接将func 的函数体内嵌到了调用函数的地方,这样就可以节省了很多函数调用。

不过,函数体太长的话,用 final 可能适得其反,因为经过编译器内嵌之后代码长度增加,于是就是增加了 jvm 解释字节码的时间。

在使用final修饰方法的时候,编译器会将被final修饰过的方法插入到调用者代码处,提高运行速度和效率,但被final修饰的方法体不能过大,编译器可能会放弃内联,但究竟多大的方法会放弃,还没有测试计算过。

使用final修饰变量会让变量的值不能被改变吗;

final关键字只能保证变量本身不能被赋与新值,而不能保证变量的内部结构不被修改。

如何保证应用内部数据不被修改

降低访问级别,把引用设为private。这样的话,就解决了引用在外部被修改的不安全性,但也产生了另一个问题,那就是这个引用要被外部使用的。

final方法的三条规则

规则1:final修饰的方法不可以被重写。

规则2:final修饰的方法仅仅是不能重写,但它完全可以被重载。

规则3:父类中private final方法,子类可以重新定义,这种情况不是重写。

final 和 jvm 的关系

对于final 域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含 final 域的对象引用,与随后初次读这个 final 域,这两个操作之间不能重排序。

下面,通过一些示例代码来分别说明这两个规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FinalExample{
int i;
final int j;
static FinalExample obj;

public FinalExample(){ // 构造函数
i = 1; // 写普通域
j = 2; // 写 final 域
}

public static void writer(){ // 写线程 A 执行
obj = new FinalExample();
}

public static void reader(){ // 读线程 B执行
FinalExample object = obj; // 读对象引用
int a = object.i; // 读普通域
int b = object.j; // 读 final 域
}
}

这里假设一个线程 A 执行 writer() 方法,随后另一个线程 B 执行 reader() 方法。下面我们通过这两个线程的交互来说明这两个规则。

写 final 域的重排序

写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面 2 个方面:

  • JMM 禁止编译器把 final 域的写重排序到构造函数之外。
  • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreSotre 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。

现在分析 writer() 方法。writer() 方法只包含一行代码:finalExample = new FinalExample()。这行代码包含两个步骤:

  1. 构造一个 FinalExample 类型的对象;
  2. 把这个对象的引用赋值给引用变量 obj。

假设 线程 B读对象引用与读对象成员域之间没有重排序(下面分析需要用到这个假设),下图是一种可能的执行时序:

java-basics-final-01

在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程B 错误的读取了普通变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规则 “限定” 在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。

写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在线程 B “看到” 对象引用 obj 时,很可能 obj 对象还没有构造

完成(对普通域 i 的写操作被重排序到构造函数外,此时初始值 2 还没有写入普通域 i)。


读 final 域的重排序规则

读 final 域的重排序规则如下:

  • 在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意:这个规则仅仅针对处理器)。编译器会在 读 final 域操作的前面插入一个 LoadLoad 屏障。

初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器。

reader() 方法包含三个操作:

  1. 初次读引用变量 obj;
  2. 初次读引用变量 obj 指向对象的普通域 j。
  3. 初次读引用变量 obj 指向对象的 final 域 i。

假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:

java-basics-final-02.jpg

在上图中,读对象的普通域的操作被处理器重排序到都对象引用之前。读普通域是,该域还没有被写线程 A 写入,这是一个错误的读取操作。而读 final 域的重排序规则会把读对象 final 域的操作 “限定” 在读对象引用之后,此时该 final 域已经被 A 线程初始化过了,这是一个正确的读取操作。

读取 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象引用。在这个实例程序中,如果该引用不为 null,那么引用对象的 final 域一定已经被 A 线程初始化过了。


如果 final 域是引用类型

上面我们看到的 final 域是基础数据类型,下面让我们看看如果 final 域是引用类型,将会有什么效果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FinalReferenceExample {
final int[] intArray; //final 是引用类型
static FinalReferenceExample obj;

public FinalReferenceExample () { // 构造函数
intArray = new int[1]; //1
intArray[0] = 1; //2
}

public static void writerOne () { // 写线程 A 执行
obj = new FinalReferenceExample (); //3
}

public static void writerTwo () { // 写线程 B 执行
obj.intArray[0] = 2; //4
}

public static void reader () { // 读线程 C 执行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}

这里 final 域为一个引用类型,它引用一个 int 型的数组对象。对于引用类型,写 final 域的重排序规则对编译器核处理器增加了如下约束:

  1. 在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量这两个操作之间不能重排序。

对上面的例程,我们假设首先线程 A 执行 writerOne() 方法,执行完后线程 B 执行 writerTow() 方法,执行完后,线程 C 执行reader() 方法。下面是一种可能的执行时序:

java-basics-final-03.jpg

在上图中,

  1. 是对 final 域的写入
  2. 是对这个 final 域引用的对象的成员域的写入
  3. 是把被构造的对象的引用赋值给某个引用变量。

这里除了前面提到的 1不能和 3 重排序外,2和3也不能重排序。

JMM 可以确保线程 C 至少能看到写线程 A 在构造函数中对 final 引用对象的成员域的写入。即 C 至少能看到数组下标 0 的值为 1。而线程 B 对数组元素的写入,读线程 C 可能看得到,也可能看不到。JMM 不保证线程 B 的写入对线程 C 可见,因为写线程 B 和读线程 C 之间存在数据竞争,此时的执行结果不可预知。

如果想要确保读线程 C 看到写线程 B 对数组元素的写入,写线程 B 和读线程 C 之间需要使用同步原语(lock 或 volatile)来确保内存可见性。

资料来源

https://www.cnblogs.com/AliCoder/p/11594960.html

赏

谢谢你请我吃糖果

  • Java

扫一扫,分享到微信

微信分享二维码
Volatile 原理
Java CAS
© 2025 Allen
Hexo Theme Yilia by Litten
  • 所有文章
  • 关于我

tag:

  • Java
  • Android
  • SQL
  • 随笔
  • 写作
  • git
  • java
  • hexo blog
  • jvm
  • Docker
  • Python
  • Android framework
  • Data Structures and Algorithms

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

很惭愧

只做了一点微小的工作
谢谢大家