写点什么

Java 提高篇——Java 中的异常处理(绝对详细,建议收藏

  • 2021 年 11 月 11 日
  • 本文字数:6964 字

    阅读完需:约 23 分钟

提示


对于运行时异常、错误和检查异常,Java 技术所要求的异常处理方式有所不同。


由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java 规定,运行时异常将由 Java 运行时系统自动抛出,允许应用程序忽略运行时异常。


对于方法运行中可能出现的 Error,当运行方法不予捕捉时,Java 允许该方法不做任何抛出声明。因为,大多数 Error 异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。


对于所有的检查异常,Java 规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉检查异常时,它必须声明将抛出异常。


Java 异常处理涉及到五个关键字,分别是:try、catch、finally、throw、throws。下面将骤一介绍,通过认识这五个关键字,掌握基本异常处理知识。


try -- 用于监听。将要被监听的代码(可能抛出异常的代码)放在 try 语句块之内,当 try 语句块内发生异常时,异常就被抛出。


catch -- 用于捕获异常。catch 用来捕获 try 语句块中发生的异常。


finally -- finally 语句块总是会被执行。它主要用于回收在 try 块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有 finally 块,执行完成之后,才会回来执行 try 或者 catch 块中的 return 或者 throw 语句,如果 finally 中使用了 return 或者 throw 等终止方法的语句,则就不会跳回执行,直接停止。


throw -- 用于抛出异常。


throws -- 用在方法签名中,用于声明该方法可能抛出的异常。


异常处理的基本语法


=========


1. try-catch


try{


//code?that?might?generate?exceptions


}catch(Exception?e){


//the?code?of?handling?exception1


}catch(Exception?e){


//the?code?of?handling?exception2


}


要明白异常捕获,还要理解监控区域(guarded region)的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。


因而可知,上述 try-catch 所描述的即是监控区域,关键词 try 后的一对大括号将一块可能发生异常的代码包起来,即为监控区域。Java 方法在运行过程中发生了异常,则创建异常对象。将异常抛出监控区域之外,由 Java 运行时系统负责寻找匹配的 catch 子句来捕获异常。若有一个 catch 语句匹配到了,则执行该 catch 块中的异常处理代码,就不再尝试匹配别的 catch 块了。


匹配的原则是:如果抛出的异常对象属于 catch 子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与 catch 块捕获的异常类型相匹配。


举个例子算术异常:


public?class?TestException?{


public?static?void?main(String[]?args)?{


int?a?=?1;


int?b?=?0;


try?{?//?try 监控区域


if?(b?==?0)?throw?new?ArithmeticException();?//?通过 throw 语句抛出异常


System.out.println("a/b 的值是:"?+?a?/?b);


System.out.println("this?will?not?be?printed!");


}


catch?(ArithmeticException?e)?{?//?catch 捕捉异常


System.out.println("程序出现异常,变量 b 不能为 0!");


}


System.out.println("程序正常结束。");


}


}


运行结果:


D:\java>java?TestException


程序出现异常,变量 b 不能为 0!


程序正常结束。


显示一个异常的描述,Throwable 重载了 toString()方法(由 Object 定义),所以它将返回一个包含异常描述的字符串。例如,将前面的 catch 块重写成:


catch?(ArithmeticException?e)?{?//?catch 捕捉异常


System.out.println("程序出现异常"+e);


}


结果:


D:\java>java?TestException


程序出现异常 java.lang.ArithmeticException


程序正常结束。


根据前面讲述的,算术异常属于运行时异常,因而实际上该异常不需要程序抛出,运行时系统自动抛出,将例子改为如下:


public?class?TestException?{


public?static?void?main(String[]?args)?{


int?a?=?1;


int?b?=?0;


System.out.println("a/b 的值是:"?+?a?/?b);


System.out.println("this?will?not?be?printed!");


}}


结果:


D:\java>java?TestException


Exception?in?thread?"main"?java.lang.ArithmeticException:?/?by?zero


at?TestException.main(TestException.java:7)


使用多重的 catch 语句:很多情况下,由单个的代码段可能引起多个异常。处理这种情况,我们需要定义两个或者更多的 catch 子句,每个子句捕获一种类型的异常,当异常被引发时,每个 catch 子句被依次检查,第一个匹配异常类型的子句执行,当一个 catch 子句执行以后,其他的子句将被旁路。


编写多种 catch 语句块注意事项:


顺序问题:先小后大,即先子类后父类



Java 通过异常类描述异常类型。对于有多个 catch 子句的异常程序而言,应该尽量将捕获底层异常类的 catch 子句放在前面,同时尽量将捕获相对高层的异常类的 catch 子句放在后面。否则,捕获底层异常类的 catch 子句将可能会被屏蔽。


RuntimeException 异常类包括运行时各种常见的异常,ArithmeticException 类和 ArrayIndexOutOfBoundsException 类都是它的子类。因此,RuntimeException 异常类的 catch 子句应该放在最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误。


嵌套 try 语句:try 语句可以被嵌套。也就是说,一个 try 语句可以在另一个 try 块的内部。每次进入 try 语句,异常的前后关系都会被推入堆栈。如果一个内部的 try 语句不含特殊异常的 catch 处理程序,堆栈将弹出,下一个 try 语句的 catch 处理程序将检查是否与之匹配。这个过程将继续直到一个 catch 语句被匹配成功,或者是直到所有的嵌套 try 语句被检查完毕。如果没有 catch 语句匹配,Java 运行时系统将处理这个异常。


例如:


class?NestTry{


public?static?void?main(String[]?args){


try{


int?a?=?args.length;


int?b?=?42?/?a;


System.out.println("a?=?"+?a);


try{


if(a?==?1){


a?=?a/(a-a);}if(a?==?2){


int?c[]?=?{1};


c[42]?=99;


}}catch(ArrayIndexOutOfBoundsException?e){


System.out.println("ArrayIndexOutOfBounds?:"+e);


}}catch(ArithmeticException?e){


System.out.println("Divide?by?0"+?e);


}}}


正如程序中所显示的,该程序在一个 try 块中嵌套了另一个 try 块。程序工作如下:当你在没有命令行参数的情况下执行该程序,外面的 try 块将产生一个被 0 除的异常。程序在有一个命令行参数条件下执行,由嵌套的 try 块产生一个被 0 除的异常,由于内部的 catch 块不匹配这个异常,它将把异常传给外部的 try 块,在外部异常被处理。如果你在具有两个命令行参数的条件下执行该程序,将由内部 try 块产生一个数组边界异常。


结果:


D:\java>javac?estTry.java


D:\java>>java?NestTryDivide?by?0?java.lang.ArithmeticExceptio:?/?by?zero


D:\java>java?NestTry?onea?=?1


Divide?by?0java.lang.ArithmeticException:?/?by?zero


D:\java>java?NestTry?one?twoa?=?2


ArrayIndexOutOfBounds?:java.lang.ArrayIndexOutOfBoundsException:?42


注意


当有方法调用时,try 语句的嵌套可以很隐蔽的发生。例如,我们可以将对方法的调用放在一个 try 块中。在该方法的内部,有另一个 try 语句。在这种情况下,方法内部的 try 仍然是嵌套在外部调用该方法的 try 块中的。下面我们将对上述例子进行修改,嵌套的 try 块移到方法 nesttry()的内部:


class?NestTry{


static?void?nesttry(int?a){


try{


if(a?==?1){


a?=?a/(a-a);}if(a?==?2){


int?c[]?=?{1};


c[42]?=99;


}}catch(ArrayIndexOutOfBoundsException?e){


System.out.println("ArrayIndexOutOfBounds?:"+e);


}}public?static?void?main(String[]?args){


try{


int?a?=?args.length;


int?b?=?42?/?a;


System.out.println("a?=?"+?a);


nesttry(a);}catch(ArithmeticException?e){


System.out.println("Divide?by?0"+?e);


}}}


结果输出与前面例子一致:


D:\java>javac?NestTry.java


D:\java>java?NestTryDivide?by?0java.lang.ArithmeticException:?/?by?zero


D:\java>java?NestTry?onea?=?1


Divide?by?0java.lang.ArithmeticException:?/?by?zero


D:\java>java?NestTry?one?twoa?=?2


ArrayIndexOutOfBounds?:java.lang.ArrayIndexOutOfBoundsException:?42


}


