首頁  >  文章  >  Java  >  為什麼要進行異常處理?詳解java異常處理機制

為什麼要進行異常處理?詳解java異常處理機制

php是最好的语言
php是最好的语言原創
2018-08-10 11:34:267562瀏覽

1、背景介紹

為什麼要進行異常處理?

對於電腦程式而言,沒有人能保證它在運行時不會出錯,出錯來源主要有以下幾個:

  • 程式碼錯誤

  • 使用者非法輸入

  • 裝置錯誤及實體限制:磁碟滿了、記憶體溢位、硬體出現問題、網路中斷…

         程式出現錯誤,那麼該如何解決呢?在Java語言中,它提供了異常處理機制來幫助我們處理這個問題。

         例外機制可讓程式中的例外處理程式碼和正常業務程式碼分開離開,並確保程式碼更為優雅,並且可以提升程式的健全性。

         Java的例外機制主要依賴於try、catch、finally、throw和throws五個關鍵字。

         try關鍵字後緊跟著一個花括號括起來的程式碼區塊(花括號不可省略),簡稱try區塊,它裡面放置可能引發異常的程式碼;catch後對應異常類型和一個程式碼區塊,用於表示該catch區塊用於處理這種類型的程式碼區塊;多個catch區塊後還可以接一個finally區塊,finally區塊用於回收在try區塊中開啟的實體資源,異常機制會保證finally區塊總被執行;throws關鍵字主要在方法簽名中使用,用於聲明該方法可能拋出的異常;throw用於拋出一個實際的異常,throw可以單獨作為語句使用,拋出一個具體的異常對象。

          簡單範例如下 

try {
   // args表示传入的形参,args[0]表示传入的第一个形参,args[1]表示传入的第二个形参
   int a = Integer.parseInt(args[0]);
   int b = Integer.parseInt(args[1]);
   int c = a / b;
   System.out.println("您输入的两个数相除的结果是:"+c);
} catch (IndexOutOfBoundsException ie){
   System.out.println("数组越界:运行程序时输入的参数个数不够");
} catch (NumberFormatException ne){
   System.out.println("数字格式异常:程序只能接收整数参数");
} catch (ArithmeticException ae){
   // 除0异常
   System.out.println("算术异常");
} catch (Exception e){
   System.out.println("未知异常");
}

2、知識剖面

2.1 例外處理機制

2.1.1 使用try... catch擷取例外

try{

// 業務實作程式碼

} catch (Exception e){

// 例外處理程式碼

#}

執行流程(或邏輯):

         如果try區塊程式碼能夠順利運行,那就「一切正常」;如果執行try區塊裡的業務邏輯程式碼時出現異常,系統會自動產生一個異常對象,該異常對像被提交給Java運行時環境,這個過程稱為拋出(throw)異常。當Java運行時環境收到異常物件後,會尋找能處理該異常物件的catch區塊,如果找到合適的catch區塊,則把該異常物件交給該catch區塊處理,這個過程稱為捕獲(catch)異常;如果Java執行時間環境找不到捕獲異常的catch區塊,則執行環境終止,Java程式也會退出。

附註:不管程式碼區塊是否處於try區塊中,甚至包含catch區塊中的程式碼,只要執行程式碼區塊時出現了異常,系統總是會自動產生一個異常物件。如果程式沒有為這段程式碼定義任何的catch區塊,則Java執行時間環境無法找到處理該例外的catch區塊,程式就此退出。

2.1.2 異常類別的繼承系統

       當Java執行時間環境接收到例外物件後,會一次判斷該例外物件是否為catch區塊後異常類別或其子類別的實例,如果是,Java執行環境將呼叫該catch區塊來處理該異常,否則再次拿該異常物件和下一個catch區塊裡的異常類別進行比較。

Java異常捕獲流程

為什麼要進行異常處理?詳解java異常處理機制

       從圖中可以看出,try區塊後可以有多個catch區塊,這是為了針對不同的異常類提供不同的異常處理方式。當系統發生不同的意外情況時,系統會產生不同的異常對象,Java運行時就會根據該異常對象所屬的異常類別來決定使用哪個catch區塊來處理該異常。

         在通常情況下,如果try區塊執行一次,則try區塊後只有一個catch區塊會被執行,絕不可能有多個catch區塊被執行。除非在循環中使用了continue開始下一次循環,下一次循環又重新運行了try塊,這才可能導致多個catch塊被執行。

Java常見的例外類別之間的繼承關係

為什麼要進行異常處理?詳解java異常處理機制

Java把所有的非正常情况分为两种:异常和错误,它们都继承Throwable父类。

        Error错误,一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败、资源耗尽  等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象,也无须声明可能抛出Error及其任何子类对象。

常见异常:

IndexOutOfBoundsException:数组越界异常,原因在于运行程序时输入的参数个数不够

NumberFormatException:数字格式异常,原因在于运行程序时输入的参数不是数字,而是字母

ArithmeticException:除0异常

Exception:当发生未知异常时,该异常对象总是Exception类或其子类的实例,可用Exception对应的catch块处理该异常

NullPointerException:空指针异常,当试图调用一个null对象的实例或者实例变量时,会引发此异常

注:进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面,即先捕获小异常,再捕获大异常,否则将出现编译错误。(若父类在前,则排在它后面的子类的catch块将永远不会获得执行的机会,因为检索catch块是从上到下依次执行的)

2.1.3 Java7提供多异常捕获

在Java7之前,每个catch块只能捕获一种类型的异常,但从Java7开始,一个catch块可以捕获多种类型的异常。

使用一个catch块捕获多种类型的异常时需要注意如下两个地方:

  • 捕获多种类型的异常时,多种异常类型之间用 | 隔开。

  • 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。

try {
         int a = Integer.parseInt(args[0]);
         int b = Integer.parseInt(args[1]);
         int c = a / b;
         System.out.println("您输入的两个数相除的结果是:"+c);
      } catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException e){
         System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
         
         // 捕获多异常时,异常变量默认有final修饰
         // 所以下面代码编译报错
//        e = new ArithmeticException("text");
      } catch (Exception e){
         System.out.println("未知错误");
         
         // 捕获一种类型的异常时,异常变量没有final修饰
         // 所有下面代码完全正确
         e = new RuntimeException("test");
      }

2.1.4 访问异常信息

         如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch后面括号中的异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。

所有的异常对象都包含了如下几个常用方法:

  • getMessage():返回该异常的详细描述字符串。

  • printStackTrace():将该异常的跟踪栈信息输出到标准错误流。

  • printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。

  • getStackTrace():返回该异常的跟踪栈信息。

2.1.5 使用finally

 有时,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显示回收。

注:Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。

         为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块是否被执行,甚至在try块或者catch块中执行了return语句,finally块总会被执行。

完整的Java异常处理语法结构:

try{

// 业务实现代码

} catch (Exception1 e){

// 异常处理块1

} catch (Exception2 e){

// 异常处理块2

} finally {

// 资源回收块

}      

        異常處理語法結構中只有try區塊是必需的,如果沒有try區塊,則不能有後面的catch區塊和finally區塊;catch區塊和finally區塊都是可選的,但catch區塊和finally區塊至少出現其中之一,也可以同時出現;可以有多個catch塊,捕獲父類異常的catch塊必須位於捕獲子類異常的後面;但不能只有try塊,沒有catch塊,也沒有finally塊;多個catch區塊必須位於try區塊之後,finally區塊必須位於所有catch區塊之後。

註:除非在try區塊、catch區塊中呼叫了退出虛擬機器的方法,否則不管在try區塊、catch區塊中執行怎樣的程式碼,出現怎樣的情況,異常處理的finally區塊總是會被執行。

         在通常情況下,盡量避免在finally區塊中使用return或throw等導致方法終止的語句,否則可能會出現一些很奇怪的情況。

2.1.6 例外處理的巢狀

 在try區塊、catch區塊或finally區塊中包含完整的例外處理流程的情況稱為異常處理的巢狀。

         異常處理流程程式碼可以放在任何能放置可執行性程式碼的地方,因此完整的例外處理流程既可放在try區塊裡,也可放在catch區塊裡,還可放在finally塊裡。

         異常處理巢狀的深度並沒有明顯的限制,但通常沒有必要使用超過兩層的巢狀異常處理,且層次太深一是沒必要,而是導致程式可讀性降低。

2.1.7 Java7的自動關閉資源的try語句

         Java7增強了try語句的功能,它允許在try關鍵字後緊接一對圓括號,圓括號可以宣告、初始化一個或多個資源,此處的資源指的是那些必須在程式結束時顯示關閉的資源(例如資料庫連線、網路連線等),try語句在該語句結束時自動關閉這些資源。

註:為了確保try語句可以正常關閉資源,這些資源實作類別必須實作AutoCloseable或Closeable接口,實作這兩個介面就必須實作close()方法。 (Closeable是AutoCloseable的子介面;Closeable介面裡的close()方法宣告拋出了IOException,因此它的實作類別在實作close()方法時只能宣告拋出IOException或其子類別;AutoCloseable介面裡的close ()方法宣告拋出了Exception,因此它的實作類別在實作close()方法時可以宣告拋出任何例外)

         自動關閉資源的try語句相當於包含了隱含的finally區塊(這個finally區塊用來關閉資源),因此這個try語句可以既沒catch區塊,也沒有finally區塊。如果程式需要,自動關閉資源的try語句後也可以帶多個catch區塊和一個finally區塊。

         Java7幾乎將所有的「資源類別」(包括檔案的IO的各種類別、JDBC程式設計的Connection、Statement等介面)進行了改寫,改寫後的資源類別都實作了AutoCloseable或Closeable介面。

2.2 Checked異常和Runtime異常系統

        Java的異常值分為兩大類:Checked異常和Runtime異常(運行時異常,有的也稱為未檢查異常)。所有的RuntimeException類別及其子類別的實例稱為Runtime異常;不是RuntimeException類別及其子類別的異常實例則稱為Checked異常。 (使用時要分辨是什麼型別異常,只要看聲明的異常類別是啥就知道了)

         只有Java語言提供了Checked異常,而其他語言都沒有提供Checked異常。 Java認為Checked異常都是可以處理(修復)的異常,所以Java程式必須顯示處理Checked異常,如果程式沒有處理Checked異常,程式在編譯時會發生錯誤,無法通過編譯。

對於Checked異常的處理方式有以下兩種:

  • 目前方法明確知道如何處理該異常,程式應該使用try...catch區塊來捕獲該異常,然後再對應的catch區塊中修復該異常。

  • 目前方法不知道如何處理這種異常,並且應該在定義方法時聲明拋出該異常。

       Runtime例外則較為靈活,Runtime異常無須顯示宣告拋出,如果程式需要擷取Runtime異常,也可以使用try...catch區塊來實現。

附註:Java核心技術

        在Exception层次结构中,有两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。划分这两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。

          Java语言规范将派生于Error类或RuntimeException类的所有异常称为未(不受?)检查异常(unchecked),所有其他的异常称为已检查异常(checked)。(在编译期间进行检查,编译器将检查是否为所有的已检查异常提供了异常处理器)  ??????????? 有待讨论

2.3 使用throws抛出异常

       使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是平时我们的程序在遇到异常后自动结束的原因。

         throws声明抛出只能在方法签名中使用,throws可以声明多个异常类,多个异常类之间使用逗号隔开。

         一旦使用throws语句声明抛出异常,那么就无需使用try...catch块来捕获该异常了。

        使用throws声明抛出异常时有一个限制,就是方法重写时“两下”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。(即设置了上限)

