原文出处 :http://www.cnblogs.com/lulipro/p/7504267.htmlhtml
程序运行时,发生的不被指望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,马上退出终止,仍是输出错误给用户?或者用C语言风格:用函数返回值做为执行状态?。java
Java提供了更加优秀的解决办法:异常处理机制。程序员
异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java中的异常能够是函数中的语句执行时引起的,也能够是程序员经过throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。数据库
Throwable类是Java异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些经常使用的异常类,咱们也能够自定义异常。编程
Java异常的分类和类结构图
Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。数组
Throwable又派生出Error类和Exception类。多线程
错误:Error类以及他的子类的实例,表明了JVM自己的错误。错误不能被程序员经过代码处理,Error不多出现。所以,程序员应该关注Exception为父类的分支下的各类异常类。编程语言
异常:Exception以及他的子类,表明程序运行时发送的各类不指望发生的事件。能够被Java异常处理机制使用,是异常处理的核心。ide
整体上咱们根据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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package
com.example;
import
java. util .Scanner ;
public
class
AllDemo
{
public
static
void
main (String [] args )
{
System . out. println(
"----欢迎使用命令行除法计算器----"
) ;
CMDCalculate ();
}
public
static
void
CMDCalculate ()
{
Scanner scan =
new
Scanner ( System. in );
int
num1 = scan .nextInt () ;
int
num2 = scan .nextInt () ;
int
result = devide (num1 , num2 ) ;
System . out. println(
"result:"
+ result) ;
scan .close () ;
}
public
static
int
devide (
int
num1,
int
num2 ){
return
num1 / num2 ;
}
}
/*****************************************
----欢迎使用命令行除法计算器----
0
Exception in thread "main" java.lang.ArithmeticException : / by zero
at com.example.AllDemo.devide( AllDemo.java:30 )
at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )
at com.example.AllDemo.main( AllDemo.java:12 )
----欢迎使用命令行除法计算器----
r
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor( Scanner.java:864 )
at java.util.Scanner.next( Scanner.java:1485 )
at java.util.Scanner.nextInt( Scanner.java:2117 )
at java.util.Scanner.nextInt( Scanner.java:2076 )
at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )
at com.example.AllDemo.main( AllDemo.java:12 )
*****************************************/
|
异常是在执行某个函数时引起的,而函数又是层级调用,造成调用栈的,由于,只要一个函数发生了异常,那么他的全部的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就造成的了异常追踪栈。
异常最早发生的地方,叫作异常抛出点。
从上面的例子能够看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,所以调用他的CMDCalculate函数也没法正常完成,所以也发送异常,而CMDCalculate的caller——main 由于CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫作异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。因为这个例子中没有使用任何异常处理机制,所以异常最终由main函数抛给JRE,致使程序终止。
上面的代码不使用异常处理机制,也能够顺利编译,由于2个异常都是非检查异常。可是下面的例子就必须使用异常处理机制,由于异常是检查异常。
代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。可是为何只throws了IOException呢?由于FileNotFoundException是IOException的子类,在处理范围内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Test
public
void
testException()
throws
IOException
{
//FileInputStream的构造函数会抛出FileNotFoundException
FileInputStream fileIn =
new
FileInputStream(
"E:\\a.txt"
);
int
word;
//read方法会抛出IOException
while
((word = fileIn.read())!=-
1
)
{
System.out.print((
char
)word);
}
//close方法会抛出IOException
fileIn.clos
}
|
异常处理的基本语法
在编写代码处理异常时,对于检查异常,有2种不一样的处理方式:使用try…catch…finally语句块处理它。或者,在函数签名中使用throws 声明交给函数调用者caller去解决。
try…catch…finally语句块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
try
{
//try块中放可能发生异常的代码。
//若是执行完try且不发生异常,则接着去执行finally块和finally后面的代码(若是有的话)。
//若是发生异常,则尝试去匹配catch块。
}
catch
(SQLException SQLexception){
//每个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中能够将多个异常声明在一个catch中。
//catch后面的括号定义了异常类型和异常参数。若是异常与之匹配且是最早匹配到的,则虚拟机将使用这个catch块来处理异常。
//在catch块中可使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。
//若是当前try块中发生的异常在后续的全部catch中都没捕获到,则先去执行finally,而后到这个函数的外部caller中去匹配异常处理器。
//若是try中没有发生异常,则全部的catch块将被忽略。
}
catch
(Exception exception){
//...
}
finally
{
//finally块一般是可选的。
//不管异常是否发生,异常是否匹配被处理,finally都会执行。
//一个try至少要有一个catch块,不然, 至少要有1个finally块。可是finally不是用来处理异常的,finally不会捕获异常。
//finally主要作一些清理工做,如流的关闭,数据库链接的关闭等。
}
|
须要注意的地方
一、try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
二、每个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会获得执行。匹配时,不只运行精确匹配,也支持父类匹配,所以,若是同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每一个catch块都有存在的意义。
三、java中,异常处理的任务就是将执行控制流从异常发生的地方转移到可以处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫作:resumption model of exception handling(恢复式异常处理模式 )
而Java则是让执行流恢复处处理了异常的catch块后接着执行,这种策略叫作:termination model of exception handling(终结式异常处理模式)
1
2
3
4
5
6
7
8
9
10
11
|
public
static
void
main(String[] args){
try
{
foo();
}
catch
(ArithmeticException ae) {
System.out.println(
"处理异常"
);
}
}
public
static
void
foo(){
int
a =
5
/
0
;
//异常抛出点
System.out.println(
"为何还不给我涨工资!!!"
);
//////////////////////不会执行
}
|
throws 函数声明
throws声明:若是一个方法内部的代码会抛出检查异常(checked exception),而方法本身又没有彻底处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,不然编译不经过。
throws是另外一种处理异常的方式,它不一样于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而本身则不具体处理。
采起这种异常处理的缘由多是:方法自己不知道如何处理这样的异常,或者说让调用者处理更好,调用者须要为可能发生的异常负责。
1
2
3
4
|
public
void
foo()
throws
ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{
//foo内部能够抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}
|
finally块
finally块无论异常是否发生,只要对应的try执行了,则它必定也执行。只有一种方法让finally块不执行:System.exit()。所以finally块一般用来作资源释放操做:关闭文件,关闭数据库链接等等。
良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。
须要注意的地方:
一、finally块没有处理异常的能力。处理异常的只能是catch块。
二、在同一try…catch…finally块中 ,若是try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。若是没有catch块匹配,则先执行finally,而后去外面的调用者中寻找合适的catch块。
三、在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,而后去外围调用者中寻找合适的catch块。
这是正常的状况,可是也有特例。关于finally有不少恶心,偏、怪、难的问题,我在本文最后统一介绍了,电梯速达->:finally块和return
throw 异常抛出语句
throw exceptionObject
程序员也能够经过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。
throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动造成的异常抛出点没有任何差异。
1
2
3
4
5
6
7
|
public
void
save(User user)
{
if
(user ==
null
)
throw
new
IllegalArgumentException(
"User对象为空"
);
//......
}
|
异常的链化
在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应同样,将致使一连串的异常。假设B模块完成本身的逻辑须要调用A模块的方法,若是A模块发生异常,则B也将不能完成而发生异常,可是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化能够将多个模块的异常串联起来,使得异常信息不会丢失。
异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当作参数的异常,咱们叫他根源异常(cause)。
查看Throwable类源码,能够发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计一模一样,所以造成链也是天然的了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
class
Throwable
implements
Serializable {
private
Throwable cause =
this
;
public
Throwable(String message, Throwable cause) {
fillInStackTrace();
detailMessage = message;
this
.cause = cause;
}
public
Throwable(Throwable cause) {
fillInStackTrace();
detailMessage = (cause==
null
?
null
: cause.toString());
this
.cause = cause;
}
//........
}
|
下面是一个例子,演示了异常的链化:从命令行输入2个int,将他们相加,输出。输入的数不是int,则致使getInputNumbers异常,从而致使add函数异常,则能够在add函数中抛出
一个链化的异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
public
static
void
main(String[] args)
{
System.out.println(
"请输入2个加数"
);
int
result;
try
{
result = add();
System.out.println(
"结果:"
+result);
}
catch
(Exception e){
e.printStackTrace();
}
}
//获取输入的2个整数返回
private
static
List<Integer> getInputNumbers()
{
List<Integer> nums =
new
ArrayList<>();
Scanner scan =
new
Scanner(System.in);
try
{
int
num1 = scan.nextInt();
int
num2 = scan.nextInt();
nums.add(
new
Integer(num1));
nums.add(
new
Integer(num2));
}
catch
(InputMismatchException immExp){
throw
immExp;
}
finally
{
scan.close();
}
return
nums;
}
//执行加法计算
private
static
int
add()
throws
Exception
{
int
result;
try
{
List<Integer> nums =getInputNumbers();
result = nums.get(
0
) + nums.get(
1
);
}
catch
(InputMismatchException immExp){
throw
new
Exception(
"计算失败"
,immExp);
/////////////////////////////链化:以一个异常对象为参数构造新的异常对象。
}
return
result;
}
/*
请输入2个加数
r 1
java.lang.Exception: 计算失败
at practise.ExceptionTest.add(ExceptionTest.java:53)
at practise.ExceptionTest.main(ExceptionTest.java:18)
Caused by: java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
at practise.ExceptionTest.add(ExceptionTest.java:48)
... 1 more
*/
|
自定义异常
若是要自定义异常类,则扩展Exception类便可,所以这样的自定义异常都属于检查异常(checked exception)。若是要自定义非检查异常,则扩展自RuntimeException。
按照国际惯例,自定义的异常应该老是包含以下的构造函数:
- 一个无参构造函数
- 一个带有String参数的构造函数,并传递给父类的构造函数。
- 一个带有String参数和Throwable参数,并都传递给父类构造函数
- 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
下面是IOException类的完整源代码,能够借鉴。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public
class
IOException
extends
Exception
{
static
final
long
serialVersionUID = 7818375828146090155L;
public
IOException()
{
super
();
}
public
IOException(String message)
{
super
(message);
}
public
IOException(String message, Throwable cause)
{
super
(message, cause);
}
public
IOException(Throwable cause)
{
super
(cause);
}
}
|
异常的注意事项
一、当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。
例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。
至于为何?我想,也许下面的例子能够说明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
class
Father
{
public
void
start()
throws
IOException
{
throw
new
IOException();
}
}
class
Son
extends
Father
{
public
void
start()
throws
Exception
{
throw
new
SQLException();
}
}
/**********************假设上面的代码是容许的(实质是错误的)***********************/
class
Test
{
public
static
void
main(String[] args)
{
Father[] objs =
new
Father[
2
];
objs[
0
] =
new
Father();
objs[
1
] =
new
Son();
for
(Father obj:objs)
{
//由于Son类抛出的实质是SQLException,而IOException没法处理它。
//那么这里的try。。catch就不能处理Son中的异常。
//多态就不能实现了。
try
{
obj.start();
}
catch
(IOException)
{
//处理IOException
}
}
}
}
|
二、Java程序能够是多线程的。每个线程都是一个独立的执行流,独立的函数调用栈。若是程序只有一个线程,那么没有被任何代码处理的异常 会致使程序终止。若是是多线程的,那么没有被任何代码处理的异常仅仅会致使异常所在的线程结束。
也就是说,Java中的异常是线程独立的,线程的问题应该由线程本身来解决,而不要委托到外部,也不会直接影响到其它线程的执行。
finally块和return
首先一个不容易理解的事实:在 try块中即使有return,break,continue等改变执行流的语句,finally也会执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
static
void
main(String[] args)
{
int
re = bar();
System.out.println(re);
}
private
static
int
bar()
{
try
{
return
5
;
}
finally
{
System.out.println(
"finally"
);
}
}
/*输出:
finally
*/
|
不少人面对这个问题时,老是在概括执行的顺序和规律,不过我以为仍是很难理解。我本身总结了一个方法。用以下GIF图说明。
也就是说:try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。
finally中的return 会覆盖 try 或者catch中的返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
public
static
void
main(String[] args)
{
int
result;
result = foo();
System.out.println(result);
/////////2
result = bar();
System.out.println(result);
/////////2
}
@SuppressWarnings
(
"finally"
)
public
static
int
foo()
{
trz{
int
a =
5
/
0
;
}
catch
(Exception e){
return
1
;
}
finally
{
return
2
;
}
}
@SuppressWarnings
(
"finally"
)
public
static
int
bar()
{
try
{
return
1
;
}
finally
{
return
2
;
}
}
|
finally中的return会抑制(消灭)前面try或者catch块中的异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
class
TestException
{
public
static
void
main(String[] args)
{
int
result;
try
{
result = foo();
System.out.println(result);
//输出100
}
catch
(Exception e){
System.out.println(e.getMessage());
//没有捕获到异常
}
try
{
result = bar();
System.out.println(result);
//输出100
}
catch
(Exception e){
System.out.println(e.getMessage());
//没有捕获到异常
}
}
//catch中的异常被抑制
@SuppressWarnings
(
"finally"
)
public
static
int
foo()
throws
Exception
{
try
{
int
a =
5
/
0
;
return
1
;
}
catch
(ArithmeticException amExp) {
throw
new
Exception(
"我将被忽略,由于下面的finally中使用了return"
);
}
finally
{
return
100
;
}
}
//try中的异常被抑制
@SuppressWarnings
(
"finally"
)
public
static
int
bar()
throws
Exception
{
try
{
int
a =
5
/
0
;
return
1
;
}
finally
{
return
100
;
}
}
}
|
finally中的异常会覆盖(消灭)前面try或者catch中的异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
class
TestException
{
public
static
void
main(String[] args)
{
int
result;
try
{
result = foo();
}
catch
(Exception e){
System.out.println(e.getMessage());
//输出:我是finaly中的Exception
}
try
{
result = bar();
}
catch
(Exception e){
System.out.println(e.getMessage());
//输出:我是finaly中的Exception
}
}
//catch中的异常被抑制
@SuppressWarnings
(
"finally"
)
public
static
int
foo()
throws
Exception
{
try
{
int
a =
5
/
0
;
return
1
;
}
catch
(ArithmeticException amExp) {
throw
new
Exception(
"我将被忽略,由于下面的finally中抛出了新的异常"
);
}
finally
{
throw
new
Exception(
"我是finaly中的Exception"
);
}
}
//try中的异常被抑制
@SuppressWarnings
(
"finally"
)
public
static
int
bar()
throws
Exception
{
try
{
int
a =
5
/
0
;
return
1
;
}
finally
{
throw
new
Exception(
"我是finaly中的Exception"
);
}
}
}
|
上面的3个例子都异于常人的编码思惟,所以我建议:
- 不要在fianlly中使用return。
- 不要在finally中抛出异常。
- 减轻finally的任务,不要在finally中作一些其它的事情,finally块仅仅用来释放资源是最合适的。
- 将尽可能将全部的return写在函数的最后面,而不是try … catch … finally中。