2. throw


到目前为止,我们只是获取了被 Java 运行时系统引发的异常。然而,我们还可以用 throw 语句抛出明确的异常。Throw 的语法形式如下:


throw?ThrowableInstance;


这里的 ThrowableInstance 一定是 Throwable 类类型或者 Throwable 子类类型的一个对象。简单的数据类型,例如 int,char,以及非 Throwable 类,例如 String 或 Object,不能用作异常。有两种方法可以获取 Throwable 对象:在 catch 子句中使用参数或者使用 new 操作符创建。


程序执行完 throw 语句之后立即停止;throw 后面的任何语句不被执行,最邻近的 try 块用来检查它是否含有一个与异常类型匹配的 catch 语句。如果发现了匹配的块,控制转向该语句;如果没有发现,以包围的 try 块来检查,以此类推。如果没有发现匹配的 catch 块,默认异常处理程序中断程序的执行并且打印堆栈轨迹。


例如:


class?TestThrow{


static?void?proc(){


try{


throw?new?NullPointerException("demo");


}catch(NullPointerException?e){


System.out.println("Caught?inside?proc");


throw?e;


}}public?static?void?main(String?[]?args){


try{


proc();}catch(NullPointerException?e){


System.out.println("Recaught:?"+e);


}}}


结果:


D:\java>java?TestThrow


Caught?inside?proc


Recaught:?java.lang.NullPointerException:?demo


该程序两次处理相同的错误,首先,main()方法设立了一个异常关系然后调用 proc()。proc()方法设立了另一个异常处理关系并且立即抛出一个 NullPointerException 实例,NullPointerException 在 main()中被再次捕获。


该程序阐述了怎样创建 Java 的标准异常对象,特别注意这一行:


throw?new?NullPointerException("demo");


此处 new 用来构造一个 NullPointerException 实例,所有的 Java 内置的运行时异常有两个构造方法:一个没有参数,一个带有一个字符串参数。当用第二种形式时,参数指定描述异常的字符串。如果对象用作 print()或者 println()的参数时,该字符串被显示。这同样可以通过调用 getMessage()来实现,getMessage()是由 Throwable 定义的。


3. throws


如果一个方法可以导致一


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


个异常但不处理它,它必须指定这种行为以使方法的调用者可以保护它们自己而不发生异常。要做到这点,我们可以在方法声明中包含一个 throws 子句。一个 throws 子句列举了一个方法可能引发的所有异常类型。这对于除了 Error 或 RuntimeException 及它们子类以外类型的所有异常是必要的。一个方法可以引发的所有其他类型的异常必须在 throws 子句中声明,否则会导致编译错误。


下面是 throws 子句的方法声明的通用形式:


public?void?info()?throws?Exception


{//body?of?method


}


Exception 是该方法可能引发的所有的异常,也可以是异常列表,中间以逗号隔开。


例如:


class?TestThrows{


static?void?throw1(){


System.out.println("Inside?throw1?.?");


throw?new?IllegalAccessException("demo");


}public?static?void?main(String[]?args){


throw1();}}


上述例子中有两个地方存在错误,你能看出来吗?


该例子中存在两个错误,首先,throw1()方法不想处理所导致的异常,因而它必须声明 throws 子句来列举可能引发的异常即 IllegalAccessException;其次,main()方法必须定义 try/catch 语句来捕获该异常。


正确例子如下:


class?TestThrows{


static?void?throw1()?throws?IllegalAccessException?{


System.out.println("Inside?throw1?.?");


throw?new?IllegalAccessException("demo");


}public?static?void?main(String[]?args){


try?{


throw1();}catch(IllegalAccessException?e?){


System.out.println("Caught?"?+?e);


}}}


