Java 基礎(九)字元流

diamond_lin發表於2017-10-23

終於結束了集合的學習,今天我們就開始學習 I/O的操作了。 I/O 系列的內容分為 I/O概述、字元流、位元組流。今天要學的是 I/O和字元流的操作

由於概述篇幅較短,所以就把概述壓縮到這裡來了。

I/O 概述

I/O:即 Input Output,輸入輸出的意思。

  • IO 流用來處理裝置之間的資料傳輸。
  • JAVA 對資料的操作都是通過流的方式
  • JAVA 用於操作流的物件都在 IO 包裡面
  • 流的操作分兩種:字元流、位元組流
  • 流的流向分兩種:輸入流、輸出流

對資料的操作,其實就是對 File 檔案。我偷了一張祖師爺傳下來的圖來描述 IO 流類結構關係。

從圖中可以看出,都是從這以下四個類中派生出來的子類,子類的型別也好區分,字尾都是抽象基類名。

  • 位元組流抽象基類
    • InputStream
    • OutputStream
  • 字元流抽象基類
    • Reader
    • Writer

IOException

IO 異常大致分為三種,一是 IO 異常、二是找不到檔案異常、三是沒有物件異常。

因此,我們在異常處理的時候,比較嚴峻的寫法應該這樣

FileWriter fileWriter = null;
try {
    fileWriter = new FileWriter("demo.txt");
} catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} finally {
    try {
        if (fileWriter != null) {
            fileWriter.close();
            }
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}複製程式碼

字元流-Reader/Writer

Reader 和 Writer的定義

Reader: 用於讀取字元流的抽象類。子類必須實現的方法只有 read(char[], int, int) 和 close()。但是,多數子類將重寫此處定義的一些方法,以提供更高的效率和/或其他功能。

Writer: 寫入字元流的抽象類。子類必須實現的方法僅有 write(char[], int, int)、flush() 和 close()。但是,多數子類將重寫此處定義的一些方法,以提供更高的效率和/或其他功能。

字元流的讀寫FileReader 和 FileWriter。

先看看基本運用吧~

try {
        // 建立讀取流和寫入流
        FileReader fileReader = new FileReader("raw.txt");
        FileWriter fileWriter = new FileWriter("target.txt");
        // 讀取單個字元,自動往下讀
        int ch;
        while ((ch = fileReader.read()) != -1) {
            fileWriter.write(ch);
        }
            fileReader.close();
            fileWriter.close();
    } catch (IOException e) {
        e.printStackTrace();
    }複製程式碼

每次讀取一個字元再執行寫入操作,效率比較慢,我們可以嘗試一次讀取更多的資料再一次性寫入。

try {
        // 建立讀取流和寫入流
        FileReader fileReader = new FileReader("raw.txt");
        FileWriter fileWriter = new FileWriter("target.txt");
        // 讀取1024個字元,自動往下讀
        char[] buf = new char[1024];
        while (fileReader.read(buf) != -1) {
            fileWriter.write(buf);
        }
        fileReader.close();
        fileWriter.close();
    } catch (IOException e) {
        e.printStackTrace();
    }複製程式碼

以上兩種方法實現了檔案的讀寫操作,將 raw.txt 讀取寫入 targer.txt 檔案裡面。

同時,這步操作也可以當成是檔案的拷貝

需要注意的是:

  • 讀取檔案時,如果路徑對應的檔案不存在會報 IOException
  • 寫入檔案時,如果路徑對應的檔案不存在會自動在該目錄下建立檔案

如果要實現追加寫入檔案的操作,比如將 raw1.txt 和 raw2.txt 共同寫入 target.txt 裡面,只需要在建立 FileWriter 的時候使用兩個引數的構造方法即可,如:FileWriter fileWriter = new FileWriter("target.txt",true);

至於原始碼讀寫的實現,我簡單說一下吧。其實 FileReader/FileWriter 的構造方法裡面都有 new FileInputStream/FileOutputStream 的物件,然後繼承的是InputStreamReader/OutputStreamWriter.這說明啥,我想大家心裡應該有數了吧,其實還是呼叫了位元組流的讀取,然後使用StreamEncoder/StreamDecoder進行編碼解碼操作。

具體流程是這樣的,這裡每次讀/寫太長了,我只說讀取操作了,反正都是相對的。

  1. 首先建立 FileReader 物件,FileReader物件構造方法裡面建立了FileInputStream。然後再使用 FileInputStream 作為引數,建立 StreamDecoder物件。
  2. 呼叫FileReader.read()讀取一個字元,實際上就是呼叫了 StreamDecoder 同時讀取兩個位元組,再使用這兩個位元組組成一個字元返回。

字元流的緩衝區

字元流的緩衝區,提高了對資料的讀寫效率,他有兩個子類

  • BufferedWriter
  • BufferedReader

緩衝區要結合流才可以使用
在流的基礎上對流的功能進行了增強

原始碼就不帶著大家一起讀了,我給大家分析一下 BufferedWriter 的思想。以下內容劃重點,期末考試要考!

要想理解 BufferReader,就要先理解它的思想。BufferReader 的作用是為其它Reader 提供緩衝功能。建立BufferReader 時,我們會通過它的建構函式指定某個Reader 為引數。BufferReader 會將該Reader 中的資料分批讀取,每次讀取一部分到緩衝中;操作完緩衝中的這部分資料之後,再從Reader 中讀取下一部分的資料。
為什麼需要緩衝呢?原因很簡單,效率問題!緩衝中的資料實際上是儲存在記憶體中,而原始資料可能是儲存在硬碟中;而我們知道,從記憶體中讀取資料的速度比從硬碟讀取資料的速度至少快10倍以上。
那幹嘛不乾脆一次性將全部資料都讀取到緩衝中呢?第一,讀取全部的資料所需要的時間可能會很長。第二,記憶體價格很貴,容量不想硬碟那麼大。

通過字元流緩衝區來複制檔案操作

還用上面那個案例

BufferedReader bufferedReader;
BufferedWriter bufferedWriter;
try {
    // 建立讀取流和寫入流
    bufferedReader = new BufferedReader(new FileReader("raw.txt"));
    bufferedWriter = new BufferedWriter(new FileWriter("target.txt", true));
    // 讀取一行字串
    String line;
    while ((line = bufferedReader.readLine()) != null) {
        bufferedWriter.write(line);
    }
    bufferedReader.close();
    bufferedWriter.close();
} catch (IOException e) {
    e.printStackTrace();
}複製程式碼

沒什麼特別的,很簡單

仿寫一個 readLine

上文中,出現了一個 readLine 方法,可以一次讀取一行字串。
其實這個一次讀取一行字串還是蠻有用的,比如說讀取一些Key-value 形式的配置檔案。

我們來看看 JDK 中關於 readLine 的描述

讀取一個文字行。通過下列字元之一即可認為某行已終止:換行 ('\n')、回車 ('\r') 或回車後直接跟著換行。
返回:包含該行內容的字串,不包含任何行終止符,如果已到達流末尾,則返回 null
丟擲:IOException - 如果發生 I/O 錯誤

所以,我們自己要封裝一個 readLine,只需要判斷讀取到的字元是否為'\n'、'\r',再一次性返回就行了。

class MyBufferReaderLine {

    private Reader fr;

    public MyBufferReaderLine(Reader fr) {
        this.fr = fr;
    }

    // 一次讀取一行的方法
    public String readLine() throws IOException {

        // 定義臨時容器
        StringBuilder sb = new StringBuilder();
        int ch = 0;
        while ((ch = fr.read()) != -1) {

            if (ch == '\r' || ch == '\n') {
                return sb.toString();
            } else {
                sb.append((char) ch);
            }
        }
        if(sb.length() != 0){
            return sb.toString();
        }
        return null;
    }

    public void close() throws IOException {
        fr.close();
    }
}複製程式碼

程式碼實現很簡單,就是參考 BufferedReader 寫的一個包裝類。

LineNumberReader

你們先感受一下這個類的用法

FileReader fr;
try {
    fr = new FileReader("test.txt");
    LineNumberReader lnr = new LineNumberReader(fr);
    String line;
    while ((line = lnr.readLine()) != null) {
        System.out.println(lnr.getLineNumber() + ":" + line);
    }
    lnr.close();
} catch (IOException e) {
    e.printStackTrace();
}複製程式碼

這貨和我們剛剛手寫的MyBufferReaderLine 基本沒啥區別,繼承自BufferedReader ,然後多了一個lineNumber 屬性,lineNumber用來記錄當前行數。
實現沒有什麼意義,我們在MyBufferReaderLine 上新增一個欄位lineNumber,每次 readLine 成功之後 lineNumber++ 即可。

但是,為什麼要講他呢,因為他和 BufferedReader 一樣,也是一個包裝類啊。

包裝類就是裝飾設計模式啊~

好了,你們都知道了,就是提一下裝飾模式。

裝飾模式

當想要對已有的物件進行功能增強時,可以定義一個類,將已有物件傳入,並且提供加強功能,那麼自定義的該類就稱為裝飾類。
裝飾模式又名包裝(Wrapper)模式。裝飾模式以對客戶端透明的方式擴充套件物件的功能,是繼承關係的一個替代方案。

可能有的同學會問,那麼為什麼不用繼承呢?我寫一個新的類,繼承、再新增擴充套件方法或者重寫方法也可以實現呀。

假如咖啡廳賣咖啡,運營可一段時間,發現客戶對咖啡的甜度有不同的需求,有如下三種需求加少量糖、一般糖、多糖。程式碼實現可以給 Coffee 新增一個糖量的屬性,但是一開始設計 Coffee 這個類的時候沒有加這個屬性,根據開發守則,我們是不應該去修改原 Coffee 類,此時可以選擇新增三個子類,LowSurgeCoffee、MidSurgeCoffee、HightSurgeCoffee,或者使用裝飾模式,新增3個不同糖量的 SurgeDecorator。此時,使用裝飾模式和繼承沒什麼區別。但是執行了一段時間之後,需求又加了,咖啡需要新增口味卡布奇諾和摩卡。此時再組合之前的三種糖量,一共需要9個咖啡類。但是如果使用裝飾模式,只需要新增摩卡和卡布奇諾裝飾器就行了。一共6個裝飾類。之後再擴充套件新的口味需要的子類是乘算,但是如果是裝飾類,就只是加算。

以上這個例子沒有程式碼實現,因為我懶。。。。。。

針對的問題:

動態地給一個物件新增一些額外的職責。就增加功能來說,Decorator模式相比生成子類更為靈活。不改變介面的前提下,增強所考慮的類的效能。

何時使用
  • 需要擴充套件一個類的功能,或給一個類增加附加責任。
  • 需要動態的給一個物件增加功能,這些功能可以再動態地撤銷。
  • 需要增加一些基本功能的排列組合而產生的非常大量的功能,從而使繼承變得不現實。
優缺點
  • 裝飾者模式比繼承要靈活,避免了繼承體系的臃腫,而且降低了類與類之間的關係
  • 裝飾類因為增強已有物件,具備功能和已有的想相同,只不過提供了更強的功能,所以裝飾類和被裝飾類通常屬於一個體系中的

相關文章