005_异常处理

所谓异常,指的就是你运行良好时候的代码,突然发生了错误。我们给这个意料之外的东西取个名叫异常。而对应的异常处理机制就是当正常的程序碰到了异常之后,我们的代码需要识别出这种问题,并且提供应对此问题的手段。

异常的基本认识

异常是在执行某个方法时引发的,而方法又是方法调用产生的,方法调用将会形成调用栈。而只要一个方法发生了异常,那么他的所有的调用者都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。异常最先发生的地方,叫做异常抛出点。例如执行下列代码之后:

public class Exception001 {
    public static void main(String[] args) {
        f1();
    }

    public static void f1() {
        int num1 = 1;
        int num2 = 0;
        int result = f2(num1, num2);
        System.out.println("result:" + result);
    }

    public static int f2(int num1, int num2) {
        return num1 / num2;
    }
}

得到的报错是:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
可以清晰的看到,JVM基本上将错误信息都完整暴露出来了,这对我们排查问题有很大的帮助。

异常的抽象分类

我们针对异常进行了抽象,在代码层次上,产生这样的结构:
image.png
从上图上看,可以知道Throwable 是所有异常类型的基类,Throwable 有两种异常子类,分别是 Error 与 Exception,他们分别代表了两种类型的错误。

Error 描述了 JAVA 程序运行时系统的内部错误,通常比较严重,除了通知用户和尽力使应用程序安全地终止之外,无能为力,应用程序不应该尝试去处理这种异常。通常为一些虚拟机异常,如 StackOverflowError 等。

Exception 类型下面又分为两种类型,一种是派生自 RuntimeException,这种异常通常为程序运行时错误导致的异常;剩下的所有实现表达的是代码没有问题,更多的是和当前运行环境有关。

这种分类就导出了另一个概念:受检异常和非受检异常。非受检异常指的是Error 和 RuntimeException 以及他们的子类。这些错误不会在编译时被检测,不要求在程序处理这些异常。与之相对的受检异常指的是要求程序员必须对异常做预处理,要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。
image.png
如图,f1抛出一个非受检异常,编译通过;f2主动抛出一个受检异常之后,拒绝编译。

异常的处理方式

try-catch-finally捕捉异常

异常发生,代码是有责任去处理异常的,最为标准的处理方式是使用 try-catch-finally 语法。

public void f(){
  try{
     // 可能发生异常的代码
  } catch(Exception exception){
      // 异常处理的代码
  } finally{
      // 都会被指定的代码
  }
  // other code
}

这个语法的基本逻辑是:一个try至少要有一个catch块,否则, 至少要有1个finally块。在try的语句块儿内放可能发生异常的代码,如果执行完try且不发生异常,则接着去执行finally块(如果有的话),然后去执行finally后面的代码,如果发生异常,则尝试去匹配catch块。

每一个catch块用于捕获并处理一个特定的异常,或者这个异常类型的子类,也可以将多个异常声明在一个catch中。catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。如果try中没有发生异常,则所有的catch块将被忽略。

finally块是可选的,无论异常是否发生,异常是否匹配被处理,finally都会执行。但是finally不是用来处理异常的,主要做一些清理工作,如流的关闭,数据库连接的关闭等。 调用下面这个方法时,读取文件时若发生异常,代码会进入 catch 代码块,之后进入 finally 代码块;若读取文件时未发生异常,则会跳过 catch 代码块直接进入 finally 代码块。

private static void readFile(String filePath) throws MyException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(file));
        while((result = reader.readLine())!=null) {
            System.out.println(result);
        }
    } catch (IOException e) {
        System.out.println("readFile method catch block.");
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    } finally {
        System.out.println("readFile method finally block.");
        if (null != reader) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这里需要注意以下几点:

  1. try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
  2. 异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。

捕获多个异常

在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理

private static void readFile(String filePath) {
    try {
        // code
    } catch (FileNotFoundException e) {
        // handle FileNotFoundException
    } catch (IOException e){
        // handle IOException
    }
}

同一个 catch 也可以捕获多种类型异常,用 | 隔开

private static void readFile(String filePath) {
    try {
        // code
    } catch (FileNotFoundException | UnknownHostException e) {
        // handle FileNotFoundException or UnknownHostException
    } catch (IOException e){
        // handle IOException
    }
}

这个时候需要注意,如果多个异常的类类型是父子继承关系的,需要将子类放在前面,而不是将父类放在前面,例如:

private static void readFile(String filePath) {
    try {
        // code
    } catch (IOException e) {
        // handle IOException 
    } catch (FileNotFoundException e){
        // handle FileNotFoundException
    }
}

分析上面的代码,如果发生了FileNotFoundException,那么捕捉机制按照异常定义顺序进行捕捉,IOException异常捕捉块儿将会进行处理,那么永远不会执行到FileNotFoundException的异常处理块儿。在编译的过程中将识别此问题,拒绝编译。

自定义异常

如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常。如果要自定义非检查异常,则扩展自RuntimeException。按照国际惯例,自定义的异常应该总是包含如下的构造函数:

  • 一个无参构造函数
  • 一个带有String参数的构造函数,并传递给父类的构造函数。
  • 一个带有String参数和Throwable参数,并都传递给父类构造函数
  • 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
public class IOException extends Exception{
    static final long serialVersionUID = 7818375828146090155L;

    public IOException(){
        super();
    }

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

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

    
    public IOException(Throwable cause){
        super(cause);
    }
}

封装异常再抛出

有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。

private static void readFile(String filePath) throws MyException {    
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    }
}