Throws 抛出异常的规则:


如果是不受检查异常(unchecked exception),即 Error、RuntimeException 或它们的子类,那么可以不使用 throws 关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。


必须声明方法可抛出的任何检查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用 try-catch 语句捕获,要么用 throws 子句声明将它抛出,否则会导致编译错误


仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。


调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。


4. finally


当异常发生时,通常方法的执行将做一个陡峭的非线性的转向,它甚至会过早的导致方法返回。例如,如果一个方法打开了一个文件并关闭,然后退出,你不希望关闭文件的代码被异常处理机制旁路。finally 关键字为处理这种意外而设计。


finally 创建的代码块在 try/catch 块完成之后另一个 try/catch 出现之前执行。finally 块无论有没有异常抛出都会执行。如果抛出异常,即使没有 catch 子句匹配,finally 也会执行。一个方法将从一个 try/catch 块返回到调用程序的任何时候,经过一个未捕获的异常或者是一个明确的返回语句,finally 子句在方法返回之前仍将执行。这在关闭文件句柄和释放任何在方法开始时被分配的其他资源是很有用。


finally 子句是可选项,可以有也可以无,但是每个 try 语句至少需要一个 catch 或者 finally 子句。


class?TestFinally{


static?void?proc1(){


try{


System.out.println("inside?proc1");


throw?new?RuntimeException("demo");


}finally{


System.out.println("proc1's?finally");


}}static?void?proc2(){


try{


System.out.println("inside?proc2");


return?;


}?finally{


System.out.println("proc2's?finally");


}}static?void?proc3(){


try{


System.out.println("inside?proc3");


}finally{


System.out.println("proc3's?finally");


}}public?static?void?main(String?[]?args){


try{


proc1();}catch(Exception?e){


System.out.println("Exception?caught");


}proc2();proc3();}}


该例子中,proc1()抛出了异常中断了 try,它的 finally 子句在退出时执行。proc2 的 try 语句通过 return 语句返回,但在返回之前 finally 语句执行。在 proc3()中 try 语句正常执行,没有错误,finally 语句也被执行。


输出结果:


D:\java>java?TestFinally


inside?proc1proc1's?finally


Exception?caughtinside?proc2proc2's?finally


inside?proc3proc3's?finally


注:如果 finally 块与一个 try 联合使用,finally 块将在 try 结束之前执行。


问题扩展(面试题):


1、try{} 里有一个 return 语句,那么紧跟在这个 try 后的 finally{} 里的 code 会不会被执行,什么时候被执行,在 return 前还是后?


答案:会执行,在方法返回调用者前执行。


注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try 中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误,Eclipse 中可以在如图所示的地方进行设置,强烈建议将此项设置为编译错误。



2、Java 语言如何进行异常处理,关键字:throws、throw、try、catch、finally 分别如何使用?


答:Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。一般情况下是用 try 来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try 用来指定一块预防所有异常的程序;catch 子句紧跟在 try 块后面,用来指定你想要捕获的异常的类型;throw 语句用来明确地抛出一个异常;throws 用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟);finally 为确保一段代码不管发生什么异常状况都要被执行;try 语句可以嵌套,每当遇到一个 try 语句,异常的结构就会被放入异常栈中,直到所有的 try 语句都完成。如果下一级的 try 语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的 try 语句或者最终将异常抛给 JVM。


3、运行时异常与受检异常有何异同?


答:异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java 编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对象程序设计中经常被滥用的东西,在 Effective Java 中对异常的使用给出了以下指导原则:


- 不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调用者为了正常的控制流而使用异常)


- 对可以恢复的情况使用受检异常,对编程错误使用运行时异常


- 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)


- 优先使用标准的异常


- 每个方法抛出的异常都要有文档


- 保持异常的原子性


- 不要在 catch 中忽略掉捕获到的异常


4、列出一些你常见的运行时异常?


答:

评论

发布
暂无评论
Java提高篇——Java中的异常处理(绝对详细,建议收藏