Java异常处理全解析:从崩溃到优雅恢复的秘诀

一、异常是什么?为什么需要处理?

程序运行时的"意外事故",比如:

  • 🚫 用户输入了非法数据(如字母输入到数字框)
  • 🚫 读取不存在的文件
  • 🚫 网络突然中断
    不处理的后果:将导致 JVM 异常终止,程序直接崩溃退出!就像开车遇到障碍物不刹车一样危险。

异常的抛出机制
Java 中把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象,并且抛出(throw)。然后程序员可以捕获(catch)到这个异常对象,并处理;如果没有捕获(catch)这个异常对象,那么这个异常对象将会导致程序终止。
如何对待异常?
对于程序出现的异常,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是程序员在编写程序时,就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,要编写相应的代码进行异常的检测、以及异常的处理,保证代码的健壮性

二、Java异常家族图谱

Java 中所有异常都继承自 Throwable 类,其常用方法包括:

  • getMessage():返回异常的详细信息。
  • toString():返回异常的简短描述。
  • printStackTrace():输出异常的错误信息,包括类型、原因和位置。
Throwable  
├── Error(不可恢复的致命错误, 一般不处理)  
│   ├── OutOfMemoryError(内存耗尽)  
│   └── StackOverflowError(无限递归)  
└── Exception(可处理的异常)  
    ├── RuntimeException(代码写错导致的异常,如空指针访问、数据角标越界)  
    └── 其他编译时异常(必须处理的意外情况)

在这里插入图片描述

编译时异常和运行时异常

  • 编译时异常 (Checked Exception):在编译阶段被检查,程序员必须处理,例如 FileNotFoundException。
  • 运行时异常 (Unchecked Exception):在运行时才会发生,编译器不检查,例如 ArrayIndexOutOfBoundsException。

三、常见的错误和异常

3.1 Error示例

最常见的就是 VirtualMachineError,它有两个经典的子类:StackOverflowError、OutOfMemoryError。

public class TestStackOverflowError {
    public void recursion() {
        recursion(); // 无限递归
    }
}

public class TestOutOfMemoryError {
    public void allocateMemory() {
        int[] arr = new int[Integer.MAX_VALUE]; // 内存溢出
    }
}

程序输出:
在这里插入图片描述
在这里插入图片描述

3.2 运行时异常

public class TestRunTimeException {

    @Test
    public void test01(){
        //NullPointerException
        int[][] arr = new int[3][];
        System.out.println(arr[0].length);
    }


    @Test
    public void test02(){
        //ClassCastException
        Object obj = 15;
        String str = (String) obj;
    }


    @Test
    public void test03(){
        //ArrayIndexOutOfBoundsException
        int[] arr = new int[5];
        for (int i = 1; i <= 5; i++) {
            System.out.println(arr[i]);
        }
    }


    @Test
    public void test04(){
        //InputMismatchException
        Scanner input = new Scanner(System.in);
        System.out.print("请输入一个整数:");//输入非整数
        int num = input.nextInt();
        input.close();
    }


    @Test
    public void test05(){
        int a = 1;
        int b = 0;
        //ArithmeticException
        System.out.println(a/b);
    }

}

3.3 编译时异常

无法正常通过编译
在这里插入图片描述

四、异常的处理

异常处理概述
Java 采用异常处理机制,使异常处理代码与正常代码分开,便于维护。常用的两种方法是:

1. 提前预防(防御式编程)

// 检查除数是否为0
if(b != 0) {
    System.out.println(a / b);
}

2 及时捕获 try … catch …finally

try {
    //可能出现异常的代码;
} catch (异常类名 变量名) {
    //异常的处理代码;
} finally {
    //最终执行的代码;
}

执行流程:

  1. 代码执行:程序从 try 块开始执行,若出现异常,将生成对应的异常对象并提交给 Java 运行时系统。
  2. 异常匹配:运行时系统在 catch 块中查找匹配的异常类进行处理。处理完后,程序继续执行 try…catch 之后的代码。(注意:try 中发生异常后,后续代码不再执行。)
  3. 未捕获异常:若找不到匹配的异常,JVM 会终止当前方法,抛出异常给调用者,若调用者不处理,程序将崩溃。
    说明:
  • finally 块:可选,用于确保特定代码(如关闭数据库连接或流)始终执行。
  • 异常匹配:try 中的代码被包装,一旦出现异常,生成异常对象并在 catch 中匹配处理。
  • 异常处理后:处理完后,程序继续执行后续代码。
  • catch 顺序:若 catch 中异常类型有子父类关系,子类需在父类前声明,否则会报错。
  • 常用方法:String getMessage()printStackTrace() 是处理异常的常用方式。
  • 变量作用域:在 try 块中声明的变量在块外不可用。

举例 :确保资源关闭

try {
    FileInputStream fis = new FileInputStream("test.txt"); // 可能抛出FileNotFoundException
} catch (FileNotFoundException e) {
    System.out.println("⚠️ 文件没找到!");
    e.printStackTrace();
} finally {
    System.out.println("♻️ 无论是否异常都会执行(适合关闭资源)");
}

程序输出:
在这里插入图片描述

4.3 方式 3:向上传递(throws)

尽管 try…catch…finally 可以处理异常,但并非所有异常都能由我们处理。对于无法处理的异常,Java 提供了 throws 关键字。
使用格式

修饰符 返回值类型 方法名(参数) throws 异常类名1, 异常类名2 {...}
在方法签名后使用 throws 声明多个异常类型。

异常处理要求

  • 编译时异常:必须处理,使用 try…catch…finally 或 throws。若使用 throws,调用者需处理。
  • 运行时异常:可选择不处理,出现问题后需修改代码。

