《java8學習筆記》讀書筆記(九)

摘要:花了差不多一個月的時間,寫完了第8章異常處理,這章講述了try-catch-finally的Java異常處理機制,講述了throw和throws的拋出異常機制。還介紹了assert語法,JDK 7開始出現的一些變化,比如try-with-resource語法蜜糖。對于Java異常的介紹還是比較全面的,希望大家喜歡。歡迎留言提問,歡迎分享,歡迎關注。

第8章 異常處理

學習目標

  • 使用try、catch處理異常
  • 認識異常繼承架構
  • 認識throw、throws的使用時機
  • 運用finally關閉資源
  • 使用自動關閉資源語法
  • 認識AutoCloseable接口

8.1 語法與繼承結構

程序正如生活,人間之事,不如意十之八九。總有意想不到的事情引發錯誤,有時即使你的代碼全對了,一點語法錯誤也沒有,也會在運行時出現錯誤。比如你寫了一個保存文件的程序,可是在保存的時候,磁盤滿了……程序也就崩潰了,然而你的程序真的沒有錯呀。Java中提供了很好錯誤處理機制,就叫異常處理,它能夠使你的程序即使遇上天災人禍(比如磁盤滿了,網絡斷了)都不會崩潰,而是給出正確的處理提示,讓用戶在使用時可以想辦法挽回損失,這也是我們常常所說的“健壯性”。
Java中的異常也以對象方式呈現,它們均有一個父接口java.lang.Throwable(默然說話:換個說法,如果你要自己寫個異常類——當然,剛開始,我們只用現成的異常類——你只要實現Throwable接口,Java就會正確的識別你寫的異常了)的各種子類實例。只要你能捕捉包裝異常的對象,就可以針對該異常做一些處理,比如嘗試恢復正常流程,進行日志記錄,以某種形式提醒用戶等。

8.1.1 使用try、catch

來看一個簡單的程序,用戶可以連續輸入整數,每個整數之間用空格分隔,輸入0表示結束,程序會計算平均分并顯示出來:

package cn.speakermore.ch08;

import java.util.Scanner;

/**
 * 對異常講解的引入,從輸入的錯誤說起
 * @author mouyong
 */
public class Average {
    public static void main(String[] args){
        Scanner input=new Scanner(System.in);
        double sum=0;
        int count=0;
        System.out.println("請輸入一組整數,每個數之間用空格分隔,0為結束:");
        while(true){
            //這個輸入存在不可控制的風險,因為用戶什么都會輸入
            int number=input.nextInt();
            if(number==0){
                break;
            }
            sum+=number;
            count++;
        }
        System.out.println("平均分:"+(sum/count));
    }
}

在乖乖妹看來,自然是要聽老師的話,只輸入整數,最后還要輸個0,程序當然也如預期的反應:



圖8.1 程序真好使
在調皮哥看來,規則就是拿來破壞的。你說用空格我就用空格,我不是很沒面子?而且,為何不能用逗號呢?逗號不是看上去更好些?



圖8.2 程序很無奈
嗯,出錯了不是?我們可以看到一段錯誤信息出現在了控制臺。

這段錯誤信息對我們發現錯誤,找到錯誤原因并改正是很有用的。我們來看下第一行。

Exception in thread "main" java.util.InputMismatchException

我們的代碼中,在接收輸入的時候使用了nextInt(),這表示我們只能接收整型數,如果出現了InputMismatchException就說明,用戶輸入的不是數字。當程序遇到了這樣的情況,它是不知道如何處理的,這時,它就掛了,停止運行,退出。我們稱之為:程序崩潰。
異常處理最重要的一個目的,就是希望在程序遇到了不期望的情況時,不會崩潰,而是以一種恰當的方式進行處理,并能繼續正常運行。Java中已經做好了一步,就是它會將所有的錯誤都打包為異常對象(默然說話:如上面的java.util.InputMismatchException就是一個異常對象的類型),你需要做的,就是去捕獲這些錯誤的對象,以便提供后繼的異常處理。如何做?我們看示例:

package cn.speakermore.ch08;

import java.util.InputMismatchException;
import java.util.Scanner;

/**
 * 帶了try-catch的平均計算
 * @author mouyong
 */
public class AverageWithTryCatch {
    public static void main(String[] args){
        Scanner input=new Scanner(System.in);
        double sum=0;
        int count=0;
        System.out.println("請輸入一組整數,每個數之間用空格分隔,0為結束:");
        //try塊:放置有運行時錯誤風險的代碼
        try{
            while(true){
                //有了try-catch,即使出現輸入風險,我們也能捕獲它,并進行適當的處理
                int number=input.nextInt();
                if(number==0){
                    break;
                }
                sum+=number;
                count++;
            }
            System.out.println("平均分:"+(sum/count));
        }catch(InputMismatchException e){
            //catch塊:捕獲指定的異常對象并進行錯誤處理
            //進行錯誤處理:再次錯誤消息輸出
            System.out.println("必須輸入正確的格式,親。以空格分隔的整數,0為結束");
        }
    }
}

這里使用了try-catch的語法,大家要注意的是,try部分是必須要用大括號括起來的部分,我們稱為“try塊”,里面的代碼就是嘗試(try翻譯過來就是嘗試)運行的部分。如果沒有錯誤發生,代碼就繼續try-catch后面的代碼(默然說話:額,我們這里try-catch后面沒有代碼了,所以程序就會結束運行。)。如果try塊內的代碼發生了錯誤,JVM就會打包這個錯誤為一個異常對象(默然說話:對的,異常的類型是很多的,我們甚至可以自行聲明我們自己的異常類型),離開錯誤發生點(默然說話:這又是一個細節。當try塊發生了錯誤時,從錯誤點開始之后的代碼就不會被運行了)并拋出這個異常對象,程序執行將轉到catch塊。JVM會核對catch后的小括號中寫的異常對象類型,找到對應異常類型的catch塊,并執行這個catch塊的代碼(默然說話:也就是對應此異常對象的錯誤處理代碼)。執行完畢之后,繼續執行try-catch后面的代碼。
所以,如果nextInt()發生了InputMismatchException錯誤,流程就會離開try塊,跳到捕獲InputMismatchException對象的catch塊中,輸出錯誤消息提示,然后正常結束,而不是程序崩潰。


圖8.3 即使程序輸入不正確,也能正常結束
這時會存在一個小問題。如果拋出的異常并沒有對應的catch塊呢?在實際代碼中這樣的情況是會存在的。這時JVM會直接把出這種情況的程序給斃了(默然說話:也就是我們的程序崩潰了,JVM把它清除出了內存),然后自己來處理這個異常,處理的辦法大部分情況就和圖8.1一樣,把異常對象包裝的錯誤信息打印出來。

8.1.2 異常繼承架構

關于前面的平均分例子,在學了異常之后幾年,突然有一天我發現了一個問題。nextInt()是會拋出一個異常的。可是,編譯器居然不提示你需要使用try-catch。而很多的其他異常,編譯都會給出報錯的提示,如果你不寫try-catch,就不給你運行的。當時的我對這個問題還是困擾了一段時間的,直到學習了異常的繼承架構,才明白了是怎么回事情。


圖8.4 Throwable繼承架構
從上面的繼承架構圖可以看出,頂層是一個可拋出類Throwable(默然說話:Throwable的意思就是“可拋出”。Java規定了所有的錯誤類都要繼承自Throwable,否則try-catch將不理會。),它的下面擴展了兩個類,一個是錯誤類Error,另一個是異常類Exception。
Error及其子類代表嚴重的系統錯誤,如硬件層面的錯誤、JVM錯誤等。這類錯誤發生時,我們的程序通常是做不了什么事情來挽回的,所以我們都不會對這些問題進行處理,而是交給JVM自裁。
Exception及其子類則代表我們寫的代碼本身包含的錯誤。這也就是為何我們把這章叫異常(Exception)處理的原因。也是我們主要研究的部分。
如果某個方法拋出了RuntimeException或者其子類對象(默然說話:如前面平均分那個例子中的InputMismatchException,它就是RuntimeException的子類),編譯器認為這類異常應該是由我們自己事前預防并處理好,所以,編譯器并不會對可能拋出此類異常的代碼進行編譯時報錯處理。也就是,你寫不寫try-catch由你自己決定,你可以使用try-catch來處理此類異常,也可以使用別的方式來處理此類異常。所以,我們又把RuntimeException及其子類稱為非受檢異常(unchecked exception)。
在前面平均分的例子中,由于用戶的輸入是不可控的,所以我們可以在從控制臺取用戶輸入的數據時,并不用nextInt(),而是使用next(),以字符串的方式獲得,然后在代碼中對數據進行判斷之后,再進行相應的處理。

package cn.speakermore.ch08;

import java.util.Scanner;

/**
 * 不使用try-catch的平均分計算
 * @author mouyong
 */
public class AverageWithoutTryCatch {
    
     public static void main(String[] args){
        
        double sum=0;
        int count=0;
        System.out.println("請輸入一組整數,每個數之間用空格分隔,0為結束:");
        while(true){
            //myNextInt()對用戶的輸入進行了處理,媽媽再也不擔心淘氣哥的破壞了
            int number=myNextInt();
            if(number==0){
                break;
            }
            sum+=number;
            count++;
        }
        System.out.println("平均分:"+(sum/count));
    }
     private static int myNextInt(){
         Scanner input=new Scanner(System.in);
         String number=input.next();
         //matches()方法使用正則表達式比較字符串,\d*的意思表達純整數
         //使用循環的目的是為了讓用戶在輸入錯誤的情況下,可以重復輸入,直到輸入正確
         while(!number.matches("\\d*")){
             System.out.println("請輸入數字,親,并以空隔分隔每個數字,0為結束");
             number=input.next();
         }
         //將字符串轉化為整數
         return Integer.parseInt(number);
     }
}

一個可能的執行結果如下圖:



