Java的基本理念是,结构不佳的代码不能运行。
- 异常
异常情形是指阻止当前方法或作用域继续执行的问题
- 抛出异常
在堆上创建异常对象,当前的执行路径被终止,并从当前环境中弹出对异常对象的引用,异常处理机制接管程序,并运行异常处理
程序,它的作用是将程序从错误状态中恢复,以使程序要么换一种方式运行,要么继续运行下去
- 异常参数
异常有两个构造器,一个是默认构造器,一个是接受字符串作为参数,以便能把相关信息放入异常对象的构造器
在使用new创建了异常对象之后,此对象的引用将传递给throw
可有简单地把异常处理看成一种返回机制
能够抛出任意类异常的类Throwable对象,它是异常类型的根类,通常,对于不同类型的异常,要抛出相应的异常,
这可以是通过异常类名称或者异常对象内部来表示
- 捕获异常
监控区域是一段可能产生异常的代码,并且后面跟着处理这些异常的代码
try块就是这样的一个监控区
- try块
如果直接在方法里抛出异常,那这个方法会在抛出异常的过程中结束,在方法中加入这个try块,可以在这个try块中尝试各种可能产
生异常的操作,以便捕获异常
- 异常处理程序
抛出的异常必须在某处处理,这个地方就是异常处理程序
try {
} catch(Type1 id1) {
} catch(Type2 id2) {
} catch(Type3 id3) {
}
异常处理程序必须紧跟在try后面,当异常被抛出时,异常处理机制将搜索参数与异常类型相匹配的第一个处理程序
创建自定义异常
可以自己定义异常类,但必须继承自已有的异常类
class MyException extends Exception {
}
public static void f() throws MyException {}
try {
f();
} catch(MyException e) {
print(e);
}
像这样写一个抛出异常的方法,在try块中执行,就会抛出异常,在catch块中捕获并输出异常
把异常输出到标准错误流
用System.err把错误发送给标准错误流
catch(MyException e) {
e.printStackTrace(System.out);
}
调用了在Throwable类中声明(Exception类从此类继承)的printStackTrace()方法
它会打印,从方法调用出直到异常抛出处的方法调用序列
这里信息被发送到标准输出流System.out
如果是没有任何参数时,将被发送到System.err标准错误流
- 异常与记录日志
使用java.util.logging类,将输出记录到日志中
private static Logger logger = Logger.getLogger("LoggingException");
public LoggingException() {
StringWriter trace = new StringWriter();
printStackTrace(new PrintWriter(trace));
logger.severe(trace.toString());
}
Logger.getLogger()产生了一个String参数相关的Logger对象,它会将其输出发送到System.err
调用与日志的消息级别相关联的方法severe
为了让printStackkTrace产生字符串,将一个StringWriter对象传送给PrintWriter的构造器,
通过调用toString()方法,为输出抽取一个String
getMessage()方法,在Throwable中类似于toString()的方法,默认是返回String构造器中的String参数
- 异常说明
告知客户端程序员某个方法可能会抛出的异常类型,它属于方法声明的一部分,紧跟在形式参数列表的后面
void f() throws Ex1, Ex2 {}
使用throws声明要抛出的异常列表
捕获所有异常
catch(Exception e) {}
这是所有异常的基类,所以这条一般写在捕获异常的最后,以防捕获了其他类型的异常
它可以使用从它基类Throwable继承的方法
String getMessage();
String getLocalizedMessage();
获取详细信息,或用本地语言表示的详细信息
void printStackTrace();
void printStackTrace(PrintStream);
void printStackTrace(java.io.PrintWriter);
打印Throwable和它的调用栈轨迹,后两个版本允许选择要输出到的流
Throwable fillStackTrace();
用于在Throwable对象内部记录栈帧的当前状态
栈轨迹
getStackTrace()返回一个由轨迹栈中的元素所构成的数组,其中每一个元素都代表栈中的一帧,元素0是栈顶元素
数组中的最后一个元素和栈底是序列中的第一个方法调用
for(StackTraceElement ste : e.getStackTrace())
print(ste.getMethodName());
- 重新抛出异常
catch(Exception e) {
throw e;
}
重抛异常会把异常抛给上一级的异常处理程序
同一个try后面的catch子句将会被忽略
如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的是原来异常抛出点的调用栈信息
可以调用fillInStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立
调用fillInStackTrace()的地方成了异常的新发生地了
也可以在捕获异常之后抛出一个新的异常,这样原来异常的栈轨迹就丢失了,就能获得类似fillInStackTrace的效果
- 异常链
想要在捕获一个异常后抛出一个异常,并把原始异常的信息记录下来,这被称作异常链
所有的Throwable子类都可以接受一个cause对象作为构造器参数,用来表示原始异常
但是只有三种异常类提供了带cause参数的构造器,它们是Error,Exception,RuntimeException,如果要把其他类型的异常链接起
来,要使用initCause()方法
DynamicFieldsException dfe = new DynamicFieldsException();
dfe.initCause(new NullPointerException());
输出会包括链接上的错误Caused by: ...
- Java标准异常
Throwable有两种基本类型
Error用来表示编译和系统错误
Exception则用来表示一切可以被抛出的错误
异常除了名称外基本没什么差别
异常的基本概念是用名称代表发生的问题,并且可以望文生义
特例:RuntimeException
if(t == null)
throw new NoPointerException();
像这种检查是否用了空引用的异常,并不需要自己抛出和捕获,因为它们继承自运行时异常,会被Java虚拟机自动抛出
它们也被称为不受检查异常,这种异常属于错误,将被自动捕获
如果RuntimeException异常没有被捕获,而直达了main,那么在程序退出前将运行它的printStackTrace()方法
RuntimeException代表的是编程错误
1)无法预料的错误,如传递进来的null引用
2)作为程序员,应该在代码中检查的错误
finally
try {
} catch(Type1 t) {
} catch(Type2 t) {
} finally {
}
无论try中的异常是否抛出,finally中的代码都会执行
当要把除了内存之外的资源恢复到它们的初始状态时,就要用到finally子句
即使try中的异常没有被异常处理程序捕获,其finally子句也会在返回外层try之前被执行
try {
try {}
finally {
这个也会执行哦
}
} catch(Type e) {
} finally {
}
在return中使用finally
即使在try中return了,并不会影响finally的执行,所以在一个方法中,可以多点返回
try {
if(i == 1) return;
if(i == 2) return;
} finally {
依旧会执行
}
- 异常丢失
try {
try {
throw new A();
} finally {
throw new B();
}
} catch(Exception e) {
这里就只能捕获到B,捕获不到A了 因为抛出B的时候A还没有被处理,就被B覆盖了
}
在finally中抛出异常,而try中抛出的异常还没有被异常处理程序处理就被finally中的异常覆盖了
try {
throw new RuntimeException();
} finally {
return;
}
这里的这个return也会导致没有任何输出就结束程序
- 异常的限制
在基类方法的异常说明里声明的那些异常,到了其派生类中依然有效,这些方法只能抛出说明中声明过的异常类型
如果一个类实现了一个基类并扩展了一个接口,而两者里面都包含一个相同的方法,那么接口里的异常限制将不起作用,不能覆盖
在基类中的异常限制
异常限制对于构造器是不起作用的,构造器中仍可以抛出任意类型的异常
当基类中声明的方法中没规定异常限制时,不能在派生类继承的方法中声明异常限制,这是因为这样一来,可以绕过派生类的方法
直接调用基类的方法而不需要考虑对异常的处理
而基类如果规定了异常限制,派生类则可以不加异常限制,因为即使基类会抛出异常,也不会影响已有的程序
还有,如果处理的是派生类的对象,编译器只会强制要求你捕获这个对象类所抛出的异常,而如果你把它向上转型成基类的话,它
就会要求你捕获基类的异常
构造器
对于在构造阶段可能会抛出的异常,最安全的方法是使用嵌套的try子句
try {
A a = new A();
try {
} catch(Exception e) {
} finally() {
dispose();
}
} catch(Exception e) {
print("construction failed");
}
把对象的构造过程放在一个try中,如果构造失败,抛出异常,就会直接进入外层的catch中,如果构造成功,就可以进入内层的try
块中,可以把清理对象用的dispose()方法放在内层的finally中
对于构造不能失败的对象,只要些try-finally语句就可以,多个不能失败构造的对象可以群组处理
然而对于可以构造失败的对象,对于每一个类,都需要对构造语句进行try-catch处理,如果有多种可以构造失败的对象,就需要进
行try-catch的嵌套
- 异常匹配
异常处理系统会按照代码的书写顺序找出最近的处理程序,找到匹配的程序后,它就认为异常得到了处理,不再继续查找
派生类的对象也可以被基类的处理程序捕获
try {
throw new Child();
} catch(Parent p) {
}
如果后面再加上catch(Child c)就会报错,因为这个时候这个异常处理程序一定会被基类的异常处理程序所屏蔽,它是无效的,永远
得不到执行
- 其他可选方式
只有在你知道如何处理异常的时候才去捕获异常
异常处理的一个重要目标是把错误处理的代码和错误发生的地点进行分离
- 异常使用指南
1.在恰当的级别处理问题(在知道该如何处理的情况才捕获异常)
2.解决问题并且重新调用产生异常的方法
3.进行少许修补,然后绕过异常发生的地方继续执行
4.用别的数据进行计算,以代替方法预计会返回的值
5.把当前运行环境下能做的事情尽量做完,然后把相同的异常抛到更高层
6.把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层
7.终止程序
8.进行简化
9.让类库和程序更安全
“报告”功能是Java异常处理的精髓所在