Java 的 I/O 類庫(kù)的基本架構(gòu)
I/O 問(wèn)題是任何編程語(yǔ)言都無(wú)法回避的問(wèn)題,Java中使用IO(輸入輸出)來(lái)讀取和寫(xiě)入,讀寫(xiě)設(shè)備上的數(shù)據(jù)、硬盤(pán)文件底哗、內(nèi)存、鍵盤(pán)......锚沸,根據(jù)數(shù)據(jù)的走向可分為輸入流和輸出流跋选,這個(gè)走向是以內(nèi)存為基準(zhǔn)的,即往內(nèi)存中讀數(shù)據(jù)是輸入流哗蜈,從內(nèi)存中往外寫(xiě)是輸出流前标。正因如此坠韩,所以 Java 在 I/O 上也一直在做持續(xù)的優(yōu)化,如從 1.4 開(kāi)始引入了 NIO炼列,提升了 I/O 的性能只搁。
本文主要講IO體系與邏輯關(guān)系,不去細(xì)講如何使用類中的API與方法俭尖。文末有FileUtils和FileIOUtils可以將您所想要的所有有關(guān)流操作的相關(guān)代碼涵蓋氢惋,可進(jìn)行仔細(xì)觀看。
代碼傳送門(mén)
FileUtils
FileIOUtils
Java 的 I/O 操作類在包 java.io 下稽犁,大概有將近 80 個(gè)類焰望,從上圖可以看到,整個(gè)Java IO體系都是基于字符流(InputStream/OutputStream) 和 字節(jié)流(Reader/Writer)作為基類已亥,根據(jù)不同的數(shù)據(jù)載體或功能派生出來(lái)的熊赖。這些類大概可以分成四組,分別是:
基于字節(jié)操作的 I/O 接口:InputStream 和 OutputStream
基于字符操作的 I/O 接口:Writer 和 Reader
基于磁盤(pán)操作的 I/O 接口:File
基于網(wǎng)絡(luò)操作的 I/O 接口:Socket(不在IO分組中)
前兩組主要是根據(jù)傳輸數(shù)據(jù)的數(shù)據(jù)格式虑椎,后兩組主要是根據(jù)傳輸數(shù)據(jù)的方式震鹉。
字節(jié)流和字符流的區(qū)別(解惑)
1.字節(jié)流讀取的時(shí)候,讀到一個(gè)字節(jié)就返回一個(gè)字節(jié)绣檬; 字符流使用了字節(jié)流讀到一個(gè)或多個(gè)字節(jié)(中文對(duì)應(yīng)的字節(jié)數(shù)是兩個(gè)足陨,在UTF-8碼表中是3個(gè)字節(jié))時(shí)嫂粟。先去查指定的編碼表娇未,將查到的字符返回。
2.字節(jié)流可以處理所有類型數(shù)據(jù)星虹,如:圖片零抬,MP3,AVI視頻文件宽涌,而字符流只能處理字符數(shù)據(jù)平夜。只要是處理純文本數(shù)據(jù),就要優(yōu)先考慮使用字符流卸亮,除此之外都用字節(jié)流忽妒。
轉(zhuǎn)換流
InputStreamReader 是字節(jié)流通向字符流的橋梁
OutputStreamWriter 是字符流通向字節(jié)流的橋梁
轉(zhuǎn)換流可以將字節(jié)轉(zhuǎn)成字符,原因在于兼贸,將獲取到的字節(jié)通過(guò)查編碼表獲取到指定對(duì)應(yīng)字符段直。 轉(zhuǎn)換流的最強(qiáng)功能就是基于 字節(jié)流 + 編碼表 。沒(méi)有轉(zhuǎn)換溶诞,沒(méi)有字符流鸯檬。
打印流
PrintWriter
-
PrintStream
注打印流: A:只操作目的地,不操作數(shù)據(jù)源 B:可以操作任意類型的數(shù)據(jù) C:如果啟用了自動(dòng)刷新,在調(diào)用println(),printf(),format()方法的時(shí)候螺垢,能夠換行并刷新 D:可以直接操作文件
序列化流
ObjectOutputStream序列化流
-
ObjectInputStream反序列化流 序列化:把對(duì)象作為一個(gè)整體按照流一樣的方式傳輸或者存儲(chǔ)喧务。 反序列化:把網(wǎng)絡(luò)中的流數(shù)據(jù)或者文件中的流數(shù)據(jù)還原成對(duì)象
主要方法有:
Object readObject();該方法拋出異常:ClassNotFountException赖歌。
void writeObject(Object):被寫(xiě)入的對(duì)象必須實(shí)現(xiàn)一個(gè)接口:Serializable,否則就會(huì)拋出:NotSerializableException
如果實(shí)現(xiàn)了該接口功茴,想解決黃線問(wèn)題就生成一個(gè)隨機(jī)的serialVersionUID號(hào)碼
分析基于這兩個(gè)因素來(lái)展開(kāi)
基于字節(jié)的 I/O 操作接口
基于字節(jié)的 I/O 操作接口輸入和輸出分別是:InputStream 和 OutputStream庐冯,InputStream 輸入流的類繼承層次如下圖所示:
輸入流根據(jù)數(shù)據(jù)類型和操作方式又被劃分成若干個(gè)子類,每個(gè)子類分別處理不同操作類型坎穿,OutputStream 輸出流的類層次結(jié)構(gòu)也是類似肄扎,如下圖所示:
這里就不詳細(xì)解釋每個(gè)子類如何使用了,如果不清楚的話可以參考一下 JDK 的 API 說(shuō)明文檔赁酝,這里只想說(shuō)明兩點(diǎn)犯祠,一個(gè)是操作數(shù)據(jù)的方式是可以組合使用的,如這樣組合使用
還有一點(diǎn)是流最終寫(xiě)到什么地方必須要指定酌呆,要么是寫(xiě)到磁盤(pán)要么是寫(xiě)到網(wǎng)絡(luò)中衡载,其實(shí)從上面的類圖中我們發(fā)現(xiàn),寫(xiě)網(wǎng)絡(luò)實(shí)際上也是寫(xiě)文件隙袁,只不過(guò)寫(xiě)網(wǎng)絡(luò)還有一步需要處理就是底層操作系統(tǒng)再將數(shù)據(jù)傳送到其它地方而不是本地磁盤(pán)痰娱。關(guān)于網(wǎng)絡(luò) I/O 和磁盤(pán) I/O 我們將在后面詳細(xì)介紹。
基于字符的 I/O 操作接口
不管是磁盤(pán)還是網(wǎng)絡(luò)傳輸菩收,最小的存儲(chǔ)單元都是字節(jié)梨睁,而不是字符,所以 I/O 操作的都是字節(jié)而不是字符娜饵,但是為啥有操作字符的 I/O 接口呢坡贺?這是因?yàn)槲覀兊某绦蛑型ǔ2僮鞯臄?shù)據(jù)都是以字符形式,為了操作方便當(dāng)然要提供一個(gè)直接寫(xiě)字符的 I/O 接口箱舞,如此而已遍坟。我們知道字符到字節(jié)必須要經(jīng)過(guò)編碼轉(zhuǎn)換,而這個(gè)編碼又非常耗時(shí)晴股,而且還會(huì)經(jīng)常出現(xiàn)亂碼問(wèn)題愿伴,所以 I/O 的編碼問(wèn)題經(jīng)常是讓人頭疼的問(wèn)題。
下圖是寫(xiě)字符的 I/O 操作接口涉及到的類电湘,Writer 類提供了一個(gè)抽象方法 write(char cbuf[], int off, int len) 由子類去實(shí)現(xiàn)隔节。
讀字符的操作接口也有類似的類結(jié)構(gòu),如下圖所示:
讀字符的操作接口中也是 int read(char cbuf[], int off, int len)寂呛,返回讀到的 n 個(gè)字節(jié)數(shù)怎诫,不管是 Writer 還是 Reader 類它們都只定義了讀取或?qū)懭氲臄?shù)據(jù)字符的方式,也就是怎么寫(xiě)或讀昧谊,但是并沒(méi)有規(guī)定數(shù)據(jù)要寫(xiě)到哪去刽虹,寫(xiě)到哪去就是我們后面要討論的基于磁盤(pán)和網(wǎng)絡(luò)的工作機(jī)制。
字節(jié)與字符的轉(zhuǎn)化接口
另外數(shù)據(jù)持久化或網(wǎng)絡(luò)傳輸都是以字節(jié)進(jìn)行的呢诬,所以必須要有字符到字節(jié)或字節(jié)到字符的轉(zhuǎn)化涌哲。字符到字節(jié)需要轉(zhuǎn)化胖缤,其中讀的轉(zhuǎn)化過(guò)程如下圖所示:
image
InputStreamReader 類是字節(jié)到字符的轉(zhuǎn)化橋梁,InputStream 到 Reader 的過(guò)程要指定編碼字符集阀圾,否則將采用操作系統(tǒng)默認(rèn)字符集哪廓,很可能會(huì)出現(xiàn)亂碼問(wèn)題。StreamDecoder 正是完成字節(jié)到字符的解碼的實(shí)現(xiàn)類初烘。也就是當(dāng)你用如下方式讀取一個(gè)文件時(shí):
清單 1.讀取文件
try {
StringBuffer str = new StringBuffer();
char[] buf = new char[1024];
FileReader f = new FileReader("file");
while(f.read(buf)>0){
str.append(buf);
}
str.toString();
} catch (IOException e) {}
FileReader 類就是按照上面的工作方式讀取文件的涡真,F(xiàn)ileReader 是繼承了 InputStreamReader 類,實(shí)際上是讀取文件流肾筐,然后通過(guò) StreamDecoder 解碼成 char哆料,只不過(guò)這里的解碼字符集是默認(rèn)字符集。
寫(xiě)入也是類似的過(guò)程如下圖所示:
image
通過(guò) OutputStreamWriter 類完成吗铐,字符到字節(jié)的編碼過(guò)程东亦,由 StreamEncoder 完成編碼過(guò)程。
同步和異步唬渗、阻塞和非阻塞
同步和異步是針對(duì)IO來(lái)說(shuō)的典阵。所謂同步就是一個(gè)任務(wù)的完成需要依賴另外一個(gè)任務(wù)時(shí),只有等待被依賴的任務(wù)完成后镊逝,依賴的任務(wù)才能算完成壮啊,這是一種可靠的任務(wù)序列。要么成功都成功撑蒜,失敗都失敗歹啼,兩個(gè)任務(wù)的狀態(tài)可以保持一致。而異步是不需要等待被依賴的任務(wù)完成减江,只是通知被依賴的任務(wù)要完成什么工作染突,依賴的任務(wù)也立即執(zhí)行,只要自己完成了整個(gè)任務(wù)就算完成了辈灼。至于被依賴的任務(wù)最終是否真正完成,依賴它的任務(wù)無(wú)法確定也榄,所以它是不可靠的任務(wù)序列巡莹。我們可以用打電話和發(fā)短信來(lái)很好的比喻同步與異步操作。
阻塞和非阻塞是針對(duì)CPU來(lái)說(shuō)的甜紫。阻塞與非阻塞主要是從 CPU 的消耗上來(lái)說(shuō)的降宅,阻塞就是 CPU 停下來(lái)等待一個(gè)慢的操作完成 CPU 才接著完成其它的事。非阻塞就是在這個(gè)慢的操作在執(zhí)行時(shí) CPU 去干其它別的事囚霸,等這個(gè)慢的操作完成時(shí)腰根,CPU 再接著完成后續(xù)的操作。雖然表面上看非阻塞的方式可以明顯的提高 CPU 的利用率拓型,但是也帶了另外一種后果就是系統(tǒng)的線程切換增加额嘿。增加的 CPU 使用時(shí)間能不能補(bǔ)償系統(tǒng)的切換成本需要好好評(píng)估瘸恼。
序列化
Java的對(duì)象序列化將那些實(shí)現(xiàn)了Serializable接口的對(duì)象轉(zhuǎn)換成一個(gè)字節(jié)序列,并能夠在以后將這個(gè)字節(jié)序列完全恢復(fù)為原來(lái)的對(duì)象册养。這一過(guò)程可通過(guò)網(wǎng)絡(luò)進(jìn)行东帅,這樣序列化機(jī)制能夠自動(dòng)彌補(bǔ)不同操作系統(tǒng)之間的差異。對(duì)應(yīng)序列化的聰明之處在于它不僅保存了對(duì)象的“全景圖”球拦,而且能夠追蹤到對(duì)象自所包含的引用靠闭,并保存這些對(duì)象;接著又能夠?qū)?duì)象內(nèi)包含的每個(gè)這樣的引用進(jìn)行最終坎炼;以此類推愧膀。
要實(shí)例化一個(gè)對(duì)象,首先創(chuàng)建某些OutputStream對(duì)象谣光,然后將其封裝在一個(gè)ObjectOutputStream對(duì)象內(nèi)扇调,這是,只需要調(diào)用writeObject()即可將對(duì)象序列化抢肛,并將其發(fā)送到OutputStream(對(duì)象序列化基于字節(jié)狼钮,因此使用InputStream和OutputStream繼承類層次結(jié)構(gòu))。反序列化和序列化過(guò)程正好相反捡絮,需要將一個(gè)InputStream封裝在ObjectInputStream內(nèi)熬芜,然后調(diào)用readObject()獲取一個(gè)引用,它指向一個(gè)向上轉(zhuǎn)型的Object福稳,所以必須向下轉(zhuǎn)型才能直接設(shè)置它們涎拉。
磁盤(pán) I/O 工作機(jī)制
前面介紹了基本的 Java I/O 的操作接口,這些接口主要定義了如何操作數(shù)據(jù)的圆,以及介紹了操作兩種數(shù)據(jù)結(jié)構(gòu):字節(jié)和字符的方式鼓拧。還有一個(gè)關(guān)鍵問(wèn)題就是數(shù)據(jù)寫(xiě)到何處,其中一個(gè)主要方式就是將數(shù)據(jù)持久化到物理磁盤(pán)越妈,下面將介紹如何將數(shù)據(jù)持久化到物理磁盤(pán)的過(guò)程季俩。
我們知道數(shù)據(jù)在磁盤(pán)的唯一最小描述就是文件,也就是說(shuō)上層應(yīng)用程序只能通過(guò)文件來(lái)操作磁盤(pán)上的數(shù)據(jù)梅掠,文件也是操作系統(tǒng)和磁盤(pán)驅(qū)動(dòng)器交互的一個(gè)最小單元酌住。
何時(shí)真正會(huì)要檢查一個(gè)文件存不存?就是在真正要讀取這個(gè)文件時(shí)阎抒,例如 FileInputStream 類都是操作一個(gè)文件的接口酪我,注意到在創(chuàng)建一個(gè) FileInputStream 對(duì)象時(shí),會(huì)創(chuàng)建一個(gè) FileDescriptor 對(duì)象且叁,其實(shí)這個(gè)對(duì)象就是真正代表一個(gè)存在的文件對(duì)象的描述都哭,當(dāng)我們?cè)诓僮饕粋€(gè)文件對(duì)象時(shí)可以通過(guò) getFD() 方法獲取真正操作的與底層操作系統(tǒng)關(guān)聯(lián)的文件描述。例如可以調(diào)用 FileDescriptor.sync() 方法將操作系統(tǒng)緩存中的數(shù)據(jù)強(qiáng)制刷新到物理磁盤(pán)中。
下面以清單 1 的程序?yàn)槔劢茫榻B下如何從磁盤(pán)讀取一段文本字符纱新。如下圖所示:
image
當(dāng)傳入一個(gè)文件路徑,將會(huì)根據(jù)這個(gè)路徑創(chuàng)建一個(gè) File 對(duì)象來(lái)標(biāo)識(shí)這個(gè)文件汇陆,然后將會(huì)根據(jù)這個(gè) File 對(duì)象創(chuàng)建真正讀取文件的操作對(duì)象怒炸,這時(shí)將會(huì)真正創(chuàng)建一個(gè)關(guān)聯(lián)真實(shí)存在的磁盤(pán)文件的文件描述符 FileDescriptor,通過(guò)這個(gè)對(duì)象可以直接控制這個(gè)磁盤(pán)文件毡代。由于我們需要讀取的是字符格式阅羹,所以需要 StreamDecoder 類將 byte 解碼為 char 格式,至于如何從磁盤(pán)驅(qū)動(dòng)器上讀取一段數(shù)據(jù)教寂,由操作系統(tǒng)幫我們完成捏鱼。
Java Socket 的工作機(jī)制
Socket 這個(gè)概念沒(méi)有對(duì)應(yīng)到一個(gè)具體的實(shí)體,它是描述計(jì)算機(jī)之間完成相互通信一種抽象功能酪耕。打個(gè)比方导梆,可以把 Socket 比作為兩個(gè)城市之間的交通工具,有了它迂烁,就可以在城市之間來(lái)回穿梭了看尼。交通工具有多種,每種交通工具也有相應(yīng)的交通規(guī)則盟步。Socket 也一樣藏斩,也有多種。大部分情況下我們使用的都是基于 TCP/IP 的流套接字却盘,它是一種穩(wěn)定的通信協(xié)議狰域。
下圖是典型的基于 Socket 的通信的場(chǎng)景:
image
主機(jī) A 的應(yīng)用程序要能和主機(jī) B 的應(yīng)用程序通信,必須通過(guò) Socket 建立連接黄橘,而建立 Socket 連接必須需要底層 TCP/IP 協(xié)議來(lái)建立 TCP 連接兆览。建立 TCP 連接需要底層 IP 協(xié)議來(lái)尋址網(wǎng)絡(luò)中的主機(jī)。我們知道網(wǎng)絡(luò)層使用的 IP 協(xié)議可以幫助我們根據(jù) IP 地址來(lái)找到目標(biāo)主機(jī)塞关,但是一臺(tái)主機(jī)上可能運(yùn)行著多個(gè)應(yīng)用程序抬探,如何才能與指定的應(yīng)用程序通信就要通過(guò) TCP 或 UPD 的地址也就是端口號(hào)來(lái)指定。這樣就可以通過(guò)一個(gè) Socket 實(shí)例唯一代表一個(gè)主機(jī)上的一個(gè)應(yīng)用程序的通信鏈路了描孟。
建立通信鏈路
當(dāng)客戶端要與服務(wù)端通信驶睦,客戶端首先要?jiǎng)?chuàng)建一個(gè) Socket 實(shí)例,操作系統(tǒng)將為這個(gè) Socket 實(shí)例分配一個(gè)沒(méi)有被使用的本地端口號(hào)匿醒,并創(chuàng)建一個(gè)包含本地和遠(yuǎn)程地址和端口號(hào)的套接字?jǐn)?shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)將一直保存在系統(tǒng)中直到這個(gè)連接關(guān)閉缠导。在創(chuàng)建 Socket 實(shí)例的構(gòu)造函數(shù)正確返回之前廉羔,將要進(jìn)行 TCP 的三次握手協(xié)議,TCP 握手協(xié)議完成后僻造,Socket 實(shí)例對(duì)象將創(chuàng)建完成憋他,否則將拋出 IOException 錯(cuò)誤孩饼。
與之對(duì)應(yīng)的服務(wù)端將創(chuàng)建一個(gè) ServerSocket 實(shí)例,ServerSocket 創(chuàng)建比較簡(jiǎn)單只要指定的端口號(hào)沒(méi)有被占用竹挡,一般實(shí)例創(chuàng)建都會(huì)成功镀娶,同時(shí)操作系統(tǒng)也會(huì)為 ServerSocket 實(shí)例創(chuàng)建一個(gè)底層數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)中包含指定監(jiān)聽(tīng)的端口號(hào)和包含監(jiān)聽(tīng)地址的通配符揪罕,通常情況下都是“*”即監(jiān)聽(tīng)所有地址梯码。之后當(dāng)調(diào)用 accept() 方法時(shí),將進(jìn)入阻塞狀態(tài)好啰,等待客戶端的請(qǐng)求轩娶。當(dāng)一個(gè)新的請(qǐng)求到來(lái)時(shí),將為這個(gè)連接創(chuàng)建一個(gè)新的套接字?jǐn)?shù)據(jù)結(jié)構(gòu)框往,該套接字?jǐn)?shù)據(jù)的信息包含的地址和端口信息正是請(qǐng)求源地址和端口鳄抒。這個(gè)新創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)將會(huì)關(guān)聯(lián)到 ServerSocket 實(shí)例的一個(gè)未完成的連接數(shù)據(jù)結(jié)構(gòu)列表中,注意這時(shí)服務(wù)端與之對(duì)應(yīng)的 Socket 實(shí)例并沒(méi)有完成創(chuàng)建椰弊,而要等到與客戶端的三次握手完成后许溅,這個(gè)服務(wù)端的 Socket 實(shí)例才會(huì)返回,并將這個(gè) Socket 實(shí)例對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)從未完成列表中移到已完成列表中秉版。所以 ServerSocket 所關(guān)聯(lián)的列表中每個(gè)數(shù)據(jù)結(jié)構(gòu)贤重,都代表與一個(gè)客戶端的建立的 TCP 連接。
數(shù)據(jù)傳輸
傳輸數(shù)據(jù)是我們建立連接的主要目的沐飘,如何通過(guò) Socket 傳輸數(shù)據(jù)游桩,下面將詳細(xì)介紹。
當(dāng)連接已經(jīng)建立成功耐朴,服務(wù)端和客戶端都會(huì)擁有一個(gè) Socket 實(shí)例借卧,每個(gè) Socket 實(shí)例都有一個(gè) InputStream 和 OutputStream,正是通過(guò)這兩個(gè)對(duì)象來(lái)交換數(shù)據(jù)筛峭。同時(shí)我們也知道網(wǎng)絡(luò) I/O 都是以字節(jié)流傳輸?shù)念砹酢.?dāng) Socket 對(duì)象創(chuàng)建時(shí),操作系統(tǒng)將會(huì)為 InputStream 和 OutputStream 分別分配一定大小的緩沖區(qū)影晓,數(shù)據(jù)的寫(xiě)入和讀取都是通過(guò)這個(gè)緩存區(qū)完成的镰吵。寫(xiě)入端將數(shù)據(jù)寫(xiě)到 OutputStream 對(duì)應(yīng)的 SendQ 隊(duì)列中,當(dāng)隊(duì)列填滿時(shí)挂签,數(shù)據(jù)將被發(fā)送到另一端 InputStream 的 RecvQ 隊(duì)列中疤祭,如果這時(shí) RecvQ 已經(jīng)滿了,那么 OutputStream 的 write 方法將會(huì)阻塞直到 RecvQ 隊(duì)列有足夠的空間容納 SendQ 發(fā)送的數(shù)據(jù)饵婆。值得特別注意的是勺馆,這個(gè)緩存區(qū)的大小以及寫(xiě)入端的速度和讀取端的速度非常影響這個(gè)連接的數(shù)據(jù)傳輸效率,由于可能會(huì)發(fā)生阻塞,所以網(wǎng)絡(luò) I/O 與磁盤(pán) I/O 在數(shù)據(jù)的寫(xiě)入和讀取還要有一個(gè)協(xié)調(diào)的過(guò)程草穆,如果兩邊同時(shí)傳送數(shù)據(jù)時(shí)可能會(huì)產(chǎn)生死鎖灌灾,在后面 NIO 部分將介紹避免這種情況。