> 文章列表 > 善用 final 关键字,提升编码内功的一大捷径

善用 final 关键字,提升编码内功的一大捷径

善用 final 关键字,提升编码内功的一大捷径

目录

final关键字的作用

修饰类

修饰方法

修饰成员变量

修饰局部变量

static final 常量的编译期优化

不可变对象在 GC 中的优化

final 域的重排序规则

题外话

如何扩展被 final 修饰的类?


在阅读本文章前,我想问在读的你一个问题。

为什么源码中喜欢用 final 关键字修饰变量呢?

下面截取自 JDK 8 中 HashMap 类的官方源码

如果你已经对该问题了如指掌,那么恭喜,你已经掌握了 final 关键字的奥秘 ~

final关键字的作用

修饰类

final 关键字修饰类代表该类不可被继承

修饰方法

final 关键字修饰方法代表该方法不能被重写

修饰成员变量

解决上述问题有三种方式

第一:构造器中初始化

class Person {final String name;public Person(String name) {this.name = name;}public void say() {System.out.println("I am " + this.name);}}

 第二:显示赋值 

class Person {final String name = "iron man";public void say() {System.out.println("I am " + this.name);}}

第三:代码块中初始化

class Person {final String name;{name = "iron man";}public void say() {System.out.println("I am " + this.name);}}

最后来看Java中对于final修饰的成员变量有哪些限制

对于 final 修饰的基本类型变量,在该属性被初始化后就不能被重新赋值了。

对于 final 修饰的引用类型变量,虽然在该属性被初始化后不能被重新赋值【这里不能重新赋值指的是不能指向另一个地址值】但能够修改里面的属性内容。

修饰局部变量

使用限制:使用前必须显示初始化,否则报错。

其它限制与成员变量一致,这里不再赘述。 

static final 常量的编译期优化

static 搭配 final 修饰成员变量表示该变量为编译期常量

编译期常量必须显示初始化或者在静态代码块中初始化,否则编译器会爆红。

编译期优化演示:

FinalTest1中定义了一个DEFAULT_VALUE常量,FinalTest2中引用这个常量进行输出

class FinalTest1 {static final String DEFAULT_VALUE = "hello world";static {System.out.println("FinalTest1 被初始化了...");}}class FinalTest2 {public static void main(String[] args) {System.out.println(FinalTest1.DEFAULT_VALUE);}}

加了final关键字结果如下:

没有加 final 关键字结果如下:

从而我们可以得出结论,一个类中引用另一个类的常量并不会引发该类进行初始化。 

这里补充一下类初始化时机相关知识

主动引用
在Java虚拟机规范中,对于类加载中的第一阶段“加载”并没有明确的规定,但是对于“初始化”阶段什么时候开始则做出了非常严格的规定,指出有且只有 6 种场景会触发初始化。

1、遇到new、getStatic、putStatic、invokeStatic这四条字节码指令时:使用new创建一个实例对象、读取或设置一个类型的静态字段【被final修饰除外】、调用一个类型的静态方法的时候。
2、使用反射调用方法时,如果类型没有被初始化,那么首先要进行初始化。

3、当初始化类时,如果其父类没有被初始化,则会先触发其父类的初始化

4、当虚拟机启动时,用户首先需要制定一个main方法作为启动类,虚拟机会先初始化这个主类。

5、JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial这四种类型的方法句柄,并且这个方法句柄没有被初始化时,则需先触发其初始化。

6、JDK8中,一个接口定义了默认方法(被default修饰的接口方法),如果有这个接口的实现类发生了初始化,那么该接口要先在该实现类初始化之前初始化。

这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化操作,称为被动引用。

被动引用

  • 通过子类引用父类的静态字段,不会导致子类初始化。
  • 通过数组定义来引用类,不会触发此类的初始化。
  • 常量在编译阶段会存入调用类的常量池中,本质上没有直接应用到定义常量的类,因此不会触发定义常量的类的初始化。

不可变对象在 GC 中的优化

垃圾回收算法在扫描存活对象的时候会从GC Roots节点开始,扫描所有存活对象的引用来构建对象图。不可变对象在 GC 中的优化主要体现在老年代中,如果老年代对象引用了新生代对象,HotSpot 虚拟机中使用了一种称为卡表的结构来避免每次Young GC都扫描老年代中全部的对象引用。简单来说,当老年代中的对象发生对新生代中对象产生新的引用关系或者释放引用时,都会在卡表中相应的标记为脏(dirty),所以在发生Young GC时,扫描这些脏的项就可以了。而可变对象对其它对象的引用关系可能会频繁变化,并且有可能在运行过程中持有越来越多的引用,特别是容器。这些都会导致对应的卡表项被频繁的标记为脏项。不可变对象的引用关系非常稳定,在扫描卡表时就不会扫到它们对应的项了。

final 域的重排序规则

对于 final 域,编译器和处理器要遵守两个重排序规则:

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能进行重排序。【会在写final域之后,构造函数返回之前插入一个StoreStore屏障

初次读一个包含 final 域对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。【会在读final域之前插入一个LoadLoad屏障

也就是说,如果对象构造函数内有 final 域,必须先用构造函数构造对象,再把对象赋值给其它引用。如果对象有final域,必须先读对象的引用,再读final域。

这两条规则可以确保在引用变量为任意线程可见之前,该引用变量指向的对象final域已经在构造函数中被正确初始化了。

拓展阅读:关键字: final详解 | Java 全栈知识体系

题外话

如何扩展被 final 修饰的类?

final class FinalClass {private String name;public void say() {System.out.println("Hello, I am " + this.name);}}

一种解决思路是提供一个包装类,将final类定义为包装类的成员属性

class FinalClassWrapper {private FinalClass finalClass;public FinalClassWrapper(FinalClass finalClass) {this.finalClass = finalClass;}public void wrapperSay() {System.out.println("Who are you ?");finalClass.say();}}

其实被修饰为 final 的类本就不该被扩展。

想起一句话:所有不能解决的问题,答案都在另外一个层次。 

参考文章:

java基础之 GC