全面解析Java异常机制

目录

异常:

异常体系:

异常的产生过程:

抛出异常:

Objects类的非空判断:

如何对异常进行处理:

方式一(throws):

方式二(捕获异常):

常用的异常处理方式:

多个异常分别处理:

多个异常一次捕获,多次处理:

多个异常一次捕获,一次处理:

finally代码块:

注意事项:

关于异常:

建议:

注:

Throwable类的三个处理异常的方法:

自定义异常类:

包装异常:

try-with-resources语句:

参考文献:


异常:

指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

异常指的并不是语法错误语法错了编译不通过,而是指运行阶段出现的错误,如空指针异常,语法错误不会产生字节码文件根本就不能运行。

异常体系:

异常机制其实是帮助我们找到程序中的问题,他们都有一个共同的父类(java.lang.Throwable),其下有两个子类:java.lang.Error (工程师不能处理,只能尽量避免)与 java.lang.Exception (由于使用不当导致,可以避免的),平常所说的异常指的是 java.lang.Exception 。

java.lang.Exception 下也有一个特殊的子类:RuntimeException ,称为运行期异常。其余子类均为编译期异常。

异常的产生过程:

抛出异常:

使用throw关键字抛出异常。

throw关键字在指定的方法中抛出指定的异常使用格式:throw new xxxException("异常产生的原因");

注意:

1、throw关键字必须写在方法的内部。

2、throw关键字后边new的对象必须是Exception或者Exception的子类对象。

3、throw关键字抛出指定的异常对象,我们就必须处理这个异常对象。

如果throw关键字后边创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理默认交给JVM处理(打印异常对象,中断程序。

但如果throw关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常,要么throws,要么try...catch。

new对象时实参可以传一个字符串,将打印到控制台上。

public static void getNumber(int[] arry ,int index){
    if (index < 0 || index > arry.length-1){
        // throw new IndexOutOfBoundsException("数组下标越界啦");
        String T = "数组下标越界啦,下标为:" + index;
        throw new IndexOutOfBoundsException(T);    // 也可输出提前定义的字符串
    }
}

数组下标越界异常(IndexOutOfBoundsException)是一个运行期异常,可不处理默认交给JVM处理。

Objects类的非空判断:

Objects类由一些静态的实用方法组成,这些方法是 null-save(空指针安全的)或 null-tolerant(容忍空指针的),那么在它的源码中,都对对象为null的值进行了抛出异常操作。

如判断对象是否为NULL的方法:

public static <T> T requireNonNull(T obj) {
    if (obj == null)
          throw new NullPointerException();
    return obj;
}

如何对异常进行处理:

"运行期异常"我们可以不处理,但如果抛出的异常是"编译期异常",则必须手动处理。

方式一(throws):

方法内有可能抛出异常时(可能在if语句中,不会执行,但也要有throws语句),我们可以使用throws关键字把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理),如果一直throws到main方法后,main方法继续throws,则会交给jvm处理最终交给JVM处理(中断处理)。

格式:

在方法声明时使用:

修饰符 返回值类型方法名(参数列表) throws AAAExcepiton,BBBExcepiton...{
    throw new AAAExcepiton("产生原因"); 
    throw new BBBExcepiton("产生原因");
}

注意:

1、throws关键字必须写在方法声明处。

2、throws关键字后边声明的异常必须是Exception或者是Exception的子类。

3、throws后会终止本方法的继续执行,而 try...catch 不会。

3、调用了一个声明抛出异常的方法,我们就必须的外理声明的异常。要么继续使用throws声明抛出,又交给方法的调用者处理,要么try...catch自己处理异常。

4、方法内部如果抽出了多个异常对象,那么throws后边必须也声明多个异常。但如果多个异常有共同的父类异常,那么直接throws父类异常即可。

public static void kk(int index) throws Exception{    // 抛出他们的共同父类异常
    if (index == 1)
        throw new LambdaConversionException();
    if (index == 2)
        throw new DataFormatException();
}

方式二(捕获异常):

抛出异常有缺陷,一旦throws异常,该方法将终止运行。但如果捕获异常(try...catch),那么该方法依旧可以继续执行。

格式:

try{
    可能产生异常的代码
}catch( 定义一个异常类型的变量,用于接收try中抛出的异常对象 ){
    异常的处理逻辑(产生异常对象之后,怎么处理异常对象)。
    一般在工作中,会把异常的信息记录到一个日志中。
}catch( 异常类名 变量名 ){
    ......
}

注意:

1、try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象。

2、如果try中产生了异常,那么就会执行catch中的异常处理逻辑,执行完毕catch中的处理逻辑后,继续执行try...catch之后的代码。如果try中没有产生异常,那么就不会执行catch中的异常处理逻辑,而是继续执行try...catch之后的代码。

3、如果捕获了异常,那么调用该可能产生异常被调方法时调用方法不用throws声明。

4、如果抛出了一个catch语句没有声明的异常类型,那么该方法会立即退出。

当两个异常类型之间不存在互为子父类关系时,如果需要的话,我们可以使用catch语句合并:

如: 

try {
    ......    
} catch (RegisterException | IndexOutOfBoundsException e) {
    e.printStackTrace();
}


 

常用的异常处理方式:

多个异常我们又怎么处理呢?

1、多个异常分别处理。

2、多个异常一次捕获,多次处理。

3、多个异常一次捕获一次处理。

一般我们使用的是一次捕获多次处理的方式。

多个异常分别处理:

try {
    String[] a = new String[10];
    System.out.println(a[0].equals(a[1]));
}catch (NullPointerException e){
    System.out.println("空指针异常");
}

try {
    int[] b = new int[3];
    System.out.println(b[3]);
}catch (IndexOutOfBoundsException e){
    System.out.println("数组下标越界异常");
}

System.out.println("后续代码");

输出:

多个异常一次捕获,多次处理:

注意:当一个异常被捕获后,则整个try...catch语句会结束,try中的后续代码不会执行。

try {
    String[] a = new String[10];
    System.out.println(a[0].equals(a[1]));
    int[] b = new int[3];
    System.out.println(b[3]);
}catch (NullPointerException e){
    System.out.println("空指针异常");
}catch (IndexOutOfBoundsException e){
    System.out.println("数组下标越界异常");
}

System.out.println("后续代码");

输出:

注意:catch里面定义的异常变量如果有子父类关系,则子类的异常变量必须写在上边。否则报错。(如果子类的异常变量在下边,但是上边的父类异常变量也可以接收子类异常对象(向上转型),程序目的不明确)

try {
    int[] a = new int[3];
    System.out.println(a[3]);
}catch (IndexOutOfBoundsException e){    // 数组下标越界异常是下标越界异常的子类
    System.out.println("下标越界异常");
}catch (ArrayIndexOutOfBoundsException e){    // 报错,已捕捉到异常'java.lang.ArrayIndexOutOfBoundsException'
    System.out.println("数组下标越界异常");
}

多个异常一次捕获,一次处理:

如果catch里面定义的异常变量有子父类关系,可只定义父类异常的变量来接收父类与子类可能产生的异常对象。

当然,如果catch里面定义的异常变量同为某个异常的子类,那么也可定义他们的公共父类异常的变量来接收两个子类可能产生的异常对象。

注意:与第二种情况同理,当一个异常被捕获后,则整个try...catch语句会结束,try中的后续代码不会执行。

try {
    String[] a = new String[10];
    System.out.println(a[0].equals(a[1]));
    int[] b = new int[3];
    System.out.println(b[3]);
}catch (Exception e){
    System.out.println("异常");
}

System.out.println("后续代码");

finally代码块:

1、finally代码块不能单独使用,而是要配合try一起使用。

2、当try中的异常被catch捕获后,其他catch不会执行.。但如果有finally语句块,无论是否出现异常都会在try...catch语句执行完后执行finally语句块。

3、finally一般用于资源释放(资源回收),无论程序是否出现异常,最后都要资源释放。(IO流)

try{

}catch(){

}catch(){
    
}finally{

}

注意事项:

关于异常:

1、子类继承父类,子类覆盖父类的方法时,子类方法抛出的异常不能更多。可以是父类异常的子类,也可以是父类异常的子集,但不能抛出父类异常的父类。

如:父类抛出了CloneNotSupportedException和NullPointerException,子类可以抛出CloneNotSupportedException和NullPointerException,也可以只抛出CloneNotSupportedException,也可以只抛出ServerCloneException(ServerCloneException是CloneNotSupportedException的子类),但不能抛出Exception。

2、如果父类方法没有抛出异常,那么子类覆盖父类方法时也不能抛出异常,只能捕获处理。

3、即使try...catch块中有return语句,finally代码块也会被执行。

4、try...catch 中如果 return 的是基本类型的,那么finally里对变量的改动将不起效果,如果 return 的是引用类型的,改动将可以起效果。

5、finally 里的 return 语句会把 try...catch 块里的 return 语句效果给覆盖掉。(在 try...catch...finally 中 return 语句并不一定都是函数的出口,执行 return 时,只是把 return 后面的值复制了一份到返回值变量里去,到程序结束后把最后一个赋值给返回值的值返回。)

6、System.exit(0) 退出JVM会导致 finally 语句不执行。

public class ExceptionTest12 {
    public static void main(String[] args) {
        try {
            System.out.println("try...");
            // 退出JVM
            System.exit(0); // 退出JVM之后,finally语句中的代码就不执行了!
        } finally {
            System.out.println("finally...");
        }
    }
}

建议:

1、最好把return放到方法尾而不要在try cath 里return

2、不要在try catch块和finally块里都包含return

3、如果在try catch块里return, 则不要在finally块里操作被return的变量

4、finally语句块内要用于清理资源,最好不要把改变控制流的语句放到finally语句块中。(return、throw、break、continue)

注:

方法覆盖只是针对于“实例方法”。

首先,static方法是可以继承的,父类中的static方法,在子类中也可以调用。

其次,子类中允许定义和父类static方法 同名、参数列表一致 并且 返回值类型相同 的static方法。

但子类对象(是子类对象,不是在子类内)在调用时,调用的是子类的静态方法。

父类对象(是父类对象,不是在父类内)调用时,调用的是父类的静态方法。

Throwable类的三个处理异常的方法:

  • public void printStackTrace();    打印异常的详细信息。包含了异常的类型,异常的原因,还包括异常出现的位置。JVM打印对象时默认调用此方法。
  • public String toString();    获取异常的类型和异常的原因。(直接输出异常对象默认调用该方法)
  • public String getMessage();    获取发生异常的原因。

因为异常都是在Throwable类下,所以可以直接调用以上方法。

出现异常可以把异常的简单类名拷贝到API中去查。


 

自定义异常类:

java提供的异常类不够我们使用,如年龄不能为负等,这个时候可以直接定义一些异常类。

格式:

public class XXXXException extends Exception /*RuntimeException*/ {
    添加一个空参数的构造方法。
    添加一个带异常信息的构造方法。
}

如果想自定义一个编译期异常类:自定义类并继承于java.lang.Exception,如果方法内部抛出了编译期异常,就必须处理整个异常,要么throws,要么try...catch。

如果想自定义一个运行期的异常类:自定义类并继承于java.lang.RuntimeException。如果方法内部抛出了运行期异常,可不处理这个异常,让虚拟机处理(中断处理)。

当然也可以继承其他的异常类,如IOException。

定义构造方法时,只需要调用父类的构造方法即可(在构造异常对象时调用的就是这些构造函数)(含参的构造方法可以有一个String类型的参数,用于new对象时实参传一个字符串打印到控制台上)(异常名称就是所定义异常类的类名)。

import java.util.Scanner;

class Demo{
    static String[] UserName = {"张三","李四","王五"};
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String newName = in.next();
        try {
            checkUserName(newName);
        } catch (RegisterException e) {
            e.printStackTrace();
            return;
        }
        System.out.println("注册成功");
    }

    public static void checkUserName(String newName) throws RegisterException{
        for (String n : UserName){
            if (newName.equals(n)){
                throw new RegisterException("对不起,该用户名已存在");
            }
        }
    }
}

class RegisterException extends Exception{
    public RegisterException(){
        super();
    }

    public RegisterException(String name){
        super(name);
    }
}

包装异常:

在一个项目中,越往底层,可能抛出的异常类型会用很多,如果你在上层想要处理这些异常,你就需要挨个的写很多catch语句块来捕捉各个具体的异常类型,这样是很麻烦的。如果我们对底层抛出的异常捕获后,抛出一个新的统一的父类异常,会避免这个问题。但是直接抛出一个新的异常,会让最原始的异常信息丢失,这样不利于排查问题。举个例子,在底层会出现一个A异常,然后在中间代码层捕获A异常,对上层抛出一个B异常。如果在中间代码层不对A进行包装,在上层代码捕捉到B异常后就不知道为什么会导致B异常的发生。所以我们可以使用initCause()这个方法来对异常来进行包装,包装以后我们就可以用getCause()方法获得原始的A异常。这对追查BUG是很有利的。

错误方法:

class A{
    try{
    ....
    }catch(AException a){
        throw new BException();
    }
}
...
class B{
    try{
        ...
    }catch(BException b){
        // 在A类里面没有对AException进行包装
        // 所以你无法知道是A类的哪个异常抛出的BException异常
    }
}

正确方法:

class A{
    try{
        ...
    } catch(AException a) {
        BException b = new BEexception();    // 创建父类异常对象
        b.initCause(a);    // 把子类异常对象包装到父类异常对象
        throw b;    // 抛出父类异常对象
    }
}
...
class B throws Throwable{ // getCause得到的异常是Throwable类型的(Error和Exception的父类)。
    try {
        ...
    } catch(BException b) {    // 捕获父类异常对象
        throw b.getCause();// 调用方法得到导致B异常的原始异常
        // 抛出查看错误
    }
}

try-with-resources语句:

在Java编程过程中,如果打开了外部资源(文件、数据库连接、网络连接等),我们必须在这些外部资源使用完毕后,手动关闭它们。因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制,如果我们不在编程时确保在正确的时机关闭外部资源,就会导致外部资源泄露,紧接着就会出现文件被异常占用,数据库连接过多导致连接池溢出等诸多很严重的问题。

为了确保外部资源一定被关闭,通常关闭代码被写入finally代码块中,又因为关闭资源时也可能抛出的异常,所有在java1.7以前,我们关闭资源的方式如下:

public class CloseTest {
    public static void main(String[] args){
 
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("file.txt");
            fileInputStream.read();
        } catch (IOException e) {
            e.printStackTrace();    // 第一处异常处理
        }finally {
            if (fileInputStream != null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();    // 第二处异常处理
                }
            }
        }
    }
}

try-catch-final语句异常处理有两种情况:

  1. try 块没有发生异常时,直接调用finally块,如果 close 发生异常,就处理。
  2. try 块发生异常,catch 块捕捉,进行第一处异常处理,然后调用 finally 块,如果 close 发生异常,就进行第二处异常处理。

从JDK1.7开始,java引入了 try-with-resources 声明,将 try-catch-finally 简化为 try-catch,这其实是一种语法糖,在编译时会进行转化为 try-catch-finally 语句。

新的声明包含三部分:try-with-resources 声明、try 块、catch 块。我们将把外部资源变量的声明或外部资源对象的引用的创建放在try关键字后面的括号中(try-with-resources 声明),存在多个语句时则使用分号断开(仅一个语句则无需写分号)。注意不是所有try中所需的变量都需要在这里面声明,只需声明外部的引用即可,并且由于在try-with-resources 声明中的变量都默认是final修饰的,所以必须在声明的时候就给赋上所需值。当这个try-catch代码块执行完毕后,Java会确保外部资源的close方法被调用。

它要求在 try-with-resources 声明中定义的变量所在的类实现了 AutoCloseable 接口,这样在系统可以自动调用它们的close方法,从而替代了finally中关闭资源的功能。

在 try-with-resources 结构中,异常处理也有两种情况(注意,不论 try 中是否有异常,都会首先自动执行 close 方法,然后才判断是否进入 catch 块,建议阅读后面的反编译代码):

  1. try 块没有发生异常时,自动调用 close 方法,如果发生异常,catch 块捕捉并处理异常。
  2. try 块发生异常,然后自动调用 close 方法,如果 close 也发生异常,catch 块只会捕捉 try 块抛出的异常,close 方法的异常会在catch 中被压制,随后 try-with-resources 会自动调用 Throwable.addSuppressed(Throwable exception) 方法把所有被压制的close方法的异常存储到一个Throwable类型的数组中,我们可以在catch块中,用 getSuppressed 方法来获取该数组。

我们可以通过自定义的 AutoCloseable 类来理解这个过程。

