基础Java异常处理 Basic Exception handling in Java

Made by Mike_Zhang


Java主题:


1. Exception

在运行程序的时候,经常会遇到很多异常(Exception)被抛出,使得程序不受我们的控制,因此在设计代码的时候会经常考虑很多情况,如:

1
2
3
4
5
6
if (object != null){
object.doSth();
}
else if (object == null){
System.out.println("Error!")
}

但是很多时候我们并不能考虑到所有的异常情况,或者不能表达出这种异常,这会导致某个步骤因为异常而没有完成。

当一个成员方法抛出异常时,此方法不会返回任何值,只会抛出一个包含了异常信息的对象,并且此方法会立即终止。调用次方法的代码不会继续执行,而会用一种异常处理机制来寻找能够处理此异常的异常处理方法。

此时程序应该做:

  1. 使程序回到一个安全的状态并让用户执行其他程序;或者,
  2. 使用户保存所有步骤并且温柔地终止次程序。

因此需要有一种异常处理方法来把产生异常的地方转移到某一能过处理此异常的异常处理器。


1.2 Exception classification

异常(Exception)对象所属的类是从Throwable类继承来的,因此除了Java所定义的异常外,用户还可以通过继承来定义自己的异常。
以下为Throwable类的层级关系图:


Exception hierarchy in Java - C. S. Horstmann, Core Java. Boston: Pearson, 2019.

Throwable类有两个子类,ErrorException

  1. Error类描述的是在运行时的内部错误和资源穷尽情况。程序员不该抛出这类对象的错误,对应措施很少。

  2. Exception类也有两个子类,IOExceptionRuntimeExceptionRuntimeException的产生是由于程序员自己在编写代码时产生了错误IOException是由于客观因素产生的,如I/O的错误。

    RuntimeException包含:
    不当的转型;
    数组的out-of-bounds错误;
    访问一个空指针(null pointer)等

    IOException包括:
    在EOF(End of file)之后继续读取;
    打开一个不存在的文件;
    从一个不存在的类中寻找对象等

记住:“如果产生了一个RuntimeException,这就是你的错。”

Java规定所有从Error类或者RuntimeException类继承来的Exception对象都是Unchecked exception。其余的被称为Checked exception

Java编译器会根据你提供的异常处理器去检查所有的Checked exception


2. Throwing exception

2.1 Checked exception declaration

在Java中,成员方法在监测到其不能处理的情况后可以抛出异常。成员方法不仅仅可以返回值给编译器,也可以给编译器抛出异常
此类方法语法如下:

1
public FileInput(String name) throws FileNotFoundException

这个方法说明在创建此对象时,不仅仅可以产生一个FileInput对象,也可以在此对象产生错误的时候抛出一个FileNotFoundException异常。如果在创建对象的过程中产生了错误,那次对象不会被初始化,只会抛出一个异常。同时,系统也会去寻找一个能处理FileNotFoundException异常的异常处理器。

异常会在一下四种情况中被抛出:

  1. 引用一个会抛出Checked exception的方法,如上面的FileInput方法;
  2. 当检测到异常时通过throw语句抛出Checked exception;
  3. 在编写程序时产生了错误,如RuntimeException
  4. 运行程序时的客观环境错误,如JVM的错误。

当遇到到前两点抛出的错误时,必须有相对应的异常处理器去处理这些异常,因为可以抛出异常的方法都是有可能陷入死循环的,必须有相应的处理措施。

在以上例子中,使用了exception specification去定义了一个可抛出异常的方法,语法如下:

1
2
3
4
5
6
7
class FileInputClass{
// ...
public FileInput(String name) throws FileNotFoundException{
//exception specification:throws FileNotFoundException
// ...
}
}

当然,一个方法也可能抛出不止一种类型的异常,需要在定义时列出所有异常类型,并用逗号隔开

1
2
3
4
5
6
class FileInputClass{
// ...
public FileInput(String name) throws FileNotFoundException,EOFException{
// ...
}
}

总的来说,一方法必须声明其所有的可能抛出的Checked exception。Unchecked exception是你不能控制的 (Error) 或者是你应该避免的 (RuntimeExcwption)。如果一个方法没有全部定义出其可能抛出的Checked exception,编译器会报错。


