Java异常处理的20个最佳实践:告别系统崩溃

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: 你是否在为如何处理异常而困扰?你是否曾被面试官问道Java异常处理的最佳实践有哪些?本文汇总了Java异常处理的20个最佳实践:让你告别系统崩溃,面试游刃有余

引言

在Java编程中,异常处理是一个至关重要的环节,它不仅涉及到程序的稳定性和安全性,还关系到用户体验和系统资源的合理利用。合理的异常处理能够使得程序在面对不可预知错误时,能够优雅地恢复或者给出明确的反馈,而不是简单地崩溃退出。

文章开始前,我们先看下思维导图熟悉下有哪些异常

image.png

正文

1、尽量不要捕获 RuntimeException(Unchecked Exception)

阿里巴巴Java开发手册上这样规定:

尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。

正例

if (obj != null) {
   
  //...
}
 " + (a / b));
}

反例

try {
    
  obj.method(); 
} catch (NullPointerException e) {
   
  //...
}

如果有些异常预检查不出来呢?比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理。

2、 切勿在代码中使用异常来进行流程控制

异常应当是在真正的异常情况下使用,而不是用来控制程序流程。

public class Demo {
   
    public static void main(String[] args) {
   
        String input = "1,2,3,a,5";
        String[] values = input.split(",");
        for (String value : values) {
   
            try {
   
                int num = Integer.parseInt(value);
                System.out.println(num);
            } catch (NumberFormatException e) {
   
                System.err.println(value + " is not a valid number");
            }
        }
    }
}

}

3、 合理利用finally块

确保在finally块中释放资源,比如关闭文件流或数据库连接,无论是否发生异常。

FileInputStream file = null;
try {
   
    file = new FileInputStream("someFile.txt");
    // 使用文件流
} catch (IOException e) {
   
    e.printStackTrace();
} finally {
   
    if (file != null) {
   
        try {
   
            file.close();
        } catch (IOException e) {
   
            e.printStackTrace();
        }
    }
}

4、 不要在finally块中使用return

这会导致try块中的return语句被忽略。

public int notGood() {
   
    try {
   
        // 假设这里有逻辑代码
        return 1;
    } finally {
   
        return 2;
    }
}

5、 避免忽略异常

即使认为某些异常不重要也不应该完全忽略它们,至少要记录下来。

反例

public void doNotIgnoreExceptions() {
   
    try {
   
    } catch (NumberFormatException e) {
   
        // 没有记录异常
    }
}

正例

public void logAnException() {
   
    try {
   
    } catch (NumberFormatException e) {
   
        log.error("哦,错误竟然发生了: " + e);
    }
}

6、 捕获具体的子类而不是捕获 Exception 类

对不同类型的异常给出不同的处理逻辑。

反例

try {
   
   someMethod();
} catch (Exception e) {
    //错误方式
   LOGGER.error("method has failed", e);
}

正例

try {
   
    // 某些可能产生异常的操作
} catch (FileNotFoundException e) {
   
    // 文件未找到的处理逻辑
} catch (IOException e) {
   
    // IO异常的处理逻辑
}

7、 将所有相关信息尽可能地传递给异常

有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢?

try {
   
    // 某些可能产生异常的操作
} catch (IOException | SQLException e) {
   
 // Log exception message and stack trace
    LOGGER.debug("Error reading file", e);
}

应该尽量把 String message, Throwable cause 异常信息和堆栈都输出。

8、 使用自定义异常传递更多信息

当内置的异常类型不能满足需求时,可以创建自定义异常。

public class MyException extends Exception {
   
    public MyException(String message) {
   
        super(message);
    }
}

public void doSomething() throws MyException {
   
    // 某些逻辑
    throw new MyException("特定错误信息");
}

9、 自定义异常时不要丢失堆栈跟踪

在捕获一个异常并抛出另一个异常时,保留原始异常的信息。

catch (NoSuchMethodException e) {
   
  //错误方式 
 throw new MyServiceException("Some information: " + e.getMessage());
}

这破坏了原始异常的堆栈跟踪,正确的做法是:

catch (NoSuchMethodException e) {
   
 //正确方式 
 throw new MyServiceException("Some information: " , e); 
}

10、优先使用标准异常

在可能的情况下,应优先使用Java标准库中定义的异常。

public void setValue(int value) {
   
    if (value < 0) {
   
        throw new IllegalArgumentException("值不能为负"); // 使用标准异常
    }
    // 设置值的逻辑
}

