javaIO流之缓冲流
目錄
- 簡介
- 1、字節緩沖流
- 1.1構造方法
- 1.2緩沖流的高效
- 1.3為什么字節緩沖流會這么快?
- 1.4byte & 0xFF
- 2、字符緩沖流
- 2.1構造方法
- 2.2字符緩沖流特有方法
- 3、練習
簡介
Java 的緩沖流是對字節流和字符流的一種封裝,通過在內存中開辟緩沖區來提高 I/O 操作的效率。Java 通過 BufferedInputStream 和 BufferedOutputStream 來實現字節流的緩沖,通過 BufferedReader 和 BufferedWriter 來實現字符流的緩沖。
緩沖流的工作原理是將數據先寫入緩沖區中,當緩沖區滿時再一次性寫入文件或輸出流,或者當緩沖區為空時一次性從文件或輸入流中讀取一定量的數據。這樣可以減少系統的 I/O 操作次數,提高系統的 I/O 效率,從而提高程序的運行效率。
1、字節緩沖流
BufferedInputStream 和 BufferedOutputStream 屬于字節緩沖流,強化了字節流 InputStream 和 OutputStream,關于字節流。
1.1構造方法
- BufferedInputStream(InputStream in) :創建一個新的緩沖輸入流,注意參數類型為InputStream。
- BufferedOutputStream(OutputStream out): 創建一個新的緩沖輸出流,注意參數類型為OutputStream。
代碼示例如下:
1.2緩沖流的高效
我們通過復制一個 370M+ 的大文件,來測試緩沖流的效率。為了做對比,我們先用基本流來實現一下,代碼如下:
// 記錄開始時間 long start = System.currentTimeMillis(); // 創建流對象 try (FileInputStream fis = new FileInputStream("py.mp4");//exe文件夠大FileOutputStream fos = new FileOutputStream("copyPy.mp4")){// 讀寫數據int b;while ((b = fis.read()) != -1) {fos.write(b);} } // 記錄結束時間 long end = System.currentTimeMillis(); System.out.println("普通流復制時間:"+(end - start)+" 毫秒");不好意思,我本機比較菜,10 分鐘還在復制中。切換到緩沖流試一下,代碼如下:
// 記錄開始時間 long start = System.currentTimeMillis(); // 創建流對象 try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.mp4"));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.mp4"));){// 讀寫數據int b;while ((b = bis.read()) != -1) {bos.write(b);} } // 記錄結束時間 long end = System.currentTimeMillis(); System.out.println("緩沖流復制時間:"+(end - start)+" 毫秒");只需要 8016 毫秒,如何更快呢?
可以換數組的方式來讀寫,這個我們前面也有講到,代碼如下:
// 記錄開始時間 long start = System.currentTimeMillis(); // 創建流對象 try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("py.mp4"));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copyPy.mp4"));){// 讀寫數據int len;byte[] bytes = new byte[8*1024];while ((len = bis.read(bytes)) != -1) {bos.write(bytes, 0 , len);} } // 記錄結束時間 long end = System.currentTimeMillis(); System.out.println("緩沖流使用數組復制時間:"+(end - start)+" 毫秒");這下就更快了,只需要 521 毫秒。
1.3為什么字節緩沖流會這么快?
傳統的 Java IO 是阻塞模式的,它的工作狀態就是“讀/寫,等待,讀/寫,等待。。。。。。”
字節緩沖流解決的就是這個問題:一次多讀點多寫點,減少讀寫的頻率,用空間換時間。
- 減少系統調用次數:在使用字節緩沖流時,數據不是立即寫入磁盤或輸出流,而是先寫入緩沖區,當緩沖區滿時再一次性寫入磁盤或輸出流。這樣可以減少系統調用的次數,從而提高 I/O 操作的效率。
- 減少磁盤讀寫次數:在使用字節緩沖流時,當需要讀取數據時,緩沖流會先從緩沖區中讀取數據,如果緩沖區中沒有足夠的數據,則會一次性從磁盤或輸入流中讀取一定量的數據。同樣地,當需要寫入數據時,緩沖流會先將數據寫入緩沖區,如果緩沖區滿了,則會一次性將緩沖區中的數據寫入磁盤或輸出流。這樣可以減少磁盤讀寫的次數,從而提高 I/O 操作的效率。
- 提高數據傳輸效率:在使用字節緩沖流時,由于數據是以塊的形式進行傳輸,因此可以減少數據傳輸的次數,從而提高數據傳輸的效率。
我們來看 BufferedInputStream 的 read 方法:
public synchronized int read() throws IOException {if (pos >= count) { // 如果當前位置已經到達緩沖區末尾fill(); // 填充緩沖區if (pos >= count) // 如果填充后仍然到達緩沖區末尾,說明已經讀取完畢return -1; // 返回 -1 表示已經讀取完畢}return getBufIfOpen()[pos++] & 0xff; // 返回當前位置的字節,并將位置加 1 }我們來看 BufferedInputStream 的 read 方法:
public synchronized int read() throws IOException {if (pos >= count) { // 如果當前位置已經到達緩沖區末尾fill(); // 填充緩沖區if (pos >= count) // 如果填充后仍然到達緩沖區末尾,說明已經讀取完畢return -1; // 返回 -1 表示已經讀取完畢}return getBufIfOpen()[pos++] & 0xff; // 返回當前位置的字節,并將位置加 1 }這段代碼主要有兩部分:
- fill():該方法會將緩沖 buf 填滿。
- getBufIfOpen()[pos++] & 0xff:返回當前讀取位置 pos 處的字節(getBufIfOpen()返回的是 buffer 數組,是 byte 類型),并將其與 0xff 進行位與運算。這里的目的是將讀取到的字節 b 當做無符號的字節處理,因為 Java 的 byte 類型是有符號的,而將 b 與 0xff 進行位與運算,就可以將其轉換為無符號的字節,其范圍為 0 到 255。
再來看 FileInputStream 的 read 方法:
在這段代碼中,read0() 方法是一個本地方法,它的實現是由底層操作系統提供的,并不是 Java 語言實現的。在不同的操作系統上,read0() 方法的實現可能會有所不同,但是它們的功能都是相同的,都是用于讀取一個字節。
再來看一下 BufferedOutputStream 的 write(byte b[], int off, int len) 方法:
public synchronized void write(byte b[], int off, int len) throws IOException {if (len >= buf.length) { // 如果寫入的字節數大于等于緩沖區長度/* 如果請求的長度超過了輸出緩沖區的大小,先刷新緩沖區,然后直接將數據寫入。這樣可以避免緩沖流級聯時的問題。*/flushBuffer(); // 先刷新緩沖區out.write(b, off, len); // 直接將數據寫入輸出流return;}if (len > buf.length - count) { // 如果寫入的字節數大于空余空間flushBuffer(); // 先刷新緩沖區}System.arraycopy(b, off, buf, count, len); // 將數據拷貝到緩沖區中count += len; // 更新計數器 }-
首先,該方法會檢查寫入的字節數是否大于等于緩沖區長度,如果是,則先將緩沖區中的數據刷新到磁盤中,然后直接將數據寫入輸出流。這樣做是為了避免緩沖流級聯時的問題,即緩沖區的大小不足以容納寫入的數據時,可能會引發級聯刷新,導致效率降低。
-
級聯問題(Cascade Problem)是指在一組緩沖流(Buffered Stream)中,由于緩沖區的大小不足以容納要寫入的數據,導致數據被分割成多個部分,并分別寫入到不同的緩沖區中,最終需要逐個刷新緩沖區,從而導致性能下降的問題。
-
其次,如果寫入的字節數小于緩沖區長度,則檢查緩沖區中剩余的空間是否足夠容納要寫入的字節數,如果不夠,則先將緩沖區中的數據刷新到磁盤中。然后,使用 System.arraycopy() 方法將要寫入的數據拷貝到緩沖區中,并更新計數器 count。
-
最后,如果寫入的字節數小于緩沖區長度且緩沖區中還有剩余空間,則直接將要寫入的數據拷貝到緩沖區中,并更新計數器 count。
也就是說,只有當 buf 寫滿了,才會 flush,將數據刷到磁盤,默認一次刷 8192 個字節。
public BufferedOutputStream(OutputStream out) {this(out, 8192); }如果 buf 沒有寫滿,會繼續寫 buf。
對比一下 FileOutputStream 的 write 方法,同樣是本地方法,一次只能寫入一個字節。
當把 BufferedOutputStream 和 BufferedInputStream 配合起來使用后,就減少了大量的讀寫次數,尤其是 byte[] bytes = new byte[8*1024],就相當于緩沖區的空間有 8 個 1024 字節,那讀寫效率就會大大提高。
1.4byte & 0xFF
byte 類型通常被用于存儲二進制數據,例如讀取和寫入文件、網絡傳輸等場景。在這些場景下,byte 類型的變量可以用來存儲數據流中的每個字節,從而進行讀取和寫入操作。
byte 類型是有符號的,即其取值范圍為 -128 到 127。如果我們希望得到的是一個無符號的 byte 值,就需要使用 byte & 0xFF 來進行轉換。
這是因為 0xFF 是一個無符號的整數,它的二進制表示為 11111111。當一個 byte 類型的值與 0xFF 進行位與運算時,會將 byte 類型的值轉換為一個無符號的整數,其范圍為 0 到 255。
0xff 是一個十六進制的數,相當于二進制的 11111111,& 運算符的意思是:如果兩個操作數的對應位為 1,則輸出 1,否則為 0;由于 0xff 有 8 個 1,單個 byte 轉成 int 其實就是將 byte 和 int 類型的 255 進行(&)與運算。
例如,如果我們有一個 byte 類型的變量 b,其值為 -1,那么 b & 0xFF 的結果就是 255。這樣就可以將一個有符號的 byte 類型的值轉換為一個無符號的整數。
& 運算是一種二進制數據的計算方式, 兩個操作位都為1,結果才為1,否則結果為0. 在上面的 getBufIfOpen()[pos++] & 0xff 計算過程中, byte 有 8bit, OXFF 是16進制的255, 表示的是 int 類型, int 有 32bit.
如果 getBufIfOpen()[pos++] 為 -118, 那么其原碼表示為
00000000 00000000 00000000 10001010反碼為
11111111 11111111 11111111 11110101補碼為
11111111 11111111 11111111 111101100XFF 表示16進制的數據255, 原碼, 反碼, 補碼都是一樣的, 其二進制數據為
00000000 00000000 00000000 111111110XFF 和 -118 進行&運算后結果為
00000000 00000000 00000000 11110110還原為原碼后為
00000000 00000000 00000000 10001010其表示的 int 值為 138,可見將 byte 類型的 -118 與 0XFF 進行與運算后值由 -118 變成了 int 類型的 138,其中低8位和byte的-118完全一致。
順帶聊一下 原碼、反碼和補碼。
①、原碼
原碼就是符號位加上真值的絕對值,即用第一位表示符號,其余位表示值。比如如果是8位二進制:
第一位是符號位。因為第一位是符號位,所以8位二進制數的取值范圍就是:
[1111 1111 , 0111 1111]即
[-127 , 127]
②、反碼
反碼的表示方法是:
正數的反碼是其本身
負數的反碼是在其原碼的基礎上,符號位不變,其余各個位取反。
例如:
可見如果一個反碼表示的是負數,人腦無法直觀的看出來它的數值。通常要將其轉換成原碼再計算。
③、補碼
補碼的表示方法是:
正數的補碼就是其本身
負數的補碼是在其原碼的基礎上,符號位不變,其余各位取反,最后+1。(即在反碼的基礎上+1)
對于負數,補碼表示方式也是人腦無法直觀看出其數值的。通常也需要轉換成原碼在計算其數值。
從上面可以看到:
對于正數:原碼,反碼,補碼都是一樣的
對于負數:原碼,反碼,補碼都是不一樣的
2、字符緩沖流
BufferedReader 類繼承自 Reader 類,提供了一些便捷的方法,例如 readLine() 方法可以一次讀取一行數據,而不是一個字符一個字符地讀取。
BufferedWriter 類繼承自 Writer 類,提供了一些便捷的方法,例如 newLine() 方法可以寫入一個系統特定的行分隔符。
2.1構造方法
BufferedReader(Reader in) :創建一個新的緩沖輸入流,注意參數類型為Reader。
BufferedWriter(Writer out): 創建一個新的緩沖輸出流,注意參數類型為Writer。
代碼示例如下:
2.2字符緩沖流特有方法
字符緩沖流的基本方法與普通字符流調用方式一致,這里不再贅述,我們來看字符緩沖流特有的方法。
BufferedReader:String readLine(): 讀一行數據,讀取到最后返回 null
BufferedWriter:newLine(): 換行,由系統定義換行符。
來看 readLine()方法的代碼示例:
再來看 newLine() 方法的代碼示例:
// 創建流對象 BfferedWriter bw = new BufferedWriter(new FileWriter("b.txt")); // 寫出數據 bw.write("追"); // 寫出換行 bw.newLine(); bw.write("風"); bw.newLine(); bw.write("少"); bw.newLine(); bw.write("年"); bw.newLine(); // 釋放資源 bw.close();3、練習
來欣賞一下我寫的這篇詩:
6.岑夫子,丹丘生,將進酒,杯莫停。 1.君不見黃河之水天上來,奔流到海不復回。 8.鐘鼓饌玉不足貴,但愿長醉不愿醒。 3.人生得意須盡歡,莫使金樽空對月。 5.烹羊宰牛且為樂,會須一飲三百杯。 2.君不見高堂明鏡悲白發,朝如青絲暮成雪。 7.與君歌一曲,請君為我傾耳聽。 4.天生我材必有用,千金散盡還復來。欣賞完了沒?
估計你也看出來了,這是李白寫的《將進酒》,不是我寫的。😝
不過,順序是亂的,還好,我都編了號。那如何才能按照正確的順序來呢?
來看代碼實現:
來看輸出結果:
1.君不見黃河之水天上來,奔流到海不復回。 2.君不見高堂明鏡悲白發,朝如青絲暮成雪。 3.人生得意須盡歡,莫使金樽空對月。 4.天生我材必有用,千金散盡還復來。 5.烹羊宰牛且為樂,會須一飲三百杯。 6.岑夫子,丹丘生,將進酒,杯莫停。 7.與君歌一曲,請君為我傾耳聽。 8.鐘鼓饌玉不足貴,但愿長醉不愿醒。相關文章鏈接:
javaIO之各種流的分類與實際應用
javaIO流之文件流
javaIO流之字節流
javaIO流之字符流
javaIO流之緩沖流
javaIO流之轉換流
javaIO流之序列流
吾之榮耀,離別已久。如果覺得有用,點個贊吧~~~~
總結
以上是生活随笔為你收集整理的javaIO流之缓冲流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: V-补提交卡
- 下一篇: 很自由的PDF在线转换器