2.2 Exception throwing

异常声明完成后,需要在方法体中抛出(throw)异常。
一般过程为:

  1. 寻找合适的异常类型;
  2. 创建此类异常的对象;
  3. 抛出异常对象。

首先需要寻找并定义异常的类型,语法如下:

1
throw new EOFException();

或者:

1
2
var e = new EOFException();
throw e;

再抛出异常,
结合到例子中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String readData(Scanner in) throws EOFException
{
//. . .
while (. . .)
{
if (!in.hasNext()) // EOF encountered
{
if (n < len)
throw new EOFException();
}
//. . .
}
return s;
}

EOFException()同时有一个有参构造方法,参数为字符串类型,可以更好的的描述此异常的含义:

1
2
String i = "This is an End of file Exception!";
throw new EOFException(i);

2.3 Create exception class

当遇到的异常类型并不在标准异常类中时,可以创建我们自己的异常类。只需要从父类Exception或者其子类继承即可,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FileChildException extends EOFException
{
public EOFException(){}
public EOFException(String i){
super(i);
}
}

String readData(Scanner in) throws FileChildException
{
//. . .
while (. . .)
{
if (!in.hasNext()) // EOF encountered
{
if (n < len)
throw new FileChildException();
}
//. . .
}
return s;
}

FileChildException子类一般会有一个默认的无参构造方法,以及一个有参的构造方法以表明此异常的具体信息。父类Throwable的方法toString()可以返回前面定义在有参构造方法里的信息。


3. Catching exception

如果一个异常被抛出后没有对应的处理措施,则包含其的程序就回被终止,并且在终端输出异常的类型以及stack trace。

为获取一个异常并提供异常处理方法,可以使用try-catch语句块,语法如下:

1
2
3
4
5
6
try{
// code
}
catch(Exception e){
// handler of Exception e
}

如果try语句块中抛出了定义在catch中类型的异常,则:

  1. 跳过try语句块中剩余的语句;
  2. 执行catch语句块中的语句。

如果try语句块中没有抛出任何异常,则catch语句块就会被跳过。
如果一个方法中的try语句块抛出了一个没有定义在catch中的异常,则立即跳出此方法,不会执行后面的语句。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void read(String filename)
{
try
{
var in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1)
{
process input
}
}
catch (IOException exception)
{
exception.printStackTrace();
}
}

read()方法可能会抛出IOException异常,一旦异常被抛出,程序就回跳过其后的while循环,直接进入catch中的语句,最后输出StackTrace.

同时我们也可以直接把异常声明在方法头中,一旦方法体中有异常,直接让此方法抛出异常,并不用去catch,例如:

1
2
3
4
5
6
7
8
9
public void read(String filename) throws IOException
{
var in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1)
{
process input
}
}

总的来说,明确该如何处理某一异常情况下使用try-catch方法否则就把异常在方法头中声明,抛出给方法

注意:

当一子类继承没有抛出异常的父类后,必须在子类方法中抓取所有的check exception,不允许在子类方法的方法头后使用throws关键字声明异常。

也可以抓取多个exception,分开进行异常处理,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
try{
// code
}
catch(FileNotFoundException e){
// handler of Exception e
}
catch(UnknownHostException e){
// handler of Exception e
}
catch(IOException e){
// handler of Exception e
}

也可以进行结合 (Java7):

1
2
3
4
5
6
7
8
9
try{
// code
}
catch(FileNotFoundException | UnknownHostException e){
// handler of Exception e
}
catch(IOException e){
// handler of Exception e
}

但是使用这种结构的异常类之间不能有继承关系。

在抓取到异常后,也可以受用异常类的方法去获取异常对象的信息:

得到异常对象的更多信息:

1
e.getMessage();

得到具体的错误信息,或者异常对象的所属类:
1
e.getClass.getName();

retry语句可以使try语句块再次运行一遍,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public void foo(){ // pseudo code
boolean hasFailed = false;
try{
if(!hasFailed){
// do the normal thing, which may trigger
// an exception
}
else{
// do something safe
}
}
catch(ExceptionType et){
// log exception
hasFailed = true;
retry; // resumes from the beginning of try }
}
}


