2023年11月29日发(作者:)

Java中的异常和处理详解

简介

程序运⾏时,发⽣的不被期望的事件,它阻⽌了程序按照程序员的预期正常执⾏,这就是异常。异常发⽣时,是任程序⾃⽣⾃灭,⽴刻退出

终⽌,还是输出错误给⽤户?或者⽤C语⾔风格:⽤函数返回值作为执⾏状态?。

Java提供了更加优秀的解决办法:异常处理机制。

异常处理机制能让程序在异常发⽣时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最⼤可能恢复正常并继续执⾏,

且保持代码的清晰。

Java中的异常可以是函数中的语句执⾏时引发的,也可以是程序员通过throw 语句⼿动抛出的,只要在Java程序中产⽣了异常,就会⽤⼀个

对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。

Throwable类是Java异常类型的顶层⽗类,⼀个对象只有是 Throwable 类的(直接或者间接)实例,他才是⼀个异常对象,才能被异常处理

机制识别。JDK中内建了⼀些常⽤的异常类,我们也可以⾃定义异常。

Java异常的分类和类结构图

Java标准库内建了⼀些通⽤的异常,这些类以Throwable为顶层⽗类。

Throwable⼜派⽣出Error类和Exception类。

错误:Error类以及他的⼦类的实例,代表了JVM本⾝的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注

Exception为⽗类的分⽀下的各种异常类。

异常:Exception以及他的⼦类,代表程序运⾏时发送的各种不期望发⽣的事件。可以被Java异常处理机制使⽤,是异常处理的核⼼。

总体上我们根据Javac对异常的处理要求,将异常类分为2类。

⾮检查异常(unckecked exception):Error RuntimeException 以及他们的⼦类。javac在编译时,不会提⽰和发现这样的异常,不要求

在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使⽤try…catch…finally)这样的异常,也可以不处理。对于这些异常,我们

应该修正代码,⽽不是去通过异常处理器处理 。这样的异常发⽣的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强

制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使⽤了空对象NullPointerException等等。

检查异常(checked exception):除了Error RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理⼯作(使⽤

try…catch…finally或者throws)。在⽅法中要么⽤try-catch语句捕获它并处理,要么⽤throws⼦句声明抛出它,否则编译不会通过。这样的

异常⼀般是由程序的运⾏环境导致的。因为程序可能被运⾏在各种未知的环境下,⽽程序员⽆法⼲预⽤户如何使⽤他编写的程序,于是程序

员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。

需要明确的是:检查和⾮检查是对于javac来说的,这样就很好理解和区分了。

初识异常

下⾯的代码会演⽰2个异常类型:ArithmeticException InputMismatchException。前者由于整数除0引发,后者是输⼊的数据不能被转换

int类型引发。

1

package

e;

2

import

java. util .Scanner ;

3

public

class

AllDemo

4

