Java字符流文件讀寫

首先須要明確一點的是,字節流處理文件的時候是基於字節的,而字符流處理文件則是基於一個個字符爲基本單元的。java

但實際上,字符流操做的本質就是「字節流操做」+「編碼」兩個過程的封裝,你想是否是,不管你是寫一個字符到文件,你須要將字符編碼成二進制,而後以字節爲基本單位寫入文件,或是你讀一個字符到內存,你須要以字節爲基本單位讀出,而後轉碼成字符。數組

理解這一點很重要,這將決定你對字符流總體上的理解是怎樣的,下面咱們一塊兒看看相關 API 的設計。緩存

基類 Reader/Writer

在正式學習字符流基類以前,咱們須要知道 Java 中是如何表示一個字符的。學習

首先,Java 中的默認字符編碼爲:UTF-8,而咱們知道 UTF-8 編碼的字符使用 1 到 4 個字節進行存儲,越經常使用的字符使用越少的字節數。this

而 char 類型被定義爲兩個字節大小,也就是說,對於一般的字符來講,一個 char 便可存儲一個字符,但對於一些增補字符集來講,每每會使用兩個 char 來表示一個字符。編碼

Reader 做爲讀字符流的基類,它提供了最基本的字符讀取操做,咱們一塊兒看看。spa

先看看它的構造器:設計

protected Object lock;

protected Reader() {
    this.lock = this;
}

protected Reader(Object lock) {
    if (lock == null) {
        throw new NullPointerException();
    }
    this.lock = lock;
}

Reader 是一個抽象類,因此毋庸置疑的是,這些構造器是給子類調用的,用於初始化 lock 鎖對象,這一點咱們後續會詳細解釋。code

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,只是參數不一樣而已。咱們不妨看看這個方法的實現:

image

這是一個典型的靜態工廠模式,三個參數,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,理論上它倆就已經可以完成基本的字符流操做了,但也僅僅侷限於最基本的操做,而構造它們的實例所必需的就是「一個字節流實例」+「一種編碼格式」。

因此,字符流和字節流的關係也就如上述的等式同樣,你寫一個字符到磁盤文件中所必需的步驟就是,按照指定編碼格式編碼該字符,而後使用字節流將編碼後的字符二進制寫入文件中,讀操做是相反的。

相關文章
相關標籤/搜索