[Java] 详细解说final关键字
final
final 可以修饰变量、方法和类,表示所修饰的内容一旦赋值之后就不会再被改变。例如String类就是一个final类型的类。
1.具体使用场景
1.1 变量
1.1.1 成员变量
每个类中的成员变量可以分为类变量(static修饰的变量)以及实例变量。针对这两种类型的变量赋初值的时机是不同的。
类变量(两个赋初值时机):
- 在声明变量的时候直接赋初值
- 在静态代码块中给类变量赋初值
实例变量(三个赋初值时机):
- 可以在声明变量的时候给实例变量赋初值
- 在非静态初始化块中赋初值
- 在构造器中赋初值
初始化报错情况:
- 当
final在变量未初始化的时候,系统不会进行隐式的初始化,则会出现报错情况 - 被
final修饰的变量一经赋值,就不可再更改,否则报错。 - 实例变量不可以在静态初始化块中赋初值,否则报错。
- 实例方法不能为
final类型变量赋值,否则报错

总结:
类变量:必须要在静态初始化块中指定初始值或者声明该类变量时指定初始值,而且只能在这两个地方之一进行指定;
实例变量:必要要在非静态初始化块,声明该实例变量或者在构造器中指定初始值,而且只能在这三个地方进行指定。
1.1.2 局部变量
final局部变量由程序员进行显式初始化,如果final局部变量已经进行了初始化则后面就不能再次进行更改,如果final变量未进行初始化,可以进行赋值,当且仅有一次赋值,一旦赋值之后再次赋值就会出错。
方法形参:
- 调用方法,给参数列表传入实参时,完成初始化
方法局部变量:
- 在使用前 进行初始化
初始化报错情况:
- 变量当且仅有一次赋值,若赋值后进行修改则会报错。
- 没有对局部变量进行初始化操作就引用,则会报错。

1.1.3 基本数据类型
同上成员变量/局部变量。
1.1.4 引用数据类型
重新初始化赋值操作,会报错。因为引用地址发生了变化。
对引用类型中的属性进行修改,可以,因为指向地址没有发生变化。
当final修饰基本数据类型变量时,不能对基本数据类型变量重新赋值,因此基本数据类型变量不能被改变。而对于引用类型变量而言,它仅仅保存的是一个引用,final只保证这个引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象属性是可以改变的。
1.2 宏变量
宏变量是指,可以执行宏替换的变量。也就是说,编译器会将程序中用到该变量的地方全部替换成该变量的值。
利用final变量的不可更改性,在满足一下三个条件时,该变量就会成为一个“宏变量”,即是一个常量:
- 使用
final修饰符修饰 - 在定义该final变量时就指定了初始值
- 该初始值在编译时就能够唯一指定
宏变量已经不再是变量范畴,而是直接量。因为使用宏变量的地方,编译器会直接替换成对应的直接量。
1.3 方法
重写:
当父类的方法被 final 修饰的时候,子类不能重写父类的方法。如 Object 类中的 getClass() 方法就是 final 的,不可重写。但是 hashCode() 不是被 final 所修饰的,所以可以重写。
也就是说,被final修饰的方法不能够被子类所重写
重载:
被 final 修饰的方法可以被重载。
1.4 类
当一个类被final修饰时,表名该类是不能被子类继承的。是最终类。
另,
final不允许和abstract同时修饰一个方法或类。因为final不允许被重写和继承,而abstract是抽象的,没有实现,所以必须被子类继承重写。
2. 不可变类
2.1 概念
不可变类:不变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。
可变类:创建该对象后,该对象的实例变量是可变的。
2.2 设计规则
不可变类有以下几个设计规则:
- 无法扩展。是最终类(将类声明为
final) - 所有成员都是
private final的 - 不提供对成员的改变方法,如
set方法 - 通过构造器初始化成员,若构造器传入引用数据类型,需要进行深拷贝(复制该类的新实例而不是引用),确保类的不可变。
- 在公开修改类状态的方法时,必须始终返回该类的新实例。
- 必要时重写
hashCode和equals方法,保证两个equals方法判断为相等的对象,其hashCode也应该相等
2.3 举例
比如,java中八个基本类型的包装类,和 String 类都属于不可变类。举例看看String的实现:
/** The value is used for character storage. */
private final char value[];
可以看出String的value就是final修饰的,上述其他几条性质也是吻合的。
2.4 优点
优点:
- 高效率
- 拷贝对象内容不用复制本身,而是复制地址,复制地址只需要很小的内存空间,具有非常高的效率。
- 保证了
hashCode的唯一性,因此可以放心的进行缓存而不必每次重新计算新的哈希码。可以提高在HashMap等以不可变类实例为key的容器的性能
- 线程安全。避免值被其他进程修改的情况,同时省去了同步加锁等过程。
3. 多线程中的final
3.1 抛出问题
在java内存模型中,为了能让编译器和处理器底层发挥他们的最大优势,所以对其的约束很少。那么处理器和编译器为了性能优化就会对指令序列有编译器和处理器的重排序操作。那么问题来了,在多线程情况下, final 会进行怎样的重排序?会导致线程安全的问题吗?
3.2 final 域的重排序规则
3.2.1 final 域为基本类型
示例代码:
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}
public static void writer() {
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
}
假设线程A在执行writer()方法,线程B执行reader()方法。
3.2.1.1 写操作
写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:
JMM禁止编译器把final域的写重排序到构造函数之外;编译器会在
final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
那么对于writer方法,就存在的一种可能执行时序图:

