您的位置:首页 > 编程语言 > Java开发

Java:如何正确地使用异常详解

2018-03-18 16:53 288 查看
Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为什么“会抛出。
Java异常机制用到的几个关键字:try、catch、finally、throw、throws。
try         用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
catch 用于捕获异常。catch用来捕获try语句块中发生的异常。
finally finally语句块总是会被执行.主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
throw 用于抛出异常,用于方法体中。
throws 用在方法签名中,用于声明该方法可能抛出的异常。

/**
 * 异常的处理方式:
 * 第一种: 使用try...catch语句
 * 第二种: throws
 * 
 * try...catch语句的格式:
 * 完整版:
 * try{
 * 可能会出现异常的代码
 * }catch(异常名 变量名){
 * 对异常的处理方式 ;
 * }finally {
 * 释放资源的代码 ;无论是否捕获到异常都会执行的程序
 * }
 * 
 * 简化版:
 * try{
 * 可能会出现异常的代码
 * }catch(异常名 变量名){
 * 对异常的处理方式 ;
 * }
 *      try出现可以不用catch,有finally也行 
 * 注意事项:
 * (1)try语句中的代码越少越好
 * (2): 需要在catch语句中做处理,哪怕是一条输出语句也是可以的
 */

try…catch…finally语句块
注意: 
finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有四种方法让finally块不执行:System.exit(),代码块发生异常,程序终端,cpu关闭。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。
良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。
需要注意的地方:

1、finally块没有处理异常的能力。处理异常的只能是catch块。
2、在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块,如果catch有返回值  会在返回值之前执行finaylly的代码 如果finally中也有返回值那么会覆盖catch中的返回值。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。
3、在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。http://www.importnew.com/26613.html
下面通过几个示例对这几个关键字进行简单了解。
示例一: 了解try和catch finally基本用法
public static void main(String[] args) {
try {
int i = 10 / 0;
System.out.println( "i=" + i );
} catch (ArithmeticException e) {
System.out.println( "Caught Exception" );
System.out.println( "e.getMessage(): " + e.getMessage() );
System.out.println( "e.toString(): " + e.toString() );
System.out.println( "e.printStackTrace():" );
e.printStackTrace();
} finally {
System.out.println( "run finally" );
}
}运行结果:

Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
at Demo2.main(Demo2.java:6)
run finally
结果说明:最终执行了finally语句块。观察结果我们发现,并没有执行System.out.println("i="+i)。这说明try语句块发生异常之后,try语句块中的剩余内容就不会再被执行了,最终执行finally语句块.
示例二: 了解throws和throw(抛出的是一个对象且只能抛出一个对象)的基本用法
class MyException extends Exception {
public MyException() {}
public MyException(String msg) {
super(msg);
}
}
public static void main(String[] args) {
try {
test();
} catch (MyException e) {
System.out.println( "Catch My Exception" );
e.printStackTrace();
}
}

public static void test() {
try {
int i = 10 / 0;
System.out.println( "i=" + i );
} catch (ArithmeticException e) {
throw new MyException( "This is MyException" ); //如果方法的返回值不为空 那么throw异常或可以不用返回value
}
}
public static void main(String[] args) {
try{
praseDate();//还可以继续往外抛 方法抛出的异常必须处理
}catch(ParseException e){
System.out.println("日期解析错误....");
}
}

//异常的第二种处理方式:	使用throws   throws是用在方法上,作用是用来声明对应的异常.就是我这个方法可能会出现异常, 如果你调用我这个方法
 就需要对这个异常进行处理,如果你不处理,那么你可以继续往外抛 throws,一般是调用者处理.多种异常用英文字符逗号’,’隔开
public static void praseDate() throws ParseException {
// 定义一个日期字符串
String dateStr = "2015-08/10" ;
// 创建日期格式化类
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd") ;
// 对日期字符串进行解析
Date date = sdf.parse(dateStr) ;
// 输出
System.out.println(date);
}
运行结果:
Catch My Exception
MyException: This is MyException
at Demo3.test(Demo3.java:24)
at Demo3.main(Demo3.java:13)
结果说明:MyException是继承于Exception的子类。test()的try语句块中产生ArithmeticException异常(除数为0),并在catch中捕获该异常;接着抛出MyException异常。main()方法对test()中抛出的MyException进行捕获处理。异常的注意事项:
    a: 子类重写父类方法时,子类的方法必须抛出相同的异常或父类异常的子类。(父亲坏了,儿子不能比父亲更坏)
    b:  如果父类抛出了多个异常,子类重写父类时,只能抛出相同的异常或者是他的子集,子类不能抛出父类没有的异常
    c: 如果被重写的方法没有异常抛出,那么子类的方法绝对不可以抛出异常,如果子类方法内有异常发生,那么子类只能try,不能throws

