在第一節中,我們瞭解了 File 類對於文件的操作。File對象封裝了文件或者路徑屬性,但是不包括從/向文件讀/寫數據的方法,本節主要講解Java的字節流。
I/O流解決的問題:設備與設備之間數據傳輸的問題。
Java流的分類:
- 從流的方向上可以分爲輸入流和輸出流,應用程序通過輸入流讀取數據,通過輸出流發送數據。
- 按照數據傳輸單位可以劃分爲字節流和字符流。
字節流讀取的是文件中的二進制數據,不會對數據進行任何處理。
字符流讀取的也是文件中的二進制數據,但是會對這些數據進行處理(解碼)
Java I/O類的設計是應用繼承一個很好的例子,使用裝飾者模式進行設計。關於I/O中裝飾者模式的內容請關注後續章節。
1、字節流
1.1、FileInputStream
構造方法:
- FileInputStream(File file) 由file對象創建一個FileInputStream
FileInputStream(String filename) 由文件名創建一個FileInputStream
- int read() 從輸入流中讀取一個字節的數據,返回讀取的數據,如果達到輸入流末尾,則返回-1。
- int read(byte[] buffer) 從輸入流中讀取b.length個字節的數據到字節數組buffer中,返回實際讀取的字節數。如果到達輸入流末尾,則返回-1。
- int read(byte[] buffer,int off,int len) 從輸入流中讀取多個字節的數據,並存儲在buffer[off]~buffer[off+len-1]中,返回實際讀取的字節數。如果到達輸入流末尾,則返回-1。
最常見的FileInputStream讀取文件的方式:
private static void readFile() {
FileInputStream fis = null;
File f = new File("./src/a.txt");
try {
fis = new FileInputStream(f);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
byte[] buf = new byte[4]; // 一次性讀取buf.length個字節的數據
int length = 0;
try {
while ((length = fis.read(buf)) != -1) { //將讀取的字節數據裝入byte數組中,返回的是讀取的字節數;讀至文件末尾返回 -1
/**
* read方法將數據讀進byte數組採用覆蓋的方式,而並非清空重新賦值的方式。
* 故,在獲取輸出時要指定byte數組中有效的元素。
*/
System.out.print(new String(buf, 0, length));
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
int read(byte[] b) 方法將數據讀進byte數組中,採用覆蓋的方式,並不是清空後重新添加的方式。所以從byte數組中獲取輸出數據時需要指定有效元素。
使用流讀取文件後務必要關閉資源 fis.close() 。
1.2、FileOutputStream
構造方法:
- FileOutputStream(File file) 由file對象創建一個FileOutputStream
- FileOutputStream(String filename) 由文件名創建一個FileOutputStream
- FileOutputStream(File file,boolean append) append爲真,數據追加至文件末尾
FileOutputStream(File file,boolean append) append爲真,數據追加至文件末尾
- void write(int b) 將指定字節寫入輸出流
- void write(byte[] b) 將字節數組b中的所有字節寫入輸出流
- void write(byte[] b,int off,int len) 將b[off]~b[off+len-1]寫入輸出流
- void close() 關閉輸出流並釋放與其有關的系統資源
- void flush() 刷新輸出流並強制寫出所有緩衝的輸出字節
常見的使用FileOutputStream寫文件的方法:
private static void writeFile() throws IOException {
FileOutputStream fos = null;
File file = new File("./src/b.txt");
fos = new FileOutputStream(file,true);
String data = "xiaopeng";
fos.write(data.getBytes()); //寫入字節數組
fos.close();
}
write(int b)方法傳入int類型的變量(4個字節),但是該方法只會將最低位的那個字節寫入文件,其餘的三個字節會被捨棄。二進制數:00000000-00000000-00000001-01100001(353),將353寫入,但是文件中卻只有低八位(97)。
private static void writeTest() throws IOException {
FileOutputStream fos = null;
FileInputStream fis = null;
File file = new File("./src/b.txt");
fos = new FileOutputStream(file);
fis = new FileInputStream(file);
fos.write(353); //00000000-00000000-00000001-01100001
byte[] bytes = new byte[4];
fis.read(bytes);
System.out.println(Arrays.toString(bytes));
fis.close();
fos.close();
}
輸出:
[97, 0, 0, 0]
1.3、BufferedInputStream/BufferedOutputStream
Java提供了專門的緩衝字節流來提高文件讀取的效率。BufferedInputStream/BufferedOutputStream類內部有一個緩衝字節數組(默認8192B),用來提高處理效率。
private static void bufferedReadTest() throws IOException {
BufferedInputStream bufferedInputStream = null;
//BufferedInputStream本身不具備讀取文件的功能,需要藉助FileInputStream進行讀取文件
bufferedInputStream = new BufferedInputStream(new FileInputStream(new File("./src/a.txt")));
int content = 0;
while((content = bufferedInputStream.read()) != -1){
System.out.print((char)content);
}
bufferedInputStream.close(); //內部其實調用 fileInputStream.close()
}
BufferedInputStream不具備讀取文件的功能,需要藉助FileInputStream進行文件讀取,查看BufferedInputStream的read方法可以發現:
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
- count:代表BufferedInputStream內部維護的字節數組大小
- pos:代表當前讀取字節數組中那個下標的數據
若pos<=count,那麼調用 fill() 方法(其實是FileInputStream的int read(byte[] b,int off,int len))將數據讀入字節數組;否則直接從字節數組中讀取一個字節的數據並返回。
BufferedInputStream的 close() 方法其實內部調用傳入的FileInputStream的 close() 方法,因此關閉資源時只需要調用BufferedInputStream即可。
public void close() throws IOException {
byte[] buffer;
while ( (buffer = buf) != null) {
if (bufUpdater.compareAndSet(this, buffer, null)) {
InputStream input = in;
in = null;
if (input != null)
input.close(); //FileInputStream.close()
return;
}
}
}
關於BufferedOutputStream將數據寫入磁盤的實現方式:
- 調用父類FilterOutputStream的 close() 方法關閉資源:
public void close() throws IOException {
try (OutputStream ostream = out) {
flush();
}
}
- 手工調用BufferOutputStream的 flush() 方法將數據寫入:
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
- 查看BufferedOutputStream的 write() 方法可以發現,當緩衝字節數組容量滿時,也會調用將數據寫入:
public synchronized void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer(); //實際調用FilterOutputStream.write()寫入文件
}
buf[count++] = (byte)b;
}