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.让库类和程序更安全