异常机制及其处理
一、异常
异常就是一个表示阻止执行正常进行的错误或者情况。比如数组下标越界访问异常:ArrayIndexOutOfBoundsException;文件不存在异常:FileNotFoundException等等。
抛出异常与异常处理的例子:除数为0异常。
package demo; import java.util.Scanner; /** * Created by luts on 2015/11/28. */ public class QuotientWithException { public static void main(String[] args){ Scanner input = new Scanner(System.in); System.out.println("输入两个数:"); int num1 = input.nextInt(); int num2 = input.nextInt(); try { if (num2 == 0) throw new ArithmeticException("除数不能为0"); System.out.println(num1 + " / " + num2 + " = " + (num1 / num2)); } catch (ArithmeticException ex){ System.out.println("除数不能为0"); } System.out.println("除法运算结束"); } }
输出:
输入两个数: 3 0 除数不能为0 除法运算结束
输入两个数: 6 3 6 / 3 = 2 除法运算结束
上述例子中,try语句是正常执行情况下的代码,如果出现异常则抛出异常。catch语句是对异常情况的处理。当程序中抛出一个异常的时候,程序的正常流程就被破坏了。throw语句用于抛出一个异常,类似于方法的调用,其调用的是catch块。catch块处理完异常后,不返回throw语句,而是执行catch块的下一条语句。
一个try-throw-catch块的模板为:
try { Code to try; Throw an exception with a throw statement or from method if necessary; more code to try; } catch (type ex){ Code to process the exception; }
1.1. 异常的类型
throwable类是所有异常类的根。所有异常类都直接或间接的继承自throwable类。可以通过Exception 或者Exception 的子类来创建自己的异常类。异常类大体分为三类:系统错误、异常和运行时异常。
· 系统错误(system error):有java虚拟机抛出,用Error类表示。Error类描述的是内部系统错误,无法对错误进行处理。
· 异常(exception):用Exception类表示,描述的是程序和外部环境所引起的错误,这些错误是能被程序捕获和处理的。
· 运行时异常(runtime exception):用RuntimeException类表示,描述的是程序设计的错误。
Error类的子类例子:
类 | 可能引起异常的原因 |
LinkageError | 一个类对另一个类由某种依赖性,编译前者后,后者变得不兼容 |
VirtualMachineError | Java虚拟机中断或者没有继续运行所必需的资源可用 |
Exception类的子类例子:
类 | 可能引起异常的原因 |
ClassNotFoundException | 企图使用一个不存在的类。例如试图使用Java命令来运行一个不存的类或者程序要调用三个类文件,而只能找到两个,都会发生这种异常。 |
IOException | 同输入/输出相关的操作,例如,无效的输入、读文件时超出文件尾、打开一个不存在的文件等。 |
RuntimeException类子类的例子:
类 | 可能引起异常的原因 |
ArithmetricException | 一个整数除以0. 注意:浮点运算不抛出异常 |
NullPointerException | 企图通过一个null引用变量访问一个对象 |
IndexOutOfBoundsException |
数组下标越界 |
IllegalArgumentException | 传递给方法的参数非法或者不合适 |
通常情况下,免检异常都会反映出程序设计上不可恢复的逻辑错误。为了避免过多使用try-catch块,java语言不允许编写代码捕获或声明免检异常。
1.2. 异常的处理
java的异常处理模型包括:声明一个异常、抛出一个异常和捕获一个异常。
1.2.1 声明异常
声明异常:每个方法都必须声明它可能抛出的必检异常的类型。因为任何代码都可能在程序中发生系统错误或者运行时错误,所以不要求显示声明Error和RuntimeException(免检异常)。声明异常的语法:
抛出一个异常: public void myMethod() throws IOException 抛出多个异常: public void myMethod() throws IOException1,throws IOException2, ..., throws IOExceptionN
注意:如果在父类中没有声明异常,那么在子类中就不能对其进行覆盖来声明异常。
1.2.2. 抛出异常
检测一个错误的程序可以创建一个正确的异常类型的实例并抛出它,这就称为抛出异常。
例如程序调用方法时传递的参数不符合方法的签名,可以创建一个IllegalArgumentException的实例并抛出它:
IllegalArgumentException ex = new IllegalArgumentException("Wrong Arguments"); throw ex; 或者使用下列语句: throw new IllegalArgumentException("Wrong Arguments");
注意:声明异常使用关键字throws,抛出异常使用关键字throw。
1.2.3. 捕获异常
使用try-catch块来捕获异常:
try{ statements; //statemenrs that may throw exceptions } catch(Exception1 exVar1){ handle for Exception1 ; } catch(Exception2 exVar2){ handle for Exception2 ; } ... catch(ExceptionN exVarN){ handle for ExceptionN ; }
· 如果在try块中没出现异常,则跳过catch块语句。
· 如果try块中的某条语句抛出了一个异常,java就会跳过try块中剩下的语句,然后开始查找处理这个异常的代码的过程。处理这个异常的代码称为异常处理器;可以从当前方法开始,沿着调用方法的调用链,按照异常的反向传播方向找到这个处理器。从第一个到最后一个逐一检查catch块,判断是catch块中的异常实例是否是该异常对象的类型。如果是,就将该异常对象赋值给所声明的变量,然后执行catch块中的代码。如果没有发现异常处理器,java会退出这个方法,把异常传递给调用方法的方法,继续同样的过程来查找处理器。如果在调用的方法链中都找不到处理器,程序就会终止并且在控制台上打印出错信息。
需要注意的是:
· 一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。
· 一个通用父类可以派生各种异常类。如果一个catch块可以捕获一个父类的异常对象,那么它就能捕获那个父类的所有子类的异常对象。
· 子类的catch块必须出现在父类的catch块之前,否则会导致编译错误。正确的例子:
try{ .... } catch (RuntimeException ex){ .... } catch (Exception ex){ .... }
· java强迫程序员必须处理必检异常。如果一个方法中声明了一个必检异常,就必须在try-catch块中调用它,或者在调用方法中声明要抛出的异常。
假设方法P1调用方法P2,P2可能会抛出一个IOException异常,那么代码编写有两种方式: 捕获异常: void p1(){ try{ p2(); } catch (IOException ex){ ...... } } 或者抛出异常: void p1() throws IOException { p2(); }
一个完整的声明、抛出、捕获异常的例子:
1 package demo; 2 3 /* 4 * 利用Circle类的setRadius方法操作如何声明、抛出和捕获异常 5 * Created by luts on 2015/11/29. 6 */ 7 public class CircleWithException { 8 9 private double radius; //圆的半径 10 11 private static int numberOfObjects = 0; //记录实例对象的个数 12 13 //构建一个半径为1的圆 14 public CircleWithException(){ 15 this(1.0); 16 } 17 18 19 //构建一个指定半径的圆 20 public CircleWithException(double newRadius){ 21 setRadius(newRadius); 22 numberOfObjects++; 23 } 24 25 //访问器 26 public double getRadius(){ 27 return radius; 28 } 29 30 //设置器,声明异常、抛出异常 31 public void setRadius(double newRadius) throws IllegalArgumentException{ 32 if (newRadius >= 0) 33 radius = newRadius; 34 else throw new IllegalArgumentException("Radius cannot be negative"); 35 } 36 37 public static int getNumberOfObjects(){ 38 return numberOfObjects; 39 } 40 41 public double findArea(){ 42 return radius * radius * Math.PI; 43 } 44 }
1 package demo; 2 3 /** 4 * Created by luts on 2015/11/29. 5 */ 6 public class TestCircleWithException { 7 public static void main(String[] args){ 8 9 //捕获异常 10 try { 11 CircleWithException c1 = new CircleWithException(5); 12 CircleWithException c2 = new CircleWithException(-5); 13 CircleWithException c3 = new CircleWithException(0); 14 } 15 catch (IllegalArgumentException ex){ 16 System.out.println(ex); 17 } 18 19 System.out.println("Number of objects created: " + CircleWithException.getNumberOfObjects()); 20 } 21 }
输出结果:
java.lang.IllegalArgumentException: Radius cannot be negative
Number of objects created: 1
1.2.4. 从异常中获取信息
异常对象包含关于异常信息的有用信息,这些信息可以通过java.lang.Throwable类中的实例方法获得:
· printStackTrace()方法在控制台上输出栈跟踪信息。
· getStackTrace()方法提高方案来访问由 printStackTrace()打印输出的栈跟踪信息。
例如上述例子,将测试类修改如下:
1 package demo; 2 3 /** 4 * Created by luts on 2015/11/29. 5 */ 6 public class TestCircleWithException { 7 public static void main(String[] args){ 8 9 //捕获异常 10 try { 11 CircleWithException c1 = new CircleWithException(5); 12 CircleWithException c2 = new CircleWithException(-5); 13 CircleWithException c3 = new CircleWithException(0); 14 } 15 catch (IllegalArgumentException ex){ 16 ex.printStackTrace(); 17 System.out.println("\n" + ex.getMessage()); 18 System.out.println("\n" + ex.toString()); 19 20 21 System.out.println("\nTrace Info Obtained from getStackTrace"); 22 StackTraceElement[] traceElements = ex.getStackTrace(); 23 for (int i = 0; i < traceElements.length; i++) { 24 System.out.print("method " + traceElements[i].getMethodName()); 25 System.out.print("(" + traceElements[i].getClassName() + ":"); 26 System.out.print( traceElements[i].getLineNumber() + ")"); 27 } 28 29 30 // System.out.println(ex); //打印一个有关异常信息的短信息ex.toStrin() 31 } 32 33 System.out.println("Number of objects created: " + CircleWithException.getNumberOfObjects()); 34 } 35 }
控制台输出:
1 java.lang.IllegalArgumentException: Radius cannot be negative <-------printStackTrac() 2 3 at demo.CircleWithException.setRadius(CircleWithException.java:34) <--------getMessage() 4 Radius cannot be negative 5 at demo.CircleWithException.<init>(CircleWithException.java:21) 6 7 at demo.TestCircleWithException.main(TestCircleWithException.java:12) <--------toString() 8 java.lang.IllegalArgumentException: Radius cannot be negative 9 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 10 11 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) <------using getStackTrace() 12 Trace Info Obtained from getStackTrace 13 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 14 method setRadius(demo.CircleWithException:34)method <init>(demo.CircleWithException:21)method main(demo.TestCircleWithException:12)method invoke0(sun.reflect.NativeMethodAccessorImpl:-2)method invoke(sun.reflect.NativeMethodAccessorImpl:62)method invoke(sun.reflect.DelegatingMethodAccessorImpl:43)method invoke(java.lang.reflect.Method:483)method main(com.intellij.rt.execution.application.AppMain:144)Number of objects created: 1 15 at java.lang.reflect.Method.invoke(Method.java:483) 16 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
1.2.5 finally子句
finally子句:不管是否发生异常,都会执行的语句,其语法如下:
try { ..... } catch (TheException ex){ ....... } finally { fianlStatements; }
任何情况下,finally块中的代码都会执行,不管try块中是否出现异常或是否捕获异常:
· 如果try块中没有出现异常,执行finalStatements,然后执行try语句的下一条语句。
· 如果try块中有一条语句会引起异常,并被catch块捕获,然后跳过try块中的其他语句,执行catch块和finally子句。执行try语句之后的下一条语句。
· 如果try块中有一条语句引起异常,但是没有被任何catch块捕获,就会跳出try块中其他的语句,执行finally子句,并且将异常传递给该方法的调用者。
即使在到达finally块之前有一个return语句,finally语句还是会执行。
finally例子:
1 public class TestException { 2 public static void main(String args[]) { 3 int i = 0; 4 String greetings[] = { " Hello world !", " Hello World !! ", 5 " HELLO WORLD !!!" }; 6 while (i < 4) { 7 try { 8 // 特别注意循环控制变量i的设计,避免造成无限循环 9 System.out.println(greetings[i++]); 10 } catch (ArrayIndexOutOfBoundsException e) { 11 System.out.println("数组下标越界异常"); 12 } finally { 13 System.out.println("--------------------------"); 14 } 15 } 16 } 17 }
输出结果:
Hello world ! -------------------------- Hello World !! -------------------------- HELLO WORLD !!! -------------------------- 数组下标越界异常 --------------------------
小结:
· try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
· catch 块:用于处理try捕获到的异常。
· finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:
1)在finally语句块中发生了异常。
2)在前面的代码中用了System.exit()退出程序。
3)程序所在的线程死亡。
4)关闭CPU。
try-catch-finally 规则(异常处理语句的语法规则):
1) 必须在 try 之后添加 catch 或 finally 块。try 块后可同时接 catch 和 finally 块,但至少有一个块。
2) 必须遵循块顺序:若代码同时使用 catch 和 finally 块,则必须将 catch 块放在 try 块之后。
3) catch 块与相应的异常类的类型相关。
4) 一个 try 块可能有多个 catch 块。若如此,则执行第一个匹配块。即Java虚拟机会把实际抛出的异常对象依次和各个catch代码块声明的异常类型匹配,如果异常对象为
某个异常类型或其子类的实例,就执行这个catch代码块,不会再执行其他的 catch代码块
5) 可嵌套 try-catch-finally 结构。
6) 在 try-catch-finally 结构中,可重新抛出异常。
7) 除了下列情况,总将执行 finally 做为结束:JVM 过早终止(调用 System.exit(int));在 finally 块中抛出一个未处理的异常;计算机断电、失火、或遭遇病毒攻击。
try、catch、finally语句块的执行顺序:
1) 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
2)当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
3)当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句。
1.2.6 重新抛出异常
如果异常处理器没有处理某种异常,或者处理器只是希望它的调用者注意到该异常,java就允许异常处理器重新抛出异常。语法如下:
try{ ..... } catch(TheException ex){ perform operations before exits; throw ex; //重新抛出异常,以便调用者的其他处理器获得处理异常ex的机会。 }
异常链
1 package demo; 2 3 public class TestException { 4 public static void main(String args[]) { 5 try { 6 method1(); //捕获method1中的异常 7 } 8 catch (Exception ex){ 9 ex.printStackTrace(); //打印输出异常,首先显示method1 中抛出的异常,然后显示method2中抛出的异常。 10 } 11 } 12 13 public static void method1() throws Exception{ 14 try{ 15 method2(); //捕获method2中的异常 16 } 17 catch(Exception ex){ 18 throw new Exception("New info from method1", ex); //抛出一个新的异常 19 } 20 } 21 22 public static void method2() throws Exception{ 23 throw new Exception("New info from metho2"); //抛出一个异常,该异常被method1 的catch块捕获 24 } 25 }
输出:
1 java.lang.Exception: New info from method1 2 at demo.TestException.method1(TestException.java:18) 3 at demo.TestException.main(TestException.java:6) 4 Caused by: java.lang.Exception: New info from metho2 5 at demo.TestException.method2(TestException.java:23) 6 at demo.TestException.method1(TestException.java:15) 7 ... 1 more
关于异常处理知识的深入知识,可以参阅:[深入理解java异常处理机制]