异常的概念
程序运行的环境是复杂的,程序在执行过程中可能遇到各种错误。如程序打开的文件不存在、网络连接遇到中断、除零操作、操作数越界等等。方法执行中遇到意外/例外/异常的情况/条件,称为异常(事件)。意外情况可能是不恰当的外部环境,可能是方法调用者传递了不恰当的实参——即错误环境或非法参数。
作为方法的设计者,通常希望方法被调用时满足其前置条件,但是,不能够因为调用者没有遵守契约——如在网络服务器未启动时发出网络连接请求,于是服务代码就导致死机、死循环或其他后果。
比较String.charAt(int index)方法。其文档为:在index索引取值范围从 0 到length()-1时,返回指定索引处的 char 值,否则,抛出索引超界异常:IndexOutOfBoundsException。而[例程 8‑1为什么需要Exception]中给出了任性的代码。
/**服务的设计者,必须考虑到可能发生的异常事件、必须考虑抛出异常对象、做好本方法的文档使方法具有良好的接口。有义务对方法参数的有效性进行检查。不管方法的文档中是否使用前置条件对参数加以明确地说明, 非法参数的检查是方法实现的第一步。对异常事件进行了 防御性编程。
* @return 如果索引范围从 0 到 length() - 1,遵循String.charAt(int)的契约;
* 如果不遵循契约,就没有String.charAt(int)那么好说话。
*/
public static char charAt(int index ){//取 -1、2、7
if(index< 0 || index >=str.length()){ // 成员变量 str = "yqj2065";
return (char)(Math.random() * 26 + 'a');
}else {
return str.charAt(index);
}
}
良好的异常设计,能够提高程序的可读性、可维护性、健壮性。Java通过面向对象的方式处理异常,最大限度地调解了程序的 正确性和清晰性(可读性和可维护性)间的矛盾。
Java语言将各种各样的异常事件加以数据抽象,所有的错误或异常都抽象为一个可以被抛出的对象,即java.lang.Throwable的(各种具体子类的)对象。
遇到异常事件咋办
当程序执行遇到异常事件,程序是就此结束运行还是应该从异常事件中恢复呢?这就得看异常事件的严重程度。
- (1)病入膏肓就让程序安乐死—— 严重的系统错误封装为Error。
- (2)大病小灾还得救死扶伤,
- (3)程序bug引起的(不应该出现的异常事件)则不予理会(打电话给编写那段导致异常代码的程序员,让他修改)。需要剔除的异常封装为RuntimeException。
Error和RuntimeException是程序不需要加以处理的,需要在源代码中寻找逻辑或用法错误并修改。Exception及Exception的非RuntimeException子类,编译器认为方法的调用者必须处理这类异常,也会监督、检查调用者是否处理了该异常,故称它们为被检查的异常(checked Exceptions),一般称为检查型异常,口语化为“要处理的异常”。
- Error:程序员不抛出、不捕捉、不处理。
- RuntimeException:由系统通过默认的异常处理程序自动抛出,自行处理。不强制要求程序员捕捉和处理。调试程序时改正。
- 检查型异常:方法头中用throws子句声明,方法体中以throw语句抛出;调用者以throws子句向上推卸处理责任、或者使用try-catch捕捉和处理。
抛出异常
Java程序(你的类、以及JDK类库或第三方包中的代码)和Java虚拟机都可能抛出异常。JVM自动抛出的异常,如*Error等Error、遇到4/0抛出ArithmeticException。我们关注的是,在方法体中手工抛出异常。手工抛出异常,指方法在执行过程中遭遇某种异常事件,从而创建相应的Throwable对象并抛出。例如:
throw new IndexOutOfBoundsException();
你不要抛出非Throwable对象,如throw new Dog(),程序将无法通过编译。抛出的应该是检查型异常。
一旦执行了throw语句,JVM新建一个异常对象、终止本方法的执行流程,返回到方法的调用者希望得到处理。
- 如果该异常是检查型异常,调用者(1)准备了catch子句进行异常处理;(2)官僚主义地使用throws子句,将异常处理推到调用者的调用者。
- 如果该异常是非检查异常,调用者又没有附加的异常处理代码,系统将调用默认的异常处理流程,在标准输出设备上打印/输出该异常对象的堆栈使用轨迹。
- 各种Java书籍在讲解IO流、网络编程时,为了集中精力,书中的例程常常使用throws以保持代码的简短。在实际编程中,官僚主义地throws是不好的风格。许多程序员经常忘记处理异常,或者将异常处理推迟到遥远的未来。
- 方法体中使用throw语句抛出的所有检查型异常,必须在方法头中throws该异常(或者其父类)。换言之,throws子句严格地强制性要求程序员:方法体中只能够使用throw语句抛出throws子句中声明的检查型异常类型或子类型。