??最重要的因素區(qū)別一個(gè)好的設(shè)計(jì)組件和不好的設(shè)計(jì)組件是組件隱藏了它的內(nèi)部數(shù)據(jù)和其他組件的實(shí)現(xiàn)細(xì)節(jié)。好的設(shè)計(jì)組件隱藏了它的所有實(shí)現(xiàn)夸赫,將它的API和實(shí)現(xiàn)完全分離。組件之間只通過API進(jìn)行通信咖城,并且不清楚各自的內(nèi)部工作茬腿。這個(gè)概念呼奢,被稱為信息隱藏或封裝,是軟件設(shè)計(jì)的基本原則[Parnas72]切平。
??信息隱藏很重要握础,有很多原因,其中很大部分原因是它將構(gòu)成系統(tǒng)的組件解耦悴品,允許它們單獨(dú)開發(fā)禀综,測試,優(yōu)化苔严,使用定枷,理解和修改。這加速了系統(tǒng)開發(fā)届氢,因?yàn)榻M件可以被并行開發(fā)欠窒。它減輕了維護(hù)的負(fù)擔(dān),因?yàn)榻M件可以被更快理解退子,調(diào)試或更換而不必?fù)?dān)心損害其他組件岖妄,雖然信息隱藏本身不會造成好的性能,但是它實(shí)現(xiàn)了有效的性能調(diào)整:一旦系統(tǒng)完成并分析確定那些組件造成了性能問題(item67)寂祥,這些組件可以在不影響其他正常組件的情況下進(jìn)行優(yōu)化荐虐。信息隱藏增加了軟件重用,因?yàn)闆]有緊密耦合的組件在其他環(huán)境中是有用的丸凭,除了它們開發(fā)的那些缚俏。最后,信息隱藏減少了構(gòu)建大型系統(tǒng)的風(fēng)險(xiǎn)贮乳,因?yàn)榧词瓜到y(tǒng)沒有構(gòu)建好忧换,獨(dú)立的組件也能成功。
??Java有許多設(shè)備來輔助信息隱藏向拆。 控制訪問機(jī)制[JLS, 6.6]指定了類亚茬,接口和成員變量的可訪問性。實(shí)體的可訪問性由其聲明的位置確定浓恳,如果有刹缝,聲明存在訪問修飾符(private, protected, 和 public)。正確使用這些修飾符是信息隱藏必不可少的颈将。
??經(jīng)驗(yàn)法則很簡單:讓每個(gè)類或成員盡可能無法訪問梢夯。換句話說,在編寫軟件正常運(yùn)行下晴圾,盡可能使用最低可訪問級別颂砸。
??對于頂級(非嵌套)類和接口,只有兩種可能的訪問級別:package-private 和public。如果你用public修飾符聲明頂級類或接口人乓,它將是公共的勤篮;否則,它將是包私有的色罚。如果一個(gè)定繼類或接口可以變?yōu)榘接械呐龅蓿撬蛻?yīng)該是。通過使其成為包私有戳护,你可以使它稱為實(shí)現(xiàn)的一部分而不是出口API金抡,你可以在后續(xù)版本中修改它,替換他或消除它腌且,而不用擔(dān)心影響現(xiàn)有的客戶端梗肝。如果你使其public,你有義務(wù)一直支持它以保留兼容性切蟋。
??如果只有一個(gè)類使用包私有的頂級類或接口统捶,考慮將頂級類成為使用它的唯一類的私有靜態(tài)嵌套類(item24) 。這降低了所有類到它的一個(gè)類的可訪問性柄粹。但是比包私有頂級類更重要的是降低public類的可訪問性:public類是包API的一部分喘鸟,而包私有類早已是它實(shí)現(xiàn)的一部分。
??對于成員變量(字段驻右,方法什黑,嵌套類和嵌套接口),有四種可訪問等級堪夭,如下按照增加訪問性的順序列出:
? private—成員變量只能從聲明它的頂級類中訪問愕把。
? package-private—成本變量能在聲明的包中的任意類訪問。技術(shù)上成為默認(rèn)訪問森爽,如果你沒有加上訪問修飾符恨豁,這就是默認(rèn)訪問級別(除了默認(rèn)的為public的接口除外)。
? protected—成員變量能從聲明的類和子類中訪問(有一些限制[JLS, 6.6.2])以及聲明的類的包中的任何類都能訪問爬迟。
? public—任何地方任何類都能訪問橘蜜。
??仔細(xì)設(shè)計(jì)你的類的公共API后,你的反應(yīng)應(yīng)該是讓所有其他成員都為private付呕。只有當(dāng)同一個(gè)報(bào)另一個(gè)類真的需要訪問成為计福,你應(yīng)該去掉private修飾符,使得成員變得package-private徽职。如果你發(fā)現(xiàn)你自己經(jīng)常這么做象颖,你應(yīng)該重新檢查你的系統(tǒng)的設(shè)計(jì),看看另一個(gè)分解是否產(chǎn)生更好彼此分離的類姆钉。也就是說说订,private和package-private成員都是類實(shí)現(xiàn)的一部分抄瓦,但通常不會影響出口API。然而克蚂,如果類實(shí)現(xiàn)了Serializable闺鲸,這些字段可能會“泄漏”到出口API(item86 和item87 )筋讨。
??對于public類的成員埃叭,當(dāng)訪問等級從package-private 到 protected時(shí),可訪問性大大增加悉罕。受保護(hù)的成員是類出口API的一部分赤屋,必須永久支持。此外壁袄,出口類的protected成員表示對實(shí)現(xiàn)細(xì)節(jié)的公開承諾(item19)类早。對protected成員的需求相對比較罕見。
??
??有一個(gè)關(guān)鍵規(guī)則限制你降低方法的可訪問性的能力嗜逻。如果一個(gè)方法覆寫了超類的方法涩僻,它不能在子類中比父類的訪問性低[JLS, 8.4.8.3]。這是必要的栈顷,因?yàn)檫@樣可以確保子類實(shí)例在父類實(shí)例中的任何地方都是可用的(里氏替換原則)逆日,如果你違反了這個(gè)規(guī)則,當(dāng)你嘗試編譯子類時(shí)萄凤,編譯器將生成錯誤消息室抽。這個(gè)規(guī)則的特例是如果一個(gè)類實(shí)現(xiàn)了一個(gè)接口,所有這個(gè)類的方法都必須在類中聲明為public靡努。
??為了方便測試你的代碼坪圾,除了其他必要的,你可能傾向使類惑朦,接口或成員擁有更多訪問性兽泄。這一點(diǎn)很好。為了對其測試漾月,可以將public類的private成員package-private病梢,但是提高可訪問性是不可接受的。換句話說栅屏,為了便于測試飘千,將類,接口或成員作為出口API的一部分是不可接受的栈雳。幸運(yùn)的是护奈,這也沒有必要,因?yàn)闇y試可以作為被被測試包的一部分運(yùn)行哥纫,從而獲得對其package-private元素的訪問霉旗。
??public類的實(shí)例字段應(yīng)該幾乎不為public(item16) .如果一個(gè)實(shí)例字段是nonfinal的或它是一個(gè)可變對象的引用,使其public,你就放棄了可以在字段中存儲值得能力了厌秒。這意味著你放棄強(qiáng)制執(zhí)行涉及該字段的不變量的功能读拆。此外,你放棄在修改字段時(shí)采取任何操作的能力鸵闪。所有具有public可變字段的類通常不是線程安全的檐晕。即使字段是final的,引用了一個(gè)不可變對象蚌讼,讓其oublic辟灰,你就放棄切換到不存在該字段的新內(nèi)部數(shù)據(jù)表示的靈活性。
??同樣的建議適用于靜態(tài)字段篡石,但有一個(gè)例外芥喇。你 可以通過public static final字段暴露常量,假設(shè)常量形成了類提供的抽象組成部分凰萨。按照慣例继控,這些字段的名字由大寫字母組成,單詞用下劃線分割(item68) 胖眷。這些字段包含原始值或不可變對象的引用至關(guān)重要(item17) 武通。
??包含對可變對象的引用的字段具有nonfinal字段的所有缺點(diǎn)。雖然無法修改引用瘦材,但是修改引用的對象會帶來災(zāi)難性的結(jié)果厅须。
??請注意,非零長度數(shù)組總是可變的食棕,所以類擁有一個(gè)public static final 數(shù)組字段或但會此類字段的訪問器是錯誤的朗和。如果一個(gè)類擁有這樣的一個(gè)字段或訪問器,客戶端將有可能修改數(shù)組的內(nèi)容簿晓,這就是安全漏洞的常見原因:
??注意到一些IDE生成的訪問器會返回對自由數(shù)組字段的引用眶拉,從而導(dǎo)致這個(gè)問題。有兩種方法來解決這個(gè)問題憔儿。你可以使public數(shù)組設(shè)為private忆植,并添加public不可變列表:
??或者,你可以將數(shù)組設(shè)為private谒臼,添加public方法返回這個(gè)private數(shù)組的副本朝刊。
??在這些選擇方案中選擇,請考慮客戶端可能會對結(jié)果做什么蜈缤。哪種返回類型更方便拾氓?哪個(gè)會帶來更好的性能?
??從Java9開始底哥,作為模塊系統(tǒng)的一部分引入了兩個(gè)額外的隱式訪問級別咙鞍。模塊是一組包房官,就好像一個(gè)包是一組類一樣。一個(gè)模塊可以在模塊聲明中通過導(dǎo)出聲明顯式導(dǎo)出一些包(按照慣例包含在名為module-info.java的源文件中)续滋,未公開包的public和protected成員在模塊中不受導(dǎo)出聲明的影響翰守,在模塊外無法訪問。使用模塊系統(tǒng)允許你在包中分享類疲酌,而不會對整個(gè)世界可見蜡峰。未導(dǎo)出包的public類的public和protected成員產(chǎn)生兩個(gè)隱式訪問級別,它們是正常公共級別和受保護(hù)級別的模塊內(nèi)類似物徐勃。這種共享的需求相對較少事示,通吃缦瘢可以通過重新安排包中的類可以消除僻肖。
??不像四個(gè)主要訪問級別,這兩個(gè)基于模塊的等級主要是建議性的卢鹦。如果你將模塊的JAR包放在應(yīng)用程序的類路徑上而不是它的模塊路徑上臀脏,模塊中的包重新恢復(fù)非模塊化的行為:包的共用類的所有公共和受保護(hù)成員都具有正常的可訪問性,無論軟件包是否由模塊導(dǎo)出[Reinhold, 1.2]冀自。嚴(yán)格執(zhí)行新引入的訪問級別的地方是JDK本身:Java庫中未導(dǎo)出的包在其模塊之外是真正無法訪問的揉稚。
??對于典型的Java程序員而言,有限的實(shí)用程序模塊不僅提供訪問保護(hù)熬粗,而且主要是咨詢性的搀玖;為了利用它,你必須將你的包分組到模塊中驻呐,在模塊聲明中明確其所有依賴項(xiàng)灌诅,重新排列源代碼樹,并采取特殊操作以適應(yīng)模塊內(nèi)對非模塊化軟件包的任何訪問 [Reinhold, 3].現(xiàn)在說模塊是否會在JDK本身之外得到廣泛使用還為時(shí)過早含末。同時(shí)猜拾,除非你有迫切需要,你最好避免使用它們佣盒。
??總而言之挎袜,你應(yīng)該減少程序元素的可訪問性,盡可能地(在合理范圍內(nèi))肥惭。在仔細(xì)設(shè)計(jì)最小公共API之后盯仪,你應(yīng)該阻止任何雜散類,接口或成員成為API的一部分蜜葱。除了作為常量的公共靜態(tài)final字段之外全景,public類應(yīng)該沒有public字段。確保public static final字段引用的對象是不可變的笼沥。
本文寫于2019.3.19蚪燕,歷時(shí)1天