圖8.5 淘氣哥再也無孔可入了
我們可以看到,上例中我們寫了一個叫myNextInt()的方法,以next()方法,以String類型來接收用戶的輸入,之后調用了String的matches()方法來比較輸入是否為純數字。這里的“\d*”是一個正則表達式,關于正則表達式我們會在第15章說明。
如果某個方法拋出的是除RuntimeException及其子類的異常對象,這樣的異常對象被稱為受檢對象,如果你沒有使用try-catch,編譯器就會提醒你必須使用。
除了了解Error與Exception的區別,以及Exception與RuntimeException的區別之外,我們還要知道,如果父類異常對象在子類異常對象前,由于父類會捕獲子類異常,導致catch子類異常將永遠不會被執行。編譯器會報錯。如下:



圖8.6 父類會捕獲子類的對象
所以,在進行catch時,要注意子類在前,父類在后。
        try{
            int result=input.nextInt();
        }catch(InputMismatchException e){
            e.printStackTrace();
        }catch(RuntimeException e){
            e.printStackTrace();
        }catch(Exception e){
            e.printStackTrace();
        }

如果多個catch塊對異常的處理都是相同的,那么上面的寫法其實是很苦逼的,而且造成代碼的嚴重重復(默然說話:當然,你可以寫個方法在各catch中調用,以此解決代碼重復的問題。)。JDK7針對這個問題推出了多重捕獲(Multi-catch)語法:

try{
    //可能出錯的代碼
} (InputMismatchException |IndexOutOfBoundsException|ClassCastException e){
    e.printStackTrace();
}

這個寫法看上去要簡潔很多。不過這個寫法要注意一個問題,就是在同一個catch塊中同時捕獲的異常類不能有直接的繼承關系,否則會報錯。(默然說話:個人很不喜歡這樣的寫法,覺得這種寫法在實際開發中并沒有什么卵用。當然,也可能是因為自己十多年都是寫的多個catch吧,你們自己可以選擇。

8.1.3 要抓還是要拋

假設你今天受命開發一個中間庫,其中有個功能是讀取txt文本文件,并以字符串返回文檔中的所有字符,你也許會這樣:
雖然還沒有正式講到Java如何存取文本文件,不過前面也說過,Scanner在創建的時候,可以給構造函數傳入InputStream的對象,而FileInputStream就是用來讀取指定名稱的文件的。它是InputStream的子類,所以也可以把它傳遞給Scanner。而在創建FileInputStream對象時會拋出FileNotFoundException,根據目前學過的異常處理語法,于是你catch了FileNotFoundException,并在控制臺中顯示錯誤信息。

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * try-catch的誤用
 * @author mouyong
 */
public class MiddleLib {
    public String readFile(String filename){
        StringBuffer sb=new StringBuffer();
        try {
            Scanner input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine())
                        .append('\n');
                
            }
        } catch (FileNotFoundException ex) {
            //在控制臺輸出錯誤真的合適嗎?
            ex.printStackTrace();
        }
        return sb.toString();
    }
}

不過,你要考慮到一個問題,你做的中間庫。中間庫的意思就是,它可能用于任何可以用它的地方。也許會在一個網站中用來讀取用戶上傳的配置信息,可是控制臺卻在服務器端,你在控制臺輸出,用戶不是就會無法知道他上傳的配置有沒有讀取成功了么?說到這兒,你會發現,在這個假設的場景中,你的catch做任何事情,都是不符需求的,換個說法,如果你的方法出了拋出了異常,你根本不知道應該如何處理這個異常。
當我們上學的時候,如果做作業遇到了困難,我們首先會嘗試自己解決,如果我們自己解決不了,我們會怎么做呢?對,我們會把問題拋出,去讓能解決這個問題的對象去解決。異常處理也做了同樣的語法結構。try就是給我們嘗試執行代碼的地方,catch就是讓我們捕獲異常作對應處理的地方。如果異常我們無法處理時,Java設計了throws來聲明這個方法有異常,讓調用這個方法的程序去處理,象這樣。

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;


/**
 * 不使用try-catch,而使用throws
 * @author mouyong
 */
public class MiddleLibWithThrows {
    public String readFile(String filename) throws FileNotFoundException{
        StringBuffer sb=new StringBuffer();
        
        Scanner input=new Scanner(new FileInputStream(filename));
        while(input.hasNext()){
            sb.append(input.nextLine())
                    .append('\n');  
        }
        return sb.toString();
    }
}

默然說話:throws是一個聲明,它在說“這里可能會有異常發生,請注意!”。就好象我們路過豪宅,看到門口掛著的“小心!內有惡狗”一樣,throws就是那塊掛在那里的牌子,“惡狗”并不一定會讓你遇上,但是你必須準備好進行異常處理。例如:換上一雙可以讓你比同伴跑得更快一些的跑鞋。
當我們聲明拋出的是一個受檢異常時(例如前面所說的FileNotFoundException),調用我們方法的程序就會得到提示:如果不把我們的方法放到try-catch中,就會得到一個編譯錯誤信息,提示他們使用try-catch,或者象我們一樣繼續使用throws聲明拋出這個受檢異常,讓別的程序調用去處理這個異常。
聲明拋出受檢異常,表示你認為調用方法的程序有能力且應該處理此異常。因為throws關鍵字使用于方法聲明的頭部,所以在用戶查看API文檔時可以直接就看到它,而無需去查看你的源代碼。


