類與介面 JAVA筆記3
目錄
1.使類和成員的可訪問性最小化
模組設計的好壞與它是否隱藏
其內部資料和其他實現細節有很大的關係。設計良好的模組會隱藏所有的實現細節,把他的API
和它的實現清晰地隔離開來。最後使模組之間只能通過他們的API進行通訊,一個模組不需要知道其他模組的內部工作情況。
這個概念被稱為資訊隱藏
或者封裝
,是軟體設計的基本原則之一。
這樣做的好處在於:
-
它可以有效地接觸組成系統的各模組之間的耦合關係,使得這些模組可以獨立地開發、測試、優化、使用、理解和修改。
-
加快開發速度,使之可以
並行開發
。 -
減輕維護負擔,更好理解模組,並且除錯時不會影響其他的模組。
-
雖然不論是對內還是對外都不會帶來更好的效能但是卻可以更好的
調節效能
。在完成大型系統的時候,它的優點有更好的體現,即使整個系統不可用,但是這些獨立的模組有些卻是有用的,當我們對其中一個模組進行優化時,也不會影響到其他模組的正確性。
公有類不應該直接暴露資料域。
可以使用包含私有域和共有訪問方法(getter)和包含私有域和公有設值方法(setter)的類代替
2.使可變性最小化
不可變類只是其例項不可被修改的類。每個例項包含的所有資訊都必須在建立該例項的時候就提供,並在物件的整個生命週期內不可變。 java中有許多不可變的類,其中就包括String,基本型別的包裝類,BigInteger和BigDecimal。
好處在於:不可變的類比可變的類更加易於設計、實現和使用。他們不容易出錯,且更加安全。
為了使類變得不可變,需要遵循以下五條規則:
-
不要提供任何會修改物件的方法
-
保證類不會被擴充套件。這樣是為了粗心和惡意子類改變原本的不可變性。通常方法是將類設為
final
的。 -
使所有的域都是final的。通過系統的強制方式,更加清楚表明意圖。
-
是所有的域都成為私有的。為了防止客戶端獲得訪問被域引用的可變物件的許可權,從而直接修改這些物件。
-
確保對於可變元件的互斥訪問。如果類裡有指向可變物件的域,那麼就要確保該類的客戶端無法獲得這些可變物件的引用。
但是依舊有一個不容忽視的缺點
對於每個不同的值都需要一個單獨的物件,這樣的話,創造這種物件的代價可能會變得很高,例如我們有一個上百萬位的BigInteger,當我們只需要修改它其中一位時,我們卻要重新創造一個物件。 如果能夠猜測會經常用到的多步驟操作,然後將他們作為基本型別提供。若無法預測,最好的方法時提供一個公有的可變配套類。可以這樣認為,在特定環境下,相對於BigInteger而言,BigSet就扮演了這個角色。
3.複合優先與繼承
繼承是程式碼重用的有力手段,但並非最佳。
在以下情況繼承是安全
的:
-
包的內部使用,在那裡超類以及子類的實現都在同一個程式設計師的控制下。
-
對於專門為了繼承而設計、並且有很好的文件說明的類。
然而,對於普通的具體類進行跨越包邊界
的繼承,則是非常危險的!
複合(composition):不擴充套件現有的類,而是在新的類中增加一個私有域,引用現有類的一個例項。
轉發(fowarding):新類中的每個例項方法都可以呼叫被包含的現有類例項中對應的方法,並返回結果。
public class FowardSet<E> implements Set<E> { #轉發類,被裝飾類
//引用現有類的例項,增加私有域
private final Set<E> set;
public FowardSet(Set<E> set){
this.set = set;
}
/*
*轉發方法
*/
@Override
public int size() {
return set.size();
}
@Override
public boolean isEmpty() {
return set.isEmpty();
}
@Override
public boolean contains(Object o) {
return set.contains(o);
}
@NotNull
@Override
public Iterator<E> iterator() {
return set.iterator();
}
@NotNull
@Override
public Object[] toArray() {
return set.toArray();
}
@NotNull
@Override
public <T> T[] toArray(T[] a) {
return set.toArray(a);
}
@Override
public boolean add(E e) {
return set.add(e);
}
@Override
public boolean remove(Object o) {
return set.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return set.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}
@Override
public void clear() {
set.clear();
}
@Override
public boolean equals(Object obj) {
return set.equals(obj);
}
@Override
public String toString() {
return set.toString();
}
@Override
public int hashCode() {
return set.hashCode();
}
}
/*
* 包裝類(wrapper class),採用裝飾者模式
*/
public class InstrumentedSet<E> extends FowardSet<E> {
private int addCount=0;
public InstrumentedSet(Set<E> set) {
super(set);
}
@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;
}
}
4.要麼為繼承而設計,並提供文件說明,要麼就禁止繼承
專門為繼承而設計的類,很有必要提供文件說明,不僅僅要描述作用,其重要的實現細節也是必不可少的。
為了允許繼承,類還必須遵守一些其他的約束:
-
構造器
絕不能呼叫可被覆蓋的方法。 -
在決定實現Cloneable或者Serializable介面時,不論是
clone
還是readObject
都不允許呼叫可被覆蓋的方法。 -
在決定實現Serializable,並且該類有
readResolve
或者writeReplace
方法,就必須使這兩種方法成為受保護的方法而不是私有的方法。
5.介面總體看來優於抽象類
-
現有的類可以很容易被更新,以實現新的介面。但是若要實現由抽象類定義的型別,類就必須成為抽象類的一個子類。
-
介面是定義mixin(混合型別)的理想選擇。介面允許類來新增任選的功能,將之合併到其基本的型別中。但抽象類不行,類不可能有一個以上的父親,類層次結構中也沒有適當的地方來插入mixin。
-
介面允許我們構造非層次結構的型別框架。比如我們有一個介面表示歌唱家,另一個表示作曲家,在現實生活中存在一個有音樂天賦的人,他既是歌唱家又是作曲家,同時實現這兩個介面是完全允許的,我們甚至可以新增更多這樣類似的介面。若沒有介面,另一種做法則是編寫一個臃腫的類層次,每一個屬性都包含一個單獨的類,如果整個系統中有n個屬性,那麼就必須支援2^n種可能的組合。這種現象稱為組合爆炸。
-
抽象類的演變比介面的演變要容易的多。抽象類中新增新的方法,始終可以增加具體方法,它包含合理的預設實現。但對於介面,這樣行不通。因此設計公有的介面要十分的謹慎。介面一旦被公開發行,並且被廣泛實現,再想改變這個介面幾乎是不可能的。我們必須保證介面在初次設計時就是正確的。
6.介面只用於定義型別。
當類實現介面時,介面就充當這個類的例項的型別。因此,類實現了該介面,就表明客戶端可以對這個類的的例項進行某些動作。如果出於其他的目的來定義介面是不恰當
的。
有一種介面型別,它不包含方法,只有靜態的final域,每一個域都匯出一個常量,這樣的介面被稱為常量介面。這種介面模式實際上是對介面的不良使用。實現常量介面會把實現細節洩露出到該類匯出的api中,類實現常量介面對使用者來講,本身也沒什麼價值,這樣反而會讓他們更加糊塗。更糟糕的是,在將來發行的版本中,這個類被修改了,它不再需要使用這些常量了,但是依然必須實現這個介面,以保證二進位制相容性。
在java平臺庫中有幾個常量介面,例如java.io.ObjectStreamConstants
,這些應當被看作反面教材。正常情況下應當採用列舉型別或者不可例項化的工具類來匯出這些常量。
7.類層次優於標籤類
有時候我們可能會遇到含有兩種或者多種風格的類,並且包含表示例項風格的標籤(tag)域。例如一個Figure類,它可以同時表示圓形或者矩形,可以將它的建構函式分為兩種,一種只有一個引數為圓形,一種有兩個引數為矩形來區分,並給標籤賦值。
class Figure{
enum Shape {RECTANGLE,CIRCLE};
final Shape shape;//標籤
double r;
double x,y;
Figure(double a){
r = a;
shape = Shape.CIRCLE;//標籤賦值
}
Figure(double a,double b){
x = a;
y = b;
shape = Shape.RECTANGLE;//標籤賦值
}
//.....
}
實際上,這種方法並不好,可讀性差,且記憶體佔用增加。
我們應當先編寫他們的父類,具有他們公共的資訊,然後分別繼承(這個很容易理解,就不舉例說明了)。這樣層次型
的類,簡單而且清楚,並且不包含在原來版本中所見到的樣板程式碼,沒有受到不相關的資料域的拖累,還可以反應型別之間本質的層次關係,有助於增強靈活性,更好在編譯時進行型別檢查。
8.用函式物件表示策略。
java沒有提供函式指標,但是我們可以用物件引用來實現同樣的功能。我們可以定義一個物件,它的方法執行其他物件上的操作。如果一個類僅僅匯出這樣一個方法,它的例項實際上就等同於一個指向該方法的指標,這樣的例項被稱為函式物件
。
例如
class StringLengthComparator{//該類用於比較兩個String長度
public int compare(String s1,String s2){
return s1.length() - s2.length()
}
}
我們也可以將之優化,把這個類作為一個Singleton可以說是非常合適的。
9.優先考慮靜態成員類。
如果成員類不要求訪問外圍例項,就要始終把static修飾符放在他的宣告中,使它作為靜態成員類,而不是非靜態成員類。如果省略了static修飾符,那麼每個例項都包含一個額外的指向外圍物件的引用,保留這份引用需要消耗時間和空間,並且會導致我外圍例項在符合垃圾回收時依然得以保留。