Java中的字節(jié)流文件讀取教程(一)
前言
上篇文章我們介紹了抽象化磁盤文件的 File 類型,它僅僅用于抽象化描述一個磁盤文件或目錄,卻不具備訪問和修改一個文件內容的能力。
Java 的 IO 流就是用于讀寫文件內容的一種設計,它能完成將磁盤文件內容輸出到內存或者是將內存數據輸出到磁盤文件的數據傳輸工作。
Java IO 流的設計并不是完美的,設計了大量的類,增加了我們對于 IO 流的理解,但無外乎為兩大類,一類是針對二進制文件的字節(jié)流,另一類是針對文本文件的字符流。而本篇我們就先來學習有關字節(jié)流的相關類型的原理以及使用場景等細節(jié),主要涉及的具體流類型如下:

基類字節(jié)流 Input/OutputStream
InputStream 和 OutputStream 分別作為讀字節(jié)流和寫字節(jié)流的基類,所有字節(jié)相關的流都必然繼承自他們中任意一個,而它們本身作為一個抽象類,也定義了最基本的讀寫操作,我們一起來看看:
以 InputStream 為例:
public abstract int read() throws IOException;
這是一個抽象的方法,并沒有提供默認實現,要求子類必須實現。而這個方法的作用就是為你返回當前文件的下一個字節(jié)。
當然,你也會發(fā)現這個方法的返回值是使用的整型類型「int」來接收的,為什么不用「byte」?
首先,read 方法返回的值一定是一個八位的二進制,而一個八位的二進制可以取值的值區(qū)間為:「0000 0000,1111 1111」,也就是范圍 [-128,127]。
read 方法同時又規(guī)定當讀取到文件的末尾,即文件沒有下一個字節(jié)供讀取了,將返回值 -1 。所以如果使用 byte 作為返回值類型,那么當方法返回一個 -1 ,我們該判定這是文件中數據內容,還是流的末尾呢?
而 int 類型占四個字節(jié),高位的三個字節(jié)全部為 0,我們只使用它的最低位字節(jié),當遇到流結尾標志時,返回四個字節(jié)表示的 -1(32 個 1),這就自然的和表示數據的值 -1(24 個 0 + 8 個 1)區(qū)別開來了。
接下來也是一個 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 源碼
}
這兩個方法本質上是一樣的,第一個方法是第二個方法的特殊形態(tài),它允許傳入一個字節(jié)數組,并要求程序將文件中讀到的字節(jié)從數組索引位置 0 開始填充,供填充數組長度個字節(jié)數。
而第二個方法更加寬泛一點,它允許你指定起始位置和字節(jié)總數。
InputStream 中還有其他幾個方法,基本都沒怎么具體實現,留待子類實現,我們簡單看看。
- public long skip(long n):跳過 n 個字節(jié),返回實際跳過的字節(jié)數
- public void close():關閉流并釋放對應的資源
- public synchronized void mark(int readlimit)
- public synchronized void reset()
- public boolean markSupported()
mark 方法會在當前流讀取位置打上一個標志,reset 方法即重置讀取指針到該標志處。
事實上,文件讀取是不可能重置回頭讀取的,而一般都是將標志位置到重置點之間所有的字節(jié)臨時保存了,當調用 reset 方法時,其實是從保存的臨時字節(jié)集合進行重復讀取,所以 readlimit 用于限制最大緩存容量。
而 markSupported 方法則用于確定當前流是否支持這種「回退式」讀取操作。
OutputStream 和 InputStream 是類似的,只不過一個是寫一個是讀,此處我們不再贅述了。
文件字節(jié)流 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);
}
這兩個構造器本質上也是一樣的,前者是后者的特殊形態(tài)。其實你別看后者的方法體一大堆代碼,大部分都只是在做安全校驗,核心的就是一個 open 方法,用于打開一個文件。
主要是這兩種構造器,如果文件不存在或者文件路徑和名稱不合法,都將拋出 FileNotFoundException 異常。
記得我們說過,基類 InputStream 中有一個抽象方法 read 要求所有子類進行實現,而 FileInputStream 使用本地方法進行了實現:
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
這個 read0 的具體實現我們暫時無從探究,但是你必須明確的是,這個 read 方法的作用,它用于返回流中下一個字節(jié),返回 -1 說明讀取到文件末尾,已無字節(jié)可讀。
除此之外,FileInputStream 中還有一些其他的讀取相關方法,但大多采用了本地方法進行了實現,此處我們簡單看看:
- public int read(byte b[]):讀取 b.length() 個長度的字節(jié)到數組中
- public int read(byte b[], int off, int len):讀取指定長度的字節(jié)數到數組中
- public native long skip(long n):跳過 n 的字節(jié)進行讀取
- 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 文件中的內容和實際讀出的字節(jié)數,但細心的同學就會發(fā)現了,你怎么就能保證 test 文件中內容不會超過 1024 個字節(jié)呢?
為了能夠完整的讀出文件中的內容,一種解決辦法是:將 buffer 定義的足夠大,以期望盡可能的能夠存儲下文件中的所有內容。
這種方法顯然是不可取的,因為我們根本不可能實現知道待讀文件的實際大小,一味的創(chuàng)建過大的字節(jié)數組其本身也是一種很差勁的方案。
第二種方式就是使用我們的動態(tài)字節(jié)數組流,它可以動態(tài)調整內部字節(jié)數組的大小,保證適當的容量,這一點我們后文中將詳細介紹。
關于 FileOutputStream,還需要強調一點的是它的構造器,其中有以下兩個構造器:
public FileOutputStream(String name, boolean append) public FileOutputStream(File file, boolean append)
參數 append 指明了,此流的寫入操作是覆蓋還是追加,true 表示追加,false 表示覆蓋。
字節(jié)數組流 ByteArrayInput/OutputStream
所謂的「字節(jié)數組流」就是圍繞一個字節(jié)數組運作的流,它并不像其他流一樣,針對文件進行流的讀寫操作。
字節(jié)數組流雖然并不是基于文件的流,但卻依然是一個很重要的流,因為它內部封裝的字節(jié)數組并不是固定的,而是動態(tài)可擴容的,往往基于某些場景下,非常合適。
ByteArrayInputStream 是讀字節(jié)數組流,可以通過以下構造函數被實例化:
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 內部的一個字節(jié)數組,ByteArrayInputStream 的所有讀操作都是圍繞著它進行的。
所以,實例化一個 ByteArrayInputStream 對象的時候,至少傳入一個目標字節(jié)數組的。
pos 屬性用于記錄當前流讀取的位置,count 記錄了目標字節(jié)數組最后一個有效字節(jié)索引的后一個位置。
理解了這一點,有關它各種的 read 方法就不難了:
//讀取下一個字節(jié)
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
//讀取 len 個字節(jié)放到字節(jié)數組 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 是基于字節(jié)數組的,所有重復讀取操作的實現就比較容易了,基于索引實現就可以了。
ByteArrayOutputStream 是寫的字節(jié)數組流,很多實現還是很有自己的特點的,我們一起來看看。
首先,這兩個屬性是必須的:
protected byte buf[]; //這里的 count 表示的是 buf 中有效字節(jié)個個數 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];
}
構造器的核心任務是,初始化內部的字節(jié)數組 buf,允許你傳入 size 顯式限制初始化的字節(jié)數組大小,否則將默認長度 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 方法的調用,目的是為了確保當前流內的字節(jié)數組能容納本次寫操作。
而這個方法也很有意思了,如果計算后發(fā)現,內部的 buf 不能夠支持本次寫操作,則會調用 grow 方法做一次擴容。擴容的原理和 ArrayList 的實現是類似的,擴大為原來的兩倍容量。
除此之外,ByteArrayOutputStream 還有一個 writeTo 方法:
public synchronized void writeTo(OutputStream out) throws IOException {
out.write(buf, 0, count);
}
將我們內部封裝的字節(jié)數組寫到某個輸出流當中。
剩余的一些方法也很常用:
- public synchronized byte toByteArray()[]:返回內部封裝的字節(jié)數組
- public synchronized int size():返回 buf 的有效字節(jié)數
- public synchronized String toString():返回該數組對應的字符串形式
注意到,這兩個流雖然被稱作「流」,但是它們本質上并沒有像真正的流一樣去分配一些資源,所以我們無需調用它的 close 方法,調了也沒用(人家官方說了,has no effect)。
測試的案例就不放出來了,等會我會上傳本篇文章用到的所有代碼案例,大家自行選擇下載即可。
為了控制篇幅,余下流的學習,放在下篇文章。
文章中的所有代碼、圖片、文件都云存儲在我的 GitHub 上:
(https://github.com/SingleYam/overview_java)
大家也可以選擇通過本地下載。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關文章
詳解spring boot集成ehcache 2.x 用于hibernate二級緩存
本篇文章主要介紹了詳解spring boot集成ehcache 2.x 用于hibernate二級緩存,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
Spring?Boot集成validation實現參數校驗功能
Bean?Validation?是一個運行時的數據驗證框架,在驗證之后驗證的錯誤信息會被馬上返回,這篇文章主要介紹了Spring?Boot集成validation實現參數校驗功能,需要的朋友可以參考下2024-05-05
PipedWriter和PipedReader源碼分析_動力節(jié)點Java學院整理
這篇文章主要介紹了PipedWriter和PipedReader源碼分析_動力節(jié)點Java學院整理,需要的朋友可以參考下2017-05-05
mybatisplus?selectOne查詢,有數據,但返回為null問題
這篇文章主要介紹了mybatisplus?selectOne查詢,有數據,但返回為null問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11