{

5

public

static

6

void

main (String [] args )

7

{

8

System . out. println(

9) ;

"----欢迎使⽤命令⾏除法计算器----"

10CMDCalculate ();

at culate( :20 )

at ( :12 )

*****************************************/

异常是在执⾏某个函数时引发的,⽽函数⼜是层级调⽤,形成调⽤栈的,因为,只要⼀个函数发⽣了异常,那么他的所有的caller都会被异

常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。

异常最先发⽣的地⽅,叫做异常抛出点。

从上⾯的例⼦可以看出,当devide函数发⽣除0异常时,devide函数将抛出ArithmeticException异常,因此调⽤他的CMDCalculate函数也⽆

法正常完成,因此也发送异常,⽽CMDCalculatecaller——main 因为CMDCalculate抛出异常,也发⽣了异常,这样⼀直向调⽤栈的栈底

回溯。这种⾏为叫做异常的冒泡,异常的冒泡是为了在当前发⽣异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例

⼦中没有使⽤任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终⽌。

上⾯的代码不使⽤异常处理机制,也可以顺利编译,因为2个异常都是⾮检查异常。但是下⾯的例⼦就必须使⽤异常处理机制,因为异常是

检查异常。

代码中我选择使⽤throws声明异常,让函数的调⽤者去处理可能发⽣的异常。但是为什么只throwsIOException呢?因为

FileNotFoundExceptionIOException的⼦类,在处理范围内。

1@Test

public

2

void

testException()

throws

3IOException

{

4

//FileInputStream的构造函数会抛出FileNotFoundException

5

FileInputStream fileIn =

6

new

FileInputStream(

"E:"

7);

8

9

int

word;

10

//read⽅法会抛出IOException

11

while

((word = ())!=-

121

)

13

2

//try块中放可能发⽣异常的代码。

3

//如果执⾏完try且不发⽣异常,则接着去执⾏finally块和finally后⾯的代码(如果有的话)。

4

//如果发⽣异常,则尝试去匹配catch块。

5

6}

catch

(SQLException SQLexception){

7

//每⼀个catch块⽤于捕获并处理⼀个特定的异常,或者这异常类型的⼦类。Java7中可以将多个异常声明在⼀个catch中。

8

//catch后⾯的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使⽤这个catch块来处理异常。

9

//catch块中可以使⽤这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。

10

//如果当前try块中发⽣的异常在后续的所有catch中都没捕获到,则先去执⾏finally,然后到这个函数的外部caller中去匹配异常处理器。

11

//如果try中没有发⽣异常,则所有的catch块将被忽略。

12

13

}

catch

14(Exception exception){

15//...

}

16

finally

{

17

18

//finally块通常是可选的。

19

//⽆论异常是否发⽣,异常是否匹配被处理,finally都会执⾏。

20

//⼀个try⾄少要有⼀个catch块,否则, ⾄少要有1finally块。但是finally不是⽤来处理异常的,finally不会捕获异常。

21

//finally主要做⼀些清理⼯作,如流的关闭,数据库连接的关闭等。

}

需要注意的地⽅

1try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使⽤。

2、每⼀个catch块⽤于处理⼀个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第⼀个匹配的catch会得到执⾏。匹配时,不仅

运⾏精确匹配,也⽀持⽗类匹配,因此,如果同⼀个try块下的多个catch异常类型有⽗⼦关系,应该将⼦类异常放在前⾯,⽗类异常放在后

⾯,这样保证每个catch块都有存在的意义。

3java中,异常处理的任务就是将执⾏控制流从异常发⽣的地⽅转移到能够处理这种异常的地⽅去。也就是说:当⼀个函数的某条语句发

⽣异常时,这条语句的后⾯的语句不会再执⾏,它失去了焦点。执⾏流跳转到最近的匹配的异常处理catch代码块去执⾏,异常被处理完

后,执⾏流会接着在处理了这个异常的catch代码块后⾯接着执⾏。

有的编程语⾔当异常被处理后,控制流会恢复到异常抛出点接着执⾏,这种策略叫做:resumption model of exception handling(恢复式异

常处理模式

Java则是让执⾏流恢复到处理了异常的catch块后接着执⾏,这种策略叫做:termination model of exception handling(终结式异常处理模

式)

1

public

static

void

2main(String[] args){

3

try

{

4

foo();

5

}

catch

6(ArithmeticException ae) {

7n(

"处理异常"

);

8

}

9

}

10

public

static

void

11foo(){

int

a =

5

/

0

;

//异常抛出点

n(

"为什么还不给我涨⼯资"

);

//不会执⾏

throws 函数声明

throws声明:如果⼀个⽅法内部的代码会抛出检查异常(checked exception),⽽⽅法⾃⼰⼜没有完全处理掉,则javac保证你必须在⽅法

的签名上使⽤throws关键字声明这些可能抛出的异常,否则编译不通过。

throws是另⼀种处理异常的⽅式,它不同于try…catch…finallythrows仅仅是将函数中可能出现的异常向调⽤者声明,⽽⾃⼰则不具体处

理。

采取这种异常处理的原因可能是:⽅法本⾝不知道如何处理这样的异常,或者说让调⽤者处理更好,调⽤者需要为可能发⽣的异常负责。

1

public

void

foo()

2

throws

ExceptionType1 , ExceptionType2 ,ExceptionTypeN

3{

4//foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的⼦类的异常对象。

}

}

finally

finally块不管异常是否发⽣,只要对应的try执⾏了,则它⼀定也执⾏。只有⼀种⽅法让finally块不执⾏:()。因此finally块通常⽤

来做资源释放操作:关闭⽂件,关闭数据库连接等等。

良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。

需要注意的地⽅:

1finally块没有处理异常的能⼒。处理异常的只能是catch块。

2、在同⼀try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执⾏catch块,再执⾏finally块。如果没有catch块匹配,

则先执⾏finally,然后去外⾯的调⽤者中寻找合适的catch块。

3、在同⼀try…catch…finally块中 try发⽣异常,且匹配的catch块中处理异常时也抛出异常,那么后⾯的finally也会执⾏:⾸先执⾏finally

块,然后去外围调⽤者中寻找合适的catch块。

这是正常的情况,但是也有特例。关于finally有很多恶⼼,偏、怪、难的问题,我在本⽂最后统⼀介绍了,电梯速达->finally块和return

throw 异常抛出语句

throw exceptionObject

程序员也可以通过throw语句⼿动显式的抛出⼀个异常。throw语句的后⾯必须是⼀个异常对象。

throw 语句必须写在函数中,执⾏throw 语句的地⽅就是⼀个异常抛出点,它和由JRE⾃动形成的异常抛出点没有任何差别。

1

public

void

save(User user)

2

{

3

if

(user ==

4

null

)

5

throw

new

6IllegalArgumentException(

"User对象为空"

);

7

//......

}

异常的链化

在⼀些⼤型的,模块化的软件开发中,⼀旦⼀个地⽅发⽣异常,则如⾻牌效应⼀样,将导致⼀连串的异常。假设B模块完成⾃⼰的逻辑需要

调⽤A模块的⽅法,如果A模块发⽣异常,则B也将不能完成⽽发⽣异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的

根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。

异常链化:以⼀个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的⼀个带Throwable参数的

函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

查看Throwable类源码,可以发现⾥⾯有⼀个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设

计如出⼀辙,因此形成链也是⾃然的了。

//........

}

下⾯是⼀个例⼦,演⽰了异常的链化:从命令⾏输⼊2int,将他们相加,输出。输⼊的数不是int,则导致getInputNumbers异常,从⽽导

add函数异常,则可以在add函数中抛出

⼀个链化的异常。

1

public

static

2main(String[] args)

void

3

{

4

5"请输⼊2个加数"

n(

);

6

int

32immExp;

33}

finally

34

{

35

();

36

}

... 1 more

*/

⾃定义异常

如果要⾃定义异常类,则扩展Exception类即可,因此这样的⾃定义异常都属于检查异常(checked exception)。如果要⾃定义⾮检查异

常,则扩展⾃RuntimeException

按照国际惯例,⾃定义的异常应该总是包含如下的构造函数:

⼀个⽆参构造函数

⼀个带有String参数的构造函数,并传递给⽗类的构造函数。

⼀个带有String参数和Throwable参数,并都传递给⽗类构造函数

⼀个带有Throwable 参数的构造函数,并传递给⽗类的构造函数。

下⾯是IOException类的完整源代码,可以借鉴。

1

public

super

(cause);

}

}

