第1條:考慮使用靜態(tài)工廠方法代替構(gòu)造器
獲取類實(shí)例的兩種方法:公有的構(gòu)造器、公有的靜態(tài)工廠方法返回類的實(shí)例。
靜態(tài)工廠方法的優(yōu)勢(shì):
1.它們有名稱
當(dāng)一個(gè)類需要多個(gè)帶有相同簽名的構(gòu)造器時(shí),就用靜態(tài)工廠方法代替構(gòu)造器,并且慎重的選擇名稱以便突出它們之間的區(qū)別。
2.不必每次調(diào)用它們的時(shí)候都創(chuàng)建一個(gè)新對(duì)象
可以避免創(chuàng)建不必要的重復(fù)對(duì)象。這種方法類似于享元模式,如果程序經(jīng)常請(qǐng)求創(chuàng)建相同的對(duì)象,可以使用靜態(tài)工廠方法。
創(chuàng)建單例的時(shí)候,也用到靜態(tài)工廠方法。
3.它們可以返回原返回類型的任何子類型的對(duì)象
這比較適用于基于接口的框架。API可以返回各種子對(duì)象,同時(shí)又不會(huì)使對(duì)象的類變成公有的。這樣的API變得非常整潔。比如java.util.Collections??蛻舳擞肋h(yuǎn)不知道也不關(guān)心他們從工廠方法中得到的對(duì)象的類,而且在后續(xù)的版本中發(fā)生變化也不會(huì)對(duì)客戶端造成影響。
4.在創(chuàng)建參數(shù)化類型實(shí)例的時(shí)候,它們使代碼變得更加簡潔
//調(diào)用參數(shù)化類的構(gòu)造器時(shí),必須要提供類型參數(shù)
Map<String, List<String>> m = new HashMap<String, List<String>>();
//工具類,提供靜態(tài)工廠方法創(chuàng)建參數(shù)化類型實(shí)例
public static <K,V> HashMap<K,V> newInstance(){
return new HashMap<K,V>();
}
Map<String, List<String>> m = HashMap.newInstance();
靜態(tài)工廠方法的缺點(diǎn):
1.類如果不含公有的或者受保護(hù)的構(gòu)造器,就不能被子類化。
不能被子類化就是不能被繼承。
2.靜態(tài)工廠方法與其他的靜態(tài)方法實(shí)際上沒有任何區(qū)別。
第2條:遇到多個(gè)構(gòu)造器參數(shù)時(shí)要考慮用構(gòu)造器
遇到參數(shù)比較多時(shí),一般的構(gòu)造器(重疊構(gòu)造器),首先提供一個(gè)只有必要參數(shù)的構(gòu)造器,第二個(gè)構(gòu)造器有一個(gè)可選參數(shù),第三個(gè)有兩個(gè)可選參數(shù),以此類推……重疊構(gòu)造器模式可行,但是當(dāng)有許多參數(shù)的時(shí)候,客戶端代碼會(huì)很難寫,并且較難閱讀。
//重疊構(gòu)造器的代碼示例
public class NutritionFacts {
private final int servingSize; //(ml)
private final int servings; //(per container)
private final int calories; //
private final int fat; //(g)
private final int sodium; //(mg)
private final int carbohydrate; //(g)
public NutritionFacts (int servingSize,int servings) {
this(servingSize,servings,0);
}
public NutritionFacts (int servingSize,int servings,int calories) {
this(servingSize,servings,calories,0);
}
public NutritionFacts (int servingSize,int servings,int calories,int fat) {
this(servingSize,servings,calories,fat,0);
}
public NutritionFacts (int servingSize,int servings,int calories,int fat,int sodium) {
this(servingSize,servings,calories,fat,sodium,0);
}
public NutritionFacts (int servingSize,int servings,int calories,int fat,int sodium,int carbohydrate) {
this.servingSize=servingSize;
this.servings=servings;
this.calories=calories;
this.fat=fat;
this.sodium=sodium;
this.carbohydrate=carbohydrate;
}
}
第二種方式,即JavaBeans模式。只提供一個(gè)無參的構(gòu)造函數(shù)。然后調(diào)用setter方法來設(shè)置參數(shù)。在構(gòu)造過程中,JavaBean可能處于不一致的狀態(tài),類無法通過檢驗(yàn)參數(shù)的有效性來保證一致性。
public class NutritionFacts {
private int servingSize=-1; //(ml)
private int servings=-1; //(per container)
private int calories=0; //
private int fat=0; //(g)
private int sodium=0; //(mg)
private int carbohydrate=0; //(g)
public NutritionFacts () {}
public void setServingSize(int servingSize) {this.servingSize = servingSize;}
public void setServings(int servings) {this.servings = servings;}
public void setCalories(int calories) {this.calories = calories;}
public void setFat(int fat) {this.fat = fat;}
public void setSodium(int sodium) {this.sodium = sodium;}
public void setCarbohydrate(int carbohydrate) {this.carbohydrate = carbohydrate;}
}
第三種方式就是Builder模式。builder像個(gè)構(gòu)造器一樣,可以對(duì)其參數(shù)強(qiáng)加約束條件。build方法可以檢驗(yàn)這些約束條件。其實(shí)Builder就是一個(gè)JavaBean,只不過增加了一個(gè)build方法來檢查參數(shù)的有效性。
public class NutritionFacts {
private int servingSize=-1; //(ml)
private int servings=-1; //(per container)
private int calories=0; //
private int fat=0; //(g)
private int sodium=0; //(mg)
private int carbohydrate=0; //(g)
public static class Builder{
//Required parameters
private final int servingSize;
private final int servings;
//Optional parameters - initialized to default values
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 calories) {
this.calories = calories;
return this;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder carbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
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;
}
}
第3條:用私有構(gòu)造器或者枚舉類型強(qiáng)化Singleton屬性
1.餓漢式(類加載時(shí)創(chuàng)建實(shí)例)
//final公有的靜態(tài)成員
public class Elvis01 {
public static final Elvis01 INSTANCE = new Elvis01();
private Elvis01(){ //私用構(gòu)造器僅被調(diào)用一次
if(INSTANCE != NULL){
throw new SomeException();
}
......
}
public void leaveTheBuilding(){
System.out.println("Elvis01 leaveTheBuilding");
}
}
客戶端通過反射機(jī)制調(diào)用私有構(gòu)造器,將構(gòu)造器修改成可訪問的,從而創(chuàng)建出另一個(gè)實(shí)例。所以可以在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋出異常。
2.餓漢變種(提供靜態(tài)方法獲取實(shí)例)
//公有的成員是個(gè)靜態(tài)工廠
public class Elvis02 implements Serializable{
private static final Elvis02 INSTANCE = new Elvis02();
private Elvis02(){
}
public void leaveTheBuilding(){
System.out.println("Elvis02 leaveTheBuilding");
}
public static Elvis02 getInstance(){
return INSTANCE;
}
}
3.懶漢式
public class Elvis04 implements Serializable{
private Elvis04 instance;
private Elvis04(){
}
public Elvis04 getInstance(){
if(instance == null){
instance = new Elvis04();
}
return instance;
}
}
這是線程不安全的。
上述三種方式,在序列化的時(shí)候都一些額外的工作,否則沒有辦法保證單例,因?yàn)槊看畏葱蛄幸粋€(gè)序列化的實(shí)例的時(shí)候,都會(huì)創(chuàng)建一個(gè)新的實(shí)例。
必須聲明所有的實(shí)例域都是瞬時(shí)的(transient),并提供一個(gè)readResolve方法。
private Object readResolve(){
return INSTANCE;
}
4.枚舉單例
public enum Elvis03 implements Serializable{
INSTANCE;
public void leaveTheBuilding(){
System.out.println("Elvis03 leaveTheBuilding");
}
}
5.靜態(tài)內(nèi)部類
實(shí)現(xiàn)了懶加載,并且是線程安全的。
利用類加載機(jī)制,實(shí)現(xiàn)了線程安全:客戶端代碼調(diào)用了getInstance時(shí),JVM加載SingletonHolder,初始化靜態(tài)成員,從而實(shí)例化了instance。
public class Elvis05 implements Serializable{
private Elvis05(){
}
public Elvis05 getInstance(){
return SingletonHolder.instance;
}
private static class SingletonHolder{
private static Elvis05 instance = new Elvis05();
}
public void leaveTheBuilding(){
System.out.println("Elvis05 leaveTheBuilding");
}
}
第4條:通過私有構(gòu)造器強(qiáng)化不可實(shí)例化的能力
有些類不希望被實(shí)例化,比如說一些工具類:java.util.Collections。
在缺少顯式構(gòu)造器的情況下,編譯器會(huì)自動(dòng)提供一個(gè)缺省的公有的無參構(gòu)造器。這樣的類仍然可能被實(shí)例化。
要寫類不能被實(shí)例化,只要讓這個(gè)類只提供一個(gè)私有構(gòu)造器(private),就不能在外部被實(shí)例化。
這種做法使得這個(gè)類不能被子類化,因?yàn)樗械臉?gòu)造器都會(huì)顯式或則隱式地調(diào)用超類構(gòu)造器,在這種情況下,子類就沒有可訪問的超類構(gòu)造器可調(diào)用了。
第5條:避免創(chuàng)建不必要的對(duì)象
最好能重用對(duì)象,而不是每次需要的時(shí)候就創(chuàng)建一個(gè)相同功能的新對(duì)象。
如果對(duì)象是不可變的,它始終可以被重用。
String s = new String("stringette")//每次都會(huì)創(chuàng)建一個(gè)新的實(shí)例
String s = “stringette” //重用同一個(gè)對(duì)象
除了重用不可變的對(duì)象之外,也可以重用那些一直不會(huì)被修改的可變對(duì)象。
//書上的一個(gè)坑
public static void main(String[] args){
Long sum = 0L;
for(long i = 0 ; i < Integer.MAX_VALUE; i++){
sum += i;
}
System.out.println(sum);
}
變量sum被聲明為Long,每次執(zhí)行sum += i的時(shí)候都要自動(dòng)裝箱,創(chuàng)建了很多不必要的Long對(duì)象。要優(yōu)先使用基本類型而不是裝箱基本類型,當(dāng)心無意識(shí)的自動(dòng)裝箱。
第6條:消除過期的對(duì)象引用
Java雖然有垃圾回收機(jī)制,但是并不意味著你不用手動(dòng)去管理內(nèi)存。
只要類是自己管理內(nèi)存,就應(yīng)該警惕內(nèi)存泄漏的問題。
下面就是內(nèi)存泄漏常見的三種情形:
//簡單的棧實(shí)現(xiàn),從棧中取出一個(gè)元素
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
return elements[--size];
}
這段程序存在著“內(nèi)存泄漏”。如果一個(gè)棧先是增長的,然后再收縮。那么,從棧中彈出來的對(duì)象將不會(huì)被當(dāng)做垃圾回收。因?yàn)闂?nèi)部維護(hù)著對(duì)這些對(duì)象的“過期引用”。
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
內(nèi)存泄漏的另一個(gè)常見的來源是緩存。一旦你把對(duì)象引用放到緩存中,他就很容易被遺忘掉,從而使它不再有用很長時(shí)間內(nèi)仍然留在緩存中。
可以使用WeakHashMap,自動(dòng)清除沒有被外部引用的鍵值。(緩存本身就是為了提高數(shù)據(jù)的讀取速度,如果在緩存中沒有讀到數(shù)據(jù),再去內(nèi)存中找。這并不會(huì)影響程序的正確性,所以WeakHashMap可以加快數(shù)據(jù)讀取速度,也能夠避免出現(xiàn)內(nèi)存泄漏,因?yàn)楸蝗跻藐P(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。)
LinkedHashMap構(gòu)建LRU緩存。
第三個(gè)常見的來源:監(jiān)聽器和其他回調(diào)。客戶端通過API注冊(cè)了回調(diào),卻沒有顯式地取消注冊(cè),導(dǎo)致對(duì)象積聚。
最佳方法是只保存回調(diào)的弱引用。
第7條:避免使用終結(jié)方法
終結(jié)方法只會(huì)被調(diào)用一次。
大致描述一下finalize流程:當(dāng)對(duì)象變成(GC Roots)不可達(dá)時(shí),GC會(huì)判斷該對(duì)象是否覆蓋了finalize方法,若未覆蓋或者finalize方法已經(jīng)被執(zhí)行過,則直接將其回收。否則,若對(duì)象未執(zhí)行過finalize方法,將其放入F-Queue隊(duì)列,由一低優(yōu)先級(jí)線程執(zhí)行該隊(duì)列中對(duì)象的finalize方法。執(zhí)行finalize方法完畢后,GC會(huì)再次判斷該對(duì)象是否可達(dá),若不可達(dá),則進(jìn)行回收,否則,對(duì)象“復(fù)活”。(可對(duì)著下面的代碼看)
上述流程存在一個(gè)嚴(yán)重問題就是將fianlize方法交給一個(gè)低優(yōu)先級(jí)線程執(zhí)行,因此不能保證被及時(shí)地執(zhí)行,甚至根本不會(huì)保證被執(zhí)行。
替代方法就是提供一個(gè)顯式的終止方法。比如InputStream、OutputStream的close方法以及Timer的cancel方法。
顯式的終止方法通常與try-finally結(jié)構(gòu)結(jié)合起來使用,以確保及時(shí)終止。即使有異常拋出,finally塊也始終會(huì)執(zhí)行,因此終止方法肯定會(huì)被執(zhí)行。
對(duì)象再生問題:finalize方法中,可將待回收對(duì)象賦值給GC Roots可達(dá)的對(duì)象引用,從而達(dá)到對(duì)象再生的目的。
public class GC {
public static GC SAVE_HOOK = null;
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new GC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (null != SAVE_HOOK) { //此時(shí)對(duì)象應(yīng)該處于(reachable, finalized)狀態(tài)
System.out.println("Yes , I am still alive");
} else {
System.out.println("No , I am dead");
}
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (null != SAVE_HOOK) {
System.out.println("Yes , I am still alive");
} else {
System.out.println("No , I am dead");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("execute method finalize()");
SAVE_HOOK = this;
}
}
代碼執(zhí)行結(jié)果
execute method finalize()
Yes , I am still alive
No , I am dead
終結(jié)方法有兩種合理用途:
1.終結(jié)方法可以充當(dāng)安全網(wǎng),如果對(duì)象的所有者忘記調(diào)用了顯式的終止方法,那么終結(jié)方法可以充當(dāng)安全網(wǎng),遲一點(diǎn)釋放資源總比不釋放資源要好。同時(shí)輸出日志提示用戶去調(diào)用顯式的終止方法。
2.終止非關(guān)鍵的本地資源。如果是關(guān)鍵資源,必須調(diào)用終止方法。