【Java核心技术卷】I/O详析
文章目錄
- 概述
- Java io基本概念
- 關于流
- 流的分類
- Java io框架
- 一、以字節為單位的輸出流的框架圖
- (1)框架圖圖示
- (2)OutputStream詳解
- (3)OutputStream子類
- (4)引申:打印流
- 二、以字節為單位的輸入流的框架圖
- (1)框架圖圖示
- (2)InputStream詳解
- (3)InputStream子類
- (4)引申:緩沖流(含字節輸出流的內容)
- (5)引申:數據流(含字節輸出流的內容)
- 三、以字符為單位的輸入流和輸出流的框架圖
- (1)框架圖圖示
- (2)框架詳解
- (3)引申:過濾器流
- Java io中的設計模式
- 適配器模式
- 裝飾者模式
- 復盤
概述
java的IO系統的設計是為了實現 “文件、控制臺、網絡設備” 這些IO設置的通信,它建立在流之上,輸入流讀取數據,輸出流寫出數據,不同的流類,會讀/寫某個特定的數據源。
數據源(Data Source)顧名思義,數據的來源
在展開整個面之前有必要弄清楚三個概念: 比特、字節和字符
- Bit最小的二進制單位 ,是計算機的操作部分,取值0或者1,也是計算機網絡的物理層最小的傳輸單位。
- Byte是計算機操作數據的最小單位由8位bit組成 取值(-128-127)
- Char是用戶的可讀寫的最小單位,在Java里面由16位bit組成 取值(0-65535)
Java將IO進行了封裝,所以我們看到的都是API,我們能做到的就是理解熟悉這些API,靈活運用即可。如果想要深入理解IO,建議從C語言入手,因為C語言是唯一一個能將IO講明白的高級語言。
Java的IO系統是Java SE學習的重要一環,Java的文件操作 , Java網絡編程基于此。它對你理解Tomcat的架構等也是必備的。
Java io基本概念
關于流
流是一串連續不斷的數據的集合,你可以把它理解為水管里的水流,在水管的源頭有水的供應,在另一端則是娟娟的水流,數據寫入程序可以理解為供水,數據段會按先后順序形成一個長的數據流。
對數據讀取程序來說,不知道流在寫入時的分段情況,每次可以讀取其中的任意長度的數據,但只能先讀取前面的數據后,再讀取后面的數據。
流的分類
Java IO中包含字節流、字符流。按照流向還分為輸入流,輸出流
- 字節流:操作byte類型數據,主要操作類是OutputStream、InputStream的子類;不用緩沖區,直接對文件本身操作。
- 字符流:操作字符類型數據,主要操作類是Reader、Writer的子類;使用緩沖區緩沖字符,不關閉流就不會輸出任何內容。
輸入流和輸出流的區分很簡單:輸入流是把數據寫入存儲介質的。輸出流是從存儲介質中把數據讀取出來
Java io框架
一、以字節為單位的輸出流的框架圖
(1)框架圖圖示
圖示基于Jdk1.8
(2)OutputStream詳解
OutputStream是抽象類,它是所有字節輸出流的類的超類,它與InputStream構成了IO類層次結構的基礎。
它展示了五種方法,子類針對這五個方法進行拓展, 不管是哪種介質,大都使用同樣的這5種方法
?
值得注意的是 wirte(int b)
它雖然接收0~255的整數,但是實際上會寫出一個無符號字節,因為Java是沒有無符號字節整型數據的,所以這里用int來代替。
?
下面來看這兩個方法,可用于多個字節的處理,相比上面一次處理一個字節效率要更高。
參數
- b:數據讀入的數組
- off 第一個讀入的字節 應該被放置的位置在b的偏移量
- len 讀入字節的最大數量
這個還是比較好理解的,就不多說了。
?
當結束一個流的操作的時候 會調用close方法將其關閉,釋放與這個流相關的所有資源,如文件句柄或端口。
文件句柄
在文件I/O中,要從一個文件讀取數據,應用程序首先要調用操作系統函數并傳送文件名,并選一個到該文件的路徑來打開文件。該函數取回一個順序號,即文件句柄(file handle),該文件句柄對于打開的文件是唯一的識別依據。要從文件中讀取一塊數據,應用程序需要調用函數ReadFile,并將文件句柄在內存中的地址和要拷貝的字節數傳送給操作系統。當完成任務后,再通過調用系統函數來關閉該文件。
其實關于結束一個流,在Java6及之前有一個很經典的 完成清理的釋放模式
OutputStream out = null;try {out = new FileOutputStream("/temp/data.txt");}catch (IOException ex){System.err.println(ex.getMessage());}finally {if(out != null){try {out.close();}catch (IOException ex){//忽略}}}這個不僅可以用于流,還可以用于socket,通道,JDBC的連接
在Java7之后出現一個語法糖 — 帶資源的 try構造。寫法類似于這種:
try(OutputStream out = new FileOutputStream("/temp/data.txt")){//處理輸出流}catch (IOException ex){System.err.println(ex.getMessage());}由于這個很重要,我們深究一下:
AutoCloseable接口對JDK7新添加的帶資源的try語句提供了支持,這種try語句可以自動執行資源關閉過程。
只有實現了AutoCloseable接口的類的對象才可以由帶資源的try語句進行管理。AutoCloseable接口只定義了close()方法:
Closeable接口也定義了close()方法。實現了Closeable接口的類的對象可以被關閉。
但是注意!!! 從JDK7開始,Closeable擴展了AutoCloseable。因此,在JDK7中,所有實現了Closeable接口的類也都實現了AutoCloseable接口。
關于帶資源的try語句的3個關鍵點:
此外請記住,所聲明資源的作用域被限制在帶資源的try語句中。
我們看一下Java編譯器為我們解析語法糖tryWithResource 做了哪些事情:
原本的代碼
public class TryWith {public static void main(String[] args) {try (BufferedReader br = new BufferedReader(new FileReader("c:\\Test.jad"))) {String line;while ((line = br.readLine()) != null) {System.out.println(line);}} catch (IOException e) {e.printStackTrace();}} }反編譯之后
public class TryWith {public TryWith(){}public static void main(String args[]){try{BufferedReader br = new BufferedReader(new FileReader("c:Test.jad"));String line;try{while((line = br.readLine()) != null) System.out.println(line);}catch(Throwable throwable){try{br.close();}catch(Throwable throwable1){throwable.addSuppressed(throwable1);}throw throwable;}br.close();}catch(IOException e){e.printStackTrace();}} }?
最后一個方法是flush()
意思是沖刷輸出流,也就是將所有緩沖的數據發送到目的地。這個在 子類 緩沖流BufferedOutputStream 中體現最為明顯
(3)OutputStream子類
關于子類的實現這里更多地知識點一下,如果一一詳細介紹,這篇博文實在太長了。(所以把重要的放在下一篇寫)
- ByteArrayOutputStream 是字節數組輸出流。寫入ByteArrayOutputStream的數據被寫入一個 byte 數組。緩沖區會隨著數據的不斷寫入而自動增長。可使用 toByteArray() 和 toString() 獲取數據。
- PipedOutputStream 是管道輸出流,它和PipedInputStream一起使用,能實現多線程間的管道通信。
- FilterOutputStream 是過濾輸出流。它是DataOutputStream,BufferedOutputStream和PrintStream等的超類。
- DataOutputStream 是數據輸出流。它是用來裝飾其它輸出流,它“允許應用程序以與機器無關方式向底層寫入基本 Java 數據類型”。(簡單來說就是以二進制格式寫出所有的基本Java類型)
- BufferedOutputStream 是緩沖輸出流。它的作用是為另一個輸出流添加緩沖功能。
- PrintStream 是打印輸出流。它是用來裝飾其它輸出流,能為其他輸出流添加了功能,使它們能夠方便地打印各種數據值表示形式。
- FileOutputStream 是文件輸出流。它通常用于向文件進行寫入操作。
- ObjectOutputStream 是對象輸出流。它和ObjectInputStream一起,用來提供對“基本數據或對象”的持久存儲。
(4)引申:打印流
平時我們在控制臺打印輸出,是調用 print 方法和 println 方法完成的,這兩個方法都來自于java.io.PrintStream 類,該類能夠方便地打印各種數據類型的值,是一種便捷的輸出方式。
打印流PrintStream 為其他輸出流添加了功能,使它們能夠方便地打印各種數據值表示形式。
PrintStream特點:
API信息
Print Stream(File file):輸出的目的地是一個文件PrintStream(Outputstream out):輸出的目的地是一個字節輸出流PrintStream(string fileName):輸出的目的地是一個文件路徑 PrintStream extends OutputStream繼承自父類的成員方法: public void close():關閉此輸出流并釋放與此流相關聯的任何系統資源。 public void flush():刷新此輸出流并強制任何緩沖的輸出字節被寫出。 public void write(byte[] b):將b.length字節從指定的字節數組寫入此輸出流。 public void write(byte[]b,int off,int len):從指定的字節數組寫入len字節,從偏移量off開始輸出到此輸出流。 public abstract void write(int b):將指定的字節輸出流。注意:
如果使用繼承自父類的write方法寫數據,那么查看數據的時候會查詢編碼表97->a
如果使用自己特有的方法print/println方法寫數據,寫的數據原樣輸出97->97
結果:
PrintStream可以改變輸出語句的目的地(打印流的流向)輸出語句,默認在控制臺輸出
使用System.setout方法改變輸出語句的目的地改為參數中傳遞的打印流的目的地
static void setout(PrintStream out) 重新分配標準*輸出流。
結果:
PringtStream是存在一些問題的:
二、以字節為單位的輸入流的框架圖
(1)框架圖圖示
圖示版本Jdk1.8
(2)InputStream詳解
它提供了寫入數據的基本方法,圈起來的是需要重點關注的:
?
沒有參數的read()方法: 從輸入流的數據源中讀入一個字節數據轉為0~255的int返回, 流的結束通過返回-1來表示
這個應與OutputStream的write(int b)方法放在一起理解
.read()方法會等待并阻塞其后任何代碼的執行,直到有1個字節可供讀取.輸入輸出可能很慢,所以如果程序還要做其他事情,盡量把I/O放在單獨的線程中.
read讀取一個無符號字節,注意這里要求進行類型轉換.可以通過下面的方法 將手里的有符號字節轉換成無符號字節
int i = b>=0?b:256+b;?
另外還有兩個read() 返回-1的時候 都表示流的結束
不結束的時候 返回值為實際讀取到的字節數
?
如果不想等待所需的全部字節都立即可用,可以使用available()方法來確定不阻塞的情況下有多少字節可以讀取.它會返回可以讀取的最少字節數(事實上還能夠讀取更多的字節),但至少讀取available()建議的字節數
?
在少數的情況下,可能會希望跳過數據不進行讀取.skip()方法會完成這項任務.
與讀取文件相比,在網絡連接中它的用處不大.網絡連接是順序的,一般會很慢.所以與跳過這些數據(不讀取)相比,讀取并不會耗費太長的時間.
文件是隨機訪問的,所以要跳過數據,可以簡單地實現為重新指定文件指針的位置,而不需要處理要跳過的各字節.
?
InputStream類還有三個不太常用的方法,就是沒圈起來的
mark()方法和reset()方法常被合起來稱之為標記與重置
為了重新讀取數據,需要用mark()方法標記流的當前位置.在以后的某個時刻,可以用reset()方法把流重置到之前標記的位置.
接下來的讀取操作會煩會從標記位置開始的數據.
不過,不能隨心所欲地向前重置任意遠的位置.從標記處讀取和重置的字節數由mark()的readAheadlimit參數確定.重置太遠會有IO異常.
一個很重要的問題是,一個流在任何時刻都只能有一個標記.標記的第二個位置會清除第一個標記.
?
markSupported()方法是否返回true.如果返回true,這個流就支持標記和重置
Java.io僅有兩個始終支持標記的輸入流類是 BufferedInputStream和ByteArrayInputStream 而其他輸入流(如TelnetInputStream)需要串聯到緩沖的輸入流才支持標記.
TelnetInputStream在文檔中沒有展示,被隱藏在了sun包下,這個類我們是直接接觸不到的
關于串聯其實是設計模式的思想,后面會單獨詳解
(3)InputStream子類
- ByteArrayInputStream 是字節數組輸入流。它包含一個內部緩沖區,該緩沖區包含從流中讀取的字節;通俗點說,它的內部緩沖區就是一個字節數組,而ByteArrayInputStream本質就是通過字節數組來實現的。
- PipedInputStream 是管道輸入流,它和PipedOutputStream一起使用,能實現多線程間的管道通信。
- FilterInputStream 是過濾輸入流。它是DataInputStream和BufferedInputStream的超類。
- DataInputStream 是數據輸入流。它是用來裝飾其它輸入流,它“允許應用程序以與機器無關方式從底層輸入流中讀取基本 Java 數據類型”。
- BufferedInputStream 是緩沖輸入流。它的作用是為另一個輸入流添加緩沖功能。
- FileInputStream 是文件輸入流。它通常用于對文件進行讀取操作。
- ObjectInputStream 是對象輸入流。它和ObjectOutputStream一起,用來提供對“基本數據或對象”的持久存儲。
(4)引申:緩沖流(含字節輸出流的內容)
BufferedOutputStream
BufferedOutputStream類將寫入的數據存儲在緩沖區中(一個名字是buf的保護字節數組字段),直到緩沖區滿或者刷新輸出流。然后它將數據一次性寫入底層輸出流。
這里需要好好說一下flush方法了
關于緩沖流,如果輸出流有一個1024字節的緩沖區,未滿之前,它會一直等待數據的到達,如果是網絡發送數據的話,因為緩沖區的等待,會產生死鎖,flush()方法可以強迫緩沖的流發送數據。
有一個細節就是當close緩沖流的時候會自動調用flush方法
具體方法如下:
BufferedInputStream
值得一提的是它的構造方法:
第一個參數是底層流,可以從中讀取未緩沖的數據。或者向其寫入數據。
如果給出第二個參數,它會指定緩沖區的字節數,否則輸入流的緩沖區大小設置為2048字節.輸出流的緩沖區大小設置為512字節.緩沖區的理想大小取決于所緩沖的流是何種類型.
其他方法與超類類似:
(5)引申:數據流(含字節輸出流的內容)
DataInputSteam和DataOutputSteam類提供了一些方法,可以用二進制格式讀/寫Java的基本數據類型和字符串.
DataOutputSteam 寫入的每一種數據 DatainputSteam都能夠讀取.
所有的數據都以big-endian格式寫入.
小端格式和大端格式(Little-Endian&Big-Endian)屬于計算機組成原理的內容
不同的CPU有不同的字節序類型,這些字節序是指整數在內存中保存的順序。
最常見的有兩種:
1. Little-endian:將低序字節存儲在起始地址(低位編址)
2. Big-endian:將高序字節存儲在起始地址(高位編址)
三、以字符為單位的輸入流和輸出流的框架圖
(1)框架圖圖示
-
CharArrayReader 是字符數組輸入流。它用于讀取字符數組r。操作的數據是以字符為單位!
-
PipedReader 是字符類型的管道輸入流。它和PipedWriter一起是可以通過管道進行線程間的通訊。在使用管道通信時,必須將PipedWriter和PipedReader配套使用。
-
FilterReader 是字符類型的過濾輸入流。
-
BufferedReader 是字符緩沖輸入流。它的作用是為另一個輸入流添加緩沖功能。
-
InputStreamReader 是字節轉字符的輸入流。它是字節流通向字符流的橋梁:它使用指定的 charset 讀取字節并將其解碼為字符。
-
FileReader 是字符類型的文件輸入流。它通常用于對文件進行讀取操作。
-
CharArrayWriter 是字符數組輸出流。它用于讀取字符數組。操作的數據是以字符為單位!
-
PipedWriter 是字符類型的管道輸出流。它和PipedReader一起是可以通過管道進行線程間的通訊。在使用管道通信時,必須將PipedWriter和PipedWriter配套使用。
-
FilterWriter 是字符類型的過濾輸出流。
-
BufferedWriter 是字符緩沖輸出流。它的作用是為另一個輸出流添加緩沖功能。
-
OutputStreamWriter 是字節轉字符的輸出流。它是字節流通向字符流的橋梁:它使用指定的 charset 將字節轉換為字符并寫入。
-
FileWriter 是字符類型的文件輸出流。它通常用于對文件進行讀取操作。
-
PrintWriter 是字符類型的打印輸出流。它是用來裝飾其它輸出流,能為其他輸出流添加了功能,使它們能夠方便地打印各種數據值表示形式。
(2)框架詳解
對于Unicode文本,可以使用抽象類Reader和Writer的子類。
小知識點: Java內置字符集是Unicode的UTF-16編碼
很多人都將Reader及其子類稱之為 閱讀器, 將Writer及其子類稱之為書寫器,個人認為這是一個不錯的做法, 后面會探討Java io的設計模式,兩個簡稱會用到。
Reader類的基本方法
read()方法將一個Unicode字符(實際上是UTF-16碼點)作為一個 int返回,可以是0~65535之間的一個值,等遇到流結束的時候返回-1
read(char[] cbuf) read(char[] cbuf,int off,int len)第一個方法嘗試使用字符填充數組cbuf,并返回實際讀取到的字節數,等遇到流結束的時候返回-1
第二個方法嘗試將len個字符讀入cbuf的數組中(從off開始 長度為len)它會返回實際讀取到的字節數,如果遇到流結束的時候返回-1
skip(Long n)等其他方法與InputStream基本上類似。
Writer類的基本方法
有了前面的基礎,這些api也很好理解的,你會發現Writer中有一個append方法
它和writer方法最大區別是,append方法中的參數可以為null,而writer中的參數不能為null
Reader和Writer最重要的具體子類是InputStreamReader和OutputStreamWirter, 以這個為例我們看一下它的實現過程,其他類類似
關于InputStreamReader:
它包含一個底層輸入流,可以從數據源中讀取原始字節。它根據指定的編碼方式,將這些字節轉換成Unicode字符。
關于OutputStreamWriter
它從運行的程序中接收Unicode字符,然后使用指定的編碼方式將這些字符轉換成字節,然后字節寫入底層輸入流中。
有一道Java io的面試題是這樣問的:字節流和字符流之間如何相互轉換?
其實就是我們說的這兩個具體子類,回答可以是下面的說法:
整個IO包實際上分為字節流和字符流,但是除了這兩個流之外,還存在一組字節流-字符流的轉換類。
OutputStreamWriter是Writer的子類,將輸出的字符流變為字節流,即將一個字符流的輸出對象變為字節流輸出對象。
InputStreamReader是Reader的子類,將輸入的字節流變為字符流,即將一個字節流的輸入對象變為字符流的輸入對象。
(3)引申:過濾器流
這部分內容是我學習《Java網絡編程》第四版摘錄下來的
Java提供了很多的過濾器類,可以附加到原始流中,在原始字節和各種格式之間進行轉換.。
過濾器有兩種版本:過濾器流及閱讀器和書寫器。
過濾器流仍然主要將原始數據作為字節處理,通過壓縮數據或解釋為二進制數字。閱讀器和書寫器處理各種編碼文本的特殊情況,如UTF-8和ISO 8859-1。它以鏈的形式進行組織。鏈中的每個環節都接收前一個過濾器或流的數據,并把數據傳遞給鏈中的下一個環節。在這個示例中,從本地網絡接口接收到一個加密的文本文件,在這里本地代碼將這個文件表示TelnetInputStream。通過一個BufferedInputtStream緩沖這個數據來加速整個過程。由一個CipherInputStream將數據解密。再由一GZIPInputStream解壓解密后的數據。一個InputStream將解壓后的數據轉換為Unicode文本。最后,文本由應用程序進行處理。
這其中體現了一些設計模式
Java io中的設計模式
適配器模式
關于適配器模式,這里回顧一下:
定義: 將一個類的接口轉換成客戶期望的另一個接口
說的通俗點就是 筆記本電腦的電源就是個適配器吧,不管它外部電壓是多少,只要經過了適配器就能轉變成我電腦需要的伏數
作用: 使原本接口不兼容的類可以一起工作
類型: 結構型
演示:
(1)類適配器模式
定義一個目標接口
實現接口的類
public class ConcreteTarget implements Target{@Overridepublic void request() {System.out.println("ConcreteTarget目標方法");} }被適配者
public class Adaptee {public void adapteeRequest(){System.out.println("被適配者的方法");}}關鍵 : 繼承被適配者且繼承目標接口
//繼承被適配者 public class Adapter extends Adaptee implements Target{@Overridepublic void request() {super.adapteeRequest();} }類圖:
測試:
結果:
(2)下面來看一下對象適配器模式
和類適配器模式相比 只有一處發生了變化
Adapter 類 之前還繼承了Adaptee,現在沒有繼承
//繼承被適配者 public class Adapter implements Target{private Adaptee adaptee = new Adaptee();@Overridepublic void request() {adaptee.adapteeRequest();} }類圖:
前面已經說了字節流和字符流之間的相互轉換靠的是OutputStreamWriter和InputStreamReader,其實它們充當了適配器,如果不相信可以翻源碼。
這里舉個轉換的例子:
裝飾者模式
同樣回顧一下:
定義:在不改變原有對象的基礎之上,將功能附加到對象上
作用: 提供了比繼承更有彈性的替代方案(擴展原有對象功能)
類型: 結構型
使用場景:
演示
不滿足裝飾者模式的情況
以買煎餅為例
煎餅 加一個雞蛋
public class BattercakeWithEgg extends Battercake{@Overrideprotected String getDesc() {return super.getDesc()+"加一個雞蛋";}@Overrideprotected int cost() {return super.cost()+ 1;} }煎餅加一個雞蛋 加一個烤腸
public class BattercakeWithEggSausage extends BattercakeWithEgg{@Overrideprotected String getDesc() {return super.getDesc()+"加一根香腸";}@Overridepublic int cost() {return super.cost()+2;} }測試:
public class Test {public static void main(String[] args) {Battercake battercake = new Battercake();System.out.println(battercake.getDesc()+ " 銷售價格為:"+battercake.cost());BattercakeWithEgg battercakeWithEgg = new BattercakeWithEgg();System.out.println(battercakeWithEgg.getDesc()+ " 銷售價格為:"+battercakeWithEgg.cost());BattercakeWithEggSausage battercakeWithEggSausage = new BattercakeWithEggSausage();System.out.println(battercakeWithEgg.getDesc()+ " 銷售價格為:"+battercakeWithEgg.cost());} }我們看一下它的UML類圖 很容易發現 單純的繼承關系
如果一個煎餅加兩個雞蛋 或者別的其他的呢 容易引起"類爆炸"
所以為了解決這個問題 引入裝飾著模式,下面讓我們看一下滿足裝飾者模式的寫法
這里被裝飾的是煎餅 裝飾者是雞蛋和香腸
創建煎餅的抽象類
public abstract class ABattercake {protected abstract String getDesc();protected abstract int cost(); }創建煎餅
public class Battercake extends ABattercake{protected String getDesc(){return "煎餅";}protected int cost(){return 5;} }創建裝飾著的抽象類
public class AbstractDecorator extends ABattercake{private ABattercake aBattercake;public AbstractDecorator(ABattercake aBattercake) { //注入煎餅this.aBattercake = aBattercake;}@Overrideprotected String getDesc() {return this.getDesc();}@Overrideprotected int cost() {return this.cost();} }雞蛋的裝飾者
public class EggDecorator extends AbstractDecorator {public EggDecorator(ABattercake aBattercake) {super(aBattercake);}@Overrideprotected String getDesc() {return super.getDesc()+"加一個雞蛋";}@Overrideprotected int cost() {return super.cost()+1;} }香腸的裝飾者
public class SausageDecorator extends AbstractDecorator{public SausageDecorator(ABattercake aBattercake) {super(aBattercake);}@Overrideprotected String getDesc() {return super.getDesc()+"加一根香腸";}@Overrideprotected int cost() {return super.cost()+1;} }類圖:
測試:
結果:
類圖:
嵌套的風格有沒有讓你回憶出點什么?
這個時候我們就需要關注過濾器流了 FilterInputStream和 FilterOutputStream,這里以FilterInputStream為例:
舉個例子
其中DataInputStream給FileInputStream起到包裝作用。
Java的IO流裝飾模式很明顯體現在這兩點:
1、有原數據、可以被包裝的。
2、包裝類,能包裝其他事物的,就是在原事物上添加點東西。
復盤
文章內容是有點多的,其實重要的是一定要理解這幾個概念:
Java IO涉及的東西還并未結束,畢竟它是Java的一些高級技術的基石,而且也是一些大廠面試必不可少的東西,打得越牢固越好。下面要么再寫一篇,要么在該篇繼續補充,歡迎各位拍磚~
總結
以上是生活随笔為你收集整理的【Java核心技术卷】I/O详析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flash基本概念和原理
- 下一篇: PHP面向对象常见的关键字和魔术方法