面试题:谈谈 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();
       });
   }
}
相关推荐
软件测试曦曦6 小时前
16:00开始面试,16:08就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
拉不动的猪7 小时前
设计模式之------策略模式
前端·javascript·面试
独行soc7 小时前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf
uhakadotcom7 小时前
Google Earth Engine 机器学习入门:基础知识与实用示例详解
前端·javascript·面试
uhakadotcom8 小时前
Amazon GameLift 入门指南:六大核心组件详解与实用示例
后端·面试·github
_一条咸鱼_9 小时前
深入解析 Vue API 模块原理:从基础到源码的全方位探究(八)
前端·javascript·面试
_一条咸鱼_10 小时前
深入剖析 Vue 状态管理模块原理(七)
前端·javascript·面试
uhakadotcom10 小时前
一文读懂DSP(需求方平台):程序化广告投放的核心基础与实战案例
后端·面试·github
uhakadotcom10 小时前
拟牛顿算法入门:用简单方法快速找到函数最优解
算法·面试·github
小黑屋的黑小子11 小时前
【数据结构】反射、枚举以及lambda表达式
数据结构·面试·枚举·lambda表达式·反射机制