异常的注意事项

1、当⼦类重写⽗类的带有 throws声明的函数时,其throws声明的异常必须在⽗类异常的可控范围内——⽤于处理⽗类的throws⽅法的异常

处理器,必须也适⽤于⼦类的这个带throws⽅法 。这是为了⽀持多态。

例如,⽗类⽅法throws 的是2个异常,⼦类就不能throws 3个及以上的异常。⽗类throws IOException,⼦类就必须throws IOException或者

IOException的⼦类。

⾄于为什么?我想,也许下⾯的例⼦可以说明。

1

class

Father

2{

3

public

void

start()

4

throws

IOException

5

{

6

throw

new

Father();

29

objs[

301

] =

new

31Son();

32

33

for

(Father obj:objs)

34

{

35

//因为Son类抛出的实质是SQLException,⽽IOException⽆法处理它。

36

//那么这⾥的try。。catch就不能处理Son中的异常。

37

//多态就不能实现了。

38

try

{

();

}

catch

finally

{

13

n(

14"finally"

);

15

}

16}

/*输出:

finally

*/

很多⼈⾯对这个问题时,总是在归纳执⾏的顺序和规律,不过我觉得还是很难理解。我⾃⼰总结了⼀个⽅法。⽤如下GIF图说明。

也就是说:try…catch…finally中的return 只要能执⾏,就都执⾏了,他们共同向同⼀个内存地址(假设地址是0×80)写⼊返回值,后执⾏的

将覆盖先执⾏的数据,⽽真正被调⽤者取的返回值就是最后⼀次写⼊的。那么,按照这个思想,下⾯的这个例⼦也就不难理解了。

finally中的return 会覆盖 try 或者catch中的返回值。

1

{

27

return

282

;

29

}

30

31

}

32

16

{

try

17

result = bar();

18

n(result);

19

//输出100

20

bar()

throws

Exception

{

try

{

int

a =

5

/

0

22

23//catch中的异常被抑制

24@SuppressWarnings

(

25)

"finally"

26

}

finally

{

throw

new

Exception(

"我是finaly中的Exception"

);

}

}

}

上⾯的3个例⼦都异于常⼈的编码思维,因此我建议:

不要在fianlly中使⽤return

不要在finally中抛出异常。

减轻finally的任务,不要在finally中做⼀些其它的事情,finally块仅仅⽤来释放资源是最合适的。

将尽量将所有的return写在函数的最后⾯,⽽不是try … catch … finally中。