一、 基本概念
Java的基本理念是“结构不佳的代码不能运行”。Java属于强类型、强检查语言,即编译器会处理大量的的安全和基本的语法错误问题。但还是有一些错误巧妙地隐藏了起来,只有在运行时才被发现。
老式的程序设计语言,例如C的多重错误处理模式,往往建立在约定俗成的基础上,并不是语言的一部分。大致上,通过一个函数返回某个特殊值或设置某个标志来通知接收者,出错了。这就把错误的发出与程序的正确运行含混地绑定在了一起。这让程序员感到厌烦,于是很多时候拒绝针对所有的可能发生错误的位置进行细致地检查,如果程序员真的那么做了,那么实际上整个程序的可读性还会变得更差。而且这种基于约定的习惯错误处理方法,在程序员太忙的时候,就会被有意无意地忽略。Java中有时候的取容器元素API也会在返回值提示一些错误。
于是产生了这样一种做法,在“正常执行中做什么事”的代码中抛出一个异常,交给“出了问题怎么办”的代码。这就实现了隔离。使得代码井井有条,可读性、可维护性更好。
异常处理的实现,可以上溯20世纪60年代。实际上异常处理的时间比面向对象还要早。C++的异常处理机制基于Ada,Java的异常处理则建立在C++的基础上(尽管看上去更像Object Pascal)。
异常的涵义:我对此感到意外,但我不知道该如何处理它。
二、 基本异常
1 抛出异常对象的方式和返回普通对象的方式差不多。只不过返回普通对象的方式是return new 构造器,而抛出异常对象的方式是throw new 构造器(一切Throwable类型,而不仅仅是Exception类型)。异常代表的思想是,不允许程序沿着某条路经继续走下去。实际上这样看来,抛出异常也是一种返回机制,因为程序退出了。
识别异常有两种形式: 通过无参构造器返回的异常类型确认;通过字符串参数构造器携带的字符串来确认。
三、 捕获异常
一个熟悉的概念,可能发生异常的代码段称为guarded region--监控区域。
即
try { doWork(); } catch(type id) { doSth(); }
异常处理理论上有两种模型,终止与恢复模型。
终止模型理论上很好理解,就是出错了以后就不要再来重复执行了。
恢复模型则把try放入while循环中,处理成功就break,如果出了异常,就不断回到循环中。
用while循环建立 恢复模型 不断重复 直到不再出现异常,例如:
- public class Ex5 {
- private static int[] ia = new int[2];
- static int x = 5;
- public static void main(String[] args) {
- while(true) {
- try {
- ia[x] = 1;
- System.out.println(ia[x]);
- break;
- } catch(ArrayIndexOutOfBoundsException e) {
- System.err.println(
- "Caught ArrayIndexOutOfBoundsException");
- e.printStackTrace();
- x--;
- } finally {
- System.out.println("Are we done yet?");
- }
- }
- System.out.println("Now, we're done.");
- }
- }
结果:
- Caught ArrayIndexOutOfBoundsException
- Are we done yet?
- Are we done yet?
- java.lang.ArrayIndexOutOfBoundsException: 5
- at my.test.Ex5.main(Ex5.java:9)
- Caught ArrayIndexOutOfBoundsException
- java.lang.ArrayIndexOutOfBoundsException: 4
- at my.test.Ex5.main(Ex5.java:9)
- Caught ArrayIndexOutOfBoundsException
- java.lang.ArrayIndexOutOfBoundsException: 3
- at my.test.Ex5.main(Ex5.java:9)
- Are we done yet?
- Caught ArrayIndexOutOfBoundsException
- java.lang.ArrayIndexOutOfBoundsException: 2
- at my.test.Ex5.main(Ex5.java:9)
- Are we done yet?
- 1
- Are we done yet?
- Now, we're done.
看起来虽然throw 看起来让程序返回了。但被catch以后,程序还可以继续这里走下去。所以可以在while里不断循环。
关于trycatch语句块的一些思考:
实际上我们不管在try里面手动throw 一个异常,虚拟机都有可能自动throw抛出一个异常。那我们手动抛出又有什么意义呢?手动抛出就要求我们严格管理我们的异常。
根据《TIJ》:
第一个例子,
/** * */ /** * @author acer * */ public class 异常实验 { //预先声明了这个方法可能抛出抛出的异常的种类。 public void f() throws SimpleException { System.out.println("Throw SimpleException from f()"); throw new SimpleException();//此处显而易见,可以抛出,也可以不抛出 } public void g() throws SimpleException { System.out.println("Throw SimpleException from g()"); throw new SimpleException("你好,试用一下字符串参数的异常构造器"); } /** * @param args */ public static void main(String[] args) { 异常实验 ycsy = new 异常实验(); //注意,因为上面有了抛出异常的声明,实验这里必须使用一个警戒区域来试验捕获这个异常。否则IDE会 //发出警告,实际上编译器也不会通过。 try { ycsy.f(); } //try语句块要么匹配catch,要么匹配finally,缺一不可。 catch(SimpleException e) { System.out.println("抓到这个异常!!"); //这是所有的Throwable类型都有的一个方法。 //实际上这里做了一个输出流的重定向,如果调用无参数类型的方法,则输出到System.err //打印顺序为从方法调用处直到异常抛出处的方法调用序列。 //从控制台可以读出,实际上是由方法的外壳处抛出的异常。即在main中抛出的异常。 //如果同时发生,控制台会先打出System.out的信息,再打出System.err的信息。并且System.err的信息会标红。 e.printStackTrace(System.out); } try { ycsy.g(); } catch(SimpleException e) { System.out.println("抓到这个异常!!"); e.printStackTrace(System.out); } finally { System.out.println("Finally!"); } } } //大部分情况下,我们只需要简单继承Exception类,让编译器为我们直接实现构造器即可。 class SimpleException extends Exception { public SimpleException(){} //我们可以从栈上面读出这个字符串,即可以用e.printstacktrace的形式来获取它。 public SimpleException(String msg) { super(msg); } }
第二个例子,异常与记录日志
import java.util.logging.*; import java.io.*; class LoggingException extends Exception { private static Logger logger = Logger.getLogger("LoggingException"); public LoggingException() { StringWriter trace = new StringWriter(); //这里做了一个装饰器模式的重定向。 printStackTrace(new PrintWriter(trace)); //向logger中写东西的时候System.err就会自动输出了!!甚至要快过System.out logger.severe(trace.toString()); } } public class 异常与记录日志 { public static void main(String[] args) { try { throw new LoggingException(); } catch(LoggingException e) { System.err.println("Caught" + e); } try { throw new LoggingException(); } catch(LoggingException e) { System.err.println("Caught" + e); } } }
第三个例子,LoggingExceptions2
import java.io.*;
import java.util.logging.*; public class LoggingExceptions2 { private static Logger logger = Logger.getLogger("LoggingExceptions2"); //本例子与“异常与日志记录”例子的区别在于,思路不同,前一个例子要求我们在自定义的exception类内就 //记日志。而这里的例子是捕获一个别人的异常,在它们里面重定向printStackTrace。这种情况更普遍。 static void logException(Exception e) { StringWriter trace = new StringWriter(); e.printStackTrace(new PrintWriter(trace)); logger.severe(trace.toString()); } public static void main(String[] args) { try { throw new NullPointerException(); } catch(NullPointerException e) { logException(e); } } }
第四个例子,ExceptionWithMessage
import java.io.*; class MessageException extends Exception { public MessageException(){} public MessageException(String msg,int x) { super(msg); this.x = x; } //对于异常对象而言,getMessage()犹如toString一样,是printStackTrace的时候输出用的。 //默认的getMessage()只打印msg。 public String getMessage() { return "Detail Message:" + x + " " + super.getMessage(); } private int x; } public class ExceptionWithMessage { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub try { throw new MessageException("你好",55); } catch(MessageException e) { e.printStackTrace(); } } }
第五个例子,ExceptionMethods
/** * */ /** * @author acer * */ //本例子说明了Exception类的各种原生方法类的调用。 public class ExceptionMethods { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub try { throw new Exception("My Exception"); } catch (Exception e) { System.out.println("Caught Exception"); System.out.println("getMessage(): " + e.getMessage()); System.out.println("getLoacalizedMessage(): " + e.getLocalizedMessage()); //在println中可以自动调用e.toString(),在printStackTrace中则不可以。 System.out.println("toString()" + e); System.out.println("printStackTrace()"); e.printStackTrace(); } } }
第六个例子,打印栈帧轨迹
package exceptions;
public class WhoCalled { static void f() { try { throw new Exception(); } catch(Exception e) { for(StackTraceElement ste : e.getStackTrace()) System.out.println(ste.getMethodName()); } } static void g() { f(); } static void h() { g(); } /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub f(); System.out.println("---------------------------------"); g(); System.out.println("---------------------------------"); h(); } }
第七个例子,异常链
第八个例子,finally引发的异常丢失
第九个例子, 异常的限制
涉及到经典的异常与继承问题。
子类的方法可以不抛出基类抛出的异常,但不能跑出基类没有声明的异常。
第十个例子,构造器与异常
正确的思路是:
三、最后的思考
强制实施的checked exception 是Java长于C++的地方。因为Java完全摒弃了通过不同的返回值或者设定标志位的方法来保留错误信息的方式,而统一为使用异常来报告。但是这也带来了一些问题,就是我们必须在可能抛出异常的地方用使用catch语句。还有一个懒的人的方法,我们可以在外层方法声明的地方使用Throws 关键字把这些错误丢给最外层,一层一层,甚至直接丢给控制台,这样就可以不用try-catch。
勤用Throws关键字是个好习惯吗?这样抛出去实际上就是我自己不想做异常处理,交给其他人,异常处理变得形同虚设。