11、不要在生产环境中使用 printStackTrace()

在 Java 中,printStackTrace() 方法用于将异常的堆栈跟踪信息输出到标准错误流中。这个方法对于调试和排错非常有用。但在生产环境中,不应该使用 printStackTrace() 方法,因为它可能会导致以下问题:

  • printStackTrace() 方法将异常的堆栈跟踪信息输出到标准错误流中,这可能会暴露敏感信息,如文件路径、用户名、密码等。
  • printStackTrace() 方法会将堆栈跟踪信息输出到标准错误流中,这可能会影响程序的性能和稳定性。在高并发的生产环境中,大量的异常堆栈跟踪信息可能会导致系统崩溃或出现意外的行为。
  • 由于生产环境中往往是多线程、分布式的复杂系统,printStackTrace() 方法输出的堆栈跟踪信息可能并不完整或准确。

在生产环境中,应该使用日志系统来记录异常信息,例如 log4j、slf4j、logback等。日志系统可以将异常信息记录到文件或数据库中,而不会暴露敏感信息,也不会影响程序的性能和稳定性。同时,日志系统也提供了更多的功能,如级别控制、滚动日志、邮件通知等。

//例如,可以使用 logback 记录异常信息,如下所示:
try {
   
    // some code
} catch (Exception e) {
   
    logger.error("An error occurred: ", e);
}

12、 不要捕获 Throwable

Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。

public void doNotCatchThrowable() {
   
    try {
   
    } catch (Throwable t) {
   
        // 不要这样做
    }
}

13、利用try-with-resources自动管理资源

Java 7引入的try-with-resources语句可以自动管理资源,减少代码冗余。

try (FileInputStream input = new FileInputStream("file.txt")) {
   
    // 使用资源
} catch (IOException e) {
   
    e.printStackTrace();
}

14、为异常提供详细的上下文信息

在抛出异常时,提供足够的上下文信息,以帮助定位和解决问题。

public void loadConfiguration(String path) throws IOException {
   
    try {
   
        // 加载配置逻辑
    } catch (IOException e) {
   
        throw new IOException("加载配置文件失败,路径:" + path, e);
    }
}

15、不要记录了异常又抛出了异常

这纯属画蛇添足,并且容易造成错误信息的混乱。

反例:

try {
   
} catch (NumberFormatException e) {
   
    log.error(e);
    throw e;
}

要抛出就抛出,不要记录,记录了又抛出,等于多此一举。

正例:

public void wrapException(String input) throws MyBusinessException {
   
    try {
   
    } catch (NumberFormatException e) {
   
        throw new MyBusinessException("错误信息描述:", e);
    }
}

16、finally 块中不要抛出任何异常

try {
   
  someMethod();  //Throws exceptionOne
} finally {
   
  cleanUp();    //如果finally还抛出异常,那么exceptionOne将永远丢失
}

finally 块用于定义一段代码,无论 try 块中是否出现异常,都会被执行。finally 块通常用于释放资源、关闭文件等必须执行的操作。

如果在 finally 块中抛出异常,可能会导致原始异常被掩盖。比如说上例中,一旦 cleanup 抛出异常,someMethod 中的异常将会被覆盖

17、只抛出和方法相关的异常

相关性对于保持代码的整洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户提供有价值的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException 则对该方法的用户更有用。

public class Demo {
   
    public static void main(String[] args) {
   
        try {
   
            int result = divide(10, 0);
            System.out.println("The result is: " + result);
        } catch (ArithmeticException e) {
   
            System.err.println("Error: " + e.getMessage());
        }
    }

    public static int divide(int a, int b) throws ArithmeticException {
   
        if (b == 0) {
   
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}

18、尽早验证用户输入以在请求处理的早期捕获异常

再好的异常捕获不如没有异常,所以我们在业务开发中,可以提前验证用户输入,以在请求处理的早期就捕获处理异常

举个例子,我们用 JDBC 的方式往数据库插入数据,那么最好是先 validate 再 insert,而不是 validateUserInput、insertUserData、validateAddressInput、insertAddressData。

Connection conn = null;
try {
   
    // Connect to the database
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");

    // Start a transaction
    conn.setAutoCommit(false);

    // Validate user input
    validateUserInput();

    // Insert user data
    insertUserData(conn);

    // Validate address input
    validateAddressInput();

    // Insert address data
    insertAddressData(conn);

    // Commit the transaction if everything is successful
    conn.commit();

} catch (SQLException e) {
   
    // Rollback the transaction if there is an error
    if (conn != null) {
   
        try {
   
            conn.rollback();
        } catch (SQLException ex) {
   
            System.err.println("Error: " + ex.getMessage());
        }
    }
    System.err.println("Error: " + e.getMessage());
} finally {
   
    // Close the database connection
    if (conn != null) {
   
        try {
   
            conn.close();
        } catch (SQLException e) {
   
            System.err.println("Error: " + e.getMessage());
        }
    }
}

19、 一个异常只能包含在一个日志中

反例

log.debug("Using redis one");
log.debug("Using redis two");

在单线程环境中,这样看起来没什么问题,但如果在多线程环境中,这两行紧挨着的代码中间可能会输出很多其他的内容,导致问题查起来会很难受。应该这样做:

LOGGER.debug("Using redis one, Using redis two");

20、对于不打算处理的异常,直接使用 try-finally,不用 catch

try {
   
  method1();  // 会调用 Method 2
} finally {
   
  cleanUp();    //do cleanup here
}

如果 method1 正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 1 中处理的异常,但是仍然希望在发生异常时进行一些清理,可以直接在 finally 块中进行清理,不要使用 catch 块。

总结

有效的异常处理是高质量Java应用开发的基石。也是Java面试中被问频率很高的问题,通过遵循上述20个最佳实践,开发者不仅能够编写出更加健壮和可维护的代码,还能应对面试中面试官的各种关于异常的问题。

最后说一句(求关注,求赞,别白嫖我)

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。

这是大佬写的, 7701页的BAT大佬写的刷题笔记,让我offer拿到手软

本文,已收录于,我的技术网站 aijiangsir.com,有大厂完整面经,工作技术,架构师成长之路,等经验分享

求一键三连:点赞、分享、收藏

点赞对我真的非常重要!在线求赞,加个关注我会非常感激!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
27天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
15天前
|
安全 Java 程序员
Java中的异常处理:从新手到专家
在Java编程的世界里,异常处理是每个开发者必须面对的挑战。本文将带你从基础的异常概念出发,逐步深入到高级处理技巧,让你在遇到代码中的“意外”时,能够从容应对,甚至化险为夷。
|
20天前
|
Java 数据库连接 开发者
Java中的异常处理:从基础到高级
【10月更文挑战第42天】在Java的世界中,异常处理是维护程序稳定性和健壮性的关键。本文将带你深入了解Java的异常处理机制,从基本的try-catch语句出发,逐步探索更复杂的异常处理策略。我们将通过实际代码示例来演示如何捕获和处理异常,以及如何自定义异常类型来满足特定需求。无论你是Java新手还是有经验的开发者,这篇文章都将帮助你更好地理解和应用Java的异常处理。
|
27天前
|
Java 开发者
Java中的异常处理:从基础到高级
【10月更文挑战第35天】在Java的世界里,异常处理是维护程序健壮性的关键。本文将深入浅出地探讨Java的异常处理机制,从基本的try-catch语句到自定义异常类的实现,带领读者理解并掌握如何在Java中优雅地处理错误和异常。我们将通过实际代码示例,展示如何捕获、处理以及预防潜在的运行时错误,确保程序即使在面临意外情况时也能保持稳定运行。
37 7
|
26天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
1月前
|
Java
Java 异常处理下篇:11 个异常处理最佳实践
本文深入探讨了 Java 异常处理的最佳实践,包括早抛出晚捕获、只捕获可处理的异常、不要忽略捕获的异常、抛出具体检查性异常、正确包装自定义异常、记录或抛出异常但不同时执行、避免在 `finally` 块中抛出异常、避免使用异常进行流程控制、使用模板方法处理重复的 `try-catch`、尽量只抛出与方法相关的异常以及异常处理后清理资源。通过遵循这些实践,可以提高代码的健壮性和可维护性。
|
27天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
47 1
|
1月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
49 2
|
1月前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
75 3
|
1月前
|
Java 程序员 数据库连接
深入浅出Java异常处理
【10月更文挑战第30天】在Java的世界里,异常处理就像是生活中的急救箱,遇到意外时能及时救治。本文不仅教你如何使用try-catch语句包扎“伤口”,还会深入讲解如何通过自定义异常来应对那些常见的“头疼脑热”。准备好,我们将一起探索Java异常处理的奥秘,让你的程序更加健壮。