本部分包含的一些指導原則,可以幫助哦我們更好滴利用這些語言元素,以便讓設計出來的類更加有用、健壯和靈活。
十二、使類和成員的訪問能力最小化
三個關鍵詞訪問修飾符:private(私有的=類級別的)、未指定(包級私有的)、protected(受保護的=繼承級別的+包級別的訪問)、pulbic(共有的)
備註:其中未指定,使用的是預設的訪問級別,包內部的任何類都可以訪問這個成員。如果類或者介面是包級私有的,就應該做成包級私有的。包級私有的是這個包實現的一部分,而不是這個報API的一部分,包級私有的可以更改實現、修改或去除,不必擔心傷害到客戶,如果是共有的,你就要永遠支援它,並且保持相容性。
經驗表明,儘可能地使每一個類或成員都不被外界訪問。即,在保證軟體功能正確的前提下,使用最低的訪問級別。
公有類不應該包含公有域,除了後面的共有靜態final域的特殊情形——透過公有域的靜態final域來暴露類的常量。按照慣例,這樣的域的名字由大寫字母構成,單詞之間用下劃線隔開(見三十八)。很重要的一點是,這個域要麼包含原語型別的值,要麼包含指向非可變物件的引用(見十三)。
注意:非零長度的陣列總是可變的,所以具有共有靜態final資料域幾乎總是錯誤的。如果一個類包含這樣的一個域,客戶能夠修改陣列中的內容。這是安全漏洞的一個常見根源:
//潛在的安全漏洞 public static final Type[] VALUES={……};
共有陣列應該被替換成私有陣列,以及一個共有的非可變列表:
private static final Type[] PRIVATE_VALUES={……}; public static final List VALUES= Collection.unmodifiableList(Arrays.asList(PRIVATE_VALUES))
十三、支援非可變性
非可變性類是一個簡單的類,它的例項不能被修改。每個例項中包含的所有資訊都必須在該例項被建立的時候就提供出來,並且在物件的整個生命週期保持不變。Java平臺庫包含許多非可變類,其中String、原語型別的包裝類、BigInteger和BigDecimal。
非可變類要遵循的五條規則:
- 不要提供任何會修改物件的方法。
- 保證沒有可被子類改寫的方法。 ->通常將類設定成final,其他方法後面討論。
- 使所有域都是final的。
- 是所有的域都成為私有的。
- 保證對於任何可變元件的互質訪問。 ->在構造方法、訪問方法、和readObject方法(見五十六)中請使用保護性複製(defensive copy)技術(見二十四)。
- 非可變物件本質上是安全的,他們不要求同步。 ->非可變物件可以被自由的共享,對於頻繁用到的值,為它們提供公有的靜態final常量。
如:public static final Complext ZERO=new Complex(0,0);
這種方法可以進一步擴充套件,一個非可變物件可以提供一些靜態工廠,它們吧頻繁用到的例項快取起來,當請求一個預先存在的例項的時候,可以不再建立新的例項。BigInteger和Boolean都有這樣的靜態工廠。使用這樣的靜態工廠可以使得客戶之間可以共享已有的例項,而不是建立新的例項,從而降低記憶體佔用和垃圾回收的代價。
- 你不僅可以共享非可變物件,甚至可以共享它們的內部資訊。
- 非可變物件為其他物件——無論是可變的還是非可變的——提供了大量的構建
- 非可變物件的唯一缺點是,對於每一個不同的值都需要一個單獨的物件。
注:StringBuffer是String類的可變配套類,在特定的環境下,相對BigInteger而言,BigSet是String類的可變配套類。StringBuffer是可變物件,可以對字串進行改寫,主要是insert和append兩個方法,用於多執行緒。而StringBuilder是JDK1.5之後才加入的,和StringBuffer沒有本質區別,但是在單執行緒的情況下使用,速度更快。
- 除非有更好的理由讓一個類成為可變類,否則英愛是非可變的。 ->有get方法,不一定就要有set方法。
- 如果一個類不能做成非可變類,那麼你要儘可能的限制其行為。建構函式應該建立完全初始化的物件,所有的約束關係都應該在這個時候起來。
下面是延遲初始化技術的習慣用法:
//不可變物件的快取,延遲初始化函式 private volatile Foo cacheFooVal=UNLIKE_FOO_VALUE public Foo foo(){ Foo result=cachedFooVal; if(result==UNLIKE_FOO_VALUE) result=cachedFooVal=fooVal(); return result; } //fool值的私有幫助函式 private Fool fooVal(){}
十四、組合優先於繼承
與方法不同的是,繼承打破了封裝性。能用組合完成的就用組合,組合優先於繼承。
用組合的方式可以避免有不合適的繼承所帶來的問題。使用組合不在擴充套件一個已有的類,而是在新類中增加一個私有域,它引用了這個類的一個例項。新類中的每個例項方法都可以呼叫被包含已有例項中對應的方法,並返回它的結果。這被稱為轉發(forwarding),新類中的方法被稱為轉發方法(forwarding method)。這樣的類將會非常穩固,它不依賴已有類的實現細節。即使已有的類增加了新的方法,也不會影響新的類。
// 使用組合取代繼承的包裝類 public class InstrumentedSet implements Set { private final Set s; private int addCount = 0; public InstrumentedSet(Set s) { this.s = s; } public boolean add(Object o) { addCount++; return s.add(o); } public boolean addAll(Collection c) { addCount += c.size(); return s.addAll(c); } public int getAddCount() { return addCount; } // 轉發方法 public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } public boolean isEmpty() { return s.isEmpty(); } public int size() { return s.size(); } public Iterator iterator() { return s.iterator(); } public boolean remove(Object o) { return s.remove(o); } public boolean containsAll(Collection c) { return s.containsAll(c); } public boolean removeAll(Collection c) { return s.removeAll(c); } public boolean retainAll(Collection c) { return s.retainAll(c); } public Object[] toArray() { return s.toArray(); } public Object[] toArray(Object[] a) { return s.toArray(a); } public boolean equals(Object o) { return s.equals(o); } public int hashCode() { return s.hashCode(); } public String toString() { return s.toString(); } }
這裡的包裝類可以用來包裝任何一個Set實現,並且可以與任何以前已有的建構函式一起工作。如
Set s1=new InstrumentedSet(new TreeSet(list)); Set s2=new InstrumentedSet(new HashSet(capacity, loadFactor));
因為一個Instrument例項都把另一個 Set例項包裝了起來,所以我們稱其為包裝類。這也正是裝飾模式,因為Instrument對一個集合進行了修飾,為它增加了計數器的特性。有時候,修改和轉發這兩項技術的結合被錯誤的引用為“委託(delegation)”,從技術角度講,這不是委託,除非包裝類把自己傳遞給一個被包裝的物件。
十五、要麼專門為繼承而設計,給出文件說明,要麼禁止繼承
對於專門為繼承而設計的類而言,需要滿足:
- 該類的文件必須清晰的描述改寫每一個方法所帶來的影響。改寫的方法是指非final的,公有的或受保護的。
- 一個類必須透過某種形式提供適當的鉤子(hook),以便能進入它的內部工作流程中,這樣的形式可以是精心選擇的受保護(protected)方法。
- 建構函式一定不能呼叫可被改寫的方法,無論是直接進行還是間接進行。 ->注:
注:如果違反了第三條規則,很有可能會導致程式失敗。超類的建構函式在子類的建構函式之前執行,所以子類中改寫版本的方法將會在子類的建構函式執行之前先被呼叫。如果改寫版本的方法依賴於子類建構函式所執行的初始化工作,那麼該方法就不會如期執行。
public class Super { // 違反了規則 -建構函式呼叫了重寫的方法 public Super() { m(); } public void m() { } }
下面的子類改寫了方法m,Super唯一的建構函式就錯誤的呼叫了這個方法m:
final class Sub extends Super { private final Date date; // 空的終結欄位,由建構函式設定 Sub() { date = new Date(); } // 重寫了 Super.m, 被Super的建構函式呼叫 public void m() { System.out.println(date); } public static void main(String[] args) { Sub s = new Sub(); s.m(); }
本來期望列印出兩個日期,但是第一次列印出Null,因為方法被建構函式Super()呼叫的時候,造函式Sub還沒有機會初始化data域。
這個的執行順序是父類的建構函式->重寫的方法(回到子類)->子類的建構函式。
- Cloneable的clone()和Serializable的readObject方法,在行為上非常相似於建構函式,所以一個類的限制規則也是適用的。無論是clone或者是readObject方法都不能呼叫一個可被改寫的方法,不管是直接的方式還是間接地方式。
- 如果你決定在一個為了繼承而設計的類中實線Serializable,並且該類有一個readResolve或者writeReplace方法,那麼你必須使readReslove或者writeReplace方法稱為受保護的方法,而不是私有的方法。
十六、 介面優於抽象類
介面和抽象類都是允許多個實現的型別。兩者的區別是抽象類允許包含某些方法的實現,但是介面不允許。實現一個抽象類的型別,它必須成為抽象類的子類。因為Java只允許單繼承,所以抽象類作為型別定義收到了極大的限制。
- 已有的類可以被更新,以實現新的介面。
- 介面是定義mixin(混合型別)的理想選擇。
- 介面可以使我們構造出非層次結構的型別框架。
- 介面使得安全的增加一個類的功能成為可能。
- 雖然介面不允許包含方法的實現,但是,我們可以介面和抽象類的優點結合起來,對於你期望匯出的每一個重要介面,都提供一個抽象的骨架(skeletal implements)實現類。
下面是一個靜態工廠方法,它包含了一個靜態的工廠方法,它包含一個完整的、功能全面的List實現:
//整形陣列的List介面卡 static List intArrayAsList(final int[] a){ if(a==null) throw new NullPointerException(); return new AbstractList(){ public Object get(int i){ return new Integer(a[i]); } public int size(){ return a.length; } public Object set(int i, Object o){ int oldVal=a[i]; a[i]=((Integer)o).intValue(); return new Integer(oldVal); } }; }
這個例子是一個介面卡模式,它使得int陣列可以被看做一個Integer例項列表。這個例子只提供了一個靜態工廠,並且這個類是一個可被訪問的匿名類,它被隱藏在靜態工廠的內部。
下面是Map.Entry介面的骨架實現類:
public abstract class AbstractMapEntry implements Map.Entry { // 基本的 public abstract Object getKey(); public abstract Object getValue(); // 要改變maps的實體必須要重寫的方法 public Object setValue(Object value) { throw new UnsupportedOperationException(); } // 實現Map.Entry.equals的通用約定 public boolean equals(Object o) { if (o == this) return true; if (! (o instanceof Map.Entry)) return false; Map.Entry arg = (Map.Entry)o; return eq(getKey(), arg.getKey()) && eq(getValue(), arg.getValue()); } private static boolean eq(Object o1, Object o2) { return (o1 == null ? o2 == null : o1.equals(o2)); } // 實現Map.Entry.hashCode的通用約定 public int hashCode() { return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); } }
- 抽象類的演化比介面的演化要容易的多。
十七、介面只是被用於定義型別
當一個類實現了一個介面的時候,這個介面被用做一個型別。透過這個型別可以引用這個類的例項。因此,一個類實現了某個介面,就表明客戶可以對這個類的例項實施某些動作。為了其他的目的而定義的介面是不合適的。
常量介面是對介面的不良使用,下面是常量介面的例子:
// 常量介面模式- 請勿使用! public interface PhysicalConstants { // Avogadro's number (1/mol) static final double AVOGADROS_NUMBER = 6.02214199e23; // Boltzmann constant (J/K) static final double BOLTZMANN_CONSTANT = 1.3806503e-23; // Mass of the electron (kg) static final double ELECTRON_MASS = 9.10938188e-31; }
匯出常量的幾種可行方案:
- 常量與類或介面緊密的聯絡在一起,那麼將常量新增到類或介面中。如Java平臺中所有的數值包裝類,比如Integer和Float,都匯出了常量MAX_VALUE和MIN_VALUE。
- 如果這些常量最好被看做一個列舉型別的成員,那麼應使用型別安全列舉類(typesafe enum class)來匯出這些常量。
- 使用不可例項化的工具類(utility class)來匯出常量。下面是PysicalConstants的工具類版本
// 常量工具類 public class PhysicalConstants { private PhysicalConstants() { } // 防止例項化 public static final double AVOGADROS_NUMBER = 6.02214199e23; public static final double BOLTZMANN_CONSTANT = 1.3806503e-23; public static final double ELECTRON_MASS = 9.10938188e-31; }
總之,介面是被用來定義型別的,它們不應該被匯出常量。
十八、優先考慮靜態成員類
巢狀類(nested class)是指被定義在一個類的內部的類。巢狀類存在的目的是為外圍的類提供服務。
巢狀類有四種:靜態成員類(static member class)、非靜態成員類(nostatic member class)、匿名類(anonymous class)和區域性類(local class)。
除了第一種之外,其他三種都被成為內部類(inner class)。靜態成員類是一種簡單的巢狀類,最好把它看做一個普通類,只是碰巧被宣告在類的內部而已。
非靜態成員的另一個用法是定義一個Adapter,它允許外部類的一個例項被看做另一個不相關的例項。如,Map介面的實現往往使用非靜態成員類來實現它們的集合檢視(collection view),這些集合檢視是有Map的keySet、entrySet和Value方法返回的。類似地,諸如Set和List這樣的集合介面的實現往往也使用非靜態成員類來實現它們的迭代器。
//非靜態成員的典型用法 public class MySet extends AbstractSet{ ……//省去不相關的 public Iterator iterator(){ return MyIterator(); } priavate class MyIterator implements Iterator{ …… } }
如果你宣告的成員類不要求訪問外圍例項,那麼請記住把static修飾符放到成員類的宣告中。
非靜態成員類需要訪問外圍例項,如果省略了static修飾符,則每個例項都將包含一個額外的指向外圍例項的引用,維護這份引用需要耗費時間和空間,但又沒有相應的好處。
匿名類僅僅在使用的時候被宣告和例項化,行為與靜態成員類或非靜態成員類非常相似,取決於它所處的環境:如果匿名類出現在一個非靜態的環境中,則它有一個外圍例項。
匿名類通常出現在表示式的中間,可能20行或者更短,太長影響到程式的可讀性。
匿名類的一個通用方法是建立一個函式物件(function object),比如Comparator例項。例如,下面的方法呼叫對一組字串按照其長度進行排序:
//匿名類的典型使用 Arrays.sort(args, new Comparator(){ public int compare(Object o1, Object o2){ return ((String)o1).length()-((String)o2).length(); } });
匿名類的另一個用法是建立一個過程物件(process object),比如Thread、Runable或者TimeTask例項。第三個常見用法是在一個靜態工廠方法的內部(見十六intArrayAsList方法)。第四個常見的用法是在複雜的型別安全列舉型別(它要求為每個例項提供單獨的子類)中,用於公有的靜態final域的初始化器中(見二十一Operation類)。如果Operation類是Calculator的一個靜態成員類,那麼Operation類是雙重巢狀類。
// 公有靜態成員類的典型使用 public class Calculator { public static abstract class Operation { private final String name; Operation(String name) { this.name = name; } public String toString() { return this.name; } // 透過這一常量進行運算子表示 abstract double eval(double x, double y); // 雙重巢狀匿名類 public static final Operation PLUS = new Operation("+") { double eval(double x, double y) { return x + y; } }; public static final Operation MINUS = new Operation("-") { double eval(double x, double y) { return x - y; } }; public static final Operation TIMES = new Operation("*") { double eval(double x, double y) { return x * y; } }; public static final Operation DIVIDE = new Operation("/") { double eval(double x, double y) { return x / y; } }; } // 返回指定的計算結果 public double calculate(double x, Operation op, double y) { return op.eval(x, y); } }