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 方法
  • 通过构造器初始化成员,若构造器传入引用数据类型,需要进行深拷贝(复制该类的新实例而不是引用),确保类的不可变。
  • 在公开修改类状态的方法时,必须始终返回该类的新实例。
  • 必要时重写 hashCodeequals 方法,保证两个 equals 方法判断为相等的对象,其 hashCode 也应该相等

2.3 举例

比如,java中八个基本类型的包装类,和 String 类都属于不可变类。举例看看String的实现:

/** The value is used for character storage. */
private final char value[];

可以看出Stringvalue就是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关键字的更多相关文章

  1. JAVA面向对象-----final关键字

    JAVA面向对象-–final关键字 1:定义静态方法求圆的面积 2:定义静态方法求圆的周长 3:发现方法中有重复的代码,就是PI,圆周率. 1:如果需要提高计算精度,就需要修改每个方法中圆周率. 4 ...

  2. 聊聊Java的final关键字

    Java的final关键字在日常工作中经常会用到,比如定义常量的时候.如果是C++程序员出身的话,可能会类比C++语言中的define或者const关键字,但其实它们在语义上差距还是挺大的. 在Jav ...

  3. java之final关键字

    final关键字(可以读不可以写.只读) 1.final的变量的值不能够被改变 ①.final的成员变量 ②.final的局部变量(形参) //意思是“实参”一旦传进我的方法里面,就不允许改变 2.f ...

  4. Java的final关键字详解

    Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使 ...

  5. java中final 关键字的作用

    final 关键字的作用 java中的final关键字可以用来声明成员变量.本地变量.类.方法,并且经常和static一起使用声明常量. final关键字的含义: final在Java中是一个保留的关 ...

  6. Java基础 -- final关键字

    在java的关键字中,static和final是两个我们必须掌握的关键字.不同于其他关键字,他们都有多种用法,而且在一定环境下使用,可以提高程序的运行性能,优化程序的结构.下面我们来了解一下final ...

  7. Java中final关键字修饰变量、方法、类的含义是什么

    Java中的关键字final修饰变量.方法.类分别表示什么含义? 先看一个简单的介绍 修饰对象 解释说明 备注 类 无子类,不可以被继承,更不可能被重写. final类中的方法默认是final的 方法 ...

  8. java浅析final关键字

    谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法. ...

  9. 关于Java中final关键字的详细介绍

    Java中的final关键字非常重要,它可以应用于类.方法以及变量.这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使 ...

  10. java基础---->final关键字的使用

    这里介绍一些java基础关于final的使用,文字说明部分摘自java语言规范.心甘情愿这四个字,透着一股卑微,但也有藏不住的勇敢. Final关键字的说明 一.关于final变量规范说明 .A fi ...

随机推荐

  1. C#将汉字转换为拼音

    首先上效果图 方法调用 private void txt_Chinese_TextChanged(object sender, EventArgs e) { txt_PinYIn.Text = //调 ...

  2. 入门 shell 从脚本开始 - lazy_find

    编写脚本实现在指定文件路径下查找文件夹或文件名.   脚本如下: #!/bin/sh # lazy find # GNU All-Permissive License # Copying and di ...

  3. STM32F429 实测基本数据类型占用空间

    实测代码 1 void CalculateDataTypeSize(void) 2 { 3 printf("sizeof(char} = %u\r\n", sizeof(char) ...

  4. css - absolute居中

    position:absolut; left:50%; top:50%; margin-left:  -(自身一半宽度); margin-top: -(自身一半高度)

  5. [转帖]ORACLE新参数MAX_IDLE_TIME和MAX_IDLE_BLOCKING_TIME简介

    https://www.cnblogs.com/kerrycode/p/16856171.html Oracle 12.2 引入了新参数MAX_IDLE_TIME.它可以指定会话空闲的最大分钟数.如果 ...

  6. [转帖]OceanBase 4.2.1 LTS 发版 | 一体化数据库首个长期支持版本

    2013.11.20 https://open.oceanbase.com/blog/7746655008?_gl=1*1qv10rf*_ga*Nzk3MjIxOTk0LjE3MDI2MTAxMzk. ...

  7. [转帖]nginx配置文件中对于if条件语句的写法(附nginx跨域文件配置)

    前言 在nginx配置文件中,可以使用if语句,但是对于else语句其实是不支持的,并且and条件和or条件也是不支持的 实现 else条件的写法 新建一个开关变量flag,初始值为0,如果为1说明进 ...

  8. [转帖]Linux Shell编程 循环语法

    https://zhuanlan.zhihu.com/ for循环 for 循环是固定循环,也就是在循环时已经知道需要进行几次循环.有时也把 for 循环称为计数循环.语法: for 变量 in 值1 ...

  9. [转帖]Skip List--跳表(全网最详细的跳表文章没有之一)

    https://www.jianshu.com/p/9d8296562806 跳表是一种神奇的数据结构,因为几乎所有版本的大学本科教材上都没有跳表这种数据结构,而且神书<算法导论>.< ...

  10. [转帖]JVM监控及诊断工具-命令行

    https://www.cnblogs.com/xiaojiesir/p/15622372.html 性能指标 停顿时间(响应时间) 提交请求和返回响应之间使用的时间,一般比较关注平均响应时间 常用操 ...