因此,写final域的重排序规则可以确保在对象引用为任意线程可见之前,对象的 final 域已经被正确的初始化过了,而普通域就不具有这个保障。(也就是说,final 可以保证正在创建中的对象不能被其他线程访问到。)
但是这其实是有个前提条件的:在构造函数中,不能让这个被构造的对象被其他线程可见。也就是说该对象不能再构造函数中"溢出".
如下代码:
public class FinalReferenceEscapeDemo {
private final int a;
private FinalReferenceEscapeDemo referenceDemo;
public FinalReferenceEscapeDemo() {
a = 1; //1
referenceDemo = this; //2
}
public void writer() {
new FinalReferenceEscapeDemo();
}
public void reader() {
if (referenceDemo != null) { //3
int temp = referenceDemo.a; //4
}
}
}
假设一个线程A执行writer方法线程B执行reader方法。
因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。
3.2.1.2 读操作
读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。也就是说,确保在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。
3.2.2 final 域为引用类型
3.2.2.1 写操作
针对引用数据类型,在之前基本类型的规则基础删,增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。这句话是比较拗口的,下面结合实例来看。
public class FinalReferenceDemo {
final int[] arrays;
private FinalReferenceDemo finalReferenceDemo;
public FinalReferenceDemo() {
arrays = new int[1]; //1
arrays[0] = 1; //2
}
public void writerOne() {
finalReferenceDemo = new FinalReferenceDemo(); //3
}
public void writerTwo() {
arrays[0] = 2; //4
}
public void reader() {
if (finalReferenceDemo != null) { //5
int temp = finalReferenceDemo.arrays[0]; //6
}
}
}
线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。下图就以这种执行时序出现的一种情况来讨论。

