Java异常处理:用好try-catch,代码更健壮

更新日期: 2025-12-03 阅读: 24 标签: Java

一、从常见的错误写法说起

很多Java程序员都写过这样的代码

try {
    // 一堆业务代码
} catch (Exception e) {
    e.printStackTrace();
}

刚开始觉得这样写很安全,至少程序不会直接崩溃。但用久了就会发现几个问题:

  1. 控制台全是红色错误信息,用户根本看不懂

  2. 业务逻辑和异常处理混在一起,很难维护

  3. 真正有用的错误信息没有被好好利用

  4. 想复用代码时,被各种throws和catch搞得很头疼

我自己也踩过不少坑,慢慢总结出了一套处理异常的方法。今天就来分享怎么用好try-catch,写出更好维护、更好排查的代码。


二、try-catch到底是怎么工作的

先看个基本例子:

public void readFile(String filename) {
    try {
        FileInputStream in = new FileInputStream(filename); // 可能出错
        int b;
        while ((b = in.read()) != -1) {
            // 处理文件内容
        }
        System.out.println("读取完成");
    } catch (IOException e) {
        e.printStackTrace();
    }
    System.out.println("方法结束");
}

这里有几个关键点:

  • try里面的代码只要有一行抛出异常,后面的代码就不会执行了

  • 程序直接跳到对应的catch块

  • catch执行完后,如果没有再抛出异常,就继续执行try-catch后面的代码

所以如果文件读取到一半出错,System.out.println("读取完成")这行就不会执行,但System.out.println("方法结束")还是会执行的。


三、什么时候该catch,什么时候该throw

上面的写法是在当前方法里处理异常。但有时候这样并不合适。

比如你在DAO层读取文件或数据库,真正知道该怎么给用户反馈错误信息的应该是上层的Service或Controller。

这种情况下,我建议不要在底层直接catch,而是把异常抛出去:

public void readFile(String filename) throws IOException {
    try (FileInputStream in = new FileInputStream(filename)) {
        int b;
        while ((b = in.read()) != -1) {
            // 处理文件内容
        }
    }
}

上层可以选择:

  • 统一记录日志

  • 转成业务异常

  • 给用户友好的提示

记住:如果你不知道怎么处理这个异常,就不要假装自己能处理。


四、处理多种异常类型

实际项目中,一个try块里经常会有多种可能的异常。

1. 用多个catch分别处理

try {
    URL url = new URL("https://example.com/config.json");
    URLConnection conn = url.openConnection();
    try (InputStream in = conn.getInputStream()) {
        // 读取配置
    }
} catch (MalformedURLException e) {
    // URL格式错误
    System.err.println("配置地址不合法");
} catch (UnknownHostException e) {
    // 域名解析失败
    System.err.println("无法解析服务器域名");
} catch (IOException e) {
    // 其他IO问题
    System.err.println("读取配置失败");
}

两个技巧:

  • 具体的异常放在前面,通用的异常放在后面

  • 这样可以针对不同的错误给出不同的提示

2. 多种异常用同样的方式处理

如果几种异常的处理方式一样,可以这样写:

try {
    loadResource("config.json");
} catch (FileNotFoundException | UnknownHostException e) {
    // 这两种情况都算"资源找不到"
    System.err.println("资源不可用");
} catch (IOException e) {
    System.err.println("读取资源失败");
}

注意:这里的e变量是final的,不能重新赋值。


五、包装异常传递上下文

有时候会遇到这种情况:

  • 底层抛出SQLException、IOException

  • 上层并不关心具体细节,只想知道业务子系统出了什么问题

这时可以包装一层自己的异常:

public class ConfigException extends Exception {
    public ConfigException(String message, Throwable cause) {
        super(message, cause);
    }
}

public Properties loadConfig(Path path) throws ConfigException {
    try (InputStream in = Files.newInputStream(path)) {
        Properties props = new Properties();
        props.load(in);
        return props;
    } catch (IOException e) {
        // 包装成业务异常
        throw new ConfigException("加载配置失败:" + path, e);
    }
}

上层既能拿到业务描述,也能追溯原始错误:

try {
    loadConfig(Paths.get("config/app.properties"));
} catch (ConfigException e) {
    System.err.println(e.getMessage());  // 业务提示
    Throwable root = e.getCause();       // 原始异常
    root.printStackTrace();              // 排查用
}

