Java基本概念:异常


一、简介

描述:

  • 异常(Exception)指不期而至的各种状况,异常发生的原因有很多,通常包含以下几大类:
    • 用户输入了非法数据。
    • 要打开的文件不存在。
    • 网络通信时连接中断,或者JVM内存溢出。
  • 异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。
  • Java语言在设计的当初就考虑到这些问题,提出异常处理的框架的方案,所有的异常都可以用一个异常类来表示,不同类型的异常对应不同的子类异常(目前我们所说的异常包括错误概念),定义异常处理的规范,在 JDK1.4 版本以后增加了异常链机制,从而便于跟踪异常。
  • Java异常是一个描述在代码段中发生异常的对象,当发生异常情况时,一个代表该异常的对象被创建并且在导致该异常的方法中被抛出,而该方法可以选择自己处理异常或者传递该异常。
  • Java异常处理本质:抛出异常和捕获异常。

分类:

  • 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。




二、异常体系

描述:

  • Java把异常当作对象来处理,并定义一个'java.lang.Throwable' 类作为所有异常的父类
  • 在Java API中已经定义了许多异常类,这些异常类分为两大类,错误(Error)异常(Exception)
  • 'Error'表示不希望被程序捕获或者是程序无法处理的错误。
  • 'Exception'表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常,'Exception'又分为运行时异常(RuntimeException)非运行时异常
  • Java异常(Exception)又可以分为不受检查异常(Unchecked Exception)检查异常(Checked Exception)

图示:

Java异常层次结构图




三、异常的区别与联系


Ⅰ、Error与Exception

Error:

  • 'Error'类对象由Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。
  • 例如 Java虚拟机运行错误(VirtualMachineError),当JVM不再有继续执行操作所需的内存资源时,将出现'OutOfMemoryError'。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况
  • 对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。
  • 在Java中,错误通常是使用'Error'的子类描述。

Exception:

  • 在 Exception 分支中有一个重要的子类'RuntimeException'运行时异常,该类型的异常会自动为你所编写的程序定义异常('ArrayIndexOutOfBoundsException'数组下标越界、'NullPointerException'空指针、'ArithmeticException'算术异常、'MissingResourceException'丢失资源、'ClassNotFoundException'找不到类等),这些异常是不受检查异常,程序中可以选择捕获处理,也可以不处理
  • 'RuntimeException'一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而'RuntimeException'之外的异常(Exception)我们统称为非运行时异常,类型上属于'Exception'类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过,因此又称为检查异常。如'IOException'、'SQLException'等以及用户自定义的'Exception'异常,一般情况下不自定义检查异常。




Ⅱ、 Unchecked Exception与Checked Exception

Unchecked Exception:

  • 不受检查异常包括'RuntimeException'及其子类和'Error'。
  • 不受检查异常为编译器不要求强制处理的异常。

Checked Exception:

  • 除了'RuntimeException'及其子类以外,其他的'Exception'类及其子类都属于检查异常。
  • 当程序中可能出现检查异常,要么使用'try-catch'语句进行捕获,要么用'throws'子句抛出否则编译无法通过
  • 检查异常是编译器要求必须处置的异常。




四、异常处理机制


Ⅰ、抛出异常

描述:

  • 要理解抛出异常,首先要明白什么是异常情形(Exception Condition)

    • 异常情形是指阻止当前方法或作用域继续执行的问题

    • 异常情形和普通问题不同,普通问题是指在当前环境下能得到足够的信息,总能处理这个错误

  • 对于异常情形,已经无法继续下去了,因为在当前环境下无法获得必要的信息来解决问题,你所能做的就是从当前环境中跳出,并把问题提交给上一级环境,这就是抛出异常时所发生的事情。

  • 抛出异常后,会有几件事随之发生:

    • 首先,是像创建普通的java对象一样,将使用'new'在堆上创建一个异常对象
    • 然后当前的执行路径(已经无法继续下去了)被终止,并且从当前环境中弹出对异常对象的引用
    • 此时,异常处理机制接管程序,并开始寻找一个恰当的地方继续执行程序,这个恰当的地方就是异常处理程序或者异常处理器,它的任务是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去。

示例:

public class Test {

    public void test1(Student student) {
        if (student == null) {
            /* 抛出一个不受检查异常时,方法声明上不需要强制声明该异常 */
            throw new RuntimeException();
        }
        System.out.println(student.name);
    }

    public void test2(Student student) throws Exception {
        if (student == null) {
            /* 抛出一个受检查异常时,方法声明必须声明该异常,否则编译报错 */
            throw new Exception();
        }
        System.out.println(student.name);
    }

