天天看點

對Java Inputstream的一次采訪

在學習java.io.*包的時候,InputStream那一群類很讓人反感,子類繁多就不用說,使用起來非常奇怪。我們想以緩存的方式從檔案中讀取位元組流。總要先建立一個FileInputStream,然後把它放入BufferedInputStream構造函數中去建立BufferedInputStream。完成這些工作後才能開始讀取檔案。

對Java Inputstream的一次采訪

為什麼我們不能直接以緩存方式(BufferedInputStream)從檔案中讀取資料呢?今天我帶着這樣的問題走進InputStream的家,對老人家進行一次采訪, 希望他能解決我心頭的疑惑。

我:老人家您好,我用InputStrem用了很久了, 一直以來都有一個問題困擾着我, 想請教您一下,為什麼我們想以帶緩存的方式從檔案中讀取位元組流需要建立FileInputStream和BufferedInputStream兩個類,這太麻煩了,你看看人家Python, Ruby, 都是打開檔案後直接就可以讀了,多友善啊!

 InputStream:年輕人,不要有情緒 ! 存在的就是合理的,  這其實是一個很長的故事, 要從很久很久以前說起。那時候Java剛剛誕生,我也幸運地被創造出來。那時候帝國實施嚴格的計劃生育,我還沒有任何子孫,很是寂寞。一天,有一個叫小霍的年輕人找到了我。他說他要讓我飛黃騰達,子孫滿堂。

我:這麼神?那您講講您和小霍之間的故事吧。

時光倒流回20年前,小霍初見年輕的InputStream。

小霍:InputStream先生你好,我是JAVA帝國計劃生育委員會的從業人員,組織上聽說你孤苦伶仃的,很是于心不忍,又給你争取了好幾個生育名額, 讓你像Java集合架構那樣兒孫滿堂。

InputStream:真的嗎? 我知道争取生育名額很不容易,那你說說,組織準備讓我生幾個?

小霍:你是IO的輸入類,負責讀取資料(位元組流)。資料就是你的包裹,你一般從哪些管道獲得包裹?

InputStream:檔案,位元組數組,StringBuffer,其它線程,對了還有已經被序列化的對象。

小霍: 那你先根據資料來源的管道生5個孩子,老大叫FileInputStream,處理檔案,老二叫ByteArrayInputStream,處理位元組數組,老三叫StringBufferInputStream,處理StringBuffer,老四叫PipedInputStream,處理線程間的輸入流,老五叫ObjectInputStream,處理被序列化的對象。

InputStream:萬一有一個包裹裡面有多個或者多種資料輸入流呢。

小霍:那就再生一個SequenceInputStream,處理一個包裹裡有多種資料來源的業務。還有其它問題嗎?沒問題我就回機關了。帝國剛建立,我們計劃生育委員會掌管着全國的生育名額,我還忙着呢。你抓緊時間生孩子,有問題再找我。

InputStream:好咧,我這就關燈造人。

交流完畢後,小霍走了,我也抓緊時間把我6個孩子生了出來,為國家做貢獻。InputStream的6個孩子齊心協力,處理了JAVA早期很多的輸入業務。但是他們也面臨了新的問題。沒過多久。年輕的計生委員小霍再次找到了InputStream。

小霍:你那6個孩子都是能人啊,但是現在客戶抱怨他們的工作還不夠到位。

InputStream:我那6個孩子各司其職,工作勤勤懇懇,怎麼還有人抱怨?

小霍:客戶嘛,都比較挑剔。他們抱怨你們讀取資料太慢了,尤其是你的老大FileInputStream每次讀資料都慢死了。好多客戶等待都超過了幾秒了,還沒把資料等回來。

InputStream:幾秒很慢嗎?

小霍:我們計算機都是以納秒計時的,所謂世間一秒鐘,機器已千年。那些客戶頭發都等白了。

InputStream: 讀資料慢能怪我嗎?這不是硬碟慢造成的嗎?

小霍:是,是硬碟造成的,我們想一個辦法讓使用者減少通路硬碟的次數。比如建一個buffer怎麼樣?使用者需要的資料先讓他們在buffer裡面找,能找到就直接從buffer裡傳回,實在找不到再去硬碟裡找。Buffer在記憶體裡,記憶體可比硬碟快10萬倍呢(記憶體在随機通路的速度上是硬碟的10萬倍以上)。

InputStream:這辦法好。客戶抱怨的其他問題呢?

小霍:客戶想要的資料類型都是int, long, String這樣的JAVA基本類型,而你提供給他們的都是byte類型,還需要客戶自己進行類型轉換。客戶覺得麻煩。還有一個問題,Stream裡面讀出來的資料就不能重新放回去,客戶想要一個功能,能把讀出來的資料再推回Stream裡面。

InputStream:看來我得再生3個孩子: 擁有緩存的BufferedInputStream,把byte轉換成JAVA基本類型的DataInputStream和回寫資料到stream的PushbackInputStream。

小霍:老夥計,你糊塗了,不止3個。就拿FileInputStream 來說吧, 加上這三個功能就需要三個子類

Buffered + FileInputStream

Data+ FileInputStream

PushBack + FileInputStream

