Java:异常/Exception、try...catch...finally(2021.5.5)

1、Java异常

Java内置了一套异常处理机制,总是使用异常来表示错误。

异常也是一种class,它本身带有类型信息。异常可以在任何地方抛出,只需要在上层捕获,这样就和方法调用分离了。

抛出错误的语句块放在try后,捕获则是catch之后:

复制代码
try {
    //可能出错的语句
}
catch(Exception1 e){
    //捕获到1类错误时的后续处理
}
catch(Exception2 e){
    //捕获到2类错误时的后续处理
}
....
复制代码

异常class的继承关系为:

 

从继承关系可知,Throwable是异常体系的根,它继承自ObjectThrowable有两个体系:ErrorExceptionError表示严重错误,程序对此一般无能为力,例如:

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

Exception异常,它可以被捕获并处理。某些异常是应用程序逻辑的一部分,应该捕获并处理,例如:

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:网络异常

还有一些异常是程序逻辑错误导致的,应该修复程序本身,例如:

  • NullPointerException:对某个null对象调用方法或访问属性
  • IndexOutOfBoundsException:数组索引越界

根据上文继承关系,Exception又分为两类

  • RuntimeException及其子类
  • RuntimeException,(包括IOException、ReflectiveOperationException等等)

 

Java规定了:

  • 必须捕获的异常:包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常被称为Checked Exception;
  • 不需捕获的异常,包括Error及其子类RuntimeException及其子类。

捕获异常

捕获异常使用try...catch...语句,把可能发生异常的代码放在try {...}中,然后使用catch捕获对应的Exception及其子类

有些异常是必须捕获的,不论某个语句是否运行时真的会报错,这就是上一章最后所说的必须捕获的异常,可能产生这类异常的语句必须放在try语句块下

比如:字符串转byte的方法为s.getBytes("编码方式"),该方法可能抛出UnsupportedEncodingException异常,该异常是Checked Exception,必须被捕获。这是因为在定义String.getBytes(String)的方法时,指定了throws xxx表示该方法可能抛出的异常类型(定义方法时,主动throws异常的方法,必须放在try语句块中,用exception接收该异常):

public byte[] getByte(String charsetName) throws UnsupportedEncodingException{
    ...
}

调用方在调用的时候,必须强制捕获(也就是说,主动用throws抛出异常的方法的语句必须在try语句块中),否则编译器会报错。

 

如果不明确指明throw UnsupportedEncodingException,还有一种方法是把这个语句另外写在某个函数中,在函数定义时throws表示该方法可能会抛出UnsupportedEncodingException,就可以让该方法通过编译器检查。

所以,能正确通过编译的两种方式为:try...catch...;②写在函数中,并在函数定义时throws 对应的Exception

对于后者,调用函数时,依然要用try...catch...进行捕获。也就是说,这类错误,使用时必须捕获(编译时可以不用

复制代码
//
try{
    return s.getBytes("GBK");
}catch(UnsupportedEncodingException e){
    System.out.println(e);
    return s.getBytes();
}

//
static byte[] toGBK(String s) throws UnsupportedEncodingException{
    return s.getBytes("GBK");
}
try{
  byte[] bs= toGBK("中文");
}catch(UnsupportedEncodingException e){
  System.out.println(e);
}
复制代码

由此可见,只要是方法声明的Checked Exception不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在main()方法中捕获不会出现漏写try的情况。这是由编译器保证的,main()方法也是最后捕获Exception的机会

 

如果是测试代码,上边的写法就略显麻烦。如果不想写try代码,可以直接把main()方法定义为throws Exception

public static void main(String[] args) throws Exception{
    byte[] bs = toGBK("中文");
}

因为main()方法声明了可能抛出Exception,也就声明了可能抛出的所有Exception,因此在内部也就无需捕获了。代价就是,一旦发生异常,程序就会立刻退出

 

有些人喜欢在最内层的函数(如上文的toGBK())中“消化”异常(但是什么都不做,就是catch后跟一个空代码块)

static byte[] toGBK(String s){
    try{
        return s.getBytes("GBK");
    } catch (UnsupportedEncodingException e){
        //什么都不干
    }
    return null;
}

这种捕获后不处理的方式是非常不好的,即使真的什么也做不了,也要先把异常记录下来:

static byte[] toGBK(String s) {
    try {
        return s.getBytes("GBK");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    return null;

所有异常都可以调用printStackTrace()(实例方法,所有Exception实例,都可以用e.xxx()的写法调用)方法打印异常栈,这是一个简单有用的快速打印异常的方法。

小结

  • Java使用异常Exception表示错误,并通过try...catch捕获异常;
  • 异常是class,继承自Throwable
  • Error无需捕获的严重错误,Exception应该捕获可处理错误,其中RuntimeException无需强制捕获,非RuntimeException异常也叫Checked Exception,需要强制捕获,或者在其所在函数定义时通过throws声明;
  • 不要捕获了错误但不做任何处理,常用方法是e.printStackTrace()

 

2、抛出异常

异常的传播

当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try...catch被捕获为止。

通过e.printStackTrace()方法可以打印出异常栈——产生异常的语句在各个函数中的位置。该方法对于调试错误很有用。

抛出异常

这里的异常抛出是指人为用throw手动抛出,抛出的异常必须被上文捕获。

抛出异常通常分为两步:

  1. 创建某Exception的实例
  2. throw抛出

例子:

void process2(String s){
    if(!s){
        NullPointerException e = new NullPointerException();
        throw e;
    }
}

实际上,绝大部分主动抛出异常的代码都会合并写成一行:

void process2(String s){
    if(!s){
        throw new NullPointerException();
    }
}

如果一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就相当于把抛出的异常类型“转换”了:

复制代码
    void process1(){
        try{
            process2();
        }catch(NullPointerException){
            throw new IllegalArgumentException();
        }
    }
    void process2(){
        if(s==null){
            throw new NullPointerException();
        }
    }
复制代码

当process2()抛出NullPointerException之后,被process1()捕获,然后抛出IllegalArgumentException()

如果在main()中捕获IllegalArgumentException,让我们看看打印的异常栈:

复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        try {
            process1();
        }catch(Exception e){
            e.printStackTrace();
    }
}

    static void process1() {
        try {
            process2();
        } catch (NullPointerException e) {
            throw new IllegalArgumentException();
        }
    }

    static void process2() {
            throw new NullPointerException();
    }
}
复制代码

打印出的异常栈为:

java.lang.IllegalArgumentException
    at pack1.Main.process1(Main.java:18)
    at pack1.Main.main(Main.java:8)

这说明新的异常已经丢失了原始的异常信息,我们已经看不到原始异常NullPointerException的信息了。

为了能够追踪到完整的异常栈,在构造异常的时候,把原始的Exception实例传进去,新的Exception就可以持有原始的Exception信息。对上述代码改进如下:

    static void process1() {
        try {
            process2();
        } catch (NullPointerException e) {
            throw new IllegalArgumentException(e);
        }
    }

运行上述代码,打印出来的异常栈为:

java.lang.IllegalArgumentException: java.lang.NullPointerException
    at pack1.Main.process1(Main.java:15)
    at pack1.Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
    at pack1.Main.process2(Main.java:20)
    at pack1.Main.process1(Main.java:13)
    ... 1 more

注意到Caused by: Xxx,说明捕获的IllegalArgumentException并不是造成问题的根源,根源在于NullPointerException,是在Main.process2()方法抛出的。

在代码中获取原始异常可以使用Throwable.getCause()方法。如果返回null,说明已经是“根异常”了。

注意:捕获到异常并再次抛出时,一定要留住原始异常,否则很难定位第一案发现场。

如果我们在try或者catch语句块中用throw抛出异常,并加以处理后,finally语句仍会执行。

执行顺序是:JVM先执行finally,然后再抛出异常

异常屏蔽

如果在执行finally语句时抛出异常,那么,catch语句的异常是否还能继续抛出呢?例如:

复制代码
public class Main {
    public static void main(String[] args) {
       try{
           Integer.parseInt("abc");
       }catch(Exception e){
           System.out.println("Catched");
           throw new RuntimeException(e);
       }finally{
           System.out.println("finally");
           throw new IllegalArgumentException();
       }
}
复制代码

执行上述代码,发现异常信息如下:

Catched
finally
Exception in thread "main" java.lang.IllegalArgumentException
    at pack1.Main.main(Main.java:11)

这说明finally抛出异常后,原来的catch准备抛出的异常就消失了,因为只能抛出一个异常没有被抛出的异常称为“被屏蔽”的异常(Suppressed Exception)

在极少数的情况下,我们需要获知所有的异常。如何保存所有的异常信息呢?方法是先用origin变量保存原始异常,然后调用Throwable.addSuppressed(),把原始异常添加进来,最后再在finally抛出:

复制代码
public class Main {
    public static void main(String[] args) throws Exception{
        Exception origin = null;
        try{
            System.out.println(Integer.parseInt("abc"));
        }catch (Exception e){
            origin =e;
            throw e;
        }finally{
            Exception e=new IllegalArgumentException();
            if(origin!=null){
                e.addSuppressed(origin);
            }
            throw e;
        }
    }
}
复制代码

当catch和finally都抛出了异常时,虽然catch的异常被屏蔽了,但是finally抛出的异常仍然包含它:

Exception in thread "main" java.lang.IllegalArgumentException
    at pack1.Main.main(Main.java:11)
    Suppressed: java.lang.NumberFormatException: For input string: "abc"
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
        at java.base/java.lang.Integer.parseInt(Integer.java:660)
        at java.base/java.lang.Integer.parseInt(Integer.java:778)
        at pack1.Main.main(Main.java:6)

通过Throwable.getSuppressed()可以获取所有的Suppressed Exception。绝大多数情况下,在finally中不要抛出异常,因此,我们通常不需要关心Suppressed Exception

 

在提问Java相关问题时,一定要贴出异常栈。

小结

  • 调用printStackTrace()可以打印异常传播栈,对于调试很有用;
  • 通常不要在finally中抛出异常。如果在finally中抛出异常,应该将原始异常加入到现有一场中。调用方可以通过Throwable.getSuppressed()获取所有添加的Suppressed Exception

总结

  1. 异常可以在任何地方抛出,在上层捕获。整个语句是:
    复制代码
    try {
        //可能出错的语句
    }
    catch(Exception1 e){
        //捕获到1类错误时的后续处理
    }
    catch(Exception2 e){
        //捕获到2类错误时的后续处理
    }
    ....
    finally {
      //最终处理
    }
    复制代码
  2. 有两种类型的错误:ErrorExceptionError严重错误,程序一般对此无能为力,Exception运行时错误,它可以被捕获并处理。
  3. Exception包括RuntimeException非RuntimeException
  4. Java必须捕获的异常包括Exception及其子类,但不包括RuntimeException及其子类,这种异常被称为Checked Exception不需要捕获的异常,包括ErrorRuntimeError及其子类。
  5. 有些异常是必须捕获的,不论某个语句运行时是否真的会出错,这类语句必须放在try语句块之下。
  6. 对这种语句,能正确通过编译的两种为:①放在try...catch...中;②写在函数中,并在函数定义时定义throws不过后者在调用函数时,调用函数的语句仍然要放在try...catch...之中。
  7. 这类异常,使用时必须用try...catch...捕获,但是编译时可以不用;
  8. main方法是捕获Exception的最后机会。
  9. 如果不想写try代码,可以直接把main方法定义为throws Exception
    public static void main(String[] args) throws Exception{
        byte[] bs = toGBK("中文");
    }

     

  10. 不要捕获了异常而不做任何处理,常用方法是printStackTrace(),作用是打印异常栈
    try {
        ...
    }
    catch (xxxException e){
        e.printStackTrace();
    }

     

  11. 可以用throw关键字手动抛出异常,抛出的异常必须被上文捕获,常用的抛出异常的代码是:
    void process2(String s){
        if(!s){
            throw new NullPointerException();
        }
    }

    为了能够追踪到完整的异常栈,在构造异常的时候,需要把原始Exception实例传入进去,新的Exception就可以持有原始的Exception。方法是:

            try {
                ...;
            } catch (NullPointerException e) {
                throw new IllegalArgumentException(e);
            }

     

  12. try...catch...finally的执行顺序是①try异常;②catch捕获并处理;③finally最终一定会执行
posted @   ShineLe  阅读(281)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示