3.1 NDECC

当一个异常被抛出,程序就会寻找能够处理此异常的nearest dynamically enclosing catch clause(NDECC),例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void h(){
try{g();}
catch(ET3 e){ ... } // c1
}

void g(){
try{f();}
catch(ET2 e){ ... } // c2
}

void f(){
// ...
try{statement;}
catch(ET1 e){ ... } // c3
// ...
}

讨论以下种情况:

  1. statement语句抛出了ET1类的异常,并且处于c3的语句能够处理此异常,则进行处理;
  2. statement语句抛出了ET2类的异常,并且处于c3的语句不能够处理此异常,则处于c2的语句进行处理;
  3. statement语句抛出了ET3类的异常,并且处于c3的语句不能够处理此异常,则处于c1的语句进行处理;

注意:

ET1类是ET类的子类,则ET类的catch clause可以处理ET1类的异常。

3.2 Rethrowing excepltion

重新抛出异常通常有两种情况:
1.改变异常的类型;

例子如下:

1
2
3
4
5
6
7
8
try
{
access the database
}
catch (SQLException e)
{
throw new ServletException("database error: " + e.getMessage());
}

2.只记录异常,不改变异常的类型,再次抛出。

1
2
3
4
5
6
7
8
9
try
{
access the database
}
catch (Exception e)
{
logger.log(level, message, e);
throw e;
}

4. The finally statement

当一个方法抛出异常后,它就回终止运行之后的代码并跳出此方法。但有时需要此方法运行完成,此时就需要用到finally关键字。

不管有无异常抛出,finally语句块中的内容都会被执行。

看以下例子,来自C. S. Horstmann, Core Java. Boston: Pearson, 2019(优秀的Java参考书,十分推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var in = new FileInputStream(. . .);
try
{
// 1
// code that might throw exceptions
// 2
}
catch (IOException e)
{
// 3
// show error message
// 4
}
finally
{
// 5
in.close();
}
// 6

讨论以下几种情况:

  1. 没有抛出异常

    首先执行try语句块中的代码,再执行finally语句块中的代码,最后执行finally语句块后的第一行代码。顺序为1,2,5,6

  2. 抛出异常并被catch语句 获取

    首先执行try语句块中的代码,直到抛出异常的那行代码,其后的代码都会被跳过。再执行catch语句块中的代码,最后执行finally代码块中的代码。之后再分两类情况:

    catch代码块中没有 异常抛出
    在上述过程后,最后执行finally语句块后的第一行代码。顺序为1,3,4,5,6

    catch代码块中 异常抛出
    catch语句块中只会运行到抛出异常的那行代码,之后的代码会被跳过。此异常被抛出后,会抛出至引用此方法的方法。顺序为1,3,5

  3. 抛出异常并没有 被catch语句获取

    首先执行try语句块中的代码,直到抛出异常的那行代码,其后的代码都会被跳过。再执行finally代码块中的代码。异常被抛出至引用此方法的方法。 顺序为1,5


6. Standard exception in Java

参考:
Exception (Java SE 11 & JDK 11) - docs.oracle.com


参考

B. Eckel, Thinking in java. Upper Saddle River, N.Y. Prentice Hall, 2014.
C. S. Horstmann, Core Java. Boston: Pearson, 2019.
M. Goodrich, R. Tamassia, and A. O’reilly, Data Structures and Algorithms in Java, 6th Edition. John Wiley & Sons, 2014.
“Java API Reference,” docs.oracle.com. https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Exception.html (accessed Oct. 08, 2021).


写在最后

本章只介绍了Java异常抛出的基本内容。如Rethrowing and Chaining Exceptions、try-with-Resources Statement、Stack Trace等内容会持续更新。
最后,希望大家一起交流,分享,指出问题,谢谢!


原创文章,转载请标明出处
Made by Mike_Zhang




感谢你的支持

基础Java异常处理 Basic Exception handling in Java
https://ultrafish.io/post/Java-learning-3/
Author
Mike_Zhang
Posted on
October 8, 2021
Licensed under