《Java 100 天进阶之路》第35篇:Java异常处理最佳实践

第35篇:Java异常处理最佳实践

📌 系列导航《Java 100 天进阶之路》完整目录 |

⬅️ 上一篇:第34篇:Java序列化与反序列化详解 |

➡️ 下一篇:第36篇:选择最适合自己的NIO,一探流技术


一、核心知识点

  • 异常处理的基本原则:只捕获能处理的异常,不要捕获后什么都不做
  • 检查型异常与运行时异常的使用场景
  • try-catch-finally 的正确用法
  • try-with-resources(自动关闭资源)
  • 自定义异常的规范
  • 异常链与异常包装
  • 日志记录的最佳实践

二、通俗讲解(1分钟开心学)

1. 异常处理的核心思想

  • 只捕获你能处理的异常。
  • 捕获后要做出有意义的响应(记录日志、返回默认值、重试、包装后重新抛出)。

2. 检查型异常 vs 运行时异常

  • 检查型异常 (如 IOException):调用者必须处理或声明抛出,代表可恢复的外部错误(文件不存在、网络中断)。
  • 运行时异常 (如 NullPointerException):不强制处理,代表编程错误(参数校验不足、数组越界),应当通过改进代码避免。

3. try-with-resources

自动关闭实现了 AutoCloseable 接口的资源(文件、网络连接、数据库连接),避免 finally 中手动关闭的繁琐。

4. 异常链

当你在一个异常中包装另一个异常时,可以保留原始异常信息,便于排查。

5. 日志记录

捕获异常后务必记录日志(使用 log.error("消息", e)),不要只打印 e.printStackTrace()(在正式环境可能不可见)。

6. 自定义异常

  • 继承 Exception(检查型)或 RuntimeException(运行时)。
  • 提供多个构造方法(无参、带消息、带消息+原异常)。

生活类比

异常处理就像开车遇到故障。能自己修的小毛病(运行时异常)就用工具修好(改进代码)。大问题(检查型异常)需要呼叫救援(try-catch),然后记录事故(日志)。

三、实操代码案例 + 场景说明

场景:读取配置文件,处理文件不存在的异常,并确保流被正确关闭。

java 复制代码
import java.io.*;
import java.util.Properties;
import java.util.logging.*;

public class ExceptionBestPractice {
    private static final Logger logger = Logger.getLogger("ExceptionDemo");
    
    // 1. 正确使用 try-with-resources 自动关闭
    public static String readFirstLine(String path) {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        } catch (IOException e) {
            // 记录日志,不要吞掉异常
            logger.log(Level.SEVERE, "读取文件失败: " + path, e);
            return null;  // 或抛出运行时异常
        }
    }
    
    // 2. 异常包装与链
    public static Properties loadConfig(String path) throws ConfigException {
        try (InputStream in = new FileInputStream(path)) {
            Properties props = new Properties();
            props.load(in);
            return props;
        } catch (IOException e) {
            // 包装为自定义异常,保留原始原因
            throw new ConfigException("加载配置失败: " + path, e);
        }
    }
    
    // 3. 运行时异常用于参数校验
    public static void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄必须在0~150之间,实际: " + age);
        }
        // 正常处理
    }
    
    // 4. 不要空 catch
    public static void badPractice() {
        try {
            int a = 10 / 0;
        } catch (ArithmeticException e) {
            // 什么都不做,问题被隐藏了!这是最坏的实践
        }
    }
    
    public static void main(String[] args) {
        String line = readFirstLine("nonexist.txt");
        System.out.println("读取结果: " + line);
        
        try {
            Properties props = loadConfig("config.properties");
        } catch (ConfigException e) {
            e.printStackTrace();  // 正式环境用 logger
        }
        
        try {
            setAge(200);
        } catch (IllegalArgumentException e) {
            System.err.println(e.getMessage());
        }
    }
}

// 自定义检查型异常
class ConfigException extends Exception {
    public ConfigException(String message, Throwable cause) {
        super(message, cause);
    }
    public ConfigException(String message) {
        super(message);
    }
}

四、避坑要点

错误/误区 后果 正确做法
捕获异常后什么都不做(空 catch) 掩盖问题,难以排查 至少记录日志
finally 中关闭资源但不处理异常 如果关闭失败,异常被吞没 使用 try-with-resources
抛出检查型异常但调用者无法处理 传递上层,导致处处 throws 考虑包装为运行时异常
在循环中 catch 异常 性能差,且可能陷入无限循环 循环外处理,或使用重试策略
日志只打印 e.getMessage() 丢失栈轨迹 使用 log.error("msg", e) 打印完整堆栈

五、面试高频考点

Q1:什么时候应该捕获检查型异常,什么时候应该向上抛出?

如果当前层能恢复或做出合理响应(如重试、使用默认值),则捕获;否则向上抛出,让上层处理。对于框架代码,常包装为运行时异常抛出。

Q2:try-with-resources 的原理是什么?

实现了 AutoCloseable 的资源可以在 try 括号中声明,编译后生成 finally 块调用 close(),并处理异常抑制(addSuppressed)。

Q3:如何正确记录异常日志?

使用日志框架(如 java.util.logging、Log4j、SLF4J),记录完整堆栈:logger.error("操作失败", e); 不要只记录 e.getMessage()

六、练习题

  1. 代码改错 :指出下面代码的问题并改正。

    java 复制代码
    try {
        FileInputStream fis = new FileInputStream("test.txt");
        fis.read();
        fis.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
  2. 设计 :写一个方法 divide(int a, int b),如果 b 为 0,抛出自定义运行时异常 DivideByZeroException

  3. 场景:你正在开发一个网络请求库,当网络超时时应如何处理异常?


📊 你的学习进度

  • 当前:第35篇 / 共44篇 · 第五阶段:工具类、异常最佳实践、序列化(第32~35篇)
  • ✅ 已完成:第1~34篇
  • 📖 正在学:第35篇
  • ⏳ 待学习:第36~44篇

👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇

💡 本专栏每篇都包含:避坑表 + 面试高频考点 + 练习题。每天30分钟,100天拿offer!


👉 下一篇文章预告

《第36篇:选择最适合自己的NIO,一探流技术》

内容简介:BIO/NIO/AIO区别、流体系(字节/字符/缓冲/转换)、NIO核心组件(Channel/Buffer/Selector)、零拷贝。

💡 学完这篇,你将掌握Java I/O的全貌,并能根据场景选择合适的I/O模型。

📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注 ,一起100天拿offer!

👉 点击关注我,更新后第一时间收到推送!

📌 除了Java,我也在深挖智能物流实战(出版社WMS、托盘调度、机器学习落地)。如果你对技术在不同领域的实战感兴趣,欢迎点击我的头像,看看专栏《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

相关推荐
IT_陈寒21 分钟前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro1 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax2 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH2 小时前
Koa和Express的区别
后端
MariaH2 小时前
Koa框架的使用
后端
luckdewei3 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某4 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy4 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom4 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
唐青枫8 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java