Java#异常

Java#异常,第1张

简介

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

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

异常的处理 1、异常处理的方案

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

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

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

2、异常处理关键字

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

try关键字后紧跟一个花括号扩起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块,在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常。Java 虚拟机会从上到下匹配异常处理器。因此,前面的 catch 代码块所捕获的异常类型不能覆盖后面的,否则编译器会报错。finally块跟catch块之后,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。

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

3、异常处理栗子

(1)编译时异常

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

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

/**
 * Create by SunnyDay on 2022/04/21 18:26
 */
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 块");
        }

    }

    /**
     * 方法签名处抛出异常栗子。
     * 注意若是此方法被其他方法A调用,那么A需要捕获或者抛出处理。
     */
    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。
    }

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

    /**
     * 不过一般不会建议采取捕获处理的方式,完全可通过name的判空处理。
     */
    private static void runtimeException(String name) {
        // 捕获,出现异常也不会导致crash,不影响try catch 块之外的逻辑。
        try {
            name.length(); //java.lang.NullPointerException
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

(3)throw关键字

前面两个栗子就把异常相关关键字熟悉了下,不过还有个throw未涉及到,这里就再补充下~

    /**
     * 栗子:throw的用法。
     *
     * @function 当方法调用者不小心传递参数type为null时 直接抛出异常,程序crash.
     */
    private static int getCode(String type) {

        if (type == null) throw new IllegalArgumentException("参数不能为空");

        int code;

        if ("0".equals(type)) {
            code = 0;
        } else if ("1".equals(type)) {
            code = 1;
        } else {
            code = 2;
        }
        return code;

    }

(4)访问异常信息

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

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

看似两三个栗子吧异常过了一遍,其实异常相关的注意点还是很多的,这些需要我们在开发、学习中总结~

(1)不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此异常退出。一般表现为方法执行非正常完成。

(2)try块与if语句不一样,try块后的花括号不可以省略,即使try块里只有一行代码,也不可省略这个花括号。与之类似的是,catch块后的花括号也不可以省略。

(3)try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块中不能访问该变量。

(4)触发异常时虚拟机会生成对应的异常,并会自上而下遍历我们catch中定义的异常条目,寻找匹配的异常条目。catch 中定义异常条目时要遵循只能扩大或者不相关的原则,否则编译失败。

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

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

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

    private static void mutipleException(){
        // 多异常
        try {

        }catch (ArrayIndexOutOfBoundsException|NumberFormatException e){
            e = new IllegalArgumentException("") // 编译报错
        }
        
        // 单个异常
        try {
            
        }catch (Exception e){
            e = new IllegalArgumentException(""); // 编译通过
        }
    }

(6)在某些不幸的情况下,catch 代码块也触发了异常,那么 finally 代码块同样会运行,并会抛出 catch 代码块触发的异常。在某些极端不幸的情况下,finally 代码块也触发了异常,那么只好中断当前 finally 代码块的执行,并往外抛异常。

    /**
     * try中捕获异常A,catch中又触发异常B,这时finally执行完后系统只会抛出异常B。
     * 这种case也可以看做try catch的弊端,丢失了try中的异常。
     * */
    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");
        }
    }
    /**
     * try中捕获异常A,catch中又触发异常B,这时finally执行又触发异常C系统只会抛出异常C。
     * 这种case也可以看做try catch的弊端,丢失了try,catch中的异常。
     */
    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");
        }
    }

(7)不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。也即try、catch中的return语句不会影响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,但是异常机制有这么一个原则如果在 catch 中遇到了 return 或者异常等能使该函数终止的话,那么有 finally 就必须先执行完 finally 代码块里面的代码然后再返回到catch中抛出或者return。此时会先执行finally中的return3。finally这里正好碰到了return语句,正常结束方法。

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

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

    private static int test() {
        try {
            int a = 10 / 0; //  ArithmeticException: / by zero
            System.out.println("try");
            System.exit(1);
        } 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。

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

(10)在try块、catch块或finally块中包含完整的异常处理流程的情形被称为异常处理的嵌套。

  • 异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程既可放在try块里,也可放在catch块里,还可放在finally块里。
  • 异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。
Java 7 Supressed 异常以及语法糖

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

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

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

(1)自动关闭资源

系统提供了一些类自动实现了AutoCloseable ,这时我们直接使用try-with-resources 语法糖即可,不用再使用finally做繁琐的关闭处理的工作了~

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

        /**
         * 1、try()中进行变量定义(创建、赋值),类必须实现了AutoCloseable接口(或者是AutoCloseable实现类)。
         * 2、try后的代码块中可进行逻辑的 *** 作。
         * 3、自动关闭资源的try语句相当于包含了隐式的finally块,执行了close回调,因此这个try语句可以既没有catch块,
         *    也没有finally块。
         * 4、注意AutoCloseable#close()方法抛出了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都实现了Closeable接口,把它们放在try语句中声明、初始化,try语句会自动关闭它们。当然我们也可以自定义类实现接口即可,在接口中实现资源的处理工作。接下来验证下异常的捕获~

(2)避免异常的丢失

/**
 * Create by SunnyDay on 2022/04/22 17:37
 */
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。
/**
 * Create by SunnyDay on 2022/04/22 18:45
 */
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");
        }

        // 异常条目1
        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 虚拟机仍未匹配到异常处理器,那么它会d出当前方法对应的Java 栈帧,并且在调用者(caller)中重复上述 *** 作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java栈上所有方法的异常表。

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


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

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

UncaughtExceptionHandler

待续~

End

参考:深入拆解 Java 虚拟机

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/719834.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存