面试题:谈谈 final、finally、finalize 有什么不同?

在 Java 开发面试中,finalfinallyfinalize 的区别是一个经典问题。这个问题不仅考察你对 Java 基础语法的掌握,还考察你对资源管理、垃圾回收机制的理解。下面我们将从三个方面来分析这个问题:考察知识点、答案解析、形象场景以及知识拓展。


考察知识点

这个问题主要涉及以下知识点:

  1. final 关键字:用于修饰类、方法、变量,分别表示不可继承、不可重写、不可修改。
  2. finally 块 :用于确保代码在 try-catch 块中无论是否发生异常都会被执行,常用于资源释放。
  3. finalize 方法 :是 Object 类的一个方法,用于在对象被垃圾回收前执行清理操作。但由于其性能问题和不确定性,已被标记为 @Deprecated

答案解析

final

  • 修饰类:表示该类不可被继承,例如 String 类就是 final 类。
  • 修饰方法:表示该方法不可被子类重写。
  • 修饰变量:表示该变量一旦赋值后不可修改(如果是基本类型,值不可变;如果是引用类型,引用不可变,但对象内容可以修改)。

finally

  • 用于 try-catch 块中,无论是否发生异常,finally 块中的代码都会被执行。
  • 常用于释放资源,例如关闭文件、数据库连接等。

finalize

  • Object 类的一个方法,用于在对象被垃圾回收前执行清理操作。
  • 由于 finalize 的执行时机不确定且性能较差,Java 9 开始已将其标记为 @Deprecated

示例代码

final 示例

java 复制代码
/**
 * final 关键字示例
 */
public class FinalExample {
    final int value = 10; // final 变量,不可修改

    final void display() { // final 方法,不可重写
        System.out.println("Value: " + value);
    }

    public static void main(String[] args) {
        FinalExample example = new FinalExample();
        example.display();
        // example.value = 20; // 编译错误,final 变量不可修改
    }
}

// final 类,不可继承
final class FinalClass {
    void show() {
        System.out.println("This is a final class.");
    }
}

// class SubClass extends FinalClass { // 编译错误,final 类不可继承
// }

finally 示例

java 复制代码
/**
 * finally 块示例
 */
public class FinallyExample {
    public static void main(String[] args) {
        try {
            int result = 10 / 0; // 抛出 ArithmeticException
        } catch (ArithmeticException e) {
            System.out.println("捕获到异常: " + e.getMessage());
        } finally {
            System.out.println("finally 块被执行,用于释放资源");
        }
    }
}

finalize 示例

java 复制代码
/**
 * finalize 方法示例
 */
public class FinalizeExample {
    @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("finalize 方法被调用,执行清理操作");
        } finally {
            super.finalize();
        }
    }

    public static void main(String[] args) {
        FinalizeExample example = new FinalizeExample();
        example = null; // 使对象成为垃圾
        System.gc(); // 建议 JVM 执行垃圾回收
    }
}

形象助记

想象一下,你是一个餐厅的厨师:

  1. final :你有一把祖传的菜刀(final 变量),这把刀不能换,但你可以用它切不同的菜(对象内容可以修改)。
  2. finally :无论今天的菜做得好不好(是否发生异常),你都要在下班前打扫厨房(finally 块),确保厨房干净。
  3. finalize :你有一本食谱(finalize 方法),在你退休前(对象被垃圾回收前),你会把食谱整理好。但问题是,你并不知道自己什么时候退休(finalize 的执行时机不确定),所以这本食谱可能永远也用不上。

知识拓展

1、finally 一定执行吗?

final 修饰的变量如果是引用类型,虽然引用不可变,但对象的内容是可以修改的。例如:

java 复制代码
try {
    // do something
    System.exit(1);
} finally{
    System.out.println("Print from finally");
}

这段代码不会执行 finally 块中的 System.out.println("Print from finally"); 语句,因此不会输出任何内容。

原因如下:

  • System.exit(1) 会立即终止 Java 虚拟机(JVM),程序直接退出,状态码为 1(通常表示异常退出)。
  • finally 块通常在任何情况下(包括 try 块中的 return 或异常)都会执行,但 System.exit(1) 会强制终止 JVM,导致 finally 块无法执行。

2、finalize 存在什么问题?怎么解决呢?

在 Java 中,finalize 方法用于在对象被垃圾回收之前执行清理操作,但它存在以下问题:

  • 不确定性 :无法保证 finalize 何时执行,甚至可能根本不执行。
  • 性能开销 :使用 finalize 会增加垃圾回收的负担。
  • 复杂性:容易引发资源泄漏或异常。

因此,Java 9 开始废弃了 finalize 方法,并推荐使用以下替代机制:


2.1、java.lang.ref.Cleanerjava.lang.ref.PhantomReference

  • Cleaner 是一个更安全、更可控的机制,用于在对象不再可达时执行清理操作。
  • PhantomReference 可以与引用队列(ReferenceQueue)结合使用,实现类似的功能。
java 复制代码
import java.lang.ref.Cleaner;

public class ResourceCleanerExample {
   private static final Cleaner cleaner = Cleaner.create();

   private static class Resource implements Runnable {
       @Override
       public void run() {
           // 清理操作
           System.out.println("Cleaning up resources");
       }
   }

