final是Java语言中的修饰符,可以修饰类、方法、属性、函数入参、局部变量,意图表达被修饰对象的最终状态,即不可再被修改。
final关键字的使用场景
使用final关键字一般有以下显示的作用:
- final修饰类 无法被继承
- final修饰类方法 无法被重写(Override)
- final修饰的类属性、函数参数、局部变量 只能被赋值一次
对于final关键字,还有一些隐式的作用:
- final修饰对象 对象的引用只能被赋值一次,但对象本身是可以被修改的
- final修饰基本类型 与生俱来就是线程安全的,因为变量只会被写一次
此外,final关键字可以清晰了表达了代码设计者的意图,也能增强代码的可读性:
-
final修饰函数入参 函数不会对入参进行修改,调用者可以放心的使用该函数,而不用担心调用后传入的参数被篡改。
-
final修饰局部变量 在一个函数中,更为被关注的是一些与逻辑相关的局部变量(返回值,循环体计数器),如果某些局部变量的存在就是为了一次数据的存储,那么,就可以用final修饰这些局部变量。 这样,读代码的时候,就能聚焦到没有通过final修饰的局部变量,更快的理解代码逻辑。
以下是一个final修饰符的使用实例:
import java.util.*;
import java.lang.reflect.Field;
/** 该类不能被继承. */
public final class Boat {
public Boat(final String aName, final int aLength, final Date aDateManufactured){
fName = aName;
fLength = aLength;
fDateManufactured = new Date(aDateManufactured.getTime());
// 以下两行代码会导致编译报错,因为对final修饰函数入参做了赋值操作
//aDateManufactured = null;
//aLength = 0;
}
public Integer bestRaceScore(){
// 返回值result,没有使用final修饰,因为返回值需要为调用者所用
Integer result = Integer.valueOf(0);
// 使用final修饰返回值,会导致编译报错
//final Integer result = Integer.valueOf(0);
// final修饰局部变量scores
final List<Integer> scores = fRaceScores;
for(Integer score : scores){
if (score > result){
result = score;
}
}
return result;
}
// PRIVATE
private final String fName;
private final int fLength;
private List<Integer> fRaceScores = new ArrayList<>();
private final Date fDateManufactured;
}
在早期的Java实现版本中,使用final方法的原因有两个。第一个原因是把锁定,以防任何继承类修改它的含义;第二个原因是效率,编译器会将final方法转为内嵌调用,节省函数调用的时间。 但是函数调用时间一般不会成为性能瓶颈,当函数体过大时,内嵌调用带来的任何性能提升也是微乎其微的,所以,新的Java实现版本中,已经没有这项优化了。
final关键字更重要的还是反映代码设计者的意图。final关键字的使用细节
final修饰局部变量,相当于给编译器一个信号,与普通局部变量不同的是:
- final局部变量只能被赋值一次,而普通局部变量不受限
- 编译优化。如果编译器能够确定final局部变量的值,那么就会把final局部变量当作常量使用
具体可以看如下代码:
final String final_str = "ABC";
String common_str = "ABC";
System.out.println(final_str == common_str); // 打印值为true
String combined_final_str = final_str + "D";
String combined_common_str = common_str + "D";
System.out.println(combined_final_str == combined_common_str); //打印值为false
由于final_str在编译时就已经确定下来了,所以,在combined_final_str中,编译器直接将final_str替换成了字符串,存储到常量池中,所以combined_final_str的地址是指向常量池的。 而combined_common_str变量的地址是需要运行时链接生成的,地址显然与combined_final_str变量的地址是不一样的。
在Java内部类可以访问外围类的局部变量,这是内部类的一项特性,如下代码所示:
public void test(final String outParam) { // 必须通过final修饰形参
final String outLocal = "ABC"; // 必须通过final修饰局部变量
new Thread(){
public void run() {
System.out.println(outParam); // 内部类中直接访问外围类方法的参数
System.out.println(outLocal); // 内部类中直接访问外围类的局部变量
};
}.start();
}
局部变量是有生命周期的,outParam和outLocal的生命周期,伴随这test()方法结束而结束,但此时内部类的生命周期可能还未结束,要做到访问外围的生命周期有限的变量,Java想出了一套办法:就是将这些变量拷贝到内部类的作用域中。
实际上,内部类中访问的变量与外围类的局部变量不是同一个变量,这样就解决了局部变量生命周期有限的问题,但如果不用final来修饰局部变量,就会引发数据不一致的问题,因为内部类如果可以对变量值进行修改,会同一个变量的两份拷贝数据不一致。从这个层面上,我们也能理解为什么final与生俱来的线程安全性。