第5 章 异常处理 本章内容 JVM 会将程序运行过程中出现导致程序中断的信息封装成异常对象返回用户。Java 官方为各类异常情况建立了丰富的异常类。基于异常对象,程序就可以通过匹配机制识别 出异常信息并激活一整套异常处理的代码。异常对象的出现从根本上实现了程序中核心业 务代码与防护性代码的分离。本章将介绍Java中异常的定义和分类,以及捕获和处理异常 对象的方法,最后是自定义异常的一些规范。 学习目标 . 了解异常的定义,理解在Java中Exception和Error的区别。 . 掌握异常的分类,理解RuntimeException的特殊性,知道一些常用的RuntimeException 子类。 . 熟练掌握异常的处理方法。 . 掌握异常的声明和抛出。 . 了解自定义异常类的方法。 5.1 异常基础 5.1.1 程序出错和解决方案 异常状态是指程序在运行过程中发生的、由外部问题导致的程序运行异常事件。异常 的发生往往会中断程序的运行,而这种中断出现是否就意味着程序编写出现了错误吗? 不 尽然,这种运行时异常情况产生的原因有很多,有的是用户错误引起的,如在计算器中除以0; 有的是程序错误引起的,如给出的数组下标为负值;有的是其他一些物理硬件错误引起的, 如用户的内存容量不足。归根到底,异常状态的出现就是由于代码实际工作状态与预计的 工作状态不一致。虽然不是全部,但很多异常状态都是可以通过防护性代码在程序内部解 决的。程 序出现的异常状态可以分为3种,不同类型异常状态的解决方式也是不同的。 1.语法错误 在编译过程中因为没有遵循语法规则而导致的程序异常状态称为语法错误或编译错 误,如缺少必要的标点符号、关键字输入错误、数据类型不匹配等。对于语法错误,一般程序 编译器会自动提示相应的错误地点和错误原因。因此,这类异常状态在三类异常状态中最 容易发现。通过修正代码中错误的语法、查阅API文档或其他资料,这类异常状态基本都 第 5 章 异常处理157 可以在程序编写阶段得到修正。 2. 逻辑错误 逻辑错误是指程序没有按期望的要求、预期的逻辑顺序执行而产生的异常状态。程序 的逻辑错误仅依靠编译器是无法检测的,大多数逻辑错误是程序运行时出现,甚至是特定时 刻才会出现的,而产生的原因也是多样且复杂的。逻辑错误是现阶段最复杂的一类异常状 态,而且没有通用的解决方案,因此人们在设计程序语言时,会尽量将逻辑错误转换为语法 错误或运行错误,从代码层面以一种规范的方法,明确异常状态处理的流程,以降低程序出 现逻辑错误的可能性。本章所讲的异常处理就是这一逻辑下的产物。 3. 运行错误 运行错误是指程序在运行过程中,运行环境发现了超出程序代码承载能力的情况下出 现的异常状态,一个开放的尤其是与用户互动的程序在运行时不可避免会出现不符合程序 主逻辑的分支,如一个读写文件内容的程序在运行时发现无法打开指定文件;一个可以运行 整数加法的加法器,被赋予了两个浮点数;用户输入的用户名和密码不匹配等。 为了让程序可以在可控的条件下正常运行,一个最直观的想法就是限定并规范程序的 输入内容,即建立黑白名单。然而,黑白名单的建立会大大影响核心程序的开发速度,如一 个加法计算器,如果使用黑名单限制非字母的字符输入,那么科学记数法的E、十六制的 ABCDEF 是否要加入例外? 如果使用重载的方法标定白名单,用户输入整数和浮点数,甚 至是虚数都是合理的,程序里需要大量的重载方法。另外,使用黑白名单的方式处理各类错 误和异常的方法,需要程序员了解程序运行细节,方能定制各种情况的处理代码,这样建立 的异常处理代码必然与核心代码是强耦合的,在修改时会出现牵一发动全身的情况。 例 程序员送给他的女朋友一支精美的口红(主逻辑)。为了确保送口红的过程顺利 进行,他可能还需要确定口红包装精美、口红是否与女朋友的造型匹配、女朋友对颜 色的喜好等问题(黑白名单处理方案)… … 等待他的将会是一个异常状态爆炸性增长 的局面。 “陷阱式”的异常生成机制为我们处理程序异常提供了一种新思路。在Java这个面向 对象程序语言中,JVM 会根据内存状态、异常状态产生的条件或程序预先定义条件产生特 定异常类对象。这个异常对象包含该异常状态生成时的栈轨迹,它包括异常栈所指向方法 的名字,方法所在的类名、文件名及在代码第几行触发了错误、异常等信息。 异常对象的出现,改变了程序员应对异常的方式。通过异常对象包含的信息,程序员知 道程序哪里出现了问题,出了什么类型的问题。问题的原因和类型被封装成一个独立的对 象,这个对象与正常运行的代码是分离的。这意味着,程序员可以更专注于代码核心逻辑的 编写,所有不适合该逻辑核心的异常状态都被封装为一个个异常对象抛出给系统。面对出 现的异常对象,程序员可以选择自己处理,还是交给其他人来处理。基于异常对象的处理代 码与核心逻辑是弱耦合,甚至是解耦合的。在Java中这种基于捕获的异常处理方案,实现 了程序主体与异常处理代码的分离,简化主逻辑功能设计时的代码复杂度,也丰富了异常情 况的处理手段,让专业的人员做专业的事情,十分符合现阶段大规模软件合作开发模式。 158 面向大数据的Java 程序设计基础(微课版) 续例 “陷阱式”的异常处理机制可以将那个送口红的程序员从包装的选择、氛围的营 造、喜好调查等问题的漩涡中“解救”出来,他只需要关注送口红这个主进程就行。出 现包装问题就交给导购员小姐姐,出现造型问题就交给Tony老师… … 高效甩锅! 成功地将所有异常情况排除到送口红这个主进程之外。 5.1.2 Error 和Exception Jaa将程序运行过程中的异常状态分为Eror和Excpein,它们都是jaa.ag. vtovlnThrowable类的子类。Throwable类型的对象在出现异常状态时由JVM抛出,或者可以由 程序中throw语句主动抛出。Java中异常类的继承关系,如图5-1所示。 图5- 1 Java中异常类的继承关系图(局部) 1.Eror Eror是程序无法处理的异常状态。在Java中,这类程序异常状态通过Eror的子类 描述,且程序在编译时也不会要求程序处理Eror,因为它们在应用程序的控制和处理能力 之外。Eror是程序不可自查的,且无法自我修正的。在Eror发生时,JVM一般会选择线 程终止,大多数Eror与代码执行环境有关,例如: JVM在为程序分配内存时,发现物理内存不足时,JVM会抛出OutOfMemoryEror 对象。 JVM在加载类的定义时,但无法找到该类的定义时,会抛出NoClasDefFoundEror 对象。 2.Exception Exception是程序本身可以处理的异常,它是由程序或外部环境不匹配所引起的,程序 中应当尽可能去捕获和处理这些异常。Exception类对象可以被Java异常处理机制所捕 获,它是异常处理的核心。Java中的Exception主要分为以下两类。 第 5 章 异常处理159 (1)非检查型异常(UncheckedException)。 Java语言在编译时,在程序中不要求用户给出(也可以选择给出)处理这些异常的代 码。从继承结构来看,这类异常都是RuntimeException以及它们的子类异常对象。 RuntimeException类异常对象出现的原因多半是代码中出现了明显的逻辑问题。例如: 当程序中用数字除以0时会抛出ArithmeticException异常对象。 当强制类型转换失败时会抛出ClasCastException类型转换异常对象。 当使用数组索引越界时就会抛出ArayIndexOutOfBoundsException异常对象。 当程序中引用变量在未持有对象的情况下,引用变量调用成员方法或访问成员属性时 就会抛出NulPointerException异常对象。 (2)检查型异常(CheckedException)。 Java语言强制要求程序员为这类异常对象编写异常处理代码,否则编译不会通过。 Exception自身及非RuntimeException分支的子类都是检查型异常,这样的异常一般是由 程序的运行环境导致的,CheckedException是Java中异常处理的主要工作目标。 检查型异常对象的处理可以让程序更加健壮稳定,因为程序可能被运行在各种未知的 环境下,而程序员无法干预用户如何使用他编写的程序,程序员就应该为这些可预见的异常 (各类具体异常子类)和不可预见的异常(Exception类)准备好处理代码。例如: 程序与数据源进行交互时数据库报错时抛出的SQLException的异常对象。 程序使用I/O资源时出现异常时抛出的IOException异常。 将应用试图通过字符串名加载类,但没有找到指定名称的类的定义时,会抛出 ClasNotFoundException异常对象。 续例 对于那个送口红的程序员来说,各类异常都会导致主进程中断。 (1)Eror运行环境的问题。它是送口红这个程序依附的,但这个程序无法解决 的问题,如程序员没有女朋友。 (2)UncheckedException是常识性的问题,一般人不会犯。程序员希望女友告 诉他她喜欢口红颜色的RGB值。 (3)CheckedException是程序员必须处理的问题。女友家有熊孩子,玩具必须 备上,否则口红难逃被偷去练书法的下场。 所有异常都是绑定在具体的方法之上的。会抛出异常的方法有成员方法、构造方法和 静态方法 t 。 i 例如: usrnitbiIdx) dxOuonsxei .Srng类的成员方法sbtig(negnne会抛出InetOfBudEcpton。 .String类的构造方法String(byte[]bytes,StringcharsetName)会抛出Unsupported EncodingException。 .Integer类中的静态方法parseInt(Stringstr)会抛出NumberFormatException。 在调用相关方法时可以多看看API文档,确定程序中调用的方法是否会抛出异常。如 果抛出异常,异常是什么类型的,以及抛出异常是CheckedException还是Unchecked Exception,了解这些情况对用户编写代码都是至关重要的。 1 60 面向大数据的Java 程序设计基础(微课版) 5.2 异常处理 本节将以java.lang包中的ArithmeticException异常为例介绍如何编写异常处理代 码。需要注意ArithmeticException是UncheckedException,这种异常即使不附加异常处 理代码,编译依然可以通过。基于这个UncheckedException,可以更全面地展示各种处理 异常的方法。通过比较,也可以更直观地看到Java中异常处理的思路和各种方案的优 缺点。 5.2.1 异常出现 这是一个有潜在风险的例子:两个随机整数相除,打印它们的商。如代码5-1所示,x 是1~3的随机数,y是0~2的随机数。若y随机到0,因为除数不能为0,程序会出现 ArithmeticException异常。 //代码5-1 可能出现异常的程序 public class TestArException { public static void computer(){ int x =(int) (1 +(Math.random() * 2)); //x=1,2,3 int y =(int) (Math.random() * 2); //y=0,1,2 int result; result =x/y; System.out.println("the result is:" +result); } public static void main(String[]args){ computer(); } } 某一次运行结果可能显示为: Exception in thread "main" java.lang.ArithmeticException:/by zero at ***.TestArException.computer(TestArException.java:8) at ***.TestArException.main(TestArException.java:12) 上面的程序在运行时报告了算术异常(ArithmeticException),程序运行停止,代码中输 出语句System.out.println("theresultis:"+result)未被执行,main()主线程在异常抛出处 中止。建 立黑白名单,程序可以通过添加一个if条件对除数的值进行测试,避免异常发生,如 代码5-2所示。 //代码5-2 使用if 语句屏蔽异常情况出现 public class TestArithmeticException_If { public static void computer() { int x =(int) (1 +(Math.random() * 2)); 第5 章 异常处理1 61 int y =(int) (Math.random() * 2); int result ; if (y !=0) { result =x/y; System.out.println("the result is:" +result); } else { System.out.println("Devisor cannot be zero"); } } public static void main(String[]args) { computer(); } } 如果y的值为0,那么运行结果为: Devisor cannot be zero 在代码5-2中,if分支语句用来防止出现除数y为0的情况。这意味着程序员需要预 先具备两个知识“除法中除零是一种错误,以及除数是y”这两条先验知识,同时将错误预防 代码嵌入程序当中。随着问题规模的不断扩大,这种上知天文、下知地理的要求,对于程序 设计人员来说几乎是不可能实现的。 现在解决异常的主流思路是将异常情况的处理交给专业处理异常的代码去处理。Java 中采用的就是“陷阱式”异常处理机制———如果不触发异常这个陷阱,程序正常运行;一旦触 发异常陷阱,生成异常对象,将异常对象转交给异常处理代码。这就是Java的异常处理方 法的核心。 5.2.2 主动异常处理———定义异常处理代码 Java使用try和catch关键字可以捕获程序段出现的异常,并将程序引向指定的异常 处理代码段。具体来说就是:我们需要通过查阅API文档或测试代码确定哪些语句会抛出 异常,抛出哪些类型的异常,然后按异常类型使用catch语句进行匹配异常,将程序导向异 常处理代码块。 主动异常处理的try-catch-finally代码块的Java语法为: try{ 语句块; //可能出现异常的语句块 } catch (***Exception e) { 语句块; //该类异常对象出现后的语句块 } finally { 语句块; //异常无论是否发生, 总是要执行的代码 } 在try…catch…finally代码块中: try语句块是必须存在且只能存在一块的语句块。用户将被监视运行的代码放入try 语句块中。如果被监视运行的语句抛出异常对象,则代码在异常出现处中断,转而在catch 1 62 面向大数据的Java 程序设计基础(微课版) 中查找可以匹配异常对象的引用变量(异常类型),并激活该catch语句段。 注意 try语句块中只会抛出一个异常对象,在第一个异常出现时立即中断try语句段 的代码执行。因为代码不再执行,即使后续语句也可以抛出异常对象,后续的异常对 象也不会出现。 catch语句也是必须存在的。它可以有多个并列的语句块。当try语句段中出现异常 对象时,catch会捕获到发生的异常,并执行相应的catch块中代码。需要额外说明的是,持 有异常对象的引用变量e是final的,用户不能在catch块中修改它的值。如果try段没有 抛出任何异常或抛出的异常无法匹配(需要委托处理),则被略过所有catch语句块。 finally语句块是可选的。finally语句块为异常处理代码提供了统一的出口,使得控制 流程在转到程序其他部分之前,能够对程序的状态做统一的管理。在finally语句中,通常 使用finally可以进行资源的清除、回收物理资源工作,如关闭打开的文件、删除临时文件、 变量还原默认值、网络的断开等。finally语句块紧跟在catch语句之后,无论try语句块和 catch语句块执行情况如何,该语句块都会被执行。 try…catch…finally代码块的执行流程为:若try段语句正常执行完毕,就会跳过catch 段,之后进入finally 段;若try 段中语句出现异常,则匹配catch 语句块,之后进入 finally段。 注意 (1)try、catch、finally三个代码块中变量的作用域为独立代码块,彼此之间局部 变量不能通用。若需要在代码块中统一使用一个变量,需要在异常处理代码块外声 明,如6.2.1节中所示的异常处理框架,在try段及finally段都需要使用变量fis的情 况下,将其声明在try…catch…finally代码块之外就是必然的选择。 (2)Java垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存 中对象所占用的内存。虽然物理资源的持有者会在其生命周期结束后自动释放,但 是对于相对稀缺的物理资源还是建议使用finally语句段立即释放回收,以免后续程 序因为物理资源访问受限出现异常。 这里使用主动异常处理技术对代码5-2进行改造,如代码5-3所示。 //代码5-3 主动异常处理除0 异常 public class TestTry { public static void computer(){ try { int x =(int) (1 +(Math.random() * 2)); int y =(int) (Math.random() * 2); int result =x/y; //可能产生异常的语句 System.out.println("the result is:" +result); } catch (ArithmeticException e) { System.out.println("ArithmeticException 类的对象出现"); e.printStackTrace(); } finally { 第5 章 异常处理1 63 System.out.println("这是finally 语句块,总会被执行的!"); } } public static void main(String[]args){ computer(); } } 在代码5-3中,程序的主体result=x/y放置在try语句块当中,被监视起来,而未对该 语句的运行做任何限制(如y! =0)。 如果x的值是2,y的值是1,那么try块中程序段正常执行,就像没有使用try段进行 包裹一样。结果如下。 the result is: 2 这是finally 语句块,总会被执行的! 如果y的值是0,那么result= x/y出现ArithmeticException异常对象。因为该语句 被try段监视,异常对象出现后正常代码中断,语句System.out.println("theresultis:" + result)不再执行,转而执行异常处理段代码: catch (ArithmeticException e) { System.out.println("ArithmeticException 类的对象出现"); e.printStackTrace(); } 输出结果如下。 ArithmeticException 类的对象出现 这是finally 语句块,总会被执行的! java.lang.ArithmeticException:/by zero at ***.TestTry.computer(TestTry.java:9) at ***.TestTry.main(TestTry.java:21) 上面例子中对捕获到的异常对象的处理方式虽然仅仅是输出了异常的信息,但是有这 个框架,用户就可以将更专业的异常处理代码引入其中。另外,为了方便debug程序,一般 会使用printStackTrace()方法打印异常对象e中包含的异常堆栈信息。 在主动异常处理过程中,业务逻辑与异常处理代码在一个方法内实现了分离,业务逻辑 在try段,异常处理代码在catch语句群中。这种代码的分离可以让核心业务逻辑更加 清晰。 5.2.3 委托异常处理———方法抛出异常 在主动异常处理的过程中,业务代码和异常处理代码的分离是在方法体内实现的。比 方法体内分离更彻底的分离就是将异常处理代码分离到方法体之外,这就需要使用异常处 理的第二种技术:委托异常处理。 如果方法体内出现异常对象,本方法不处理,用户可以将异常对象返回给方法的调用者 1 64 面向大数据的Java 程序设计基础(微课版) (就像发现犯罪要报警一样),主逻辑与异常处理分离到不同的方法当中了。在代码层面,只 需要在可能出现异常对象的方法声明处,通过throws关键字声明在运行过程中方法体抛出 异常类型即可。一个方法可以同时声明抛出多种异常,各异常类之间用逗号隔开,Java的 基本语法为: 访问控制字符其他状态修饰符返回值类型方法名(参数列表) throws Exception1,Exception2,… { //语句块; } 需要注意的是: (1)throws后罗列的异常类型必须可以覆盖所有可能抛出异常对象。这里“可能抛 出”还包括方法体主动处理异常的漏网之鱼,即catch语句段无法匹配处理的异常类型。 (2)对于所有可能抛出的异常类型,用户可以通过简单罗列的方式或写父类异常类覆 盖若干子类异常的方式编写,但一定要全面覆盖方法体中可能出现的异常对象涉及的种类。 一个极端的写法就是throwsException,但这样的“覆盖”无助于后继程序识别异常对象的 种类,因此标准Java代码一般会选择在throws语句后罗列所有的异常类。 (3)throws中委托抛出的异常会由最精确的子类类型异常类型变量所持有,与throws 后面登记的异常类型顺序无关。例如: public void f() throws IOException, FileNotFoundException{ … } FileNotFoundException继承自IOException,若f()方法中出现FileNotFoundException类 型的异常,则该异常由FileNotFoundException类型变量持有,而不是IOException类型变量, 尽管IOException写在前面。 异常处理将程序员从庞大的异常维护if分支中解放出来,这也意味着Java的异常处理 具有if分支的逻辑功能,在Java中通过throws关键字声明方法可能抛出的异常,这些异常 对象的出现就可以理解为一个激活分支的信号,让方法调用者可以以信号为契机做一些分 支操作。只不过通过throws语法实现的分支跨越了方法。这种向外throws异常对象行 为,像是一种令人鄙视的甩锅操作,然而,这种行为也为用户提供了解决问题的契机。这种 将解决问题的契机交给用户自行处理是一种开放的编程态度,也是Java中异常处理的核心 设计理念,因此,我们在Java的API中经常可以看到方法会抛出异常。 使用这种方法本身不处理异常的方案,可以极大地提高程序设计的自由度。例如,一个 检查密码匹配的方法只专心于“检查用户名与密码是否匹配”这个主要的业务逻辑即可,至 于用户名不存在或密码为空等问题被封装为异常对象,由本方法抛出,提醒方法调用者专门 处理用户名不存在和密码为空两种情况。前面除0问题使用throws技术,委托调用方法处 理异常的代码如代码5-4所示。 //代码5-4 委托处理除0 异常 public class TestThrows { //委托调用方法处理异常 public static void computer() throws ArithmeticException{ 第5 章 异常处理1 65 int x =(int) (1 +(Math.random() * 2)); int y =(int) (Math.random() * 2); int result; result =x/y; System.out.println("the result is:" +result); } public static void exp(ArithmeticException e){ System.out.println("ArithmeticException 类的对象出现"); e.printStackTrace(); } public static void main(String []args){ try { //在调用方法中主动处理异常 computer(); }catch (ArithmeticException e) { exp(e); } } } 如果y的值是0,那么运行结果如下。 ArithmeticException 类的对象出现 java.lang.ArithmeticException:/by zero at ***.TestThrows.computer(TestThrows.java:8) at ***.TestThrows.main(TestThrows.java:14) 在上述代码中,computer()方法本地不处理ArithmeticException对象,而是将这个异常委 托给调用它的main()方法。在main()方法中,程序员可以选择主动处理,如代码5-3所示,也 可以不处理,将其抛出,委托给调用main()方法的JVM,而JVM 使用默认处理异常对象的方 法printStackTrace()打印异常堆栈。例如,main()方法抛出ArithmeticException异常的写法 如下。 public static void main(String []args) throws ArithmeticException { computer(); } 如果y的值是0,那么运行结果如下。 Exception in thread "main" java.lang.ArithmeticException: /by zero at ***.TestThrows.computer(TestThrows.java:9) at ***.TestThrows.main(TestThrows.java:22) 与代码5-1的错误显示一致。这就是默认的Java异常处理RuntimeException的方 法,从出现RuntimeException对象后,逐层上报到main()方法、JVM,然后打印异常堆栈 信息。