   private final Cleaner.Cleanable cleanable;
   private final Resource resource;

   public ResourceCleanerExample() {
       this.resource = new Resource();
       this.cleanable = cleaner.register(this, resource);
   }
}

ResourceCleanerExample 对象不再被引用时,Cleaner 会自动调用 Resourcerun 方法执行清理。


2.2、try-with-resources

对于需要显式释放的资源(如文件、网络连接等),可以使用 try-with-resources 语句,确保资源在使用完毕后自动关闭。

java 复制代码
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
   String line;
   while ((line = br.readLine()) != null) {
       System.out.println(line);
   }
} catch (IOException e) {
   e.printStackTrace();
}

这种方式适用于实现了 AutoCloseable 接口的资源。


2.3、显式清理方法

提供一个显式的清理方法(如 close()dispose()),由调用者在不再需要对象时手动调用。

java 复制代码
public class ResourceHolder {
   private boolean closed = false;

   public void close() {
       if (!closed) {
           // 执行清理操作
           System.out.println("Resource closed");
           closed = true;
       }
   }

   @Override
   protected void finalize() throws Throwable {
       try {
           if (!closed) {
               System.out.println("Resource not closed properly!");
           }
       } finally {
           super.finalize();
       }
   }
}

调用者需要显式调用 close() 方法:

java 复制代码
ResourceHolder holder = new ResourceHolder();
// 使用资源
holder.close();

2.4、java.lang.ref.WeakReference 和引用队列

对于需要监控对象生命周期的场景,可以使用 WeakReferenceReferenceQueue

java 复制代码
import java.lang.ref.WeakReference;
import java.lang.ref.ReferenceQueue;

public class WeakReferenceExample {
   public static void main(String[] args) {
       Object obj = new Object();
       ReferenceQueue<Object> queue = new ReferenceQueue<>();
       WeakReference<Object> weakRef = new WeakReference<>(obj, queue);

       obj = null; // 使对象只有弱引用

       System.gc(); // 触发垃圾回收

       if (weakRef.get() == null) {
           System.out.println("Object has been garbage collected");
       }
   }
}

3、JDK 自身使用的 Cleaner 机制仍然是有缺陷的,还有什么更好的建议吗?

Cleaner 的缺陷

  • 性能开销Cleaner 仍然依赖于垃圾回收器(GC)来触发清理操作,因此会引入额外的性能开销。在高性能场景中,这种开销可能会成为瓶颈。

  • 不确定性 :虽然 Cleanerfinalize 更可控,但它仍然依赖于垃圾回收器的行为,清理操作的执行时间仍然是不确定的。

  • 线程管理Cleaner 使用一个单独的线程来执行清理操作。如果清理任务过多或某些任务执行时间过长,可能会导致线程阻塞,影响其他清理任务的执行。

  • 资源泄漏风险 :如果 Cleaner 的清理任务本身抛出未捕获的异常,可能会导致清理任务中断,从而引发资源泄漏。

  • 复杂性 :使用 Cleaner 需要额外的代码和设计,尤其是在管理多个资源时,可能会增加代码的复杂性。

改进建议

  • 对于大多数场景,显式资源管理 (如 try-with-resources)是最佳选择。
  • 如果需要更复杂的生命周期监控,可以结合 引用队列自定义清理机制
  • 避免过度依赖 Cleaner,尤其是在高性能场景中。

1、显式资源管理

对于需要清理的资源,优先使用显式管理方式(如 try-with-resources 或手动调用 close() 方法)。这种方式完全避免了垃圾回收的不确定性,并且性能更高。

java 复制代码
public class Resource implements AutoCloseable {
   @Override
   public void close() {
       // 显式清理资源
       System.out.println("Resource closed");
   }
}

try (Resource resource = new Resource()) {
   // 使用资源
} // 自动调用 close()

2、基于引用队列的监控

使用 PhantomReferenceWeakReference 结合 ReferenceQueue 来监控对象的生命周期。这种方式比 Cleaner 更灵活,并且可以避免 Cleaner 的线程管理问题。

java 复制代码
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {
   public static void main(String[] args) {
       Object obj = new Object();
       ReferenceQueue<Object> queue = new ReferenceQueue<>();
       PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);

       obj = null; // 使对象只有虚引用

       System.gc(); // 触发垃圾回收

       if (phantomRef.get() == null) {
           System.out.println("Object has been garbage collected");
           // 执行清理操作
       }
   }
}

3、自定义清理机制 对于复杂的资源管理需求,可以设计一个自定义的清理机制,结合显式管理和引用队列。例如,使用一个全局的清理线程池来处理清理任务,避免 Cleaner 的单线程瓶颈。

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CustomCleaner {
   private static final ExecutorService cleanupExecutor = Executors.newCachedThreadPool();

   public static void register(Object obj, Runnable cleanupTask) {
       cleanupExecutor.submit(() -> {
           // 监控对象状态(例如通过 WeakReference)
           // 当对象被回收时,执行清理任务
           cleanupTask.run();
       });
   }
}
相关推荐
Lee川6 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川9 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i11 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有11 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有12 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫12 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫13 小时前
Handler基本概念
面试
Wect13 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼14 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼14 小时前
Next.js 企业级落地
前端·javascript·面试