2018年12月5日 星期三

Java SE 入門教學 - IO 流

更新時間:12/05/2018

前言

Java 流式輸入/輸出原理

在 Java 程序中,對於數據的輸入/輸出操作以「流(Stream)」方式進行;JDK提供了各種各樣「流」的類別,用以獲取不同種類的數據;程序中通過標準的方法輸入或輸出數據。

流是用來讀寫數據的,java 有一個類別叫 File,它封裝的是文件的文件名,只是記憶體裡面的一個物件,真正的文件是在硬碟上的一塊空間,在這個文件裡面存放著各種各樣的數據,我們想讀文件裡面的數據怎麼辦呢?是通過一個流的方式來讀,我們想從程序讀數據,對於計算機來說,無論讀什麼類型的數據都是以 010101101010 這樣的形式讀取的。怎麼把文件裡面的數據讀出來呢?您可以把文件想像成一個小桶,文件就是一個桶,文件裡面的數據就相當於是這個桶裡面的水,那麼我們怎麼從這個桶裡面取水呢?也就是怎麼從這個文件讀取數據呢?

常見的取水辦法是我們用一根管道插到桶上面,然後在管道的另一邊打開水龍頭,桶裡面的水就開始嘩啦嘩啦地從水龍頭里流出來了,桶裡面的水是通過這根管道流出來的,因此這根管道就叫流,Java 裡面的流式輸入/輸出跟水流的原理一模一樣,當您要從文件讀取數據的時候,一根管道插到文件裡面去,然後文件裡面的數據就順著管道流出來,這時你在管道的另一頭就可以讀取到從文件流出來的各種各樣的數據了。當您要往文件寫入數據時,也是通過一根管道,讓要寫入的數據通過這根管道嘩啦嘩啦地流進文件裡面去。除了從文件去取數據以外,還可以通過網絡,比如用一根管道把我和你的機子連接起來,我說一句話,通過這個管道流進你的機子裡面,你馬上就可以看得到,而你說一句話,通過這根管道流到我的機子裡面,我也馬上就可以看到。有的時候,一根管道不夠用,比方說這根管道流過來的水有一些雜質,我們就可以在這個根管道的外面再包一層管道,把雜質給過濾掉。從程序的角度來講,從計算機讀取到的原始數據肯定都是 010101 這種形式的,一個字節一個字節地往外讀,當你這樣讀的時候你覺得這樣的方法不合適,沒關係,你再在這根管道的外面再包一層比較強大的管道,這個管道可以把 010101 幫你轉換成字符串。這樣你使用程序讀取數據時讀到的就不再是 010101 這種形式的數據了,而是一些可以看得懂的字符串了。



一、輸入輸出流分類

java.io 套件包中定義了多個流型態(類別或抽象類別)來實現輸入/輸出功能。可以從不同的角度對其進行分類:

  • 按數據流的方向不同可以分為輸入流和輸出流。
  • 按處理數據單位不同可以分為字節流(byte stream, 位元組流)和字符流(character stream, 字符串流)。
  • 按照功能不同可以分為節點流和處理流。

字節流(byte stream):最原始的一個流,讀出來的數據就是 010101 這種最底層的數據表示形式,只不過它是按照字節(byte)來讀的,一個字節(byte)是8個位元(bit),讀的時候不是一個位元一個位元來讀,而是一個字節一個字節來讀。

字符流(character stream):字符流是一個字符一個字符地往外讀取數據。一個字符是2個字節。

Java 所提供的所有流類型位於套件包 java.io 內都分別繼承自以下四種流類別。

字節流 字符流
輸入流 InputStream Reader
輸出流 OutputStream Writer

什麼叫輸入流?什麼叫輸出流?用一根管道一端插進文件裡程序裡,然後開始讀數據,那麼這是輸入還是輸出呢?如果站在文件的角度上,這叫輸出,如果站在程序的角度上,這叫輸入。

記住,以後說輸入流和輸出流都是站在程序的角度上來說。



二、節點流和處理流

節點流為可以從一個特定的數據源(節點)讀寫數據(如:文件、記憶體)

處理流是連接已存在的流(節點流或處理流),通過對數據的處理為程序提供更為強大的讀寫功能。

你要是對原始的流不滿意,你可以在這根管道外面再套其它的管道,套在其它管道之上的流叫處理流。為什麼需要處理流呢?這就跟水流裡面有雜質,你要過濾它,你可以再套一層管道過濾這些雜質一樣。

3.1 節點流類型

