JavaSE丨异常处理详解,高效应对程序中的“意外”

一、异常

1.1 概述

        程序在运行过程中,由于意外情况导致程序发生异常事件,默认情况下发生的异常会中断程序的运行

        在Java中,把常见的异常情况,都抽象成了对应的异常类型,那么每种异常类型都代表了一种特定的异常情况。

        当程序中出现一种异常情况时,也会创建并抛出一个异常类型对象,这个对象就表示当前程序所出现的问题。

如图:

        例如,程序中有一种异常情况是,当前使用下标从数组中取值的时候,这个下标值超过了数组下标的最大值,那么程序中就出现了异常情况,java中把这种异常情况抽象成了一个类: java.lang.ArrayIndexOutOfBoundsException ,表示程序中出现了数组下标超过边界的异常情况。

案例展示:

观察下面各种异常情况:

//如何理解异常:
// 程序不正常情况,统称为 异常
public class Test_Basic {
    public static void main(String[] args) {
        // ArithmeticException
        int a = 10 / 0;

        String s = "abc";
        //: NumberFormatException
        int n = Integer.parseInt(s);

        Object obj = new Object();//new String("hello");
        //类型转换异常:ClassCastException
        s = (String)obj;

        int[] arr = {1,2,3,4};

        arr = null;
        //空指针异常:NullPointerException
        System.out.println(arr[0]);

        //数组索引越界 ArrayIndexOutOfBoundsException
        System.out.println(arr[4]);
    }
}

运行结果:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Test_Basic.main(Test_Basic.java:6)

        可以看出,当前程序出现异常情况时,会创建并抛出和该异常情况对应的异常类的对象,这个异常对象中保存了一些信息,用来表示当前程序到底发生了什么异常情况。

        通过异常信息,我们可以定位异常发生的位置,以及异常发生的原因

1.2 异常体系

        异常体系中的根类是: java.lang.Throwable ,该类下面有两个子类型, java.lang.Errorjava.lang.Exception

        注意:Throwable 表示可以被抛出的

  • Error ,表示错误情况,一般是程序中出现了比较严重的问题,并且程序自身并无法进行处理。
  • Exception ,表示异常情况,程序中出了这种异常,大多是可以通过特定的方式进行处理和纠正的,并且处理完了之后,程序还可以继续往下正常运行

1.3 异常种类

        我们平时使用的异常类型,都是两种:

  •  编译时异常
  •  运行时异常
编译时异常:
  • 继承自 Exception 类的子类型,也称为checked exception
  • 编译器在编译期间,会主动检查这种异常,如果发现异常则必须显示处理, 否则程序就会发生错误,无法通过编译
运行时异常 :
  • RuntimeException 类及其子类,也称为unchecked exception
  • 编译器在编译期间,不会检查这种异常,也不要求我们去处理,但是在运行期间,如果出现这种异常则自动抛出

1.4 异常传播

        如果一个方法中出现了异常的情况,系统默认的处理方式是:自动创建异常对象,并将这个异常对象抛给当前方法的调用者,并一直向上抛出,最终传递给 JVM,JVM默认处理步骤有2步:

  1. 把异常的名称,错误原因及异常出现的位置等信息输出在了控制台
  2. 程序停止执行
案例展示:
public class Test_Default {
    public static void main(String[] args) {
        System.out.println("hello");
        test1();
        System.out.println("world");
    }

    public static void test1() {
        test2();
    }

    public static void test2() {
        test3();
    }

    public static void test3() {
        //下面代码会抛出异常
        int a = 1 / 0;
    }
}

运行结果:

hello
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Test_Default.test3(Test_Default.java:18)
	at Test_Default.test2(Test_Default.java:13)
	at Test_Default.test1(Test_Default.java:9)
	at Test_Default.main(Test_Default.java:4)
代码执行步骤解析:
  1. 因为 java.lang.ArithmeticException 是运行时异常,所以代码可以编译通过
  2. 程序运行时,先输出"hello",然后一层一层调用,最终执行test3方法
  3. 执行test3方法时,出现除数为0的情况,系统自动抛出异常
  4. java.lang.ArithmeticException 代码中没有对异常进行任何捕获处理,所以该异常往上传递给test2 --> test1 --> main --> JVM
  5. JVM虚拟机拿到异常后,输出异常相关信息,然后终止程序

二、异常抛出

2.1 自动抛出

        Java代码中,出现了提前指定好的异常情况的时候,代码会自动创建异常对象, 并且将该异常对象抛出。

        例如,上述案例中执行 int a = 1/0; 的时候,代码会自动创建并抛出 ArithmeticException 类型的异常对象,来表示当前的这种异常情况。(算术异常)

        又如,代码中执行自动创建并抛出 String str = null; str.toString(); 的时候,代码会 NullPointerException 类型的异常对象,来表示当前这种异常情况。(空指针异常)

2.2 手动抛出

        以上描述的异常情况,都是JVM中提前规定好的,我们不需要干预,JVM内部自己就会创建并抛出异常对象。

        但是在其他的一些情况下,我们也可以手动的创建并抛出异常对象,抛出后系统也会按照默认的方式去处理

        手动抛出异常固定格式: throw 异常对象;

案例展示:
public class AgeValidator {
    // 验证年龄的方法
    public static void checkAge(int age) {
        // 规定年龄必须在0-150之间,否则视为无效
        if (age < 0 || age > 150) {
            // 手动创建并抛出异常对象
            throw new IllegalArgumentException("年龄必须在0到150之间,当前值:" + age);
        }
        System.out.println("年龄验证通过:" + age);
    }

    public static void main(String[] args) {
        // 测试正常情况
        checkAge(25); // 年龄有效,会输出验证通过信息
        
        // 测试异常情况
        checkAge(-5); // 年龄无效,会触发手动抛出的异常
    }
}

三、异常处理

代码中出现了异常,除了默认的处理方式外,我们还可以手动处理异常

  • 声明继续抛出异常,借助throws关键字实现
  • 捕获并处理异常,借助try、catch、finally关键字实现

3.1 throws

        throws关键字用于在方法声明中指定该方法可能抛出的异常类型

        这个声明的目的,就是告诉方法的调用者,调用这个方法的时候要小心 ,方法在运行的时候可能会抛出指定类型的异常。

案例展示:

多个异常声明

import java.io.FileInputStream;
import java.io.IOException;

public class ThrowsExample {
    // 声明该方法可能会抛出IOException和NullPointerException异常
    public static void processFile(String filePath) throws IOException, NullPointerException {
        if (filePath == null) {
            throw new NullPointerException("文件路径不能为null");
        }
        FileInputStream fis = new FileInputStream(filePath);
        // 其他文件处理逻辑
        fis.close();
    }

    public static void main(String[] args) {
        String filePath = null;
        try {
            processFile(filePath);
        } catch (IOException | NullPointerException e) {
            System.out.println("发生异常: " + e.getMessage());
        }
    }
}
我们将throw与throws进行一个对比:
对比维度throw 关键字throws 关键字
作用手动抛出一个具体的异常对象(触发异常)声明方法可能会抛出的异常类型(告知风险)
使用位置方法体内部(用于执行具体的异常抛出动作)方法签名末尾(用于声明异常,格式:方法名() throws 异常类型
抛出内容必须是异常对象(如 throw new Exception()必须是异常类型(如 throws IOException
数量限制一次只能抛出一个异常对象可以声明多个异常类型,用逗号分隔(如 throws AException, BException
处理要求抛出异常后,要么用 try-catch 处理,要么用 throws 声明抛给上层声明异常后,调用者必须处理(try-catch)或继续用 throws 向上声明
适用场景主动触发异常(如业务规则校验失败时)告知方法调用者 “此方法可能会引发这些异常,需注意处理”
总结
  • throw 是 “主动扔出一个具体的异常”(执行动作);
  • throws 是 “提前声明方法可能会扔出哪些异常”(告知风险)。

3.2 try-catch

        try-catch 语句块,就是用来对指定代码,进行异常捕获处理,并且处理完成后,JVM不会停止运行,代码还可以正常的往下运行。

捕获异常语法:
 try {
    可能会出现异常的代码;
 }catch(异常类型 引用名) {
    //处理异常的代码,可以是简单的输出异常信息
    //也可以使用日志进行了记录,也可以对数据进行修改纠正等操作

    //一般输出异常信息
    //e.printStackTrace();
}

try:该代码块中包含可能产生异常的代码

catch:用来进行某种类型异常的捕获,并对捕获到的异常进行处理

执行流程:
  1. 程序从 try 里面的代码开始执行
  2. 出现异常,就会跳转到对应的 catch块 里面去执行
  3. 执行完毕之后,程序出 catch块,继续往下执行
案例展示:

模拟用户输入数字并进行除法运算的场景,处理可能出现的输入格式错误和除零异常

import java.util.Scanner;

public class DivisionCalculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        System.out.println("===== 除法计算器 =====");
        
        try {
            // 尝试获取用户输入的被除数
            System.out.print("请输入被除数(整数):");
            String num1Str = scanner.nextLine();
            int num1 = Integer.parseInt(num1Str);  // 可能抛出NumberFormatException
            
            // 尝试获取用户输入的除数
            System.out.print("请输入除数(整数):");
            String num2Str = scanner.nextLine();
            int num2 = Integer.parseInt(num2Str);  // 可能抛出NumberFormatException
            
            // 尝试执行除法运算
            int result = num1 / num2;  // 可能抛出ArithmeticException(除数为0时)
            
            // 如果以上步骤都没有异常,输出计算结果
            System.out.println("计算结果:" + num1 + " ÷ " + num2 + " = " + result);
        } 
        catch (NumberFormatException e) {
            // 处理输入格式错误(用户输入的不是整数)
            System.out.println("错误:请输入有效的整数!" + e.getMessage());
        } 
        catch (ArithmeticException e) {
            // 处理除法异常(除数为0)
            System.out.println("错误:" + e.getMessage() + ",除数不能为0!");
        }
        
        // 无论是否发生异常,都会执行以下代码
        System.out.println("\n程序执行结束,感谢使用!");
        scanner.close();
    }
}
    

上述案例中体现出,如果try语句块中的代码可能抛出多种异常,并且是不同类型的,则可以写多个 catch语句块,用来同时捕获多种类型异常。

注意事项:

这种异常处理方式,要求多个catch中的异常不能相同

如果catch中的多个异常类之间有子父类关系的话,那么子类异常必须写在父类异常上面的catch块中,父类异常必须写在下面的catch块中。 因为如果父类型异常再最上面的话,下面catch语句代码,永远不会被执行!

3.3 finally 语句 

        finally 关键字可以和 try、catch关键字一起使用,固定搭配为: try catch-finally ,它可以保证指定finally中的代码一定会执行,无论是否发生异常!

finally 块的主要作用:
  • 资源释放:在 try 块中打开的资源(例如文件、数据库连接、网络连接等) 可以在 finally 块中关闭或释放,以确保资源的正确释放,即使在发生异常的情况下也能够执行释放操作。
  • 清理操作: finally 块可以用于执行一些清理操作,例如关闭打开的流、释放锁、取消注册监听器等。
  • 异常处理的补充:finally块可以用于在try块和catch块之后执行一些必要的操作,例如记录日志、发送通知等。
案例展示:
import java.io.FileInputStream;
import java.io.IOException;

public class FileReaderExample {
    public static void main(String[] args) {
        FileInputStream fis = null; // 声明文件输入流变量
        
        try {
            // 尝试打开文件并读取内容
            fis = new FileInputStream("data.txt");
            System.out.println("文件打开成功,准备读取...");
            
            // 模拟读取过程中可能发生的异常(例如文件内容异常)
            int data = fis.read();
            if (data == -1) {
                throw new IOException("文件内容为空");
            }
            
            System.out.println("文件读取完成,内容:" + (char) data);
        } catch (IOException e) {
            // 处理文件操作相关异常
            System.out.println("文件操作出错:" + e.getMessage());
        } finally {
            // 无论是否发生异常,都必须关闭文件流(释放资源)
            System.out.println("进入finally块,准备关闭文件流...");
            if (fis != null) { // 确保流对象已初始化
                try {
                    fis.close(); // 关闭流可能也会抛出IOException
                    System.out.println("文件流已成功关闭");
                } catch (IOException e) {
                    System.out.println("关闭文件流时出错:" + e.getMessage());
                }
            }
        }
        
        System.out.println("程序执行结束");
    }
}

四、自定义异常

如果要自定义一个编译时异常类型,就自定义一个类,并继承 Exceptionn

如果要自定义一个运行时异常类型,就自定义一个类,并继承 RuntimeException

自定义异常步骤:

无论哪种类型的自定义异常,定义步骤基本一致,核心是 “继承父类 + 提供构造方法”

  1. 定义异常类
    类名通常以 Exception 结尾(如 InvalidAgeExceptionPasswordErrorException),直观体现异常含义。

  2. 指定继承关系

    • 编译时异常:public class 自定义类名 extends Exception { ... }
    • 运行时异常:public class 自定义类名 extends RuntimeException { ... }
  3. 提供构造方法
    必须至少包含两种构造方法(通过 super() 调用父类构造):

    • 空参构造:用于创建无详细信息的异常对象。
    • 带参构造:接收一个 String 类型的异常信息(描述错误原因),传给父类保存(便于通过 getMessage() 获取)。
案例展示:
// 自定义编译时异常
public class InvalidAgeException extends Exception {
    // 空参构造
    public InvalidAgeException() {
        super(); // 调用父类Exception的空参构造
    }
    
    // 带异常信息的构造
    public InvalidAgeException(String message) {
        super(message); // 调用父类Exception的带参构造,保存异常信息
    }
}

// 自定义运行时异常
public class EmptyNameException extends RuntimeException {
    public EmptyNameException() {
        super();
    }
    
    public EmptyNameException(String message) {
        super(message);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值