愉快地學Java語言:第十六章 輸入與輸出 第2講

導讀

本文適合Java入門,不太適合Java中高級軟件工程師。本文以《Java語言程序設計基礎》第10版為藍本,採用不斷提出問題,然後解答問題的方式來講述。本篇文章只是這個系列中的一篇,如果你喜歡這種講解方式,或者覺得從中能學到知識,可以關注我,以便查閱本系列其他文章。

愉快地學Java語言:第十六章 輸入與輸出 第2講

讓我們開始愉快地學習Java語言吧!

1輸入/輸出流

輸入流:能夠從目標資源中讀出一個字節序列的對象

輸出流:能夠向目標資源中寫入一個字節序列的對象

InputStream、OutputStream這兩個抽象類構成輸入輸出流的基類。

下面是InputStream的定義,列出了一些方法。InputStream實現了Closeable接口,這表明可以使用try語句來關閉資源。

public abstract class InputStream implements Closeable{
\t...
\t//從輸入流中讀取下一個字節數據。返回值範圍為0到255。如果已經達到流\t
//的最後,則返回值-1。
public abstract int read() throws IOException;
\t//從輸入流中讀取b.length個字節到數組b中,並且返回實際讀取的宇節數。 \t
//如果已經達到流的最後,則返回值-1。
\tpublic int read(byte b[]) throws IOException{...}
\t//從輸入流中讀取len個字節,將它們依次存儲到
\t//b[off], b[off + 1],… ,b[off + \tlen-1] 中,返回實際讀取的字節數,

\t//也就是說,實際讀取的字節數可能小於len
\tpublic int read(byte b[], int off, int len)throws IOException{...}
\t//跳過並丟棄輸入流中前n個字節的數據
\tpublic long skip(long n) throws IOException {...}
\t...
}

下面是OutputStream的定義,這裡列出一些方法。

public abstract class OutputStream implements Closeable, Flushable {
\t...
\t//將b寫入輸出流,一般是將b的8個低階位寫入輸出流,
\t//忽略b的24個高\t階位。
\tpublic abstract void write(int b) throws IOException;
\t//將b[off], b[off+l],…,b[off+len-l]寫入輸出流中
\tpublic void write(byte b[], int off, int len) throws IOException {...}
\t...
}

2 二進制文件讀寫

2.1 FileInputStream和FileOutputStream

FileInputStream從文件中讀取,FileOutputStream將數據寫入文件。這兩個類是用於讀寫二進制數據的。

其實文本文件和二進制文件並沒有本質的區別,所有文件在計算機上都是以二進制存儲的。只不過對文本文件的讀寫涉及到字符編碼和解碼的過程,而二進制文件不需要這個過程,當然對二進制文件加密與解密是另外一種情況。

讓我們看一下使用FileInputStream讀取一個文本文件會得到什麼結果,文件名為test文本文件的內容為:文件讀寫操作。

愉快地學Java語言:第十六章 輸入與輸出 第2講

下面舉一個例子,讀取圖片,然後寫入一個新文件中,和拷貝是一個效果。

愉快地學Java語言:第十六章 輸入與輸出 第2講

在相應的文件夾下確實可以看到操作結果。

愉快地學Java語言:第十六章 輸入與輸出 第2講

在講文本文件讀寫的時候,沒有給出追加寫操作,現在我們利用Fi1eOutputStream來實現文本文件追加寫。

愉快地學Java語言:第十六章 輸入與輸出 第2講

2.2 FilterInputStream和FilterOutputStream

為什麼要使用過濾器類呢?

FileInputStream和FileOutputStream只能讀寫二進制數據,他們無法讀取基本數據類型和字符串,這樣就需要使用一個類將字節流包裝一下,FileInputStream和FileOutputStream正式提供這種功能的包裝類。

那麼如何使用這兩個包裝類呢?

先看一下這兩個類的定義:

public class FilterInputStream extends InputStream {
\tprotected volatile InputStream in;
\tprotected FilterInputStream(InputStream in) { this.in = in;}
\t...
}
public class FilterOutputStream extends OutputStream{
\tprotected OutputStream out;
\tpublic FilterOutputStream(OutputStream out) { this.out = out; }
\t...
}

上面僅列出了構造方法,構造方法有一個參數,過濾器就是對這個參數類型的包裝。通過查看源碼發現,這兩個過濾器沒有實現操作基本類型的方法。如果要操作基本類型要使用DatalnputStream和DataOutputStream來操作基本數據類型。

2.3 DataInputStream和DataOutputStream

我們來看DataInputStream的定義,並且只分析它的實現,DataOutputStream和DataInputStream實現基本類似。

public class DataInputStream extends FilterInputStream implements DataInput {
\tpublic DataInputStream(InputStream in) {
super(in);
\t}
\tpublic final boolean readBoolean() throws IOException {
int ch = in.read();
if (ch < 0)
throw new EOFException();
return (ch != 0);

\t}
\tpublic final short readShort() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (short)((ch1 << 8) + (ch2 << 0));
}
\t...
}

DataInputStream的構造方法有一個形參,它就是輸入流。DataInputStream擴展了FilterInputStream,並在其構造方法中調用父類的構造方法,將輸入流保存到私有變量in。

DataInputStream還實現了DataInput接口,這個接口中定義了讀取基本數據類型的方法。

以readBoolean為例,調用in的read方法讀取一個比特數據,最後返回的是(ch != 0),也就是說不為零,那麼一定返回true。

readShort方法的實現過程與readBoolean類似,也是調用in的read方法讀取一個比特數據,最終將其轉換為short類型。

DataInputStream提供的方法用法比較簡單,如果有不清楚的地方,可以隨時查看API文檔或者源碼。

下面對DataOutputStream的幾個方法說明一下。

DataOutputStream的幾個方法都是將參數編碼後再執行操作的。需要注意的是writeBytes(String s)方法將字符串s中每個字符進行編碼,丟棄高字節,然後將低字節寫到輸出流。這裡採用的編碼為Unicode碼。

有一個問題就要特別注意了,writeBytes將高字節丟棄,所以此方法無法用於非ASCII編碼的字符;ASCII使用低字節(低八位)編碼,所以使用writeBytes處理沒有問題。

上述無法使用writeBytes操作的數據,可以使用writeChars(String s),因為這個方法不會將s的高字節丟棄。

writeUTF(String s)這個方法將字符串轉換為改進版UTF-8編碼,然後寫入文件。使用改進版UTF-8編碼,那麼系統就可以同時處理Unicode編碼和ASCII編碼的數據了。改進版UTF-8編碼方案根據實際情況將字符編碼為1字節、2字節、3字節。如果字符的編碼值小於或等於0x7F就將該字符編碼為一個宇節,如果字符的編碼值大於 0X7F而小於或等於0X7FF就將該字符編碼為兩個字節,如果該字符的編碼值大於0X7FF就將該字符編碼為三個字節。

如何判斷是用幾個字節編碼的呢?

如果第一位是0,那麼就是一個字節;如果頭幾位是110,那麼就是2字節;如果頭幾位是1110,那麼就是3字節。

標準UTF-8編碼和改進版UTF-8編碼有啥不同呢?

標準UTF-8也是變長編碼,但它使用1到4個字節。

