[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 ...
随机推荐
- opensips简介
概述 在众多的sip服务器中,主要有俩大类,一类侧重于媒体/业务服务器,比如freeswitch/asterisk,另一类侧重于代理/负载服务器,比如opensips/kamailio. 今天我们对o ...
- 开源项目《Elight.MVC-ASP.NET》的研究学习
一 观看效果 将源码下载来后,发现不能直接运行,读了一下md文件,发现还要做自己一些改动. 由于我本机是sqlserver, 所以我改了下 appsetting.json 里要使用的数据库,然后 ...
- [转帖]Run Grafana behind a reverse proxy
On this page Introduction Configure NGINX Configure HAProxy Configure IIS Configure Traefik Summary ...
- [转帖]探索惊群 ⑥ - nginx - reuseport
https://wenfh2020.com/2021/10/12/thundering-herd-tcp-reuseport/ SO_REUSEPORT (reuseport) 是网络的一个选项设 ...
- [转帖]【JVM】JDK命令行工具
在JDK/bin目录下我发现了许多命令行工具 这些命令有哪些作用呢,接下来我就来详细介绍一下 常用JDK命令行工具 命令名称 全称 用途 jstat JVM Statistics Monitoring ...
- [转帖]【JVM】线程安全与锁优化
线程安全 1.定义 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果 2. ...
- 在线获取所有依赖rpm包的方法
背景 现在经常有一些不能上网的LInux机器但是需要安装一些软件. 但是有时候经常因为有依赖关系找不到的情况比较麻烦. 或者是一些公司的网络总是受限,网络速度非常慢. 下载安装非常折磨人. 这个时候就 ...
- SAP标准附件-GOS(generic object service)
标准附件功能显示需要用户是 A类型 对话用户 本文介绍一个Public Class :CL_GOS_DOCUMENT_SERVICE 该类包含了创建附件,注释,URL,个人注释等方法,这些方法中都会涉 ...
- js中数组reduce的使用原来这么简单
reduce 的学习方法 array.reduce(callback(prev, currentValue, index, arr), initialValue) //简写就是下面这样的 arr.re ...
- FMEA:总监和架构师都在用的高可用架构分析方法
FMEA:总监和架构师都在用的高可用架构分析方法 记得之前准备春晚项目的时候,团队成员在一起过架构,老板最常问的问题是"这个组件挂了怎么办?有什么影响?",我当时还在心里默默嘀咕: ...