第十三條、使類和成員的可訪問性最小化
設計良好的模塊會隱藏所有的實現細節,把它的API和它的實現清晰地隔離開來。然后模塊之間只通過它們的API進行通信,一個模塊不需要知道其他模塊的內部工作情況。(信息隱藏(infomation hiding)和封裝(encapsulation))
好處:可以有效地解除組成系統的各模塊之間的耦合關系,使得這些模塊可以獨立地開發、測試、優化、使用、理解和修改。
java的訪問機制決定了類、接口和成員的可訪問性。實體的可訪問性是由該實體聲明所在的位置以及訪問修飾符共同決定的。對于頂層(非嵌套)的類和接口,只有兩種可能的訪問級別:包級私有(package-private)和公有的(public),如果用public修飾符聲明了頂層類或者接口,那他就是公有的,否則它將是包級私有的。通過把類做成包級私有,它實際上成了這個包的實現部分,而不是該包導出API的一部分。如果一個包級私有的頂層類或者接口只是在某一個類的內部被用到,就應該考慮使它成為唯一使用它的那個類的私有嵌套類。
-
對于成員(域、方法、嵌套類和嵌套接口)有四種可能的訪問級別:
- 私有的private:只有在聲明該成員的頂層類內部才可以訪問這個成員。
- 包級私有的:聲明該成員的包內部的任何類都可以訪問這個成員。“缺省default的訪問級別”
- 受保護的protected:聲明該成員的類的子類可以訪問這個成員,且聲明該成員的包內部的任何類也可以訪問這個成員。受保護的成員是類的導出的API的一部分,必須永遠得到支持。導出的類的受保護成員也代表了該類對于某個實現細節的公開承諾,應該盡量少用。
- 公有的public:在任何地方都可以訪問。
如果方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低于超類中的訪問級別。這樣可以確保任何可使用超類實例的地方也都可以使用子類的實例。如果一個類實現了一個接口,那么接口中的所有類方法在這個類中也都必須被聲明為公有的。(因為接口中所有的方法都隱含著公有訪問級別。)
除了公有靜態final域的特殊情況之外,公有類都不應該包含公有域,并且要確保公有靜態final域所引用的對象都是不可變的。
第十四條、在公有類中使用訪問方法而非公有域
應該用包含私有域和公有訪問/設置方法的類帶進行封裝。
第十五條、使可變性最小化
不可變類只是其實例不能被修改的類。每個實例中包含的信息都必須在創建該實例的時候提供,并在對象的整個生命周期內固定不變。(比如:String、基本類型的包裝類、BigInteger和BigDecimal)
不可變類的優點:更加易于設計、實現和使用,不容易出錯且更加安全。-
使類成為不可變遵循的五條規則:
- 不要提供任何會修改對象狀態的方法(mutator);
- 保證類不會被擴展。
- 使所有的域都是final的。
- 使所有的域都成為私有的。
- 確保對于任何可變組件的互斥訪問。
-
一個類的實例:
/** * Created by laneruan on 2017/6/7. * 這個類表示一個復數。 * 這些算術運算都是創建并返回新的Complex實例,而不是修改這個實例的做法。 * 這種被稱為函數的做法,因為這些方法返回惡一個函數的結果,這些函數對操作數進行運算但不修改它。 * 對應的是過程式或命令式的做法。 */ public class Complex { private final double re; private final double im; //對于頻繁使用的值,為他們提供公有的的靜態常量。 public static final Complex ZERO = new Complex(0,0); public static final Complex ONE = new Complex(1,0); public static final Complex I = new Complex(0,1); public Complex(double re,double im){ this.re = re ; this.im = im ; } //使類變成final的一種方式 // private Complex(double re,double im){ // this.re = re ; // this.im = im ; // } public static Complex valueOf(double re,double im){ return new Complex(re,im); } // Accessors with no corresponding mutators //基于極坐標創建復數 public static Complex valueOfPolar(double r,double theta){ return new Complex(r * Math.cos(theta),r * Math.sin(theta)); } public double realPart(){return re;} public double imaginaryPart(){return im;} public Complex add(Complex c){ return new Complex(re + c.re,im + c.re); } public Complex subtract(Complex c) { return new Complex(re - c.re,im - c.im); } public Complex multiply(Complex c){ return new Complex(re*c.re-im*c.im,re*c.im+im*c.re); } public Complex divide(Complex c){ double tmp = c.re * c.re + c.im * c.im; return new Complex((re*c.re+im*c.im)/tmp,(im*c.re-re*c.im)/tmp); } @Override public boolean equals(Object o) { if(o == this){ return true; } if(!(o instanceof Complex)){ return false; } Complex c = (Complex) o ; return Double.compare(re,c.re) == 0 && Double.compare(im,c.im) == 0; } @Override public int hashCode(){ int result = 17 + hashDouble(re); result = 31 * result + hashDouble(im); return result; } private int hashDouble(double val){ long longBits = Double.doubleToLongBits(val); return (int)(longBits ^ (longBits >>>32)); } @Override public String toString(){ return "(" + re + "+" + im+"i)"; } }
這個類表示一個復數。這些算術運算都是創建并返回新的Complex實例,而不是修改這個實例的做法。這種被稱為函數式的做法,因為這些方法返回了一個函數的結果,這些函數對操作數進行運算但不修改它。對應的是過程式或命令式的做法。
這種函數方法的優點帶來了不可變性,不可變對象只有一種狀態,即被創建時的狀態。不可變對象本質上是線程安全的,不要求同步。當多個線程并發訪問這樣的對象,不會發生破壞,所以不可變對象可以被自由地共享。不可變對象為其他對象提供了大量的構件,如果知道一個復雜對象內部的組件不會改變,要維護它的不變性約束是比較容易的。
不可變類的真正唯一缺點在于:對于每個不同的值都需要一個單獨的對象。
-
如何使不可變類自身不被子類化?除了使類成為final外,讓類的所有構造器都變成私有的或者包級私有的,并添加共有的靜態工廠來代替公有的構造器。以Complex為例:
//這種方式雖不常用,但是最靈活。而且可以肯定不能擴展。 private Complex(double re,double im){ this.re = re ; this.im = im ; } public static Complex valueOf(double re,double im){ return new Complex(re,im); }
有關不可變類的序列化:
實現Serializable接口,并且它包含一個或者多個指向可變對象的域,就必須提供一個顯式的readObject或者readResolve方法,或者使用ObjectOutPutStream.writeUnshared和ObjectInputStream.readUnshared方法,即使默認的序列化形式是可接受的。
第十六條、復合優先于繼承
繼承(inheritance)是實現代碼重用的有力手段,但是使用不當會導致軟件變得很脆弱,在包的內部使用繼承是非常安全的,在那里,子類和父類的實現都處在同一個程序員的控制下。然而,對于普通的具體類進行跨越包邊界的繼承,則是非常危險的。繼承打破了封裝性,子類依賴于父類中特定功能的實現細節。父類的實現有可能會隨著發行版本的不同而有所變化,子類可能隨之遭到破壞,因此子類也必須隨著父類的更新而演變。
-
導致子類脆弱的一個相關原因是:它們的父類在后續發行版本中可以獲得新的方法。這些問題都來源于覆蓋(overriding)動作。下面是一個脆弱的實例:
import java.util.Arrays; import java.util.Collection; import java.util.HashSet; /** * Created by laneruan on 2017/6/7. * 需要查詢HashSet。看看自從它從被創建以來曾經添加了多少個元素。 * HashSet類中添加元素的方法:add和addAll,因此這兩個方法都要覆蓋,但并不能正常工作。 * 因為在HashSet的內部,addAll方法是基于add實現的,所以通過addAll方法增加的每個元素都計算了兩次。 * 此時可以通過去掉addAll的覆蓋方法來修正這個問題,但是這是十分脆弱的,它的功能正確性是需要依賴于HashSet的addAll方法是在 * add方法上實現的,不能保證隨著發行版本的不同而不發生變化。所以此時的InstrumentedHashSet是十分脆弱的。 */ //Broken - Inappropriate use of inheritance public class InstrumentedHashSet<E> extends HashSet<E> { private int addCount = 0 ; public InstrumentedHashSet(){} public InstrumentedHashSet(int initCap,Float loadFactor){ super(initCap,loadFactor); } @Override public boolean add(E e){ addCount ++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c){ addCount += c.size(); return super.addAll(c); } public int getAddCount(){ return addCount; } public static void main(String[] args){ InstrumentedHashSet<String> s = new InstrumentedHashSet<String>(); s.addAll(Arrays.asList("Snap","Pop","Crackle")); System.out.println(s.getAddCount());//打印出來是6? } }
-
復合(composition):不用擴展現有的類,而是在新的類中增加一個私有域,它引用現有類的一個實例。因為現有的類變成了新類的一個組件。新類中的每個實例方法都可以調用被包含的現有類實例中對應的方法,并返回它的結果。這被稱為轉發(forwarding),新類中的方法被稱為轉發方法。這樣新得到的類會非常穩固,不依賴于現有類的實現細節。下面是上面脆弱的實例的復合版本:
import java.util.*; /** * Created by laneruan on 2017/6/8. * Set接口的存在使得InstrumentedSet類的設計成為可能,因為Set類保存了HashSet類的功能特性。 * 從本質上來說這個類把一個Set變成了一個增加計數功能的Set。 * 因為每個InstrumentedSet實例都把另一個Set實例包裝起來了,所以稱為wrapper class */ //Wrapper class 包裝類 public class InstrumentedSet<E> extends ForwardingSet<E>{ private int addCount = 0; public InstrumentedSet(Set<E> s){ super(s); } @Override public boolean add(E e){ addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c){ addCount += c.size(); return super.addAll(c); } public int getAddCount(){ return addCount; } public static void main(String[] args){ InstrumentedSet<String> s = new InstrumentedSet<String>(new HashSet<String>()); s.addAll(Arrays.asList("Snap","Pop","Crackle")); System.out.println(s.getAddCount());//打印出來是3 System.out.println(s); } } //Reusable forwarding class class ForwardingSet<E> implements Set<E>{ private final Set<E> s; public ForwardingSet(Set<E> s){this.s = s;} @Override public int size() { return s.size(); } @Override public boolean isEmpty() { return s.isEmpty(); } @Override public boolean contains(Object o) { return s.contains(o); } @Override public Iterator<E> iterator() { return s.iterator(); } @Override public Object[] toArray() { return s.toArray(); } @Override public <T> T[] toArray(T[] a) { return s.toArray(a); } @Override public boolean add(E e) { return s.add(e); } @Override public boolean remove(Object o) { return s.remove(o); } @Override public boolean containsAll(Collection<?> c) { return s.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { return s.addAll(c); } @Override public boolean retainAll(Collection<?> c) { return s.retainAll(c); } @Override public boolean removeAll(Collection<?> c) { return s.removeAll(c); } @Override public void clear() { s.clear(); } @Override public boolean equals(Object o) { return s.equals(o); } @Override public int hashCode() { return s.hashCode(); } @Override public String toString() { return s.toString(); } }
什么時候使用繼承?
只有當子類真正是父類的子類型(subtype)時,即當兩者之間確實存在“is-a”關系時,類B才應該擴展A。如果不能確定每個B確實都是A,通常情況下,B應該包含A的一個私有實例,而且暴露一個較小的、較簡單的API: A本質上不是B的一部分,只是它的實現細節而已。