一、異常簡介
什麼是異常?
異常就是有異於常態,和正常情況不一樣,有錯誤出錯。在java中,阻止目前方法或作用域的情況,稱之為異常。
java中異常的體係是怎麼樣的呢? (建議:java影片教學)
1.Java中的所有不正常類別都繼承於Throwable類別。 Throwable主要包括兩個大類,一個是Error類,另一個是Exception類;
#2.其中Error類中包括虛擬機錯誤和線程死鎖,一旦Error出現了,程式就徹底的掛了,被稱為程式終結者;
#3.Exception類,也就是通常所說的「異常」。主要指編碼、環境、使用者操作輸入出現問題,Exception主要包括兩大類,非檢查異常(RuntimeException)和檢查異常(其他的一些異常)
4 .RuntimeException異常主要包括以下四種異常(其實還有很多其他異常,這裡不一一列出):空指標異常、陣列下標越界異常、型別轉換異常、算術異常。 RuntimeException異常會由java虛擬機器自動拋出並自動捕獲(就算我們沒寫異常捕獲語句運行時也會拋出錯誤!!),此類異常的出現絕大數情況是代碼本身有問題應該從邏輯上去解決並改進程式碼。
5.檢查異常,造成該異常的原因多種多樣,例如檔案不存在、或是連線錯誤等等。跟它的「兄弟」RuntimeException運行異常不同,該異常我們必須手動在程式碼裡添加捕獲語句來處理該異常,這也是我們學習java異常語句中主要處理的異常對象。
二、try-catch-finally語句
(1)try區塊:負責捕獲異常,一旦try中發現異常,程式的控制權將移交給catch區塊中的異常處理程序。
【try語句區塊不可以獨立存在,必須與 catch 或 finally 區塊同存】
(2)catch區塊:如何處理?例如發出警告:提示、檢查配置、網路連接,記錄錯誤等。執行完catch區塊之後程式跳出catch區塊,繼續執行後面的程式碼。
【編寫catch塊的注意事項:多個catch塊處理的異常類,要按照先catch子類後catch父類的處理方式,因為會【就近處理】異常(由上自下) 。 】
(3)finally:最終執行的程式碼,用於關閉和釋放資源。
語法格式如下:
try{ //一些会抛出的异常 }catch(Exception e){ //第一个catch //处理该异常的代码块 }catch(Exception e){ //第二个catch,可以有多个catch //处理该异常的代码块 }finally{ //最终要执行的代码 }
當異常出現時,程式將終止執行,交由異常處理程序(拋出提醒或記錄日誌等),異常代碼區塊外程式碼正常執行。 try會拋出很多種類型的異常,由多個catch區塊捕捉多鐘錯誤。
多重異常處理程式碼區塊順序問題:先子類別再父類別(順序不對編譯器會提醒錯誤),finally語句區塊處理最終將要執行的程式碼。
接下來,我們用實例來鞏固try-catch語句吧~
先看範例:
package com.hysum.test; public class TryCatchTest { /** * divider:除数 * result:结果 * try-catch捕获while循环 * 每次循环,divider减一,result=result+100/divider * 如果:捕获异常,打印输出“异常抛出了”,返回-1 * 否则:返回result * @return */ public int test1(){ int divider=10; int result=100; try{ while(divider>-1){ divider--; result=result+100/divider; } return result; }catch(Exception e){ e.printStackTrace(); System.out.println("异常抛出了!!"); return -1; } } public static void main(String[] args) { // TODO Auto-generated method stub TryCatchTest t1=new TryCatchTest(); System.out.println("test1方法执行完毕!result的值为:"+t1.test1()); } }
執行結果:
結果分析:結果中的紅色字拋出的異常訊息是由e.printStackTrace()來輸出的,它說明了這裡我們拋出的異常類型是算數異常,後面還跟著原因:by zero (由0造成的算數異常),下面兩行at顯示了造成此異常的程式碼具體位置。
在上面範例中再加上一個test2()方法來測試finally語句的執行狀況:
/** * divider:除数 * result:结果 * try-catch捕获while循环 * 每次循环,divider减一,result=result+100/divider * 如果:捕获异常,打印输出“异常抛出了”,返回result=999 * 否则:返回result * finally:打印输出“这是finally,哈哈哈!!”同时打印输出result * @return */ public int test2(){ int divider=10; int result=100; try{ while(divider>-1){ divider--; result=result+100/divider; } return result; }catch(Exception e){ e.printStackTrace(); System.out.println("异常抛出了!!"); return result=999; }finally{ System.out.println("这是finally,哈哈哈!!"); System.out.println("result的值为:"+result); } } public static void main(String[] args) { // TODO Auto-generated method stub TryCatchTest t1=new TryCatchTest(); //System.out.println("test1方法执行完毕!result的值为:"+t1.test1()); t1.test2(); System.out.println("test2方法执行完毕!"); }
運行結果:
# #結果分析:我們可以從結果看出,finally語句區塊是在try區塊和catch區塊語句執行之後最後執行的。 finally是在return後面的表達式運算後執行的(此時並沒有回傳運算後的值,而是先把要回傳的值保存起來,管finally中的程式碼怎麼樣,傳回的值都不會改變,仍然是之前儲存的值),所以函數傳回值是在finally執行前確定的;这里有个有趣的问题,如果把上述中的test2方法中的finally语句块中加上return,编译器就会提示警告:finally block does not complete normally
public int test2(){ int divider=10; int result=100; try{ while(divider>-1){ divider--; result=result+100/divider; } return result; }catch(Exception e){ e.printStackTrace(); System.out.println("异常抛出了!!"); return result=999; }finally{ System.out.println("这是finally,哈哈哈!!"); System.out.println("result的值为:"+result); return result;//编译器警告 } }
分析问题: finally块中的return语句可能会覆盖try块、catch块中的return语句;如果finally块中包含了return语句,即使前面的catch块重新抛出了异常,则调用该方法的语句也不会获得catch块重新抛出的异常,而是会得到finally块的返回值,并且不会捕获异常。
解决问题:面对上述情况,其实更合理的做法是,既不在try block内部中使用return语句,也不在finally内部使用 return语句,而应该在 finally 语句之后使用return来表示函数的结束和返回。如:
总结:
1、不管有木有出现异常或者try和catch中有返回值return,finally块中代码都会执行;
2、finally中最好不要包含return,否则程序会提前退出,返回会覆盖try或catch中保存的返回值。
3、 e.printStackTrace()可以输出异常信息。
4、 return值为-1为抛出异常的习惯写法。
5、 如果方法中try,catch,finally中没有返回语句,则会调用这三个语句块之外的return结果。
6、 finally 在try中的return之后 在返回主调函数之前执行。
三、throw和throws关键字
java中的异常抛出通常使用throw和throws关键字来实现。
throw ----将产生的异常抛出,是抛出异常的一个动作。
一般会用于程序出现某种逻辑时程序员主动抛出某种特定类型的异常。如:
语法:throw (异常对象),如:
public static void main(String[] args) { String s = "abc"; if(s.equals("abc")) { throw new NumberFormatException(); } else { System.out.println(s); } //function(); }
运行结果:
Exception in thread "main" java.lang.NumberFormatException at test.ExceptionTest.main(ExceptionTest.java:67)
throws----声明将要抛出何种类型的异常(声明)。
语法格式:
public void 方法名(参数列表) throws 异常列表{ //调用会抛出异常的方法或者: throw new Exception(); }
当某个方法可能会抛出某种异常时用于throws 声明可能抛出的异常,然后交给上层调用它的方法程序处理。如:
public static void function() throws NumberFormatException{ String s = "abc"; System.out.println(Double.parseDouble(s)); } public static void main(String[] args) { try { function(); } catch (NumberFormatException e) { System.err.println("非数据类型不能转换。"); //e.printStackTrace(); } }
throw与throws的比较
1、throws出现在方法函数头;而throw出现在函数体。
2、throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。
3、两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
来看个例子:
throws e1,e2,e3只是告诉程序这个方法可能会抛出这些异常,方法的调用者可能要处理这些异常,而这些异常e1,e2,e3可能是该函数体产生的。
throw则是明确了这个地方要抛出这个异常。如:
void doA(int a) throws (Exception1,Exception2,Exception3){ try{ ...... }catch(Exception1 e){ throw e; }catch(Exception2 e){ System.out.println("出错了!"); } if(a!=b) throw new Exception3("自定义异常"); }
分析:
1、代码块中可能会产生3个异常,(Exception1,Exception2,Exception3)。
2、如果产生Exception1异常,则捕获之后再抛出,由该方法的调用者去处理。
3、如果产生Exception2异常,则该方法自己处理了(即System.out.println("出错了!");)。所以该方法就不会再向外抛出Exception2异常了,void doA() throws Exception1,Exception3 里面的Exception2也就不用写了。因为已经用try-catch语句捕获并处理了。
4、Exception3异常是该方法的某段逻辑出错,程序员自己做了处理,在该段逻辑错误的情况下抛出异常Exception3,则该方法的调用者也要处理此异常。这里用到了自定义异常,该异常下面会由解释。
使用throw和throws关键字需要注意以下几点:
1.throws的异常列表可以是抛出一条异常,也可以是抛出多条异常,每个类型的异常中间用逗号隔开
2.方法体中调用会抛出异常的方法或者是先抛出一个异常:用throw new Exception() throw写在方法体里,表示“抛出异常”这个动作。
3.如果某个方法调用了抛出异常的方法,那么必须添加try catch语句去尝试捕获这种异常, 或者添加声明,将异常抛出给更上一层的调用者进行处理
自定义异常
为什么要使用自定义异常,有什么好处?
1、我们在工作的时候,项目是分模块或者分功能开发的 ,基本不会你一个人开发一整个项目,使用自定义异常类就统一了对外异常展示的方式。
2、有时候我们遇到某些校验或者问题时,需要直接结束掉当前的请求,这时便可以通过抛出自定义异常来结束,如果你项目中使用了SpringMVC比较新的版本的话有控制器增强,可以通过@ControllerAdvice注解写一个控制器增强类来拦截自定义的异常并响应给前端相应的信息。
3、自定义异常可以在我们项目中某些特殊的业务逻辑时抛出异常,比如"中性".equals(sex),性别等于中性时我们要抛出异常,而Java是不会有这种异常的。系统中有些错误是符合Java语法的,但不符合我们项目的业务逻辑。
4、使用自定义异常继承相关的异常来抛出处理后的异常信息可以隐藏底层的异常,这样更安全,异常信息也更加的直观。自定义异常可以抛出我们自己想要抛出的信息,可以通过抛出的信息区分异常发生的位置,根据异常名我们就可以知道哪里有异常,根据异常提示信息进行程序修改。比如空指针异常NullPointException,我们可以抛出信息为“xxx为空”定位异常位置,而不用输出堆栈信息。
说完了为什么要使用自定义异常,有什么好处,我们再来看看自定义异常的毛病:
我们不可能期待JVM(Java虚拟机)自动抛出一个自定义异常,也不能够期待JVM会自动处理一个自定义异常。发现异常、抛出异常以及处理异常的工作必须靠编程人员在代码中利用异常处理机制自己完成。这样就相应的增加了一些开发成本和工作量,所以项目没必要的话,也不一定非得要用上自定义异常,要能够自己去权衡。
最后,我们来看看怎么使用自定义异常:
在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。
所有异常都必须是 Throwable 的子类。
如果希望写一个检查性异常类,则需要继承 Exception 类。
如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
可以像下面这样定义自己的异常类:
class MyException extends Exception{ }
我们来看一个实例:
package com.hysum.test; public class MyException extends Exception { /** * 错误编码 */ private String errorCode; public MyException(){} /** * 构造一个基本异常. * * @param message * 信息描述 */ public MyException(String message) { super(message); } public String getErrorCode() { return errorCode; } public void setErrorCode(String errorCode) { this.errorCode = errorCode; } }
使用自定义异常抛出异常信息:
package com.hysum.test; public class Main { public static void main(String[] args) { // TODO Auto-generated method stub String[] sexs = {"男性","女性","中性"}; for(int i = 0; i < sexs.length; i++){ if("中性".equals(sexs[i])){ try { throw new MyException("不存在中性的人!"); } catch (MyException e) { // TODO Auto-generated catch block e.printStackTrace(); } }else{ System.out.println(sexs[i]); } } } }
运行结果:
四、java中的异常链
异常需要封装,但是仅仅封装还是不够的,还需要传递异常。
异常链是一种面向对象编程技术,指将捕获的异常包装进一个新的异常中并重新抛出的异常处理方式。原异常被保存为新异常的一个属性(比如cause)。这样做的意义是一个方法应该抛出定义在相同的抽象层次上的异常,但不会丢弃更低层次的信息。
我可以这样理解异常链:
把捕获的异常包装成新的异常,在新异常里添加原始的异常,并将新异常抛出,它们就像是链式反应一样,一个导致(cause)另一个。这样在最后的顶层抛出的异常信息就包括了最底层的异常信息。
》场景
比如我们的JEE项目一般都又三层:持久层、逻辑层、展现层,持久层负责与数据库交互,逻辑层负责业务逻辑的实现,展现层负责UI数据的处理。
有这样一个模块:用户第一次访问的时候,需要持久层从user.xml中读取数据,如果该文件不存在则提示用户创建之。
那问题就来了:如果我们直接把持久层的异常FileNotFoundException抛弃掉,逻辑层根本无从得知发生任何事情,也就不能为展现层提供一个友好的处理结果,最终倒霉的就是展现层:没有办法提供异常信息,只能告诉用户“出错了,我也不知道出了什么错了”—毫无友好性而言。
正确的做法是先封装,然后传递,过程如下:
1、把FileNotFoundException封装为MyException。
2、抛出到逻辑层,逻辑层根据异常代码(或者自定义的异常类型)确定后续处理逻辑,然后抛出到展现层。
3、展现层自行确定展现什么,如果管理员则可以展现低层级的异常,如果是普通用户则展示封装后的异常。
示例
package com.hysum.test; public class Main { public void test1() throws RuntimeException{ String[] sexs = {"男性","女性","中性"}; for(int i = 0; i < sexs.length; i++){ if("中性".equals(sexs[i])){ try { throw new MyException("不存在中性的人!"); } catch (MyException e) { // TODO Auto-generated catch block e.printStackTrace(); RuntimeException rte=new RuntimeException(e);//包装成RuntimeException异常 //rte.initCause(e); throw rte;//抛出包装后的新的异常 } }else{ System.out.println(sexs[i]); } } } public static void main(String[] args) { // TODO Auto-generated method stub Main m =new Main(); try{ m.test1(); }catch (Exception e){ e.printStackTrace(); e.getCause();//获得原始异常 } } }
运行结果:
結果分析:我們可以看到控制台先是輸出了原始異常,這是由e.getCause()輸出的;然後輸出了e.printStackTrace(),在這裡可以看到Caused by:原始異常和e .getCause()輸出的一致。這樣就是形成一個異常鏈。
initCause()的作用是包裝原始的異常,當想要知道底層發生了什麼異常的時候呼叫getCause()就能得到原始異常。
建議
異常需要封裝和傳遞,我們在進行系統開發的時候,不要「吞噬」異常,也不要「赤裸裸」的拋出異常,封裝後在拋出,或者透過異常鏈傳遞,可以達到系統更健壯、友善的目的。
五、結束語
java的異常處理的知識點雜而且理解起來也有點困難,我在這裡給大家總結了以下幾點使用java異常處理的時候,良好的編碼習慣:
1、處理運行時異常時,採用邏輯去合理規避同時輔助try-catch處理
#2、在多重catch區塊後面,可以加上一個catch(Exception)來處理可能會被遺漏的例外
3、對於不確定的程式碼,也可以加上try-catch,處理潛在的例外
4.盡量去處理異常,切記只是簡單的呼叫printStackTrace()去列印
5、具體如何處理異常,要根據不同的業務需求和異常類型去決定
##6、盡量新增finally語句區塊去釋放佔用的資源更多java知識請追蹤java基礎教學欄位。
以上是JAVA異常與異常處理詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!