Java 异常

在程序执行过程中,可能有各种出错的情况,有的是不可控的内部原因,比如内存不够了、磁盘满了,有的是不可控的外部原因,比如网络连接有问题,更多的可能是程序的编写错误,比如引用变量未初始化就直接调用实例方法。这些非正常情况在Java中都被认为是异常。

异常简介

异常是相对于 return 的一种退出机制,可以由系统触发,也可以由程序通过 throw 语句触发,异常可以通过 try/catch 语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息。

当发生异常时,Java 会启用异常处理机制,首先创建一个异常对象,然后异常处理机制会从当前方法开始查找看谁 “捕获” 了这个异常,当前方法没有就查看上一层,直到 main 方法,如果也没有,就使用默认机制,输出异常栈信息并退出。

异常栈信息包括从异常发生点到最上层调用者的轨迹,还包括行号,是分析异常最为重要的信息。

对于异常栈信息,普通用户无法理解,也不知道该怎么办,我们需要给用户一个更为友好的信息,又或者我们不希望程序在出现异常后就退出程序,而是抛出异常然后继续执行,为此需要使用 try/catch 捕获异常。

try 后面的花括号 {} 内包含可能抛出异常的代码,catch 语句包含能捕获的异常和处理代码,捕获异常后,程序就不会异常退出了,但 try 语句内异常点之后的代码不会执行,执行完 catch 内的语句后,程序会继续执行 try/catch 之后的代码。

public class TestUtil {

    public static void main(String[] args) {
        excepTest1();
        excepTest2();
        System.out.println("main 方法没有捕获异常,不会继续执行");
    }

    public static void excepTest1(){
        /**
         * 发生异常时,会启用异常处理机制,首先创建一个异常对象
         * 异常处理机制会从当前方法开始查找看谁 “捕获” 了这个异常,
         * 当前方法没有就查看上一层,直到 main 方法,
         * 如果也没有,就使用默认机制,输出异常栈信息并退出
         */
        String str = "abc";
        try {
            int num = Integer.parseInt(str);
            System.out.println("如果异常,try 语句内,异常点后的代码不会执行");
            System.out.println(num);
        } catch (NumberFormatException e1){
            System.err.println("参数:"+ str +",不是有效数字");
            e1.getMessage();
            e1.getCause();
            e1.printStackTrace();
        } catch (Exception e2){
            /**
             * 异常处理机制将根据抛出的异常类型找第一个匹配的 catch 块,
             * (Java 7 开始,多个异常之间可以用“|”操作符,但注意异常之间不能有父子关系)
             * 找到后,执行catch块内的代码,不再执行其他 catch 块,
             * 如果没有找到,会继续到上层方法中查找。
             * 需要注意的是,如果抛出的异常类型是 catch 中声明异常的子类也算匹配(多态)
             */
            System.err.println("异常类型不是 NumberFormatException 才会执行");
            throw e2;
        } finally {
            System.out.println("不管有没有异常,一定执行");
        }
        System.out.println("当前有捕获异常,方法会继续执行");
    }

    public static void excepTest2(){
        String str = "abc";

        int num = Integer.parseInt(str);
        System.out.println("当前没有捕获异常,方法不会继续执行,向上级方法查找异常捕获");
        System.out.println(num);
    }

    /**
     * throws 跟在方法的括号后面,可以声明多个异常,以逗号分隔
     * 主要用于在父类方法中声明可能(包括子类)抛出的异常
     * @throws Exception
     */
    public static void excepTest3() throws Exception {
        try {

        } catch (Exception e){
            e = new Exception(); // 注释该行,可以去掉方法后的 throws
            throw e;
        }
    }
}

异常体系

[图片上传失败…(image-3fc672-1677849073641)]

Throwable

java.lang.Throwable 是所有异常的基类,它有两个子类:Error 和 Exception。

Error 用来指示运行时环境发生的错误,一般发生在严重故障时,Java 程序通常不捕获错误,比如 内存溢出错误(OutOfMemory-Error)和栈溢出错误(StackOverflowError)。

Exception 表示应用程序错误,它有很多子类,应用程序也可以通过继承 Exception 或其子类创建自定义异常。

RuntimeException 是未受检异常(unchecked exception),IOException、SQLException 和 Exception 自身则是受检异常(checked exception)。对于受检异常,Java 会强制要求程序员进行处理,否则会有编译错误,而对于未受检异常则没有这个要求。

大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。定义这么多不同的异常类主要是为了名字不同,异常类的名字本身就代表了异常的关键信息。

  • 构造方法
public Throwable()
public Throwable(String message)
public Throwable(String message, Throwable cause)
public Throwable(Throwable cause)

message,表示异常消息;cause,表示触发该异常的其他异常。

异常可以形成一个异常链,上层的异常由底层异常触发,cause表示底层异常。

  • 设置 cause

这个方法最多只能被调用一次

Throwable initCause(Throwable cause)
  • 保存异常栈信息

用当前的调用栈层次填充 Throwable 对象栈层次,添加到栈层次信息中。

public Throwable fillInStackTrace()
  • 获取异常信息
public String getMessage()
  
public Throwable getCause()
  • 打印异常栈信息
public void printStackTrace()

自定义异常

一般是继承 Exception 或者它的某个子类。如果父类是 RuntimeException 或它的某个子类,则自定义异常也是未受检异常;如果是 Exception 或 RuntimeException 以外的其他子类,则自定义异常是受检异常。

// 自定义异常类,继承Exception类
public class InsufficientFundsException extends Exception {
  // 此处的amount用来储存当出现异常(取出钱多于余额时)所缺乏的钱
  private double amount;
  
  public InsufficientFundsException(double amount) {
    this.amount = amount;
  } 
  
  public double getAmount() {
    return amount;
  }
}

异常处理

try catch

使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方,try/catch 中的代码称为保护代码。

// 自定义异常类,继承Exception类
public class InsufficientFundsException extends Exception {
  // 此处的amount用来储存当出现异常(取出钱多于余额时)所缺乏的钱
  private double amount;
  public InsufficientFundsException(double amount) {
    this.amount = amount;
  } 
  public double getAmount() {
    return amount;
  }
}

一个 try 代码块后面跟随多个 catch 代码块的情况就叫多重捕获。

try {
    file = new FileInputStream(fileName);
    x = (byte) file.read();
} catch(FileNotFoundException f) { // Not valid!
    f.printStackTrace();
    return -1;
} catch(IOException i) {
    i.printStackTrace();
    return -1;
}

如果保护代码中发生异常,异常被抛给第一个 catch 块。如果不匹配,它会被传递给第二个 catch 块,直到异常被捕获或者通过所有的 catch 块。

需要注意的是,抛出的异常类型是 catch 中声明异常的子类也算匹配,所以需要将最具体的子类放在前面。

这种写法比较烦琐,Java 7开始支持一种新的语法,多个异常之间可以用 “|” 操作符。

try {
  //可能抛出 ExceptionA和ExceptionB
} catch (ExceptionA | ExceptionB e) {
  e.printStackTrace();
}

throw

在 catch 块内处理完后,可以重新抛出异常,异常可以是原来的,也可以是新建的。

如果一个方法抛出检查性异常,那么该方法必须使用 throws 关键字来声明抛出的异常,否则不能抛出。

throws 跟在方法的括号后面,可以声明多个异常,以逗号分隔。

声明的含义是,这个方法内可能抛出这些异常,且没有没有处理完这些异常,调用者必须进行处理。

public void test() throws Exception {
  try{
    // 可能触发异常的代码
  }catch(NumberFormatException e){
    System.out.println("not valid number");
    throw new AppException("输入格式不正确", e);
  }catch(Exception e){
    e.printStackTrace();
    throw e;
  }
}

finally

finally 内的代码不管有无异常发生,都会执行。一般用于释放资源,如数据库连接、文件流等。

  • 如果没有异常发生,在 try 内的代码执行结束后执行
  • 如果有异常发生且被 catch 捕获,在 catch 内的代码执行结束后执行
  • 如果有异常发生但没被 catch 捕获,则在异常 throw 给上层之前执行
  • 如果在 try 或者 catch 语句内有 return 语句,则 return 语句在 finally 语句执行结束后才执行,对于基本数据类型,finally 中并不能改变返回值(可以理解为返回值存储在返回值存储器中,finally 的操作不会改变返回值存储器中的值),对于对象类型,可以改变其中的值(返回值存储器存储的是对象地址)。
  • 如果 finally 中有 return,不仅会覆盖 try 和 catch 内的返回值,还会掩盖 try 和 catch 内的异常,就像异常没有发生一样,应该避免在 finally 中使用 return 语句或者抛出异常
try {
  // 可能抛出异常
} catch (Exception e){
  // 捕获异常
} finally {
  // 不管有无异常都执行
}

try-with-resources

对于一些使用资源的场景,比如文件和数据库连接,典型的使用流程是首先打开资源,最后在 finally 语句中关闭资源。

Java 7 开始支持一种新的语法 try-with-resources,针对实现了 java.lang.AutoCloseable 接口的对象。

资源的声明和初始化放在 try 语句内,不用再调用 finally,在语句执行完 try 语句后,会自动调用资源的 close() 方法。

try-with-resources 语句中可以声明多个资源,使用分号 ; 分隔各个资源。

Java 9 之前,资源必须声明和初始化在try语句块内,Java 9 去除了这个限制,资源可以在 try 语句外被声明和初始化,但必须是 final 的或者是事实上 final 的(即虽然没有声明为 final 但也没有被重新赋值)。

try(AutoCloseable autoClose = new FileInputStream("hello")){
  //
}

异常处理的目标

异常大概可以分为三种来源:用户、程序员、第三方。

用户是指用户的输入有问题;程序员是指编程错误;第三方泛指其他情况,如 I/O 错误、网络、数据库、第三方服务等。

处理的目标可以分为恢复和报告。恢复是指通过程序自动解决问题。报告的最终对象可能是用户,即程序使用者,也可能是系统运维人员或程序员。报告的目的也是为了恢复,但这个恢复经常需要人的参与。

对用户,如果用户输入不对,可以提示用户具体哪里输入不对,如果是编程错误,可以提示用户系统错误、建议联系客服,如果是第三方连接问题,可以提示用户稍后重试。

异常处理的一般逻辑

如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。

如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为 cause 重新抛出一个异常。

总有一层代码需要为异常负责,可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是主程序。如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值