節點流就是一根管道直接插到數據源上面,直接讀數據源裡面的數據,或者是直接往數據源裡面寫入數據。典型的節點流是文件流:文件的字節輸入流(FileInputStream),文件的字節輸出流(FileOutputStream),文件的字符輸入流(FileReader),文件的字符輸出流(FileWriter)。

類型 字節流 字符流
File(文件) FileInputStream
FileOutputStream
FileReader
FileWriter
Memory Array ByteArrayInputStream
ByteArrayOutputStream
CharArrayReader
CharArrayWriter
Memory String - StringReader
StringWriter
Pipe(管道) PipedInputStream
PipedOutputStream
PipedReader
PipedWriter

3.2 處理流類型

處理流是包在別的流上面的流,相當於是包到別的管道上面的管道。

類型 字節流 字符流
Buffering BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter
Filtering FilterInputStream
FilterOutputStream
FilterReader
FilterWriter
Converting between
bytes and character
- InputStreamReader
OutputStreamWriter
Object Serialization ObjectInputStream
ObjectOutputStream
-
Data Conversion DataInputStream
DataOutputStream
-
Counting LineNumberInputStream LineNumberReader
Peeking ahead PushbackInputStream PushbackReader
Printing PrintStream PrintWriter


三、OutputStream(輸出流)

繼承自 OutputStream 的流是用於程序中輸出數據,且數據的單位為字節(byte, 8 bits)。下圖中有淺藍色背景為節點流,白色為處理流。

基本用法:

//向輸出流中寫入一個字節數據,
//要寫入的字節數據是參數 b 的 8 個低位元,而 24 個高位元被忽略

void write(int b) throws IOException

//將一個字節類型的數組中的數據寫入輸出流
void write(byte[] b) throws IOException

//將一個字節類型數組中的指定位置(off)開始的 len 個字節寫入到輸出流
void write(byte[] b, int off, int len) throws IOException

//關閉流釋放內存資源
void close() throws IOException

//將輸出流中緩衝的數據全部寫出到目的地
void flush() throws IOException

以 File(文件) 這個類型作為講解的典型代表:

建立物件時,要處理 FileNotFoundException 例外。
寫出檔案時,要處理 IOException 例外。
記得要關檔。

FileOutputStream 範例:

Try-with-resources:JDK 1.7 後新增的語法,只有實作 java.lang.AutoCloseable 的任何類別(包括實作 java.io.Closeable 的所有類別)才可以使用新語法。使用新語法的好處有 3 點:

(i) 使用後,資源自動關閉,無須開發人員撰寫程式碼關閉。
(ii) 在 try-with-resources 中,關閉資源時拋出的異常將被抑制。
(iii) 使您的代碼看起來更短,更易讀。

JDK 1.7 之前:

try{
  //open resources like File, Database connection, Sockets etc
} catch (FileNotFoundException e) {
  // exception handling like FileNotFoundException, IOException etc
}finally{
  // close resources
}

JDK 1.7 之後:

try(// open resources here){
  // use resources
} catch (FileNotFoundException e) {
  // exception handling
}
// resources are closed as soon as try-catch block is executed.

將上述程式碼改成使用 try-with-resources

FileOutputStream 的建構子如果第二個參數打上 true,表示寫入時,舊資料會保留,新資料而增加在後面。



四、InputStream(輸入流)

繼承自 InputStream 的流都是用於向程序中輸入數據,且數據的單位為字節(byte, 8 bits)。下圖中有淺藍色背景為節點流,白色為處理流。

基本用法:

//讀取一個字節並以整數的形式返回(0~255)。
//如果返回 -1,則已到輸入流的末尾。

int read() throws IOException

//讀取一系列字節並儲存到一個數組 Buffer。
//返回實際讀取的字節數,如果讀取前已到輸入流的末尾則返回 -1。

int read(byte[] buffer) throws IOException

//讀取 length 個字節
//並儲存到一個字節數組 buffer,從 length 位置開始
//返回實際讀取的字節數,如果讀取前已到輸入流的末尾則返回 -1。

int read(byte[] buffer, int offset, int length) throws IOException

//關閉流釋放內存資源
void close() throws IOException

//跳過 n 個字節不讀,返回實際跳過的字節數
long skip(long n) throws IOException

以 File(文件) 這個類型作為講解的典型代表:

建立物件時,要處理 FileNotFoundException 例外。
讀取檔案時,要處理 IOException 例外。

FileInputStream 範例:使用 try-with-resources 新語法。

您會發現輸出的是一堆介於 -128 ~ 127 的數字,那是因為使用「字節符」讀取檔案,所以每一個都是一個字節(byte)。

我們把第 13 行改成 "System.out.println((char)data[0]);" 執行後,會出現

