一、考慮用靜态工廠方法代替構造器:
構造器是建立一個對象執行個體最基本也最通用的方法,大部分開發者在使用某個class的時候,首先需要考慮的就是如何構造和初始化一個對象示例,而構造的方式首先考慮到的就是通過構造函數來完成,是以在看javadoc中的文檔時首先關注的函數也是構造器。然而在有些時候構造器并非我們唯一的選擇,通過反射也是可以輕松達到的。我們這裡主要提到的方式是通過靜态類工廠的方式來建立class的執行個體,如:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
靜态工廠方法和構造器不同有以下主要優勢:
1. 有意義的名稱。
在架構設計中,針對某些工具類通常會考慮dummy對象或者空對象以辨識該對象是否已經被初始化,如我曾在我的C++基礎庫中實作了String類型,見如下代碼:
void showExample() {
String strEmpty = String::empty();
String strEmpty2 = "";
String strData = String::prellocate(1024);
if (strEmpty.isEmpty()) {
//TODO: do something
}
static String String::emptyString;
String& String::empty() {
return emptyString;
bool String::isEmpty() {
if (this->_internal == &emptyString->_internal)
return true;
//TODO: do other justice to verify whether it is empty.
在上面的代碼中,提供了兩個靜态工廠方法empty和preallocate用于分别建立一個空對象和一個帶有指定配置設定空間的String對象。從使用方式來看,這些靜态方法确實提供了有意義的名稱,使用者很容易就可以判斷出它們的作用和應用場景,而不必在一組重載的構造器中去搜尋每一個構造函數及其參數清單,以找出适合目前場景的構造函數。從效率方面來講,由于提供了唯一的靜态空對象,當判讀對象執行個體是否為空時(isEmpty),直接使用預制靜态空對象(emptyString)的位址與目前對象進行比較,如果是同一位址,即可确認目前執行個體為空對象了。對于preallocate函數,顧名思義,該函數預配置設定了指定大小的記憶體空間,後面在使用該String執行個體時,不必擔心指派或追加的字元過多而導緻頻繁的realloc等操作。
2. 不必在每次調用它們的時候建立一個新的對象。
還是基于上面的代碼執行個體,由于所有的空對象都共享同一個靜态空對象,這樣也節省了更多的記憶體開銷,如果是strEmpty2方式構造出的空對象,在執行比較等操作時會帶來更多的效率開銷。事實上,Java在String對象的實作中,使用了常量資源池也是基于了同樣的優化政策。該優勢同樣适用于單執行個體模式。
3. 可以傳回原傳回類型的任何子類型。
在Java Collections Framework的集合接口中,提供了大量的靜态方法傳回集合接口類型的實作類型,如Collections.subList()、Collections.unmodifiableList()等。傳回的接口是明确的,然而針對具體的實作類,函數的使用者并不也無需知曉。這樣不僅極大的減少了導出類的數量,而且在今後如果發現某個子類的實作效率較低或者發現更好的資料結構和算法來替換目前實作子類時,對于集合接口的使用者來說,不會帶來任何的影響。本書在例子中提到EnumSet是通過靜态工廠方法傳回對象執行個體的,沒有提供任何構造函數,其内部在傳回實作類時做了一個優化,即如果枚舉的數量小于64,該工廠方法将傳回一個經過特殊優化的實作類執行個體(RegularEnumSet),其内部使用long(64bits在Java中) 中的不同位來表示不同的枚舉值。如果枚舉的數量大于64,将使用long的數組作為底層支撐。然而這些内部實作類的優化對于使用者來說是透明的。
4. 在建立參數化類型執行個體的時候,它們使代碼變得更加簡潔。
Map<String,String> m = new HashMap<String,String>();
由于Java在構造函數的調用中無法進行類型的推演,是以也就無法通過構造器的參數類型來執行個體化指定類型參數的執行個體化對象。然而通過靜态工廠方法則可以利用參數類型推演的優勢,避免了類型參數在一次聲明中被多次重寫所帶來的煩憂,見如下代碼:
public static <K,V> HashMap<K,V> newInstance() {
return new HashMap<K,V>();
}
二、遇到多個構造參數時要考慮用建構器(Builder模式):
如果一個class在構造初始化的時候存在非常多的參數,将會導緻構造函數或者靜态工廠函數帶有大量的、類型相同的函數參數,特别是當一部分參數隻是可選參數的時候,class的使用者不得不為這些可選參數也傳入預設值,有的時候會發現使用者傳入的預設值可能是有意義的,而并非class内部實作所認可的預設值,比如某個整型可選參數,通常使用者會傳入0,然後class内部的實作恰恰認為0是一種重要的狀态,而該狀态并不是該調用者關心的,但是該狀态卻間接導緻其他狀态的改變,因而帶來了一些潛在的狀态不一緻問題。與此同時,過多的函數參數也給使用者的學習和使用帶來很多不必要的麻煩,我相信任何使用者都希望看到class的接口是簡單易用、函數功能清晰可見的。在Effective C++中針對接口的設計有這樣的一句話:"接口要完滿而最小化"。針對該類問題通常會考慮的方法是将所有的參數歸結到一個JavaBean對象中,執行個體化這個Bean對象,然後再将執行個體化的結果傳給這個class的構造函數,這種方法仍然沒有避免預設值的問題。該條目推薦了Builder模式來建立這個帶有很多可選參數的執行個體對象。
class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
//對象的必選參數
private final int servingSize;
private final int servings;
//對象的可選參數的預設值初始化
private int calories = 0;
private int fat = 0;
private int carbohydrate = 0;
private int sodium = 0;
//隻用少數的必選參數作為構造器的函數參數
public Builder(int servingSize,int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
public Builder fat(int val) {
fat = val;
public Builder carbohydrate(int val) {
carbohydrate = val;
public Builder sodium(int val) {
sodium = val;
public NutritionFacts build() {
return new NutritionFacts(this);
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
//使用方式
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100)
.sodium(35).carbohydrate(27).build();
System.out.println(cocaCola);
對于Builder方式,可選參數的預設值問題也将不再困擾着所有的使用者。這種方式還帶來了一個間接的好處是,不可變對象的初始化以及參數合法性的驗證等工作在構造函數中原子性的完成了。
Map<String,String> m = MyHashMap.newInstance();
三、用私有構造器或者枚舉類型強化Singleton屬性:
對于單執行個體模式,相信很多開發者并不陌生,然而如何更好更安全的建立單執行個體對象還是需要一些推敲和斟酌的,在Java中主要的建立方式有以下三種,我們分别作出解釋和适當的比較。
1. 将構造函數私有化,直接通過靜态公有的final域字段擷取單執行個體對象:
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elivs() { ... }
public void leaveTheBuilding() { ... }
這樣的方式主要優勢在于簡潔高效,使用者很快就能判定目前類為單執行個體類,在調用時直接操作Elivs.INSTANCE即可,由于沒有函數的調用,是以效率也非常高效。然而事物是具有一定的雙面性的,這種設計方式在一個方向上走的過于極端了,是以他的缺點也會是非常明顯的。如果今後Elvis的使用代碼被遷移到多線程的應用環境下了,系統希望能夠做到每個線程使用同一個Elvis執行個體,不同線程之間則使用不同的對象執行個體。那麼這種建立方式将無法實作該需求,是以需要修改接口以及接口的調用者代碼,這樣就帶來了更高的修改成本。
2. 通過公有域成員的方式傳回單執行個體對象:
public static Elvis getInstance() { return INSTANCE; }
這種方法很好的彌補了第一種方式的缺陷,如果今後需要适應多線程環境的對象建立邏輯,僅需要修改Elvis的getInstance()方法内部即可,對用調用者而言則是不變的,這樣便極大的縮小了影響的範圍。至于效率問題,現今的JVM針對該種函數都做了很好的内聯優化,是以不會産生因函數頻繁調用而帶來的開銷。
3. 使用枚舉的方式(Java SE5):
public enum Elvis {
INSTANCE;
就目前而言,這種方法在功能上和公有域方式相近,但是他更加簡潔更加清晰,擴充性更強也更加安全。
四、通過私有構造器強化不可執行個體化的能力:
我在設計自己的表達式解析器時,曾将所有的操作符設計為enum中不同的枚舉元素,同時提供了帶有參數的構造函數,傳入他們的優先級、操作符名稱等資訊。
對于有些工具類如java.lang.Math、java.util.Arrays等,其中隻是包含了靜态方法和靜态域字段,是以對這樣的class執行個體化就顯得沒有任何意義了。然而在實際的使用中,如果不加任何特殊的處理,這樣的classes是可以像其他classes一樣被執行個體化的。這裡介紹了一種方式,既将預設構造函數設定為private,這樣類的外部将無法執行個體化該類,與此同時,在這個私有的構造函數的實作中直接抛出異常,進而也避免了類的内部方法調用該構造函數。
public class UtilityClass {
//Suppress default constructor for noninstantiability.
private UtilityClass() {
throw new AssertionError();
這樣定義之後,該類将不會再被外部執行個體化了,否則會産生編譯錯誤。然而這樣的定義帶來的最直接的負面影響是該類将不能再被子類化。
五、避免建立不必要的對象:
試比較以下兩行代碼在被多次反複執行時的效率差異:
String s = new String("stringette");
String s = "stringette";
由于String被實作為不可變對象,JVM底層将其實作為常量池,既所有值等于"stringette" 的String對象執行個體共享同一對象位址,而且還可以保證,對于所有在同一JVM中運作的代碼,隻要他們包含相同的字元串字面常量,該對象就會被重用。
我們繼續比較下面的例子,并測試他們在運作時的效率差異:
Boolean b = Boolean.valueOf("true");
Boolean b = new Boolean("true");
前者通過靜态工廠方法保證了每次傳回的對象,如果他們都是true或false,那麼他們将傳回相同的對象。換句話說,valueOf将隻會傳回Boolean.TRUE或Boolean.FALSE兩個靜态域字段之一。而後面的Boolean構造方式,每次都會構造出一個新的Boolean執行個體對象。這樣在多次調用後,第一種靜态工廠方法将會避免大量不必要的Boolean對象被建立,進而提高了程式的運作效率,也降低了垃圾回收的負擔。
繼續比較下面的代碼:
public class Person {
private final Date birthDate;
//判斷該嬰兒是否是在生育高峰期出生的。
public boolean isBabyBoomer {
Calender c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
c.set(1946,Calendar.JANUARY,1,0,0,0);
Date dstart = c.getTime();
c.set(1965,Calendar.JANUARY,1,0,0,0);
Date dend = c.getTime();
return birthDate.compareTo(dstart) >= 0 && birthDate.compareTo(dend) < 0;
public class Person {
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
BOOM_START = c.getTime();
BOOM_END = c.getTime();
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
改進後的Person類隻是在初始化的時候建立Calender、TimeZone和Date執行個體一次,而不是在每次調用isBabyBoomer方法時都建立一次他們。如果該方法會被頻繁調用,效率的提升将會極為顯著。
集合架構中的Map接口提供keySet方法,該方法每次都将傳回底層原始Map對象鍵資料的視圖,而并不會為該操作建立一個Set對象并填充底層Map所有鍵的對象拷貝。是以當多次調用該方法并傳回不同的Set對象執行個體時,事實上他們底層指向的将是同一段資料的引用。
在該條目中還提到了自動裝箱行為給程式運作帶來的性能沖擊,如果可以通過原始類型完成的操作應該盡量避免使用裝箱類型以及他們之間的互動使用。見下例:
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; ++i) {
sum += i;
System.out.println(sum);
本例中由于錯把long sum定義成Long sum,其效率降低了近10倍,這其中的主要原因便是該錯誤導緻了2的31次方個臨時Long對象被建立了。
六、消除過期的對象引用:
盡管Java不像C/C++那樣需要手工管理記憶體資源,而是通過更為友善、更為智能的垃圾回收機制來幫助開發者清理過期的資源。即便如此,記憶體洩露問題仍然會發生在你的程式中,隻是和C/C++相比,Java中記憶體洩露更加隐匿,更加難以發現,見如下代碼:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copys(elements,2*size+1);
以上示例代碼,在正常的使用中不會産生任何邏輯問題,然而随着程式運作時間不斷加長,記憶體洩露造成的副作用将會慢慢的顯現出來,如磁盤頁交換、OutOfMemoryError等。那麼記憶體洩露隐藏在程式中的什麼地方呢?當我們調用pop方法是,該方法将傳回目前棧頂的elements,同時将該棧的活動區間(size)減一,然而此時被彈出的Object仍然保持至少兩處引用,一個是傳回的對象,另一個則是該傳回對象在elements數組中原有棧頂位置的引用。這樣即便外部對象在使用之後不再引用該Object,那麼它仍然不會被垃圾收集器釋放,久而久之導緻了更多類似對象的記憶體洩露。修改方式如下:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; //手工将數組中的該對象置空
return result;
由于現有的Java垃圾收集器已經足夠隻能和強大,是以沒有必要對所有不在需要的對象執行obj = null的顯示置空操作,這樣反而會給程式代碼的閱讀帶來不必要的麻煩,該條目隻是推薦在以下3中情形下需要考慮資源手工處理問題:
1) 類是自己管理記憶體,如例子中的Stack類。
2) 使用對象緩存機制時,需要考慮被從緩存中換出的對象,或是長期不會被通路到的對象。
3) 事件監聽器和相關回調。使用者經常會在需要時顯示的注冊,然而卻經常會忘記在不用的時候登出這些回調接口實作類。
七、避免使用終結方法:
任何事情都存在其一定的雙面性或者多面性,對于C++的開發者,記憶體資源是需要手工配置設定和釋放的,而對于Java和C#這種資源托管的開發語言,更多的工作可以交給虛拟機的垃圾回收器來完成,由此C++程式得到了運作效率,卻失去了安全。在Java的實際開發中,并非所有的資源都是可以被垃圾回收器自動釋放的,如FileInputStream、Graphic2D等class中使用的底層作業系統資源句柄,并不會随着對象執行個體被GC回收而被釋放,然而這些資源對于整個作業系統而言,都是非常重要的稀缺資源,更多的資源句柄洩露将會導緻整個作業系統及其運作的各種服務程式的運作效率直線下降。那麼如何保證系統資源不會被洩露了?在C++中,由于其資源完全交由開發者自行管理,是以在決定資源何時釋放的問題上有着很優雅的支援,C++中的析構函數可以說是完成這一工作的天然候選者。任何在棧上聲明的C++對象,當棧退出或者目前對象離開其作用域時,該對象執行個體的析構函數都會被自動調用,是以當函數中有任何異常(Exception)發生時,在棧被銷毀之前,所有棧對象的析構函數均會被自動調用。然而對于Java的開發者而言,從語言自身視角來看,Java本身并未提供析構函數這樣的機制,當然這也是和其資源被JVM托管有一定關系的。
在Java中完成這樣的工作主要是依靠try-finally機制來協助完成的。然而Java中還提供了另外一種被稱為finalizer的機制,使用者僅僅需要重載Object對象提供的finalize方法,這樣當JVM的在進行垃圾回收時,就可以自動調用該方法。但是由于對象何時被垃圾收集的不确定性,以及finalizer給GC帶來的性能上的影響,是以并不推薦使用者依靠該方法來達到關鍵資源釋放的目的。比如,有數千個圖形句柄都在等待被終結和回收,可惜的是執行終結方法的線程優先級要低于普通的工作者線程,這樣就會有大量的圖形句柄資源停留在finalizer的隊列中而不能被及時的釋放,最終導緻了系統運作效率的下降,甚至還會引發JVM報出OutOfMemoryError的錯誤。
Java的語言規範中并沒有保證該方法會被及時的執行,甚至都沒有保證一定會被執行。即便開發者在code中手工調用了System.gc和System.runFinalization這兩個方法,這僅僅是提高了finalizer被執行的幾率而已。還有一點需要注意的是,被重載的finalize()方法中如果抛出異常,其棧幀軌迹是不會被列印出來的。在Java中被推薦的資源釋放方法為,提供顯式的具有良好命名的接口方法,如FileInputStream.close()和Graphic2D.dispose()等。然後使用者在finally區塊中調用該方法,見如下代碼:
public void test() {
FileInputStream fin = null;
try {
fin = new FileInputStream(filename);
//do something.
} finally {
fin.close();
那麼在實際的開發中,利用finalizer又能給我們帶來什麼樣的幫助呢?見下例:
public class FinalizeTest {
//@Override
protected void finalize() throws Throwable {
try {
//在調試過程中通過該方法,列印對象在被收集前的各種狀态,
//如判斷是否仍有資源未被釋放,或者是否有狀态不一緻的現象存在。
//推薦将該finalize方法設計成僅在debug狀态下可用,而在release
//下該方法并不存在,以避免其對運作時效率的影響。
System.out.println("The current status: " + _myStatus);
} finally {
//在finally中對超類finalize方法的調用是必須的,這樣可以保證整個class繼承
//體系中的finalize鍊都被執行。
super.finalize();
八、覆寫equals時請遵守通用約定:
對于Object類中提供的equals方法在必要的時候是必要重載的,然而如果違背了一些通用的重載準則,将會給程式帶來一些潛在的運作時錯誤。如果自定義的class沒有重載該方法,那麼該類執行個體之間的相等性的比較将是基于兩個對象是否指向同一位址來判定的。是以對于以下幾種情況可以考慮不重載該方法:
1. 類的每一個執行個體本質上都是唯一的。
不同于值對象,需要根據其内容作出一定的判定,然而該類型的類,其執行個體的自身便具備了一定的唯一性,如Thread、Timer等,他本身并不具備更多邏輯比較的必要性。
2. 不關心類是否提供了“邏輯相等”的測試功能。
如Random類,開發者在使用過程中并不關心兩個Random對象是否可以生成同樣随機數的值,對于一些工具類亦是如此,如NumberFormat和DateFormat等。
3. 超類已經覆寫了equals,從超類繼承過來的行為對于子類也是合适的。
如Set實作都從AbstractSet中繼承了equals實作,是以其子類将不在需要重新定義該方法,當然這也是充分利用了繼承的一個優勢。
4. 類是私有的或是包級别私有的,可以确定它的equals方法永遠不會被調用。
那麼什麼時候應該覆寫Object.equals呢?如果類具有自己特有的“邏輯相等”概念,而且超類中沒有覆寫equals以實作期望的行為,這是我們就需要覆寫equals方法,如各種值對象,或者像Integer和Date這種表示某個值的對象。在重載之後,當對象插入Map和Set等容器中時,可以得到預期的行為。枚舉也可以被視為值對象,然而卻是這種情形的一個例外,對于枚舉是沒有必要重載equals方法,直接比較對象位址即可,而且效率也更高。
在覆寫equals是,該條目給出了通用的重載原則:
1. 自反性:對于非null的引用值x,x.equals(x)傳回true。
如果違反了該原則,當x對象執行個體被存入集合之後,下次希望從該集合中取出該對象時,集合的contains方法将直接無法找到之前存入的對象執行個體。
2. 對稱性:對于任何非null的引用值x和y,如果y.equals(x)為true,那麼x.equals(y)也為true。
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = s;
@Override public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase((CaseInsensitiveString)o).s);
if (o instanceof String) //One-way interoperability
return s.equalsIgnoreCase((String)o);
return false;
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
List<CaseInsensitiveString> l = new ArrayList<CaseInsensitiveString>();
l.add(cis);
if (l.contains(s))
System.out.println("s can be found in the List");
對于上例,如果執行cis.equals(s)将會傳回true,因為在該class的equals方法中對參數o的類型針對String作了特殊的判斷和特殊的處理,是以如果equals中傳入的參數類型為String時,可以進一步完成大小寫不敏感的比較。然而在String的equals中,并沒有針對CaseInsensitiveString類型做任何處理,是以s.equals(cis)将一定傳回false。針對該示例代碼,由于無法确定List.contains的實作是基于cis.equals(s)還是基于s.equals(cis),對于實作邏輯兩者都是可以接受的,既然如此,外部的使用者在調用該方法時也應該同樣保證并不依賴于底層的具體實作邏輯。由此可見,equals方法的對稱性是非常必要的。以上的equals實作可以做如下修改:
@Override public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase((CaseInsensitiveString)o).s);
return false;
這樣修改之後,cis.equals(s)和s.equals(cis)都将傳回false。
3. 傳遞性:對于任何非null的引用值x、y和z,如果x.equals(y)傳回true,同時y.equals(z)也傳回true,那麼x.equals(z)也必須傳回true。
public class Point {
private final int x;
private final int y;
public Point(int x,int y) {
this.x = x;
this.y = y;
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
對于該類的equals重載是沒有任何問題了,該邏輯可以保證傳遞性,然而在我們試圖給Point類添加新的子類時,會是什麼樣呢?
public class ColorPoint extends Point {
private final Color c;
public ColorPoint(int x,int y,Color c) {
super(x,y);
this.c = c;
if (!(o instanceof ColorPoint))
return super.equals(o) && ((ColorPoint)o).c == c;
如果在ColorPoint中沒有重載自己的equals方法而是直接繼承自超類,這樣的相等性比較邏輯将會給使用者帶來極大的迷惑,畢竟Color域字段對于ColorPoint而言确實是非常有意義的比較性字段,是以該類重載了自己的equals方法。然而這樣的重載方式确實帶來了一些潛在的問題,見如下代碼:
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);
if (p.equals(cp))
System.out.println("p.equals(cp) is true");
if (!cp.equals(p))
System.out.println("cp.equals(p) is false");
從輸出結果來看,ColorPoint.equals方法破壞了相等性規則中的對稱性,是以需要做如下修改:@Override public boolean equals(Object o) {
if (!(o instanceof Point))
if (!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o) && ((ColorPoint)o).c == c;
經過這樣的修改,對稱性确實得到了保證,但是卻犧牲了傳遞性,見如下代碼:
ColorPoint p1 = new ColorPoint(1,2,Color.RED);
Point p2 = new Point(1,2);
ColorPoint p1 = new ColorPoint(1,2,Color.BLUE);
if (p1.equals(p2) && p2.equals(p3))
System.out.println("p1.equals(p2) && p2.equals(p3) is true");
if (!(p1.equals(p3))
System.out.println("p1.equals(p3) is false");
再次看輸出結果,傳遞性确實被打破了。如果我們在Point.equals中不使用instanceof而是直接使用getClass呢?
if (o == null || o.getClass() == getClass())
Point p = (Point)o;
return p.x == x && p.y == y;
這樣的Point.equals确實保證了對象相等性的這幾條規則,然而在實際應用中又是什麼樣子呢?
class MyTest {
private static final Set<Point> unitCircle;
unitCircle = new HashSet<Point>();
unitCircle.add(new Point(1,0));
unitCircle.add(new Point(0,1));
unitCircle.add(new Point(-1,0));
unitCircle.add(new Point(0,-1));
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
如果此時我們測試的不是Point類本身,而是ColorPoint,那麼按照目前Point.equals(getClass方式)的實作邏輯,ColorPoint對象在被傳入onUnitCircle方法後,将永遠不會傳回true,這樣的行為違反了"裡氏替換原則"(靈活軟體開發一書中給出了很多的解釋),既一個類型的任何重要屬性也将适用于它的子類型。是以該類型編寫的任何方法,在它的子類型上也應該同樣運作的很好。
如何解決這個問題,該條目給出了一個折中的方案,既複合優先于繼承,見如下代碼:
public class ColorPoint {
//包含了Point的代理類
private final Point p;
if (c == null)
throw new NullPointerException();
p = new Point(x,y);
//提供一個視圖方法傳回内部的Point對象執行個體。這裡Point執行個體為final對象非常重要,
//可以避免使用者的誤改動。視圖方法在Java的集合架構中有着大量的應用。
public Point asPoint() {
return p;
ColorPoint cp = (ColorPoint)o;
return cp.p.equals(p) && cp.c.equals(c);
4. 一緻性:對于任何非null的引用值x和y,隻要equals的比較操作在對象中所用的資訊沒有被改變,多次調用x.equals(y)就會一緻的傳回true,或者一緻傳回false。
在實際的編碼中,盡量不要讓類的equals方法依賴一些不确定性較強的域字段,如path。由于path有多種表示方式可以指向相同的目錄,特别是當path中包含主機名稱或ip位址等資訊時,更增加了它的不确定性。再有就是path還存在一定的平台依賴性。
5. 非空性:很難想象會存在o.equals(null)傳回true的正常邏輯。作為JDK架構中極為重要的方法之一,equals方法被JDK中的基礎類廣泛的使用,是以作為一種通用的約定,像equals、toString、hashCode和compareTo等重要的通用方法,開發者在重載時不應該讓自己的實作抛出異常,否則會引起很多潛在的Bug。如在Map集合中查找指定的鍵,由于查找過程中的鍵相等性的比較就是利用鍵對象的equals方法,如果此時重載後的equals方法抛出NullPointerException異常,而Map的get方法并未捕獲該異常,進而導緻系統的運作時崩潰錯誤,然而事實上,這樣的問題是完全可以通過正常的校驗手段來避免的。綜上所述,很多對象在重載equals方法時都會首先對輸入的參數進行是否為null的判斷,見如下代碼:
if (o == null)
if (!(o instanceof MyType))
...
注意以上代碼中的instanceof判斷,由于在後面的實作中需要将參數o進行類型強轉,如果類型不比對則會抛出ClassCastException,導緻equals方法提前退出。在此需要指出的是instanceof還有一個潛在的規則,如果其左值為null,instanceof操作符将始終傳回false,是以上面的代碼可以優化為:
鑒于之上所述,該條目中給出了重載equals方法的最佳邏輯:
1. 使用==操作符檢查"參數是否為這個對象的引用",如果是則傳回true。由于==操作符是基于對象位址的比較,是以特别針對擁有複雜比較邏輯的對象而言,這是一種性能優化的方式。
2. 使用instanceof操作符檢查"參數是否為正确的類型",如果不是則傳回false。
3. 把參數轉換成為正确的類型。由于已經通過instanceof的測試,是以不會抛出ClassCastException異常。
4. 對于該類中的每個"關鍵"域字段,檢查參數中的域是否與該對象中對應的域相比對。
如果以上測試均全部成功傳回true,否則false。見如下示例代碼:
if (o == this)
if (!(o instanceof MyType))
MyType myType = (MyType)o;
return objField.equals(o.objField) && intField == o.intField
&& Double.compare(doubleField,o.doubleField) == 0
&& Arrays.equals(arrayField,o.arrayField);
從上面的示例中可以看出,如果域字段為Object對象,則使用equals方法進行兩者之間的相等性比較,如果為int等整型基本類型,可以直接比較,如果為浮點型基本類型,考慮到精度和Double.NaN和Float.NaN等問題,推薦使用其對應包裝類的compare方法,如果是數組,可以使用JDK 1.5中新增的Arrays.equals方法。衆所周知,&&操作符是有短路原則的,是以應該将最有可能不相同和比較開銷更低的域比較放在最前面。
最後需要提起注意的是Object.equals的參數類型為Object,如果要重載該方法,必須保持參數清單的一緻性,如果我們将子類的equals方法寫成:public boolean equals(MyType o);Java的編譯器将會視其為Object.equals的過載(Overload)方法,是以推薦在聲明該重載方法時,在方法名的前面加上@Override注釋标簽,一旦目前聲明的方法因為各種原因并沒有重載超類中的方法,該标簽的存在将會導緻編譯錯誤,進而提醒開發者此方法的聲明存在文法問題。
九、覆寫equals時總要覆寫hashCode:
一個通用的約定,如果類覆寫了equals方法,那麼hashCode方法也需要被覆寫。如果将會導緻該類無法和基于散列的集合一起正常的工作,如HashMap、HashSet。來自JavaSE6的約定如下:
1. 在應用程式執行期間,隻要對象的equals方法的比較操作所用到的資訊沒有被修改,那麼對這同一個對象多次調用,hashCode方法都必須始終如一地傳回同一個整數。在同一個應用程式的多次執行過程中,每次執行所傳回的整數可以不一緻。
2. 如果兩個對象根據equals(Object)方法比較是相等的,那麼調用這兩個對象中任意一個對象的hashCode方法都必須産生同樣的整數結果。
3. 如果兩個對象根據equals(Object)方法比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode方法,則不一定要産生不同的整數結果。但是程式員應該知道,給不相等的對象産生截然不同的整數結果,有可能提高散清單的性能。
如果類沒有覆寫hashCode方法,那麼Object中預設的hashCode實作是基于對象位址的,就像equals在Object中的預設實作一樣。如果我們覆寫了equals方法,那麼對象之間的相等性比較将會産生新的邏輯,而此邏輯也應該同樣适用于hashCode中散列碼的計算,既參與equals比較的域字段也同樣要參與hashCode散列碼的計算。見下面的示例代碼:
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode,int prefix,int lineNumber) {
//做一些基于參數範圍的檢驗。
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber = lineNumber && pn.prefix == prefix && pn.areaCode = areaCode;
Map<PhoneNumber,String> m = new HashMap<PhoneNumber,String>();
PhoneNumber pn1 = new PhoneNumber(707,867,5309);
m.put(pn1,"Jenny");
PhoneNumber pn2 = new PhoneNumber(707,867,5309);
if (m.get(pn) == null)
System.out.println("Object can't be found in the Map");
從以上示例的輸出結果可以看出,新new出來的pn2對象并沒有在Map中找到,盡管pn2和pn1的相等性比較将傳回true。這樣的結果很顯然是有悖我們的初衷的。如果想從Map中基于pn2找到pn1,那麼我們就需要在PhoneNumber類中覆寫預設的hashCode方法,見如下代碼:
@Override public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
在上面的代碼中,可以看到參與hashCode計算的域字段也同樣參與了PhoneNumber的相等性(equals)比較。對于生成的散列碼,推薦不同的對象能夠盡可能生成不同的散列,這樣可以保證在存入HashMap或HashSet中時,這些對象被分散到不同的散列桶中,進而提高容器的存取效率。對于有些不可變對象,如果需要被頻繁的存取于哈希集合,為了提高效率,可以在對象構造的時候就已經計算出其hashCode值,hashCode()方法直接傳回該值即可,如:
private final int myHashCode;
myHashCode = 17;
myHashCode = 31 * myHashCode + areaCode;
myHashCode = 31 * myHashCode + prefix;
myHashCode = 31 * myHashCode + lineNumber;
@Override public int hashCode() {
return myHashCode;
另外,該條目還建議不要僅僅利用某一域字段的部分資訊來計算hashCode,如早期版本的String,為了提高計算哈希值的效率,隻是挑選其中16個字元參與hashCode的計算,這樣将會導緻大量的String對象具有重複的hashCode,進而極大的降低了哈希集合的存取效率。
十、始終要覆寫toString:
與equals和hashCode不同的是,該條目推薦應該始終覆寫該方法,以便在輸出時可以得到更明确、更有意義的文字資訊和表達格式。這樣在我們輸出調試資訊和日志資訊時,能夠更快速的定位出現的異常或錯誤。如上一個條目中PhoneNumber的例子,如果不覆寫該方法,就會輸出PhoneNumber@163b91 這樣的不可讀資訊,是以也不會給我們診斷問題帶來更多的幫助。以下代碼重載了該方法,那麼在我們調用toString或者println時,将會得到"(408)867-5309"。
@Override String toString() {
return String.format("(%03d) %03d-%04d",areaCode,prefix,lineNumber);
對于toString傳回字元串中包含的域字段,如本例中的areaCode、prefix和lineNumber,應該在該類(PhoneNumber)的聲明中提供這些字段的getter方法,以避免toString的使用者為了擷取其中的資訊而不得不手工解析該字元串。這樣不僅帶來不必要的效率損失,而且在今後修改toString的格式時,也會給使用者的代碼帶來負面影響。提到toString傳回字元串的格式,有兩個建議,其一是盡量不要固定格式,這樣會給今後添加新的字段資訊帶來一定的束縛,因為必須要考慮到格式的相容性問題,再者就是推薦可以利用toString傳回的字元串作為該類的構造函數參數來執行個體化該類的對象,如BigDecimal和BigInteger等裝箱類。
這裡還有一點建議是和hashCode、equals相關的,如果類的實作者已經覆寫了toString的方法,那麼完全可以利用toString傳回的字元串來生成hashCode,以及作為equals比較對象相等性的基礎。這樣的好處是可以充分的保證toString、hashCode和equals的一緻性,也降低了在對類進行修訂時造成的一些潛在問題。盡管這不是剛性要求的,卻也不失為一個好的實作方式。該建議并不是源于該條目,而是去年在看effective C#中了解到的。