栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java#异常

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java#异常

简介

Java语言中Throwable是所有异常的根类,Throwable 派生了两个直接子类Error 和 Exception。Error 表示应用程序本身无法克服和恢复的一种严重问题,触发Error时会终止线程甚至是虚拟机。Exception 表示程序还能够克服和恢复的问题,Exception按照处理时机可以分为编译时异常和运行时异常。

编译时异常都是可以被修复的异常,代码编译期间Java程序必须显式处理编译时异常,否则无法编译通过。运行时异常通常是软件开发人员考虑不周所导致的问题,软件使用者无法克服和恢复这种问题,但在这种问题下软件系统可能会继续运行,严重情况下软件系统才会死掉。

异常的处理 异常处理方案

Java中异常处理有两种方案,捕获处理异常和抛出异常。

对编译时异常处理方案有两种,当前方法知道如何处理该异常则捕获处理。当前方法不知道如何处理则在定义该方法时声明抛出该异常。

运行时异常只有当代码在运行时才发现的异常,编译时不需要捕获处理。如除数是0、数组下标越界等等,其产生频繁,处理麻烦,若显示声明或者捕获将会对程序的可读性和运行效率影响很大。所以由虚拟机自动检测抛出。当然也可以主动显示捕获处理。

异常相关关键字

Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字。一般try,catch,finally结合使用,用于捕获异常。throws,throw单独使用,用于抛出异常。

      // try catch finally
       try {
             // 可能触发异常的代码
        } catch (XXXException e) { // XXXException  :代表异常类型 
            // 这里进行处理异常
        } finally {
           //这里进行资源释放
       }

try后紧跟一个花括号扩起来的代码块简称try块,try块它里面放置可能引发异常的代码。catch后定义一个异常类型和一个代码块。当try块某段代码触发了异常并匹配上catch定义的异常类型,这时便走catch块处理逻辑。

try 代码块后面可以跟着多个 catch 代码块,用于捕获不同类型的异常。Java 虚拟机会从上到下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后面的,否则编译器会报错。

finally块跟catch块之后,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。

    //throws 抛出异常,方法签名处抛出。
    private static void test() throws XXXException {}
    
    //throw 作为语句使用,代码中直接抛出一个异常。 throw new XXXException();
    private static void getCode(String type) {
       if (type == null) throw new IllegalArgumentException("参数不能为空");
    } 

throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常。throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。

异常处理栗子

(1)编译时异常

    public static void main(String[] args) {
        File file = new File("F://a.txt");
        if (!file.exists()) {
            file.createNewFile(); // 这段代码直接运行,这里编译不通过,报编译时异常。
        }
    }

编译时异常在代码编译期间就会报错(注意编译期间报错的不一定都是编译时异常),这种异常在编码期间需要手动捕获或者抛出处理~

public class ExceptionDemo {
    public static void main(String[] args) throws IOException {
        tryCatch();
        throwsException(); // 这里选择继续抛出给main
    }

    
    private static void tryCatch() {
        try {
            File file = new File("F://a.txt");
            if (!file.exists()) {
                file.createNewFile();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("finally 块");
        }

    }

    
    private static void throwsException() throws IOException {
        File file = new File("F://a.txt");
        if (!file.exists()) {
            file.createNewFile();
        }
    }
}

(2)运行时异常