Year : ????2018?A????107?C
Time : ?U?? 09:13?C

您會發現所有的中文字都變成亂碼,只剩下英文與數字。那是因為中文字需要使用 2 bytes 才能表示,如果一個 byte 一個 byte 變成字元,自然沒有辦法組合成正確的中文字。可以使用 String 類別,幫我們把 byte 數組轉成字串。程式碼如下:

這樣可以正常顯示中文字,但不能使用很大的檔案,因為使用這種方式是一次把檔案的資料全部讀取給輸入流物件,太大的檔案會拋出 OutOfMemoryError 錯誤。
顯然這不是一個好辦法,後續我們將使用「處理流」來強化這種狀況。



五、Writer(輸出流)

繼承自 Writer 的流都是用於向程序中輸出數據,且數據的單位為字節(2 bytes, 16 bits)。下圖中有淺藍色背景為節點流,白色為處理流。

基本用法:

//向輸出流中寫入一個字符數據,
//要寫入的字符數據是參數 c 的 16 個低位元,而 16 個高位元被忽略

void write(int c) throws IOException

//將一個字符類型的數組中的數據寫入輸出流
void write(char[] cbuf) throws IOException

//將一個字符類型數組中的指定位置(offset)開始的 length 個字符寫入到輸出流
void write(char[] cbuf, int offset, int length) throws IOException

//將一個字符串中的字符寫入到輸出流
void write(String string) throws IOException

//將一個字符串從 offset 開始的 length 個字符寫入到輸出流
void write(String string, int offset, int length) throws IOException

//關閉流釋放內存資源
void close() throws IOException

//將輸出流中緩衝的數據全部寫出到目的地
void flush() throws IOException

以 File(文件) 這個類型作為講解的典型代表:

建立物件與檔案時,要處理 IOException 例外。

FileWriter 範例:使用 try-with-resources 新語法。



六、Reader(輸入流)

繼承自 Reader 的流都是用於向程序中輸入數據,且數據的單位為字符(2 bytes, 16 bits)。下圖中有淺藍色背景為節點流,白色為處理流。

基本用法:

//讀取一個字符並以整數的形式返回(0~255)。
//如果返回 -1,則已到輸入流的末尾。

int read() throws IOException

//讀取一系列字符並儲存到一個數組 Buffer。
//返回實際讀取的字符數,如果讀取前已到輸入流的末尾則返回 -1。

int read(char[] cbuf) throws IOException

//讀取 length 個字符
//並儲存到一個字符數組 buffer,從 length 位置開始
//返回實際讀取的字節數,如果讀取前已到輸入流的末尾則返回 -1。

int read(char[] cbuf, int offset, int length) throws IOException

//關閉流釋放內存資源
void close() throws IOException

//跳過 n 個字符不讀,返回實際跳過的字符數
long skip(long n) throws IOException

以 File(文件) 這個類型作為講解的典型代表:

建立物件時,要處理 FileNotFoundException 例外。
讀取檔案時,要處理 IOException 例外。

FileReader 範例:使用 try-with-resources 新語法。



七、處理流詳解 - 串流的串接

7.1 緩衝流(Buffering)

緩衝流要"套接"在相應的節點流之上,對讀寫的數據提供了緩衝的功能,提高了讀寫的效率,同時增加了一些新的方法。

Java 提供四種緩衝流,其常用的構造方法為:
  BufferedReader(Reader in)
  BufferedReader(Reader in, int sz) // sz 為自定緩存區的大小
  BufferedWriter(Writer out)
  BufferedWriter(Writer out, int sz)
  BufferedInputStream(InputStream in)
  BufferedInputStream(InputStream in, int size)
  BufferedOutputStream(OutputStream out)
  BufferedOutputStream(OutputStream out, int size)

  • 緩衝輸入流支持其父類別的 mark() 和 reset() 方法。
  • BufferedReader 提供了 readLine() 方法,用於讀取一行字符串(以\r或\n分隔)。
  • BufferedWriter 提供了 newLine() 方法,用於寫入一個「行分隔符」。
  • 對於輸出的緩衝流,寫出的數據會先在內存中緩存,使用 flush() 方法將會使內存中的數據立刻寫出。

帶有緩衝區的,緩衝區(Buffer)就是內存裡面的一小塊區域,讀寫數據時都是先把數據放到這塊緩衝區域裡面,減少 io 對硬盤的訪問次數,保護我們的硬盤。可以把緩衝區想像成一個小桶,把要讀寫的數據想像成水,每次讀取數據或者是寫入數據之前,都是先把數據裝到這個桶裡面,裝滿了以後再做處理,這就是所謂的緩衝。先把數據放置到緩衝區上,等到緩衝區滿了以後,再一次把緩衝區裡面的數據寫入到硬盤上或者讀取出來,這樣可以有效地減少對硬盤的訪問次數,有利於保護我們的硬盤。