我的习惯是:一旦跨越业务边界(比如从DAO到Service),就用自定义异常包装底层异常。


六、资源关闭:finally和try-with-resources

1. finally的用法

传统的资源关闭方式:

FileInputStream in = null;
try {
    in = new FileInputStream("data.txt");
    // 处理文件
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (in != null) {
        try {
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

要点:

  • 无论是否抛出异常,finally都会执行

  • 即使catch里又抛出异常,finally还是会执行

重要提醒:finally里不要写return、throw这样的语句:

public static int parseInt(String s) {
    try {
        return Integer.parseInt(s);
    } finally {
        return 0;  // 危险!会吞掉异常和正常返回值
    }
}

这个方法永远返回0,而且不会报错。

2. 推荐使用try-with-resources

现在我都用这种方式:

public List<String> readAllLines(String filename) throws IOException {
    List<String> lines = new ArrayList<>();
    try (Scanner in = new Scanner(Paths.get(filename))) {
        while (in.hasNextLine()) {
            lines.add(in.nextLine());
        }
    }  // 这里自动调用close()
    return lines;
}

好处:

  • 自动关闭资源,不会忘记

  • 代码更简洁

  • 可以同时处理多个资源


七、堆栈跟踪:不只是错误信息

平时看到的红色报错就是堆栈跟踪,它记录了:

  • 异常类型

  • 错误信息

  • 方法调用链

最简单的用法:

try {
    // 可能出错的操作
} catch (Exception e) {
    e.printStackTrace();  // 开发时有用
}

生产环境建议用日志记录:

catch (Exception e) {
    logger.error("处理订单失败", e);  // 日志包含完整堆栈
}


八、实用经验总结

1. 异常不是if的替代品

不要这样写:

// 不好的写法
try {
    stack.pop();
} catch (EmptyStackException e) {
    // 什么都不做
}

问题:

  • 性能差:抛异常比普通判断慢很多

  • 语义不对:异常应该用于"意外情况"

应该这样写:

if (!stack.empty()) {
    stack.pop();
}

2. 不要每行代码都包try-catch

// 不好的写法
try {
    n = stack.pop();
} catch (EmptyStackException e) { /* ... */ }
try {
    out.writeInt(n);
} catch (IOException e) { /* ... */ }

如果整个操作失败就算失败,应该:

try {
    for (int i = 0; i < 100; i++) {
        int n = stack.pop();
        out.writeInt(n);
    }
} catch (EmptyStackException e) {
    logger.warn("栈元素不足");
} catch (IOException e) {
    logger.error("写入文件失败");
}

3. 不要吞掉异常

见过太多这样的代码:

try {
    doSomething();
} catch (Exception e) {
    // 什么都不做
}

表面安全,实际埋雷。至少应该做其中一件事:

  • 记录日志

  • 转成业务异常往上抛

  • 写注释说明为什么忽略

4. throws不丢人

有些人觉得方法签名里写throws不好看,就想把所有异常都catch掉。

我建议:如果这层不知道怎么处理,就老实往上抛。上一层知道业务该怎么做。

public void importUsers(Path path) throws IOException {
    List<String> lines = Files.readAllLines(path);
    // 解析并保存
}

5. 用标准工具做参数校验

Java自带的Objects很好用:

public void putData(int index, Object value) {
    Objects.checkIndex(index, data.length);      // 检查索引
    Objects.requireNonNull(value, "值不能为空");  // 检查null
    
    data[index] = value;
}

好处:异常类型和信息都符合JDK标准。

6. 不要给用户看堆栈信息

开发时可以这样:

e.printStackTrace();

但生产环境不要给用户看完整的堆栈信息,因为:

  • 可能包含敏感信息

  • 用户看不懂

应该:

  • 日志里记录完整堆栈

  • 给用户友好的错误提示


九、完整示例:配置加载服务

import java.io.IOException;
import java.nio.file.*;
import java.util.Properties;
import java.util.Objects;
import java.util.logging.Logger;

public class ConfigService {
    
    private static final Logger logger = Logger.getLogger(ConfigService.class.getName());
    
    public static class ConfigException extends Exception {
        public ConfigException(String message, Throwable cause) {
            super(message, cause);
        }
    }
    
    private final Path configPath;
    
    public ConfigService(Path configPath) {
        this.configPath = Objects.requireNonNull(configPath, "配置文件路径不能为空");
    }
    
    public Properties loadConfig() throws ConfigException {
        if (!Files.exists(configPath)) {
            throw new ConfigException("配置文件不存在:" + configPath, null);
        }
        
        try {
            Properties props = new Properties();
            try (var in = Files.newInputStream(configPath)) {
                props.load(in);
            }
            return props;
        } catch (IOException e) {
            logger.severe("读取配置文件失败:" + e.getMessage());
            throw new ConfigException("读取配置文件失败:" + configPath, e);
        }
    }
    
    public static void main(String[] args) {
        ConfigService service = new ConfigService(Paths.get("config/app.properties"));
        try {
            Properties props = service.loadConfig();
            System.out.println("配置加载成功");
        } catch (ConfigException e) {
            System.err.println("系统启动失败,请检查配置");  // 给用户的提示
            e.printStackTrace();  // 开发人员看的详细信息
        }
    }
}

这个例子包含了:

  • try-with-resources自动关闭资源

  • 包装异常传递业务信息

  • 给用户友好提示,同时记录详细日志


十、下一步思考

掌握了这些基础后,可以进一步思考:

  1. Web项目中,可以用全局异常处理统一转换异常

  2. 多线程环境下,异常处理有什么不同

  3. 除了异常信息,还要记录哪些上下文帮助排查问题

下次再想写catch (Exception e) { e.printStackTrace(); }时,可以先想一想:我真的需要在这里处理吗?

希望这些经验能帮你写出更好的Java代码,少踩一些坑。用好异常处理,你的程序会更健壮、更好维护。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13275

采用Java的ServerSocket进行编码一个简单的HTTP服务器

诸如tomcat等web服务器中间件简化了我们web的开发成本,但有时候我们或许并不需要这么一个完备的服务器,只是希望做一个简单地处理或者做特殊用途的服务器。

Spring Boot支持Crontab任务改造

在以往的 Tomcat 项目中,一直习惯用 Ant 打包,使用 build.xml 配置,通过 ant -buildfile 的方式在机器上执行定时任务。虽然 Spring 本身支持定时任务,但都是服务一直运行时支持。

lucene的suggest(搜索提示功能的实现)

首先引入依赖,既然要进行智能联想,那么我们需要为提供联想的数据建立一个联想索引(而不是使用原来的数据索引),既然要建立索引,那么我们需要知道建立索引的数据来源。我们使用一个扩展自InputIterator的类来定义数据来源

HashMap剖析之内部结构

本文是基于Java 8的HashMap进行分析,主要是介绍HashMap中的成员变量和类变量的用途,以及分析HashMap的数据结构。在HashMap中存在多个成员变量和类变量,搞清楚它们的用途有助于我们更深入了解HashMap,下面是它们的介绍:

自定义HttpMessageConverter接受JSON数据

Spring默认使用Jackson处理json数据。实际开发中,在业界中,使用非常受欢迎的fastjson来接受json数据。创建一个项目,在web目录下新建一个assets/js目录,加入jquery和json2的js文件,在lib下加入fastjson的jar文件。

统计两个IP地址之间的IP个数

求两个IP地址之间的IP个数,例如192.18.16.1~192.18.16.5,2001:DB8:0000:0023:0008:0800:200C:417C~2001:DB8:0:23:8:800:200C:417D之间的IP个数?

JSP和JSF之间的区别是什么?

JSP和JSF这两种技术都基于Java,主要用于基于Web的应用程序。那么它们之间有什么区别?下面本篇文章就来给大家简单比较一下JSP和JSF,介绍JSP和JSF之间的区别有哪些,希望对大家有所帮助。

JVM 发生 OOM 的 8 种原因、及解决办法

Java 堆空间:发生频率:5颗星造成原因1、无法在 Java 堆中分配对象 2、吞吐量增加 3、应用程序无意中保存了对象引用,对象无法被 GC 回收 4、应用程序过度使用 finalizer

Java版的7种单例模式

今天看到某一篇文章的一句话 单例DCL 前面加 V 。就这句话让我把 单例模式 又仔细看了一遍。Java 中的 单例模式 是我们一直且经常使用的设计模式之一,大家都很熟悉,所以这篇文章仅仅做我自己记忆。

常问的15个顶级Java多线程面试题

在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得更多职位,那么你应该准备很多关于多线程的问题。面试官会问面试者很多令人混淆的Java线程问题

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!