十三、使類和成員的可訪問性最小化:
?? ?? 信息隱藏是軟件程序設(shè)計(jì)的基本原則之一,面向?qū)ο笥譃檫@一設(shè)計(jì)原則提供了有力的支持和保障。這里我們簡要列出幾項(xiàng)受益于該原則的優(yōu)勢:
?? ?? 1.?? ?更好的解除各個(gè)模塊之間的耦合關(guān)系:
? ? ? 由于模塊間的相互調(diào)用是基于接口契約的,每個(gè)模塊只是負(fù)責(zé)完成自己內(nèi)部既定的功能目標(biāo)和單元測試,一旦今后出現(xiàn)性能優(yōu)化或需求變更時(shí),我們首先需要做的便是定位需要變動(dòng)的單個(gè)模塊或一組模塊,然后再針對(duì)各個(gè)模塊提出各自的解決方案,分別予以改動(dòng)和內(nèi)部測試。這樣便大大降低了因代碼無規(guī)則交叉而帶來的潛在風(fēng)險(xiǎn),同時(shí)也縮減了開發(fā)周期。
? ? ? 2.?? ?最大化并行開發(fā):
? ? ? 由于各個(gè)模塊之間保持著較好的獨(dú)立性,因此可以分配更多的開發(fā)人員同時(shí)實(shí)現(xiàn)更多的模塊,由于每個(gè)人都是將精力完全集中在自己負(fù)責(zé)和擅長的專一領(lǐng)域,這樣不僅提高了軟件的質(zhì)量,也大大加快了開發(fā)的進(jìn)度。
? ? ? 3.?? ?性能優(yōu)化和后期維護(hù):
? ? ? 一般來說,局部優(yōu)化的難度和可行性總是要好于來自整體的優(yōu)化,事雖如此,然而我們首先需要做的卻是如何定位需要優(yōu)化的局部,在設(shè)計(jì)良好的系統(tǒng)中,完成這樣的工作并非難事,我們只需針對(duì)每個(gè)涉及的模塊做性能和壓力測試,之后再針對(duì)測試的結(jié)果進(jìn)行分析并拿到相對(duì)合理的解決方案。
? ? ? 4.?? ?代碼的高可復(fù)用性:
? ? ? 在軟件開發(fā)的世界中,提出了眾多的設(shè)計(jì)理論,設(shè)計(jì)原則和設(shè)計(jì)模式,之所以這樣,一個(gè)非常現(xiàn)實(shí)的目標(biāo)之一就是消除重復(fù)代碼,記得《重構(gòu)》中有這樣的一句話:“重復(fù)代碼,萬惡之源”。可見提高可用代碼的復(fù)用性不僅對(duì)編程效率和產(chǎn)品質(zhì)量有著非常重要的意義,對(duì)日后產(chǎn)品的升級(jí)和維護(hù)也是至關(guān)重要的。說一句比較現(xiàn)實(shí)的話,一個(gè)設(shè)計(jì)良好的產(chǎn)品,即使因?yàn)槟承┰驅(qū)е率。敲串a(chǎn)品中應(yīng)用到的一個(gè)個(gè)獨(dú)立、可用和高效的模塊也為今后的東山再起提供了一個(gè)很好的技術(shù)基礎(chǔ)。
? ? ? 讓我們重新回到主題,Java通過訪問控制的方式來完成信息隱藏,而我們的原則是盡可能的使每個(gè)類的域成員不被外界訪問。對(duì)于包內(nèi)的類而言,則盡可能少的定義公有類,遵循這樣的原則可以極大的降低因包內(nèi)設(shè)計(jì)或?qū)崿F(xiàn)的改變而給該包的使用者帶來的影響。當(dāng)然達(dá)到這個(gè)目標(biāo)的一個(gè)重要前提是定義的接口足以完成調(diào)用者的需求。
? ? ? 該條目給出了一個(gè)比較重要的建議,既不要提供直接訪問或通過函數(shù)返回可變域?qū)ο蟮膶?shí)例,見下例:
? ? ??
public final Thing[] values = { ... };
? ? ? 即便Thing數(shù)組對(duì)象本身是final的,不能再被賦值給其他對(duì)象,然而數(shù)組內(nèi)的元素是可以改變的,這樣便給外部提供了一個(gè)機(jī)會(huì)來修改內(nèi)部數(shù)據(jù)的狀態(tài),從而在主類未知的情況下破壞了對(duì)象內(nèi)部的狀態(tài)或數(shù)據(jù)的一致性。其修訂方式如下:
1 private static final Thing[] PRIVATE_VALUES = { ... }; 2 public static final Thing[] values() { 3 return PRIVATE_VALUES.clone(); 4 }
? ? ? 總而言之,你應(yīng)該盡可能地降低可訪問性。你在仔細(xì)地設(shè)計(jì)了一個(gè)最小的公有API之后,應(yīng)該防止把任何散亂的類、接口和成員變成API的一部分。除了公有靜態(tài)final域的特殊情形之外,公有類都不應(yīng)該包含公有域。并且要確保公有靜態(tài)final域所引用的對(duì)象都是不可變的。
十四、在公有類中使用訪問方法而非公有域:
? ? ? 這個(gè)條目簡短的標(biāo)題已經(jīng)非常清晰的表達(dá)了他的含義,我們這里將只是列出幾點(diǎn)說明:
? ? ? 1.?? ?對(duì)于公有類而言,由于存在大量的使用者,因此修改API接口將會(huì)給使用者帶來極大的不便,他們的代碼也需要隨之改變。如果公有類直接暴露了域字段,一旦今后需要針對(duì)該域字段添加必要的約束邏輯時(shí),唯一的方法就是為該字段添加訪問器接口,而已有的使用者也將不得不更新其代碼,以避免破壞該類的內(nèi)部邏輯。
? ? ? 2.?? ?對(duì)于包級(jí)類和嵌套類,公有的域方法由于只能在包內(nèi)可以被訪問,因而修改接口不會(huì)給包的使用者帶來任何影響。
? ? ? 3.?? ?對(duì)于公有類中的final域字段,提供直接訪問方法也會(huì)帶來負(fù)面的影響,只是和非final對(duì)象相比可能會(huì)稍微好些,如final的數(shù)組對(duì)象,即便數(shù)組對(duì)象本身不能被修改,但是他所包含的數(shù)組成員還是可以被外部改動(dòng)的,針對(duì)該情況建議提供API接口,在該接口中可以添加必要的驗(yàn)證邏輯,以避免非法數(shù)據(jù)的插入,如:
1 public <T> boolean setXxx( int index, T value) { 2 if (index > myArray.length) 3 return false ; 4 if (!(value instanceof LegalClass)) 5 return false ; 6 ... 7 return true ; 8 }
十五、使可變性最小化:
?? ?? 只在類構(gòu)造的時(shí)候做初始化,構(gòu)造之后類的外部沒有任何方法可以修改類成員的狀態(tài),該對(duì)象在整個(gè)生命周期內(nèi)都會(huì)保持固定不變的狀態(tài),如String、Integer等。不可變類比可變類更加易于設(shè)計(jì)、實(shí)現(xiàn)和使用,而且線程安全。
? ? ? 使類成為不可變類應(yīng)遵循以下五條原則:
? ? ? 1.?? ?不要提供任何會(huì)修改對(duì)象狀態(tài)的方法;
? ? ? 2.?? ?保證類不會(huì)被擴(kuò)展,既聲明為final類,或?qū)?gòu)造函數(shù)定義為私有;
?? ?? 3.?? ?使所有的域都是final的;
? ? ? 4.?? ?使所有的域都成為私有的;
? ? ? 5.?? ?確保在返回任何可變域時(shí),返回該域的deep copy。
? ? ? 見如下Complex類:
1 final class Complex { 2 private final double re; 3 private final double im; 4 public Complex( double re, double im) { 5 this .re = re; 6 this .im = im; 7 } 8 public double realPart() { 9 return re; 10 } 11 public double imaginaryPart() { 12 return im; 13 } 14 public Complex add(Complex c) { 15 return new Complex(re + c.re,im + c.im); 16 } 17 public Complex substract(Complex c) { 18 return new Complex(re - c.re, im - c.im); 19 } 20 ... ... 21 }
? ? ? 不可變對(duì)象還有一個(gè)對(duì)象重用的優(yōu)勢,這樣可以避免創(chuàng)建多余的新對(duì)象,這樣也能減輕垃圾收集器的壓力,如:
? ? ??
public static final Complex ZERO = new Complex(0,0);
? ? ? public static final Complex ONE = new Complex(1,0);
? ? ? 這樣使用者可以重復(fù)使用上面定義的兩個(gè)靜態(tài)final類,而不需要在每次使用時(shí)都創(chuàng)建新的對(duì)象。
? ? ? 從Complex.add和Complex.substract兩個(gè)方法可以看出,每次調(diào)用他們的時(shí)候都會(huì)有新的對(duì)象被創(chuàng)建,這樣勢必會(huì)帶來一定的性能影響,特別是對(duì)于copy開銷比較大的對(duì)象,如包含幾萬Bits的BigInteger。如果我們所作的操作僅僅是修改其中的某個(gè)Bit,如bigInteger.flipBit(0),該操作只是修改了第0位的狀態(tài),而BigInteger卻為此copy了整個(gè)對(duì)象并返回。鑒于此,該條目推薦為不可變對(duì)象提供一個(gè)功能相仿的可變類,如java.util.BitSet之于java.math.BigInteger。如果我們在實(shí)際開發(fā)中確實(shí)遇到剛剛提及的場景,那么使用BitSet或許是更好的選擇。
? ? ? 對(duì)于不可變對(duì)象還有比較重要的優(yōu)化技巧,既某些關(guān)鍵值的計(jì)算,如hashCode,可以在對(duì)象構(gòu)造時(shí)或留待某特定方法(Lazy Initialization)第一次調(diào)用時(shí)進(jìn)行計(jì)算并緩存到私有域字段中,之后再獲取該值時(shí),可以直接從該域字段獲取,避免每次都重新計(jì)算。這樣的優(yōu)化主要是依賴于不可變對(duì)象的域字段在構(gòu)造后即保持不變的特征。
?? ?
十六、復(fù)合優(yōu)先于繼承:
?? ?? 由于繼承需要透露一部分實(shí)現(xiàn)細(xì)節(jié),因此不僅需要超類本身提供良好的繼承機(jī)制,同時(shí)也需要提供更好的說明文檔,以便子類在覆蓋超類方法時(shí),不會(huì)引起未知破壞行為的發(fā)生。需要特別指出的是對(duì)于跨越包邊界的繼承,很可能超類和子類的實(shí)現(xiàn)者并非同一開發(fā)人員或同一開發(fā)團(tuán)隊(duì),因此對(duì)于某些依賴實(shí)現(xiàn)細(xì)節(jié)的覆蓋方法極有可能會(huì)導(dǎo)致預(yù)料之外的結(jié)果,還需要指出的是,這些細(xì)節(jié)對(duì)于超類的普通用戶來說往往是不看見的,因此在未來的升級(jí)中,該實(shí)現(xiàn)細(xì)節(jié)仍然存在變化的可能,這樣對(duì)于子類的實(shí)現(xiàn)者而言,在該細(xì)節(jié)變化時(shí),子類的相關(guān)實(shí)現(xiàn)也需要做出必要的調(diào)整,見如下代碼:
1 // 這里我們需要擴(kuò)展HashSet類,提供新的功能用于統(tǒng)計(jì)當(dāng)前集合中元素的數(shù)量, 2 // 實(shí)現(xiàn)方法是新增一個(gè)私有域變量用于保存元素?cái)?shù)量,并每次添加新元素的方法中 3 // 更新該值,再提供一個(gè)公有的方法返回該值。 4 public class InstrumentedHashSet<E> extends HashSet<E> { 5 private int addCount = 0; 6 public InstrumentedHashSet() {} 7 public InstrumentedHashSet( int initCap, float loadFactor) { 8 super (initCap,loadFactor); 9 } 10 @Override public boolean add(E e) { 11 ++addCount; 12 return super .add(e); 13 } 14 @Override public boolean addAll(Collection<? extends E> c) { 15 addCount += c.size(); 16 return super .addAll(c); 17 } 18 public int getAddCount() { 19 return addCount; 20 } 21 }
? ? ? 該子類覆蓋了HashSet中的兩個(gè)方法add和addAll,而且從表面上看也非常合理,然而他卻不能正常的工作,見下面的測試代碼:
1 public static void main(String[] args) { 2 InstrumentedHashSet<String> s = new InstrumentedHashSet<String>(); 3 s.addAll(Arrays.asList("Snap","Crackle","Pop")); 4 System.out.println("The count of InstrumentedHashSet is " + s.getAddCount()); 5 } 6 // The count of InstrumentedHashSet is 6
? ? ? 從輸出結(jié)果中可以非常清楚的看出,我們得到的結(jié)果并不是我們期望的3,而是6。這是什么原因所致呢?在HashSet的內(nèi)部,addAll方法是基于add方法來實(shí)現(xiàn)的,而HashSet的文檔中也并未列出這樣的細(xì)節(jié)說明。了解了原因之后,我們應(yīng)該取消addAll方法的覆蓋,以保證得到正確的結(jié)果。然而仍然需要指出的是,這樣的細(xì)節(jié)既然未在API文檔中予以說明,那么也就間接的表示這種未承諾的實(shí)現(xiàn)邏輯是不可依賴的,因?yàn)樵谖磥淼哪硞€(gè)版本中他們有可能會(huì)發(fā)生悄無聲息的發(fā)生變化,而我們也無法通過API文檔獲悉這些。還有一種情況是超類在未來的版本中新增了添加新元素的接口方法,因此我們在子類中也必須覆蓋這些方法,同時(shí)也要注意一些新的超類實(shí)現(xiàn)細(xì)節(jié)。由此可見,類似的繼承是非常脆弱的,那么該如何修訂我們的設(shè)計(jì)呢?答案很簡單,復(fù)合優(yōu)先于繼承,見如下代碼:
1 // 轉(zhuǎn)發(fā)類 2 class ForwardingSet<E> implements Set<E> { 3 private final Set<E> s; 4 public ForwardingSet(Set<E> s) { 5 this .s = s; 6 } 7 @Override public int size() { 8 return s.size(); 9 } 10 @Override public void clear() { 11 s.clear(); 12 } 13 @Override public boolean add(E e) { 14 return s.add(e); 15 } 16 @Override public boolean addAll(Collection<? extends E> c) { 17 return s.addAll(c); 18 } 19 ... ... 20 } 21 // 包裝類 22 class InstrumentedHashSet<E> extends ForwardingSet<E> { 23 private int addCount = 0; 24 public InstrumentedHashSet( int initCap, float loadFactor) { 25 super (initCap,loadFactor); 26 } 27 @Override public boolean add(E e) { 28 ++addCount; 29 return super .add(e); 30 } 31 @Override public boolean addAll(Collection<? extends E> c) { 32 addCount += c.size(); 33 return super .addAll(c); 34 } 35 public int getAddCount() { 36 return addCount; 37 } 38 }
? ? ? 由上面的代碼可以看出,這種設(shè)計(jì)最大的問題就是比較瑣碎,需要將接口中的方法基于委托類重新實(shí)現(xiàn)。
? ? ? 在決定使用繼承而不是復(fù)合之間,還應(yīng)該問自己最后一組問題。對(duì)于你試圖擴(kuò)展的類,它的API中有沒有缺陷呢?如果有,你是否愿意把這些缺陷傳播到類的API中?繼承機(jī)制會(huì)把超類API中的所有缺陷傳播到子類中,而復(fù)合則允許設(shè)計(jì)新的API來隱藏這些缺陷。
?? ?
十七、要么為繼承而設(shè)計(jì),并提供文檔說明,要么就禁止繼承:
? ? ? 上一條目針對(duì)繼承將會(huì)引發(fā)的潛在問題給出了很好的解釋,本條目將繼續(xù)深化這一個(gè)設(shè)計(jì)理念,并提出一些好的建議,以便在確實(shí)需要基于繼承來設(shè)計(jì)時(shí),避免這些潛在問題的發(fā)生。
? ? ? 1)?? ?為公有方法提供更為詳細(xì)的說明文檔,這其中不僅包擴(kuò)必要的功能說明和參數(shù)描述,還要包含關(guān)鍵的實(shí)現(xiàn)細(xì)節(jié)說明,比如對(duì)其他公有方法的依賴和調(diào)用。
? ? ? 在上一條目的代碼示例中,子類同時(shí)覆蓋了HashSet的addAll和add方法,由于二者之間存在內(nèi)部的調(diào)用關(guān)系,而API文檔中并沒有給出詳細(xì)的說明,因而子類的覆蓋方法并沒有得到期望的結(jié)果。
? ? ? 2)?? ?在超類中盡可能避免公有方法之間的相互調(diào)用。
?? ?? HashSet.addAll和HashSet.add給我們提供了一個(gè)很好的案例,然而這并不表示HashSet的設(shè)計(jì)和實(shí)現(xiàn)是有問題的,我們只能說HashSet不是為了繼承而設(shè)計(jì)的類。在實(shí)際的開發(fā)中,如果確實(shí)有這樣的需要又該如何呢?很簡單,將公用的代碼提取(extract)到一個(gè)私有的幫助方法中,再在其他的公有方法中調(diào)用該幫助方法。
? ? ? 3)?? ?可以采用設(shè)計(jì)模式中模板模式的設(shè)計(jì)技巧,在超類中將需要被覆蓋的方法設(shè)定為protected級(jí)別。
? ? ? 在采用這種方式設(shè)計(jì)超類時(shí),還需要額外考慮的是哪些域字段也同時(shí)需要被設(shè)定為protected級(jí)別,以保證子類在覆蓋protected方法時(shí),可以得到必要的狀態(tài)信息。
? ? ? 4)?? ?不要在超類的構(gòu)造函數(shù)中調(diào)用可能被子類覆蓋的方法,如public和protected級(jí)別的域方法。
? ? ? 由于超類的初始化早于子類的初始化,如果此時(shí)調(diào)用的方法被子類覆蓋,而覆蓋的方法中又引用了子類中的域字段,這將很容易導(dǎo)致NullPointerException異常被拋出,見下例:
1 public class SuperClass { 2 public SuperClass() { 3 overrideMe(); 4 } 5 public void overrideMe() {} 6 } 7 public final class SubClass extends SuperClass { 8 private final Date d; 9 SubClass() { 10 d = new Date(); 11 } 12 @Override public void overrideMe() { 13 System.out.println(dd.getDay()); 14 } 15 } 16 public static void main(String[] args) { 17 SubClass sub = new SubClass(); 18 sub.overrideMe(); 19 }
? ? ? 5)?? ?如果超類實(shí)現(xiàn)了Cloneable和Serializable接口,由于clone和readObject也有構(gòu)造的能力,因此在實(shí)現(xiàn)這兩個(gè)接口方法時(shí)也需要注意,不能調(diào)用子類的覆蓋方法。
十八、接口優(yōu)先于抽象類:
?? ?? 眾所周知,Java是不支持多重繼承但是可以實(shí)現(xiàn)多個(gè)接口的,而這也恰恰成為了接口優(yōu)于抽象類的一個(gè)重要因素。現(xiàn)將他們的主要差異列舉如下:
? ? ? 1)?? ?現(xiàn)有的類可以很容易被更新,以實(shí)現(xiàn)新的接口。
? ? ? 如果現(xiàn)存的類并不具備某些功能,如比較和序列化,那么我們可以直接修改該類的定義分別實(shí)現(xiàn)Comparable和Serializable接口。倘若Comparable和Serializable不是接口而是抽象類,那么同時(shí)繼承兩個(gè)抽象類是Java語法規(guī)則所不允許的,如果當(dāng)前類已經(jīng)繼承自某個(gè)超類了,那么他將無法再擴(kuò)展任何新的超類。
? ? ? 2)?? ?接口是定義mixin(混合類型)的理想選擇。
? ? ? Comparable是一個(gè)典型的mixin接口,他允許類表明他的實(shí)例可以與其他的可相互比較的對(duì)象進(jìn)行排序。這樣的接口之所以被稱為mixin,是因?yàn)樗试S任選的功能可被混合到類型的主要功能中。抽象類不能被用于定義mixin,同樣也是因?yàn)樗麄儾荒鼙桓碌浆F(xiàn)有的類中:類不可能有一個(gè)以上的超類,類層次結(jié)構(gòu)中也沒有適當(dāng)?shù)牡胤絹聿迦雖ixin。
? ? ? 3)?? ?接口允許我們構(gòu)造非層次結(jié)構(gòu)的類型框架。
? ? ? 由于我們可以為任何已有類添加新的接口,而無需考慮他當(dāng)前所在框架中的類層次關(guān)系,這樣便給功能的擴(kuò)展帶來了極大的靈活性,也減少了對(duì)已有類層次的沖擊。如:
1 public interface Singer { // 歌唱家 2 AudioClip sing(Song s); 3 } 4 public interface SongWriter { // 作曲家 5 Song compose( boolean hit); 6 }
? ? ? 在現(xiàn)實(shí)生活中,有些歌唱家本身也是作曲家。因?yàn)槲覀冞@里是通過接口來定義這兩個(gè)角色的,所有同時(shí)實(shí)現(xiàn)他們是完全可能的。甚至可以再提供一個(gè)接口擴(kuò)展自這兩個(gè)接口,并提供新的方法,如:
1 public interface SingerWriter extends Singer, SongWriter { 2 AudioClip strum(); 3 void actSensitive(); 4 }
?? ?? 試想一下,如果將Singer和SongWriter定義為抽象類,那么完成這一擴(kuò)展就會(huì)是非常浩大的工程,甚至可能造成"組合爆炸"的現(xiàn)象。
? ? ? 我們已經(jīng)列舉出了一些接口和抽象類之間的重要差異,下面我們還可以了解一下如何組合使用接口和抽象類,以便他們能為我們設(shè)計(jì)的框架帶來更好的擴(kuò)展性和層級(jí)結(jié)構(gòu)。在Java的Collections Framework中存在一組被稱為"骨架實(shí)現(xiàn)"(skeletal implementation)的抽象類,如AbstractCollection、AbstractSet和AbstractList等。如果設(shè)計(jì)得當(dāng),骨架實(shí)現(xiàn)可以使程序員很容易的提供他們自己的接口實(shí)現(xiàn)。這種組合還可以讓我們在設(shè)計(jì)自己的類時(shí),根據(jù)實(shí)際情況選擇是直接實(shí)現(xiàn)接口,還是擴(kuò)展該抽象類。和接口相比,骨架實(shí)現(xiàn)類還存在一個(gè)非常明顯的優(yōu)勢,既如果今后為該骨架實(shí)現(xiàn)類提供新的方法,并提供了默認(rèn)的實(shí)現(xiàn),那么他的所有子類均不會(huì)受到影響,而接口則不同,由于接口不能提供任何方法實(shí)現(xiàn),因此他所有的實(shí)現(xiàn)類必須進(jìn)行修改,為接口中新增的方法提供自己的實(shí)現(xiàn),否則將無法通過編譯。
? ? ? 簡而言之,接口通常是定義允許多個(gè)實(shí)現(xiàn)的類型的最佳途徑。這條規(guī)則有個(gè)例外,即當(dāng)演變的容易性比靈活性更為重要的時(shí)候。在這種情況下,應(yīng)該使用抽象類來定義類型,但前提是必須理解并且可以接受這些局限性。如果你導(dǎo)出了一個(gè)重要的接口,就應(yīng)該堅(jiān)決考慮同時(shí)提供骨架實(shí)現(xiàn)類。
?? ?
十九、接口只用于定義類型:
? ? ? 當(dāng)類實(shí)現(xiàn)接口時(shí),接口就充當(dāng)可以引用這個(gè)類的實(shí)例的類型。因此,類實(shí)現(xiàn)了接口,就表明客戶端可以對(duì)這個(gè)類的實(shí)例實(shí)施某些動(dòng)作。為了任何其他目的定義接口是不恰當(dāng)?shù)摹H鐚?shí)現(xiàn)Comparable接口的類,表明他可以存放在排序的集合中,之后再從集合中將存入的對(duì)象有序的讀出,而實(shí)現(xiàn)Serializable接口的類,表明該類的對(duì)象具有序列化的能力。類似的接口在JDK中大量存在。
?? ?
二十、類層次優(yōu)于標(biāo)簽類:
?? ?? 這里先給出標(biāo)簽類的示例代碼:
1 class Figure { 2 enum Shape { RECT,CIRCLE }; 3 final Shape s; // 標(biāo)簽域字段,標(biāo)識(shí)當(dāng)前Figure對(duì)象的實(shí)際類型RECT或CIRCLE。 4 double length; // length和width均為RECT形狀的專有域字段 5 double width; 6 double radius; // radius是CIRCLE的專有域字段 7 Figure( double radius) { // 專為生成CIRCLE對(duì)象的構(gòu)造函數(shù) 8 s = Shape.CIRCLE; 9 this .radius = radius; 10 } 11 Figure( double length, double width) { // 專為生成RECT對(duì)象的構(gòu)造函數(shù) 12 s = Shape.RECT; 13 this .length = length; 14 this .width = width; 15 } 16 double area() { 17 switch (s) { // 存在大量的case判斷來確定實(shí)際的對(duì)象類型。 18 case RECT: 19 return length * width; 20 case CIRCLE: 21 return Math.PI * (radius * radius); 22 default : 23 throw new AssertionError(); 24 } 25 } 26 }
?? ?? 像Figure這樣的類通常被我們定義為標(biāo)簽類,他實(shí)際包含多個(gè)不同類的邏輯,其中每個(gè)類都有自己專有的域字段和類型標(biāo)識(shí),然而他們又都同屬于一個(gè)標(biāo)簽類,因此被混亂的定義在一起。在執(zhí)行真正的功能邏輯時(shí),如area(),他們又不得不通過case語句再重新進(jìn)行劃分。現(xiàn)在我們總結(jié)一下標(biāo)簽類將會(huì)給我們的程序帶來哪些負(fù)面影響。
? ? ? 1.?? ?不同類型實(shí)例要求的域字段被定義在同一個(gè)類中,不僅顯得混亂,而且在構(gòu)造新對(duì)象實(shí)例時(shí),也會(huì)加大內(nèi)存的開銷。
? ? ? 2.?? ?初始化不統(tǒng)一,從上面的代碼中已經(jīng)可以看出,在專為創(chuàng)建CIRCLE對(duì)象的構(gòu)造函數(shù)中,并沒有提供length和width的初始化功能,而是借助了JVM的缺省初始化。這樣會(huì)給程序今后的運(yùn)行帶來潛在的失敗風(fēng)險(xiǎn)。
? ? ? 3.?? ?由于沒有在構(gòu)造函數(shù)中初始化所有的域字段,因此不能將所有的域字段定義為final的,這樣該類將有可能成為可變類。
? ? ? 4.?? ?大量的swtich--case語句,在今后添加新類型的時(shí)候,不得不修改area方法,這樣便會(huì)引發(fā)因誤修改而造成錯(cuò)誤的風(fēng)險(xiǎn)。順便說一下,這一點(diǎn)可以被看做《敏捷軟件開發(fā)》中OCP原則的反面典型。
? ? ? 那么我們需要通過什么方法來解決這樣的問題呢?該條目給出了明確的答案:利用Java語句提供的繼承功能。見下面的代碼:
1 abstract class Figure { 2 abstract double area(); 3 } 4 class Circle extends Figure { 5 final double radius; 6 Circle( double radius) { 7 this .radius = radius; 8 } 9 double area() { 10 return Math.PI * (radius * radius); 11 } 12 } 13 class Rectangle extends Figure { 14 final double length; 15 final double width; 16 Rectangle( double length, double width) { 17 this .length = length; 18 this .width = width; 19 } 20 double area() { 21 return length * width; 22 } 23 }
? ? ? 現(xiàn)在我們?yōu)槊糠N標(biāo)簽類型都定義了不同的子類,可以明顯看出,這種基于類層次的設(shè)計(jì)規(guī)避了標(biāo)簽類的所有問題,同時(shí)也大大提供了程序的可讀性和可擴(kuò)展性,如:
1 class Square extends Rectangle { 2 Square( double side) { 3 super (side,side); 4 } 5 }
? ? ? 現(xiàn)在我們新增了正方形類,而我們所需要做的僅僅是繼承Rectangle類。
? ? ? 簡而言之,標(biāo)簽類很少有適用的場景。當(dāng)你想要編寫一個(gè)包含顯式標(biāo)簽域的類時(shí),應(yīng)該考慮一下,這個(gè)標(biāo)簽是否可以被取消,這個(gè)類是否可以用類層次來代替。當(dāng)你遇到一個(gè)包含標(biāo)簽域的現(xiàn)有類時(shí),就要考慮將它重構(gòu)到一個(gè)層次結(jié)構(gòu)中去。
?? ?
二十一、用函數(shù)對(duì)象表示策略:
? ? ? 函數(shù)對(duì)象可以簡單的理解為C語言中的回調(diào)函數(shù),但是我想他更加類似于C++中的仿函數(shù)對(duì)象。仿函數(shù)對(duì)象在C++的標(biāo)準(zhǔn)庫中(STL)有著廣泛的應(yīng)用,如std::less等。在Java中并未提供這樣的語法規(guī)則,因此他們在實(shí)現(xiàn)技巧上確實(shí)存在一定的差異,然而設(shè)計(jì)理念卻是完全一致的。下面是該條目中對(duì)函數(shù)對(duì)象的描述:
? ? ? Java沒有提供函數(shù)指針,但是可以用對(duì)象引用實(shí)現(xiàn)統(tǒng)一的功能。調(diào)用對(duì)象上的方法通常是執(zhí)行該對(duì)象(that Object)上的某項(xiàng)操作。然而,我們也可能定義這樣一種對(duì)象,它的方法執(zhí)行其他對(duì)象(other Objects)上的操作。如果一個(gè)類僅僅導(dǎo)出這樣的一個(gè)方法,它的實(shí)例實(shí)際上就等同于一個(gè)指向該方法的指針。這樣的實(shí)例被稱為函數(shù)對(duì)象(Function Object),如JDK中Comparator,我們可以將該對(duì)象看做是實(shí)現(xiàn)兩個(gè)對(duì)象之間進(jìn)行比較的"具體策略對(duì)象",如:
1 class StringLengthComparator { 2 public int compare(String s1,String s2) { 3 return s1.length() - s2.length(); 4 } 5 }
? ? ? 這種對(duì)象自身并不包含任何域字段,其所有實(shí)例在功能上都是等價(jià)的,因此可以看作為無狀態(tài)的對(duì)象。這樣為了提供系統(tǒng)的性能,避免不必要的對(duì)象創(chuàng)建開銷,我們可以將該類定義為Singleton對(duì)象,如:
1 class StringLengthComparator { 2 private StringLengthComparator() {} // 禁止外部實(shí)例化該類 3 public static final StringLengthComparator INSTANCE = new StringLengthComparator(); 4 public int compare(String s1,String s2) { 5 return s1.length() - s2.length(); 6 } 7 }
?? ?? StringLengthComparator類的定義極大的限制了參數(shù)的類型,這樣客戶端也無法再傳遞任何其他的比較策略。為了修正這一問題,我們需要讓該類成為Comparator<T>接口的實(shí)現(xiàn)類,由于Comparator<T>是泛型類,因此我們可以隨時(shí)替換策略對(duì)象的參數(shù)類型,如:
1 class StringLengthComparator implements Comparator<String> { 2 public int compare(String s1,String s2) { 3 return s1.length() - s2.length(); 4 } 5 }
? ? ? 簡而言之,函數(shù)指針的主要用途就是實(shí)現(xiàn)策略模式。為了在Java中實(shí)現(xiàn)這種模式,要聲明一個(gè)接口來表示策略,并且為每個(gè)具體策略聲明一個(gè)實(shí)現(xiàn)了該接口的類。當(dāng)一個(gè)具體策略只被使用一次時(shí),可以考慮使用匿名類來聲明和實(shí)例化這個(gè)具體的策略類。當(dāng)一個(gè)具體策略是設(shè)計(jì)用來重復(fù)使用的時(shí)候,他的類通常就要被實(shí)現(xiàn)為私有的靜態(tài)成員類,并通過公有的靜態(tài)final域被導(dǎo)出,其類型為該策略接口。
?? ?
二十二、優(yōu)先考慮靜態(tài)成員類:
? ? ? 在Java中嵌套類主要分為四種類型,下面給出這四種類型的應(yīng)用場景。
? ? ? 1.?? ?靜態(tài)成員類:?? ??? ?
? ? ? 靜態(tài)成員類可以看做外部類的公有輔助類,僅當(dāng)與它的外部類一起使用時(shí)才有意義。例如,考慮一個(gè)枚舉,它描述了計(jì)算器支持的各種操作。Operation枚舉應(yīng)該是Calculator類的公有靜態(tài)成員類,然后,Calculator類的客戶端就可以用諸如Calculator.Operation.PLUS和Calculator.Operation.MINUS這樣的名稱來引用這些操作。
? ? ? 2.?? ?非靜態(tài)成員類:
? ? ? 一種常見的用法是定義一個(gè)Adapter,它允許外部類的實(shí)例被看做是另一個(gè)不相關(guān)的類的實(shí)例。如Map接口的實(shí)現(xiàn)往往使用非靜態(tài)成員類來實(shí)現(xiàn)它們的集合視圖,這些集合視圖是由Map的keySet、entrySet和Values方法返回的。
? ? ? 從語法上講,靜態(tài)成員類和非靜態(tài)成員類之間唯一的區(qū)別是,靜態(tài)成員類的聲明中包含了static修飾符,盡管語法相似,但實(shí)際應(yīng)用卻是大相徑庭。每個(gè)非靜態(tài)成員類的實(shí)例中都隱含一個(gè)外部類的對(duì)象實(shí)例,在非靜態(tài)成員類的實(shí)例方法內(nèi)部,可以調(diào)用外圍實(shí)例的方法。如果嵌套類的實(shí)例可以在它的外圍類的實(shí)例之外獨(dú)立存在,這個(gè)嵌套類就必須是靜態(tài)成員類。由于靜態(tài)成員類中并不包含外部類實(shí)例的對(duì)象引用,因此在創(chuàng)建時(shí)減少了內(nèi)存開銷。
? ? ? 3.?? ?匿名類:
? ? ? 匿名類沒有自己的類名稱,也不是外圍類的一個(gè)成員。匿名類可以出現(xiàn)在代碼中任何允許存在表達(dá)式的地方。然而匿名類的適用性受到諸多限制,如不能執(zhí)行instanceof測試,或者任何需要類名稱的其他事情。我們也無法讓匿名類實(shí)現(xiàn)多個(gè)接口,當(dāng)然也不能直接訪問其任何成員。最后需要說的是,建議匿名類的代碼盡量短小,否則會(huì)影響程序的可讀性。
? ? ? 匿名類在很多時(shí)候可以用作函數(shù)對(duì)象。
? ? ? 4.?? ?局部類:
? ? ? 是四種嵌套類中最少使用的類,在任何"可以聲明局部變量"的地方,都可以聲明局部類,并且局部類也遵守同樣的作用域規(guī)則。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
