上篇文章,我們介紹了 Java 的檔案位元組流框架中的相關內容,而我們本篇文章將著重於檔案字元流的相關內容。
首先需要明確一點的是,位元組流處理檔案的時候是基於位元組的,而字元流處理檔案則是基於一個個字元為基本單元的。
但實際上,字元流操作的本質就是「位元組流操作」+「編碼」兩個過程的封裝,你想是不是,無論你是寫一個字元到檔案,你需要將字元編碼成二進位制,然後以位元組為基本單位寫入檔案,或是你讀一個字元到記憶體,你需要以位元組為基本單位讀出,然後轉碼成字元。
理解這一點很重要,這將決定你對字元流整體上的理解是怎樣的,下面我們一起看看相關 API 的設計。
基類 Reader/Writer
在正式學習字元流基類之前,我們需要知道 Java 中是如何表示一個字元的。
首先,Java 中的預設字元編碼為:UTF-8,而我們知道 UTF-8 編碼的字元使用 1 到 4 個位元組進行儲存,越常用的字元使用越少的位元組數。
而 char 型別被定義為兩個位元組大小,也就是說,對於通常的字元來說,一個 char 即可儲存一個字元,但對於一些增補字符集來說,往往會使用兩個 char 來表示一個字元。
Reader 作為讀字元流的基類,它提供了最基本的字元讀取操作,我們一起看看。
先看看它的構造器:
protected Object lock;
protected Reader() {
this.lock = this;
}
protected Reader(Object lock) {
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock;
}
複製程式碼
Reader 是一個抽象類,所以毋庸置疑的是,這些構造器是給子類呼叫的,用於初始化 lock 鎖物件,這一點我們後續會詳細解釋。
public int read() throws IOException {
char cb[] = new char[1];
if (read(cb, 0, 1) == -1)
return -1;
else
return cb[0];
}
public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}
abstract public int read(char cbuf[], int off, int len)
複製程式碼
基本的讀字元操作都在這了,第一個方法用於讀取一個字元出來,如果已經讀到了檔案末尾,將返回 -1,同樣的以 int 作為返回值型別接收,為什麼不用 char?原因是一樣的,都是由於 -1 這個值的解釋不確定性。
第二個方法和第三個方法是類似的,從檔案中讀取指定長度的字元放置到目標陣列當中。第三個方法是抽象方法,需要子類自行實現,而第二個方法卻又是基於它的。
還有一些方法也是類似的:
- public long skip(long n):跳過 n 個字元
- public boolean ready():下一個字元是否可讀
- public boolean markSupported():見 reset 方法
- public void mark(int readAheadLimit):見 reset 方法
- public void reset():用於實現重複讀操作
- abstract public void close():關閉流
這些個方法其實都見名知意,並且和我們的 InputStream 大體上都差不多,都沒有什麼核心的實現,這裡不再贅述,你大致知道它內部有些個什麼東西即可。
Writer 是寫的字元流,它用於將一個或多個字元寫入到檔案中,當然具體的 write 方法依然是一個抽象的方法,待子類來實現,所以我們這裡亦不再贅述了。
介面卡 InpustStramReader/OutputStreamWriter
介面卡字元流繼承自基類 Reader 或 Writer,它們算是字元流體系中非常重要的成員了。主要的作用就是,將一個位元組流轉換成一個字元流,我們先以讀介面卡為例。
首先就是它最核心的成員:
private final StreamDecoder sd;
複製程式碼
StreamDecoder 是一個解碼器,用於將位元組的各種操作轉換成字元的相應操作,關於它我們會在後續的介紹中不間斷的提到它,這裡不做統一的解釋。
然後就是構造器:
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
public InputStreamReader(InputStream in, String charsetName)
throws UnsupportedEncodingException
{
super(in);
if (charsetName == null)
throw new NullPointerException("charsetName");
sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
}
複製程式碼
這兩個構造器的目的都是為了初始化這個解碼器,都呼叫的方法 forInputStreamReader,只是引數不同而已。我們不妨看看這個方法的實現:
這是一個典型的靜態工廠模式,三個引數,var0 和 var1 沒什麼好說的,分別代表的是位元組流例項和介面卡例項。
而引數 var2 其實代表的是一種字元編碼的名稱,如果為 null,那麼將使用系統預設的字元編碼:UTF-8 。
最終我們能夠得到一個解碼器例項。
接著介紹的所有方法幾乎都是依賴的這個解碼器而實現的。
public String getEncoding() {
return sd.getEncoding();
}
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length){
return sd.read(cbuf, offset, length);
}
public void close() throws IOException {
sd.close();
}
複製程式碼
解碼器中相關的方法的實現程式碼還是相對複雜的,這裡我們不做深入的研究,但大體上的實現思路就是:「位元組流讀取 + 解碼」的過程。
當然了,OutputStreamWriter 中必然也存在一個相反的 StreamEncoder 例項用於編碼字元。
除了這一點外,其餘的操作並沒有什麼不同,或是通過字元陣列向檔案中寫入,或是通過字串向檔案中寫入,又或是通過 int 的低 16 位向檔案中寫入。
檔案字元流 FileReader/Writer
檔案的字元流可以說非常簡單了,除了構造器,就不存在任何其他方法了,完全依賴檔案位元組流。
我們以 FileReader 為例,
FileReader 繼承自 InputStreamReader,有且僅有以下三個構造器:
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
public FileReader(File file) throws FileNotFoundException {
super(new FileInputStream(file));
}
public FileReader(FileDescriptor fd) {
super(new FileInputStream(fd));
}
複製程式碼
理論上來說,所有的字元流都應當以我們的介面卡為基類,因為只有它提供了字元到位元組之間的轉換,無論你是寫或是讀都離不開它。
而我們的 FileReader 並沒有擴充套件任何一個自己的方法,父類 InputStreamReader 中預實現的字元操作方法對他來說已經足夠,只需要傳入一個對應的位元組流例項即可。
FileWriter 也是一樣的,這裡不再贅述了。
字元陣列流 CharArrayReader/Writer
字元陣列和位元組陣列流是類似的,都是用於解決那種不確定檔案大小,而需要讀取其中大量內容的情況。
由於它們內部提供動態擴容機制,所以既可以完全容納目標檔案,也可以控制陣列大小,不至於分配過大記憶體而浪費了大量記憶體空間。
先以 CharArrayReader 為例
protected char buf[];
public CharArrayReader(char buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
public CharArrayReader(char buf[], int offset, int length){
//....
}
複製程式碼
構造器核心任務就是初始化一個字元陣列到內部的 buf 屬性中,以後所有對該字元陣列流例項的讀操作都基於 buf 這個字元陣列。
關於 CharArrayReader 的其他方法以及 CharArrayWriter,這裡不再贅述了,和上篇的位元組陣列流基本類似。
除此之外,這裡還涉及一個 StringReader 和 StringWriter,其實本質上和字元陣列流是一樣的,畢竟 String 的本質就是 char 陣列。
緩衝陣列流 BufferedReader/Writer
同樣的,BufferedReader/Writer 作為一種緩衝流,也是裝飾者流,用於提供緩衝功能。大體上類似於我們的位元組緩衝流,這裡我們簡單介紹下。
private Reader in;
private char cb[];
private static int defaultCharBufferSize = 8192;
public BufferedReader(Reader in, int sz){..}
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
複製程式碼
cb 是一個字元陣列,用於快取從檔案流中讀取出來的部分字元,你可以在構造器中初始化這個陣列的長度,否則將使用預設值 8192 。
public int read() throws IOException {..}
public int read(char cbuf[], int off, int len){...}
複製程式碼
關於 read,它依賴成員屬性 in 的讀方法,而 in 作為一個 Reader 型別,內部往往又依賴的某個 InputStream 例項的讀方法。
所以說,幾乎所有的字元流都離不開某個位元組流例項。
關於 BufferedWriter,這裡也不再贅述了,大體上都是類似的,只不過一個是讀一個是寫而已,都圍繞著內部的字元陣列進行。
標準列印輸出流
列印輸出流主要有兩種,PrintStream 和 PrintWriter,前者是位元組流,後者是字元流。
這兩個流算是對各自類別下的流做了一個整合,內部封裝有豐富的方法,但實現也稍顯複雜,我們先來看這個 PrintStream 位元組流:
主要的構造器有這麼幾個:
- public PrintStream(OutputStream out)
- public PrintStream(OutputStream out, boolean autoFlush)
- public PrintStream(OutputStream out, boolean autoFlush, String encoding)
- public PrintStream(String fileName)
顯然,簡單的構造器會依賴複雜的構造器,這已經算是 jdk 設計「老套路」了。區別於其他位元組流的一點是,PrintStream 提供了一個標誌 autoFlush,用於指定是否自動重新整理快取。
接著就是 PrintStream 的寫方法:
- public void write(int b)
- public void write(byte buf[], int off, int len)
除此之外,PrintStream 還封裝了大量的 print 的方法,寫入不同型別的內容到檔案中,例如:
- public void print(boolean b)
- public void print(char c)
- public void print(int i)
- public void print(long l)
- public void print(float f)
- 等等
當然,這些方法並不會真正的將數值的二進位制寫入檔案,而只是將它們所對應的字串寫入檔案,例如:
print(123);
複製程式碼
最終寫入檔案的不是 123 所對應的二進位制表述,而僅僅是 123 這個字串,這就是列印流。
PrintStream 使用的緩衝字元流實現所有的列印操作,如果指明瞭自動重新整理,則遇到換行符號「\n」會自動重新整理緩衝區。
所以說,PrintStream 整合了位元組流和字元流中所有的輸出方法,其中 write 方法是用於位元組流操作,print 方法用於字元流操作,這一點需要明確。
至於 PrintWriter,它就是全字元流,完全針對字元進行操作,無論是 write 方法也好,print 方法也好,都是字元流操作。
總結一下,我們花了三篇文章講解了 Java 中的位元組流和字元流操作,位元組流基於位元組完成磁碟和記憶體之間的資料傳輸,最典型的就是檔案字元流,它的實現都是本地方法。有了基本的位元組傳輸能力後,我們還能夠通過緩衝來提高效率。
而字元流的最基本實現就是,InputStreamReader 和 OutputStreamWriter,理論上它倆就已經能夠完成基本的字元流操作了,但也僅僅侷限於最基本的操作,而構造它們的例項所必需的就是「一個位元組流例項」+「一種編碼格式」。
所以,字元流和位元組流的關係也就如上述的等式一樣,你寫一個字元到磁碟檔案中所必需的步驟就是,按照指定編碼格式編碼該字元,然後使用位元組流將編碼後的字元二進位制寫入檔案中,讀操作是相反的。
文章中的所有程式碼、圖片、檔案都雲端儲存在我的 GitHub 上:
(https://github.com/SingleYam/overview_java)
歡迎關注微信公眾號:撲在程式碼上的高爾基,所有文章都將同步在公眾號上。