圖8.7 受檢異常不捕獲,會出現編譯錯誤

如果你認為程序在調用你的方法應該先準備好前置條件,而不應該是蠻撞的調用,你可以聲明拋出一個非受檢異常(RuntimeException及其子類),這樣編譯器將不會強制調用程序必須使用try-catch來處理你的方法拋出的異常,如果一旦拋出,將會向上傳導,正確的處理方法是使用正確的代碼來完成邏輯,避免這樣的異常出現,而不是用try-catch把它消滅。

默然說話:對的,異常處理機制是一個很好的工具,但好工具也不能濫用。有的同學會在什么地方都加try-catch,這其實是一件很危險的行為。這會導致在最后項目上線后出現錯誤卻看不到任何錯誤消息的輸出,讓人無從排除錯誤的原因是什么。所以,大家在學習的時候,還是要注意慎用try-catch。不是只要有異常拋出的地方,就來個try-catch把錯誤處理掉,而是要做全局的考慮,內部的,簡單直接的,影響范圍小的異常,直接try-catch處理,而復雜的,影響范圍大的,無法處理的異常,最好不要處理,而是使用throws去聲明拋出,讓別的能處理的程序去try-catch。

其實我們有些時候還可能是這樣的,出現的異常我們可以處理一部分,但是并不能處理全部。比如,日常生活中我們也會遇到這樣的問題,交通事故如果是小事呢,車主相互商量一下就解決了(try-catch),更多的情況是事故本身商量的差不多,但對于責任認定雙方起了爭執,這時還是要讓交警出面。但這時的情況就是當事雙方處理了一部分,但不能處理全部的情況。這時就得打電話來讓交警處理剩下的問題。這里的“打電話”,就是把沒法處理的部分“拋(throw)”給交警。
程序里也會存在這樣的情況,這時我們會在異常處理之后,繼續將異常對象拋出,關鍵字throw。

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;


/**
 * 使用try-catch,同時繼續拋出異常
 * @author mouyong
 */
public class MiddleLibWithThrow {
    public String readFile(String filename) throws FileNotFoundException{
        StringBuffer sb=new StringBuffer();
        try {
            Scanner input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine())
                        .append('\n');
                
            }
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            //繼續拋出異常
            throw ex;
        }
        return sb.toString();
    }
}

在catch處理完異常之后,可以使用throw關鍵字把異常對象繼續拋出。當然,因為有這個異常對象的拋出,所以你的方法聲明處也要加上throws關鍵字來說明可能拋出的異常。

默然說話:要注意throws和throw的區別。throws是聲明拋出,是一個名詞,寫在方法聲明的最后,大括號前面。throws關鍵字后面跟著的是異常類的名稱,可以跟多個異常類,每個之間用逗號(,)分隔。而throw是拋出,是一個動詞,理論上可以寫在方法體的任何位置。throw關鍵字后面跟著的是異常的對象名,一次也只能拋一個異常對象。當代碼執行到throw時,將會強制停止執行throw之后的所有代碼,從方法中跳出,回到方法的調用位置,而調用位置將得到跟在throw后面的那個異常對象。從這一點來說,它很象方法的return關鍵字。

如果說,throws是“小心!內有惡狗!”那塊牌子的話,那throw就是把“惡狗”對象拋在你的面前了。這也解釋了throws后跟著的是異常類的名稱(僅只告訴你有某種類型的異常會拋出),而throw后要跟異常對象(已實例化的異常類)的原因。

8.1.4 貼心還是造成麻煩

異常處理的本意是,在程序錯誤發生時,能夠有明確的方式通知API客戶端,讓客戶端采取進一步的動作修正錯誤。目前Java是唯一采用受檢異常(Checked Exception)的語言.這有兩個目的:一個是受檢異常通常會在方法聲明部分使用throws聲明可能會拋出的異常,這樣在用戶讀API文檔的時候就可以明確了解這個方法的調用需要處理哪些異常,二是受檢異常有助于編譯器在編譯階段就提前發現未被處理的異常,這樣可以大大提高效率。
但是有些異常其實你是無法處理的,比如使用JDBC的數據庫連接代碼,經常要處理java.sql.SQLException,它是一個受檢異常,可是如果數據庫一旦因為物理聯接連不上而造成的異常,我們的程序真的無能為力處理。這時,除了通知用戶網絡連接有問題,別無辦法。
這時也許有的同學會說,前面不是說過了么?無法處理就拋嘛。的確如此,可是在大規模應用程序中,數據庫處理的代碼通常位于最底層(默然說話:嗯,我見過的最復雜的程序分層,從最低一直到界面有18層之多),你一層層往上拋不是一件很麻煩的事情么?特別是如果一開始其實沒拋,后來才發現要拋,你寫底層的拋一個,好嘛,因為SQLException是一個受檢異常,只要調了的方法都必須處理,如果都采用拋,后面有18個人都要改自己的代碼,一直改到界面層。這實在是一個很大的麻煩呀。
受檢異常的本意是很好的,有助于程序設計人員注意到異常的可能性并加以處理,但在應用程序規模增大時,會逐漸對維護造成困難。因為可能底層的維護引入一個受檢異常就會擴散到整個架構的相關方面都要來捕獲并拋出這個異常,直到可以處理的地方進行處理。

