java编程思想-异常处理-学习摘要

来源:互联网 发布:赵公明 知乎 编辑:程序博客网 时间:2024/04/28 14:14

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

1、使用new在堆上创建异常对象。

2:、当前的执行路径被终止,并且从当前环境中弹出对异常对象的引用。

3、异常处理机制接管程序,并寻找一个恰当的地方(异常处理程序)来继续处理程序。


* 异常最重要的方面之一是如果发生问题,它们将不允许程序沿着其正常的路径走下去。异常允许我们强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态)强制程序处理问题,并返回到稳定状态。


* 异常参数

标准异常类都有两个构造器:一个是默认构造器,另一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器。

 Java Code 
1
throw new NullPointerException("t = null");

关键字 throw 能够抛出任意类型的Throwable对象,它是异常类型的根类。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。


* 捕获异常

监控区域(guarded region)是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。它是跟在try关键字后的程序块。

异常处理程序紧跟在try块后面,以关键字catch表示。

异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常”。

 Java Code 
1
2
3
4
5
6
7
8
9
try {
// Code that might generate exceptions
catch(Type1 id1)|{
// Handle exceptions of Type1
catch(Type2 id2) {
// Handle exceptions of Type2
catch(Type3 id3) {
// Handle exceptions of Type3
}

编译器强制你在可能还没准备好处理错误的时候被迫加上catch子句,这就导致了吞食则有害(harmful if swallowed)的问题。

 Java Code 
1
2
3
4
5
try {
    
//...
catch (ObligatoryException e) {
    
//catch中忘了进行异常处理。这会导致通过了编译。异常即使发生时,但“吞食”后却消失了
}
 

* 终止与恢复

异常处理有两种基本模型:

1、终止模型:假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。

2、恢复模型:异常处理程序的工作是修正错误,并且重新尝试调用出问题的方法,并认为第二次能成功。

恢复模型看上去很吸引人,但不是很实用。主要原因是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。增加了代码编写和维护的困难。


* 创建自定义异常

自定义异常必须从已有异常类继承。

最简单的方法是让编译器产生默认构造器。也可以自定义构造器。对异常来说最重要的部分就是类名。

 Java Code 
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
//自定义异常:自定义构造器
class MyException extends Exception {
    
public MyException() {}
    
public MyException(String msg) { 
        
super(msg);//super()调用基类构造器
    }
}

//自定义异常:编译器创建默认构造器,自动调用基类的构造器
class MyException2 extends Exception {
}

public class FullConstructors {
    
public static void f() throws MyException {
        System.out.println(
"Throwing MyException from f()");
        
throw new MyException();
    }
    
public static void g() throws MyException {
        System.out.println(
"Throwing MyException from g()");
        
throw new MyException("Originated in g()");
    }
    
public static void main(String[] args) {
        
try {
            f();
        } 
catch(MyException e) {
            e.printStackTrace(System.out);
        }
        
try {
            g();
        } 
catch(MyException e) {
            e.printStackTrace(System.err);
//System.err将错误发送给标准错误流
        }
    }
}


* 异常说明

Java 鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。

异常说明能告知客户端程序员某个方法可能抛出的异常类型,然后客户端程序员就可以进行相应的处理。异常说明属于方法声明的一部分,跟在形式参数列表之后。使用关键字throws

 Java Code 
1
2
3
4
5
6
7
8
9
//该方法表示可能会抛出TooBig、TooSmall、DivZero这些异常
void f() throws TooBig, TooSmall, DivZero {
//....
}

//该方法不会抛出异常,除了从RuntimeException继承的异常,它们可以在没有异常说明的情况下被抛出
void f2() {
//...
}

代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么在异常说明中表明此方法将产生异常。

不过Java允许:声明方法将抛出异常,实际上却不抛出。这样做的好处是,为异常先占个位置,以后可以抛出这种异常而不用修改已有代码。在定义抽象基类和接口时,这种能力很重要。这样派生类或者接口实现可以预先处理这些异常。

这种编译时被强制检查的异常称为被检查的异常

 

* 捕获所有异常

 Java Code 
1
2
3
catch(Exception e) {
    System.out.println(
"捕获一个异常");
}

Exception 可以调用从其基类Throwable继承的方法

String getMessage()

String getLocalizedMessage()
String toString()

来获取详细信息。

void printStackTrace()

void printStackTrace(PrintStream)

void printStackTrace(java.io.PrintWriter)

打印栈轨迹。


Throwable fillInStackTrace()

在Throwable对象的内部记录栈帧的当前状态。


* 栈轨迹

printStackTrace()方法提供的信息可以通过getStackTrace()方法进行访问,这个方法将返回一个栈轨迹中的元素所构成的数组。其中每一个元素都表示栈中的一帧。元素0是栈顶元素,是调用序列中的最后一个方法调用。数组中的最后一个元素是栈底元素,是序列中的第一个方法调用。

 Java Code 
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
public class WhoCalled {
    
static void f() {
        
//Generate an exception to fill in the stack trace
        try {
            
throw new Exception();
        } 
catch (Exception e) {
            
//遍历栈轨迹中的元素
            for(StackTraceElement ste : e.getStackTrace())
                
//这里只打印方法名
                System.out.println(ste.getMethodName());
        }
    }
    
static void g() { f(); }
    
static void h() { g(); }
    
public static void main(String[] args) {
        f();
        System.out.println(
"--------------------------------");
        g();
        System.out.println(
"--------------------------------");
        h();
    }
}
/*
output:

f
main
--------------------------------
f
g
main
--------------------------------
f
g
h
main
*/


* 重新抛出异常:

如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。

 Java Code 
1
2
3
4
5
6
7
8
9
catch(Exception e) {
    System.out.println(
"An exception was thrown");
    
throw e;//重新抛出异常
}
--------------------------
catch(Exception e) {
    System.out.println(
"An exception was thrown");
    
throw (Exception) e.fillInStackTrace();//重新抛出异常,并且更新抛出点的信息
}

有可能捕获异常后抛出另一个异常,得到的效果类似于fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息。

* 异常链

常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始信息的异常保存下来,这被称为异常链

Throwable的子类在构造器中都可以接受一个cause对象作为参数,这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并保存了新的异常也能通过这个异常链追踪到异常最初发生的位置。

有趣的是,在Throwable的子类中只有三种基本的异常类提供了带cause参数的构造器,它们是Error (java虚拟机报告系统错误)、Exception以及RuntimeException.如果要想把其他类型的异常链接起来,应该使用initCause()方法,而不是构造器。

 Java Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object getField(String id, Object value) throws DynamicFieldsException { // DynamicFieldsException 是自定义异常
    if(value == null) {
        
//更多的异常没有“cause”作为参数的构造器,必须使用initCause()方法保存原始异常
        //这里的原始异常是NullPointerException()
        DynamicFieldsException dfe = new DynamicFieldsException();
        
def.initCause(new NullPointerException());
        
throw def;
    }
    
//...


/**
输出可能是:
DynamicFieldsException
at DynamicFields.setField(DynamicFields.java:64)
at DynamicFields.main(DynamicFields.java:94)
Caused by: java.lang.NullPointerException
at DynamicFields.setField(DynamicFields.java:66)
... 1 more
因为加入了initCause的原因,上面的栈轨迹中能追踪到NullPointerException的信息
**/


* Java标准异常

Throwable·这个类被用来表示任何可以作为异常被抛出的类。Throwable对象可分为两种类型(指从Throwable继承而得到的类型):Error用来表示编译时和系统错误;Exception是可以被抛出的基本类型,在Java类库、用户方法以及运行时故障中都可能抛出Exception型异常。所以Java程序员关心的基类通常是Exception。

* 特例:RuntimeException

属于运行时异常的类型很多,它们会自动被Java虚拟机抛出,所以不必在异常说明中把它们列出来。这些异常都是从RuntimeException中继承来的。并且也不需要在异常说明中声明方法将抛出RuntimeException类型的异常(或者任何从RuntimeException继承的异常),它们也被称为“不受检查异常”。这种异常会被自动捕获。

如果RuntimeException(或任何从它继承的异常)没有被捕获而直达main,那么程序退出前将调用异常的printStackTrace()方法。

请务必记住:只能在代码中忽略RuntimeException(极其子类)类型的异常。其他类型的异常处理都有编译器强制实施。RuntimeException代表的是编程错误:

1) 无法预料的错误。

2) 作为程序员,应该检查的错误。


*  使用finally进行清理

对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。为了达到这个效果可以在异常处理程序后面加上finally子句。

无论异常是否抛出,finally子句总是被执行。

 Java Code 
1
2
3
4
5
6
7
8
9
10
11
12
try {
    
// The guarded region: Dangerous activities
    // that might throw A, B, or C
catch(A a1) {
    
// Handler for situation A
catch(B b1) {
    
// Handler for situation B
catch(C c1) {
    
// Handler for situation C
finally {
    
// Activities that happen every time
}

finally用来做什么?答:当要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接。

当涉及到break,continue和return时,finally子句也会执行。


* 缺憾:异常丢失

遗憾的是,Java的异常实现也有瑕疵。异常作为程序出错的标志决不能被忽略,但它还是有可能被轻易地忽略。用某些特殊的方式使用finally子句,就会发生这种情况。

例如以下代码:

 Java Code 
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

class VeryImportantException extends Exception {
    
public String toString() {
        
return "A very important exception!";
    }
}
class HoHumException extends Exception {
    
public String toString() {
        
return "A trivial exception";
    }
}
public class LostMessage {
    
void f() throws VeryImportantException {
        
throw new VeryImportantException();
    }
    
void dispose() throws HoHumException {
        
throw new HoHumException();
    }
    
public static void main(String[] args) {
        
try {
            LostMessage lm = 
new LostMessage();
            
try {
                lm.f();
//f()中拋出了一个重要的异常
            } finally {
                lm.dispose();
//然后执行dispose(),但是该方法中又抛出了另一个 异常
            }
        } 
catch(Exception e) {//只捕获到后面dispose()中抛出的异常,把f()中抛出的异常丢失了,这是一个严重的缺陷
            System.out.println(e);
        }
    }
}
/*
 输出:
 A trivial exception
*/

一种更简单的丢失异常的方式是从finally子句返回:

 Java Code 
1
2
3
4
5
6
7
8
9
10
public class ExceptionSilencer {
    
public static void main(String[] args) {
        
try {
            
throw new RuntimeException();
        }
finally {
            
//在finally字句中使用return会丢失异常
            return;//如果return去掉,在main方法中最后会默认地调用异常的printStackTrace()方法
        }
    }
}


* 异常的限制

请看下面代码的注释部分:

 Java Code 
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
61
62
63
64
class BaseballException extends Exception {}

class Foul extends BaseballException {}
class Strike extends BaseballException {}

abstract class Inning {
    
public Inning() throws BaseballException {}
    
public void event() throws BaseballException {
        
throw new BaseballException();
    }
    
public abstract void atBat() throws Strike, Foul;
    
public void walk() {}
}

class StormException extends Exception {}

class RainedOut extends StormException {}

class PopFoul extends Foul {}

interface Storm {
    
public void event() throws RainedOut;
    
public void rainHard() throws RainedOut;
}

public class StormyInning extends Inning implements Storm {
    
//异常限制对构造器不起作用,StormyInning的构造器可以抛出任何异常,而不必理会基类的构造器(可以发现Inning构造器中没有抛出RainedOut异常)
    //但是基类的构造器中抛出的异常,派生类的构造器中必须抛出(这里必须抛出BaseballException,否则编译器报错)
    public StormyInning() throws RainedOut, BaseballException {
        
    }
    
    
public StormyInning(String s) throws Foul, BaseballException {
        
    }
    
    
//walk() 不能通过编译的原因是:它抛出了异常,而父类中的walk()并没有声明此异常。
    //通过强制派生类遵守基类方法的异常说明,对象的可替换得到保证。
    //! public void walk() throws PopFoul{} //编译错误
    
   
//! public void event() throws RainedOut {}
    
    
//如果这个方法在基类中不存在,那么抛出异常时可以的,但必须与接口中抛出的异常一致
    public void rainHard() throws RainedOut {}
    
    
//覆盖后的方法可以不抛出任何异常,即使在基类或者接口中都抛出了异常
    public void event(){}
    
    
//覆盖后的方法可以抛出继承的异常。基类中抛出的是Foul异常,这边抛出的是PopFoul,PopFoul是Foul的派生类
    public void atBat() throws PopFoul {}
    
    
public static void main(String[] args) {
        
try {
            StormyInning si = 
new StormyInning();
            si.atBat();
        }
catch(Foul e){//捕获atBat抛出的异常。
            System.out.println("catch PopFoul Exception");
        }
catch (RainedOut e) {
            System.out.println(
"catch RainedOut Exception");
        } 
catch (BaseballException e) {//即使上面不捕获PopFoul异常,在这里也能被捕获
            System.out.println("catch BaseballException Exception");
        }
    }
}

 

* 构造器

有一点很重要,即你要时刻询问自己“如果异常发生了,所有东西能被正确地清理吗?”尽管大多数情况下是非常安全的,但涉及构造器时,问题就出现了。构造器会把对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才得以清理。如果在构造器内抛出了异常,这些清理行为也许就不能正常工作了。

 Java Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
//下面逻辑保证了如果构造器产生异常则不执行清理程序,不产生异常时才执行清理程序
try {
    NeedCleanUp n = 
new NeedCleanUp();//假设构造器可能会产生异常,并且对象用完后,需要对资源进行手动清理
    try{//try2 原则:创建需要清理的对象后,紧跟try-finally
        //上面对象构造的过程中,没有发生异常,立即进入一个try-finally块
        //....
    }finally{
        
//执行清理程序
        //n.dispose();
    }
catch (Exception e) {
    
//捕获构造器产生的异常或者try2中产生的异常。
}

一个较详细的程序:

 Java Code 
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
class NeedsCleanup {//对象构造不会失败
    private static long counter = 1;
    
private final long id = counter++;

    
public void dispose() {
        System.out.println(
"NeedsCleanup " + id + " disposed");
    }
}

class ConstructionException extends Exception {
}

class NeedsCleanup2 extends NeedsCleanup {
    
//对象构造可能会失败
    public NeedsCleanup2() throws ConstructionException {
    }
}

public class CleanupIdiom {
    
public static void main(String[] args) {
        
// Section 1:
        NeedsCleanup nc1 = new NeedsCleanup();
        
try {
            
// ...
        } finally {
            nc1.dispose();
        }
        
// Section 2:
        //如果对象构造不会失败,可以放在一起
        NeedsCleanup nc2 = new NeedsCleanup();
        NeedsCleanup nc3 = 
new NeedsCleanup();
        
try {
            
// ...
        } finally {
            nc3.dispose(); 
//与对象构造的顺序相反
            nc2.dispose();
        }
        
// Section 3:
        // 如果构造对象时可能会失败,应该像下面那样做
        try {
            NeedsCleanup2 nc4 = 
new NeedsCleanup2();
            
try {
                NeedsCleanup2 nc5 = 
new NeedsCleanup2();
                
try {
                    
// ...
                } finally {
                    nc5.dispose();
                }
            } 
catch (ConstructionException e) { // nc5 constructor
                System.out.println(e);
            } 
finally {
                nc4.dispose();
            }
        } 
catch (ConstructionException e) { // nc4 constructor
            System.out.println(e);
        }
    }
}

 

* 异常匹配

抛出异常的时候,异常处理系统会按照代码的书写顺序找出最近的处理程序。找到匹配的处理程序后,它就认为异常将得到处理,然后就不再继续寻找。

查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。

 

* 总结

异常处理的优点之一就是它使得你可以在某处集中精力处理你要解决的问题,而在另一处处理的你编写的这段代码中产生的错误。尽管异常处理使得你可以在运行时报告错误并从错误中恢复。但是恢复这样的情况很少可以实现,“报告”功能才是异常的精髓所在!Java坚定地强调将所有的错误将都以异常形式报告的这一事实,这是它远远超过诸如C++这类语言的长处之一。因为C++语言中需要以大量不同的方式来报告错误,你再也不必对所写的每一段代码,都质问自己“错误是否正在成为漏网之鱼?”(只要你没有“吞咽”异常,这是关键所在!)

0 0
原创粉丝点击