final关键字的这8个小细节,你get到几个?

开发 后端
今天来聊 final 关键字,因为最近在看的几本书都讲到了 final 关键字,发现好多小细节自己都忽视了,抽空总结了一下,分享给大家。

[[381985]]

 今天来聊 final 关键字,因为最近在看的几本书都讲到了 final 关键字,发现好多小细节自己都忽视了,抽空总结了一下,分享给大家。

正文

final关键字是一个常用的关键字,可以修饰变量、方法、类,用来表示它修饰的类、方法和变量不可改变,下面就聊一下使用 final 关键字的一些小细节。

细节一、final 修饰类成员变量和实例成员变量的赋值时机

对于类变量:

  1.  声明变量的时候直接赋初始值
  2.  在静态代码块中给类变量赋初始值

如下代码所示: 

  1. public class FinalTest {  
  2.     //a变量直接赋值  
  3.     private final static  int a = 1 
  4.     private final static  int b;  
  5.     //b变量通过静态代码块赋值  
  6.     static {  
  7.         b=2 
  8.     }  

对于实例变量:

  1.  在声明变量的时候直接赋值
  2.  在非静态代码块中赋值
  3.  在构造器中赋初始化值

如下代码所示: 

  1. public class FinalTest {  
  2.     //c变量在在声明时直接赋值  
  3.     private final  int c =1 
  4.     private final  int d;  
  5.     private final  int e;  
  6.     //d变量在非静态代码块中赋值  
  7.     {  
  8.         d=2 
  9.     }  
  10.     //e变量在构造器中赋值  
  11.     FinalTest(){  
  12.         e=3 
  13.     }  

细节二、当 final 修饰的成员变量未对它进行初始化时,会出现错误吗?

答:会出现错误。因为 java 语法规定,final 修饰的成员变量必须由程序员显示的初始化,系统不会对变量进行隐式的初始化。

如下图所示,未初始化变量就会出现编译错误:

细节三、final 修饰基本类型变量和引用类型变量的区别

如果 fianl 修饰的是一个基本数据类型的数据,一旦赋值后就不能再次更改。

那么 final 修饰的是引用数据类型呢?这个引用的变量能够改变吗?

看下面的代码: 

  1. public class FinalTest {  
  2.     //在声明final实例成员变量时进行赋值  
  3.     private final static Student student = new Student(50, "Java");  
  4.     public static void main(String[] args) {  
  5.         //对final引用数据类型student进行更改  
  6.         student.age = 100 
  7.         System.out.println(student.toString());  
  8.     }  
  9.     static class Student {  
  10.         private int age;  
  11.         private String name;  
  12.         public Student(int age, String name) {  
  13.             this.age = age;  
  14.             this.name = name;  
  15.         }  
  16.         @Override  
  17.         public String toString() {  
  18.             return "Student{" +  
  19.                     "age=" + age +  
  20.                     ", name='" + name + '\'' +  
  21.                     '}';  
  22.         }  
  23.     }  
  24.  
  25. //下面是打印结果  
  26. Student{age=100name='Java'

从打印结果可以看到:引用数据类型变量 student 的 age 属性修改成 100,是可以修改成功的。

结论:

  1.  当 final 修饰基本数据类型变量时,不能对基本数据类型变量重新赋值,因此基本数据类型变量不能被改变。
  2.  对于引用类型变量而言,它仅仅保存的是一个引用,final 只保证这个引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象里面的属性是可以改变的。

细节四、final 修饰局部变量的场景

fianl 局部变量由程序员进行显示的初始化,如果 final 局部变量进行初始化之后就不能再次进行更改。

如果 final 变量未进行初始化,可以进行赋值,并且只能进行一次赋值,一旦赋值之后再次赋值就会出错。

下面的代码演示 final 修饰局部变量的情况:

细节五、final 修饰方法会对重载有影响吗?重写呢?

对于重载:final 修饰方法后是可以重载的

如下代码: 

  1. public class FinalTest {  
  2.     public final void test(){  
  3.     }  
  4.     //重载方法不会出现问题  
  5.     public final void test(String test){  
  6.     }  

对于重写:当父类的方法被 final 修饰的时候,子类不能重写父类的该方法

如上代码所示,可以看到会出现 cannot override ,overridden method is final 的编译错误提示

细节六、final 修饰类的场景

当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用 final 进行修饰。

final 类中的成员变量可以根据需要设为 final,但是要注意 final 类中的所有成员方法都会被隐式地指定为 final 方法。

细节七、写 final 域的重排序规则,你知道吗?

这个规则是指禁止对 final 域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:

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

给举个例子,要不太抽象了,先看一段代码 

  1. public class FinalTest{  
  2.     private int a;  //普通域  
  3.     private final int b; //final域  
  4.     private static FinalTest finalTest;  
  5.     public FinalTest() {  
  6.         a = 1; // 1. 写普通域  
  7.         b = 2; // 2. 写final域  
  8.     }  
  9.     public static void writer() {  
  10.         finalTest = new FinalTest();  
  11.     }  
  12.     public static void reader() {  
  13.         FinalTest demo = finalTest; // 3.读对象引用  
  14.         int a = demo.a;    //4.读普通域  
  15.         int b = demo.b;    //5.读final域  
  16.     }  

假设线程 A 在执行 writer()方法,线程 B 执行 reader()方法。

由于变量 a 和变量 b 之间没有依赖性,所以就有可能会出现下图所示的重排序

由于普通变量 a 可能会被重排序到构造函数之外,所以线程 B 就有可能读到的是普通变量 a 初始化之前的值(零值),这样就可能出现错误。

而 final 域变量 b,根据重排序规则,会禁止 final 修饰的变量 b 重排序到构造函数之外,从而 b 能够正确赋值,线程 B 就能够读到 final 域变量 b初始化后的值。

结论:写 final 域的重排序规则可以确保在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域就不具有这个保障。

细节八:读 final 域的重排序规则,你知道吗?

这个规则是指在一个线程中,初次读对象引用和初次读该对象包含的 final 域,JMM 会禁止这两个操作的重排序。

还是上面那段代码 

  1. public class FinalTest{  
  2.     private int a;  //普通域  
  3.     private final int b; //final域  
  4.     private static FinalTest finalTest;  
  5.     public FinalTest() {  
  6.         a = 1; // 1. 写普通域  
  7.         b = 2; // 2. 写final域  
  8.     }  
  9.     public static void writer() {  
  10.         finalTest = new FinalTest();  
  11.     }  
  12.     public static void reader() {  
  13.         FinalTest demo = finalTest; // 3.读对象引用  
  14.         int a = demo.a;    //4.读普通域  
  15.         int b = demo.b;    //5.读final域  
  16.     }  

假设线程 A 在执行 writer()方法,线程 B 执行 reader()方法。

线程 B 可能就会出现下图所示的重排序

可以看到,由于读对象的普通域被重排序到了读对象引用的前面,就会出现线程 B 还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而 final 域的读操作就“限定”了在读 final 域变量前已经读到了该对象的引用,从而就可以避免这种情况。

结论:读 final 域的重排序规则可以确保在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。

结束

今天给大家总结了一下使用 final 关键字容易忽视的一些小细节,看完希望你能有所收获。 

 

责任编辑:庞桂玉 来源: Hollis
相关推荐

2021-01-26 07:20:26

Final关键字类变量

2023-01-13 08:54:20

MySQL数据库

2020-08-10 08:00:13

JavaFinal关键字

2020-04-20 17:43:28

Java代码优化开发

2021-01-05 10:26:50

鸿蒙Javafinal

2024-01-15 10:41:31

C++关键字开发

2012-03-13 14:41:41

JavaJVM

2009-12-08 18:02:06

PHP final关键

2011-05-27 15:00:12

网站优化关键字

2018-11-19 11:43:13

Python数据函数

2011-06-24 17:39:08

长尾关键词

2019-08-28 16:38:49

finalJava编程语言

2024-01-25 11:36:08

C++构造函数关键字

2023-12-11 13:59:00

YieldPython生成器函数

2023-03-09 07:38:58

static关键字状态

2022-11-29 07:33:15

JavaLombokRecord

2021-01-07 11:10:47

关键字

2023-06-26 08:02:34

JSR重排序volatile

2023-11-28 21:50:39

finalstaticvolatile

2022-02-17 08:31:38

C语言staic关键字
点赞
收藏

51CTO技术栈公众号