8.1.5 認識堆棧追蹤

在程序中,如果想看到異常發生的根源,以及方法反復調用過程中的棧傳播,可以利用異常對象自動收集的棧軌跡(Stack Trace)來取得相關信息。
查看棧軌跡的最簡單方法,就是調用每個異常對象都有的printStackTrace()。例如:

package cn.speakermore.ch08;

/**
 * 異常棧軌跡跟蹤測試
 * @author mouyong
 */
public class ExceptionStackTraceTest {
    /**
     * 異常的根源,methodA
     * @return 字符串,其實這里會拋異常NullPointerException,所以它并沒有返回
     */
    static String methodA(){
        String text=null;
        return text.toLowerCase();
    }
    /**
     * 調用methodA,以制造調用棧
     */
    static void methodB(){
        methodA();
    }
    /**
     * 調用methodB,以制造二重調用棧
     */
    static void methodC(){
        methodB();
    }
    /**
     * 主方法,制造三重調用棧
     * @param args 字符串數組,在這里沒有什么用
     */
    public static void main(String[] args) {
        try {
            methodC();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }
}

在methodA()中故意制造了一個NullPointerException,用methodB()來調用了methodA(),又用methodC()來調用methodB(),最后在main()中調用了methodC()。運行之后的結果如下:



圖8.8 異常棧軌跡打印
上面的第一行紅色字體是一個異常類的類型名稱:NullPointerException。從第二行開始的部分為棧的軌跡,第二行就是異常拋出來的位置,如果是在IDE中,則用鼠標單擊藍色的鏈接就會跳轉到該行代碼,以便讓程序員快速定位錯誤位置并進行修改。這是程序員快速定位并調整程序,修補bug的工具,所以一定一定一定不要不輸出這樣的信息,象下面這樣。

try{
//做一些事
}catch(Exception e){
//什么都不做,絕對絕對絕對不要這樣做。
}

如果你這樣做,很可能不僅僅是影響到你的代碼,甚至會影響到整個項目因為這里的無任何異常的輸出而造成大家都不知道異常去哪里了,從而無法定位錯誤位置,找不到錯誤原因,浪費相當多的時間和精力。
我建議所有的初學者都養成使用e.printStackTrace()這樣的輸出異常棧的形式,而不是自己輸出一條連自己都弄不明白的錯誤信息,因為這樣會誤導自己及別人,造成找不到真正的錯誤原因。比如象這樣:

try{
}catch(Exception e){
    System.out.println("找不到文件");
}

這樣沒有營養的錯誤信息太過模糊,給使用軟件的人看影響不大,但是如果是程序員,提供的信息就太少太少,無法去定位錯誤的位置,甚至不知道錯誤的真正原因(默然說話:異常的類型就在代表著異常的原因,初學者看不懂,高手可是都懂的!

8.1.6 關于assert

很多時候,我們可以在需求或設計時就能確認,程序執行的某個時點或情況下,一定處于或不處于某種狀態,如果它不是這樣的,那就是嚴重的錯誤,開發過程中如果發現在這種情況,必須重新確認需求與設計。
程序在某個時段一定處于或不處于某個狀態,我們稱為“斷言”(assert)。比如:某個變量的值在這里一定是幾。斷言的特點就是成立或不成立,預期結果與實際結果相同時,斷言成立,否則斷言就不成立。
Java在JDK1.4之后就加入了斷言,它的語法如下:

assert 布爾表達式;
assert 布爾表達式 : 不成立時的文字提示;

其中,布爾表達式如果為true,則什么都不會發生。如果為false,則在第一個式子中會拋出java.lang.AssertionError,在第二個式子中則會將不成立時的文字提示顯示出來。這個提示如果是個對象,則會自動調用其toString()方法。
assert作為關鍵字的使用并不是默認的,你需要啟動斷言檢查。要在執行時啟動斷言檢查,可以在使用java指令時,使用-ea參數。
那么何時使用斷言呢?

  • 斷言客戶端調用方法前,已經準備好某些前置條件。
  • 斷言客商調用方法后,具有方法承諾的結果。
  • 斷言對象某個時間點下的狀態
  • 使用斷言取代批注
  • 斷言程序中絕對不會執行到的程序代碼

默然說話:這小節內容其實挺尷尬,因為目前公認的理論認為assert是一個很重要的單元測試概念,但是在實際項目代碼中卻不應該讓它成為信息處理、判斷和輸出的一部分,assert應該使用在單獨的測試環境中。所以思來想去,最后我決定只是簡單介紹一下概念,而不再對其進行代碼示例了。如果想要做更多的了解,可以關注單元測試的相關文章。

8.2 異常與資源管理

程序因為拋出異常會導致原來的程序執行流程被中斷,拋出點之后的代碼都會不被執行,如果程序開啟了相關資源(例如:打開了一個文件,或者連接上了數據庫等等),很可能會導致后繼工作無法進行,甚至會導致別的程序都出現無法使用相關資源的情況(默然說話:我就遇到過,在Java代碼里打開一個文件之后忘記關閉,導致這個文件再用word或記事本程序打開時報錯的情況。這一錯誤直到我重啟電腦之后才解決)。所以,在拋出異常之后,你的設計是否還能正確地關閉資源呢?

8.2.1 使用finally

前面8.1.3的寫的代碼其實并不正確,因為我們在打開一個文件之后,并沒有關閉它。
那要何時關閉資源呢?我們可以使用下面的代碼來完成:

…
public String readFile(String filename){
        StringBuffer sb=new StringBuffer();
        try {
            Scanner input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine()).append('\n');
            }
            input.close();
        } catch (FileNotFoundException ex) {
            //在控制臺輸出錯誤真的合適嗎?
            ex.printStackTrace();
        }
        
        return sb.toString();
    }
…

可是通過前面的學習,我們知道上面的代碼存在隱患,因為一旦input.close()之前的代碼出現了異常,那input.close()這句代碼就會被跳過,文件就會不被關閉。
也許有同學會想到,在catch塊再寫一次。可是這卻違反了我們的“代碼不重復”的原則。倒底應該怎么辦呢?我們需要新的幫助。
其實如果你想要的是無論如何,最后一定要執行到關閉資源的代碼,try/catch還有一個關鍵字可以用,就是finally。寫在finally里的代碼,無論任何情況(try正常執行完,還是try里拋出了異常),都會被執行到,它的優先級非常高。(默然說話:我曾經嘗試過在try里寫了return語句。return就是讓方法調用結束,可是當程序執行到return時并不會馬上停止方法代碼的執行,而是先去執行完finally里的代碼,然后才停止方法代碼的執行,回到調用位置。
經過改造后的代碼如下:

package cn.speakermore.ch08;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;


/**
 * try-catch-finally形式
 * @author mouyong
 */
public class MiddleLib {
    public String readFile(String filename){
        StringBuffer sb=new StringBuffer();
        Scanner input=null;
        try {
            input=new Scanner(new FileInputStream(filename));
            while(input.hasNext()){
                sb.append(input.nextLine()).append('\n');
            }
        } catch (FileNotFoundException ex) {
           
            ex.printStackTrace();
        }finally{//無論如何都必須執行的代碼放這兒
            if(input!=null){
                input.close();//在不為null的情況下關閉文件
            }
        }
        return sb.toString();
    }
}

細心的同學可能注意到了finally語句中,除了寫input.close(),還給這條語句加了一個非空判斷,這是由于Java變量作用域的限制造成的。在Java中,聲明在try塊中的變量只能在try的大括號范圍有效,在finally塊里是引用不到try里的變量的,所以,我們其實首先改變了input這個變量的聲明,將它聲明到了方法的大括號中。這樣一來,必須在聲明時設置input變量為空(默然說話:方法中聲明的變量必須手動初始化,對象型通常我們都設置為null)。這會帶來一個隱患,如果input在new的代碼上出了錯,那進入到finally時input就會為null,而一個null對象調用方法會拋出NullPointerException異常。所以,為了避免NullPointerException的拋出,我們加上了判斷語句,保證只有在input不為null的時候才調用close()方法來關閉文件。(默然說話:這也是說得過去的,因為如果input為null,它的含義就是文件并沒有打開,文件沒打開,也是不需要關閉的。

8.2.2 Try With Resources

在使用try-catch-finally塊或try-finally塊來關閉資源時,我們都會注意到,其實又是重復代碼,先檢查是否為空,然后調用close()方法。在JDK7后,新增了一個方便的語法蜜糖:Try-With-Resources。(***默然說話:實在不知道如何翻譯成中文呀,嘗試資源?!好奇怪的英語)看關鍵代碼:

…
try(Scanner input=new Scanner(new FileInputStream(filename))) {
            
            while(input.hasNext()){
                sb.append(input.nextLine()).append('\n');
            }
        } catch (FileNotFoundException ex) {
           
            ex.printStackTrace();
        }
…

在try的后面加上小括號,把無論有沒有拋異常都需要關閉的資源聲明放進小括號就可以。你不需要自己再寫finally塊,編譯器會自動的幫你加上合適的finally。
需要注意的一點是,try with resources僅嘗試幫你關閉資源對象,并不會幫你catch異常,所以該寫的catch塊還是得自己寫的。

8.2.3 java.lang.AutoCloseable

如果你想自已實現一個能使用try with resources機制的類,只要繼承JDK7新增的接口java.lang.AutoCloseable就可以了。如下:

package cn.speakermore.ch08;

/**
 * 嘗試關閉資源的類示例
 * 繼承AutoCloseable,實現了close方法
 * @author mouyong
 */
public class ResourceDemo implements AutoCloseable {
     private final String resName;
     /**
      * 為了便于識別,加入了一個資源名稱
      * @param resName 字符串,資源的標識名
      */
     public ResourceDemo(String resName){
         this.resName=resName;
     }
     /**
      * 重寫父接口的方法,模擬關閉的過程
      * Thread.sleep(800)的意思,就是讓程序暫停800毫秒
      * @throws Exception 
      */
    @Override
    public void close() throws Exception {
        System.out.println(resName+":現在模擬關閉資源!");
        Thread.sleep(800);//讓程序暫停800毫秒
        System.out.println(resName+":3....");
        Thread.sleep(800);//讓程序暫停800毫秒
        System.out.println(resName+":2....");
        Thread.sleep(800);//讓程序暫停800毫秒
        System.out.println(resName+":1....");
        Thread.sleep(200);//讓程序暫停200毫秒
        System.out.println(resName+":關閉資源成功!");
    }    
}

這里寫了一個叫ResourceDemo的資源類,它繼承AutoCloseable接口,重寫了close()方法。由它其實并沒有要關閉的內容,所以在close()方法我模擬了一段動畫,就是在每一行文字輸出之后讓程序暫停0.8秒(800毫秒),再輸出下一句話。
完成這個程序之后,我們再寫一個類,完成測試:

package cn.speakermore.ch08;

/**
 * 對try with resources機制的測試,單個資源資源關閉
 * @author mouyong
 */
public class CloseResourceTest {
    public static void main(String[] args){
        try(ResourceDemo rd1=new ResourceDemo("demo1");){
            //因為只是測試,所以try塊內什么都沒寫
        }catch(Exception e){
            //catch是必須要寫的,因為close()方法會拋出Exception對象
            e.printStackTrace();
        }
    }
}

執行結果如下:



圖8.9 單個資源try with resources的測試結果
很可惜書不能貼動畫,大家只能看到最后的結果。
Try with resources機制也可以關閉多個資源,只要在try的小括號中寫成多條語句,每條語句用分號分隔就可以了。關鍵代碼如下:

…
        try(ResourceDemo rd1=new ResourceDemo("demo1");
                ResourceDemo rd2=new ResourceDemo("demo2");){
            //因為只是測試,所以try塊內什么都沒寫
        }catch(Exception e){
            //catch是必須要寫的,因為close()方法會拋出Exception對象
            e.printStackTrace();
        }
…

如果這多個資源的關閉有順序要求,那么要注意了,寫在前面的資源總是靠后關閉的,寫在后面的資源總是靠前關閉的,比如前面的例子的執行結果就能明顯看出這一點。



圖8.10 先寫的資源后關閉,后寫的資源先關閉

8.3 重點復習

Java中所有的錯誤都被封裝為對象,我們可以try執行程序并catch那些代表錯誤的對象后做一起處理。try-catch機制中,JVM會首先嘗試執行try中的代碼,如果有錯誤對象拋出,就立即離開try轉到catch去匹配拋出的錯誤對象類型,找到對應的類型之后,執行對應的catch塊。需要注意的是,父類可以匹配所有的子類,所以如果要分開catch,子類一定要寫在父類的前面,否則報錯。
所有錯誤都要是可拋出的,所以Java設計了Throwable這個根錯誤類。Throwable定義了我們常用的printStackTrace()方法,以幫助程序員快速了解異常的原因及錯誤的發生位置。它下面有兩個子類:Error和Exception。
Error類代表所有嚴重的系統性錯誤,并不建議進行catch。
Exception類代表所有我們程序本身可處理的異常,這也是Java把這個機制稱為異常處理機制的原因。Java也推薦我們自定義的異常類繼承自Exception或它的其他子類。
如果我們自己寫的catch沒有與拋出的異常相匹配的部分,JVM就會捕獲這個異常對象,并讓我們的程序立即停止執行,并將捕獲到的異常棧軌跡輸出給我們。
在語法上,如果聲明拋出(throws)異常類是繼承自除RuntimeException以外的異常父類,則編譯器會強制我們使用try-catch對這個異常進行處理或使用throws進行聲明拋出,以便調用該方法的程序知道有異常可能會拋出,以便進行相應的處理,我們稱之為“受檢異常(Checked Exception)”。如果是繼承自RutimeException,編譯器則不會強制我們使用try-catch,同樣也不會強制進行聲明拋出。我們稱之為“非受檢異常(Unchecked Exception)”。
在catch塊中處理部分錯誤之后,如果需要,還可以使用throw將異常對象繼續拋出。
要善用棧追蹤,要把自己處理不了的異常拋出來,不要catch之后什么都不做(默然說話:如果你的確不知道應該做什么,那就在catch中throw并在方法上throws,讓知道應該如何處理的人去處理),這樣會給程序開發帶來災難性的后果,嚴重影響開發進度。
無論try塊有沒有發生異常,只要有finally塊,就一定會執行finally塊。
JDK 7之后引入了一個很方便的語法蜜糖----try with resource,鼓勵大家使用。JDK 7引用的這個語法中,可以使用自動關閉的資源必須繼承java.lang.AutoCloseable接口。
try with resource可以同時關閉多個資源,只要用分號分隔它們就可以了,關閉的順序是按棧的方式來關閉,先new的資源后關閉,后new的資源先關閉。

8.4 課后練習

8.4.1 選擇題

1.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(NumberFormatException ex){
            System.out.println(“必須輸入數字”);
        }
    }
}

執行時若沒有指定命令行參數,以下描述正確的是()

A. 編譯錯誤
B.顯示“必須輸入數字”
C.顯示 ArrayIndexOutOfBoundException棧追蹤
D.不顯示任何信息

2.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        Object[] objs={“java”,”7”};
        Integer number=(Integer) objs[1];
        System.out.println(number);
    }
}

以下描述正確的是()

A. 編譯錯誤
B.顯示7
C.顯示 ClassCastException棧追蹤
D.不顯示任何信息

3.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(NumberFormatException ex){
            System.out.println(“必須輸入數字”);
        }
    }
}

執行時若指定命令行參數one,以下描述正確的是()

A. 編譯錯誤
B.顯示“必須輸入數字”
C.顯示 ArrayIndexOutOfBoundException棧追蹤
D.不顯示任何信息

4.如果有以下的程序片段:

public class FileUtil{
    public static String readFile(String name) throws ____________{
        FileInputStream input=new FileInputStream(name);
        …
    }
}

請問下劃線處填入以下()選項可以通過編譯

A. Throwable
B.Error
C.IOException
D.FileNotFoundException

5.FileInputStream的構造方法使用throws聲明了FileNotFoundException,如果有以下的程序片段:

public class FileUtil{
    public static String readFile(String name) {
        FileInputStream input=null
    try{
            input=new FileInputStream(name);
            …
        }catch(______________ ex){
            …
        }
    }
}

