Java 的 IO 系統(tǒng)采用了裝飾器設(shè)計(jì)模式。其 IO 分為面向字節(jié)和面向字符兩種镜豹,面向字節(jié)以字節(jié)為輸入輸出單位帽蝶,面向字符以字符為輸入輸出單位。此外第队,在每部分中,又分為輸入和輸出兩部分刨秆,相互對(duì)應(yīng)凳谦,如InputStream類(lèi)型和OutputStream類(lèi)型。再往下分衡未,又分為數(shù)據(jù)源類(lèi)型和裝飾器類(lèi)型晾蜘。數(shù)據(jù)源類(lèi)型表示的是數(shù)據(jù)的來(lái)源和去處,而裝飾器類(lèi)型可以給輸入輸出賦予額外的功能眠屎。
?
Java IO的結(jié)構(gòu)
在使用中,為了得到我們需要的輸入輸出功能肆饶,我們常常需要將一個(gè)數(shù)據(jù)源對(duì)象和多個(gè)裝飾器對(duì)象組合起來(lái)改衩。例如,我們需要從本地文件中以緩沖的方式按字節(jié)讀入數(shù)據(jù)的話(huà)驯镊,就需要將一個(gè)FileInputStream對(duì)象和一個(gè)BufferedInputStream對(duì)象組合起來(lái)葫督,其中FileInputStream對(duì)象負(fù)責(zé)從文件中按字節(jié)為單位讀取數(shù)據(jù),而B(niǎo)ufferedInputStream對(duì)象負(fù)責(zé)對(duì)讀取數(shù)據(jù)進(jìn)行緩沖板惑。
如果不明白裝飾模式的話(huà)橄镜,Java IO 會(huì)變的難以理解。而如果不清楚 Java IO 的結(jié)構(gòu)的話(huà)冯乘,又會(huì)覺(jué)得它難以使用洽胶。這篇博客結(jié)合裝飾模式介紹了 Java IO 的結(jié)構(gòu),以及部分 IO 類(lèi)的實(shí)現(xiàn)裆馒。這其實(shí)是我的學(xué)習(xí)筆記姊氓,如有不足,歡迎指出喷好。
一翔横、輸入源
我們以輸入為例,講解 Java IO 的結(jié)構(gòu)梗搅。輸入的基本功能是將數(shù)據(jù)從某個(gè)輸入源中讀取出來(lái)禾唁。這個(gè)輸入源可能是文件效览,也有可能是一個(gè)ByteArray對(duì)象,也有可能是一個(gè)String對(duì)象荡短。數(shù)據(jù)源不同丐枉,讀入的方式也不同。因此肢预,Java 的開(kāi)發(fā)者為每種輸入源編寫(xiě)了相應(yīng)的輸入類(lèi)矛洞,有從文件中讀入數(shù)據(jù)的FileInputStream,有從ByteArray對(duì)象中讀入數(shù)據(jù)的ByteArrayInputStream烫映,……沼本。為了統(tǒng)一接口,減少重復(fù)代碼的編寫(xiě)锭沟,Java 的設(shè)計(jì)者從這些輸入類(lèi)中抽兆,抽取出了相同的部分,編寫(xiě)了抽象輸入類(lèi)InputStream族淮,作為所有輸入類(lèi)的基類(lèi)辫红。到目前為止,類(lèi)圖可以整理如下祝辣,為了方便敘述贴妻,省略了一些方法和成員變量。
?
輸入源的結(jié)構(gòu)
其中蝙斜,InputStream是一個(gè)抽象類(lèi)名惩,它是所有輸入源的父類(lèi)。它規(guī)定了輸入源的接口孕荠,其中娩鹉,read()為從輸入源中讀入一個(gè)字節(jié),并以返回值的形式返回稚伍。而read(byte[] b)為從輸入源中讀入一塊數(shù)據(jù)到byte[] b中弯予,其返回值為實(shí)際讀入的字節(jié)數(shù)。而read(byte[] b, int off, int len)則為從輸入源讀入len個(gè)字節(jié)个曙,填充到byte[] b的b[off]及之后的位置上锈嫩。
由于輸入源的讀入操作因輸入源而異,因此垦搬,InputStream中的read()方法是抽象的祠挫,由具體的輸入源子類(lèi)實(shí)現(xiàn)。
在InputStream中悼沿,read(byte[] b)和read(byte[] b, int off, int len)都是調(diào)用read()來(lái)實(shí)現(xiàn)的等舔,即不斷地使用read()來(lái)一個(gè)個(gè)地讀入字節(jié),并放到byte[] b的合適位置上糟趾。但這樣讀取慌植,效率其實(shí)并不高甚牲。以搬磚為例,我們從 A 處搬 10 塊磚給 B 處砌墻的老師傅蝶柿。以InputStream的邏輯來(lái)搬運(yùn)的話(huà)丈钙,我們需要從 A 處拿起一塊磚,跑到 B 處交汤,把磚給老師傅雏赦,跑回 B 處,再拿起一塊……芙扎。多跑了好多趟星岗,浪費(fèi)了好多時(shí)間,力氣大的話(huà)戒洼,完全可以拿起 10 塊磚俏橘,一次性搬完。所以圈浇,在其大多數(shù)子類(lèi)中寥掐,都重寫(xiě)了這些方法。
由于讀取文件需要調(diào)用操作系統(tǒng)的系統(tǒng)調(diào)用磷蜀,需要用C/C++來(lái)完成召耘,所以,在FileInputStream中褐隆,有兩個(gè)native方法污它,read0()和readBytes(byte[] b, int off, int len),分別用來(lái)調(diào)用系統(tǒng)調(diào)用讀取文件中的 1 個(gè)字節(jié)和調(diào)用系統(tǒng)調(diào)用讀取文件中的 1 堆字節(jié)妓灌。其他的讀取方法都是通過(guò)調(diào)用這兩個(gè)方法來(lái)實(shí)現(xiàn)的。
二蜜宪、裝飾器
有了輸入源之后虫埂,我們已經(jīng)可以完成各種讀入數(shù)據(jù)的操作了。我們可以從數(shù)據(jù)源中讀取一個(gè)字節(jié)圃验,或者一堆字節(jié)掉伏。但是,出于性能以及其他方面的考慮澳窑,我們通常還會(huì)給輸入操作添加一些功能斧散,如緩沖。
1. 緩沖
之前講過(guò)一個(gè)搬磚的例子摊聋,我們要從 A 處搬 10 塊磚給 B 處的老師傅鸡捐,考慮到老師傅今天砌墻任務(wù)繁重,之后很可能會(huì)再讓我們?nèi)ソo他搬磚麻裁,于是我們不如一次性多給他搬幾塊過(guò)去放在 B 處箍镜,他再要磚我們直接從 B 處拿給他就好了源祈,就不用再跑去 A 處搬磚過(guò)來(lái)了。這樣就節(jié)省了許多傳輸?shù)臅r(shí)間色迂。
緩沖就是這么個(gè)道理香缺。我們通常會(huì)給輸入和輸出都設(shè)立一個(gè)緩沖區(qū)⌒考慮到之后很可能會(huì)再次讀取數(shù)據(jù)图张,在讀入數(shù)據(jù)時(shí),除了我們需要的數(shù)據(jù)之外诈悍,還會(huì)多讀一些數(shù)據(jù)進(jìn)來(lái)祸轮,放到緩沖區(qū)里。每次讀入數(shù)據(jù)之前写隶,都會(huì)先看看緩沖區(qū)里有沒(méi)有我們要的數(shù)據(jù)倔撞,如果有的話(huà)就從緩沖區(qū)中讀入,沒(méi)有的話(huà)再去數(shù)據(jù)源里讀取慕趴。而在輸出數(shù)據(jù)時(shí)痪蝇,會(huì)先把數(shù)據(jù)輸出到緩沖區(qū)里去,當(dāng)緩沖區(qū)滿(mǎn)了冕房,再將緩沖區(qū)里的數(shù)據(jù)全部輸出到目的地里躏啰。
注意:緩沖區(qū)的讀寫(xiě)還要考慮數(shù)據(jù)的一致性問(wèn)題,這里沒(méi)有過(guò)多的闡述耙册。
2. 裝飾器類(lèi)
就像緩沖一樣给僵,我們通常會(huì)給輸入輸出加上一些額外的功能。于是問(wèn)題來(lái)了详拙,我們?cè)趺床拍茏屆糠N輸入源都具備這些功能呢帝际?最簡(jiǎn)單的,就是為每一種輸入源的每種額外功能都寫(xiě)一個(gè)類(lèi)饶辙,就像下面這樣(為了讓圖小一點(diǎn)蹲诀,省略了其他的輸入源)。
?
不使用裝飾器模式時(shí)的類(lèi)結(jié)構(gòu)
這樣的設(shè)計(jì)會(huì)帶來(lái)許多問(wèn)題弃揽。
首先脯爪,類(lèi)太多了。在不考慮功能組合的情況下矿微,如果有 m 個(gè)輸入源痕慢,要實(shí)現(xiàn) n 個(gè)功能,那就需要寫(xiě) m 乘 n 個(gè)類(lèi)涌矢,考慮功能組合的話(huà)掖举,還要更多。
其次娜庇,重復(fù)代碼太多拇泛。其實(shí)同一個(gè)功能的代碼都差不多滨巴,但要給每個(gè)輸入源都寫(xiě)一遍。寫(xiě)的時(shí)候麻煩俺叭,到時(shí)候要改這個(gè)功能的代碼恭取,還得一個(gè)個(gè)改過(guò)去,不利于維護(hù)熄守。
為了解決上面的問(wèn)題蜈垮,Java 的設(shè)計(jì)人員將各個(gè)功能拎了出來(lái),給每個(gè)功能單獨(dú)寫(xiě)了功能類(lèi)裕照,如通過(guò)BufferedInputStream類(lèi)來(lái)為輸入源提供緩沖功能攒发,通過(guò)DataInputStream類(lèi)來(lái)為輸入源提供基本類(lèi)型數(shù)據(jù)的讀入功能。請(qǐng)注意晋南,此時(shí)惠猿,功能類(lèi)僅僅提供了功能,它本身并不能從輸入源中讀取數(shù)據(jù)负间,所以在功能類(lèi)內(nèi)部都會(huì)有一個(gè)數(shù)據(jù)源類(lèi)的成員變量偶妖,從數(shù)據(jù)源中讀取數(shù)據(jù)的操作都是通過(guò)這個(gè)成員變量來(lái)完成的。就像下面這樣:
class Func1Decorator extends InputStream {
? ? private InputStream in;
? ? Func1Decorator(InputStream in){
? ? ? ? this.in = in;
? ? }
? ? public int read() {
? ? ? ? ...
? ? ? ? a = in.read();
? ? ? ? ...
? ? }
? ? ...
}
知識(shí)點(diǎn):其實(shí)從這里可以看出政溃,組合比繼承要更靈活趾访,因?yàn)榻M合可以和多態(tài)結(jié)合。
在功能類(lèi)初始化時(shí)董虱,就從外界傳入了輸入源對(duì)象扼鞋,其后,從數(shù)據(jù)源讀取數(shù)據(jù)的操作都由這個(gè)對(duì)象負(fù)責(zé)愤诱,而功能類(lèi)僅負(fù)責(zé)對(duì)讀入的數(shù)據(jù)進(jìn)行處理來(lái)完成其功能云头。
注意到,這里的功能類(lèi)還繼承了輸入源類(lèi)InputStream淫半。一方面溃槐,這是因?yàn)閺耐饨缈磥?lái),功能類(lèi)確實(shí)是一個(gè)InputStream撮慨,它實(shí)現(xiàn)了InputStream中所有的接口竿痰。它的語(yǔ)意是一個(gè)帶有Func1功能的InputStream脆粥。另一方面砌溺,這也方便了功能的組合,當(dāng)功能類(lèi)同時(shí)也是InputStream時(shí)变隔,要組合兩個(gè)功能到一起時(shí)规伐,只需要按一定的順序把一個(gè)功能類(lèi)的對(duì)象看作輸入源對(duì)象傳入進(jìn)去即可。如:
DataInputStream in = new DataInputStream(
? ? ? ? ? ? ? ? ? ? ? ? new BufferedInputStream(new FileInputStream("filename")));
上面這段代碼創(chuàng)建了一個(gè)能讀取基本數(shù)據(jù)類(lèi)型數(shù)據(jù)并帶有緩沖的文件輸入對(duì)象匣缘。因?yàn)楣δ茴?lèi)也是一個(gè)InputStream猖闪,它可以被當(dāng)作其他功能類(lèi)的數(shù)據(jù)源類(lèi)鲜棠,其他的功能類(lèi)會(huì)在它的read方法的基礎(chǔ)上,繼續(xù)拓展自己的功能培慌。
其實(shí)豁陆,之前我們所說(shuō)的功能類(lèi)就是裝飾器,用來(lái)給基礎(chǔ)類(lèi)擴(kuò)展功能吵护。而這種用組合語(yǔ)法利用多態(tài)為基礎(chǔ)類(lèi)擴(kuò)展功能的模式就是裝飾模式盒音。
3. 裝飾器模式的優(yōu)點(diǎn)
裝飾模式分離了裝飾類(lèi)和被裝飾類(lèi)的邏輯。裝飾器類(lèi)中保持了一個(gè)被裝飾對(duì)象的引用馅而,當(dāng)裝飾器類(lèi)需要底層的功能時(shí)祥诽,只需要通過(guò)這個(gè)引用調(diào)用對(duì)應(yīng)方法即可,并不需要了解其具體邏輯瓮恭。這對(duì)代碼的維護(hù)有很大的幫助雄坪。
裝飾模式可以減少類(lèi)的數(shù)量。在前面我們已經(jīng)看到了屯蹦,用純繼承語(yǔ)法來(lái)擴(kuò)展功能需要為每種基礎(chǔ)類(lèi)和功能的各種組合編寫(xiě)類(lèi)维哈,類(lèi)的數(shù)量會(huì)非常地多。而通過(guò)裝飾器模式颇玷,我們只需要寫(xiě)幾個(gè)裝飾器類(lèi)就可以了笨农。裝飾器類(lèi)中保持的被裝飾對(duì)象的引用,會(huì)發(fā)揮其多態(tài)性帖渠,我們傳入什么基礎(chǔ)類(lèi)對(duì)象谒亦,就執(zhí)行對(duì)應(yīng)的方法。這使得一個(gè)裝飾器類(lèi)可以和幾乎所有基礎(chǔ)類(lèi)(及其子類(lèi)空郊,從語(yǔ)義上來(lái)說(shuō)份招,子類(lèi)是特殊的父類(lèi))結(jié)合產(chǎn)生相應(yīng)的擴(kuò)展類(lèi)。
裝飾模式的擴(kuò)展性很好狞甚。當(dāng)要為基礎(chǔ)類(lèi)擴(kuò)展新的功能時(shí)锁摔,用純繼承語(yǔ)法需要為每種基礎(chǔ)類(lèi),為另外的各種功能組合編寫(xiě)類(lèi)哼审。但使用裝飾器模式的話(huà)谐腰,只需要編寫(xiě)一個(gè)裝飾器類(lèi)即可。
裝飾模式利用了組合語(yǔ)法涩盾,在復(fù)用代碼時(shí)十气,組合語(yǔ)法與繼承語(yǔ)法相比有一個(gè)明顯的優(yōu)點(diǎn),就是可以利用多態(tài)春霍,從而根據(jù)組合對(duì)象的不同能夠產(chǎn)生不同的語(yǔ)義砸西。
三、結(jié)構(gòu)
裝飾模式的通用類(lèi)圖如下:
?
裝飾器模式的通用類(lèi)圖
在我們之前的敘述中,是沒(méi)有中間這個(gè)Decorator抽象類(lèi)的芹枷。它是所有裝飾器類(lèi)的父類(lèi)衅疙,它一方面可以使類(lèi)的結(jié)構(gòu)更加清晰,另一方面這個(gè)抽象類(lèi)可以減少各個(gè)子類(lèi)中重復(fù)邏輯的書(shū)寫(xiě)鸳慈。當(dāng)然饱溢,我們剛才所敘述的也是裝飾模式,只不過(guò)沒(méi)有了Decorator抽象類(lèi)走芋,所有的裝飾器類(lèi)都是直接繼承自Component的理朋。這是一種簡(jiǎn)化的裝飾模式。當(dāng)裝飾器數(shù)量比較少時(shí)绿聘,可以省略裝飾器基類(lèi)嗽上。另外在確定只有一種Component時(shí),可以不寫(xiě)Component基類(lèi)熄攘,用那一個(gè)ConcreteComponent來(lái)代替Component基類(lèi)兽愤。
下面是 Java IO 的類(lèi)圖,只畫(huà)了字節(jié)流的輸入部分挪圾,其他部分相似浅萧。另外,因?yàn)轫?yè)面的大小是有限的哲思,而且一些類(lèi)在類(lèi)結(jié)構(gòu)中的位置是相似的洼畅,所以省略了一些類(lèi)。
?
Java IO 的結(jié)構(gòu)
其中棚赔,F(xiàn)ilterInputStream就是裝飾模式中的Decorator基類(lèi)帝簇。繼承自它的都是裝飾器類(lèi),它們?yōu)檩斎霐U(kuò)展了功能靠益。
四丧肴、參考資料
《Thinking in Java》
《設(shè)計(jì)模式之禪》
每天都在分享文章,也每天都有人想要我出來(lái)給大家分享下怎么去學(xué)習(xí)Java胧后。大家都知道芋浮,我們是學(xué)Java全棧的,大家就肯定以為我有全套的Java系統(tǒng)教程壳快。沒(méi)錯(cuò)纸巷,我是有Java全套系統(tǒng)教程,進(jìn)扣裙【47】974【9726】所示眶痰,今天小編就免費(fèi)送!~
后記:對(duì)于大部分轉(zhuǎn)行的人來(lái)說(shuō)瘤旨,找機(jī)會(huì)把自己的基礎(chǔ)知識(shí)補(bǔ)齊,邊工作邊補(bǔ)基礎(chǔ)知識(shí)凛驮,真心很重要裆站。
“我們相信人人都可以成為一個(gè)程序員条辟,現(xiàn)在開(kāi)始黔夭,找個(gè)師兄宏胯,帶你入門(mén),學(xué)習(xí)的路上不再迷茫本姥。這里是ja+va修真院肩袍,初學(xué)者轉(zhuǎn)行到互聯(lián)網(wǎng)行業(yè)的聚集地。"