    private static void runtimeException(String name) {
        name.length(); //name 为空时java.lang.NullPointerException.直接crash。
    }

运行时异常一般为开发人员代码考虑不周引起的,一般不需要主动来捕获或者抛出的~ 不过若是需要也可以主动捕获处理~如下。

    
    private static void runtimeException(String name) {
        // 捕获,出现异常也不会导致crash,不影响try catch 块之外的逻辑。
        try {
            name.length(); //java.lang.NullPointerException
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

(3)访问异常信息

如果需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常形参来获得。当JVM决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,这时我们可通过该参数来获得异常的相关信息。常用方法如下:

  • getMessage():返回该异常的详细描述字符串。
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
  • printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
  • getStackTrace():返回该异常的跟踪栈信息。
注意点

看似两三个栗子吧异常过了一遍,其实异常相关的东西还是很多的~

(1)不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此异常退出。
(2)触发异常时虚拟机会生成对应的异常,并会自上而下遍历catch中定义的异常条目,寻找匹配的异常条目。catch 中定义异常条目时要遵循只能扩大或者不相关的原则,否则编译失败。

(3)在Java 7以前,每个catch块只能捕获一种类型的异常;但从Java 7开始,一个catch块可以捕获多种类型的异常。多异常捕获需要注意:

  • 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。

  • 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。

    private static void mutipleException(){
        // 多异常捕获,如下catch块可以捕获处理2种异常。
        try {
        
        }catch (ArrayIndexOutOfBoundsException|NumberFormatException e){
            e = new IllegalArgumentException("") // 编译报错,多异常不能重新赋值。
        }
        
        // 单个异常捕获
        try {
            
        }catch (Exception e){
            e = new IllegalArgumentException(""); // 编译通过,单个异常可以赋值。
        }
    }

(4)在通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效。

(5)try、catch中的return语句、异常触发等导致方法结束的case不会影响finally代码块的执行。

    public static void main(String[] args) {
        System.out.println("test return value:"+test());
    }
    
    private static int test() {
        try {
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
            return 0;
        } finally {
            System.out.println("finally");
        }
        System.out.println("test finish");
        return 1;
    }
log:

finally
test return value:0

可见finally最终打印出来了,证明了我们的观点,那么为啥方法最终打印的返回值是0,而不是1呢?其实流程是这样的~

首先代码执行到try块触发ArithmeticException异常,然后catch块捕获住处理,不过异常机制有这么一个原则如果在 catch 中遇到了 return 或者异常等能使该函数终止的话,那么finally就必须先执行完finally代码块里面的代码然后再返回到catch中抛出或者return处。最终执行catch return语句方法结束。后续的代码不会再执行了。

不妨可以修改代码验证下,如下catch 代码块执行完后会继续走try catch finally 之外的代码~

    private static int test() {
        try {
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
            System.out.println("catch");
        } finally {
            System.out.println("finally");
        }
        System.out.println("test finish");
        return 1;
    }
    
log:

catch
finally
test finish
test return value:1   

来个栗子再让我们更好巩固下,彻底理解他 emmm~ 如下方法的返回值是几?

    private static int test() {
        try {
            int a = 10 / 0; //  ArithmeticException: / by zero
            return 1;
        } catch (Exception e) {
            return 2;
        } finally {    
            return 3;
        }
    }

代码执行到try的 int a这里会触发ArithmeticException,这时由异常处理器捕获,走catch中return 2,但是由于java的异常执行机制此时会先执行finally中的return3。finally这里正好碰到了return语句,正常结束方法。

若是finally只是处理一些资源关闭的代码,这里未return 3,那么本方法的返回值就是2喽~

(6)除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。

    private static int test() {
        try {
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
            System.out.println("catch");
            System.exit(0);
        } finally {
            System.out.println("finally");
        }
        return 0;
    }
log:

catch
Process finished with exit code 0

如上,首先触发ArithmeticException异常,此时会走到catch代码块,执行了打印语句后执行System.exit(0) 直接退出JVM。

(7)try catch finally 执行机制存在异常丢失的情况

    
    private static void test() {
        try {
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
            String a = null;
            a.length(); // finally 执行完毕后这里最终由系统抛出NullPointerException
        } finally {
            System.out.println("finally");
        }
    }
    
    private static void test() {
        try {
            int a = 10 / 0; //  ArithmeticException: / by zero
        } catch (Exception e) {
            String a = null;
            a.length(); // NullPointerException
        } finally {
           Integer.parseInt("aaa"); //代码执行到这里只会抛出NumberFormatException。上述两异常忽略。
           System.out.println("finally");
        }
    }
Java 7 Supressed 异常以及语法糖

前面了解到try catch 中的异常存在丢失的情况,为了解决这个问题,java7引入了Supressed 异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。

Java 7 专门构造了一个名为 try-with-resources 的语法糖,在字节码层面自动使用 Supressed 异常。当然,该语法糖的主要目的并不是使用 Supressed 异常,而是精简资源打开关闭。因为在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状态情况下都能关闭。这种做法使代码太臃肿了~

Java 7 的 try-with-resources 语法糖,极大的简化了try catch finally代码。程序可以在 try 关键字后声明并实例化实现了 AutoCloseable 接口的类,编译器将自动添加对应的 close 操作。在声明多个 AutoCloseable 实例的情况下,编译生成的字节码类似于try catch finally手工编写代码的编译结果。与手工代码相比,try-with-resources 还会使用 Supressed 异常的功能,来避免原异常 “被消失”。

(1)自动关闭资源

系统提供了一些类实现了AutoCloseable接口 ,若直接使用try-with-resources 语法糖则不需要再使用finally做繁琐的关闭处理的工作~

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

        
        try (
                BufferedReader br = new BufferedReader(new FileReader("F://a.txt"));
                PrintStream pr = new PrintStream(new FileOutputStream("F://b.txt"))
        ) {
            br.readLine();
            pr.write("emmm".getBytes());
        }
    }

BufferedReader、PrintStream都间接实现了AutoCloseable 接口,把它们放在try语句中声明、初始化,try语句会自动关闭它们。当然我们也可以自定义类实现接口即可,在接口中实现资源的处理工作。接下来验证下异常的捕获~

(2)避免异常的丢失

public class Demo implements AutoCloseable {

    private String desc;

    public Demo(String name) {
        this.desc = name;
    }

    public static void main(String[] args) throws Exception {
        try (
                Demo demo1 = new Demo("1");
                Demo demo2 = new Demo("2")) {

                int a = 10/0; // 执行代码 触发异常
        }
    }


    @Override
    public void close() throws Exception {
        // 这里直接抛出一个异常,验证 finally中触发了异常工作。
        throw new IllegalArgumentException();
    }
}

log: 打印所有的异常信息

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Demo.main(Demo.java:21)
	Suppressed: java.lang.IllegalArgumentException
		at Demo.close(Demo.java:29)
		at Demo.main(Demo.java:22)
	Suppressed: java.lang.IllegalArgumentException
		at Demo.close(Demo.java:29)
		at Demo.main(Demo.java:22)
异常实现原理

class 文件被编译成字节码时,每个方法都附带一张异常表。异常表中的每一个条目代表一个异常处理器(包括from、to、target、所捕获的异常类型)

  • from 、to 表示表示异常处理器监控范围,即用try标记的范围。
  • target表示异常处理器的起始位置,即catch起始位置。
  • 异常类型即为xxxException。
public class Test {
    public static void main(String[] args) {
        // 异常条目1(try catch finally块就是一个异常处理器)
        try {
            File file = new File("F://a.txt");
            if (!file.exists()) {
                file.createNewFile();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("finally1");
        }

        // 异常条目2
        try {
             int a =  1/0;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("finally2");
        }

    }
}
//javap 命令 查看class文件:javap -c -l Test.class  main方法中生成的异常表如下:

    Exception table:
       from    to  target type
           0    22    33   Class java/io/IOException
           0    22    49   any
          33    38    49   any
          60    64    75   Class java/lang/Exception
          60    64    91   any
          75    80    91   any

当程序触发异常时,Java 虚拟机会生成一个要抛出的异常实例,并且自上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断要抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java栈上所有方法的异常表。最终把异常抛出。

finally 代码块的编译比较复杂,当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。


针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常。这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。

如果 catch 代码块捕获了异常,并且触发了另外一个异常,那么 finally 捕获并重抛的异常是哪个呢?答案是后者,也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。

UncaughtExceptionHandler

待续~

End

参考:深入拆解 Java 虚拟机

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/825457.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号