Java编程思想笔记——通过异常处理错误2

Java标准异常

异常的基本概念是用名称代表发生的问题,并且异常的名称应该可以望文知意。异常并非全是在java.lang包里定义的,有些异常是用来支持util、net、io这样的程序包,这些异常可以通过他们完整的名称或从他们的父类中看出端倪。比如,所有的输出/输入异常都是从java.io.IOException继承来的。

RuntimeException

NullPointerException:如果对null引用进行调用,Java会自动抛出NullpointerException异常。
属于运行时异常的类型有很多,他们会自动被Java虚拟机抛出,所以不必在异常说明中把它们列出来。这些异常都是从RuntimeException类继承来的。

public class NeverCaught {
    static void f(){
        throw new RuntimeException("From f()");
    }
    static void g(){
        f();
    }

    public static void main(String[] args) {
        g();
    }
}/*
Exception in thread "main" java.lang.RuntimeException: From f()
    at thinking2.NeverCaught.f(NeverCaught.java:5)
    at thinking2.NeverCaught.g(NeverCaught.java:8)
    at thinking2.NeverCaught.main(NeverCaught.java:12)
*/

RuntimeException是一个特例,对于这种异常类型,编译器不需要异常说明,其输出被报告给了System.err,所有的RuntimeException没有被捕获而直达main(),那么程序退出前将调用异常的printStackTrace()方法。
只能在代码中忽略RuntimeException及其子类的异常,其他类型异常的处理都有编译器强制实施的。究其原因,RuntimeException代表的是编译错误:
1.无法预料的错误
2.做为程序员,应该在代码中进行检查的错误。

使用finally进行处理

finally子句:无论try块中的异常是否抛出,它们都能得到执行。这通常适用于内存回收之外的情况。

public class ThreeException extends Exception {
}
public class FinallyWorks {
    static int count = 0;

    public static void main(String[] args) {
        while (true){
            try {
                if(count++ == 0){
                    throw new ThreeException();
                }
                System.out.println("No exception");
            }catch (ThreeException e){
                System.out.println("ThreeException");
            }finally {
                System.out.println("In finally clause");
                if(count == 2){
                    break;
                }
            }
        }
    }
}/*
ThreeException
In finally clause
No exception
In finally clause
*/

无论异常是否被抛出,finally子句总能被执行。
如果把try放在循环里,就建立了一个程序继续执行之前必须要达到的条件,以达到破除java的异常不允许我们回到异常抛出点时的设定。还可以加一个static的计数器,使循环在放弃之前能尝试一定的次数。

finally用来做什么

当要把除内存之外的资源恢复到它们的初始状态时(包括已经打开的文件或网络链接等),使用finally子句。

public class Switch {
    private boolean state = false;
    public boolean read(){
        return state;
    }
    public void on(){
        state = true;
        System.out.println(this);
    }
    public void off(){
        state = false;
        System.out.println(this);
    }
    @Override
    public String toString(){
        return state ? "on" : "off";
    }
}
public class OnOffException1 extends Exception {
}
public class OnOffException2 extends Exception {
}
public class OnOffSwitch {
    private static Switch sw = new Switch();
    public static void f() throws OnOffException1,OnOffException2{}

    public static void main(String[] args) {
        try{
            sw.on();
            f();
            sw.off();
        } catch (OnOffException1 e) {
            System.out.println("OnOffException1");
            sw.off();
        } catch (OnOffException2 e) {
            System.out.println("OnOffException2");
            sw.off();
        }
    }
}/*
on
off
*/

程序的目的是main结束的时候开关必须是关闭的,所以在每个异常处理程序都加入一个off的调用

public class WithFinally {
    static Switch sw = new Switch();

    public static void main(String[] args) {
        try{
            sw.on();
            OnOffSwitch.f();
        } catch (OnOffException2 onOffException2) {
            System.out.println("OnOffException2");
        } catch (OnOffException1 onOffException1) {
            System.out.println("OnOffException1");
        } finally {
            sw.off();
        }
    }
}/*
on
off
*/

这里sw.off()被移到一处,并且保证在任何情况下都能得到执行。
甚至在异常没有被当前的异常处理程序捕获的情况下,异常处理机制也会在跳到更高一层的异常处理程序之前,执行finally子句:

public class FourException extends Exception {
}
public class AlwaysFinally {
    public static void main(String[] args) {
        System.out.println("Entering first try block");
        try{
            System.out.println("Entering second try block");
            try{
                throw new FourException();
            } finally {
                System.out.println("finally in 2nd try block");
            }
        } catch (FourException e) {
            System.out.println("Caught FourException in 1st try block");
        } finally {
            System.out.println("finally in 1st try block");
        }
    }
}/*
Entering first try block
Entering second try block
finally in 2nd try block
Caught FourException in 1st try block
finally in 1st try block
*/

当涉及break和continue语句的时候,finally子句也会得到执行。请注意,如果把finally子句和带标签的break及continue配合使用,java里就没要使用goto语句了。

在return中使用finally

因为finally子句总是会执行的,所以在一个方法中,可以从多个点返回,并且可以保证重要的清理工作仍旧会执行:

public class MultipleReturns {
    public static void f(int i){
        System.out.println("Initialization that requires cleanup");
        try{
            System.out.println("Point 1");
            if(i == 1) {return;}
            System.out.println("Point 2");
            if(i == 2) {return;}
            System.out.println("Point 3");
            if(i == 3) {return;}
            System.out.println("End");
        } finally {
            System.out.println("Perforing cleanup");
        }
    }

    public static void main(String[] args) {
        for(int i = 1; i <= 4; i++){
            f(i);
        }
    }
}/*
Initialization that requires cleanup
Point 1
Perforing cleanup
Initialization that requires cleanup
Point 1
Point 2
Perforing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
Perforing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
End
Perforing cleanup
*/

缺憾:异常丢失

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

public class VeryImportantException extends Exception {
    @Override
    public String toString() {
        return "A very important exception";
    }
}
public class HoHumException extends Exception {
    @Override
    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();
            } finally {
                lm.dispose();
            }
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}/*
A trivial exception
*/

从输出中可以看到,VeryImportException不见了,它被finally子句里的HoHumException所取代。这是相当严重的缺陷。
一种更加简单的丢失异常的方法是从finally子句中返回:

public class ExceptionSilencer {
    public static void main(String[] args) {
        try{
            throw new RuntimeException();
        } finally {
            return;
        }
    }
}

这个程序即使抛出了异常,也不会有任何输出。

异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,当基类使用的代码应用到其派生类对象的时候,一样能工作,异常也不例外。

public class BaseballException extends Exception {
}
public class Foul extends BaseballException {
}
public class Strike extends BaseballException {
}
public abstract class Inning {
    public Inning() throws BaseballException{}
    public void event() throws BaseballException{

    }
    public abstract void atBat() throws Strike, Foul;
    public void walk(){}
}
public class StormException extends Exception {
}
public class RainedOut extends Exception {
}
public class PopFoul extends Foul {
}
public interface Storm {
    void event() throws RainedOut;
    void rainHard() throws RainedOut;
}

public class StormyInning extends Inning implements Storm {
    public StormyInning() throws RainedOut, BaseballException{}
    public StormyInning(String s) throws Foul, BaseballException{}
    //! void walk() throws PopFoul{}
    //public void event() throws RainedOut {}
    @Override
    public void rainHard() throws RainedOut {}

    @Override
    public void event(){}

    @Override
    public void atBat() throws PopFoul {}

    public static void main(String[] args) {
        try {
            StormyInning si = new StormyInning();
            si.atBat();
        } catch (PopFoul e) {
            System.out.println("Pop foul");
        } catch (RainedOut e) {
            System.out.println("Rained out");
        } catch (BaseballException e) {
            System.out.println("Generic baseball exception");
        }

        try {
            Inning i = new StormyInning();
            i.atBat();
        } catch (Strike e) {
            System.out.println("Strike");
        } catch (Foul e) {
            System.out.println("Foul");
        } catch (RainedOut e) {
            System.out.println("Rained out");
        } catch (BaseballException e) {
            System.out.println("Generic baseball exception");
        }
    }
}

接口Storm值得注意,因为它包含了一个在Inning中定义的方法event()和一个不在Inning中定义的方法rainHard()。这两个方法都抛出了新的异常RainedOut。如果StormmyInning类在扩展Inning类的同时又实现了Storm接口,那么Storm里的event方法就不能改变在Inning中的event方法的异常接口。否则,在使用基类的时候就不能判断是否捕获了正确的异常。当然,如果接口里定义的方法不是来自基类,比如rainHard,那么方法抛出什么样的异常都没有问题。
异常限制对构造器不起作用。然而,因为基类构造器必须以这样或那样的方式被调用,派生类构造器的异常说明必须包含基类构造器的异常说明。
派生类构造器不能捕获基类构造器抛出的异常。
Inning.walk并没有声明异常。
覆盖后的event方法表明,派生类方法可以不抛出任何异常,即使它是基类所定义的异常。同样这是因为,假使基类的方法会抛出异常,这样做也不会破坏已有的程序。

构造器

finally会每次都执行清理代码。如果构造器在其执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在finally子句中却是要被清理的。

public class InputFile {
    private BufferedReader in;
    public InputFile(String fname) throws Exception {
        try {
            in = new BufferedReader(new FileReader(fname));
        } catch (FileNotFoundException e) {
            System.out.println("Could not open " + fname);
            throw e;
        } catch (Exception e) {
            try {
                in.close();
            } catch (IOException e2) {
                System.out.println("in.close() unsuccessful");
            }
            throw e;
        } finally {

        }
    }

    public String getLine() {
        String s;
        try {
            s = in.readLine();
        } catch (IOException e) {
            throw new RuntimeException("readLine() failed");
        }
        return s;
    }

    public void dispose() {
        try {
            in.close();
            System.out.println("dispose() successful");
        } catch (IOException e2) {
            throw new RuntimeException("in.close() failed");
        }
    }
}

如果FileReader的构造器失败了,将抛出FileNotFoundException异常。对于这个异常,并不需要关闭文件,因为这个文件还没打开。而任何其他捕获异常的catch子句必须关闭文件,因为在它们捕获到异常之时,文件已经打开了。close也会抛出异常,所以尽管他已经在另一个catch子句块里,还是要再用一层try-catch。
由于finally会在每次完成构造器之后都执行一遍,因此它实在不该是调用close关闭文件的地方,我们希望文件在InputFile对象的整个生命周期内都处于打开状态。
getLine方法调用了能抛出异常的readLine,但是这个异常已经在方法内得到处理,因此getLine不会抛出异常。在这里,getLine方法将异常转换为RuntimeException,表示一个编程错误。
用户在不需要InputFile对象时,就必须调用dispose方法,这将释放BufferedReader和/或FileReader对象多占用的系统资源。
对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方法是使用嵌套try子句:

public class Cleanup {
    public static void main(String[] args) {
        try {
            InputFile in = new InputFile("E:\\IDEAFile\\untitled\\src\\thinking2\\Cleanup.java");
            try {
                String s;
                int i = 1;
                while ((s = in.getLine()) != null) {

                }
            } catch (Exception e) {
                System.out.println("Caught Exception in main");
            } finally {
                in.dispose();
            }
        } catch (Exception e) {
            System.out.println("InputFile construction failed");
        }
    }
}/*
dispose() successful
*/

对InputFile对象的构造在其自己的try语句块中有效,如果构造失败,将进入外部catch子句,而dispose不会被调用。但是,如果构造成功,我们肯定想确保对象能够被清理,因此在构造后立即创建一个新的try语句块。执行清理的finally与内部的try语句块相关联。在这种方式中,finally子句在构造失败时是不会执行的,而在构成成功时总是执行。
在创建需要清理的对象之后,立即进入一个try-catch语句块:

public class NeedsCleanup {
    private static long counter = 1;
    private final long id = counter++;
    public void dispose() {
        System.out.println("NeedsCleanup " + id + " disposed");
    }
}
public class ConstructionException extends Exception {
}
public 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 {
            nc2.dispose();
            nc3.dispose();
        }

        // Section 3
        try {
            NeedsCleanup2 nc4 = new NeedsCleanup2();
            try {
                NeedsCleanup2 nc5 = new NeedsCleanup2();
                try {
                    // ...
                } finally {
                    nc5.dispose();
                }
            } catch (ConstructionException e) {
                System.out.println(e);
            } finally {
                nc4.dispose();
            }
        } catch (ConstructionException e) {
            System.out.println(e);
        }
    }
}/*
NeedsCleanup 1 disposed
NeedsCleanup 2 disposed
NeedsCleanup 3 disposed
NeedsCleanup 5 disposed
NeedsCleanup 4 disposed
*/

Section2中,为了构造和清理,可以看到具有不能失败的构造器的对象可以群组在一起。
Section3展示了如何处理那些具有可以失败的构造器,且需要清理的对象。对于每一个构造,都必须包含在其自己的try-finally语句块中,并且每一个对象构造必须都跟随一个try-finally语句块以确保清理。

异常匹配

抛出异常的时候,异常处理系统会按照代码的书写顺序找出最近的处理程序。找到匹配的处理程序之后,它就认为异常得到了处理,然后就不再继续查找。
查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序:

public class Annoyance extends Exception {
}
public class Sneeze extends Annoyance {
}
public class Human {
    public static void main(String[] args) {
        try {
            throw new Sneeze();
        } catch (Sneeze sneeze) {
            System.out.println("Caught Sneeze");
        } catch (Annoyance e) {
            System.out.println("Caught Annoyance");
        }

        try {
            throw new Sneeze();
        } catch (Annoyance e) {
            System.out.println("Caught Annoyance");
        }
    }
}/*
Caught Sneeze
Caught Annoyance
*/

如果把捕获基类的catch子句放在最前面,以此先把派生类的异常全给屏蔽掉:

try {
            throw new Sneeze();
        } catch (Annoyance e) {
            System.out.println("Caught Annoyance");
        } catch (Sneeze sneeze) {
            System.out.println("Caught Sneeze");
        }

这样编译器就会发现Sneeze的catch子句永远也得不到执行,因此他会向你报告错误。

其他可选方式

把错误处理的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。
“被检查的异常”(在编译时被强制检查的异常)强制在可能还没准备还处理错误的时候被迫加上catch子句,这就导致了吞食则有害(harmful if swallowed)的问题:程序员常常是无意中“吞食”了异常。

把异常传递给控制台

最简单而又不用写多少代码就能保护异常信息的方法,就能保护异常信息的方法,就是把他们从mian()传递到控制台:

public class MainException {
    public static void main(String[] args) throws Exception {
        FileInputStream file = new FileInputStream("MainException.java");
        file.close();
    }
}

通过把Exception传递到控制台,就不必在mian里写try-catch子句了。

把“被检查的异常”转换为“不检查的异常”

当一个普通方法里调用别的方法时,要考虑到不知道该怎样处理这个异常,但是也不想把它吞了,或者打印一些无用的消息,可以直接把被检查的异常包装进RuntimeException里面:

public class WrapCheckedException {
    void throwRuntimeException(int type) {
        try {
            switch (type) {
                case 0 : throw new FileNotFoundException();
                case 1 : throw new IOException();
                case 2 : throw new RuntimeException("where am I?");
                default : return;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
public class SomeOtherException extends Exception {
}

public class TurnOffChecking {
    public static void main(String[] args) {
        WrapCheckedException wce = new WrapCheckedException();
        wce.throwRuntimeException(3);
        for (int i = 0; i < 4; i++) {
            try {
                if(i < 3){
                    wce.throwRuntimeException(i);
                }else {
                    throw new SomeOtherException();
                }
            } catch (SomeOtherException e) {
                System.out.println("SomeOtherException: " + e);
            } catch (RuntimeException re) {
                try {
                    throw re.getCause();
                } catch (FileNotFoundException e) {
                    System.out.println("FileNotFoundException: " + e);
                } catch (IOException e) {
                    System.out.println("IOException: " + e);
                } catch (Throwable e) {
                    System.out.println("Throwable: " + e);
                }
            }
        }
    }
}/*
FileNotFoundException: java.io.FileNotFoundException
IOException: java.io.IOException
Throwable: java.lang.RuntimeException: where am I?
SomeOtherException: thinking2.SomeOtherException
*/

WrapCheckedException .throwRuntimeException()的代码可以生成不同类型的异常。这些异常被捕获并包装进了RuntimeException对象,所以它们成了这些运行时异常的cause了。
在TurnOffChecking 里,可以不用try块就调用throwRuntimeException,因为它没有抛出被检查的异常。但是当你准备好去捕获异常的时候,还是可以用try块来捕获任何异常的,比如SomeOtherException,RuntimeException放在最后捕获。然后把getCause的结果(也就是被包装的那个原始异常)抛出来,这样就把原先的那个异常给提取出来了,然后就可以用他们自己的catch子句处理。

异常使用指南

应该在下列情况下使用异常:
1.在恰当的级别处理问题(在知道如何处理的情况下才捕获异常)
2.解决问题并重新调用产生异常的方法
3.进行少许修补,然后绕过异常发生的地方继续执行
4.用别的数据进行计算,以替换方法预计会返回的值
5.把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层
6.把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层
7.终止程序
8.进行简化(如果你的异常模式使问题变得太复杂,那就用起来非常痛苦)
9.让库类和程序更安全

猜你喜欢

转载自blog.csdn.net/wszcy199503/article/details/79997681