還有更大的問題,萬一某個特殊的客戶既想有資料回寫,又想要輸出int,long,String這樣的資料,還有要緩存。    或者說他們隻要三個功能中的兩個,這樣組合起來, 又需要4個子類

Buffered + Data + FileInputStream

Buffered + PushBack + FileInputStream

Data + PushBack + FileInputStream

Buffered + Data + PushBack + FileInputStream

換句話說, 僅僅是FileInputStream, 就需要7個子類, 你還有其他5個孩子呢! 總共需要6 * 7 = 42個子類, 我估計客戶看到這麼多子類,眼都花了。

InputStream:看來我們得想另外的辦法。要不然我的家族就要“類爆炸”了,再說讓我一下生那麼多孩子,我也煎熬啊。

小霍:你玩過俄羅斯套娃沒?一個實心的娃娃被各種各樣娃娃外殼套着。一個實心娃娃先套一個學生的外殼,那麼他就是學生了,如果我再在外面套一個運動員的殼,那麼他就成了有運動員身份的學生。我們模仿這種形式,比如最裡面的實心娃娃是處理檔案讀取的FileInputStream,外面套一個BufferedInputStream的殼,那麼這個套娃就是帶buffer的FileInputStream。如果再套一個DataInputStream,那麼套娃就成了能輸出int這樣java 基本類型并且帶buffer的FileInputStream。搭配由客戶去決定,我們隻需要提供套娃殼(新的3個功能類)和最裡面的實心娃InputStream(InputStream的6個孩子)。                                               

對Java Inputstream的一次采訪

InputStream:這很巧妙,那如何實作這樣一種設計呢?

小霍:這有2個關鍵點:

1.  既然套娃中一定有實心娃娃,那麼套娃的殼的類必須包含一個實心娃。比如BufferedInputStream裡面要包含一個InputStream,我們把實心娃娃通過BufferedInputStream的構造函數放進去,當然DataInputStream和PushbackInputStream也一樣。

2.  BufferedInputStream+實心娃娃InputStream組成的新套娃又可以當作DataInputStream的實心娃娃,那麼我們讓這些套娃的外殼BufferedInputStream,DataInputStream,BufferedInputStream都繼承自InputStream類,這樣才能實作新組成的套娃又可以被另外的套娃殼嵌套。這3個套娃殼有着共同的特點都是裝飾實心娃娃,我們再在他們上層在抽象一個FilterInputStream,讓FilterInputStream繼承自InputStream,讓FilterInputStream裡面包含一個實心娃娃InputStream。以後所有的裝飾類都從FilterInputStream繼承。

InputStream:這樣我也省事了,隻需要再多生一個FilterInputStream,剩下的BufferedInputStream,DataInputStream,PushbackInputStream這樣的裝飾類都讓FilterInputStream去生了。

小霍:對,加上FilterInputStream,你就有7個孩子了,跟葫蘆娃一樣。前面6個哥哥都是和資料源有關,7弟就是用來裝飾6個哥哥。把資料源的InputStream類和裝飾的InputStream整合在了一起。

InputStream:而且對于BufferedInputStream,DataInputStream,PushbackInputStream來說,我還是爺爺,想着他們叫我爺爺的樣子,我心裡就美滋滋的。

小霍:美得你,Java中無論多少次繼承都是子類父類關系,沒有爺爺這麼一說。我把家譜給你。你兒子太多,我就畫ByteArrayInputStream,FileInputStream 和FilterInputStream的簡化版。

對Java Inputstream的一次采訪

小霍:這樣的設計既避免了類爆炸,又可以讓使用者自己去搭配核心類和裝飾類。而且也滿足了一個重要的設計原則:開閉原則。這是指導思想。所謂開閉原則就是要對擴充開放,對修改關閉。我們的目标是允許類很容易地進行擴充,在不修改代碼的情況下就可以搭配新的行為。至于缺點嘛,在執行個體化的時候使用者不僅僅執行個體化核心類,還要把核心類包進裝飾者中。對于初次接觸IO類庫的人,無法輕易了解。

InputStream:這是一個好的設計模式,隻是需要一些學習成本。以後要有人不了解這設計模式,我就把我和你之間的故事給他講一遍。

小霍:如此甚好。

回憶結束, 時光回到現在.

InputStream:故事講完了,這下你明白了嗎?

我:原來是這樣啊,為了防止類爆炸而采用了裝飾者模式, 要是按照我起初的想法,有一個專門的類來處理我的InputFileStream+BufferedInputStream,那InputStream您早就因為類太多引起爆炸了。小霍是個厲害的人物啊。

InputStream:是啊,年輕人就得多學習,小霍确實是個了不起的人物。

結束語:有人問過關于文章裡提及的人物真實存在嗎?其實大多數都是我杜撰的。 而本文中的小霍确有其人。亞瑟.範.霍夫(Arthurvan Hoff)早期傑出的Java工程師,大多數的Java.io類都出自他手。後來也擔任過Flipboard,Dell公司CTO.謝謝他把這麼精彩的設計帶到人間。文中提及的所有類InputFileStream,BufferedInputStream等都可以在java.io.*中找到,有興趣的可以去讀讀源碼,jdk的源碼就是最規範的java規範,最詳細的文檔。