参考博客1、博客2
7 异常、断言、日志
Java中的三种处理系统错误的机制:
- 异常处理:捕获异常情况并处理(将控制权从错误产生的地方转移给能够处理这种情况的错误处理器)
- 断言:有选择的弃用检测(测试期间用检测验证程序操作的正确性)
- 日志:记录出现的问题,以备日后分析
7.1 处理错误
- 如果由于出现错误而使得某些操作没有完成, 程序应该返回到一种安全状态,并能够让用户执行一些其他的命令;或者允许用户保存所有操作的结果,并以妥善的方式终止程序。
- 如果某方法不能采用正常途径完成它的任务,可以抛出一个封装了错误信息的对象,且这个方法立即退出不返回任何值
- 程序中可能出现的问题:用户输入错误、设备错误、物理限制、代码错误
7.1.1 异常分类
- 异常指不期而至的各种状况,是发生在程序运行期间的事件,干扰正常的指令流程。
- Java通过API中的Throwable类的众多子类描述不同的异常,即异常都是对象,都是Throwable子类的实例。异常描述了出现在一段编码中的错误条件,当条件生成时,错误将引发异常。
- 异常具有自己的语法和特定的继承结构
- Error:程序无法处理的错误。表示应用程序中较严重的问题,这些错误表示故障发生于虚拟机自身,或者发生在虚拟机试图执行应用时(即代码运行时JVM出现的问题),如系统内部错误和资源耗尽错误。这些异常发生时,JVM一般会选择的线程终止。
- Exception:程序本身可以处理的异常。
- RuntimeException运行时异常:“JVM常用操作”引发的错误,即由程序错误导致的异常(错误的类型转换、数组访问越界、访问null指针…),一般是由程序逻辑错误引起的,应该从逻辑角度尽可能避免这类异常。这些异常是不可查异常,可以选择捕获处理,也可以不处理。
- 非运行时异常:RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
- IOException:程序本身没问题,但由于像I/O错误这类问题导致的异常(试图在文件尾部后面读取数据、试图打开不存在的文件、根据给定的字符串查找不存在的类的对象…)
- Unchecked不可查异常:派生于Error类或RuntimeException类的所有异常
- Checked可查异常:正确的程序中运行中很容易出现的、情理可容的所有其他的异常。编译器将核查是否为所有的checked异常提供了异常处理器,即要么try-catch语句捕获它,要么throws字句声明抛出它,否则编译不通过。
Java异常处理机制:抛出异常,捕获异常
- 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象包含了异常类型和异常出现时程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
- 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器。潜在的异常处理器是异常发生时依次存留在调用栈中的方法集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止,即Java程序终止。
7.1.2 throws声明checked异常
- 一个方法所能捕获的异常,一定是Java代码在某处所抛出的异常,即异常总是先被抛出,后被捕获。
- 任何Java代码都可以抛出异常(自己编写的代码、来自Java开发环境包中的代码、Java运行时系统)
- 如果一个方法可能会出现异常,但没有能力处理这种异常,可以在方法声明处用throws子句声明抛出异常(抛给调用者处理):methodname throws Exception1,Exception2,…{ }
- throws语句用于在方法定义时声明该方法要抛出的异常类型,多个异常可用逗号分隔,如果抛出的是Exception异常类型,则该方法被声明为抛出所有异常。(用throws抛出可查异常,不可查异常要么不可控制,要么就如RuntimeException应该避免发生)
遇到下面的情况时应声明抛出异常:
- 调用一个抛出checked异常的方法(也可以捕获处理)
- 程序运行过程中发现错误,并且利用throw语句抛出一个checked异常
- 程序出现错误
- Java虚拟机和运行时库出现的内部错误
对于子类覆盖超类方法:
- 1)如果超类方法没有抛出任何checked异常,子类方法也不能抛出任何checked异常
- 2)子类方法中声明的checked异常不能比超类方法中声明的异常更通用,即子类可以抛出更特定的异常,或者声明不抛出异常,不能声明与被覆盖方法不同的异常
对于运行时异常、错误或可查异常,Java技术所要求的异常处理方式有所不同:
- 1)由于RuntimeException的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。
- 2) 对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
- 3)对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。
总的来说Java规定:对不可查的RuntimeException和Error允许忽略;对可查异常必须捕获或者抛出
7.1.3 throw抛出异常
对已存在的异常类,抛出异常的过程:
- 找到合适的异常类
- 创建这个类的一个对象
- 将对象抛出
// Java异常语法规则例程:
void method1() throws IOException{} //合法
//编译错误,必须捕获或声明抛出IOException
void method2(){
method1();
}
//合法,声明抛出IOException
void method3()throws IOException {
method1();
}
//合法,声明抛出Exception,IOException是Exception的子类
void method4()throws Exception {
method1();
}
//合法,捕获IOException
void method5(){
try{
method1();
}catch(IOException e){…}
}
//编译错误,必须捕获或声明抛出Exception
void method6(){
try{
method1();
}catch(IOException e){throw new Exception();}
}
//合法,声明抛出Exception
void method7()throws Exception{
try{
method1();
}catch(IOException e){throw new Exception();}
}
7.1.4 创建异常类
- 创建自定义异常类:定义一个派生于Exception或其子类的类
- 习惯上自定义的类应该包含两个构造器:默认构造器、带有详细描述信息的构造器(超类Throwable的toString方法将会打印这些详细信息,在调试中很有用)
在程序中使用自定义异常类,大体可分为以下几个步骤:
- 1)创建自定义异常类。
- 2)在方法中通过throw关键字抛出异常对象。
- 3)如果在当前抛出异常的方法中处理异常,可以使用try-catch语句捕获并处理;否则在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作。
- 4)在出现异常方法的调用者中捕获并处理异常。
7.2 捕获异常
- 方法应该捕获并处理知道如何处理的异常,也可以将不知道如何处理的异常传递给调用者(用throws说明符声明抛出异常),如果调用了一个抛出checked异常的方法,就必须对它进行处理,或者继续传递
- 如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容
几种异常处理情况:
- 1)try语句块中的代码抛出在catch子句中说明的异常类(或其子类)时,程序跳过try语句块其余代码并执行catch子句中的处理器代码,一经处理结束就意味着整个try-catch语句结束,其他的catch子句不再有匹配和捕获异常类型的机会
- 2)try语句块中的代码没有抛出任何异常,程序就跳过catch子句
- 3)如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,这个方法就立刻退出
7.2.2 捕获多个异常
- 在一个try语句块中可以捕获多个异常类型,并对不同类型的异常作出不同的处理(为每个异常类型使用一个单独的catch子句);对异常的处理方式相同时,也可以用同一个catch子句捕获多个异常类型(捕获的异常类型彼此之间不存在子类关系)
- 捕获多个异常时,异常变量隐含为final变量
- 对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子句放在前面,同时尽量将捕获相对高层(Exception最高)的异常类的catch子句放在后面,否则捕获高层异常类的catch子句可能会屏蔽捕获底层异常类的catch子句
7.2.3 再次抛出异常与异常链
在catch子句中可以抛出一个异常,目的是改变异常的类型:
- 抛出的异常可用带有异常信息文本的构造器来构造
- 也可以将原始异常设置为新异常的“原因”
7.2.4 finally子句
- 不管是否有异常被捕获,finally子句中的代码都被执行
- 需要finally子句的情景:当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收,那么就会产生资源回收问题
执行finally子句的情况
- 代码没有抛出异常:程序首先执行try语句块中的全部代码,然后执行finally子句中的代码
- 代码抛出一个在catch子句中捕获的异常:程序将执行try语句块中的代码,直到发生异常,然后跳过try中剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码
- 代码抛出了一个异常,但这个异常不是由catch子句捕获的:程序将执行try中代码直到出现异常,然后跳过try剩余代码,然后执行finally子句的的代码,并将异常抛给JVM处理,但finally块后的语句不会被执行
不执行finally块的特殊情况:
- 在finally块中发生异常
- 在finally前面的代码中用了System.exit()退出程序
- 程序所在的线程死亡
- 关闭CPU(计算机断电、失火、遭遇病毒攻击)
try-catch-finally规则:
- 必须在try之后添加catch或finally块,try后可同时接catch和finally块,但至少一个块
- Catch块应与相应的异常类的类型相关
- 含有多个catch块时,只执行第一个匹配块
- try-catch-finally结构可嵌套,还可重新抛出异常
- 除了不执行finally的特殊情况外,总将执行finally作为结束
- finally子句若包含return语句,而try语句块也包含return语句的话,finally中的返回值将覆盖try中的返回值
- finally子句也可能抛出异常,如果try和finally都有异常抛出,则try中的原始异常将会丢失转而抛出finally中的异常——可以通过适当处理,重新抛出原来的异常(如在finally中要求try没有抛出异常时,finally才能抛出异常)
7.2.5 带资源的try语句
- 带资源的try语句最简单的形式如下:
- AutoCloseable和Closeable接口提供了close方法,如果资源属于实现了AutoCloseable或Closeable接口的类,则在try块退出时(不论是正常退出还是存在异常退出,都会调用close方法
- 也可以指定多个资源,不论这个try块如何退出,in和out都会关闭
- 当try块抛出一个异常,close方法也抛出一个异常时,try块的异常会重新抛出,close的异常会“被抑制”,close的异常将自动捕获,并由addSuppressed方法增加到原来的异常(用getSuppressed方法可以得到close抛出并被抑制的异常列表)
- 带资源的try语句自身也可以有catch子句和finally子句,这些子句会在close关闭资源后执行(不过try语句中不建议加入太多内容)
7.2.6 分析堆栈轨迹元素
- 堆栈轨迹(stack trace):方法调用过程的列表,包含程序执行过程中方法调用的特定位置
- 当程序正常终止,没有捕获异常时,堆栈轨迹就会显示
- printStackTrace():访问堆栈轨迹的文本描述信息,printStackTrace(PrintWriter p)
- getStackTrace():得到StackTraceElement对象的一个数组,StackTraceElement[] getStackTrace()
- StackTraceElement类:含有能够获得文件名和当前执行代码行号的方法,还有能够获得类名和方法名的方法
- Treasd.getAllStackTrace():产生所有线程的堆栈轨迹
7.3 异常机制的使用技巧
- 1)异常处理不能代替简单的测试。即尽量避免使用异常,将异常情况提前检测出来。捕获异常花费的时间超过简单测试,应该只在异常情况使用异常机制。
- 2)不要过分地细化异常。不要为每个可能会出现遗产更多语句都设置try-catch,即不要把每条语句都分装在一个独立的try语句块中,应将整个任务包装在一个try语句块中,这样当任何一个操作出现问题时,整个任务都可以取消,从而将正常处理与错误处理分开
- 3)利用异常层次结构。避免总是catch Exception或Throwable,而要catch更具体的异常类或创建自己的异常类,这样可以根据不同的异常做不同的处理,使程序更加清晰。
- 4)不要压制异常。将不能处理的异常往外抛,而不是捕获之后随便处理。
- 5)在检测错误时,“苛刻”比放任更好。比如对空栈pop时,抛出EmptyStackException异常比返回一个null,等以后抛出NullPointerException异常好——早抛出
- 6)不要羞于传递异常。当异常发生时,不应立即捕获,而是应该考虑当前作用域是否有能力处理这一异常,如果没有则应该将异常继续向上抛出,交由更上层作用域来处理(有时候让高层次的方法通知用户发生了错误,或者放弃不成功的命令比捕获异常更好)——晚捕获
- 7)不要在循环中使用try-catch,尽量将其放在循环外或者避免使用
// 异常使用技巧例程:
//1、尽量避免使用异常,将异常情况提前检测出来
Stack<Object> stack=new Stack();
String str="123";
stack.push(str);
try{
stack.pop();
}catch(EmptyStackException e){
stack.pop();
}
//应该用下面的方式,避免使用异常
if(!stack.isEmpty()){
stack.pop();
}
//2、不要为每个可能会出现异常的语句都设置try-catch
try{
stack.pop();
}catch(EmptyStackException e){
//...
}
try{
Double.parseDouble(str);
}catch(NumberFormatException e){
//...
}
//应该用下面的方式,将两个语句放在一个try中
try{
stack.pop();
Double.parseDouble(str);
}catch(EmptyStackException e){
//...
}catch(NumberFormatException e){
//...
}
//3、避免在方法中抛出或捕获RuntimeException和Error
String[] array;
try{
array=new String[100];
//array = new String[1000000];此时会出现OutOfMemoryError异常
}catch(OutOfMemoryError e){
throw e;
}
//应直接用下面的代码
array=new String[100];
//4、避免总是catch Exception或Throwable,应捕获具体的异常
try{
stack.pop();
}catch(Exception e){
//应catch具体的EmptyStackException
}
//5、不要压制、隐瞒异常,将不能处理的异常往外抛,而不是捕获后随便处理
try{
Double.parseDouble(str);
}catch(NumberFormatException e){
//...假设此处为随便处理
throw e; //抛出不能处理的异常
}
代码练习:
public class ExceptionTest {
public static int division(int x,int y) throws MyException{ //声明方法抛出异常
if(y<0){
throw new MyException("异常:除数不能为负数"); //抛出异常
}
return x/y;
}
public static void main(String args[]){
int x=1,y=-1;
// int x=1,y=0;
// int x=1,y=1;
try{
int result=division(x,y); //调用会抛出异常的方法
System.out.println(x+"/"+y+"="+result);
}catch(MyException e){ //捕获并处理异常
System.out.println(e.getMessage());
}catch(ArithmeticException e){ //ArithmeticException异常属于运行时异常,division没有捕获,将此异常上传给其调用者main方法
System.out.println("异常:除数不能为0");
}catch(Exception e){
System.out.println("发生了其他异常");
}finally{
System.out.println("code end");
}
}
}
class MyException extends Exception{
String message;
public MyException(String message){
this.message=message;
}
public String getMessage(){
return message;
}
}
最后,对于参考博客1中的引子问题的思考:main中调用testEx,testEx调用testEx1,testEx1调用testEx2,testEx2中 i==0时发生异常,catch捕获并处理后执行testEx2的finally,然后返回testEx1时,异常已被处理,testEx1的try块正常执行完后执行finally,然后返回testEx中正常执行完try和finally,再返回main执行完try,跳过catch,程序结束
package Test;
public class TestException {
public TestException() {
}
boolean testEx() throws Exception {
boolean ret = true;
try {
ret = testEx1();
} catch (Exception e) {
System.out.println("testEx, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx, finally; return value=" + ret);
return ret;
}
}
boolean testEx1() throws Exception {
boolean ret = true;
try {
ret = testEx2();
if (!ret) {
return false;
}
System.out.println("testEx1, at the end of try");
return ret;
} catch (Exception e) {
System.out.println("testEx1, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx1, finally; return value=" + ret);
return ret;
}
}
boolean testEx2() throws Exception {
boolean ret = true;
try {
int b = 12;
int c;
for (int i = 2; i >= -2; i--) {
c = b / i;
System.out.println("i=" + i);
}
return true;
} catch (Exception e) {
System.out.println("testEx2, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx2, finally; return value=" + ret);
return ret;
}
}
public static void main(String[] args) {
TestException testException1 = new TestException();
try {
testException1.testEx();
} catch (Exception e) {
e.printStackTrace();
}
}
}