改進版UTF-8中,null字符編碼成2個字節1100000010000000(十六進制為C080)而不是標準的1個字節00000000(十六進制為00),這樣做可以保證編碼後的字符串不會嵌入null字符,如果在類C語言中處理字符串,文本不會在第一個null字符時截斷(C字符串以'\\0'結尾)(見https://en.wikipedia.org/wiki/UTF-8和https://baike.baidu.com/item/UTF-8/481798?fr=aladdin)。

下面是一個例子,我們看到確實將字符串寫入件,但是還拋出了異常,這是為什麼呢?

愉快地學Java語言:第十六章 輸入與輸出 第2講

我們在while循環中沒有考慮到達流末尾的情形。如果已達到文件末尾還繼續讀取數據,那麼就會拋出異常EOFException。如何驗證已到達流末尾呢?遺憾的是沒有一個方便的方法檢測是否到達流的末尾,只能通過拋出異常來判斷是否達到流的末尾。

2.4 BufferedlnputStream 和 BufferedOutputStream

這兩個類也是讀寫文件用的,前面已經介紹那麼多讀寫文件的類了,為啥還要介紹著兩個類呢?

假如有兩種選擇,一種是多次操作寫入文件,另一種是先將數據緩存到內存中,等到積累到一定量時,在將當前內存中的數據寫入文件。你會選擇哪一種方式呢?

我想應該選擇第二中,因為第二種方式減少了操作文件的次數,因此性能更高。當然,你可能考慮到將數據先存到內存中會消耗大量內存資源,不過,不用擔心,如果控制得好不會出現內存不夠用的情況。

默認的緩衝區為8192字節。也可以通過構造方法設定緩衝區大小。

public BufferedInputStream(InputStream in, int size)

但令人不解的是,BufferedOutputStream的構造方法也將緩衝區默認為8192字節,不過沒有定義為常量,這不太合乎一般的規約。

public BufferedOutputStream(OutputStream out) {
this(out, 8192);
\t}

怎麼使用這兩個類呢?

BufferedInputStream父類是FilterInputStream,BufferedOutputStream父類是FilterOutputStream,所以和這兩個類的使用方法類似。

3 壓縮文件讀寫

使用ZipOutputStream和ZipInputStream可以實現文件的壓縮與解壓縮。

下面給出一個例子,將一個字符串寫入壓縮文件,然後在將其解壓到內存中,最後打印出來。

愉快地學Java語言:第十六章 輸入與輸出 第2講

4 對象序列化 ObjectInputStream和ObjectOutputStream

可以實現基本數據類型與字符串的輸人和輸出,還可以用於讀寫可序列化的對象。

下面是使用這兩個類的一個例子。

愉快地學Java語言:第十六章 輸入與輸出 第2講

我發現,上面這個例子無法實現追加寫操作,FileOutputStream有相應的構造方法:public FileOutputStream(String name, boolean append),將append設置為true就可以實現追加寫,但是這裡如果這麼設置會拋異常。

在說明對象序列化之前,先看看Serializable接口。

可寫入流中的對象稱之為可序列化對象,要想使一個對象可序列化,那麼它必須實現Serializable接口(它是一個標記接口)。

看下面的例子,這個例子中定義了一個類,它並沒有實現Serializable接口,按照我們的預想,確實拋出不可序列化異常。

愉快地學Java語言:第十六章 輸入與輸出 第2講

愉快地學Java語言:第十六章 輸入與輸出 第2講

如果一個對象包含不可序列化的數據域,那麼該如何處理這種情形呢?

在數據域上使用transient關鍵字, 這樣Java虛擬機將對象寫入流時就會忽略這些數據域。

重複序列化一個對象,那麼會向流中寫入多個對象嗎?

不會重複寫入,第一次寫入一個對象時,創建一個序列號,將對象的所有內容和序列號一起寫入對象流。重複序列化一個對象時,只存儲這個列號。

下面改造上個例子中的SerialObj,使其可序列化,然後運行程序,查看結果。

愉快地學Java語言:第十六章 輸入與輸出 第2講

愉快地學Java語言:第十六章 輸入與輸出 第2講

不過有一個特殊的類型,雖然它沒有實現Serializable接口,但是它也是可以序列化的,它就是數組。

愉快地學Java語言:第十六章 輸入與輸出 第2講

5 隨機訪問文件RandomAccessFile

RandomAccessFile允許從文件的任何位置讀寫。

為什麼要使用RandomAccessFile?

假設我要對文件的一部分修改,而我不想重寫整個文件,但是之前介紹的那些操作文件的類都沒法完成這項任務,恰好RandomAccessFile可以完成這項任務。

我們稱前面介紹的那些流類為順序流,它們只提供讀寫功能。

我們看一下構造方法:

public RandomAccessFile(String name, String mode)

構造方法有兩個參數,一個是文件名,一個是操作模式。

都有哪些操作模式呢?

“r”,“rw”,“rws”,“rwd”。r表示只讀模式,rw表示可讀可寫模式,如果文件不存在就創建它,rws表示可讀可寫模式,並且對文件內容或元數據的每次更新都同步寫入基礎存儲設備,rwd表示可讀可寫模式,並且對文件內容的每次更新都同步寫入基礎存儲設備。

看下面一個例子,反覆運行發現默認不是追加寫而是覆蓋寫。

愉快地學Java語言:第十六章 輸入與輸出 第2講

設置文件指針位置,讀取第二個長整型,因為seek的參數是以字節為單位的,長整型為64位,即8字節,所以設置ra.seek(8)。


分享到:


相關文章: