典型回答:
final 可以用来修饰可变类、方法、对象,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法是不可以重写的。
finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定的资源回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 起开始被标记为 deprecated。
考点分析
上面的回答主要是从概念的角度出发的,其实还可以从很多方面再深入探讨,讲讲对性能、并发、对象生命周期或垃圾收集基本过程等方面的理解。
推荐使用 final 关键字来明确表示我们代码的语义、逻辑意图,这已经被证明在很多场景下是非常好的实践,例如:
- 我们可以将方法或者类声明为 final,这样就可以明确告知别人,这些行为是不可修改的。
- 使用 final 修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有的方法参数、本地变量、成员变量声明成 final。
- final 产生了某种程度的不可变 (immutable) 的效果,所以,可以用来保护只读数据,尤其是在并发编程中,因为明确地不能再赋值 final 变量,有利于减少同步开销,也可以省去一些防御性拷贝地必要。
总结起来就是:声明 final 关键字,从语义上告诉我们自己和阅读、合作一起编程的人,这是一个不可变对象,让大家心中有底。同时 final 关键字的修饰,可以避免我们自己不小心的意外赋值,也可以保护只读数据,减少防御性拷贝的必要,避免别人意外赋值。
对于finally,我认为明确知道怎么使用就够了,推荐使用 Java 7 中添加的 try-with-resources 语句,因为通常 Java 平台能够更好地处理异常情况,编码量也要少很多,何乐而不为呢。
另外可以注意下 finally 一些特殊的不会被执行的情况。比如
- System.exit(1)
try {
// do something
System.exit(1);
} finally{
System.out.println(“Print from finally”);
}
- 死循环
try {
while (true) {
System.out.println("hello world");
}
} finally {
System.out.println("hello world");
}
- 线程被杀死
当执行 try-finally 的线程被杀死时。finally 也无法执行。
对于 finalize,我们要明确它是不被推荐使用的,在业界的实践证明中,一再证明它不是个好的办法,在 Java 9 中,甚至将 Object.finalize() 标记为 deprecated!如果没有特别的原因,不要实现 finalize 方法,也不要指望利用它来进行资源回收。
简单说,我们无法保证 finalize 什么时候执行,执行是否符合预期。使用不当会影响性能,导致程序死锁、挂起等。
如果要实现资源回收,推荐使用 try-with-resources 或者 try-finally 机制。如果需要额外处理,可以考虑 Java 提供的 Cleaner 机制或者其他替代办法。
知识扩展
- final 不是 immutable !
当 final 修饰对象时,实际上只能约束指向这个对象的引用不可以被重新赋值,但是对象内部的行为不被 final 影响,典型的行为就是 final 修饰普通 List 时,普通 List 仍然可以添加、删除对象。另外,如果要使 List 实现 immutable,可以考虑 List.of() 方法。
- 如何构建一个 Immutable 对象
Immutable 在很多场景是非常棒的选择,某种意义上说,Java 语言目前并没有原生的不可变支持,如果需要实现 Immutable 的类,我们需要做到:
- 将 class 自身声明为 final,这样别人就不能通过扩展来绕过限制了。
- 将所有成员变量定义为 private 和 final,并且不要实现 setter 方法。
- 通常构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改。
- 如果确实需要实现 gettter 方法,或者其他可能会返回内部对象的方法,使用 copy-on-write 原则,创建私有的 copy。
- finalize 真的那么不堪吗?
答案是肯定的。
- finalize 会拖慢垃圾收集,对于消耗高频的资源,这是不可忍受的。
finalize 被设计成在垃圾收集之前调用,一旦实现了非空的 finalize 方法,就会导致回收呈现数量级上的变慢,有人专门做过 benchmark,大概是 40~50 倍的下降。
实践中,因为 finalize 拖慢垃圾收集,导致大量对象堆积,这也是一种典型的 OOM 的原因。
- finalize 回掩盖资源回收时出错的信息,下面这段代码节选自 JDK 中的 java.lang.ref.Finalizer
private void runFinalizer(JavaLangAccess jla) {
// ... 省略部分代码
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);
// Clear stack slot containing this variable, to decrease
// the chances of false retention with a conservative GC
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
- 替代 finalize 的方法。
Java 平台目前正在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。 Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。
下面是一段 JDK 提供的 Cleaner 的样例
public class CleaningExample implements AutoCloseable {
// A cleaner, preferably one shared within a library
private static final Cleaner cleaner = <cleaner>;
static class State implements Runnable {
State(...) {
// initialize State needed for cleaning action
}
public void run() {
// cleanup action accessing State, executed at most once
}
}
private final State;
private final Cleaner.Cleanable cleanable
public CleaningExample() {
this.state = new State(...);
this.cleanable = cleaner.register(this, state);
}
public void close() {
cleanable.clean();
}
}
吸取了 finalize 的教训,每个 Cleaner 的操作都是独立的,它有自己的运行线程,可以避免意外死锁等问题。
但是从可预测的角度来判断,Cleaner 或者幻象引用改善的程度仍然是有限的,由于种种原因导致幻象引用堆积,同样回出现问题。所以,Cleaner 适合作为一种最后的保证手段,而不是完全依赖 Cleaner 进行资源回收。