    public static void main(String[] args) throws Exception {
        new Test().test1(null);
        /* main方法调用声明了异常的方法,必须对该异常进行处理,否则编译报错 */
        new Test().test2(null);
    }
}

class Student {
    String name;
}




Ⅱ、捕获异常

描述:

  • 在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。
  • 潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。
  • 运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行
  • 当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

注意:

  • 对于运行时异常(RuntimeException)、错误(Error)和检查异常(Checked Exception),Java技术所要求的异常处理方式有所不同。
    • 由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常
    • 对于方法运行中可能出现的错误,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
    • 对于所有的检查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常。




Ⅲ、异常处理关键字

  1. try
    • 用于监听。将要被监听的代码(可能抛出异常的代码)放在'try'语句块之内,当'try'语句块内发生异常时,异常就被抛出
  2. catch
    • 用于捕获异常。'catch'用来捕获'try'语句块中发生的异常
  3. finally
    • 'finally'语句块总是会被执行。它主要用于回收在'try'块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有'finally'块,执行完成之后,才会回来执行'try'或者'catch'块中的'return'或者'throw'语句,如果'finally'中使用了'return'或者'throw'等终止方法的语句,则就不会跳回执行,直接停止。
  4. throw
    • 用于在方法体中抛出异常。
  5. throws
    • 用在方法签名中,用于声明该方法可能抛出的异常。




五、处理异常


Ⅰ、try-catch

描述:

  • 要明白异常捕获,还要理解监控区域(guarded region)的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。
  • 'try-catch'所描述的即是监控区域,关键词'try'后的一对大括号将一块可能发生异常的代码包起来,即为监控区域。Java方法在运行过程中发生了异常,则创建异常对象
  • 将异常抛出监控区域之外,由Java运行时系统负责寻找匹配的'catch'子句来捕获异常。若有一个'catch'语句匹配到了,则执行该'catch'块中的异常处理代码,就不再尝试匹配别的'catch'块了。
  • 匹配原则:如果抛出的异常对象属于'catch'子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与'catch'块捕获的异常类型相匹配。

多重'catch'语句:

  • 很多情况下,由单个的代码段可能引起多个异常。
  • 处理这种情况,我们需要定义两个或者更多的'catch'子句,每个子句捕获一种类型的异常,当异常被引发时,每个'catch'子句被依次检查,第一个匹配异常类型的子句执行,当一个'catch'子句执行以后,其他的子句将被跳过
  • 编写多重'catch'语句块时注意'catch'子句捕获的异常的顺序:先小后大,即先子类后父类。否则,捕获底层异常类的'catch'子句将可能会被屏蔽。

嵌套'try'语句:

  • 'try'语句可以被嵌套。也就是说,一个'try'语句可以在另一个'try'块的内部。
  • 每次进入'try'语句,异常的前后关系都会被推入堆栈。如果一个内部的'try'语句不含特殊异常的'catch'处理程序,堆栈将弹出,下一个'try'语句的'catch'处理程序将检查是否与之匹配。这个过程将继续直到一个'catch'语句被匹配成功,或者是直到所有的嵌套'try'语句被检查完毕。如果没有'catch'语句匹配,Java运行时系统将处理这个异常。
  • 当有方法调用时,'try'语句的嵌套可以很隐蔽的发生。例如,我们可以将对方法的调用放在一个'try'块中,在该方法的内部,有另一个'try'语句。在这种情况下,方法内部的'try'语句仍然是嵌套在外部调用该方法的'try'块中的。

语法:

try {
    /* 监控区域,可能产生异常的代码 */
} catch (FirstExceptionType e1) {
    /* 指定异常类型的异常e1的处理代码 */
} catch (SecondExceptionType e2) {
    /* 指定异常类型的异常e2的处理代码 */
}

示例:

class Test {
    static void nestTry(int a) {
        try {
            /* 当命令行编译运行该java文件时有一个参数的话,使此方法产生算术运算异常 */
            if (a == 1) {
                a = a / (a - a);
            }
            /* 当命令行编译运行该java文件时有两个参数的话,使此方法产生数组越界异常 */
            if (a == 2) {
                int c[] = {1};
                c[42] = 99;
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBounds :" + e);
        }
    }

    public static void main(String[] args) {
        try {
            /* 当命令行编译运行该java文件时没有参数的话,使此方法产生算术运算异常 */
            int a = args.length;
            int b = 42 / a;
            System.out.println("a = " + a);
            nestTry(a);
            /* 多重'catch'语句块捕获的异常的顺序:先小后大,即先子类后父类。 */
        } catch (ArithmeticException e) {
            System.out.println("Divide by 0 :" + e);
        } catch (Exception e) {
            /* Throwable类重载了Object类的toString()方法,所以打印e调用其toString()方法将返回一个包含异常描述的字符串。 */
            System.out.println(e);
        }
    }
}

CMD测试:

D:\studyworkspace\test\src\main\java\com\conyoo\test>javac Test.java

D:\studyworkspace\test\src\main\java\com\conyoo\test>cd ../../..

D:\studyworkspace\test\src\main\java>java com.conyoo.test.Test
Divide by 0 :java.lang.ArithmeticException: / by zero

D:\studyworkspace\test\src\main\java>java com.conyoo.test.Test 1
a = 1
Divide by 0 :java.lang.ArithmeticException: / by zero

D:\studyworkspace\test\src\main\java>java com.conyoo.test.Test 1 1
a = 2
ArrayIndexOutOfBounds :java.lang.ArrayIndexOutOfBoundsException: 42

D:\studyworkspace\test\src\main\java>




Ⅱ、throw

描述:

  • 除了Java运行时系统自动创建的异常对象,我们还可以用'throw'语句抛出明确的异常
  • 'throw'语句抛出的一定是'Throwable'类类型或者'Throwable'子类类型的一个对象
  • 'throw'语句抛出的异常对象有两种来源:使用'catch'子句中的参数,或者使用'new'操作符创建
  • 程序执行完'throw'语句之后立即停止,'throw'后面的任何语句不会执行,最邻近的'try'块会用来检查是否含有一个与异常类型匹配的'catch'语句。如果发现了匹配的'catch'块,转向执行该'catch'块里的语句;如果没有发现,次包围的'try'块来检查,以此类推。如果没有发现匹配的'catch'块,默认异常处理程序会中断程序的执行,并且打印堆栈轨迹

语法:

throw ThrowableInstance;

示例:

/*
	运行结果为在控制台依次打印:
		异常对象在test方法里被catch。
		异常对象在main方法里被catch。java.lang.NullPointerException: 一个空指针异常对象
*/
class Test {
    static void test() {
        try {
            /* 
                所有的Java内置的运行时异常有两个构造方法:一个没有参数,一个带有一个字符串参数。
                当用第二种形式时,该参数应当为描述异常的字符串。
                该字符串会被赋值给Throwable类的属性detailMessage。
                这样Throwable类定义的getMessage()和Throwable类重写的toString()等方法中,都会使用该字符串。
            */
            throw new NullPointerException("一个空指针异常对象");
        } catch (NullPointerException e) {
            System.out.println("异常对象在test方法里被catch。");
            throw e;
        }
    }

    public static void main(String[] args) {
        try {
            test();
        } catch (NullPointerException e) {
            System.out.println("异常对象在main方法里被catch。" + e);
        }
    }
}




Ⅲ、throws

描述:

  • 如果一个方法可以导致一个异常,但不在该方法中处理此异常,则该方法应当指定这种不处理的行为(若异常为检查异常则必须指定),以使该方法的调用者可以保护自身而不发生异常。要做到这点,我们可以在该方法的声明中包含一个'throws'子句
  • 一个'throws'子句可以列举一个方法可能引发的所有异常类型

注意:

  • 如果是不受检查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出
  • 必须声明方法可抛出的任何检查异常(checked exception)。即如果一个方法可能出现检查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。
  • 仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。
  • 若重写一个方法,子类方法声明的异常类型,不能为'被重写方法声明的异常类型'的父类,可以为同一类型或其子类。也可以另外再声明,不在'被重写方法声明的异常类型'的直系继承关系中的,其他类型异常。

语法:

/*  Throwable是该方法可能引发的异常,也可以是异常列表,中间以逗号隔开。 */
public void function() throws Throwable {}

示例:

class Test1 {
    void test() throws IllegalAccessException {
        throw new IllegalAccessException("IllegalAccessException1");
    }

    public static void main(String[] args) {
        try {
            new Test2().test();
        } catch (Exception e) {
            System.out.println(e);//java.lang.IllegalAccessException: IllegalAccessException2
        }
    }
}

class Test2 extends Test1 {
    /* throws可以另外再声明,不在'被重写方法声明的异常类型'的直系继承关系中的,其他类型异常。 */
    void test() throws NegativeArraySizeException, IllegalAccessException {
        throw new IllegalAccessException("IllegalAccessException2");
    }
}




Ⅳ、finally

描述:

  • 当异常发生时,通常方法的执行将做一个陡峭的非线性的转向,它甚至会过早地导致方法返回。例如,如果一个方法打开了一个文件并关闭,然后退出,你不希望关闭文件的代码被异常处理机制所跳过,就可以使用'finally'关键字。
  • 一个方法,将从内部的'try-catch'块返回到其调用者处前,即经过一个未捕获的异常或者是一个明确的返回语句前,'finally'子句会执行。这在关闭文件句柄,和释放任何在方法开始时被分配的其他资源时很有用

注意:

