1 前言
final 的意思是不变的,一般来说可以用于以下三种场景:
- 被 final 修饰的类,表明该类是无法继承的;
- 被 final 修饰的方法,表明该方法是无法覆写的;
- 被 final 修饰的变量,说明该变量在声明的时候,就必须初始化完成,而且以后也不能修改其内存地址。
注意:对于 List、Map 这些集合类来说,被 final 修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。
2 修饰的对象
final 可以用来修饰变量、字段、方法和类。
2.1 变量
可以将变量声明为 final 。final 变量的值一旦分配了就无法再修改了。在一次赋值之前,final 变量必须是明确未赋值的,否则会产生编译错误。如果 final 变量包含对对象的引用,则对象的状态可能会通过对对象的操作进行更改,但变量将始终引用相同的对象。这同样适用于数组,因为数组也是对象;如果一个 final 变量持有一个数组的对象,则可以通过对数组的操作修改数组中的元素,但是该变量始终会引用相同的数组。
注意,如果 final 用于局部变量,那么它可以不用初始化,而如果用于类中的某个字段,那么它必须进行初始化,否则会产生编译错误。
空 final 是指其声明缺少初始化器的 final 变量。
常数变量是一个用常量表达式初始化的基本类型或String类型的 final 变量。 变量是否为常数变量与类的初始化,二进制兼容性和明确赋值有关。
有三种变量会被隐式声明为 final :接口中的字段,try-with-resources 语句中的局部变量以及multi-catch子句中的异常参数。 一个单一的 catch 子句中的 exception 参数永远不会隐式声明为 final ,但是它可以被认为效果等同于 final 。
某些未被声明为 final 变量实际上可以认为效果等同于 final 变量:
- 声明带有初始化器且满足下列所有条件的局部变量:
-
- 永远不会作为赋值操作符的左操作数出现
- 永远不会作为前缀或后缀递增或递减操作符的操作数出现
- 声明缺少初始化器且满足下列所有条件的局部变量:
-
- 无论何时它作为赋值操作符的左操作数出现,都是明确未赋过值的,并且在本次赋值操作前未被明确赋值。即它是明确未赋过值的,并且在本次赋值操作的右操作数之后未被明确赋值
- 方法、构造器、lambda 和异常参数在明确其是否可以认定为等同于 final 时可以被认为是声明带有初始化器的局部变量
如果变量在效果上等同于 final ,那么对其声明添加 final 修饰符并不会引入任何编译错误。相反地,在合法程序中被声明为 final 的局部变量和参数,如果将它们的 final 修饰符移除,那么它们在效果上会等同于 final 。
2.2 类
类可以被声明为 final ,如果一个 final 类的名字出现在另一个类声明的 extends 子句中,会产生编译错误,这意味着 final 类不能有任何子类。
如果一个类同时被声明为 final 和 abstract ,也会产生编译错误,因为这样的类是永远无法实现的。
因为一个 final 类不能有任何子类,所以 final 类的方法永远都不会被覆写。
如果将一个没有被声明的 final 类声明为 final ,那么当这个类的已有子类的二进制文件被加载时,就会抛出一个 VerifyError ,因为 final 类不能有任何子类。因此不推荐在广泛分布的类中进行这种变更。
反之,将一个声明为 final 的类不再声明为 final ,不会破坏与已有二进制文件的兼容性。
2.3 方法
方法可以被声明为 final 以阻止子类覆写或者隐藏它。
如果试图覆写或者隐藏 final 方法,会产生编译错误。
private 方法和所有在 final 类中声明的方法表现就如 final方法一样,因为它们是不可能被覆写的。
在运行时,机器码生成器或优化器可以“内联” final 方法的方法体,将方法的调用替换为其方法体中的代码。内联处理必须保留方法调用的语义。特别是,如果实例方法调用的目标为null,那么即使该方法是内联的,也必须抛出空指针异常。一个Java编译器必须确保异常在正确的位置上被抛出,这样在方法调用之前,传递给方法的实际参数将被按正确的顺序计算过。
2.4 字段
字段可以被声明为 final 。类和实例变量(static 和 非 static 字段)都可以被声明为 final。
空 final 类变量必须在声明它的类的静态初始化器中赋过值,否则会产生编译错误。
空 final 实例变量必须在声明它的类的每一个构造器的末尾赋过值,否则会产生编译错误。
参考资料
- 《Java语言规范 基于 Java SE 8》
- Java Language Specification 8