使用Checked异常至少存在如下两大不便之处:

  • 对于程序中的Checked异常,Java要求必须显示捕获并处理该异常,或者显示声明抛出该异常,这增加了编程复杂度。

  • 如果在方法中显示声明抛出Checked异常,将会导致方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。

         在大部分时候推荐使用Runtime异常,而不使用Checked异常,尤其当程序需要自行抛出异常时,使用Runtime异常将更加简洁。

         当使用Runtime异常时,程序无需在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。对于Runtime异常,Java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。

         使用Runtime异常省事,但Checked异常也有其优势,Checked异常能在编译时提醒程序员代码可能存在的问题,提醒程序员必须注意处理该异常,或者声明该异常有方法的调用者来处理,从而避免程序员因为粗心而忘记处理该异常的错误。

2.4 使用throw抛出异常

       当程序出现错误时,系统会自动抛出异常,除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成。

2.5 自定义异常类

       用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(即异常对象的getMessage()方法的返回值)。

package AuctionException;

// 自定义异常都要继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。
// 自定义异常类需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串作为
// 该异常对象的描述信息(即异常对象的getMessage()方法的返回值)
public class AuctionException extends Exception{
   
   //1、无参数的构造器
   public AuctionException(){
   }
   //2、带一个字符串参数的构造器
   public AuctionException(String msg){
      // 通过super调用父类的构造器
      super(msg);
   }
}

2.6 catch和throw同时使用

       在实际应用中,当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可以完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。

         为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。

         这种catch和throw结合使用使用的情况在大型企业级应用中非常常用,企业级应用对异常的处理通常分成两个部分:一是后台需要通过日志来记录异常发生的详细情况;二是应用还需要根据异常向应用使用者传达某种提示。

package CatchAndThrow;

import AuctionException.AuctionException;

public class CatchAndThrow {
   
   private double initPrice = 30.0;
   
   // 因为该方法中显示抛出了AuctionException异常
   // 所以此处需要声明抛出AuctionException异常
   public void bid(String bidPrice) throws AuctionException{
      double d = 0.0;
      try {
         d = Double.parseDouble(bidPrice);
      } catch (Exception e){
         
         // 此处完成本方法中可以对异常执行的修复处理
         // 此处仅仅是控制台打印异常的跟踪栈信息
         e.printStackTrace();
         
         // 再次抛出自定义异常
         throw new AuctionException("竞拍价必须是数值,不能包含其他字符");
      }
      
      if (initPrice > d){
         throw new AuctionException("竞拍价比起拍价低,不允许竞拍");
      }
      
      initPrice = d;
   }
   
   public static void main(String[] args){
      
      CatchAndThrow catchAndThrow = new CatchAndThrow();
      try{
         catchAndThrow.bid("df");
      } catch (AuctionException ae){
         // 再次捕获到bid()方法中的异常,并对该异常进行处理,此处是将异常的详细描述信息输出到标准错误(err)输出
         System.err.println(ae.getMessage());
      }
   }
}

2.7 异常跟踪栈

       异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。

3、常见问题

3.1 为什么在finally块中不能访问try块中声明的变量?

答:try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块及finally块中不能访问该变量。

3.2 为什么最好不要使用catch all语句?

答:所谓catch all语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。例如使用Exception或者Throwable类捕获所有异常,虽然这种方式能够处理异常,但是它不能精确描述引发异常的原因,我们很难进行准确的排查。

3.3 有没有关于注解方式的异常处理?

答:有的,比如spring有基于注解的全局异常处理方式(使用@ExceptionHandler)

4、参考文献

摘自《疯狂Java讲义 》第三版,文中代码模仿书中例子所敲。

今天的分享就到这了,希望大家多多指正,互相学习~

相关推荐:

java异常(Exception)处理机制示例代码分享

简单介绍Java的异常和架构

以上是為什麼要進行異常處理?詳解java異常處理機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn