Effective java 讀書筆記(一)

使用API編寫程序的程序員被稱爲該API的用戶(User),在類的實現中使用了API的類成爲該API的客戶(Client)。

1.考慮用靜態工廠方法代替構造器,靜態工廠方法(static factory method)

  • 類可以通過靜態工廠方法來提供它的客戶端,而不是通過構造器
  • 提供靜態工廠方法而不是共有的構造器

靜態工廠方法與構造器相比優勢有:

1.不必每次調用它們的時候都創建一個新對象,這使得不可變類,可以使用預先構建好的實例,或者將構建好的實例緩存起來,進行重複利用,從而避免創建不必要的重複對象。如果程序經常請求創建相同的對象,並且創建對象的代價很高,則這項技術可以極大地提升性能

2.靜態方法能夠爲重複的調用返回相同的對象,這樣有助於類總能嚴格控制在某個時刻哪些實例應該存在,這種類被稱作“實例受控的類”(instance-controlled)

3.編寫實例受控的類有幾個原因:保證它是一個Singleton或者是不可實例化,它還使得不可變的類可以確保不會存在兩個相等的實例

4.靜態工廠方法與構造器方法相比,優勢還在於,它們可以返回原返回類型的任何子類型的對象。

5.在創建參數化實例時,它們使代碼更加簡潔

靜態工廠方法和構造器方法相比缺點有:

1.類如果不含公有的或者受保護的構造器,就不能被子類化

2.在API文檔中,靜態工廠方法沒有像構造器那樣在API中明確地標識出來,因此,對於提供了靜態工廠方法而不是構造器的類來說,要想查明如何實例化一個類,這是非常困難的

2.遇到多個構造器參數時考慮用builder模式

靜態工廠和構造器有個共同的侷限性,它們都不能很好地擴展到大量的可選參數。爲了滿足這種需求,可以使用:

1.重疊構造器(telescoping constructor)模式,但是缺點是,當有許多參數時,客戶端代碼會很難編寫,且難以閱讀

2.javaBean模式,但缺點是構造過程中javaBean可能出於不一致的狀態,類無法僅僅通過檢驗構造器參數的有效性來保證一致性

3.Builder模式,既能保證像重疊構造器模式那樣的安全性,也能保證像javaBean模式那麼好的可讀性。它不直接生成想要的對象,而讓客戶端利用所有必要的參數調用構造器(或者靜態工廠),得到一個builder對象。然後客戶端在builder對象上調用類似於setter的方法,來設置每個相關的可選參數。最後,客戶端調用無參的build方法來生成不可變的對象。這個builder是它構建的類的靜態成員類。另外,一個明顯的優勢是,builder利用單獨的方法來設置每個參數,你想要多少個可變參數,它們就可以有多少個,直到每個setter方法都有一個可變參數,

4.builder模式十分靈活,可以利用單個builder構建多個對象。builder的參數可以再創建對象期間進行調整,也可以隨着不同的對象而改變,builder可以自動填充某些域,例如每次創建對象時自動增加序列號。

5.簡而言之,如果類的構造器或者靜態工廠中具有多個參數,設計這種類時,Builder模式就是中不錯的選擇,特別是當大多數參數都是可選參數的時候。與使用傳統的重疊構造器模式相比,使用builder模式的客戶端代碼將更易於閱讀和編寫,構建器也比javaBean更加安全。

3.用私有構造器或者枚舉類型強化Singleton屬性

1.使類成爲Singleton會使它的客戶端測試變的十分困難,因爲無法給Singleton替換模擬實現,除非它實現一個充當其類型的接口。

2.私有構造器僅被調用一次,客戶端的任何行爲都不會改變這一點,但要提醒一點:享有特權的客戶端可以藉助AccessObject.setAccessible方法,通過反射機制調用私有構造器。如果需要抵禦這種攻擊,可以修改該構造器,讓它在被要求創建第二個實例的時候拋出異常。

3.需要注意的是,爲了實現Singleton類可序列化(Serializable),僅僅在聲明中加上“implements Serializable”是不夠的,爲了維護並保證Singleton,必須聲明所有實例域都是瞬時(transient)的,並提供一個readResolve方法。否則,每次反序列化一個序列化實例時,都會創建一個新的實例。

4.從Java1.5發行版本起,實現Singleton還有第三種方法。只需編寫一個包含單個元素的枚舉類型。

5.使用單個元素的枚舉類型來保證Singleton的方法,更加簡潔,且無償地提供了序列化機制,絕對防止多次實例化,即使是在面對複雜的序列化或者反射攻擊的時候。雖然這種方法還沒有廣泛採用,但是它已經成爲實現Singleton的最佳方法。

4.通過私有構造器強化不可實例化的能力

1.有時候,可能需要編寫只包含靜態方法和靜態域的類,如:java.lang.Math或者java.util.Arrays,這樣的工具類,不希望被實例化,實例對它沒有任何意義。然而,在缺少顯式構造器的情況下,編譯器會自動提供一個公有的、無參的缺省構造器(default constructor)。對於用戶而言,這個構造器與其他的構造器沒有任何區別。

2.企圖通過將類做成抽象類來強化該類不可被實例化是行不通的

3.由於只有當類不包含顯式的構造器時,編譯器纔會生成缺省的構造器,因此我們只要讓這個類包含私有構造器,它就不被實例化了:

	// Noninstantiable utility class
	public class UtilityClass {
		// Suppress default constructor for noninstantiability
		private UtilityClass() {
			throw new AssertionError();
		}
		// ...
		// Remainder omitted
	}

其中,AssertionError不是必須的,但是它可以避免不小心在類的內部調用構造器。它保證該類在任何情況下都不會被實例化。這種習慣用法有點違背直覺,好像構造器就是專門設計成不能被調用一樣,因此,明智的做法就是在代碼中增加一條註釋,如上代碼所示。

5.避免創建不必要的對象

package com.pansoft.staticmethod;

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

public class Person {
	private final Date birthDate = null;

	// Other fields, methods, and constructor omitted
	// DON'T DO THIS!
	public boolean isBabyBoomer() {
		// Unnecessary allocation of expensive object
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomStart = gmtCal.getTime();
		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomEnd = gmtCal.getTime();
		return birthDate.compareTo(boomStart) >= 0
				&& birthDate.compareTo(boomEnd) < 0;
	}

	private final Date birthDate2 = null;
	// Other fields, methods, and constructor omitted
	/**
	 * The starting and ending dates of the baby boom.
	 */
	private static final Date BOOM_START;
	private static final Date BOOM_END;
	static {
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_START = gmtCal.getTime();
		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_END = gmtCal.getTime();
	}

	public boolean isBabyBoomer2() {
		return birthDate2.compareTo(BOOM_START) >= 0
				&& birthDate2.compareTo(BOOM_END) < 0;
	}
}

除此之外,在Java1.5發行版本中,有一種創建“多餘”對象的新方法,乘坐自動裝箱(autoboxing),它允許程序員將基本類型和裝箱基本類型(Boxed Primitive Type)混用,按需要自動裝箱和拆箱。自動裝箱使得基本類型和裝箱基本類型之間的差別變的模糊起來,但是並沒有完全消除。非常值得關注的一點,使用自動裝箱的性能遠低於使用基本類型的性能。要當無意識的自動裝箱。

6.消除過期的對象引用

1.清空對象引用應該是一種例外,而不是一種規範行爲。消除過期引用最好的方法是讓包含該引用的變量結束其生命週期。如果你是在最緊湊的作用域範圍內定義每一個變量,這種情形就會自然而然地發生。

7.避免使用終結方法

1.終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。使用終結方法會導致行爲不穩定、降低性能,以及可移植性問題。

2.C++的程序員別告知”不要把終結方法當作是C++中析構器(destructors)的對應物“。在C++中,析構器是回收一個對象所佔用資源的常規方法,是析構器所必需的對應物。

