谈谈final、finally、finalize有什么不同?

Q:谈谈final、finally、finalize有什么不同?

典型回答

final可以用来修饰类、方法、变量,分别有不同的意义,final修饰的class代表不能继承和扩展,final修饰的变量是不可以修改的,而final的方法也是不可以被重写的。

finally则是java保证重点代码一定要被执行的一种机制。我们可以使用try-catch-finally或者try-finally来进行类似关闭jdbc连接,保证unlock锁等动作。

finalize是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源回收。finalize机制现在已经不推荐使用了,并且在jdk9开始被标记为deprecated。

知识扩展

1.将方法或者类声明为final,这样可以明确告诉别人,这些行为是不许修改的。Java核心类库java.lang包下面就有很多的类被final修饰,典型的就有String。String为什么要设计为final?

最主要的原因是因为安全。可以用StringBuffer代替String来演示一下,String不设计成final会有什么影响。看下面这段代码

class Test{
    public static void main(String[] args){
        HashSet<StringBuilder> hs=new HashSet<StringBuilder>();
        StringBuilder sb1=new StringBuilder("aaa");
        StringBuilder sb2=new StringBuilder("aaabbb");
        hs.add(sb1);
        hs.add(sb2);    //这时候HashSet里是{"aaa","aaabbb"}
 
        StringBuilder sb3=sb1;
        sb3.append("bbb");  //这时候HashSet里是{"aaabbb","aaabbb"}
        System.out.println(hs);
    }
}
//Output:
//[aaabbb, aaabbb]

StringBuilder型变量sb1和sb2分别指向了堆内的字面量"aaa"和"aaabbb"。把他们都插入一个HashSet。到这一步没问题。但如果后面我把变量sb3也指向sb1的地址,再改变sb3的值,因为StringBuilder没有不可变性的保护,sb3直接在原先"aaa"的地址上改。导致sb1的值也变了。这时候,HashSet上就出现了两个相等的键值"aaabbb"。破坏了HashSet键值的唯一性。所以千万不要用可变类型做HashMap和HashSet键值。

不可变性支持线程安全。在并发场景下,多个线程同时读一个资源,是不会引发竟态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全。

不可变性支持字符串常量池。这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。

注意final不是immutable!

 final List<String> strList = new ArrayList<>();
 strList.add("Hello");
 strList.add("world");  
 List<String> unmodifiableStrList = List.of("hello", "world");
 unmodifiableStrList.add("again");

这段代码中,final只能约束strList这个引用不可以被赋值改变,但是对strList对象的行为是不受final影响的,对元素的操作完全是正常的。而例子中List.of方法创建的本身就是不可变的List,最后那句add在运行时会抛出异常的。

Immutable在很多场景下是非常棒的选择,某种意义上说,java语言目前并没有原生的不可变支持,如果要实现immutable的类,需要做到以下几点:

●将class自身声明为final,这样就不能扩展来跳过限制。

●将所有成员变量定义为private和final,并且不要实现setter方法。

●构造对象时,成员变量通常使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为无法确定输入对象不被其他修改。

●如果要实现getter方法,或者其他可能返回内部状态的方法,使用copy-on-write原则,创建私有的copy。

2.对于finally,明确知道怎么使用就足够了。需要关闭的连接资源,推荐使用java7中添加的try-with-resources语句,因为通常java平台能够更好的处理异常情况,编码量也要少很多。

3.finalize的执行是和垃圾收集关联在一起的,一旦实现了非空的finalize方法,就会导致相应的对象回收呈现数量级变慢。

如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且再进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()已经被虚拟机执调用过,虚拟机将这两种情况都视为“没有必要执行”。如果对象被判定有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并且稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。

从finalize()执行时机可以看出,实现了finalize()方法的对象会被jvm对其进行额外处理,而且由于处理机制问题,导致finalize()执行还是不可预测的、不能保证的、所以本质上还是不能指望的。实践中,因为finalize拖慢垃圾收集,导致大量对象堆积,也是一种典型的导致OOM的原因。finalize还会掩盖资源回收的出错信息,看下面这段截取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(); 
 }

这里的Throwable是被生吞了的,也就意味着一旦出现异常或者错误,将得不到任何有效信息。况且,java在finalize阶段也没有好的方式处理错误信息,不然更加不可预测。

4.finalize替换

java平台目前在逐步使用java.lang.ref.Cleaner来替换掉原有的finalize实现。Cleaner的实现利用了幻想引用,这是一种常见的所谓post-mortem清理机制。它比finalize更加轻量、更加可靠。吸取了finalize里的教训,每个Cleaner的操作都是独立的,它有自己的运行线程,所以可以避免意外死锁等问题。

猜你喜欢

转载自blog.csdn.net/zxp0727/article/details/84785425