方法重写中的 throws 要求

  1. 方法名:必须相同。
  2. 参数列表:必须相同。
  3. 返回值类型:
    • 基本数据类型和 void:必须相同。
    • 引用数据类型:可为父类或相同类型。
  4. 权限修饰符:子类的权限应大于或等于父类。
  5. 修饰限制:不能重写 static 或 final 方法。

throws 异常列表要求

  • 如果父类方法未声明 throws 编译时异常,子类重写时也不能声明。
  • 如果父类方法声明了 throws 编译时异常,子类可声明更少或不声明。
  • 对于运行时异常,重写时没有特别要求。
// 方法签名声明可能抛出的异常
public void readFile() throws IOException {
    // 读取文件操作...
}

异常处理方式选择

选择依据

  • 资源调用:涉及流、数据库或网络连接时,必须使用 try-catch-finally 以避免内存泄漏。
  • 父类方法:若父类方法未声明 throws,子类重写时只能用 try-catch-finally 处理异常。
  • 方法调用关系:在方法链中,a调用b、c、d,若 b、c、d 有异常,通常选择 throws;a 方法则使用 try-catch-finally。

五、异常处理黄金法则

1. 精准捕获:不要用catch(Exception e)一网打尽

// 反例:隐藏具体异常类型
try {
    //...
} catch (Exception e) { // 所有异常都进这里
    e.printStackTrace();
}

// 正例:针对性处理
try {
    //...
} catch (FileNotFoundException e) {
    System.out.println("文件路径错误");
} catch (IOException e) {
    System.out.println("读写异常");
}
  1. 资源释放:必须放在finally块
Connection conn = null;
try {
    conn = DriverManager.getConnection(url);
    // 数据库操作...
} catch (SQLException e) {
    e.printStackTrace();
} finally {
    if(conn != null) {
        try {
            conn.close(); // 确保关闭连接
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  1. 日志记录:不要只printStackTrace
catch (IOException e) {
    log.error("文件操作失败:{}", e.getMessage()); // 使用日志框架记录
    throw new RuntimeException("系统繁忙,请稍后再试"); // 给用户友好提示
}

六、 手动抛出异常对象:throw

适用场景:参数校验、业务规则校验

public void setAge(int age) {
    if(age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄不合法!"); // 手动抛异常
    }
    this.age = age;
}

异常生成方式

  • 自动生成:JVM 在运行时检测到问题,自动创建并抛出异常对象。
  • 手动创建:开发者使用 throw new 异常类名(参数) 抛出异常。如果不抛出,创建异常对象不会影响程序。

注意事项

  • 编译时异常必须通过 throws 或 try-catch 处理,运行时异常则不强制要求。
  • throw 会改变程序流程,后续代码不再执行。
  • 若方法未处理异常,throw 会提前终止方法并返回异常对象。

throws 与 throw 的区别

throwsthrow
用在方法声明后,跟异常类名用在方法体内,跟异常对象名
表示可能会抛出异常,调用者处理明确抛出异常,由当前方法处理

七、自定义异常

需求
自定义异常类用于处理核心类库未定义的特定异常情况,例如年龄负数或团队成员重复。

创建步骤

  1. 继承异常类型:
    • 编译时异常:继承 Exception
    • 运行时异常:继承 RuntimeException
  2. 提供至少两个构造器:无参构造和带 String message 的构造器。
  3. 添加 serialVersionUID。

注意事项

  • 自定义异常只能通过 throw 抛出。
  • 异常类名和消息属性重要,便于识别异常类型。
  • 自定义异常对象需手动抛出,并可由 try-catch 处理或通过 throws 传递给调用者
    格式:
public class 异常类名 extends Exception {
    static final long serialVersionUID = 23423423435L;

    无参构造
    带参构造
}

范例:

class MyException extends Exception{
    static final long serialVersionUID = 23423423435L;
    private int idnumber;


    public MyException(String message, int id) {
        super(message);
        this.idnumber = id;
    }

    public int getId() {
        return idnumber;
    }

}


public class MyExpTest {
    public void regist(int num) throws MyException {
        if (num < 0) {
            throw new MyException("人数为负值,不合理", 3);
        } else {
            System.out.println("登记人数" + num);
        }
    }


    public void manager() {
        try {
            regist(-1);
        } catch (MyException e) {
            System.out.print("登记失败,出错种类" + e.getId()); //调用自定义方法
        }
        System.out.print("本次登记操作结束");
    }


    public static void main(String args[]) {
        MyExpTest t = new MyExpTest();
        t.manager();
    }

}

输出:
在这里插入图片描述

八、高频面试题解析

Q1:Error和Exception有什么区别?
Error是JVM无法处理的致命错误(如内存溢出)
Exception是可以通过代码处理的异常

Q2:finally一定会执行吗?
除非遇到System.exit()或JVM崩溃,否则finally必定执行
Q3:try-with-resources是什么?

// 自动关闭资源(JDK7+)
try (FileInputStream fis = new FileInputStream("test.txt")) {
    // 使用资源...
} catch (IOException e) {
    e.printStackTrace();
} // 无需手动关闭,自动调用close()

九、总结:异常处理最佳实践

  • 🛡️ 预防为主:80%的异常可通过参数校验避免
  • 🎯 精准捕获:不要用宽泛的Exception捕获所有异常
  • 📝 日志留痕:记录异常上下文信息便于排查
  • 🚀 快速失败:发现问题立即抛出,避免错误扩散
  • ✨ 友好提示:给用户易懂的提示,而不是堆栈信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值