- 发 音
- [ˈfaɪnl]
- 类 型
- 英文单词
- 词 性
- 名词、形容词
- 外文名
- Final
-
2021-02-26 16:29:14
这篇文章主要介绍了Java基于final修饰数据过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
final是Java中的一个重要关键字,它可以修饰数据、方法和类,本篇将从final修饰的数据角度对final做出总结。
final修饰的数据代表着:永远不变。意思是,一旦你用final修饰一块数据,你之后就只能看看它,你想修改它,没门。
我们不希望改变的数据有下面两种情况:
永不改变的编译时常量。
//编译时知道其值
private final int valueOne = 9;
在运行时(不是编译时)被初始化的值,且之后不希望它改变。
//在编译时不能知道其值
private final int i4 = rand.nextInt(20);
设置成常量有啥好处呢?
很简单,让编译器觉得简单,就是最大的好处。比如把PI设置成final,且给定值为3.14,编译器自然会觉得这个东西不会再被修改了,是足够权威的。那么,编译器就会在运行之前(编译时)就把这3.14代入所有的PI中计算,这样在真正运行的时候,速度方面当然会快一点。
有初始值的final域
即声明为final且当场就给定初始值的域。
private final int valueOne = 9;
final+基本数据类型
final修饰的基本数据类型变量存储的数值永恒不变。
/*基本类型变量*/
//带有编译时数值的final基本类型
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
public static final int VALUE_THREE = 39;
//!false:fd1.valueOne++;
//!false:fd1.VALUE_TWO++;
//!false:fd1.VALUE_THREE++;
康康上面醒目的三句false语句,很好地印证了我们之前说的:数值不让改!!!
需要注意的是,按照惯例,下面是定义常量的典型方式:
//典型常量的定义方式
public static final int VALUE_THREE = 39;
public修饰符使其可被用于包之外。
static使数据只有一份。
final表示其无法被更改
名称全为大写英文字母,以下划线隔开。
final+引用数据类型
我们之前说过,基本类型存数值,引用类型存地址值。那么既然final+基本数据类型不让改数值,聪明的我们稍微一联想就明白,final+引用数据类型就是不让你改变量存储实际对象的地址值啦。(也就是不能再让它指向新的对象,很专一)
private Value v1 = new Value(1);
private final Value v2 = new Value(22);
private static final Value V_3 = new Value(333);
//引用变量并不是常量,存储地址可以改变
fd1.v1 = new Value(10);
//v2是引用变量,final修饰之后表示地址不能改变,但是实际对象的值是可以改变的
fd1.v2.i++;
//!false:fd1.v2 = new Value(3);
//V_3与v2类似,是静态和非静态的区别,下面会说明
fd1.V_3.i++;
//!false:fd1.V_3 = new Value(10);
}
通过例子,确实也证明上面所说,一个以final修饰的引用数据类型变量,无法再指向一个新的对象,因为它所存储的地址值已经无法被更改,但是并不影响它指向的实际对象。就拿一个比较典型的引用类型来举例,我们知道数组就是一种典型的引用类型,数组的引用变量存储的是数组再堆中的地址,堆中存放的就是数组每个索引的数值。
/*引用变量之数组*/
private final int[] a = {1,2,3,4,5,6};
引用变量a被指定为final,所以它里面的地址值不能再改,也就无法再让它指向一个新的数组。
//!false:fd1.a = new int[]{2,3,4,5,6,7};
for (int i = 0; i < fd1.a.length; i++) {
fd1.a[i]++;
但是,它指向的数组里的每个元素却可以改动,因为数组中的元素并没有任何的限定。
final与static final
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
System.out.println(fd1);//fd1: i4 = 15,INT_518
FinalData fd2 = new FinalData("fd2");
System.out.println(fd2);//fd2: i4 = 13,INT_518
FinalData fd3 = new FinalData("fd3");
System.out.println(fd3);//fd3: i4 = 1,INT_5 = 18
上面示例分别创建了三个不同的对象,对其final 和final static 进行测试。
需要明确的是,两者都以final修饰,都不能被改变。
三个对象的i4值,没有用static修饰,不相同且不能改变。
而INT_5的值因为被static修饰,在类加载时已经被初始化,不随对象改变而改变。
空白final域
即声明为final却没有给定初始值的域。
private final String id;//空白final
如果只有上面的这句,编译器会报错,因为它没有初始化。
Variable 'id' might not have been initialized
所以,若定义了空白final域,一定记得在构造器中给它赋值!(必须在域的定义处或者每个构造器中以表达式对final进行赋值,因为系统不会为final域默认初始化)
//在构造器中为空白final域赋初值
public FinalData(){
id = "空白final默认id";
}
public FinalData(String id){
this.id = id;
}
不要试图在初始化之前访问域,不然会报错。
final让域可以根据对象的不同而不同,增加灵活性的同时,又保留不被改变的特性。
final修饰的参数
基本数据类型的参数
类似地,就是传入的参数不让改,只让读,这一点很好理解。
public int finalParamTest(final int i){
//!false:i++;
//不让改,只让读
return i+1;
}
但是,我又新增了许多测试,分别定义四种不同的参数传入该方法,发现传入param0和param1编译会报错。(非常疑惑,这部分书上没提,查了许多资料也没有理解清楚,希望大牛可以评论区指点迷津)
/*检测传入参数*/
int param0 = 5;
final int param1 = 10;
static final int PARAM_2 = 15;
static int param3 = 20;
//!false:System.out.println(fd1.finalParamTest(param0));
//!false:System.out.println(fd1.finalParamTest(param1));
//non-static field'param1' cannot be referenced from a static context
System.out.println(fd1.finalParamTest(PARAM_2));
System.out.println(fd1.finalParamTest(param3));
/*为什么形参列表里的参数用final修饰,但是用final修饰的param1无法传进去,
一定要static修饰?*/
引用数据类型的参数
public void finalReferenceTest(final FinalData fd){
//!false:fd = new FinalData();
//不能再指向新的对象,存储地址不准变
fd.param0++;
}
还是类似,不可以让这个引用类型的参数再指向新的对象,但是可以改变其实际指向对象的值。
最后的最后,下面的代码是根据《Thinking in Java》中的示例,结合自己的思想,将各个板块融合而成的超级无敌测试代码,冲冲冲!
package com.my.pac16;
import java.util.Arrays;
import java.util.Random;
/**
* @auther Summerday
*/
class Value{
int i;//package access
public Value(int i){
this.i =i;
}
}
/*final域在使用前必须被初始化:定义时,构造器中*/
public class FinalData {
/*检测传入参数*/
int param0 = 5;
final int param1 = 10;
static final int PARAM_2 = 15;
static int param3 = 20;
private static Random rand = new Random(47);
private final String id;//空白final
public FinalData(){
id = "空白final默认id";
}
public FinalData(String id){
this.id = id;
}
//带有编译时数值的final基本类型
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
//典型常量的定义方式
public static final int VALUE_THREE = 39;
//在编译是不能知道其值
private final int i4 = rand.nextInt(20);
static final int INT_5 = rand.nextInt(20);
private Value v1 = new Value(1);
private final Value v2 = new Value(22);
private static final Value V_3 = new Value(333);
private final int[] a = {1,2,3,4,5,6};
@Override
public String toString(){
return id+": "+"i4 = "+i4+",INT_5 = "+INT_5;
}
public int finalParamTest(final int i){
//!false:i++;
//不让改,只让读
return i+1;
}
public void finalReferenceTest(final FinalData fd){
//!false:fd = new FinalData();
//不能再指向新的对象,存储地址不准变
fd.param0++;
}
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
/*基本类型变量*/
//!false:fd1.valueOne++;
//!false:fd1.VALUE_TWO++;
//!false:fd1.VALUE_THREE++;
/*引用变量*/
fd1.v1 = new Value(10);
fd1.v2.i++
//!false:fd1.v2 = new Value(3);
System.out.println("fd1.v2.i = [" + fd1.v2.i + "]");
//!false:fd1.V_3 = new Value(10);
fd1.V_3.i++;
System.out.println("fd1.V_3.i = [" + fd1.V_3.i + "]");
/*引用变量之数组*/
System.out.println("before:fd1.a[] = " + Arrays.toString(fd1.a));
/*数组引用变量a是final修饰,
但是不代表它指向的数据值是final,
而是a存储的地址值不能改变
*/
//!false:fd1.a = new int[]{2,3,4,5,6,7};
for (int i = 0; i < fd1.a.length; i++) {
fd1.a[i]++;
}
System.out.println("after :fd1.a[] = " + Arrays.toString(fd1.a));
/*final 与static final*/
//下面示例分别创建了三个不同的对象,对其final 和final static 进行测试
/*可以发现,三个对象的i4值是随机生成且不能改变的,且不相同,
而INT_5的值不随对象改变而改变,因为被static修饰,在类加载时已经被初始化*/
System.out.println(fd1);//fd1: i4 = 15,INT_518
FinalData fd2 = new FinalData("fd2");
System.out.println(fd2);//fd2: i4 = 13,INT_518
FinalData fd3 = new FinalData("fd3");
System.out.println(fd3);//fd3: i4 = 1,INT_5 = 18
//!false:System.out.println(fd1.finalParamTest(param0));
//!false:System.out.println(fd1.finalParamTest(param1));
//non-static field'param1' cannot be referenced from a static context
System.out.println(fd1.finalParamTest(PARAM_2));
System.out.println(fd1.finalParamTest(param3));
/*为什么形参列表里的参数用final修饰,但是用final修饰的param1无法传进去,
一定要static修饰?*/
System.out.println("fd1.param0 = "+fd1.param0);
fd1.finalReferenceTest(fd1);
System.out.println("fd1.param0 = "+fd1.param0);
}
}
文章如有理解错误或叙述不到位,欢迎大家在评论区加以指正。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
更多相关内容 -
关于final的一些细节,我有话要说—— 从内存模型中了解final|CSDN创作打卡
2022-02-12 21:13:32从内存模型中继续深入了解final的用处吧,这节内容很干,建议慢慢观看,嘿嘿!茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!
愿你在未来的日子,保持热爱,奔赴山海!!
题记:关于
final
关键字,它也是我们一个经常用的关键字,可以修饰在类上、或者修饰在变量、方法上,以此看来定义它的一些不可变性!像我们经常使用的String类中,它便是
final
来修饰的类,并且它的字符数组也是被final
所修饰的。但是一些final
的一些细节你真的了解过吗?从这篇文章开始,带你深入了解
final
的细节!🍉 从内存模型中了解final
在上面,我们了解在单线程情况下的
final
,但对于多线程并发下的final
,你有了解吗?多线程并发的话,我们又必须知道一个内存模型的概念:JMM
。🍊 JMM
JMM是定义了线程和主内存之间的抽象关系:线程之间的共享变量存在主内存(MainMemory)中,每个线程都有一个私有的本地内存(LocalMemory)即共享变量副本,本地内存中存储了该线程以读、写共享变量的副本。本地内存是Java内存模型的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。
而在这一内存模型下,计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。那么问题又来了,重排序是什么?
🍒 重排序
其实对于我们程序来说,可以分为不同指令,每一个指令都会包含多个步骤,每个步骤可能使用不同的硬件。我们可以将每个指令拆分为五个阶段:
想这样如果是按顺序串行执行指令,那可能相对比较慢,因为需要等待上一条指令完成后,才能等待下一步执行:
而如果发生指令重排序呢,实际上虽然不能缩短单条指令的执行时间,但是它变相地提高了指令的吞吐量,可以在一个时钟周期内同时运行五条指令的不同阶段。
我们来分析下代码的执行情况,并思考下:
a = b + c; d = e - f ;
按原先的思路,会先加载b和c,再进行b+c操作赋值给a,接下来就会加载e和f,最后就是进行e-f操作赋值给d。
这里有什么优化的空间呢?我们在执行b+c操作赋值给a时,可能需要等待b和c加载结束,才能再进行一个求和操作,所以这里可能出现了一个停顿等待时间,依次后面的代码也可能会出现停顿等待时间,这降低了计算机的执行效率。
为了去减少这个停顿等待时间,我们可以先加载e和f,然后再去b+c操作赋值给a,这样做对程序(串行)是没有影响的,但却减少了停顿等待时间。既然b+c操作赋值给a需要停顿等待时间,那还不如去做一些有意义的事情。
总结:指令重排对于提高CPU处理性能十分必要。但是会因此引发一些指令的乱序。那么我们的
final
它对指令重排序有什么作用呢?接下来我们来看看吧!🍓 final域重排序规则
对于JMM内存模型来说,它对
final
域有以下两种重排序规则:-
写:在构造函数内对
final
域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。 -
读:初次读一个包含
final
域的对象的引用和随后初次写这个final
域,不能重排序。
具体我们根据代码演示一边来讲解吧:
代码:
package com.ygt.test; /** * 测试JMM内存模型对final域重排序的规则 */ public class JMMFinalTest { // 普通变量 private int variable; // final变量 private final int variable2; private static JMMFinalTest jmmFinalTest; // 构造方法中,将普通变量和final变量进行写的操作 public JMMFinalTest(){ variable = 1; // 1. 写普通变量 variable2 = 2; // 2. 写final变量 } // 模仿一个写操作 --> 假设线程A进行来写操作 public static void write() { // new 当前类对象 --> 并在构造函数中完成赋值操作 jmmFinalTest = new JMMFinalTest(); } // 模仿一个读操作 --> 假设线程B进行来读操作 public static void read() { // 读操作: JMMFinalTest test = jmmFinalTest; // 3. 读对象的引用 int localVariable = test.variable; int localVariable2 = test.variable2; } }
写final域重排序规则
写
final
域重排序规则在构造函数内对final
域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。代表禁止对final
域的初始化操作必须在构造函数中,不能重排序到构造函数之外,这个规则的实现主要包含了两个方面:- JMM内存模型禁止编译器把
final
域的写重排序到构造函数之外; - 编译器会在
final
域写入和构造函数return返回之前,插入一个storestore
内存屏障。这个内存屏障可以禁止处理器把final
域的写重排序到构造函数之外。
我们再来分析write方法,虽然只有一行代码,但他实际上有三个步骤:
- 在JVM的堆中申请一块内存空间
- 对象进行初始化操作
- 将堆中的内存空间的引用地址赋值给一个引用变量jmmFinalTest。
对于普通变量variable来说,它的初始化操作可以被重排序到构造函数之外,即我们的步骤不是本来1-2-3吗,现在可能造成1-3-2这样初始化操作在构造函数返回后了!
而对于
final
变量variable2来说,它的初始化操作一定在构造函数之内,即1-2-3。我们来看一个可能发生的图:
对于变量的可见性来说,因为普通变量variable可能会发生重排序的一个现象,读取的值可能会不一样,可能是0或者是1。但是
final
变量variable2,它读取的值一定是2了,因为有个StoreStore
内存屏障来保证与下面的操作进行重排序的操作。由此可见,写
final
域的重排序规则可以哪怕保证我们在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。读final域重排序规则
初次读一个包含
final
域的对象的引用和随后初次写这个final
域,不能重排序。怎么实现呢?它其实处理器会在读
final
域操作的前面插入一个LoadLoad
内存屏障。我们再来分析read方法,他实有三个步骤:
- 初次读引用变量jmmFinalTest;
- 初次读引用变量jmmFinalTest的普通域变量variable;
- 初次读引用变量jmmFinalTest的
final
域变量variable2;
我们以写操作正常排序的情况,对于读情况可能发生图解:
对于读对象的普通域变量variable可能发生重排序的现象,被重排序到了读对象引用的前面,此时就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。
而对于
final
域的读操作通过LoadLoad
内存屏障保证在读final
域变量前已经读到了该对象的引用,从而就可以避免以上情况的发生。由此可见,读
final
域的重排序规则可以确保我们在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用,而普通域就不具有这个保障。🍅 final对象是引用类型
上面我已经了解了
final
域对象是基本数据类型的一个重排序规则了,但是对象如果是引用类型呢?我们接着来:当
final
域对象是一个引用类型,写final
域的重排序规则增加了如下的约束:在构造函数内对一个
final
引用的对象的成员域的写入,与随后在构造函数外将被构造对象的引用赋值给引用变量之间不能重排序。 听起来还是有点难懂是吧,没事,代码看看!注意一点:之前的写
final
域的重排序规则一样存在,只是对引用类型对象增加了一条规则。代码:
package com.ygt.test; /** * 测试final引用类型对象时的读写情况 */ public class ReferenceFinalTest { // 定义引用对象 final Person person; private ReferenceFinalTest referenceFinalTest; // 在构造函数中初始化,并进行赋值操作 public ReferenceFinalTest(){ person = new Person(); // 1. 初始化 person.setName("詹姆斯!"); // 2. 赋值 } // 线程A进来进行写操作,实现将referenceFinalTest初始化 public void write(){ referenceFinalTest = new ReferenceFinalTest(); // 3. 初始化构造函数 } // 线程B进来进行写操作,实现person重新赋值操作。 public void write2(){ person.setName("戴维斯"); // 4. 重新赋值操作 } // 线程C进来进行读操作,读取当前person的值 public void read(){ if(referenceFinalTest != null) { // 5. 读取引用对象 String name = person.getName(); // 6. 读取person对象的值 } } } class Person{ private String name; private int age; public void setName(String name) { this.name = name; } public String getName() { return name; } }
首先,我们先画个可能发生情况的图解:
我们线程的执行顺序:A ——> B ——> C
接着我们对读写操作方法进行详解:
写final域重排序规则
从之前我们就知道,我们
final
域的写禁止重排序到构造方法外,因此1和3是不能发生重排序现象滴。而对于我们新增的约束来说,在构造函数内对一个
final
引用的对象的成员域的写入,与随后在构造函数外将被构造对象的引用赋值给引用变量之间不能重排序。即final
域的引用对象的成员属性写入setName("詹姆斯")
是不可以与随后将这个被构造出来的对象赋给引用变量jmmFinalTest重排序,因此2和3不能重排序。所以我们的步骤是1-2-3。
读final域重排序规则
对于多线程情况下,JMM内存模型至少可以确保线程C在读对象person的成员属性时,先读取到了引用对象person了,可以读取到线程A对
final
域引用对象person的成员属性的写入。可能此时线程B对于person的成员属性的写入暂时看不到,保证不了线程B的写入对线程C的可见性,因为可能线程B与线程C存在了线程抢占的竞争问题,此时的结果可能不同!
当然,如果想要保存可见,我们可以使用Volatile或者同步锁。
🥩 小结
我们可以根据数据类型分类:
基本数据类型:
- 写:在构造函数内对
final
域写入,随后将构造函数的引用赋值给一个引用变量,操作不能重排序。即禁止final
域写重排序到构造方法之外。 - 读:初次读一个包含
final
域的对象的引用和随后初次写这个final
域,不能重排序。
引用数据类型:
在基本数据类型上额外增加约束:
禁止在构造函数对一个final修饰的对象的成员域属性的写入与随后将这个被构造的对象的引用赋值给引用变量进行重排序。
🌸总结
相信各位看官都对final这一个关键字有了一定了解吧,其实额外扩展自己的知识面也是相当有必要滴,不然别人追问你的时候,你会哑口无言,而一旦你自己每天都深入剖析知识点后,你在今后的对答中都会滔滔不绝,绽放光芒的!!!对吧,我们还有一把东西等着我们探索和摸索中!接下来就是潜心学习一段时间,不浮躁,不气馁!
让我们也一起加油吧!本人不才,如有什么缺漏、错误的地方,也欢迎各位人才大佬评论中批评指正!当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才大佬们给个点赞、收藏下吧,一键三连,非常感谢!
学到这里,今天的世界打烊了,晚安!虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!
感谢各位看到这里!愿你韶华不负,青春无悔!
-
-
Java final 详解
2021-09-03 18:01:12一 final 基础使用 1.1 修饰类 当某个类的整体定义为 final 时,就表明了你不能打算继承该类,而且也不允许别人这么做。即这个类是不能有子类的。 注意:final 类中的所有方法都隐式为 final,因为无法覆盖他们,...一 final 基础使用
1.1 修饰类
当某个类的整体定义为 final 时,就表明了你不能打算继承该类,而且也不允许别人这么做。即这个类是不能有子类的。
注意:final 类中的所有方法都隐式为 final,因为无法覆盖他们,所以在 final 类中给任何方法添加 final 关键字是没有任何意义的。
这里顺道说说 final 类型的类如何拓展? 比如 String 是 final 类型,我们想写个 MyString 复用所有 String 中方法,同时增加一个新的 toMyString() 的方法,应该如何做?
设计模式中最重要的两种关系,一种是继承/实现;另外一种是组合关系。所以当遇到不能用继承的 (final 修饰的类),应该考虑用组合,如下代码大概写个组合实现的意思:
class MyString{ private String innerString; // ...init & other methods // 支持老的方法 public int length(){ return innerString.length(); // 通过innerString调用老的方法 } // 添加新方法 public String toMyString(){ //... } }
1.2 修饰方法
- private 方法是隐式的 final
- final 方法是可以被重载的
private final
类中所有 private 方法都隐式地指定为 final 的,由于无法取用 private 方法,所以也就不能覆盖它。可以对 private 方法增添 final 关键字,但这样做并没有什么好处。看下下面的例子:
public class Base { private void test() { } } public class Son extends Base{ public void test() { } public static void main(String[] args) { Son son = new Son(); Base father = son; //father.test(); } }
Base 和 Son 都有方法 test(),但是这并不是一种覆盖,因为 private 所修饰的方法是隐式的 final,也就是无法被继承,所以更不用说是覆盖了,在 Son 中的 test() 方法不过是属于 Son 的新成员罢了,Son 进行向上转型得到 father,但是 father.test() 是不可执行的,因为 Base 中的 test 方法是 private 的,无法被访问到。
final 方法是可以被重载的
我们知道父类的final方法是不能够被子类重写的,那么final方法可以被重载吗? 答案是可以的,下面代码是正确的。
public class FinalExampleParent { public final void test() { } public final void test(String str) { } }
1.3 修饰参数
Java 允许在参数列表中以声明的方式将参数指明为 final,这意味这你无法在方法中更改参数引用所指向的对象。这个特性主要用来向匿名内部类传递数据。
1.4 修饰变量
所有的 final 修饰的字段都是编译期常量吗?
现在来看编译期常量和非编译期常量, 如:
public class Test { //编译期常量 final int i = 1; final static int J = 1; final int[] a = {1,2,3,4}; //非编译期常量 Random r = new Random(); final int k = r.nextInt(); public static void main(String[] args) { } }
k 的值由随机数对象决定,所以不是所有的 final 修饰的字段都是编译期常量,只是 k 的值在被初始化后无法被更改。
static final
一个既是 static 又是 final 的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过。
import java.util.Random; public class Test { static Random r = new Random(); final int k = r.nextInt(10); static final int k2 = r.nextInt(10); public static void main(String[] args) { Test t1 = new Test(); System.out.println("k="+t1.k+" k2="+t1.k2); Test t2 = new Test(); System.out.println("k="+t2.k+" k2="+t2.k2); } }
上面代码某次输出结果:
k=2 k2=7 k=8 k2=7
我们可以发现对于不同的对象k的值是不同的,但是k2的值却是相同的,这是为什么呢? 因为static关键字所修饰的字段并不属于一个对象,而是属于这个类的。也可简单的理解为static final所修饰的字段仅占据内存的一个一份空间,一旦被初始化之后便不会被更改。
blank final
Java 允许生成空白 final,也就是说被声明为 final 但又没有给出定值的字段,但是必须在该字段被使用之前被赋值,这给予我们两种选择:
- 在定义处进行赋值(这不叫空白final)
- 在构造器中进行赋值,保证了该值在被使用前赋值。
这增强了 final 的灵活性。 看下面代码:
public class Test { final int i1 = 1; final int i2;//空白final public Test() { i2 = 1; } public Test(int x) { this.i2 = x; } }
可以看到 i2 的赋值更为灵活。但是请注意,如果字段由 static 和 final 修饰,仅能在定义处赋值,因为该字段不属于对象,属于这个类。
二 final 域重排序规则
上面我们聊的 final 使用,应该属于 Java 基础层面的,当理解这些后我们就真的算是掌握了 final 吗? 有考虑过 final 在多线程并发的情况吗? 在 java 内存模型中我们知道 java 内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有编译器和处理器重排序。那么,在多线程情况下,final会进行怎样的重排序? 会导致线程安全的问题吗? 下面,就来看看 final 的重排序。
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() 方法。
2.1.1 写 final 域重排序规则
写 final 域的重排序规则禁止对 final 域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:
- JMM 禁止编译器把 final 域的写重排序到构造函数之外
- 编译器会在 final 域写之后,构造函数 return 之前,插入一个 storestore 屏障。这个屏障可以禁止处理器把 final 域的写重排序到构造函数之外。
我们再来分析 writer 方法,虽然只有一行代码,但实际上做了两件事情:
- 构造了一个 FinalDemo 对象
- 把这个对象赋值给成员变量 finalDemo。
我们来画下存在的一种可能执行时序图,如下:
由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。
2.1.2 读final域重排序规则
读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。
read()方法主要包含了三个操作:
- 初次读引用变量finalDemo
- 初次读引用变量finalDemo的普通域a
- 初次读引用变量finalDemo的final与b
假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:
读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。
2.2 final 域为引用类型
我们已经知道了final域是基本数据类型的时候重排序规则是怎么的了? 如果是引用数据类型了? 我们接着继续来探讨。
2.2.1 对final修饰的对象的成员域写操作
针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对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方法。下图就以这种执行时序出现的一种情况来讨论(耐心看完才有收获)。
由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。2.2.2 对 final 修饰的对象的成员域读操作
JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。
2.3 关于final重排序的总结
按照final修饰的数据类型分类:
1、基本数据类型
- final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
- final域读:禁止初次读对象的引用与读该对象包含的final域的重排序。
2、引用数据类型
- 额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序
三 final 详解
3.1 final 原理
上面我们提到过,写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。
很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障可以省略。由于不会对有间接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说,以X86为例的话,对final域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理器
3.2 为什么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方法另一个线程执行reader方法。因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。3.3 使用 final 的限制条件和局限性
当声明一个 final 成员时,必须在构造函数退出前设置它的值。
public class MyClass { private final int myField = 1; public MyClass() { ... } }
或者
public class MyClass { private final int myField; public MyClass() { ... myField = 1; ... } }
将指向对象的成员声明为 final 只能将该引用设为不可变的,而非所指的对象。
下面的方法仍然可以修改该 list。
private final List myList = new ArrayList(); myList.add("Hello");
声明为 final 可以保证如下操作不合法
myList = new ArrayList(); myList = someOtherList;
如果一个对象将会在多个线程中访问并且你并没有将其成员声明为 final,则必须提供其他方式保证线程安全。 " 其他方式 " 可以包括声明成员为 volatile,使用 synchronized 或者显式 Lock 控制所有该成员的访问。
3.4 再思考一个有趣的现象
byte b1=1; byte b2=3; byte b3=b1+b2;//当程序执行到这一行的时候会出错,因为b1、b2可以自动转换成int类型的变量, // 运算时java虚拟机对它进行了转换,结果导致把一个int赋值给byte-----出错
如果对 b1 b2 加上 final 就不会出错
final byte b1=1; final byte b2=3; byte b3=b1+b2;//不会出错,相信你看了上面的解释就知道原因了。
-
hibernate-release-5.0.7.Final
2016-02-03 11:42:45hibernate 最新的开发包, 网上搜了下, 国内居然很少有最新版的下载资源, 从官方网站下载下来传CSDN上, 但是大小限制只能70M, 所以只能放到百度云盘. -
Java中的final变量、final方法和final类
2018-12-13 17:39:241、final变量 final关键字可用于变量声明,一旦该变量被设定,就不可以再改变该变量的值。通常,由final定义的变量为常量。例如,在类中定义PI值,可以使用如下语句: final double PI=3.14; 在Java中定义全局...1、final变量
final关键字可用于变量声明,一旦该变量被设定,就不可以再改变该变量的值。通常,由final定义的变量为常量。例如,在类中定义PI值,可以使用如下语句:
final double PI=3.14;
在Java中定义全局常量,通常使用public static final修饰,这样的常量只能在定义是被赋值。
public static final double PI_VAULE = 3.14;
规范:被定义为final的常量定义时需要使用大写字母命名,并且中间使用下划线进行连接。
常量示例:
import java.util.Random; class Test { int i = 0; } /** * 常量示例 * * @author pan_junbiao * */ public class FinalData { static Random rand = new Random(); private final int VALUE_1 = 9; // 声明一个final常量 private static final int VALUE_2 = 10; // 声明一个final、static常量 private final Test test = new Test(); // 声明一个final引用 private Test test2 = new Test(); // 声明一个不是final的引用 private final int[] a = { 1, 2, 3, 4, 5, 6 }; // 声明一个定义为final的数组 private final int i4 = rand.nextInt(20); private static final int i5 = rand.nextInt(20); public String toString() { return "i4值:" + i4 + " i5值:" + i5 + " "; } public static void main(String[] args) { FinalData data = new FinalData(); // 报错:不能改变定义为final的常量值 // data.VALUE_1 = 8; // 报错:不能改变定义为final的常量值 // data.VALUE_2 = 9; // 报错:不能将定义为final的引用指向其他引用 // data.test = new Test(); // 正确: 可以对指定为final的引用中的成员变量赋值 data.test.i = 1; // 正确: 可以将没有定义为final的引用指向其他引用 data.test2 = new Test(); // 报错:不能对定义为final的数组赋值 // int b[] = { 7, 8, 9 }; // data.a = b; // 但是final的数组中的每一项内容是可以改变的 for (int i = 0; i < data.a.length; i++) { data.a[i] = 9; } System.out.println(data); System.out.println("data2"); System.out.println(new FinalData()); } }
执行结果:
从上述执行结果中可以发现i5的值是相同的。
全局常量:
我们知道一个被定义为final的对象引用只能指向唯一一个对象,不可以将它再指向其它对象,但是一个对象的值却是可以改变的,那么为了使一个常量真正做到不可更改,可以将常量声明为static final。
示例:在项目中创建FinalStaticData类,在该类中创建Random类的对象,在主方法中分别输出类中定义的final变量a1与a2。
import static java.lang.System.out; import java.util.Random; /** * FinalStaticData类 * * @author pan_junbiao * */ public class FinalStaticData { private static Random rand = new Random(); // 实例化一个Random类对象 // 随机产生0~10之间的随机数赋予定义为final的a1 private final int a1 = rand.nextInt(10); // 随机产生0~10之间的随机数赋予定义为static final的a2 private static final int a2 = rand.nextInt(10); public static void main(String[] args) { FinalStaticData fdata = new FinalStaticData(); // 实例化一个对象 // 调用定义为final的a1 out.println("重新实例化对象调用a1的值:" + fdata.a1); // 调用定义为static final的a2 out.println("重新实例化对象调用a2的值:" + fdata.a2); // 实例化另外一个对象 FinalStaticData fdata2 = new FinalStaticData(); out.println("重新实例化对象调用a1的值:" + fdata2.a1); out.println("重新实例化对象调用a2的值:" + fdata2.a2); } }
运行结果:
从本示例运行结果中可以看出,定义为final的常量不是恒定不变的,将随机数赋予定义为final的常量,可以做到每次运行程序时改变a1的值。但是a2与a1不同,由于它被声明为static final形式,所以在内存中为a2开辟了一个恒定不变的区域,当再次实例化一个FinalStaticData对象时,仍然指向a2这块内存区域,所以a2的值保存不变。a2是在装载时被初始化,而不是每次创建新对象时被初始化;而a1会重新实例化对象时被更改。
最后总结一下在程序中final数据可以出现的位置,如下程序。
/** * 总结一下在程序中final数据可以出现的位置 * * @author pan_junbiao * */ public class FinalDataTest { // final成员变量不可更改 final int VALUE_ONE = 6; // 在声明final成员变量时没有赋值,称为空白final final int BLANK_FINALVAULE; // 在构造方法中为空白final赋值 public FinalDataTest() { BLANK_FINALVAULE = 8; } // 设置final参数,不可以改变参数x的值 int doIt(final int x) { return x + 1; } // 局部变量定义为final,不可以改变i的值 void doSomething() { final int i = 7; } }
2、final方法
首先,我们应该了解定义为final的方法不能被重写。
将方法定义为final类型可以防止任何子类修改该类的定义与实现方式,同时定义为final的方法执行效率要高于非final方法。在修饰权限中曾经提到过private修饰符,如果一个父类的某个方法被设置为private修饰符,子类将无法访问该方法,自然无法覆盖该方法,所以一个定义为private的方法隐式被指定为final类型,这样无须将一个定义为private的方法再定义为final类型。
语法:
private final void test() { }
3、final类
定义为final的类不能被继承。
如果希望一个类不允许任何类继承,并且不允许其他人对这个类有任何改动,可以将这个类设置为final形式。
final类的语法如下:
final 类名{}
如果将某个类设置为final形式,则类中的所有方法都被隐式设置为final形式,但是final类中的成员变量可以被定义为final或非final形式。
示例:在项目中创建FinalClass类,在类中定义doit()方法和变量a,实现在主方法中操作变量a自增。
/** * 定义final类 * * @author pan_junbiao * */ final class FinalClass { int a = 3; void doit() { } public static void main(String args[]) { FinalClass f = new FinalClass(); f.a++; System.out.println(f.a); // 结果:4 } }
4、总结
下面总结了一些使用final关键字的好处:
(1)final关键字提高了性能。JVM和Java应用都会缓存final变量。
(2)final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
(3)使用final关键字,JVM会对方法、变量及类进行优化。
不可变类:
创建不可变类要使用final关键字。不可变类是指它的对象一旦被创建了就不能被更改了。String是不可变类的代表。不可变类有很多好处,譬如它们的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销等等。
关于final的重要知识点:
(1)final关键字可以用于成员变量、本地变量、方法以及类。
(2)final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。
(3) 你不能够对final变量再次赋值。
(4)本地变量必须在声明时赋值。
(5)在匿名类中所有变量都必须是final变量。
(6)final方法不能被重写。
(7)final类不能被继承。
(8)final关键字不同于finally关键字,后者用于异常处理。
(9)final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。
(10)接口中声明的所有变量本身是final的。
(11)final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
(12)final方法在编译阶段绑定,称为静态绑定(static binding)。
(13)没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。
(14)将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
(15)按照Java代码惯例,final变量就是常量,而且通常常量名要大写。
-
final
2020-07-21 08:30:151)final修饰的类不能被继承,也就是不能派生子类 2)final修饰的方法不能被子类重写 3)final修饰的变量是常量,只能赋值一次 final修饰的局部变量可以不赋初值,但是final修饰的成员变量必须在定义时就赋初值 ... -
hibernate-release-4.2.21.Final官方原版
2015-11-19 09:19:52hibernate-release-4.2.21.Final官方原版 -
hibernate-release-5.0.2.Final API.chm
2015-10-29 16:13:54hibernate-release-5.0.2.Final API.chm 参考文档,有了这个,就不用联网查看API参考文档了 -
Java多线程系列—final的各种用法与意义(07)
2021-06-16 18:20:23final final 是 Java 中的一个关键字,简而言之,final 的作用意味着“这是无法改变的”。 不过由于 final 关键字一共有三种用法,它可以用来修饰变量、方法或者类,而且在修饰不同的地方时,效果、含义和侧重点也会... -
老哥,Java 中 final 和 effectively final 到底有什么区别?
2020-02-14 09:27:23有小伙伴很认真地参考着学习了一下,并且在学习的过程中提出了新的问题:“老哥,当我在使用 Lambda 表达式中使用变量的时候,编译器提醒我‘Local variable limit defined in an enclosing scope must be final or ... -
hibernate-3.6.10.Final.rar
2014-04-07 12:40:30hibernate-3.6.10.Final.rar,用于Java开发中的Hibernate技术 -
奇技淫巧:Java中的final字段真的不能修改么?(怎样修改final字段)
2020-02-18 15:43:48怎样修改Java中的final字段? 先说答案:通过反射是可以修改final字段的! ps:但是修改后的数据能不能生效(正确访问到修改后的数据)就得看情况了,不同的数据类型、不同的数据初始化方式、不同的访问方式都可能导致... -
Java中final关键字
2021-10-04 12:40:47一,概述 final关键字代表最终、不可改变的。 常见四种用法: 1. 可以用来修饰一个类 2. 可以用来修饰一个方法 3. 还可以用来修饰一个局部变量 ...eg:定义一个final类pubilc final MyClass,继承报错! 2).. -
final cut pro 内存不足可以更改缓存空间吗 final cut pr
2020-12-20 09:42:27Final cut pro 提示内存不足,一般是在渲染和输出时解决方案:1、一般渲染提示内存不足,先看看系统设置里边,存储路径是不是在比较大的盘里。2、系统设置里,内存与高速缓存里边,应用程序值降低一点(75/65),渲染... -
final关键字用法总结
2019-01-23 22:08:51一、final关键字的基本用法 在java中,final关键字可以用来修饰类、方法、变量(包括成员变量和局部变量)。下面我们从这三个方面了解一下final的用法。 1、修饰类 final修饰一个类时,表示该类不能继承。final类中... -
final、static和static final区别和使用
2020-07-22 10:26:52final、static和static final用法 1.final关键字 final关键字修饰变量: 1.这个变量必须在构造对象时就能够初始化 2.如果是基本数据类型的变量,那么这个变量一旦初始化就不能更改,相当于一个常量 3.如果是引用数据... -
Java基础知识——static和final
2022-04-02 15:09:53static修饰代码块 静态代码块在类构造函数之前就被执行,具体为: 父类静态代码块——>子类静态代码块——>父类非静态代码块——>父类构造方法——>子类非静态代码块——>子类构造方法 二、final 1. final修饰变量 ... -
final 和effectively final区别
2019-01-03 10:02:01针对于jdk1.8,对于一个变量,如果没有给它加final修饰,而且没有对它的二次赋值,那么这个变量就是effectively final(有效的不会变的),如果加了final那肯定是不会变了哈。就是这个意思。 在学习局部内部类... -
夯实Java基础系列4:一文了解final关键字的特性、使用方法,以及实现原理
2018-04-23 12:18:56但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。 public class FinalTest { final int p; final int q=3... -
hibernate-validator-5.0.1.Final.jar
2014-02-12 11:12:51hibernate-validator-5.0.1.Final.jar -
java中的Static、final、Static final各种用法
2019-10-22 21:38:13对Static、final、Static final这几个关键词熟悉又陌生?想说却又不知怎么准确说出口?好的,本篇博客文章将简短概要出他们之间的各自的使用,希望各位要是被你的面试官问到了,也能从容的回答… static 加载:... -
Java中的状态修饰符final与static
2021-09-11 22:44:00final static Java中的状态修饰符如下 final(最终态) static(静态) final final关键字是最终的意思,可以修饰成员方法,成员变量,类。 final修饰的特点 修饰方法:表明该方法是最终方法,不能被重写 修饰... -
String类为什么是final类型?
2021-05-14 14:42:12String类为什么是final类型? 首先,先得清楚 final 这个关键字。 final的出现就是为了为了不想改变,而不想改变的理由有两点:设计(安全)或者效率。 final 修饰的类是不被能继承的,所以 final 修饰的类是不能被篡改... -
Java——final关键字,String为什么不可变
2022-04-25 11:02:05我们在使用某些Java的类或者方法、属性(比如:String类)时,总是会发现前面有一个final进行修饰,那为什么要使用final呢?final在其中起到了什么作用呢?且往下看 final是什么? 英语角度:为形容词最终的意思... -
《菜鸟读并发》java内存模型之final
2020-02-28 22:37:40final 关键字的字面意思是最终的,不可修改的。这似乎是一个看见名字就大概知道怎么用的语法,但你是否有深究过final在各个场景中的具体用法,注意事项,以及背后涉及的Java设计思想呢? final可以修饰什么 类 ... -
Variable used in lambda expression should be final or effectively final
2019-09-06 18:09:08我们在使用 Java8 lambda 表达式的时候常常会遇到这样的编译报错 Variable used in lambda expression should be final or effectively final -
java中final的理解
2021-03-15 01:24:24final修饰变量表示变量初始化后就不能再改变。一、对于基础类型来说,用final修饰后其值不可以改变。1.final int a;a = 5;2.final int a = 5;二、对于引用类型来说,用final修饰后其引用的对象不可改变,但可以改变... -
Final IK详解
2020-07-09 21:57:53Final IK详解[Ⅰ] Limb IK Final IK是 Uinty3D中的一个付费插件,它提供了多种反向动力学解决方案。 ——————————————————————————————————————————— [Ⅰ] Limb IK Limb ... -
Java中final修饰符的理解
2022-02-10 16:21:57记录一下今天复习的final修饰符,其中 final类的成员方法隐式被final修饰部分 仅代表我个人理解 总结如下: final有三种使用方法: 1、final修饰类 final修饰类表示这个类不能被继承, 这个类的成员变量可以选择...