最近看了effect java这本书,今天分享一篇书中关于异常的章节,正好我们组内也到我分享的轮次了,准备分享这个章节知识,顺便也就发出来大家一起探讨一下,花了两天整理了这篇文章,技术有限,自己也是理解了之后写出来的,知识点就这些,网上相关文章也有很多,但是我看有些是错误的,这些都是我自己写过例子,运行了之后下的结论,当然还有一些更深的知识,希望大家一起讨论
第57条 :只针对异常的情况下才使用异常
代码1
try {
int i = 0;
while (true) {
arr[i] = 0;
i++;
}
} catch (IndexOutOfBoundsException e) {
}
代码2
for (int i = 0; i < arr.length; i++) {
arr[i] = 0;
}
两份代码的作用都是遍历arr数组,并设置数组中每一个元素的值为0。代码1的是通过异常来终止,看起来非常难懂,代码2是通过数组边界来终止。我们应该避免使用代码1这种方式,主要原因有三点:
- 异常机制的设计初衷是用于不正常的情况,所以很少会会JVM实现试图对它们的性能进行优化。所以,创建、抛出和捕获异常的开销是很昂贵的
- 把代码放在try-catch中返回阻止了JVM实现本来可能要执行的某些特定的优化。
- 对数组进行遍历的标准模式并不会导致冗余的检查,有些现代的JVM实现会将它们优化掉。
实际上,基于异常的模式比标准模式要慢得多。测试代码如下:
public class Advice1 {
private static int[] arr = new int[]{1,2,3,4,5};
private static int SIZE = 10000;
public static void main(String[] args) {
long s1 = System.currentTimeMillis();
for (int i=0; i<SIZE; i++)
endByRange(arr);
long e1 = System.currentTimeMillis();
Log.e("range:", "endByRange time:" + (e1 - s1) + "ms");
long s2 = System.currentTimeMillis();
for (int i=0; i<SIZE; i++)
endByException(arr);
long e2 = System.currentTimeMillis();
Log.e("range:","endByException time:" + (e2 - s2) + "ms");
}
// 遍历arr数组: 通过异常的方式
private static void endByException(int[] arr) {
try {
int i=0;
while (true) {
arr[i]=0;
i++;
}
} catch (IndexOutOfBoundsException e) {
}
}
// 遍历arr数组: 通过边界的方式
private static void endByRange(int[] arr) {
for (int i=0; i<arr.length; i++) {
arr[i]=0;
}
}
}
结果说明:通过异常遍历的速度比普通方式遍历数组慢很多!
第58条 :对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常
首先我们看看异常的层次结构图:
java语言提供了三种可抛的结构:受检的异常(checked exception)、运行时异常(unchecked exception)、错误(error)。
异常的两种类型:
- 运行时异常(非检查时异常):RuntimeException及其子类都属于运行时异常,常见的有IllegalArgumentException、IllegalStateException、NullPointerException、IndexOutOfBoundsException等等
- 受检的异常(检查时异常):Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常
- 它们的区别是:Java编译器会对"被检查的异常"进行检查,而对"运行时异常"不会检查。
也就是说,对于被检查的异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。而对于运行时异常,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。当然,虽说Java编译器不会检查运行时异常,但是,我们同样可以通过throws对该异常进行说明,或通过try-catch进行捕获。
第59条 避免不必要的使用检查时异常
被检查的异常"是Java语言的一个很好的特性。与返回代码不同,"被检查的异常"会强迫程序员处理例外的条件,大大提高了程序的可靠性。
但是,过分使用被检查异常会使API用起来非常不方便。如果一个方法抛出一个或多个被检查的异常,那么调用该方法的代码则必须在一个或多个catch语句块中处理这些异常,或者必须通过throws声明抛出这些异常。 无论是通过catch处理,还是通过throws声明抛出,都给程序员添加了不可忽略的负担。
适用于"被检查的异常"必须同时满足两个条件:
第一,即使正确使用API并不能阻止异常条件的发生。
第二,一旦产生了异常,使用API的程序员可以采取有用的动作对程序进行处理。
第60条 优先使用标准的异常
代码重用是值得提倡的,这是一条通用规则,异常也不例外。重用现有的异常有几个好处:
第一,它使得你的API更加易于学习和使用,因为它与程序员原来已经熟悉的习惯用法是一致的。
第二,对于用到这些API的程序而言,它们的可读性更好,因为它们不会充斥着程序员不熟悉的异常。
第三,异常类越少,意味着内存占用越小,并且转载这些类的时间开销也越小。
所以,应该尽量使用标准的异常,而不是轻易地使用自造的异常。
利用类库中现有的异常是被提倡的。但有一个重要的原则,就是你使用的场景一定要复合这个异常在文档中所描述的条件。
第61条 抛出的异常要适合于相应的抽象
如果一个方法抛出的异常与它执行的任务没有明显的关联关系,这种情形会让人不知所措。当一个方法传递一个由低层抽象抛出的异常时,往往会发生这种情况。这种情况发生时,不仅让人困惑,而且也"污染"了高层API。
为了避免这个问题,高层实现应该捕获低层的异常,同时抛出一个可以按照高层抽象进行介绍的异常。这种做法被称为"异常转译(exception translation)"。
例如,在Java的集合框架AbstractSequentialList的get()方法如下(基于JDK1.7.0_40):
public E get(int index) {
try {
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
listIterator(index)会返回ListIterator对象,调用该对象的next()方法可能会抛出NoSuchElementException异常。而在get()方法中,抛出NoSuchElementException异常会让人感到困惑。所以,get()对NoSuchElementException进行了捕获,并抛出了IndexOutOfBoundsException异常。即,相当于将NoSuchElementException转译成了IndexOutOfBoundsException异常。
第62条 每个方法抛出的异常都要有文档
要单独的声明被检查的异常,并且利用Javadoc的@throws标记,准确地记录下每个异常被抛出的条件。
如果一个类中的许多方法处于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的。
第63条 在细节消息中包含失败 -- 捕获消息
简而言之,当我们自定义异常或者抛出异常时,应该包含失败相关的信息。
当一个程序由于一个未被捕获的异常而失败的时候,系统会自动打印出该异常的栈轨迹。在栈轨迹中包含该异常的字符串表示。典型情况下它包含该异常类的类名,以及紧随其后的细节消息。
异常的细节信息应当包含所有“对异常有贡献”的参数值和域的值。如,ArrayIndexOutOfBoundsException异常细节应当包含下界、上界以及没有落在界内的下标值。这可以极大地加速诊断过程。
/**
* @hide
*/
public ArrayIndexOutOfBoundsException(int sourceLength, int index) {
super("length=" + sourceLength + "; index=" + index);
}
第64条 努力使失败保持原子性
当一个对象抛出一个异常之后,我们总期望这个对象仍然保持在一种定义良好的可用状态之中。对于被检查的异常而言,这尤为重要,因为调用者通常期望从被检查的异常中恢复过来。
一般而言,一个失败的方法调用应该保持使对象保持在"它在被调用之前的状态"。具有这种属性的方法被称为具有"失败原子性(failure atomic)"。可以理解为,失败了还保持着原子性。对象保持"失败原子性"的方式有几种:
- 设计一个非可变对象。
- 对于在可变对象上执行操作的方法,获得"失败原子性"的最常见方法是,在执行操作之前检查参数的有效性。如下(Stack.java中的pop方法):
public Object pop() {
if (size==0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
- 与上一种方法类似,可以对计算处理过程调整顺序,使得任何可能会失败的计算部分都发生在对象状态被修改之前。
- 编写一段恢复代码,由它来解释操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。
- 在对象的一份临时拷贝上执行操作,当操作完成之后再把临时拷贝中的结果复制给原来的对象。
虽然"保持对象的失败原子性"是期望目标,但它并不总是可以做得到。例如,如果多个线程企图在没有适当的同步机制的情况下,并发的访问一个对象,那么该对象就有可能被留在不一致的状态中。
即使在可以实现"失败原子性"的场合,它也不是总被期望的。对于某些操作,它会显著的增加开销或者复杂性。
总的规则是:作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态,如果这条规则被违反,则API文档中应该清楚的指明对象将会处于什么样的状态。
第65条 不要忽略异常
当一个API的设计者声明一个方法会抛出某个异常的时候,他们正在试图说明某些事情。所以,请不要忽略它!忽略异常的代码如下:
try {
...
} catch (SomeException e) {
}
空的catch块会使异常达不到应有的目的,异常的目的是强迫你处理不正常的条件。忽略一个异常,就如同忽略一个火警信号一样 -- 若把火警信号器关闭了,那么当真正的火灾发生时,就没有人看到火警信号了。所以,至少catch块应该包含一条说明,用来解释为什么忽略这个异常是合适的。
扩展一:异常限制
子类重写父类方法的时候,如何确定异常抛出声明的类型。下面是三点原则:
- 父类的方法没有声明异常,子类在重写该方法的时候不能声明异常;
- 如果父类的方法声明一个异常exception1,则子类在重写该方法的时候声明的异常不能是exception1的父类;
- 如果父类的方法声明的异常类型是检查时异常,那么子类在重写父类方法后可以抛出或者不抛出除父类异常以外的非检查时异常
注:父类的构造器抛出有异常的时候,子类的构造器必须抛出同样的异常,并且不能是其子类,但是子类构造器可以抛出额外的异常,这是因为异常限制只是作用于方法的覆盖,子类的构造器并没有覆盖父类的构造器,所以可以抛出额外的异常类型
原因:异常限制的基础在于继承和Java中方法的后期绑定(正是由于Java中方法的后期绑定带来了多态性),其根本原因在于为了防止当程序中发生向上转型时可能带来的异常处理错误从而导致的程序的失灵和崩溃
代码解释:
class Exception1 extends Exception{}
class Exception2 extends Exception{}
class Exception3 extends Exception{}
class A {
public A()throws Exception1{}
public void fun() throws Exception2 {
}
public void say() throws Exception3{}
}
class B extends A implements Work{
//子类构造器可以抛出额外的异常类型
public B() throws Exception1 ,ClassCastException {
}
//覆盖方法时,只能抛出父类同样的异常或者其子类,还可以额外抛出运行时异常,接口同理
@Override
public void fun() throws Exception2, RuntimeException{
}
//异常限制 -- 接口
@Override
public void work() throws IOException{
}
//当接口和父类都有改方法的时候取交集
@Override
public void say() {
}
}
//原因:因为java的多态,这样限制是为了在向上转型的时候,程序不会出错
class C {
A a;
{
try {
a = new B();
g(a);
} catch (Exception1 exception1) {
exception1.printStackTrace();
}
}
void g(A a){
try {
a.fun();
}catch (Exception2 e){
e.printStackTrace();
}
}
}
public interface Work {
void work() throws Exception;
void say();
}
classC中对象a调用的fun方法不再是A类中的fun方法,而是调用的B的fun方法,因此如果子类B在覆盖fun方法的时候抛出了基类A没有的异常的时候,catch代码块就捕获不到,继而造成程序出错
扩展二 重新获取异常和异常链
我们捕获异常以后两种操作:
- 捕获后抛出原来的异常,希望保留最新的异常抛出点--fillStackTrace
- 捕获后抛出新的异常,希望抛出完整的异常链--initCause
public void f()throws Exception{
throw new Exception("Exception: f()");
}
public void g() throws Exception{
try{
f();
}catch(Exception e){
throw e;
}
}
main函数中调用
try{
g();
}
catch(Exception e){
for (StackTraceElement s : e.getStackTrace()){
Log.e("range:", "stackTrace:"+ s);
}
e.printStackTrace();
}
由此可见,在不做任何处理的情况下,最新抛出异常点是在f函数中,而不是g函数,在日常业务中,调用f函数的地方会有很多,这样排查起来不方便,所以我们应该定位在最新抛出点
fillStackTrace——覆盖前边的异常抛出点(获取最新的异常抛出点)
public void g() throws Exception{
try{
f();
}catch(Exception e){
throw (Exception) e.fillInStackTrace();
}
}
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
backtrace = nativeFillInStackTrace();
stackTrace = libcore.util.EmptyArray.STACK_TRACE_ELEMENT;
}
return this;
}
在抛出之前调用exception的fillInStackTrace()方法,看源码可知,这个方法就是返回了一个Throwable对象,因此它的栈轨迹也随之更新,运行结果如下:
可以看到最新抛出点已经变成了g函数了。
捕获异常后抛出新的异常(保留原来的异常信息,区别于捕获异常之后重新抛出)
如果我们在抛出异常的时候需要保留原来的异常信息,那么有两种方式
方式1:Exception e=new Exception(); e.initCause(ex);
方式2:Exception e =new Exception(ex);
public void k() throws Exception{
try{
q();
}catch(NullPointerException ex){
//方式1
Exception e = new Exception("k -- Exception");
//将原始的异常信息保留下来
e.initCause(ex);//1
//方式2
// Exception e = new Exception("k -- Exception", ex);
throw e;
}
}
public void q() throws NullPointerException{
throw new NullPointerException("q -- NullPointerException");
}
main函数中调用:
try {
k();
} catch (Exception e) {
Log.e("range:", "getMessage():" + e.getMessage() + ".......getCause():" + e.getCause());
e.printStackTrace();
}
注释掉1处的运行结果:
q函数的空指针被覆盖了,且造成k函数exception的原因也无从查询
放开1处的e.initCause(ex)后,运行结果:
扩展三 finally相关问题
1,除了以下两种情况,finally一定执行
- 首先得运行到try代码块里面,如果在这之前就return了是不会执行的
- try块或者catch块中调用了退出虚拟机的方法,即System.exit();
2,清理未创建的资源。并不是所有的清理操作都应该在finally中,有以下情况需要注意:
public void test() {
try {
in = new BufferedReader(new FileReader());
String s = in.readLine();
} catch (FileNotFoundException e) {
} catch (Exception e) {
try {
in.close();
} catch (IOException e2) {
System.out.println("in class false");
}
} finally {
//in.close();
}
}
这个例子可以看到如果new FileReader抛出了FileNotFoundException,那么in是不会被创建的,如果此时还在finally中执行in.close()那么自然是行不同的。但如果抛出了IOExceptin异常,那么说明in成功创建但在readLine时发生错,所以在catch中进行close时in肯定已经被创建。这种情形资源的释放应该放到catch中。
3,导致异常丢失 (在finally中使用return和抛出异常)
public static int throwException () throws Exception {
try {
throw new Exception("throwException");
} catch (Exception e) {
throw e;
} finally {
// return 1;
// throw new NullPointerException("NullPointerException");
}
}
try {
Log.e("range:", "throwException():"+ throwException());
} catch (Exception e) {
Log.e("range:", "Exception():"+ e.getMessage());
e.printStackTrace();
}
注释掉的运行结果:
使用return的运行结果:
抛出异常的运行结果:
4,break/continue/return:finally也会执行,值得注意的是,即使在try块中正常执行了return,finally也在return之前执行
public int testException_finally(){
int x;
try {
x = 1;
// 1 int y = 1/0;
// 2 String str = null;
// 3 str.substring(0);
return x;
} catch (ArithmeticException e) {
x =2;
return x;
} finally {
x = 3;
}
}
如上图,注释掉1,2,3处运行结果是:1
只放开注释1运行结果是:2
只放开注释2,3的运行结果是:无返回结果,向上抛出空指针异常
但是无论以上哪种情况下,finally模块都会被执行
结论:没有异常的情况下正常返回1,当发生除0异常的时候返回2,当发生未catch的异常的时候没有返回值,抛出该异常,那为什么finally中 x = 3没有生效呢,首先我们得先知道执行顺序是try --> return的值确定好但是不会及时返回 --> 执行finally模块 --> return值