异常链

在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。

异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。

public class Throwable implements Serializable {
    private Throwable cause = this;
   
    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
     public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
    
    //........
}

finally块与return

finally与return语句放在一起会出现一些不可思议的情形,我们先将这种规矩写下来。

1)若 catch 代码块中包含 return 语句,finally 子句依然会执行。

public class Exception002 {

    public static void main(String[] args){
        int re = bar();
        System.out.println(re);
    }
    private static int bar() {
        try{
            System.out.println("处理---");
            return 5;
        } finally{
            System.out.println("finally");
        }
    }
}

怎么理解这个现象呢?我们知道每个帧栈都会有一块儿区域是写入return值的,字节码上先写入,后返回,在这个中间会卡上finally的逻辑。因此最后的输出是:

处理---
finally
5

2)若 finally 中也包含 return 语句,finally 中的 return 会覆盖前面的 return.

public class Exception003 {

    public static void main(String[] args){
        int re = bar();
        System.out.println(re);
    }
    private static int bar() {
        try{
            int i = 1/0;
        } catch (Exception e){
            return 1;
        }
        finally{
            return 2;
        }
    }
}

return语句将会把数据写入到某个区域,这个区域在特定情况下支持重复写入,那么最终的值由最后的语句来决定,因此上面代码的数据是2,而不是1;

3)若 finally 中也包含 return 语句,finally中的return会抑制前面try或者catch块中的异常

public class Exception004 {

    public static void main(String[] args){
        int re = bar();
        System.out.println(re);
    }
    private static int bar() {
        try{
            int i = 1/0;
        } catch (Exception e){
            throw new Exception("我将被忽略,因为下面的finally中使用了return");
        }
        finally{
            return 2;
        }
    }
}

在这种情况下异常会被忽略,finally继续返回2的值。

4)若 finally 中也抛出异常,finally中的异常会覆盖前面try或者catch中的异常

public class Exception005 {

    public static void main(String[] args){
        int re = bar();
        System.out.println(re);
    }
    private static int bar() {
        try{
            int i = 1/0;
        } catch (Exception e){
            throw new RuntimeException("我将被忽略,因为下面的finally抛出了新异常");
        }
        finally{
            throw new RuntimeException("finally的错误");
        }
    }
}

获得到的输出是:

Exception in thread "main" java.lang.RuntimeException: finally的错误
	at com.zifang.ex.bust.chapter5.Exception005.bar(Exception005.java:16)
	at com.zifang.ex.bust.chapter5.Exception005.main(Exception005.java:6)

Process finished with exit code 1

上面的几个例子乍一看不好理解,平时也最好不要写成上面演示代码中的样子。尽可能不要在fianlly中使用return,异常,而是只专注于释放资源。

throws直接抛出异常

例如下列代码,由于这段代码存在受检异常,是没有办法编译过去的:
image.png
这个时候,我们就可以在方法内使用try-catch方法处理受检异常:
image.png
但是这个时候还有一种选择是,当前方法不想处理受检异常或者不知道如何处理异常的时候,可以将异常抛出,由调用者处理方法抛出的异常。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。如果需要抛出多种可能错误,就把异常类型跟在throws后面,用逗号分开既可。
image.png
这里还会存在一个问题,那就是当子类重写父类的带有 throws声明的函数时应该怎么写的问题。这就涉及到多态的问题了,逻辑上子类方法上throws声明的异常必须在父类异常的可控范围内,也就是说用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。

class Father{
    public void start() throws IOException{
        throw new IOException();
    }
}

class Son extends Father {
    public void start() throws Exception {
        throw new SQLException();
    }
}

上面的代码中,子类的Exception是父类IOException的父类,因此编译错误,正确写法应该是:

class Father{
    public void start() throws Exception{
        throw new IOException();
    }
}

class Son extends Father {
    public void start() throws IOException {
        throw new SQLException();
    }
}