在java中,當一個對象變得不可到達的時候,垃圾回收器會回收與該對象相關聯的存儲空間,並不需要程序員做專門的工作。

3.C++的析構器也可以被用來回收其他的非內存資源,而在java中,一般用try-finally塊來完成類似的工作。

4.終結方法的缺點是不能保證會被及時地執行,從一個對象變得不可到達開始,到它的終結方法被執行,所花費的時間是任意長的。

5.java語言規範不僅不保證終結方法會被及時地執行,而且根本就不保證他們會被執行。因此,不應該依賴終結方法來更新重要的持久狀態。

6.不要被System.gc和System.runFinalization這兩個方法所誘惑,它們確實增加了終結方法被執行的機會,但是它們並不保證終結方法一定會被執行。唯一聲稱保證終結方法被執行的方法是System.runFinalizersOnExit,以及它臭名昭著的孿生兄弟Runtime.runFinalizersOnExit。這兩個方法都有致命的缺陷,已經被廢棄了。

更多資料,請移步這裏

8.覆蓋equals時請遵守通用約定

1.我們無法在擴展可實例化的類的同時,既增加新的值組件,同時又保留equals約定,除非願意放棄面向對象的抽象所帶來的優勢

2.在java平臺類庫中,有一些類擴展了可實例化的類,並添加了新的值組件。例如,java.sql.Timestamp對java.util.Date進行了擴展,並增加了nanoseconds域。Timestamp的equals實現確實違反了對稱性,如果Timestamp和Date對象被用於同一個集合中,或者以其他方式被混合在一起,則會引起不正確的行爲。

3.Timestamp類有一個免責聲明,告誡程序員不要混合使用Date和Timestamp對象,只要你不把它們混合在一起,就不會有麻煩,除此之外沒有其他的措施可以防止你這麼做,而結果導致的錯誤將很難調試。

4.Timestamp類的這種行爲是個錯誤,不值得效仿

5.無論類是否是不可變的,都不要使equals方法依賴於不可靠的資源如果違反了這條禁令,要想滿足一致性的要求就十分困難了。如,java.net.URL的equals方法依賴於對URL中主機IP地址的比較。將一個主機名轉變成IP地址可能需要訪問網絡,隨着時間的推移,不確保會產生相同的結果。這樣會導致URL的equals方法違反equals規定,在實踐中有可能引發一些問題(遺憾的是,由於兼容性的要求,這一行爲無法被改變),除了極少數的例外情況,equals方法都應該駐留在內存中的對象執行確定的計算。

6.Float.equals方法的特殊情況:

public boolean equals(Object obj)

Compares this object against the specified object. The result is true if and only if the argument is not null and is a Float object that represents a float with the same value as the float represented by this object. For this purpose, two float values are considered to be the same if and only if the method floatToIntBits(float) returns the identical int value when applied to each.

Note that in most cases, for two instances of class Float, f1 and f2, the value of f1.equals(f2) is true if and only if

f1.floatValue() == f2.floatValue()


also has the value true. However, there are two exceptions:

  • If f1 and f2 both represent Float.NaN, then the equals method returns true, even though Float.NaN==Float.NaN has the value false.
  • If f1 represents +0.0f while f2 represents -0.0f, or vice versa, the equal test has the value false, even though 0.0f==-0.0f has the value true.


This definition allows hash tables to operate properly.

7.覆蓋equals方法時,總要覆蓋hashCode

8.不要企圖讓equals方法過於智能

9.不要將equals聲明中的Object對象替換爲其他的類型。程序員編寫出下面這樣的equals方法並不鮮見,這回使程序員花上數個小時都搞不清楚爲什麼它不能正常工作:

public boolean equals(MyClass o) {
...
}
問題在於,這個方法並沒有覆蓋Object.equals,因爲它的參數應該是Object類型,相反,它重載了(overload)Object.equals.



END



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