請問下劃線處填入以下()選項可以通過編譯

A. Throwable
B.Error
C.IOException
D.FileNotFoundException

6.如果有以下的程序片段:

public class Resource{
    void doService() throws IOException{
        …
    }
}
class Some extends Resource{
    @Override
    void doService() throws ________ {
        …
    }
}

請問下劃線處填入以下()選項可以通過編譯

A. Throwable
B.Error
C.IOException
D.FileNotFoundException

7.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(ArrayIndexOutOfBoundException | NumberFormatException ex){
            System.out.println(“必須輸入數字”);
        }
    }
}

執行時若沒有指定命令行參數,以下描述正確的是()

A. 編譯錯誤
B.顯示“必須輸入數字”
C.顯示 ArrayIndexOutOfBoundException棧追蹤
D.不顯示任何信息

8.如果有以下的程序片段:

public class Main{
    public static void main(String[] args){
        try{
            int number=Integer.parseInt(args[0]);
            System.out.println(number++);
        }catch(RuntimeException | NumberFormatException ex){
            System.out.println(“必須輸入數字”);
        }
    }
}

執行時若沒有指定命令行參數,以下描述正確的是()

A. 編譯錯誤
B.顯示“必須輸入數字”
C.顯示 ArrayIndexOutOfBoundException棧追蹤
D.不顯示任何信息

9.FileInputStream的構造方法使用throws聲明了FileNotFoundException,如果有以下的程序片段:

public class FileUtil{
    public static String readFile(String name) {
        try(FileInputStream input= new FileInputStream(name)){
            …
        }
    }
}

以下描述正確的是()

A. 編譯失敗
B.編譯成功
C.調用readFile()時必須處理FileNotFoundException
D.調用readFile()時不一定要處理FileNotFoundException

9.如果ResourceSome與ResourceOther都繼承了AutoCloseable接口

public class Main{
    public static void main (String[] args) {
        try(ResourceSome some=new ResourceSome();
            ResourceOther other=new ResourceOther()){
            …
        }
    }
}

以下描述正確的是()

A. 執行完try后會先關閉ResourceSome
B.執行完try后會先關閉ResourceOther
C.執行完main()之后才關閉ResourceSome與ResourceOther
D.編譯失敗

8.4.2 操作題
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 八、深入理解java異常處理機制 引子try…catch…finally恐怕是大家再熟悉不過的語句了, 你的答案是...
    壹點零閱讀 1,630評論 0 0
  • 引言 在程序運行過程中(注意是運行階段,程序可以通過編譯),如果JVM檢測出一個不可能執行的操作,就會出現運行時錯...
    Steven1997閱讀 2,506評論 1 6
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,767評論 18 399
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • 對面9棟住著個姑娘,她每天很晚回家。路過草叢的時候,他會抱著滾球球,喂它吃點東西。一天我噠噠噠經過,滾球球嚴肅地端...
    路語桐閱讀 252評論 0 2