Java 异常处理详解

Java异常是Java编程语言中用于表示程序运行时错误的一种机制。Java异常体系通过异常类和异常处理来实现,允许程序在遇到预期或意外情况时,优雅地处理问题,而不是立即终止程序运行。

异常类层次结构

Java异常类都继承自java.lang.Throwable类,它是所有异常和错误的根类。Throwable类有两个直接子类:ErrorException

异常类层次结构

  • Error:这是程序无法捕获或恢复的严重错误,如系统崩溃、内存溢出等。这类错误通常不需要程序处理,因为它们通常是不可控的系统级错误。
  • Exception:这是程序在运行过程中可能出现的可以捕获和处理的异常情况。Exception又分为两类:
    • Checked Exception(编译时异常):编译器要求必须显式处理的异常,如果不处理,代码无法编译通过。例如:IOExceptionSQLException等。
    • Unchecked Exception(运行时异常):也被称为RuntimeException,编译器不要求必须捕获这类异常,但是如果出现则会导致程序立即停止运行,除非它们在代码中被捕获。例如:NullPointerExceptionIllegalArgumentExceptionArrayIndexOutOfBoundsException等。

编译时异常

编译时异常,也称为受检查异常,是指在编译阶段就需要处理的异常。这类异常通常由程序外部环境或非程序自身逻辑错误引起,比如I/O错误网络通信失败数据库连接失败等。编译时异常强调的是异常的预见性和可控性,要求程序员在编写代码时就必须考虑如何处理这些异常

特点

  • 当方法可能抛出编译时异常时,必须在方法签名中通过throws关键字声明该异常,或者在方法体内部使用try-catch块捕获并处理异常。
  • 如果调用含有声明编译时异常的方法的地方没有处理这个异常,编译器会提示错误,直到异常被适当地捕获或声明抛出为止。
  • 常见的编译时异常包括:IOExceptionSQLExceptionClassNotFoundException等。

运行时异常

运行时异常,也称为未检查异常,是指在编译时不强制处理的异常,它们通常由程序内部逻辑错误导致,如空指针异常数组越界算术异常等。运行时异常强调的是程序运行时的正确性和完整性,它们通常反映出代码逻辑的缺陷,程序员也应该尽量避免这些异常的发生,但编译器并不会强制处理。

特点:

  • 编译器不会强迫程序员在方法签名中声明运行时异常,也不会因为在方法体内没有处理运行时异常而导致编译失败。
  • 程序员可以选择捕获并处理运行时异常,但这不是必需的。若没有捕获,当运行时异常发生时,程序将终止,栈轨迹(StackTrace)会显示异常的发生位置和相关信息。
  • JVM默认会抛出运行时异常,除非在调用栈中的某一层有适当的处理代码。
  • 常见的运行时异常包括:NullPointerExceptionArrayIndexOutOfBoundsExceptionClassCastExceptionIllegalArgumentExceptionArithmeticException等。

JVM默认处理异常的方式

Java程序在运行时抛出一个异常,并且没有在当前线程的调用栈中找到合适的catch块来捕获这个异常时,JVM会按照以下步骤来处理这个未捕获的异常:

  1. 寻找最近的未处理异常处理器(UncaughtExceptionHandler)
    • 每个线程都可以设置一个未处理异常处理器,如果线程抛出了未捕获的异常,JVM会首先查找该线程是否设置了自定义的Thread.UncaughtExceptionHandler,如果有,则调用该处理器的uncaughtException(Thread t, Throwable e)方法来处理异常。
  2. 默认的未处理异常处理器
    • 如果线程没有设置自定义的UncaughtExceptionHandler,JVM将使用默认的异常处理器。默认的处理器通常会打印异常的堆栈跟踪信息到标准错误输出(System.err)。
  3. 终止线程
    • 无论是自定义的还是默认的未处理异常处理器,在处理完异常后,JVM通常会选择终止抛出异常的线程。对于主线程(main thread)而言,整个Java应用也会随之退出,因为它承载了Java程序的入口点。

说明:

JVM对于未捕获的异常,默认行为是打印堆栈追踪信息到标准错误输出,并结束抛出异常的线程(如果是主线程,则结束整个Java应用程序)。当然,开发人员可以通过设置自定义的UncaughtExceptionHandler来改变这种默认行为,以便进行更细致的异常处理,比如记录日志、通知监控系统或者进行其他必要的清理工作。

异常处理机制

Java提供了以下几种机制来处理异常:

throws关键字

  • 在方法签名中声明该方法可能抛出的异常。
  • throws格式是跟在方法的括号后面的

定义格式:

public void 方法() throws 异常类名1,异常类名2 {
}

代码示例

public class Demo01_Throws {

    public static void main(String[] args) throws ParseException {
        System.out.println("开始");
        // method();
        method2();
        System.out.println("结束");
    }

    // 运行时异常
    public static void method() throws ArrayIndexOutOfBoundsException{
        int[] arr = {1, 2, 3};
        System.out.println(arr[3]);
    }

    // 编译时异常
    public static void method2() throws ParseException {
        String s = "2048-08-09";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date d = sdf.parse(s);
        System.out.println(d);
    }
}

throw关键字

  • 使用throw关键字可以抛出一个异常对象,后面的代码不再执行。
  • 格式:throw new 异常();

代码示例

public class Demo02_Throw {
    public static void main(String[] args) {
        int [] arr = {1,2,3,4,5};
        // int [] arr = null;
        printArr(arr);//就会 接收到一个异常.
        //我们还需要自己处理一下异常.
    }

    private static void printArr(int[] arr) {
        if(arr == null){
            //调用者知道成功打印了吗?
            //System.out.println("参数不能为null");
            throw new NullPointerException(); //当参数为null的时候
            //手动创建了一个异常对象,抛给了调用者,产生了一个异常
        }else{
            for (int i = 0; i < arr.length; i++) {
                System.out.println(arr[i]);
            }
        }
    }
}

try-catch语句

  • 用于捕获异常并提供恢复措施。

执行流程

  • 程序从 try 里面的代码开始执行
  • 出现异常,就会跳转到对应的 catch 里面去执行
  • 执行完毕之后,程序还可以继续往下执行
try {
    // 可能抛出异常的代码
} catch (ExceptionType name) {
    // 异常处理代码
}

try-catch-finally语句

  • 无论是否发生异常,都会执行的代码块。
try {
    // 可能抛出异常的代码
} catch (ExceptionType name) {
    // 异常处理代码
} finally {
    // 总会执行的代码
}

代码示例

public class Demo03_Try {
    public static void main(String[] args) {
        System.out.println("开始");
        method();
        System.out.println("结束");
    }

    public static void method() {
        try {
            int[] arr = {1, 2, 3};
            System.out.println(arr[3]);
            System.out.println("这里能够访问到吗");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("你访问的数组索引不存在,请回去修改为正确的索引");
        }finally {
            System.out.println("一直会执行,一般用来释放资源...");
        }
    }
}

try-with-resources语句

try-with-resources语句是一种用于自动管理和关闭资源的异常处理机制,它从Java 7开始引入,旨在简化资源清理工作,确保即使在发生异常的情况下,资源也能被正确地关闭。这种语句适用于那些实现了java.lang.AutoCloseable接口的对象,如文件流、套接字、数据库连接等,这些对象在使用完毕后需要显式关闭以释放系统资源。

基本语法

try (
    ResourceType resource1 = initializer1;
    ResourceType resource2 = initializer2;
    // ... 其他资源声明与初始化
) {
    // 在此处使用资源进行操作
}
catch (ExceptionType1 ex1) {
    // 处理与resource1或resource2等相关的异常
}
catch (ExceptionType2 ex2) {
    // 处理其他特定类型的异常
}
finally {
    // 可选的finally块,用于执行额外的清理工作(非资源关闭)
}

特点与优势

  • 自动关闭:当try块结束时,不论是因为正常执行到结束还是因为抛出并捕获了异常,Java都会自动调用资源对象的close()方法来关闭资源。这避免了手动编写finally块来确保资源关闭,提高了代码的简洁性和可靠性。
  • 多资源支持try语句内的资源声明可以有多个,用分号隔开。所有资源按照声明顺序逆序关闭,即后声明的资源先关闭。这样即使在关闭一个资源时抛出异常,后续资源仍有机会被正确关闭。
  • 异常处理:如果有异常在try块内抛出,catch子句可以捕获并处理这些异常。如果在关闭资源过程中也抛出了异常,那么这个关闭异常会被抑制(suppressed)并添加到已存在的异常中(可通过Throwable.getSuppressed()访问)。如果try块内没有异常,但关闭资源时抛出异常,则这个关闭异常会作为try-with-resources语句的结果抛出。

代码示例

使用java.util.Scanner读取用户从控制台输入的一行文本:

try (Scanner scanner = new Scanner(System.in)) {
    System.out.println("请输入文本: ");
    String inputLine = scanner.nextLine();
    System.out.println("你输入的文本: " + inputLine);
} catch (Exception e) {
    System.err.println("读取录入内容出错:");
    e.printStackTrace();
}

自定义异常

开发者可以创建自定义异常类,通常是通过继承Exception类或其子类,用来表示程序中特有的、标准异常类无法精确描述的异常情况。

创建自定义异常类的步骤:

  1. 继承异常基类:自定义异常类通常继承自java.lang.Exception或其子类。如果希望自定义的是运行时异常(无需强制捕获),可以继承自java.lang.RuntimeException。如果希望自定义的是编译时异常(需要强制捕获),则直接继承自java.lang.Exception
  2. 添加构造方法:自定义异常类通常至少包含一个构造方法,用于初始化异常对象。构造方法通常接受一个字符串参数,用于存储详细的异常信息(如错误描述)。还可以添加其他构造方法,如接受多个参数或无参构造方法。
  3. 可选地,添加属性和方法:根据需要,自定义异常类可以添加特定的属性(如错误代码、错误详情等)和方法,以便提供更多关于异常的上下文信息。
代码示例

创建一个简单的自定义异常类示例:

public class InvalidInputException extends Exception {

    public InvalidInputException(String message) {
        super(message);
    }

    public InvalidInputException(String message, Throwable cause) {
        super(message, cause);
    }
}

解析

  • InvalidInputException继承自java.lang.Exception,意味着它是一个编译时异常,需要在代码中显式捕获或声明抛出。
  • 定义了两个构造方法:
    • 第一个构造方法接收一个字符串参数message,用来描述异常的具体信息。它调用父类Exception的构造方法,将传入的message传递给父类,以便在异常堆栈信息中显示。
    • 第二个构造方法接收两个参数:messagecausemessage同上,cause是一个Throwable对象,用于表示引发此异常的底层原因。这个构造方法同样调用父类的相应构造方法,将messagecause传递给父类。

使用自定义异常类的示例:

假设有一个方法calculateAverage(),它接收一个整数数组并计算平均值。如果数组为空,我们希望抛出InvalidInputException

public class Demo05_ExcetionTest {
    public static void main(String[] args) throws InvalidInputException {
        int[] num = {99,65,78,63,45,15,94,64};
        double ca = calculateAverage(num);
        System.out.println(ca);//输出: 65.375
    }

    /* 获取数组的平均值*/
    public static double calculateAverage(int[] numbers) throws InvalidInputException {
        if (numbers == null || numbers.length == 0) {
            throw new InvalidInputException("Input array is empty or null.");
        }

        int sum = 0;
        for (int number : numbers) {
            sum += number;
        }
        return (double) sum / numbers.length;
    }
}

解析

  • 方法calculateAverage()声明抛出InvalidInputException,表明它可能在执行过程中抛出这个自定义异常。
  • 如果传入的数组为空或为null,方法会使用InvalidInputException的构造方法创建一个新的异常对象,并通过throw关键字抛出该异常。

异常处理的最佳实践

  • 捕获具体的异常:避免捕获ThrowableException类,这会隐藏错误和程序中的问题。
  • 提供有用的异常信息:在自定义异常中提供有用的错误信息。
  • 避免在finally中抛出异常finally块中抛出的异常会覆盖之前捕获的异常,导致调试困难。
  • 使用受检异常表示非程序逻辑错误:如文件不存在、用户名无效等。
  • 使用非受检异常表示程序逻辑错误:如空指针访问、数组越界等。
  • 36
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值