  • 'finally'代码块在所属'try-catch'块将完成之时,其他'try-catch'块出现之前执行。
  • 'finally'块无论所在监控区域有没有异常抛出都会执行。如果抛出异常,即使没有'catch子句匹配,'finally'块也会执行。
  • 'finally'子句是可选项,可以有也可以无,但是每个'try'语句至少需要一个'catch'或者'finally'子句。
  • 如果'finally'块与一个'try'块(没有'catch'块)联合使用,finally块将在'try'块结束之前执行。
  • 如果存在'finally'代码块,'try'中的'return'语句不会立马返回调用者,而是记录下返回值待'finally'代码块执行完毕之后再向调用者返回其值,这样如果在'finally'中修改了返回值,就会返回修改后的值
  • 不要在'finally'中进行'return'操作或者修改返回值,可能会导致一些意想不到的逻辑错误,例如方法的调用者捕获不到异常。

示例:

/*
	运行结果为在控制台依次打印:
		inside proc1
        proc1's finally
        java.lang.RuntimeException
        inside proc2
        proc2's finally
        inside proc3
        proc3's finally
*/
class Test {
    static void proc1() {
        try {
            System.out.println("inside proc1");
            throw new RuntimeException();
        } finally {
            System.out.println("proc1's finally");
        }
    }

    static void proc2() {
        try {
            System.out.println("inside proc2");
            return;
        } finally {
            System.out.println("proc2's finally");
        }
    }

    static void proc3() {
        try {
            System.out.println("inside proc3");
        } finally {
            System.out.println("proc3's finally");
        }
    }

    public static void main(String[] args) {
        try {
            proc1();
        } catch (Exception e) {
            System.out.println(e);
        }
        proc2();
        proc3();
    }
}




Ⅴ、try、catch、finally、return执行顺序

  1. 执行'try'块里的代码。
  2. 若'try'块里出现异常,检查该异常是否能被'catch'子句捕获,若与'catch'子句声明的异常相匹配,则执行'catch'块里的代码。
  3. 执行'try-catch'块里的代码,直到遇到向外抛出异常(该异常无法被当前'catch'子句捕获)或'return'的语句,这时若有'finally'块,则先去执行'finally'块中的代码
  4. 向外抛出异常或执行'return'语句。




六、自定义异常

描述:

  • 使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常,自定义异常类只需继承'Exception'类即可。
  • 在程序中使用自定义异常类,与使用Java内置的异常类基本相同,由于是自定义的异常,所以需要在代码里主动创建该异常对象并'throw'
  • 使用自定义异常类大体可分为以下几个步骤
    1. 创建自定义异常类,继承'Exception'类或其子类。
    2. 在方法中通过'throw'关键字抛出异常对象。
    3. 如果要在当前抛出异常的方法中处理异常,可以使用'try-catch'语句捕获并处理否则在方法的声明处,通过'throws'关键字指明要抛出给方法调用者的异常。
    4. 继续在出现异常的方法的调用者中捕获处理异常或继续抛出异常。

示例:

/*
	运行结果为在控制台依次打印:
		Called compute(1)
        Normal exit!
        Called compute(20)
        Caught MyException [20]
*/
public class Test {
    static void compute(int a) throws MyException {
        System.out.println("Called compute(" + a + ")");
        if (a > 10) {
            throw new MyException(a);
        }
        System.out.println("Normal exit!");
    }

    public static void main(String[] args) {
        try {
            compute(1);
            compute(20);
        } catch (MyException me) {
            System.out.println("Caught " + me);
        }
    }
}

class MyException extends Exception {
    private int detail;

    MyException(int a) {
        detail = a;
    }

    public String toString() {
        return "MyException [" + detail + "]";
    }
}




七、总结

图示:

异常总结

实际应用:

  1. 处理运行时异常时,采用逻辑去合理规避的同时,辅助'try-catch'进行处理。
  2. 在多重'catch'块后面,可以加一个'catch (Exception e)'来处理可能会被遗漏的异常
  3. 对于不确定的代码,也可以加上'try-catch'处理潜在的异常。
  4. 尽量去处理异常,切忌只是简单地调用'printStackTrace()'去打印输出
  5. 具体如何处理异常,要根据不同的业务需求和异常类型去决定。
  6. 尽量添加'finally'语句块去释放占用的资源。




posted @ 2020-12-22 15:17  conyoo  阅读(695)  评论(0编辑  收藏  举报