Java 中的异常和处理详解
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
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主要做一些清理工作,如流的关闭,数据库连接的关闭等。 } |
需要注意的地方
1、try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
2、每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。
3、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块中清理释放这些资源。
需要注意的地方:
1、finally块没有处理异常的能力。处理异常的只能是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
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); } } |
异常的注意事项
1、当子类重写父类的带有 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 } } } } |
2、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中。
一, 什么是java里的异常
1.1 c语言里的错误
.
- #include <stdio.h>
- int f(int a, int b){
- return a/b;
- }
- int main(){
- int i = f(8,0);
- printf("i is %d\n",i);
- return 0;
- }
1.2 java里运行时出现的错误
- package Exception_kng;
- class Exp1{
- public int f(int a, int b){
- return a/b;
- }
- }
- public class Expt_1{
- public static void g(){
- Exp1 e = new Exp1();
- int i = e.f(8,0);
- System.out.printf("i is %d\n", i);
- }
- }
运行时一样会出错, 下面是出错信息:
- [java] Caused by: java.lang.ArithmeticException: / by zero
- [java] at Exception_kng.Exp1.f(Expt_1.java:5)
- [java] at Exception_kng.Expt_1.g(Expt_1.java:12)
- [java] at Enter_1.main(Enter_1.java:31)
但是可以见到, java告诉你出错的类型: 运算错误(ArithmeticExcetion), 出错信息和出错的类与文件行数输出, 方便你调试. jvm虚拟机是会对错误作出一定的处理的.
1.3 java异常的定义
二, java里的异常的分类.
xxxxxx ArithmeticException
三, java里对异常的处理
3.1 程序猿对有可能出现的异常使用try catch处理.
- package Exception_kng;
- class Exp2{
- public int f(int a, int b){
- int i = 0;
- try{
- i = a/b;
- }
- catch(Exception e){
- System.out.printf("Exception occurs!!\n");
- System.out.println(e.getMessage()); //print the root cause
- System.out.printf("===========================\n");
- e.printStackTrace(); //print the info of function stuck.
- }
- return i;
- }
- }
- public class Expt_2{
- public static void g(){
- Exp2 ex = new Exp2();
- int i = ex.f(8,0); //call f()
- System.out.printf("i is %d\n", i); //successfully executed
- }
- }
- [java] Exception occurs!!
- [java] / by zero
- [java] ===========================
- [java] java.lang.ArithmeticException: / by zero
- [java] at Exception_kng.Exp2.f(Expt_2.java:7)
- [java] at Exception_kng.Expt_2.g(Expt_2.java:23)
- [java] at Enter_1.main(Enter_1.java:31)
- [java] i is 0
3.2 函数里并不处理异常, 使用throw or throws 关键字 把可能出现的异常抛给调用该函数的上级函数处理.
- package Exception_kng;
- class Exp3{
- public int f(int a, int b){
- if (0 == b){
- throw new ArithmeticException("Shit !!! / by zero!");
- }
- return a/b;
- }
- }
- public class Expt_3{
- public static void g() throws ArithmeticException{
- Exp3 ex = new Exp3();
- int i = 22;
- i = ex.f(8,0); //throw excetpion
- System.out.printf("i is %d\n", i); //failed executed
- System.out.printf("g() is done!!\n"); //failed executed
- }
- public static void h(){
- try{
- g();
- }catch(ArithmeticException e){
- System.out.printf("Exception occurs!!\n");
- System.out.println(e.getMessage()); //print the root cause
- System.out.printf("===========================\n");
- e.printStackTrace(); //print the info of function stuck.
- }
- System.out.printf("h() is done!!\n"); //successfully executed
- }
- }
- [java] Exception occurs!!
- [java] Shit !!! / by zero!
- [java] ===========================
- [java] java.lang.ArithmeticException: Shit !!! / by zero!
- [java] at Exception_kng.Exp3.f(Expt_3.java:6)
- [java] at Exception_kng.Expt_3.g(Expt_3.java:18)
- [java] at Exception_kng.Expt_3.h(Expt_3.java:25)
- [java] at Enter_1.main(Enter_1.java:31)
- [java] h() is done!!
注意这个程序没有执行g() 最后的代码.
3.3 交给jvm虚拟机处理
实际上, 当java程序的1个函数抛出异常时,
首先会检查当前函数有没有try catch处理, 如果无检查上一级函数有无try..catch处理....
这样在函数栈里一级一级向上检查, 如果直至main函数都无try..catch, 则抛给jvm..
四,Try catch finally 的处理机制.
try{
...
4.1 try catch finally的执行路线.
下面用个例子来说明:
- try{
- f();
- ff();
- }
- catch(ArithmeticException e){
- g();
- }
- catch(IOException e){
- gg();
- }
- catch(AuthorizedException e){
- ggg();
- }
- finally{
- h();
- }
- k();
4.1.1 当try里面的f()抛出了IOException
当f()抛出了异常, 那么ff()就不会执行了. 程序会尝试捕捉异常.
首先捕捉ArithmeticException, 捕捉失败.
接下来捕捉IOException, 捕捉成功, 执行gg();
一旦捕捉到一个异常, 不会再尝试捕捉其他异常, 直接执行finally里的h();
执行后面的函数k().
也就是说路线是:
f() -> gg() -> h() -> k()
有2点要注意的.
1. f()函数极有可能未完整执行, 因为它抛出了异常, 抛出异常的语句执行失败, 之后的语句放弃执行.
2. try{} 里面, f()之后的语句, 例如ff()放弃执行.
4.1.2 没有任何异常抛出
这种情况很简单, 就是try{}里面的代码被完整执行, 因为没有抛出任何异常, 就不会尝试执行catch里的部分, 直接到finally部分了.
路线是:
f() -> ff() -> h() -> k()
4.2 如何确定要捕捉的异常名字.
也许有人会问, 我们怎么知道到底会抛出什么异常?
下面有3个解决方案.
1.看代码凭经验, 例如看到1段除法的代码, 则有可能抛出算术异常.
2.在catch的括号里写上Exception e, 毕竟Exception 是所有其他异常的超类, 这里涉及多态的知识, 至于什么是多态可以看看本人的另一篇文章.
3. 观察被调用函数的函数定义, 如果有throws后缀, 则可以尝试捕捉throws 后缀抛出的异常
4.3 为什么需要finally
包括我在内很多人会觉得finally语句简直多勾余, 既然是否捕捉到异常都会执行, 上面那个例子里的h()为什么不跟下面的k() 写在一起呢.
上面的例子的确看不出区别.
但下面两种情况下就体现了finally独特的重要性.
4.3.1 抛出了1个异常, 但是没有被任何catch子句捕捉成功.
例如try里面抛出了1个A异常, 但是只有后面只有捕捉B异常, 和C异常的子句.
这种情况下, 程序直接执行finally{}里的子句, 然后中断当前函数, 把异常抛给上一级函数, 所以当前函数finally后面的语句不会被执行.
例子:
- package Exception_kng;
- import java.net.*;
- import java.io.*;
- class Exp4{
- public int f(int a, int b) throws IOException, BindException{
- return a/b;
- }
- }
- public class Expt_4{
- public static void g(){
- Exp4 ex = new Exp4();
- int i = 22;
- try{
- System.out.printf("g() : try!!\n"); //failed
- i = ex.f(8,0); //call f()
- }
- catch(BindException e){
- System.out.printf("g() : BindException!!\n"); //failed
- }
- catch(IOException e){
- System.out.printf("g() : IOException!!\n"); //failed
- }
- finally{
- System.out.printf("g() : finaly!!\n"); //successfully executed
- }
- System.out.printf("g() is done!!\n"); //failed
- }
- public static void h(){
- try{
- g();
- }catch(ArithmeticException e){
- System.out.printf("Exception occurs!!\n");
- System.out.println(e.getMessage()); //print the root cause
- System.out.printf("===========================\n");
- e.printStackTrace(); //print the info of function stuck.
- }
- System.out.printf("h() is done!!\n"); //successfully executed
- }
- }
我所说的情况, 就在上面例子里的g()函数, g()函数里尝试捕捉两个异常, 但是抛出了第3个异常(ArithmeticException 算术异常).
所以这个异常会中断g()的执行, 因为没有被捕捉到, 然后抛给调用g()的 h()函数处理, 而在h()捕捉到了, 所以h()函数是能完整执行的.
也就是说g()里的
- System.out.printf("g() is done!!\n"); //failed
执行失败
而h()里的
- System.out.printf("h() is done!!\n"); //successfully executed
执行成功
但是无论如何, g()里的finally{}部分还是被执行了
执行结果如下:
- [java] g() : try!!
- [java] g() : finaly!!
- [java] Exception occurs!!
- [java] / by zero
- [java] ===========================
- [java] java.lang.ArithmeticException: / by zero
- [java] at Exception_kng.Exp4.f(Expt_4.java:8)
- [java] at Exception_kng.Expt_4.g(Expt_4.java:18)
- [java] at Exception_kng.Expt_4.h(Expt_4.java:34)
- [java] at Enter_1.main(Enter_1.java:31)
- [java] h() is done!!
这种情况是1中编程的低级错误, 在项目中是不允许出现.
避免方法也十分简单, 在catch子句集的最后增加1个catch(Exception e)就ok, 因为Exception是所有异常的超类, 只要有异常抛出, 则肯定会捕捉到.
4.3.2 在catch子句内有return子句.
下面例子:
- try{
- f();
- ff();
- }
- catch(ArithException e){
- g();
- return j();
- }
- catch(IOException e){
- gg();
- return j();
- }
- catch(AuthorizedException e){
- ggg();
- return j();
- }
- finally{
- h();
- }
- k();
假如在f()函数抛出了IOExcepion 异常被捕捉到.
那么执行路线就是
f() -> gg() -> j() -> h() -> 上一级function
也就说, 这种情况下finally里的子句会在return回上一级function前执行. 而后面的k()就被放弃了.
4.3.3 finally作用小结.
可以看出, finally里的语句, 无论如何都会被执行.
至有两种情况除外, 一是断电, 二是exit函数.
在项目中, 我们一般在finally编写一些释放资源的动作, 例如初始化公共变量. 关闭connections, 关闭文件等.
4.4 try catch finally里一些要注意的问题.
4.4.1 无论如何最多只有1个catch被执行
这个上面提到过了, 一旦捕捉到1个异常, 就不会尝试捕捉其他异常.
如果try里面的一段代码可能抛出3种异常A B C,
首先看它先抛出哪个异常, 如果先抛出A, 如果捕捉到A, 那么就执行catch(A)里的代码. 然后finally.. B和C就没有机会再抛出了.
如果捕捉不到A, 就执行finally{}里的语句后中断当前函数, 抛给上一级函数...(应该避免)
4.4.2 有可能所有catch都没有被执行
两种情况, 1就是没有异常抛出, 另一种就是抛出了异常但是没有捕捉不到(应该避免)
4.4.3 先捕捉子类异常, 再捕捉父类异常, 否则编译失败
加入try 里面尝试捕捉两个异常, 1个是A, 1个是B, 但是A是B的父类.
这种情况下, 应该把catch(B)写在catch(A)前面.
原因也很简单, 加入把catch(A)写在前面, 因为多态的存在, 即使抛出了B异常, 也会被catch(A)捕捉, 后面的catch(B)就没有意义了.
也就是说如果捕捉Exception这个异常基类, 应该放在最后的catch里, 项目中也强烈建议这么做, 可以避免上述4.3.1的情况出现.
4.4.4 catch与catch之间不能有任何代码.
这个没什么好说的. 语法规则
4.4.5 finally里不能访问catch里捕捉的异常对象e
每1个异常对象只能由catch它的catch子句里访问.
4.4.6 try里面的定义变量不能在try外面使用.
跟if类似, 不多说了.
4.4.7 try catch finally可以嵌套使用.
这个也不难理解..
五, throw 和throws的机制和用法.
下面开始详讲异常另一种处理方法throw 和 throws了.
注意的是, 这两种用法都没有真正的处理异常, 真正处理的异常方法只有try catch, 这两种方法只是交给上一级方法处理.
就如一个组织里 , 有1个大佬, 1个党主, 1个小弟.
大佬叫党主干活, 堂主叫小弟干活, 然后小弟碰上麻烦了, 但是小弟不会处理这个麻烦, 只能中断工作抛给党主处理, 然后堂主发现这个麻烦只有大佬能处理, 然后抛给大佬处理..
道理是相通的..
5.1 throw 的语法与作用
throws的语法很简单.
语法:
throw new XException();
其中xException必须是Exception的派生类.
这里注意throw 出的是1个异常对象, 所以new不能省略
作用就是手动令程序抛出1个异常对象.
5.2 throw 1个 RuntimeException及其派生类
我们看回上面3.2 的例子:
- public int f(int a, int b){
- if (0 == b){
- throw new ArithmeticException("Shit !!! / by zero!");
- }
- return a/b;
- }
5.2.1 throw会中断当前函数, 当前函数执行失败(不完整)
当这个函数的if 判断了b=0时, 就利用throws手动抛出了1个异常. 这个异常会中断这个函数. 也就是说f()执行不完整, 是没有返回值的.
5.2.2, 接下来哪个调用这个函数就会在调用这个函数的语句上收到异常.
- public void g(){
- int i;
- h();
- i = f(); //recevie excepton
- k();
- }
例如上没的g()函数, 在调用f() 会收到1个异常.
这时g()函数有三种选择.
1. 不做任何处理
这时, g()收到f()里抛出的异常就会打断g()执行, 也就是说g()里面的k(); 被放弃了, 然后程序会继续把这个函数抛给调用g()函数.
然后一级一级寻求处理, 如果都不处理, 则抛给jvm处理. jvm会中断程序, 输出异常信息. 这个上没提到过了.
2. 使用try catch处理
如果catch成功, 则g()函数能完整执行, 而且这个异常不会继续向上抛.
如果catch失败(尽量避免), 则跟情况1相同.
5.3 throw 1个 非RuntimeException派生类的异常
将上面的例子改一下:
- public int f(int a, int b){
- if (0 == b){
- throw new IOException("Shit !!! / by zero!");
- }
- return a/b;
- }
例如, 我不想抛出ArithmeticException, 我想抛出IOExcetpion.
注意 这里, IOException虽然逻辑上是错误的(完全不是IO的问题嘛), 但是在程序中完全可行, 因为程序猿可以根据需要控制程序指定抛出任何1个异常.
但是这段代码编译失败, 因为IOException 不是 RuntimeException的派生类.
java规定:
5.3.1 如果一个方法里利用throw手动抛出1个非RuntimeException异常, 必须在函数定义声明里加上throws 后缀
改成这样就正确了:
- public int f(int a, int b) throws IOException{
- if (0 == b){
- throw new IOException("Shit !!! / by zero!");
- }
- return a/b;
- }
注意在方法定义里加上了throws子句. 告诉调用它的函数我可能抛出这个异常.
5.3.2 调用该方法的方法则必须处理这个异常
例如抄回上面的例子, g()调用f()函数.
- public void g(){
- int i;
- h();
- i = f(); //recevie excepton
- k()
- }
但是编译失败.
因为f()利用throws 声明了会抛出1个非runtimeExcetpion. 这时g()必须做出处理.
处理方法有两种:
1. try catch自己处理:
- public void g(){
- int i = 0;
- h();
- try{
- i = f(); //recevie excepton
- }
- catch(IOException e){
- }
- k();
- }
需要注意的是, catch里面要么写上throws对应的异常(这里是 IOException), 要么写上这个异常的超类, 否则还是编译失败.
2.g()利用throws 往上一级方法抛
.
- public void g() throws IOException{
- int i = 0;
- h();
- i = f(); //recevie excepton
- k();
- }
这是调用g()的函数也要考虑上面的这两种处理方法了...
但是最终上级的方法(main 方法)还是不处理的话, 就编译失败, 上面说过了, 非runtimeException无法抛给jvm处理.
虽然这两种处理方法都能通过编译, 但是运行效果是完全不同的.
第一种, g()能完整执行.
第二种, g()被中断, 也就是g()里面的k(); 执行失败.
5.4 throws 的语法.
throws稍微比throw难理解点:
语法是:
public void f() throws Exception1, Exception2...{
}
也就是讲, thorws可以加上多个异常, 注意这里抛出的不是对象, 不能加上new.
而且不是告诉别人这个函数有可能抛出这么多个异常. 而是告诉别人, 有可能抛出这些异常的其中一种.
5.5 throws 的作用.
如果为f()函数加上throws后续, 则告诉调用f()的方法, f()函数有可能抛出这些异常的一种.
如果f()throws 了1个或若干个非RuntimeException, 则调用f()的函数必须处理这些非RuntimeException, 如上面的g()函数一样.
如果f() throws的都是RuntimeException, 则调用f()的函数可以不处理, 也能通过编译, 但是实际上还是强烈建议处理它们.
实际上, 如果1个方法f() throws A,B
那么它有可能不抛出任何异常.(程序运行状态良好)
也有能抛出C异常(应该避免, 最好在throws上加上C)
5.6 什么时候应该用throws
5.6.1 一个函数体里面手动throw了1个RumtimeException, 则这个函数的定义必须加上throws子句
这个是强制, 告诉别人这个函数内有炸弹.
5.6.2 一个函数内有可能由系统抛出异常.
这个是非强制的, 但是如果你知道一个函数内的代码有可能抛出异常, 最好还是写上throws 后缀
无论这个异常是否runtimeExcepion.
5.7 一般情况下,调用1个带有throws方法时怎么办
个人建议, 如果你调用1个函数throws A, B, C
那么你就在当前函数写上
try
catch(A)
catch(B)
catch(C)
catch(Exception)
这样能处理能保证你的函数能完整执行, 不会被收到的异常中断.
当然如果你允许你的函数可以被中断, 那么就可以在当前函数定义加上throws A, B 继续抛给上一级的函数.
5.8 重写方法时, throws的范围不能大于超类的对应方法.
例如你在一个派生类重写一个方法f(), 在超类里的f() throws A, B 你重写方法时就不throws出 A,,B,C 或者throws A和B的超类.
原因也是由于多态的存在.
因为1个超类的引用可以指向1个派生类的对象并调用不同的方法. 如果派生类throws的范围加大
那么利用多态写的代码的try catch就不再适用.
六, throw和throws一些主要区别.
面试问得多,单独拉出来写了:
6.1 throw 写在函数体内, throws写在函数定义语句中.
应付面试官.
6.2 throw 是抛出1个异常对象, throws是有能抛出异常的种类
所以throw后面的一般加上new 和exception名字().
而throws后面不能加上new的
6.3 一个方法最多只能throw1个异常, 但是可以throws多个种类异常
因为一旦一个函数throw出1个异常, 这个函数就会被中断执行, 后面的代码被放弃, 如果你尝试在函数内写两个throw, 编译失败.
而throws 是告诉别人这个函数有可能抛出这几种异常的一种. 但是最多只会抛出一种.
6.4 如果在一个函数体内throw 1个非runtimeException, 那么必须在函数定义上加上throws后缀. 但反过来就不是必须的.
原因上面讲过了.
七, 自定义异常.
我们可以自定义异常, 只需要编写1个类, 继承1个异常类就ok
例子:
- package Exception_kng;
- class User_Exception1 extends ArithmeticException{
- public User_Exception1(String Exception_name){
- super(Exception_name);
- }
- public void printStackTrace(){ //overwrite
- super.printStackTrace();
- System.out.printf("hey man, i am an user_defined excetpion\n");
- }
- }
- class Exp6{
- public int f(int a, int b){
- if (0 == b){
- throw new User_Exception1("Shit !!! / by zero!"); //use User_defined exception
- }
- return a/b;
- }
- }
- public class Expt_6{
- public static void g() {
- Exp6 ex = new Exp6();
- int i = 22;
- try{
- i = ex.f(8,0); //throw excetpion
- }catch(User_Exception1 e){
- e.printStackTrace();
- }
- System.out.printf("i is %d\n", i);
- System.out.printf("g() is done!!\n");
- }
- }
上面的类User_Exception1 就是1个自定义异常, 并重写了printStackTrace()方法.
八,java异常的优缺点.
8.1 c语言是如何处理程序错误的.
我们要理解异常的优缺点, 首先看看没有异常的C语言是如何处理错误的.
下面是个例子:
- //openfile
- if (fileOpen() > 0){
- //check the length of the file
- if (gotLengthOfTheFile() > 0){
- //check the memory
- if (gotEnoughMemory() > 0){
- //load file to memory
- if (loadFileToMem() > 0){
- readFile();
- }else{
- errorCode = -5;
- }
- }else{
- errorCode = -5;
- }
- }else{
- errorCode = -5;
- }
- }else{
- errorCode = -5;
- }
- //handle error
- case errorCode....
- //release Source
- releaseSource();
可以见到c语言处理错误有这些特点
1. 大部分精力都在错误处理.
2. 需要把各种可能出现的错误全部考虑到, 才能保证程序的稳定性.
3. 程序可读性差, 错误处理代码混杂在其他代码中.
4. 出错返回信息少, 一旦出错难以调试.
5. 一旦出现了未考虑到的错误, 资源释放代码无法执行.
8.2 java异常机制下是如何编写上述代码的.
- try{
- fileOpen();
- gotLengthOfTheFile();
- gotEnoughMemory();
- loadFileToMem();
- readFile();
- }
- catch(fileOpenFail) { handle1()}
- catch(gotLengthOfTheFileFail) { handle2()}
- catch(gotEnoughMemoryFail) { handle3()}
- catch(loadFileToMemFail) { handle4()}
- catch(readFileFail) { handle4()}
- catch(Exception e) { handle5()} //catch unexpected error
- finally{
- releasSource();
- }
8.3 java异常机制的优点:
由上面的代码可以看出部分优点:
1. 业务代码和错误处理代码分离.
2. 强制程序猿考虑程序的稳定性.
3. 有利于代码调试(异常信息)
4. 即使任何异常产生, 能保证占用的释放(finally)
8.4 java异常机制的缺点:
1. 异常嵌套难免影响代码可读性
2. 并不能令程序逻辑更加清晰.
3. 异常并不能解决所有问题
Java 中的异常和处理详解的更多相关文章
- Java中的异常和处理详解
简介 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常.异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?. ...
- Java 中的异常和处理详解(转载)
原文出处: 代码钢琴家 简介 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常.异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函 ...
- Java中的异常和处理详解(转发:https://www.cnblogs.com/lulipro/p/7504267.html)
简介 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常.异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?. ...
- Java中的IO流系统详解(转载)
摘要: Java 流在处理上分为字符流和字节流.字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符.字符数组或字符串,而字节流处理单元为 1 个字节,操作字节和字节数组. Java ...
- java中vector与hashtable操作详解
众所周知,java中vector与hashtable是线程安全的,主要是java对两者的操作都加上了synchronized,也就是上锁了.因此 在vector与hashtable的操作是不会出现问题 ...
- Java中的IO流系统详解
Java 流在处理上分为字符流和字节流.字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符.字符数组或字符串,而字节流处理单元为 1 个字节,操作字节和字节数组. Java 内用 U ...
- Java中的Calendar日历用法详解
第一部分 Calendar介绍 public abstract class Calendar implements Serializable, Cloneable, Comparable<Cal ...
- Java中23种经典设计模式详解
Java中23种设计模式目录1. 设计模式 31.1 创建型模式 41.1.1 工厂方法 41.1.2 抽象工厂 61.1.3 建造者模式 101.1.4 单态模式 131.1.5 原型模式 151. ...
- Java中的多线程技术全面详解
本文主要从整体上介绍Java中的多线程技术,对于一些重要的基础概念会进行相对详细的介绍,若有叙述不清晰或是不正确的地方,希望大家指出,谢谢大家:) 为什么使用多线程 并发与并行 我们知道,在单核机器上 ...
随机推荐
- Google Map 符号
符号 简介 如果您想在标记上使用基于矢量的图标,或者向多段线添加图像,便可使用符号. 标记支持使用光栅图像以及矢量图像.请参阅有关定制标记图标的文档. Symbol 是一种可显示在 Marker ...
- java提高篇之详解内部类
可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 第一次见面 内部类我们从外面 ...
- Android studio 如何让包有层次显示
Android studio中我新建的包在原来包名后面显示,而我想让包名能层次展示: 方法: 点击如图部分,在弹出框中 去掉 ”compact empty middle package“前面勾
- 可重入函数、线程安全、volatile
一. POSIX 中对可重入和线程安全这两个概念的定义: Reentrant Function:A function whose effect, when called by two or more ...
- 使用用户自定义类型作为map的key
有时候我们想把用户自定义类型作为std::map的键值.方法一)最简单的方法就是实现该自定义类型的<操作符,代码如下:class Foo{public: Foo(int num_) ...
- GO1.6语言学习笔记1-基础篇
一.GO语言优势 可直接编译成机器码,Go编译生成的是一个静态可执行文件,除了glibc外没有其他外部依赖 静态类型语言,但是有动态语言的感觉 语言层面支持并发.Goroutine和channel ...
- Python pow() 函数
描述 pow() 方法返回 xy(x的y次方) 的值. 语法 以下是 math 模块 pow() 方法的语法: import math math.pow( x, y ) 内置的 pow() 方法 po ...
- CheckedComboBoxEditExtension
public static class CheckedComboBoxEditExtension { public static void BindData(this CheckedComboBoxE ...
- Securecrt emacs/vi 代码无法高亮、无颜色
无法高亮: 这是因为.bashrc中没有 export term=linux 最后,代码恢复正常:
- 集群瓶颈:磁盘IO必读
首先需要知道什么是IO: IO是输入输出接口阅读本文章可以带着下面问题1.集群的瓶颈为什么IO?2.你对IO了解多少? 这里面只说个人观点:当我们面临集群作战的时候,我们所希望的是即读即得.可是面对大 ...