使用 BufferedWriter 和 BufferedReader 範例

往 dat.txt 這個文件寫入數據的時候,直接從節點流 FileWriter 寫入覺得不好寫,因此在節點流的外部,包了一層處理流 BufferedWriter,這樣寫入數據時是先通過處理流把數據寫入到緩衝區(Buffer)裡面,再通過節點流寫入到文件 dat.txt。

讀取 dat.txt 這個文件裡面的數據時,通過節點流 FileReader 直接讀取數據是一個字符一個字符地讀取,這樣讀取的效率太慢了,因此在節點流 FileReader 外部,包了一層處理流 BufferedReader,先把要讀取的數據通過 BufferedReader 處理流存放到內存的緩衝區裡面,然後再使用 flush() 方法把緩衝區裡面的數據立刻寫出來。

在 BufferedInputStream 中使用 mark() 和 reset() 範例

mark(int readlimit) 方法表示,標記當前位置,並保證在 mark 以後最多可以讀取 readlimit 字節數據,mark 標記仍有效。如果在 mark 後讀取超過 readlimit 字節數據,mark 標記就會失效,調用 reset() 方法會有異常。

7.2 轉換流

轉換流非常的有用,它可以把一個字節流轉換成一個字符流,轉換流有兩種,一種叫 InputStreamReader,另一種叫 OutputStreamWriter。 InputStream 是字節流,Reader 是字符流,InputStreamReader 就是把 InputStream 轉換成 Reader。 OutputStream 是字節流,Writer 是字符流,OutputStreamWriter 就是把 OutputStream 轉換成 Writer。把 OutputStream 轉換成 Writer 之後就可以一個字符一個字符地通過管道寫入數據了,而且還可以寫入字符串。我們如果用一個 FileOutputStream 流往文件裡面寫東西,得要一個字節一個字節地寫進去,但是如果我們在 FileOutputStream 流上面套上一個字符轉換流,那我們就可以一個字符串一個字符串地寫進去。

  • InputStreamReader 和 OutputStreamWriter 用與字節數據到字符數據之間的轉換。
  • InputStreamReader 需要和 InputStream 串接。
  • OutputStreamReader 需要和 OutputStream 串接。
  • 轉換流在構造時可以指定其編碼集合,例如
     InputStream isr = new InputStreamReader(System.in, "ISO8859_1");

使用 OutputStreamReader + FileOutputStream 和 InputStreamReader + FileInputStream 範例

如果直接從 FileOutputStream 流寫入數據,那麼只能是一個字節字節地寫入到 char.txt 文件裡面,但是如果在它外面,包了一層轉換流就可以一個字符串一個字符串地寫入到文件裡面。

使用 System.in 串接 InputStreamReader 再串接 BufferedReader 範例

7.3 數據流

  • DataInputStream 和 DataOutputStream 分別繼承自 InputStream 和 OutputStream,它屬於處理流,需要分別串接在 InputStream 和 OutputStream 類型的節點流上。
  • DataInputStream 和 DataOutputStream 提供了可以存取與機器無關的 Java 原始類型數據(如:int, double 等)的方法。
  • DataInputStream 和 DataOutputStream 的構造方法為:
     DataInputStream(InputStream in)
     DataOutputStream(OutputStream out)

通過 bais 這個流往外讀取數據的時候,是一個字節一個字節地往外讀取的,因此讀出來的數據無法判斷是字符串還是 bool 類型的值,因此要在它的外面再套一個流,通過 dataInputStream 把讀出來的數據轉換就可以判斷了。注意了:讀取數據的時候是先寫進去的就先讀出來,因此讀 ByteArray 字節數組數據的順序應該是先把佔 8 個字節的 double 類型的數讀出來,然後再讀那個只佔一個字節的 boolean 類型的數,因為 double 類型的數是先寫進數組裡面的,讀的時候也要先讀它。這就是所謂的先寫的要先讀。如果先讀 Boolean 類型的那個數,那麼讀出來的情況可能就是把 double 類型數的 8 個字節裡面的一個字節讀了出來。

7.4 打印流

  • PrintWriter 和 PrintStream 都屬於輸出流,分別針對與字符和字節。
  • PrintWriter 和 PrintStream 提供了多載(Overloading)的 print() 方法。
  • PrintWriter 和 PrintStream 的輸出操作不會拋出異常,用戶通過檢測錯誤狀態獲取錯誤信息。
  • PrintWriter 和 PrintStream 有自動 flush 功能。

PrintWriter(Writer out)
PrintWriter(Writer out, boolean autoFlush)
PrintWriter(OutputStream out)
PrintWriter(OutputStream out, boolean autoFlush)
PrintStream(OutputStream out)
PrintStream(OutputStream out, boolean autoFlush)

使用 setOut() 方法後,會把 out 輸出流默認連接 DOS 窗口給改成指定的與 out 連接的流,out 輸出流和 ps 打印流連接在一起,因此此時的 out 輸出流就不再和 DOS 窗口連在一起,這時再執行 System.out.println() 操作時就不再是在 Dos 窗口下顯示出打印的信息了。執行 System.setOut(ps); 命令時,首先會把要打印的數據信息傳遞到 ps 這個打印流,再傳遞到 fos 這個打印輸出流,因此最終打印出來的信息是顯示在 log.dat 文件裡面了。也就是說,使用 setOut() 方法可以重新設置 out 輸出流與其它流的串接方式,繼而可以改變打印信息顯示的地方。

7.5 物件流--Object

直接將物件寫入或讀取,此議題為「永續化(Persistence)」。

利用 Serializable 介面完成物件的序列化,此介面不須實作任何方法,由 JVM 完成。
也可以利用 Externalizable 介面完成物件序列化,此介面繼承 Serializable 介面,必須實作 readExternal(ObjectInput in) 和 writeExternal(ObjectOutput out) 方法,且預設建構子(無參數)必須保留。

物件序列化在 C++ 時代就有被討論的議題,Java 於 1.1 以後提供一個介面 Serializable 實作此序列化機制。所謂序列化,就是能將記憶體(Memory)中的實體物件(Object Instance)以位元流(byte stream)方式儲存於永久媒體中,如硬碟,此過程叫序列化。而且之後也能從硬碟中再讀取該物件於記憶體中重建,並回復先前狀態。

永續化的注意事項:

7.5.1) 只有非靜態的屬性變數才能序列化。非靜態方法、靜態屬性變數、靜態方法都不能被序列化。
7.5.2) 串流的物件也不能被序列化
7.5.3) transient 關鍵字
使用 transient 宣告的基本型態(primitive type)或物件(object)變數,JVM 不會將此序列化。
例如敏感性資料像是密碼等,或是在其他環境不可獲得的資源如 JDBC 及 Network Socket 等。
總之只要基本型態或物件以 transient 宣告,JVM 就不會將它序列化。
7.5.4) 如果類別有繼承關係,欲序列化子類別時,其父類別也要實作(implements) Serializable 介面。
7.5.5) 序列化失敗會拋出 NotSerializableException 例外
7.5.6) 如果有宣告 SerialVersionUID(其存取修飾子一定是 private static long)的話,在反序列化時會去檢核。
反序列化失敗會拋出 InvalidClassException 例外。

實作 Serializable 介面的範例:

獨立儲存一個 Rectangle.java 檔案,並編譯。

撰寫 WriteObj.java 測試寫入序列化物件。

撰寫 ReadObj.java 測試讀取序列化的檔案。



八、總結

InputStream / OutputStream 、 Reader / Writer 這四個類別是流裡面最基礎的類別。

FileInputStream / FileOutputStream 、 FileReader / FileWriter 都是對文件進行讀寫操作的。

BufferedInputStream / BufferedOutputStream 、 BufferedReader / BufferedWriter 緩衝流都是在內存裡面開一小塊區塊,減少 IO 對硬碟的訪問次數。

InputStreamReader / OutputStreamWriter 轉換流,把字節流轉換成字符流。

DataInputStream / DataOutputStream 通過數據流可以直接寫基礎的數據類型,另外還可以寫 Unicode 的字符串,而且是 UTF-8 的字符串,UTF-8 比較省空間,因此大多數在網上傳輸的時候用的都是 UTF-8。

PrintStream / PrintWriter 打印流,這個流沒有相對應的輸入和輸出。

ObjectInputStream / ObjectOutputStream 在這個流裡面需要掌握關鍵字 transient (透明的),某一個屬性是透明的,序列化的時候就不考慮這個屬性,還有 Serialization 介面,它是一個標記性介面,通過這個介面標記的物件可以被序列化,並且是由 JVM 幫控制這個類別的物件怎麼序列化。還有一個介面是 Externalizable,通過這個介面標記的類別就可以自己控制物件的序列化。





沒有留言:

張貼留言