手写重试框架、try-with-resources 性能对比、自定义登录异常


📖 前言

异常处理是 Java 程序健壮性的基石。很多人会用 try-catch,但说不清 finally 什么时候执行、throws 和 throw 的区别、自定义异常的设计原则、以及异常对性能的影响。本文通过五个实验带你全面掌握异常处理,并实现一个生产级重试工具类


🧠 一、知识点速览(背下这张表)

关键词 作用 使用位置
try 包裹可能抛出异常的代码 方法内
catch 捕获并处理特定异常 紧随 try 或 finally 之前
finally 无论是否发生异常都会执行的代码块 可选,放在最后
throws 声明方法可能抛出的异常(向上传递) 方法签名
throw 手动抛出异常对象 方法内
try-with-resources 自动关闭实现了 AutoCloseable 的资源 Java 7+

异常体系简图

Throwable
├── Error(系统级错误,不可处理)
└── Exception
    ├── RuntimeException(非受检异常,如 NullPointerException)
    └── 其他 Exception(受检异常,如 IOException)

💡 最佳实践:能用 if 判断避免的异常,尽量不要用异常处理(性能损耗大);自定义异常要继承 Exception 或 RuntimeException,并提供有意义的 message。


📋 二、题目驱动

题目:阐述 Java 异常处理机制的核心组成,并设计一个用户登录模块,要求使用自定义异常处理用户名或密码错误的情况。同时分析 try-catch 对性能的影响。


🛠️ 三、手把手实操(附完整代码 + 截图位置)

3.1 基础异常处理演示

运行 ExceptionBasics.java 的控制台输出及完整代码:

完整代码

public class ExceptionBasics {
    public static void main(String[] args) {
        // 1. ArithmeticException
        try {
            int a = 10 / 0;
            System.out.println("这行不会执行");
        } catch (ArithmeticException e) {
            System.out.println("捕获算术异常: " + e.getMessage());
        }

        // 2. ArrayIndexOutOfBoundsException
        try {
            int[] arr = new int[3];
            arr[5] = 100;
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("捕获数组越界异常: " + e.getMessage());
        }

        // 3. 多重 catch 和 finally
        try {
            String str = null;
            System.out.println(str.length());
        } catch (NullPointerException e) {
            System.out.println("空指针异常: " + e);
        } catch (Exception e) {
            System.out.println("其他异常: " + e);
        } finally {
            System.out.println("finally 块总会执行");
        }

        // 4. throws 声明异常
        try {
            methodThrowsException();
        } catch (InterruptedException e) {
            System.out.println("捕获线程中断异常: " + e);
        }
    }

    public static void methodThrowsException() throws InterruptedException {
        Thread.sleep(1000);
        System.out.println("sleep 结束");
    }
}

预期输出

捕获算术异常: / by zero
捕获数组越界异常: Index 5 out of bounds for length 3
空指针异常: java.lang.NullPointerException
finally 块总会执行
sleep 结束
捕获线程中断异常: java.lang.InterruptedException

🔍 要点

  • 一个 try 可跟多个 catch,注意异常类型从上到下具体到一般。

  • finally 在 return 之前执行,除非 System.exit()

  • throws 将异常抛给调用者处理。


3.2 自定义异常 + 登录验证

 运行 LoginExceptionDemo.java 的控制台输出

完整代码

预期输出

尝试登录: 用户名=, 密码=123456 -> 失败: 用户名不能为空
尝试登录: 用户名=admin, 密码= -> 失败: 密码不能为空
尝试登录: 用户名=wrong, 密码=123456 -> 失败: 用户名错误
尝试登录: 用户名=admin, 密码=wrong -> 失败: 密码错误
尝试登录: 用户名=admin, 密码=123456 -> 登录成功!

设计要点

  • 自定义异常类通常提供带 String 参数的构造器。

  • 业务方法抛出异常,由上层统一捕获处理。


3.3 创新点:try-with-resources 与性能对比

 运行 TryWithResourcesDemo.java 的控制台输出(尤其关注性能对比数字):

完整代码

预期输出示例

传统方式读取: Hello, Exception!
try-with-resources 读取: Hello, Exception!
性能对比: 无异常耗时 = 2.34 ms, 有异常耗时 = 345.67 ms

结论

  • 异常对象的创建和填充堆栈轨迹非常耗时(比普通判断慢两个数量级)。

  • 能用逻辑判断避免的异常,绝不用异常控制流程


3.4 竞赛题:实现重试工具类(模拟网络请求)

 运行 RetryUtils.java 的控制台输出及完整代码:

完整代码

java

import java.util.function.Supplier;

public class RetryUtils {
    public static <T> T retry(Supplier<T> task, int retryTimes, long delayMs,
                              Class<? extends Exception> exceptionClass) throws Exception {
        Exception lastException = null;
        for (int i = 0; i < retryTimes; i++) {
            try {
                return task.get();
            } catch (Exception e) {
                lastException = e;
                if (exceptionClass.isInstance(e) && i < retryTimes - 1) {
                    System.out.printf("第%d次执行失败: %s,%dms后重试...\n", i+1, e.getMessage(), delayMs);
                    Thread.sleep(delayMs);
                } else {
                    throw e;
                }
            }
        }
        throw lastException;
    }

    private static int unreliableService(int id) throws Exception {
        if (Math.random() < 0.7) {
            throw new Exception("服务调用失败,id=" + id);
        }
        return id * 100;
    }

    public static void main(String[] args) throws Exception {
        Integer result = retry(() -> unreliableService(42), 5, 500, Exception.class);
        System.out.println("最终成功,结果: " + result);
    }
}

预期输出示例

text

第1次执行失败: 服务调用失败,id=42,500ms后重试...
第2次执行失败: 服务调用失败,id=42,500ms后重试...
第3次执行成功,结果: 4200
最终成功,结果: 4200

竞赛价值

  • 考察函数式接口Supplier<T>)、泛型异常处理重试策略

  • 可扩展为带退避算法(指数退避)的高级重试框架。

  • 是真实项目中(如 RPC 调用、数据库连接)常用技巧。


📝 四、总结与最佳实践

规范 说明
❌ 不要忽略异常 catch 块至少打印日志,或者重新抛出
✅ 使用特定的异常类型 避免 catch (Exception e) 万能处理
✅ 早抛出,晚捕获 底层抛出具体异常,高层统一处理
✅ 资源使用 try-with-resources 比 finally 手动关闭更简洁安全
⚡ 性能敏感代码避免异常 用 if 预防代替异常捕获
📦 自定义异常要提供带 cause 的构造器 便于链式追踪

🎯 阿里巴巴 Java 开发手册:异常不要用来做流程控制;finally 块中不要使用 return。


📚 参考文献

  • Java 官方异常处理教程

  • 《Java面向对象程序设计(第四版)》

  • 《Effective Java》第三版 - 异常章节


🙏 写在最后

如果你觉得这篇文章对你有帮助,请点赞👍 + 收藏⭐ + 评论💬 支持一下!
你的鼓励是我持续输出硬核技术文章的动力。

更多推荐