示例三: 了解多个异常的基本用法 private static void method4() {
int a = 20;
int b = 0;
int[] arr = { 23, 14, 56 };
arr = null;
try { // 后面发生异常 后面不走了 直接执行到当前 catch对应的异常
System.out.println( arr[ 5 ] );
System.out.println( a / b );
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组角标越界了....");
} catch (ArithmeticException e) {
System.out.println("除数为0了....");
} catch (Exception e) {
System.out.println("对象为空了....");
}

}结果说明:
        1.如果是多个catch,多个异常是平级的关系,那么不存在先后顺序.如果多个catch中出现了子父类的继承关系,那么父类需要放在最后,放在前面的话能匹配多个异常 后面的异常就没用了 系统会报错 
        2.异常信息如果明确了,那么尽量使用明确的异常信息处理

示例四: 了解多个异常另一种写法的基本用法
private static void method() {
int a = 20;
int b = 0;
int[] arr = { 23, 14, 56 };
try {
System.out.println( a / b );
System.out.println( arr[ 5 ] );
} catch (ArithmeticException | ArrayIndexOutOfBoundsException | NullPointerException e) {
System.out.println( "出问题了...." );
}
}
结果说明: 简化了代码的书写 弊端: 对多个异常的处理方式是一致的,掩盖了编程的错误,会导致程序难以调试

注意事项: 要求多个异常是平级关系,不能存在子父类的继承关系,强烈不推荐使用

示例五: finally块和return

finally中的return 会覆盖 try 或者catch中的返回值。
finally中的return会抑制(消灭)前面try或者catch块中的异常
 
finally中的异常会覆盖(消灭)前面try或者catch中的异常

结论:
不要在fianlly中使用return。
不要在finally中抛出异常,  会导致catch块中发生的真正的异常堆栈信息则丢失了。
减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。
将尽量将所有的return写在函数的最后面,而不是try … catch … finally中。

示例五:自定义异常
Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。
所有异常都必须是 Throwable 的子类。
如果希望写一个检查性异常类,则需要继承 Exception 类。
如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。

在抛出异常时,异常类名往往包含有用的信息,所以在选择抛出异常时需要选择适合的类,从而可以明确的描述该异常情况。这时候就需要我们自己定义异常,自定义异常一定是Throwable的子类,若是检查异常就要继承自Exception,若是运行时异常就要继承自RuntimeException class MyException extends Exception {

public MyException() {}
public MyException(String msg) {
super(msg);
}
}
public static void main(String[] args) {
try {
test();
} catch (MyException e) {
System.out.println( "Catch My Exception" );
e.printStackTrace();
}
}

public static void test() {
try {
int i = 10 / 0;
System.out.println( "i=" + i );
} catch (ArithmeticException e) {
throw new MyException( "This is MyException" ); //如果方法的返回值不为空 那么throw异常或可以不用返回value
}
}

示例六:打印异常信息的三种常用方式
异常中常用的三种方法 继承了Throwable类中的方法
public String getMessage(): 返回此 throwable 的详细消息字符串。
public String toString()返回此 throwable 的简短描述。结果是以下字符串的串联:
此对象的类的 name
": "(冒号和一个空格)
调用此对象 getMessage() 方法的结果
public void printStackTrace(): 将此 throwable 及其追踪输出至标准错误流,输出最全错误信息及所在,推荐使用该方法
Java异常框架
Java异常架构图:



Throwable类是整个Java异常体系的超类,都有的异常类都是派生自这个类。包含Error和Exception两个直接子类。
Error表示程序在运行期间出现了十分严重、不可恢复的错误,在这种情况下应用程序只能中止运行,例如JAVA虚拟机出现错误。在程序中不用捕获Error类型的异常。一般情况下,在程序中也不应该抛出Error类型的异常。
Exception是应用层面上最顶层的异常类,包含RuntimeException(运行时异常)和 Checked Exception(受检异常)。 
RuntimeException是一种Unchecked Exception,即表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。一般来说,RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException。常见的RuntimeException有NullPointException、ClassCastException、IllegalArgumentException、IndexOutOfBoundException等。
Checked Exception是相对于Unchecked Exception而言的,Java中并没有一个名为Checked Exception的类。它是在编程中使用最多的Exception,所有继承自Exception并且不是RuntimeException的异常都是Checked Exception。JAVA 语言规定必须对checked Exception作处理,编译器会对此作检查,要么在方法体中声明抛出checked Exception,要么使用catch语句捕获checked Exception进行处理,不然不能通过编译。常用的Checked Exception有IOException、ClassNotFoundException等。
1、检查性异常(检查性异常): 不处理编译不能通过
2、非检查性异常(运行时异常):不处理编译可以通过,如果有抛出直接抛到控制台