import java.io.IOException;

public class Main {

    public static void startTest() {
        try (MyAutoCloseA a = new MyAutoCloseA();
             MyAutoCloseB b = new MyAutoCloseB()) {
            a.test();   // 接收到异常抛出,所以try语句终止,运行catch语句
            b.test();   // 该代码不会执行

       // 不论try中是否有异常,都会首先自动执行close方法,然后再判断是否进入catch块
        } catch (Exception e) {
            System.out.println("Main: exception");
            System.out.println(e.getMessage());

            Throwable[] suppressed = e.getSuppressed(); // 调用方法获得一个存放所有被抑制的异常的Throwable类型的数组,并复制
            for (Throwable throwable : suppressed){
                System.out.println(throwable.getMessage()); // 打印所有被抑制的异常
            }
        }
    }

    public static void main(String[] args) throws Exception {
        startTest();
    }
}

class MyAutoCloseA implements AutoCloseable {

    public void test() throws IOException {
        System.out.println("MyAutoCloaseA: test()");
        throw new IOException("MyAutoCloaseA: test() IOException");
    }

    @Override
    public void close() throws Exception {
        System.out.println("MyAutoCloseA: on close()");
        throw new ClassNotFoundException("MyAutoCloaseA: close() ClassNotFoundException");
    }
}

class MyAutoCloseB implements AutoCloseable {

    public void test() throws IOException {
        System.out.println("MyAutoCloaseB: test()");
        throw new IOException("MyAutoCloaseB: test() IOException");
    }

    @Override
    public void close() throws Exception {
        System.out.println("MyAutoCloseB: on close()");
        throw new ClassNotFoundException("MyAutoCloaseB: close() ClassNotFoundException");
    }
}

输出:

 通过反编译class文件,可以看到实际的执行过程:

    public static void startTest() {
       try {
           MyAutoCloseA a = new MyAutoCloseA();
           Throwable var33 = null;

           try {
               MyAutoCloseB b = new MyAutoCloseB();
               Throwable var3 = null;

               try { // 我们定义的 try 块
                   a.test();
                   b.test();
               } catch (Throwable var28) { // try 块中抛出的异常
                   var3 = var28;
                   throw var28;
               } finally {
                   if (b != null) {
	                   // 如果 try 块中抛出异常,就将 close 中的异常(如果有)附加为压制异常
                       if (var3 != null) {
                           try {
                               b.close();
                           } catch (Throwable var27) {
                               var3.addSuppressed(var27);
                           }
                       } else { // 如果 try 块没有抛出异常,就直接关闭,可能会抛出关闭异常
                           b.close();
                       }
                   }

               }
           } catch (Throwable var30) {
               var33 = var30;
               throw var30;
           } finally {
               if (a != null) {
                   if (var33 != null) {
                       try {
                           a.close();
                       } catch (Throwable var26) {
                           var33.addSuppressed(var26);
                       }
                   } else {
                       a.close();
                   }
               }

           }
       // 所有的异常在这里交给 catch 块处理
       } catch (Exception var32) { // 我们定义的 catch 块
           System.out.println("Main: exception");
           System.out.println(var32.getMessage());
           Throwable[] suppressed = var32.getSuppressed();

           for(int i = 0; i < suppressed.length; ++i) {
               System.out.println(suppressed[i].getMessage());
           }
       }

   }

不管什么情况下(异常或者非异常)资源都必须关闭,如果是使用jdk1.7以上的Java版本,推荐使用try-with-resources语句。

补充:

  1. catch 语句块或finally语句块中,看不到 try-with-recourse 声明中的变量,无法对 try-with-recourse 声明中的变量进行操作。
  2. try-with-recourse 中定义多个变量时,由反编译可知,关闭的顺序是从后往前。
  3. try-with-recourse 语句也可以配合finally语句使用,finally语句块会在关闭所有资源后再执行。

参考文献:

Java中关于initcause的用法说明_Rashaun`s blog-CSDN博客_initcause方法

java使用try-with-resources语句优雅的关闭资源_及时雨的csdn-CSDN博客_inputstream try-with-resources

JDK1.8中的try-with-resources声明_狂草年糕的博客-CSDN博客_java8 try

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秦矜

对你有帮助的话,请我吃颗糖吧~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值