1. 程式人生 > 實用技巧 >Java 語法 -- IO 流

Java 語法 -- IO 流

Java 的 IO 流分為兩大類:

  • 位元組流
  • 字元流

從超類到實現類,Java IO 流的繼承關係大致如下圖:

InputStream

InputStream 就是 Java 標準庫提供的最基本的位元組輸入流。它位於java.io這個包裡。java.io包提供了所有同步IO的功能。

要特別注意的一點是,InputStream 並不是一個介面,而是一個抽象類,它是所有輸入流的超類。

InputStream 類定義的一個最重要的方法就是 int read(),這個方法會讀取輸入流的下一個位元組,並返回位元組表示的 int 值(0~255)。如果已讀到末尾,返回 -1 表示不能繼續讀取了。

FileInputStream

FileInputStream 是 InputStream 的一個子類。顧名思義,FileInputStream 就是從檔案流中讀取資料。下面的程式碼演示瞭如何完整地讀取一個FileInputStream的所有位元組:

public void readFile() throws IOException {
    // 建立一個FileInputStream物件:
    InputStream input = new FileInputStream("src/readme.txt");
    for (;;) {
        int n = input.read(); // 反覆呼叫read()方法,直到返回-1
        if (n == -1) {
            break;
        }
        System.out.println(n); // 列印byte的值
    }
    input.close(); // 關閉流
}

InputStream 和 OutputStream 都是通過 close() 方法來關閉流。關閉流就會釋放對應的底層資源。

所有與 IO 操作相關的程式碼都必須正確處理 IOException, 如果讀取過程中發生了 IO 錯誤,InputStream 就沒法正確地關閉,資源也就沒法及時釋放。

利用 Java 7引入的新的 try(resource) 的語法,只需要編寫 try 語句,讓編譯器自動為我們關閉資源。推薦的寫法如下:

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println(n);
        }
    } // 編譯器在此自動為我們寫入finally並呼叫close()
}

OutputStream

和 InputStream 相反,OutputStream 是 Java 標準庫提供的最基本的位元組輸出流。

和InputStream類似,OutputStream也是抽象類,它是所有輸出流的超類。這個抽象類定義的一個最重要的方法就是void write(int b)

和InputStream類似,OutputStream也提供了close()方法關閉輸出流,以便釋放系統資源。要特別注意:OutputStream還提供了一個flush()方法,它的目的是將緩衝區的內容真正輸出到目的地。

FileOutputStream

用 FileOutputStream 可以從檔案獲取輸出流,這是 OutputStream 常用的一個實現類。

以 FileOutputStream 為例,演示如何將若干個位元組寫入檔案流:

public void writeFile() throws IOException {
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write(72); // H
    output.write(101); // e
    output.write(108); // l
    output.write(108); // l
    output.write(111); // o
    output.close();
}

每次寫入一個位元組非常麻煩,更常見的方法是一次性寫入若干位元組。這時,可以用 OutputStream 提供的過載方法void write(byte[])來實現:

public void writeFile() throws IOException {
    OutputStream output = new FileOutputStream("out/readme.txt");
    output.write("Hello".getBytes("UTF-8")); // Hello
    output.close();
}

和InputStream一樣,上述程式碼沒有考慮到在發生異常的情況下如何正確地關閉資源。寫入過程也會經常發生IO錯誤,例如,磁碟已滿,無許可權寫入等等。我們需要用try(resource)來保證OutputStream在無論是否發生IO錯誤的時候都能夠正確地關閉:

public void writeFile() throws IOException {
    try (OutputStream output = new FileOutputStream("out/readme.txt")) {
        output.write("Hello".getBytes("UTF-8")); // Hello
    } // 編譯器在此自動為我們寫入finally並呼叫close()
}

​​PrintStream

PrintStream 繼承自 FilterOutputStream, FilterOutputStream 又繼承自 OutputStream。

它在OutputStream的介面上,額外提供了一些寫入各種資料型別的方法:

寫入int:print(int)
寫入boolean:print(boolean)
寫入String:print(String)
寫入Object:print(Object),實際上相當於print(object.toString())

以及對應的一組println()方法,它會自動加上換行符

我們經常使用的System.out.println()實際上就是使用PrintStream列印各種資料。其中,System.out是系統預設提供的PrintStream,表示標準輸出:

System.out.print(12345); // 輸出12345
System.out.print(new Object()); // 輸出類似java.lang.Object@3c7a835a
System.out.println("Hello"); // 輸出Hello並換行
System.err是系統預設提供的標準錯誤輸出。

PrintStream和OutputStream相比,除了添加了一組print()/println()方法,可以列印各種資料型別,比較方便外,它還有一個額外的優點,就是不會丟擲IOException,這樣我們在編寫程式碼的時候,就不必捕獲IOException。

序列化與反序列化

序列化是指把一個Java物件變成二進位制內容,本質上就是一個byte[]陣列。

為什麼要把Java物件序列化呢?因為序列化後可以把byte[]儲存到檔案中,或者把byte[]通過網路傳輸到遠端,這樣,就相當於把Java物件儲存到檔案或者通過網路傳輸出去了。

有序列化,就有反序列化,即把一個二進位制內容(也就是byte[]陣列)變回Java物件。有了反序列化,儲存到檔案中的byte[]陣列又可以“變回”Java物件,或者從網路上讀取byte[]並把它“變回”Java物件。

ObjectOutputStream

把一個 Java 物件變為 byte[] 陣列,需要使用 ObjectOutputStream。它負責把一個 Java 物件寫入一個位元組流:

ObjectInputStream

和ObjectOutputStream相反,ObjectInputStream負責從一個位元組流讀取Java物件:

try (ObjectInputStream input = new ObjectInputStream(...)) {
    int n = input.readInt();
    String s = input.readUTF();
    Double d = (Double) input.readObject();
}

除了能讀取基本型別和String型別外,呼叫readObject()可以直接返回一個Object物件。要把它變成一個特定型別,必須強制轉型。

Java的序列化機制僅適用於Java,如果需要與其它語言交換資料,必須使用通用的序列化方法,例如JSON。

Reader 介面

Reader是Java的IO庫提供的另一個輸入流介面。和InputStream的區別是,InputStream是一個位元組流,即以byte為單位讀取,而Reader是一個字元流,即以char為單位讀取:

java.io.Reader是所有字元輸入流的超類,它最主要的方法是:int read()

這個方法讀取字元流的下一個字元,並返回字元表示的int,範圍是0~65535。如果已讀到末尾,返回-1。

​BufferedReader

BufferedReader 是緩衝字元輸入流。它繼承於Reader。

BufferedReader 的作用是為其他字元輸入流新增一些緩衝功能。

提供了讀取一行的功能:readLine()

​LineNumberReader

LineNumberReader 繼承自 BufferedReader,並且增加了下面兩個功能:

  • 獲取行號:getLineNumber()
  • 設定行號:setLineNumber()

​InputStreamReader

InputStreamReader : 是位元組流與字元流之間的橋樑,能將位元組流輸出為字元流,並且能為位元組流指定字符集,可輸出一個個的字元.

​FileReader

​FileReader 繼承自 InputStreamReader

它可以開啟檔案並獲取Reader。下面的程式碼演示瞭如何完整地讀取一個FileReader的所有字元:

public void readFile() throws IOException {
    // 建立一個FileReader物件:
    Reader reader = new FileReader("src/readme.txt"); 
    for (;;) {
        int n = reader.read(); // 反覆呼叫read()方法,直到返回-1
        if (n == -1) {
            break;
        }
        System.out.println((char)n); // 列印char
    }
    reader.close(); // 關閉流
}

​StringReader

StringReader是介面 Reader 的一個實現類,其用法是讀取一個String字串。

Writer 介面

Reader是帶編碼轉換器的InputStream,它把byte轉換為char,而Writer就是帶編碼轉換器的OutputStream,它把char轉換為byte並輸出。

Writer和OutputStream的區別如下:

Writer是所有字元輸出流的超類,它提供的方法主要有:

  • 寫入一個字元(0~65535):void write(int c);
  • 寫入字元陣列的所有字元:void write(char[] c);
  • 寫入String表示的所有字元:void write(String s)。

​BufferedWriter

BufferedWriter 為帶有預設緩衝的字元輸出流。

主要方法:

  • void write(char ch);//寫入單個字元。

  • void write(char []cbuf,int off,int len)//寫入字元資料的某一部分。

  • void write(String s,int off,int len)//寫入字串的某一部分。

  • void newLine()//寫入一個行分隔符。

  • void flush();//重新整理該流中的緩衝。將緩衝資料寫到目的檔案中去。

  • void close();//關閉此流,再關閉前會先重新整理他。

​OutputStreamWriter

整個IO包實際上分為位元組流和字元流,但是除了這兩個流之外,還存在一組位元組流-字元流的轉換類。

OutputStreamWriter:是Writer的子類,將輸出的字元流變為位元組流,即將一個字元流的輸出物件變為位元組流輸出物件。

InputStreamReader:是Reader的子類,將輸入的位元組流變為字元流,即將一個位元組流的輸入物件變為字元流的輸入物件。

如果以檔案操作為例,則記憶體中的字元資料需要通過OutputStreamWriter變為位元組流才能儲存在檔案中,讀取時需要將讀入的位元組流通過InputStreamReader變為字元流。過程如下:

寫入資料-->記憶體中的字元資料-->字元流-->OutputStreamWriter-->位元組流-->網路傳輸(或檔案儲存)
讀取資料<--記憶體中的字元資料<--字元流<--InputStreamReader<--位元組流<--網路傳輸(或檔案儲存)

可以清楚地發現,不管如何操作,最終全部是以位元組的形式儲存在檔案中或者進行網路傳輸。

​FileWriter

FileWriter 類從 OutputStreamWriter 類繼承而來。該類按字元向流中寫入資料。

它的使用方法和FileReader類似:

try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
    writer.write('H'); // 寫入單個字元
    writer.write("Hello".toCharArray()); // 寫入char[]
    writer.write("Hello"); // 寫入String
}

​PrintWriter

PrintStream最終輸出的總是byte資料,而PrintWriter則是擴充套件了Writer介面,它的print()/println()方法最終輸出的是char資料。兩者的使用方法幾乎是一模一樣的:

public class Main {
    public static void main(String[] args)     {
        StringWriter buffer = new StringWriter();
        try (PrintWriter pw = new PrintWriter(buffer)) {
            pw.println("Hello");
            pw.println(12345);
            pw.println(true);
        }
        System.out.println(buffer.toString());
    }
}

​StringWriter

​StringWriter 是介面 Writer 的一個實現類,主要作用是寫入一個字串。

每天學習一點點,每天進步一點點。