throw主动抛出异常

我们之前接触的是代码被动等待异常发生,然后将异常捕捉抛出。如果代码本身就已经感知到错误的发生时是可以主动抛出特定异常的,至于异常本身如何处理就交给方法的调用者。我们利用throw关键字即可。throw语句的后面必须是一个异常对象,throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和运行时自动形成的异常抛出点没有任何差别。

public void f(String str) {
	if(i  == null) {
		throw new IllegalArgumentException("Str为空");
	}
}

try-with-resource语法糖

上面例子中,finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。

private  static void tryWithResourceTest(){
    try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
        // code
    } catch (IOException e){
        // handle exception
    }
}

try 代码块退出时,将会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。

常见的异常类型

我们jdk内有一些已经定义好的类,他们有强烈的父子继承关系,在我们开发过程中可以利用这些类,如果不能满足需求则继承最精确匹配的类成为自定义异常。因此,知道jvm下的既有异常类与他们的继承链关系是有必要的。以下是jvm内的既有异常及其父子关系。

-  Error(错误) 
  - ThreadDeath(当调用Thread类的stop方法时抛出该错误,用于指示线程结束)
  - AssertionError(断言错误)
  - VitulMachineError(虚拟机错误) 
    - UnknownError(未知错误)
    - InternalError(虚拟机内部错误)
    - StackOverFlowError(堆栈溢出)
    - OutOfMemoryError(内存不足)
    - AWTError
    - LinkageError (链接错误) 
      - UnsatisfiedLinkError(未满足的链接错误:虚拟机未找到某个类的声明为native方法的本机语言定义时抛出)
      - VerifyError(验证错误)
      - ClassCircularityError(类循环依赖错误)
      - ClassFormatError(类格式错误) 
        - UnsupportedClassVersionError(不支持的类版本错误)
        - GenericSignatureFormatError
      - ExceptionInInitializerError(初始化程序错误)
      - IncompatibleClassChangeError(不兼容的类变化错误) 
        - AbstractMethodError(试图调用抽象方法时抛出)
        - InstantiationError(实例化错误)
        - IllegalAccessError(违法访问错误)
        - NoSuchFieldError
        - NoSuchMethodError
      - NoClassDefFoundError (没有找到类定义错误)
-  Exception(异常) 
  -  CloneNotSupportedException(不支持克隆异常) 
  -  InterruptedException(被中止异常) 
  -  SQLException(操作数据库异常) 
  -  ReflectiveOperationException 
    - ClassNotFoundException
    - NoSuchMethodException(方法未找到异常)
    - NoSuchFieldException
    - IllegalAccessException(违法的访问异常)
    - InstantiationException(实例化异常)
    - InvocationTargetException
  -  IOException(输入输出异常) 
    - EOFException(文件已结束异常)
    - FileNotFoundException(文件未找到异常)
  -  ReflectiveOperationException 
    - ClassNotFoundException(找不到类)
    - InstantiationException(实例化异常)
    - NoSuchFieldException(属性不存在异常)
    - NoSuchMethodException(方法不存在异常)
  -  RuntimeException(运行时异常) 
    -  IllegalMonitorStateException(违法的监控状态异常) 
    -  IllegalStateException(违法的状态异常) 
    -  EnumConstantNotPresentException(枚举常量不存在异常) 
    -  TypeNotPresentException(类型不存在异常) 
    -  ClassCastException(类型转换错误) 
    -  ArithmeticException(算术运算异常) 
    -  ArrayStoreException(向数组中存放与声明类型不兼容对象异常) 
    -  IndexOutOfBoundsException(下标越界异常) 
    -  NegativeArraySizeException(创建一个大小为负数的数组错误异常) 
    -  SecurityException(安全异常) 
    -  UnsupportedOperationException(不支持的操作异常) 
    -  NegativeArraySizeException(数组负下标异常) 
    -  NullPointException(空指针) 
    -  ArithmeticException(算术条件异常) 
    -  IndexOutOfBoundsException(索引越界) 
      - StringIndexOutOfBoundsException(字符串越界)
      - ArrayIndexOutOfBoundsException(数组越界)
    -  ClassCastException(类转换异常) 
    -  ArrayStoreException(数据存储异常,操作数组时类型不一致) 
    -  IllegalArgumentException(传递非法参数异常) 
      - NumberFormatException(数字格式异常)
      - IllegalThreadStateException(违法的线程状态异常)

JVM的类加载机制的委托行机制,决定了类加载器只加载一次,子类加载器不会再加载父类加载器已经加载过的类。所有在一些特定条件下就会出现编译时可以加载到类,运行时不可以加载到类,这时候就会出现java.lang.NoClassDefFoundError异常

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值