Effective java 讀書筆記( 二 )

9.覆蓋equals時總要覆蓋hashCode

1.在應用程序的執行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那麼對這同一個對象調用多次,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程序的多次執行過程中,每次執行所返回的整數可以不一致

2.如果兩個對象根據equals(Object)方法比較是相等的,那麼調用這兩個對象中任意一個對象的hashCode方法都必須產生同樣的整數結果。

3.如果兩個對象根據equals(Object)方法比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode方法,則不一定要產生不同的整數結果。但是程序員應該知道,給不相等的對象產生截然不同的整數結果,有可能提高散列表(hash table)的性能。

10.始終要覆蓋toString

1.toString約定指出,“建議所有的子類都覆蓋這個方法”

2.在實際應用中,toString方法應該返回對象中包含的所有值得關注的信息

11.謹慎地覆蓋clone

1.Cloneable接口的目的是作爲對象的一個mixin接口,表明這樣的對象允許克隆(Clone),遺憾的是,它並沒有成功地達到這個目的。其主要缺陷在於,它缺少一個clone方法,Object的clone方法是受保護的。如果不藉助於反射,就不能僅僅因爲一個對象實現了Cloneable,就可以調用clone方法。

2.即使是反射調用也可能會失敗,因爲不能保證該對象一定具有可訪問的clone方法。儘管存在這樣那樣的缺陷,這項設施仍然被廣泛地使用着,因此值得我們進一步地瞭解

3.既然Cloneable並沒有包含任何方法,那麼它到底有什麼作用呢?它決定了Object中受保護的clone方法實現的行爲:如果一個類實現了Cloneable,Object的clone方法就返回該對象的逐域拷貝,否則就會拋出CloneNotSupportException異常。這是藉口的一種極端非典型的用法,也不值得效仿。

4.通常情況下,實現接口是爲了表明類可以爲它的客戶做些什麼,然而,對於Cloneable接口,它改變了超類中受保護的方法的行爲

5.如果實現Cloneable接口是要對某個類起到作用,類和它的所有超類都必須遵守一個相當複雜的、不可實施的,並且基本上沒有文檔說明的協議

6.永遠不要讓客戶去做任何類庫能夠替客戶完成的事情

7.clone架構與引用可變對象的final域的正常用法是不相兼容的,除非在原始對象和克隆對象之間可以安全地共享此可變對象。爲了使類成爲可克隆的,可能有必要從某些域中去掉final修飾符

8.Object ’s  clone method is not synchronized, so even if it is otherwise satisfactory, you may have to write a synchronized clone method that invokes super.clone()。

9.簡而言之,所有實現了Cloneable接口的類都應該用一個公有的方法覆蓋clone。此公有方法首先調用super.clone,然後修正任何需要修正的域。

10.一般情況下,這意味着要拷貝任何包含內部“深層結構”的可變對象,並用指向新對象的引用代替原來指向這些對象的引用。雖然,這些內部拷貝操作往往可以通過遞歸地調用clone來完成,但這通常並不是最佳方法。如果該類只包含基本類型的域,或者指向不可變對象的引用,那麼多半的情況是沒有域需要修正。

11.這條規則也有例外,譬如,代表序列號或者其他唯一ID值的域,或者代表對象創建時間的域,不管這些域是基本類型還是不可變的,也都需要被修正。

12.考慮實現Comparable接口

1.無法在用新的值組件擴展可實例化的類時,同時保持compareTo約定,除非願意放棄面向對象的抽象優勢。

2.針對equals的權宜之計同樣也適用於compareTo方法。如果你想爲一個實現了Compareable接口的類增加值組件,請不要擴展這個類,而是別寫一個不相關的類,其中包含第一個類的一個實例。然後提供一個“視圖(view)”方法返回這個實例。這樣既可以讓你自由地在第二個類上實現compareTo方法,同時也允許它的客戶端在必要的時候,把第二個類的實例視同第一個類的實例

3.編寫compareTo方法與編寫equals方法非常相似,但也存在幾處重大的區別。因爲Comparable接口是參數化的,而且comparable方法是靜態的類型,因此不必進行類型檢查,也不必對它的參數進行類型轉換。

4.如果一個域並沒有實現Comparable接口,或者你需要使用一個非標準的排序關係,就可以使用一個顯式的Comparator來代替。或者編寫自己的Comparator,或者使用已有的Comparator。

13.使類和成員的可訪問性最小化

1.要區別設計良好的模塊與設計不好的模塊,最重要的因素在於,這個模塊對於外部的其他模塊而言,是否隱藏其內部數據和其他實現細節。

2.儘可能地使每個類或者成員不被外界訪問

3.如果覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於超類中的訪問級別,這樣可以保證任何可使用超類的實例的地方也都可以使用子類的實例。

4.實例域決不能是公有的,因爲包含公有可變域的類並不是線程安全的。

5.With the excep-tion of public static final fields, public classes should have no public fields.Ensure that objects referenced by public static final fields are immutable.

6.除了公有靜態final域的特殊情形之外,公有類都不應該包含公有域。並且要確保公有靜態final域索引用的對象都是不可變的

14.在公有類中使用公有方法而非公有域

1.如果類是package-private類型,或者是private的嵌套類,直接暴露它的數據域並沒有本質的錯誤

2.Java平臺類庫中有幾個類違反了“公有類不應該直接暴露數據域”的告誡。顯著的例子包括java.awt包中的Point和Dimension類。它們是不值得仿效的例子,相反,這些類應該被當作反面的 警告示例

3.public classes should never expose mutable fields. It is less harmful, though still questionable, for public classes to expose immutable fields.

4.It is, however, sometimes desirable for pac kage-private or private nested classes to expose fields, whether mutable or immutable.

15.使可變性最小化

1.不可變對象本質上是線程安全的,它們不要求同步,因此它們可以被自由地共享

2.“不可變對象可以被自由地共享”導致的結果是,永遠也不需要進行保護性拷貝。實際上,你根本無需做任何拷貝,因爲這些拷貝始終等於原始的對象。因此,你不需要,也不應該爲不可變的類提供clone方法或者拷貝構造器。

3.不僅可以共享不可變對象,甚至也可以共享它們的內部信息。

4.爲了使類成爲不可變,要遵循如下規則:

(1)不要提供任何會修改對象狀態的方法(mutatetor)

(2)保證類不會被擴展,爲了防止子類化,一般做法是使這個類成爲final的,但是也有其他方法

(3)使所有的域都是final的

(4)使所有的域都成爲私有的

(5)確保對於任何可變組件的互斥訪問,如果類具有指向可變對象的域,則必須確保該類的客戶端無法獲得指向這些對象的引用。並且,永遠不要用客戶端提供的對象引用來初始化這樣的域,也不要從任何訪問方法(accessor)中返回該對象的引用。

5.在設計新的類時,選擇用靜態工廠代替公有的構造器可以讓你以後有添加緩存的靈活性,而不必影響客戶端

6.讓不可變的類編程final的另一種辦法是,讓類的所有構造器都變成私有的或者包級私有的,並添加公有的靜態工廠(static factory)來代替共有的構造器

7.當BigInteger和BigDecimal剛被編寫出來的時候,對於“不可變的類必須爲final的”還沒有得到廣泛地理解,所以它們的所有方法都有可能會被覆蓋。遺憾的是,爲了保持向後兼容,這個問題一直無法得以修正。如果你在編寫一個類,它的安全性依賴於(來自不可信客戶端的)BigInteger或者BigDecimal參數的不可變形,就必須進行檢查,以確定這個參數是否爲“真正的”BigInteger或者BigDecimal,而不是不可信任子類的實例。如果是後者的話,就必須在假設他可能是可變的前提下對它進行保護性拷貝。示例代碼:

public static BigInteger safeInstance(BigInteger val) {
if (val.getClass() != BigInteger.class)
return new BigInteger(val.toByteArray());
return val;
}

8.本條目開頭處關於不可變類的諸多規則指出,沒有方法會修改對象,並且它的所有域都必須是final的。實際上,這些規則比真正的要求更強硬了一點,爲了提高性能可以有所放鬆。事實上,這些規則比真正的要求更強硬了一點,爲了提高性能可以有所放鬆。事實上應該是這樣:沒有一個方法能夠對對象的狀態產生外部可見(external visible)的改變。然而,許多不可變的類擁有一個或者多個非final的域,它們在第一次被請求執行這些計算的時候,把一些開銷昂貴的計算結果緩存在這些域中。如果將來再次請求同樣的計算,就直接返回這些緩存的值,從而節約了重新計算所需要的開銷。這種技巧可以很好地工作,因爲對象是不可變的,它的不可變性保證了這些計算如果被再次執行,就會產生同樣的結果。

9.堅決不要爲每個get方法編寫一個相應的set方法。除非有很好的理由要讓類成爲可變的類,否則就應該是不可變的。

10.不可變的類有許多優點,唯一缺點是在特定的情況下存在潛在的性能問題

11.在Java平臺類庫中,有幾個類如java.util.Date和java.awt.Point,他們本應該是不可變的,但實際上卻不是

12.你也應該認真考慮把一些較大的值對象做成不可變的,例如String和BigInteger,只有當你確認有必要實現令人滿意的性能時,才應該爲不可變的類提供公有的可變配套類

13.對於有些類而言,其不可變性是不切實際的。如果類不能被做成是不可變的,仍然應該儘可能地限制它的可變性。降低對象的狀態數,可以更容易地分析該對象的行爲,同時降低出錯的可能性。因此,除非有令人信服的理由要使域變成非final的,否則要使每個域是final的

16複合優先於繼承

注:這裏的“繼承”指的是“實現繼承”(implementation inheritance),即一個類擴展另一個類,這裏討論的問題並不適用於接口繼承(interface inheritance),即不適用與一個類實現一個接口的時候,或者一個接口擴展另一個接口的時候

1.與方法調用不同,繼承打破了封裝性

2.如果在適合使用複合的地方使用繼承,則會不必要地暴露實現細節。這樣得到的API會把你限制在原始的實現上,永遠限定了類的性能。更爲嚴重的是,由於暴露了內部的細節,客戶端就有可能直接訪問這些內部細節。這樣至少會導致語義上的混淆。

3.再決定使用繼承而不是複合之前,還應該問自己最後一組問題。對於你正試圖擴展的類,它的API中有沒有缺陷呢?如果有,你是否願意把那些缺陷傳播到類的API中?繼承機制會把超類API中的所有缺陷傳播到子類中,而複合則允許設計新的API來隱藏這些缺陷

4.繼承的功能雖然強大,但是存在諸多問題,它也違背了封裝原則。只有當子類和超類之間確實存在子類型關係時,使用繼承纔是恰當的。即便如此,如果子類和超類處在不同的包中,並且超類並不是爲了繼承而設計的,那麼繼承將會導致脆弱性(fragility)。爲了避免這種脆弱性,可以用複合和轉發機制來代替繼承,尤其是當存在適當的接口可以實現包裝類的時候。包裝類不僅比子類更加健壯,而且功能也更加強大。

5.使用繼承導致子類脆弱的一個相關的原因是,它們的超類在後續的發行版本中可以獲得新的方法。

6.如果在擴展一個類的時候,僅僅是增加新的方法,而不是覆蓋現有的方法,你可能會認爲這是安全的。雖然這種擴展方式比較安全一些,但是也並非完全沒有風險。

7.如果超類在後續的發行版本中獲得了一個新的方法,並且不幸的是,你給子類提供了一個簽名相同但返回類型不同的方法,那麼這樣的子類將無法通過編譯。如果給子類提供的方法帶有與新的超類方法完全相同的簽名和返回類型,實際上就覆蓋了超類中的方法。此外,你的方法是否能夠遵守新的超類方法的約定,這也是很值得懷疑的,因爲當你在編寫子類方法的時候,這個約定根本沒有面試。

8.要避免上面問題的方法,可以在新的類中增加一個私有域,他引用現有類的一個實例。這種設計被稱作“複合”(composition),因爲現有的類變成了新類的一個組件。新類中的每個實例方法都可以調用被包含的現有類實例中對應的方法,並返回它的結果。這被稱爲“轉發”(forwarding),新類中的方法被稱爲“轉發方法”(forwarding method)。這樣得到的類將會非常穩固,它不依賴與現有類的實現細節。即使現有類添加了新的方法,也不會影響到新的類。爲了進行更具體的說明,請看下面的實例:

// Reusable forwarding 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;
    }
}
// Wrapper class - uses composition in place of inheritance
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
    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<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   { return s.addAll(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 <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

17.要麼爲繼承而設計並提供文檔,要麼禁用繼承

1.對於那些並非爲了安全地進行子類化而設計和編寫文檔的類,要禁止子類化。有兩種辦法可以禁止子類化。比較容易的辦法是把這個類聲明爲final的。另一種辦法是把所有的構造器都變成私有的,或者包級私有的,並增加一些公有的靜態工廠來代替構造器。

2.對於爲了繼承而設計的類,該類的文檔必須精確地描述覆蓋每個方法所帶來的影響。話句話說,該類必須有文檔說明它可以覆蓋的方法的自用性(self-use)

3.對於每個公有的或者受保護的方法或者構造器,它的文檔必須知名該方法或者構造器調用了哪些可覆蓋的方法,是以什麼順序調用的,每個調用的結果又是如何影響後續的處理過程的

4.更一般地,類必須在文檔中說明,在哪些情況下它會調用可覆蓋的方法

5.按慣例,如果方法調用到了可覆蓋的方法,在它的文檔註釋的末尾應該包含關於這些調用的描述信息。這段描述信息要以這樣的句子開頭:“This implementation ”這樣的句子不應該被認爲是在表明該行爲可能會隨着版本的變遷而改變。它意味着這段描述關注該方法的內部工作情況

6.爲了繼承而進行的設計不僅僅涉及自用模式的文檔設計。爲了是程序員能夠編寫出更加有效的子類,而無需承受不必要的痛苦,必須通過某種形式提供適當的鉤子(hook),以便能夠進入到它的內部工作流程中,這種形式可以是精心選擇的受保護的(protected)方法,也可以是受保護的域,後者比較少見

Design for inheritance involves more than just documenting patterns of self-use. To allow programmers to write efficient subclasses without undue pain, a class may have to provide hooks into its internal working in the form of judiciously chosen protected methods or, in rare instance, protected fields.

7.爲了對於繼承而設計的類,唯一的測試方法就是編寫子類,必須在發佈類之前先編寫子類對類進行測試

8.構造器決不能調用可被覆蓋的方法,無論是直接調用還是間接調用

9.如果一個爲了繼承而設計的類中實現Cloneable或者Serializable接口,就應該意識到,因爲clone和readObject方法在行爲上非常類似於構造器,所以類似的限制規則也是適用的:無論是clone還是readObject,都不可以調用可覆蓋的方法,不管是以直接還是間接的方式。

10.如果一個爲了繼承而設計的類中實現了Serializable,並且該類有一個readResolve或者writeReplace方法,就必須使readResolve或者writeReplace成爲受保護的方法,而不是私有的方法

18.接口優於抽象類

1.現有的類可以很容易被更新,以實現新的接口。一般來說,無法更新現有的類來擴展新的抽象類。如果你希望讓兩個類擴展同一個抽象類,就必須把抽象類放到類型層次(type hierarchy)的高處,以便這兩個類的一個祖先成爲它的子類。遺憾的是,這樣做會間接地傷害到類層次,迫使這個公共祖先的所有後代類都擴展這個新的抽象類,無論它對於這些後代類是否合適。

2.接口是定義mixin(混合類型)的理想選擇

3.接口允許我們構造非層次接口的類型框架

4.Skeletal implementations are useful since they reduce the effort required to implement the interface.

5,接口一旦被公開發行,並且已被廣泛實現,要想改變這個接口幾乎是不可能的,你必須在初次設計的時候就保證接口是正確的,如果接口包含微小的瑕疵,它將會一直影響你以接口的用戶。如果接口有嚴重的缺陷,它可以導致API徹底失敗

6.在發行新接口的時候,最好的做法是,在接口被“凍結”之前,儘可能讓更多的程序員用儘可能多的方式來實現這個新接口。這樣有助於在依然可以改正缺陷的時候就發現它們

7.接口通常是定義允許多個實現的類型的最佳途徑。這條規則有個例外,即當演變的容易性比靈活性和功能更加重要的時候。在這種情況下,應該使用抽象類來定義類型,但前提是必須理解並且可以接受這些侷限性。

8.如果你導出了一個重要的接口,就應該堅決考慮同時提供骨架實現類。

9.應該儘可能謹慎地設計所有的公有接口,並通過編寫多個實現來進行全面的測試

10.骨架實現類示例代碼:

// Skeletal Implementation
public abstract class AbstractMapEntry<K,V>
implements Map.Entry<K,V> {
// Primitive operations
public abstract K getKey();
public abstract V getValue();
// Entries in modifiable maps must override this method
public V setValue(V value) {
throw new UnsupportedOperationException();
}
// Implements the general contract of Map.Entry.equals
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (! (o instanceof Map.Entry))
return false;
Map.Entry<?,?> arg = (Map.Entry) o;
return equals(getKey(),   arg.getKey()) &&
equals(getValue(), arg.getValue());
}
private static boolean equals(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
// Implements the general contract of Map.Entry.hashCode
@Override public int hashCode() {
return hashCode(getKey()) ^ hashCode(getValue());
}
private static int hashCode(Object obj) {
return obj == null ? 0 : obj.hashCode();
}
}

19.接口只用於定義類型

20.類層次優於標籤類

1.標籤類很少有適用的時候。當你想要編寫一個包含顯示標籤類的時候,應該考慮一下,這個標籤是否可以被取消,這個類是否可以用類層次來代替
2.標籤類示例:
package com.example.activitys;

//Tagged class - vastly inferior to a class hierarchy!
class Figure {
	enum Shape {
		RECTANGLE, CIRCLE
	};

	// Tag field - the shape of this figure
	final Shape shape;
	// These fields are used only if shape is RECTANGLE
	double length;
	double width;
	// This field is used only if shape is CIRCLE
	double radius;

	// Constructor for circle
	Figure(double radius) {
		shape = Shape.CIRCLE;
		this.radius = radius;
	}

	// Constructor for rectangle
	Figure(double length, double width) {
		shape = Shape.RECTANGLE;
		this.length = length;
		this.width = width;
	}

	double area() {
		switch (shape) {
		case RECTANGLE:
			return length * width;
		case CIRCLE:
			return Math.PI * (radius * radius);
		default:
			throw new AssertionError();
		}
	}
}
3.類層次代碼示例:
//Class hierarchy replacement for a tagged class
abstract class Figure {
	abstract double area();
}

class Circle extends Figure {
	final double radius;

	Circle(double radius) {
		this.radius = radius;
	}

	double area() {
		return Math.PI * (radius * radius);
	}

}

class Rectangle extends Figure {
	final double length;
	final double width;

	Rectangle(double length, double width) {
		this.length = length;
		this.width = width;
	}

	double area() {
		return length * width;
	}
}
4.當你遇到一個包含標籤域的現有類時,就要考慮將它重構到一個層次結構中去

21.用函數對象表示策略

1.函數指針的主要作用就是實現策略模式,爲了在Java中實現這種模式,要聲明一個接口表示該策略,並且爲每個具體策略聲明一個實現了該接口的類
2.當一個具體策略只被使用一次時,通常使用匿名類來聲明和實例化這個具體策略類
3.當一個具體策略是設計用來重複使用的時候,它的類通常就要被實現爲私有的靜態成員類,並通過公有的靜態final域被導出,其類型爲該策略接口
package com.example.activitys;

import java.io.Serializable;
import java.util.Comparator;

//Exporting a concrete strategy
class Host {
	private static class StrLenCmp implements Comparator<String>, Serializable {
		public int compare(String s1, String s2) {
			return s1.length() - s2.length();
		}
	}

	// Returned comparator is serializable
	public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
	// Bulk of class omitted
}

22.優先考慮靜態成員類

1.嵌套類(nested class)存在的目的應該只是爲它的外圍類(enclosing class)提供服務
2.如果嵌套類將來可能會用於其他的某個環境,它就應該是頂層類(top-level class)
3.嵌套類有四種:靜態成員類(static member class)、非靜態成員類(nonstatic member class)、匿名類(anonymous class)和局部類(local class),除了第一種之外,其他三種都被稱爲內部類(inner calss)
4.靜態成員類是最簡單的一種嵌套類,最好把它看作是普通的類,只是碰巧被生命在另一個類的內部而已
5.靜態成員類可以訪問外圍類的所有成員,包括那些聲明爲私有的成員
6.靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性規則
7.如果靜態成員類被聲明爲私有的,它就只能在外圍類的內部纔可以被訪問
8.如果嵌套類的實例可以在它外圍類的實例之外獨立存在,這個嵌套類就必須是靜態成員類,在沒有外圍實例的情況下,要想創建非靜態成員類的實例時不可能的
9.如果只想訪問成員類,而不需要訪問外圍實例,就需要始終把該成員類聲明爲靜態成員類。如果省略了static修飾符,則每個實例都將包含一個額外的指向外圍對象的引用。而保存這份引用要消耗時間和空間,並且會導致外圍實例在符合垃圾回收時卻仍然得以保留
10.匿名類的三種常見用法:動態地創建函數對象(function object),創建過程對象(process object)以及在靜態工廠方法的內部
11.局部類是四種嵌套類中用的最少的類。在任何“可以聲明局部變量”的地方,都可以聲明局部類,並且局部類也遵守同樣的作用域規則。局部類與其他三種嵌套類中的每一種都有一些共同的屬性。與成員類一樣,局部類有名字,可以被重複地使用。與匿名類一樣,只有當局部類是在非靜態環境中定義的時候,纔有外圍實例,它們也不能包含靜態成員。與匿名類一樣,它們必須非常簡短,以便不會影響到可讀性
12.假設這個嵌套類屬於一個方法的內部,如果你只需要在一個地方創建實例,並且已經有了一個預置的類型可以說明這個類的特徵,就要把它做成匿名類;否則就做成局部類




END



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章