Java 的字節流文件讀取(一)

上篇文章咱們介紹了抽象化磁盤文件的 File 類型,它僅僅用於抽象化描述一個磁盤文件或目錄,卻不具有訪問和修改一個文件內容的能力。java

Java 的 IO 流就是用於讀寫文件內容的一種設計,它能完成將磁盤文件內容輸出到內存或者是將內存數據輸出到磁盤文件的數據傳輸工做。git

Java IO 流的設計並非完美的,設計了大量的類,增長了咱們對於 IO 流的理解,但無外乎爲兩大類,一類是針對二進制文件的字節流,另外一類是針對文本文件的字符流。而本篇咱們就先來學習有關字節流的相關類型的原理以及使用場景等細節,主要涉及的具體流類型以下:github

image

基類字節流 Input/OutputStream

InputStream 和 OutputStream 分別做爲讀字節流和寫字節流的基類,全部字節相關的流都必然繼承自他們中任意一個,而它們自己做爲一個抽象類,也定義了最基本的讀寫操做,咱們一塊兒來看看:數組

以 InputStream 爲例:緩存

public abstract int read() throws IOException;
複製代碼

這是一個抽象的方法,並無提供默認實現,要求子類必須實現。而這個方法的做用就是爲你返回當前文件的下一個字節安全

固然,你也會發現這個方法的返回值是使用的整型類型「int」來接收的,爲何不用「byte」?bash

首先,read 方法返回的值必定是一個八位的二進制,而一個八位的二進制能夠取值的值區間爲:「0000 0000,1111 1111」,也就是範圍 [-128,127]。微信

read 方法同時又規定當讀取到文件的末尾,即文件沒有下一個字節供讀取了,將返回值 -1 。因此若是使用 byte 做爲返回值類型,那麼當方法返回一個 -1 ,咱們該斷定這是文件中數據內容,仍是流的末尾呢?app

而 int 類型佔四個字節,高位的三個字節所有爲 0,咱們只使用它的最低位字節,當遇到流結尾標誌時,返回四個字節表示的 -1(32 個 1),這就天然的和表示數據的值 -1(24 個 0 + 8 個 1)區別開來了。函數

接下來也是一個 read 方法,可是 InputStream 提供默認實現:

public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
}

public int read(byte b[], int off, int len) throws IOException{
    //爲了避免使篇幅過長,方法體你們可自行查看 jdk 源碼
}
複製代碼

這兩個方法本質上是同樣的,第一個方法是第二個方法的特殊形態,它容許傳入一個字節數組,並要求程序將文件中讀到的字節從數組索引位置 0 開始填充,供填充數組長度個字節數。

而第二個方法更加寬泛一點,它容許你指定起始位置和字節總數。

InputStream 中還有其餘幾個方法,基本都沒怎麼具體實現,留待子類實現,咱們簡單看看。

  • public long skip(long n):跳過 n 個字節,返回實際跳過的字節數
  • public void close():關閉流並釋放對應的資源
  • public synchronized void mark(int readlimit)
  • public synchronized void reset()
  • public boolean markSupported()

mark 方法會在當前流讀取位置打上一個標誌,reset 方法即重置讀取指針到該標誌處。

事實上,文件讀取是不可能重置回頭讀取的,而通常都是將標誌位置到重置點之間全部的字節臨時保存了,當調用 reset 方法時,實際上是從保存的臨時字節集合進行重複讀取,因此 readlimit 用於限制最大緩存容量。

而 markSupported 方法則用於肯定當前流是否支持這種「回退式」讀取操做。

OutputStream 和 InputStream 是相似的,只不過一個是寫一個是讀,此處咱們再也不贅述了。

文件字節流 FileInput/OutputStream

咱們依然着重點於 FileInputStream,而 FileOutputStream 是相似的。

首先 FileInputStream 有如下幾種構造器實例化一個對象:

public FileInputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null);
}
複製代碼
public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    fd = new FileDescriptor();
    fd.attach(this);
    path = name;
    open(name);
}
複製代碼

這兩個構造器本質上也是同樣的,前者是後者的特殊形態。其實你別看後者的方法體一大堆代碼,大部分都只是在作安全校驗,核心的就是一個 open 方法,用於打開一個文件。

主要是這兩種構造器,若是文件不存在或者文件路徑和名稱不合法,都將拋出 FileNotFoundException 異常。

記得咱們說過,基類 InputStream 中有一個抽象方法 read 要求全部子類進行實現,而 FileInputStream 使用本地方法進行了實現:

public int read() throws IOException {
    return read0();
}

private native int read0() throws IOException;
複製代碼

這個 read0 的具體實現咱們暫時無從探究,可是你必須明確的是,這個 read 方法的做用,它用於返回流中下一個字節,返回 -1 說明讀取到文件末尾,已無字節可讀。

除此以外,FileInputStream 中還有一些其餘的讀取相關方法,但大多采用了本地方法進行了實現,此處咱們簡單看看:

  • public int read(byte b[]):讀取 b.length() 個長度的字節到數組中
  • public int read(byte b[], int off, int len):讀取指定長度的字節數到數組中
  • public native long skip(long n):跳過 n 的字節進行讀取
  • public void close():釋放流資源

FileInputStream 的內部方法基本就這麼些,還有一些高級的複雜的,咱們暫時用不到,之後再進行學習,下面咱們簡單看一個文件讀取的例子:

public static void main(String[] args) throws IOException {
    FileInputStream input = new FileInputStream("C:\\Users\\yanga\\Desktop\\test.txt");
    byte[] buffer = new byte[1024];
    int len = input.read(buffer);
    String str = new String(buffer);
    System.out.println(str);
    System.out.println(len);
    input.close();
}
複製代碼