关于异常处理的几条建议第1条: 只针对不正常的情况才使用异常
建议:异常只应该被用于不正常的条件,它们永远不应该被用于正常的控制流。
通过比较下面的两份代码进行说明。
代码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();
System.out.println("endByRange time:"+(e1-s1)+"ms" );

long s2 = System.currentTimeMillis();
for (int i=0; i<SIZE; i++)
endByException(arr);
long e2 = System.currentTimeMillis();
System.out.println("endByException time:"+(e2-s2)+"ms" );
}

// 遍历arr数组: 通过异常的方式
private static void endByException(int[] arr) {
try {
int i=0;
while (true) {
arr[i]=0;
i++;
//System.out.println("endByRange: arr["+i+"]="+arr[i]);
}
} catch (IndexOutOfBoundsException e) {
}
}

// 遍历arr数组: 通过边界的方式
private static void endByRange(int[] arr) {
for (int i=0; i<arr.length; i++) {
arr[i]=0;
//System.out.println("endByException: arr["+i+"]="+arr[i]);
}
}
}
运行结果:
endByRange time:8ms
endByException time:16ms
结果说明:通过异常遍历的速度比普通方式遍历数组慢很多!不要让try块过于庞大
出于省事的目的,很多人习惯于用一个庞大的try块包含所有可能产生异常的代码,
这样有两个坏处:
阅读代码的时候,在try块冗长的代码中,不容易知道到底是哪些代码会抛出哪些异常,不利于代码维护。
使用try捕获异常是以程序执行效率为代价的,将不需要捕获异常的代码包含在try块中,影响了代码执行的效率。
第2条: 对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常
异常说明
运行时异常RuntimeException类及其子类都被称为运行时异常。
被检查的异常Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常
它们的区别是:Java编译器会对"被检查的异常"进行检查,而对"运行时异常"不会检查。也就是说,对于被检查的异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。而对于运行时异常,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。当然,虽说Java编译器不会检查运行时异常,但是,我们同样可以通过throws对该异常进行说明,或通过try-catch进行捕获。
rithmeticException(例如,除数为0),IndexOutOfBoundsException(例如,数组越界)等都属于运行时异常。对于这种异常,我们应该通过修改代码进行避免它的产生。而对于被检查的异常,则可以通过处理让程序恢复运行。例如,假设因为一个用户没有存储足够数量的前,所以他在企图在一个收费电话上进行呼叫就会失败;于是就将一个被检查异常抛出。第3条: 避免不必要的使用被检查的异常
"被检查的异常"是Java语言的一个很好的特性。与返回代码不同,"被检查的异常"会强迫程序员处理例外的条件,大大提高了程序的可靠性。
但是,过分使用被检查异常会使API用起来非常不方便。如果一个方法抛出一个或多个被检查的异常,那么调用该方法的代码则必须在一个或多个catch语句块中处理这些异常,或者必须通过throws声明抛出这些异常。 无论是通过catch处理,还是通过throws声明抛出,都给程序员添加了不可忽略的负担。
适用于"被检查的异常"必须同时满足两个条件:第一,即使正确使用API并不能阻止异常条件的发生。第二,一旦产生了异常,使用API的程序员可以采取有用的动作对程序进行处理。第4条: 尽量使用标准的异常
代码重用是值得提倡的,这是一条通用规则,异常也不例外。重用现有的异常有几个好处:
第一,它使得你的API更加易于学习和使用,因为它与程序员原来已经熟悉的习惯用法是一致的。
第二,对于用到这些API的程序而言,它们的可读性更好,因为它们不会充斥着程序员不熟悉的异常。
第三,异常类越少,意味着内存占用越小,并且转载这些类的时间开销也越小。
Java标准异常中有几个是经常被使用的异常。如下表格:
异常使用场合
IllegalArgumentException参数的值不合适
IllegalStateException参数的状态不合适
NullPointerException在null被禁止的情况下参数值为null
IndexOutOfBoundsException下标越界
ConcurrentModificationException在禁止并发修改的情况下,对象检测到并发修改
UnsupportedOperationException对象不支持客户请求的方法
虽然它们是Java平台库迄今为止最常被重用的异常,但是,在许可的条件下,其它的异常也可以被重用。例如,如果你要实现诸如复数或者矩阵之类的算术对象,那么重用ArithmeticException和NumberFormatException将是非常合适的。如果一个异常满足你的需要,则不要犹豫,使用就可以,不过你一定要确保抛出异常的条件与该异常的文档中描述的条件一致。这种重用必须建立在语义的基础上,而不是名字的基础上!
最后,一定要清楚,选择重用哪一种异常并没有必须遵循的规则。例如,考虑纸牌对象的情形,假设有一个用于发牌操作的方法,它的参数(handSize)是发一手牌的纸牌张数。假设调用者在这个参数中传递的值大于整副牌的剩余张数。那么这种情形既可以被解释为IllegalArgumentException(handSize的值太大),也可以被解释为IllegalStateException(相对客户的请求而言,纸牌对象的纸牌太少)。第5条: 抛出的异常要适合于相应的抽象
如果一个方法抛出的异常与它执行的任务没有明显的关联关系,这种情形会让人不知所措。当一个方法传递一个由低层抽象抛出的异常时,往往会发生这种情况。这种情况发生时,不仅让人困惑,而且也"污染"了高层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异常。第6条: 每个方法抛出的异常都要有文档
要单独的声明被检查的异常,并且利用Javadoc的@throws标记,准确地记录下每个异常被抛出的条件。
如果一个类中的许多方法处于同样的原因而抛出同一个异常,那么在该类的文档注释中对这个异常做文档,而不是为每个方法单独做文档,这是可以接受的。第7条: 在细节消息中包含失败 -- 捕获消息
简而言之,当我们自定义异常或者抛出异常时,应该包含失败相关的信息。
当一个程序由于一个未被捕获的异常而失败的时候,系统会自动打印出该异常的栈轨迹。在栈轨迹中包含该异常的字符串表示。典型情况下它包含该异常类的类名,以及紧随其后的细节消息。第8条: 努力使失败保持原子性
当一个对象抛出一个异常之后,我们总期望这个对象仍然保持在一种定义良好的可用状态之中。对于被检查的异常而言,这尤为重要,因为调用者通常期望从被检查的异常中恢复过来。
一般而言,一个失败的方法调用应该保持使对象保持在"它在被调用之前的状态"。具有这种属性的方法被称为具有"失败原子性(failure atomic)"。可以理解为,失败了还保持着原子性。对象保持"失败原子性"的方式有几种:
(1) 设计一个非可变对象。
(2) 对于在可变对象上执行操作的方法,获得"失败原子性"的最常见方法是,在执行操作之前检查参数的有效性。如下(Stack.java中的pop方法):
public Object pop() {
if (size==0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
(3) 与上一种方法类似,可以对计算处理过程调整顺序,使得任何可能会失败的计算部分都发生在对象状态被修改之前。
(4) 编写一段恢复代码,由它来解释操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。
(5) 在对象的一份临时拷贝上执行操作,当操作完成之后再把临时拷贝中的结果复制给原来的对象。
虽然"保持对象的失败原子性"是期望目标,但它并不总是可以做得到。例如,如果多个线程企图在没有适当的同步机制的情况下,并发的访问一个对象,那么该对象就有可能被留在不一致的状态中。
即使在可以实现"失败原子性"的场合,它也不是总被期望的。对于某些操作,它会显著的增加开销或者复杂性。
总的规则是:作为方法规范的一部分,任何一个异常都不应该改变对象调用该方法之前的状态,如果这条规则被违反,则API文档中应该清楚的指明对象将会处于什么样的状态。第9条: 不要忽略异常
当一个API的设计者声明一个方法会抛出某个异常的时候,他们正在试图说明某些事情。所以,请不要忽略它!忽略异常的代码如下:
try {
...
} catch (SomeException e) {
}
空的catch块会使异常达不到应有的目的,异常的目的是强迫你处理不正常的条件。忽略一个异常,就如同忽略一个火警信号一样 -- 若把火警信号器关闭了,那么当真正的火灾发生时,就没有人看到火警信号了。所以,至少catch块应该包含一条说明,用来解释为什么忽略这个异常是合适的。转载至: http://www.jb51.net/article/86141.htm
            https://www.cnblogs.com/alsf/p/5626384.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: