学习目标
- 要想开发出健壮的应用,就必须认真掌握这部分知识。否则用户随时可以因为你程序中有未捕获的异常而对程序产生不满甚至立即卸载。
- 认真地对待每一个示例,并且一定要亲自动手运行它们
- 介绍了“捕获JDK异常转换为业务逻辑异常”的异常处理策略,这在多层应用中很有用,需要特别注意
异常处理基础
- 异常(Exception):发生于程序执行期间,表明出现了一个非法的运行情况。许多JDK中的方法在检测到非法情况时,都会抛出一个异常对象。
关于异常
- 异常处理的目的是依据实际情况提供不同的错误应对策略与手段,使程序更稳定,更安全
- 异常处理的主要用途是提供准确的错误消息,解释失败原因、位置和错误类型等,同时提供一定的恢复能力,尽可能地保证数据完整性不被破坏,并让程序能继续运行
Java中的异常捕获语句
Java中所有可捕获的异常都派生自Exception类
try{
//可能发生运行错误的代码
}
catch(异常类型 异常对象的引用){
//用于处理异常的代码
}
finally{
//用于“善后”的代码
}
使用Java异常处理机制
- 把可能会发生错误的代码放进try语句块中
- 当程序检测到出现了一个错误时会抛出一个异常对象。异常处理代码会捕获并处理这个错误,catch语句块中的代码用于处理错误
- 当异常发生时,程序控制流程由try语句块跳转到catch语句块
- 不管是否有异常发生,finally语句块中的语句始终保证被执行
- 如果没有提供合适的异常处理代码,JVM将会结束掉整个应用程序
JDK中与异常相关的类
Java中的异常分类
- Throwable类有两个直接子类:
- Exception:出现的问题是可以被捕获的;
- Error:系统错误,通常由JVM处理
- 可捕获的异常又可以分为两类:
- check异常:直接派生自Exception的异常类,必须被捕获或再次声明抛出
- runtime异常:派生自RuntimeException的异常类。使用throw语句可以随时抛出这种异常对象:
throw new ArithmeticException(...);
Error示例:AssertionError
- JDK1.4以上提供了assert语句,允许程序在运行期间判断某个条件是否满足,不满足时,抛出AssertionError
另一个著名的Error——OOM Error
- 在Android应用中,我们会经常遇到一个Error,就是“OutOfMemoryError”,表示系统内存不足,这个Error简写为“OOM Error”
- Android应用之所以经常发生OOM,其原因在于手机的内存有限,在处理大图片等耗费大量内存资源的工作时,会耗尽可用内存
- 与一般Error不太一样,在Android应用中通常需要catch OOM并手动释放内存,以避免Android强制结束引发OOM的应用程序
public static void main(String[] args) {
try {
double data = 100 / 0.0;
("the answer is:" + data);
if(String.valueOf(data).equals("Infinity"))
{
System.out.println("In Here" );
}
throw new ArithmeticException("≥");
}
catch(ArithmeticException e) {
System.out.println(e);
}
}
运行结果:
如果定义
int i=1,j=0,k;
k=i/j;
//会引发异常
double d1=100,d2=0,result;
result=d1/d2;
System.out.println("浮点数除以0“+data);
//运行时不会引发异常
浮点数除以0:Infinity(无穷大)
异常的“多态”特性
- 可以有多个catch语句块,每个代码块捕获一种异常。在某个try块后有两个不同的catch块捕获两个相同类型的异常是语法错误
- 使用catch语句,只能捕获Exception类及其子类的对象。因此,一个捕获Exception对象的catch语句块可以捕获所有“可捕获”的异常
- 将catch(Exception e)放在别的catch块前面会使这些catch块都不执行,因此Java不会编译这个程序
finally的功用
- 资源泄漏:当一个资源不再被某应用程序使用,但此程序并未向系统声明不再使用此资源时发生这种情况
- finally语句块主要用于解决一些资源泄露问题,它位于catch语句块之后,JVM保证它们一定执行
- ⚠️finally语句块中也可能发生异常,如果这种情况发生,先前的异常被放弃
自动释放的资源
- try-with-resources
try(ClassName theObject = new ClassName())
{
//use theObject here
}
catch(Exception e)
{
//catch exception that occur while using the resource
}
要求ClassName指定的类型必须实现AutoCloseable接口
- AutoCloseable接口的定义如下:
public interface AutoCloseable{
void close() throws Exception;
}
当程序执行流程离开try-with-resources 结构时,AutoCloseable接口所定义的close()方法会被自动调用,因此,无需在finally子句中显示调用close()方法
多层异常的捕获
运行结果:
运行结果:
多层嵌套
public class welcome1 {
public static void main(String args[]) {
int result;
try {
System.out.println("in Level 1");
try {
System.out.println("in Level 2");
// result=100/0; //Level 2
//level3的try和finally语句块均执行
//在level2捕获执行catch,然后依次执行finally
try {
System.out.println("in Level 3");
// result=100/0; //Level 3
//当exception在第三层被捕获时,第三层所有try-catch-finally正常执行
//后面语句的catch均不执行,但是后面语句的finally顺序依次执行
}
catch (Exception e) {
System.out.println("Level 3:" + e.getClass().toString());
}
finally {
System.out.println("In Level 3 finally");
}
//result=100/0; //Level 2
}
catch (Exception e) {
System.out.println("Level 2:" + e.getClass().toString());
} finally {
System.out.println("In Level 2 finally");
}
//result = 100 / 0; //level 1
}
catch (Exception e) {
System.out.println("Level 1:" + e.getClass().toString());
}
finally {
System.out.println("In Level 1 finally");
}
}
}
- 当有多个嵌套的try…catch…finally时,要特别注意finally的执行实际
- 特别注意:当有多层嵌套的finally时,异常在不同的层次抛出,在不同的位置抛出,可能会导致不同的finally语句块执行顺序
finally语句块一定会执行吗?
public static void main(String[] args)
{
try{
System.out.println("in main");
throw new Exception("Exception is thrown in main");
//System.exit(0);error
}
catch(Exception e)
{
System.out.println(e.getMessage());
System.exit(0);
}
finally
{
System.out.println("in finally");
}
}
当exit在try块中时执行结果如下:
当exit在catch块中的执行结果如下:
finally块不一定会被执行,但是【如果抛出的异常没有被捕获,向上找也没有能够捕获异常的catch,程序直接退出发生error】,这就相当于直接exit,所以出现error
如何跟踪异常的传播途径?
- 当程序中出现异常时,JVM会根据方法调用顺序依次查找有关的错误处理程序
- 可使用printStackTrace和getMessage方法了解异常发生情况
- printStackTrace:打印方法调用堆栈
- 每个Throwable类的对象都有一个getMessage方法,它返回一个字串,这个字串是在Exception构造函数中传入的,通常让这一字串包含特定异常的相关信息
public static void main( String args[] )
{
try {
method1();
}
catch ( Exception e ) {
System.err.println( e.getMessage() + "\n" );
e.printStackTrace();
}
}
public static void method1() throws Exception
{
method2();
}
public static void method2() throws Exception
{
method3();
}
public static void method3() throws Exception
{
throw new Exception( "Exception thrown in method3" );
}
}
执行流程:
method1开始执行然后调用method2,接着调用method3,在method3中抛出异常Exception,然后返回method2,接着返回method1,在最初的catch中被捕获,打印出exception 的信息,
然后执行printStackTrace打印出栈的轨迹
受控“异常”
throws语句
- throws语句表明某方法中可能出现某种(或多种)异常,但它自己不能处理这种异常,而需要调用者来处理
- 当一个方法包含throws子句时,需要调用此方法的代码中使用try/catch/finally进行捕获,或者是重新对其进行声明,否则编译时报错
受控与不受控异常
- throws语句中声明的异常称为受控的异常,通常直接派生自Exception类
- RuntimeException(其基类为Exception)和Error(基类为Throwable)称为非受控的异常。这种异常不用在throws语句中声明
抛出多个受控异常的方法
- 一个方法可以声明抛出多个异常
int func(float h)throws OneException,TwoException{
//...
}
- 注意一个Java异常处理中的一个比较独特的地方:
当一个方法声明抛出多个异常时,在此方法调用语句处只要catch其中任何一个异常,代码就可以顺利编译
子类抛出受控异常的限制
- 一个子类的throws子句抛出的异常,不能是其基类同名方法抛出的异常对象的父类
public class OverrideThrows
{
public void test()throws IOException
{
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows
{
public void test() throws FileNotFoundException
{
//...
}
}
分析:overrideThrows类是Sub类的父类,则在override与sub总同名方法抛出的异常类型应该是sub同名方法异常类型的父类或二者相同
自定义异常与异常处理链
实际开发中的异常处理
创建自己的异常类
- 自定义异常通常选择直接派生自Exception:
class MyException extends Exception{...}
- 在合适的地方使用throw语句抛出自定义异常对象:
class MyClass{
void someMethod(){
if(条件)throw new MyException();
}
}
异常处理类
- 在实际开发中,经常需要将待定的“过于专业”的异常转换为一个“业务”异常,然后在调用者处进行捕获或处理
- 典型的异常处理代码模版
package welcome;
class MyException extends Exception
{
public MyException(String Message) {
super(Message);
}
public MyException(String message, Throwable cause) {
super(message, cause);
}
public MyException( Throwable cause) {
super(cause);
}
}
public class welcome1 {
public static void main( String args[] )
{
try {
throwExceptionMethod();
}
catch ( MyException e )
{
System.err.println( e.getMessage() );
System.err.println(e.getCause().getMessage());
}
catch ( Exception e )
{
System.err.println( "Exception handled in main" );
}
doesNotThrowException();
}
public static void throwExceptionMethod() throws MyException
{
try {
System.out.println( "Method throwException" );
throw new Exception("œµÕ≥‘À–– ±“˝∑¢µƒÃÿ∂®µƒ“Ï≥£");
}
catch( Exception e )
{
System.err.println(
"Exception handled in method throwException" );
throw new MyException("‘⁄∑Ω∑®÷¥–– ±≥ˆœ÷“Ï≥£",e);
}
finally {
System.err.println(
"Finally executed in throwException" );
}
// any code here would not be reached
}
public static void doesNotThrowException()
{
try {
System.out.println( "Method doesNotThrowException" );
}
catch( Exception e )
{
System.err.println( e.toString() );
}
finally {
System.err.println(
"Finally executed in doesNotThrowException" );
}
System.out.println(
"End of method doesNotThrowException" );
}
}
- 在实际开发中,可以参照ExceptionLinkInRealWorld.java示例的做法,定义一些与业务逻辑相关的自定义异常类,供上层代码进行捕获,从而能更精确的反映系统真实运行情况并及时进行处理
关于开发中异常处理的建议
- 在中间层组件中抛出异常,在界面层组件中捕获异常
- 在底层组件中捕获JVM抛出的“只有程序员能看懂的”异常,转换为中间层的业务逻辑异常,再由界面层捕获以提供有意义的信息
- 自身能够处理的异常,不要再向外界抛出
- 尽可能地在靠近异常发生的地方捕获并处理异常
- 尽可能地捕获最具体的异常类型,不要在中间层用catch(Exception)“吃掉”所有异常
- 在开发阶段捕获并显示所有异常信息,发布阶段要移除部分代码,以避免“过于专业“的异常信息困扰用户,特别地,系统发布之后,不要将服务端异常的详细信息发给客户端,以免被黑客利用
异常是程序中的一些错误,但并不是所有的错误都是异常,错误有时候是可以避免的。异常的对象有两个来源,一是Java运行时环境自动抛出系统生成的异常,而不管你是否愿意捕获和处理,它总要被抛出!比如除数为0的异常。二是程序员自己抛出的异常,这个异常可以是程序员自己定义的,也可以是Java语言中定义的,用throw 关键字抛出异常,这种异常常用来向调用者汇报异常的一些信息。
异常是针对方法来说的,抛出、声明抛出、捕获和处理异常都是在方法中进行的。
Java异常处理通过5个关键字try、catch、throw、throws、finally进行管理。基本过程是用try语句块包住要监视的语句,如果在try语句块内出现异常,则异常会被抛出,你的代码在catch语句块中可以捕获到这个异常并做处理;还有以部分系统生成的异常在Java运行时自动抛出。你也可以通过throws关键字在方法上声明该方法要抛出异常,然后在方法内部通过throw抛出异常对象。finally语句块会在方法执行return之前执行。
使用try…catch捕获异常。
执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块并把该异常对象交给catch块处理那这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。
Java的异常体系:
访问异常讯息。
如果程序需要在catch块中访问遗常对象的相关信息,可以通过调用catch后异常形参的方法来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将该异常对象赋给catch块后的异常参数,程序就可以通过该参数来获得该异常的相关信息。所有的异常对象都包含了如下几个常用方法:getMessage():返回该异常的详细描述字串。printStackTrace():将该异常的跟踪栈信息输出到标准错误信息输出。printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。getStackTrace():返回该异常的跟踪栈信息。
异常处理:
try
{
需要检测的代码;
}
catch(异常类 变量)
{
异常处理代码;
}
finally
{
一定会执行的代码;
}
异常处理的嵌套。
异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程既可放在try块里,也可放在catch块里,也可放在finally块里。
异常处理嵌套的深度没有明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。
checked异常与Runtime异常:
Java的异常被分为两大类:Checked异常和Runtime异常(运行时异常)。所有RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。
Checked异常的处理:
当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修改该异常。当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。
Runtime异常的处理:
Runtime异常则更加灵活,Runtime异常无需显示声明抛出。如果程序捕捉Runtime异常,也可以使用try…catch块来捕捉Runtime异常。
使用throws声明抛出异常:
throws声明抛出异常的思路是:当前方法不知道应该如何这种类型的异常,该异常应该由上一级调用者处理,如果main方法也不知道应该如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常交给JVM处理。JVM对异常的处理方法是:打印异常跟踪栈信息,并中止程序的运行,这就是程序在遇到异常后自动结束的原因。
throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开。
如果需要在程序中自行抛出异常,应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的语法格式如下:throw ExceptionInstance;
如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块里,显示捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给方法的调用者处理。
异常处理原则:
不要过度使用异常;
不要使用过于庞大的try块;
避免使用Catch AII 语句;
不要忽略捕获到异常。