輸出結果很簡單,會打印出咱們 test 文件中的內容和實際讀出的字節數,但細心的同窗就會發現了,你怎麼就能保證 test 文件中內容不會超過 1024 個字節呢?

爲了可以完整的讀出文件中的內容,一種解決辦法是:將 buffer 定義的足夠大,以指望儘量的可以存儲下文件中的全部內容。

這種方法顯然是不可取的,由於咱們根本不可能實現知道待讀文件的實際大小,一味的建立過大的字節數組其自己也是一種不好勁的方案。

第二種方式就是使用咱們的動態字節數組流,它能夠動態調整內部字節數組的大小,保證適當的容量,這一點咱們後文中將詳細介紹。

關於 FileOutputStream,還須要強調一點的是它的構造器,其中有如下兩個構造器:

public FileOutputStream(String name, boolean append)

public FileOutputStream(File file, boolean append)
複製代碼

參數 append 指明瞭,此流的寫入操做是覆蓋仍是追加,true 表示追加,false 表示覆蓋。

字節數組流 ByteArrayInput/OutputStream

所謂的「字節數組流」就是圍繞一個字節數組運做的流,它並不像其餘流同樣,針對文件進行流的讀寫操做。

字節數組流雖然並非基於文件的流,但卻依然是一個很重要的流,由於它內部封裝的字節數組並非固定的,而是動態可擴容的,每每基於某些場景下,很是合適。

ByteArrayInputStream 是讀字節數組流,能夠經過如下構造函數被實例化:

protected byte buf[];
protected int pos;
protected int count;

public ByteArrayInputStream(byte buf[]) {
    this.buf = buf;
    this.pos = 0;
    this.count = buf.length;
}

public ByteArrayInputStream(byte buf[], int offset, int length)
複製代碼

buf 就是被封裝在 ByteArrayInputStream 內部的一個字節數組,ByteArrayInputStream 的全部讀操做都是圍繞着它進行的。

因此,實例化一個 ByteArrayInputStream 對象的時候,至少傳入一個目標字節數組的。

pos 屬性用於記錄當前流讀取的位置,count 記錄了目標字節數組最後一個有效字節索引的後一個位置。

理解了這一點,有關它各類的 read 方法就不難了:

//讀取下一個字節
public synchronized int read() {
    return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
複製代碼
//讀取 len 個字節放到字節數組 b 中
public synchronized int read(byte b[], int off, int len){
    //一樣的,方法體較長,你們查看本身的 jdk
}
複製代碼

除此以外,ByteArrayInputStream 還很是簡單的實現了「重複讀取」操做。

public void mark(int readAheadLimit) {
    mark = pos;
}

public synchronized void reset() {
    pos = mark;
}
複製代碼

由於 ByteArrayInputStream 是基於字節數組的,全部重複讀取操做的實現就比較容易了,基於索引實現就能夠了。

ByteArrayOutputStream 是寫的字節數組流,不少實現仍是頗有本身的特色的,咱們一塊兒來看看。

首先,這兩個屬性是必須的:

protected byte buf[];

//這裏的 count 表示的是 buf 中有效字節個個數
protected int count;
複製代碼

構造器:

public ByteArrayOutputStream() {
    this(32);
}
    
public ByteArrayOutputStream(int size) {
    if (size < 0) {
        throw new IllegalArgumentException("Negative initial size: "+ size);
    }
    buf = new byte[size];
}
複製代碼

構造器的核心任務是,初始化內部的字節數組 buf,容許你傳入 size 顯式限制初始化的字節數組大小,不然將默認長度 32 。

從外部向 ByteArrayOutputStream 寫內容:

public synchronized void write(int b) {
    ensureCapacity(count + 1);
    buf[count] = (byte) b;
    count += 1;
}

public synchronized void write(byte b[], int off, int len){
    if ((off < 0) || (off > b.length) || (len < 0) ||
            ((off + len) - b.length > 0)) {
            throw new IndexOutOfBoundsException();
        }
        ensureCapacity(count + len);
        System.arraycopy(b, off, buf, count, len);
        count += len;
}
複製代碼

看到沒有,全部寫操做的第一步都是 ensureCapacity 方法的調用,目的是爲了確保當前流內的字節數組能容納本次寫操做。

而這個方法也頗有意思了,若是計算後發現,內部的 buf 不可以支持本次寫操做,則會調用 grow 方法作一次擴容。擴容的原理和 ArrayList 的實現是相似的,擴大爲原來的兩倍容量。

除此以外,ByteArrayOutputStream 還有一個 writeTo 方法:

public synchronized void writeTo(OutputStream out) throws IOException {
    out.write(buf, 0, count);
}
複製代碼

將咱們內部封裝的字節數組寫到某個輸出流當中。

剩餘的一些方法也很經常使用:

  • public synchronized byte toByteArray()[]:返回內部封裝的字節數組
  • public synchronized int size():返回 buf 的有效字節數
  • public synchronized String toString():返回該數組對應的字符串形式

注意到,這兩個流雖然被稱做「流」,可是它們本質上並無像真正的流同樣去分配一些資源,因此咱們無需調用它的 close 方法,調了也沒用(人家官方說了,has no effect)。

測試的案例就不放出來了,等會我會上傳本篇文章用到的全部代碼案例,你們自行選擇下載便可。

爲了控制篇幅,餘下流的學習,放在下篇文章。


文章中的全部代碼、圖片、文件都雲存儲在個人 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公衆號:撲在代碼上的高爾基,全部文章都將同步在公衆號上。

image
相關文章
相關標籤/搜索