3.2.2.2 读操作
JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。
4.扩展
4.1 final、finally、finalize的区别
final:用于声明属性,方法和类,表示属性不可变性,方法不可覆盖,类不可继承。
finally:是异常处理语句结构的一部分,表示总是会执行。
finalize:是Object类中的一个方法,在垃圾收集器执行的时候,会去调用被回收对象的此方法,供垃圾收集时其他资源的回收。比如关闭文件等。
[Java] 详细解说final关键字的更多相关文章
- JAVA面向对象-----final关键字
JAVA面向对象-–final关键字 1:定义静态方法求圆的面积 2:定义静态方法求圆的周长 3:发现方法中有重复的代码,就是PI,圆周率. 1:如果需要提高计算精度,就需要修改每个方法中圆周率. 4 ...
- 聊聊Java的final关键字
Java的final关键字在日常工作中经常会用到,比如定义常量的时候.如果是C++程序员出身的话,可能会类比C++语言中的define或者const关键字,但其实它们在语义上差距还是挺大的. 在Jav ...
- java之final关键字
final关键字(可以读不可以写.只读) 1.final的变量的值不能够被改变 ①.final的成员变量 ②.final的局部变量(形参) //意思是“实参”一旦传进我的方法里面,就不允许改变 2.f ...
- Java的final关键字详解
Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使 ...
- java中final 关键字的作用
final 关键字的作用 java中的final关键字可以用来声明成员变量.本地变量.类.方法,并且经常和static一起使用声明常量. final关键字的含义: final在Java中是一个保留的关 ...
- Java基础 -- final关键字
在java的关键字中,static和final是两个我们必须掌握的关键字.不同于其他关键字,他们都有多种用法,而且在一定环境下使用,可以提高程序的运行性能,优化程序的结构.下面我们来了解一下final ...
- Java中final关键字修饰变量、方法、类的含义是什么
Java中的关键字final修饰变量.方法.类分别表示什么含义? 先看一个简单的介绍 修饰对象 解释说明 备注 类 无子类,不可以被继承,更不可能被重写. final类中的方法默认是final的 方法 ...
- java浅析final关键字
谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法. ...
- 关于Java中final关键字的详细介绍
Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使 ...
- java基础---->final关键字的使用
这里介绍一些java基础关于final的使用,文字说明部分摘自java语言规范.心甘情愿这四个字,透着一股卑微,但也有藏不住的勇敢. Final关键字的说明 一.关于final变量规范说明 .A fi ...
随机推荐
- 我让 ChatGPT 化身为全知全能的文档小助理,啥姿势她都会......
ChatGPT 虽然只是一个对话型人工智能,但已经震惊了全世界,有人甚至认为人工智能的奇点已经到来.未来一定会有很多人失业,从工业革命开始,每出现一次重大的技术变革,就必然会有一批人失业,我们要直面现 ...
- freeswitch的3XX重定向
概述 sip协议标准RFC3261中,对3XX重定向有明确的定义. freeswitch中如何使用3XX redirect的特性,如何落地,应用场景有哪些? 环境 centos:CentOS rel ...
- AHB-SRAMC Design-01
AHB-SRAMC Design 1.AHB-SoC芯片架构图 CPU赋予了SoC的可编程性 SRAM可以存储数据和代码 2.AHB-SRAMC Features 总线版本号 在进行设计的时候可以将地 ...
- 使用pip安装pycharm插件时,要使用管理员权限打开cmd安装
1.问题 安装到一半报错 报错1 报错2 2.解决 解决1 原文:https://blog.csdn.net/weixin_44899752/article/details/128372969 下面是 ...
- [kubernetes]服务健康检查
前言 进程在运行,但是不代表应用是正常的,对此pod提供的探针可用来检测容器内的应用是否正常.k8s对pod的健康状态可以通过三类探针来检查:LivenessProbe.ReadinessProbe和 ...
- 【C/C++】sscanf函数和正则表达式
此文所有的实验都是基于下面的程序:char str[10];for (int i = 0; i < 10; i++) str[i] = '!';执行完后str的值为str = "!!! ...
- sql server 数据恢复
1) 备份当前数据库的事务日志:BACKUP LOG [数据库名] TO disk= N'备份文件名' WITH NORECOVERY 2) 恢复一个误删除之前的完全备份:RESTORE DATABA ...
- [转帖]influxdb 2.0.3 tar.gz的安装与配置
下载地址:https://dl.influxdata.com/influxdb/releases/influxdb2-2.0.3_linux_amd64.tar.gz 安装influxdb ### 解 ...
- [转帖]Arm vs X86 (unfinished)
http://home.ustc.edu.cn/~shaojiemike/posts/arm/ ARM Ltd history 诞生 1981年,被Intel拒绝的Acorn(橡子) Comput ...
- [转帖]总结:Tomcat的IO模型
一.介绍 对于 linux 操作系统,IO 多路复用使用的是 epoll 方式,对于 windows 操作系统中 IO 多路复用使用的是 iocp 方式,对于 mac 操作系统 IO 多路复用使用的是 ...