《Effective Java》讀書筆記 —— 對象的創建和銷毀

本文主題是創建和銷毀對象,關注一下幾個問題:

  • 何時以及如何創建對象
  • 何時以及如何避免創建對象
  • 如何去報它們能夠適時銷毀
  • 如何管理對象銷毀之前必須進行的各種清理動作

1.考慮使用靜態工廠方法代替構造器(靜態工廠模式)

創建類實例的方式有兩種:

  • 公有的構造器
  • 公有的靜態工廠方法
靜態工廠方法
  • 優勢

    • 靜態工廠方法與構造器不同的第一大優勢在于,它們有名稱,如果構造器的參數本身沒有確切的描述返回的對象,那么適當名稱的靜態工廠會更加合適
    • 不必每次調用它們的時候都創建一個新對象,使得不可變對象可以使用預先構建好的實例,利用緩存實例進行復用,為重復的調用返回相同的對象,如果創建對象的代價很高,這個技術可以極大提升性能
    • 可以返回類型的任何子類型的對象,在選擇返回對象時有更大的靈活性。
      • 可以返回非公有對象,同時又不會使對象的類變成公有的,隱藏實現類
      • 公有的靜態工廠方法所返回對象的類不僅可以是非公有的,而且該類可以對著每次調用而發生變化,取決于靜態工廠方法的參數值(工廠方法模式)
    • 創建參數化類型(泛型)實例時,使代碼變得更加簡潔
  • 缺點

    • 類如果不含有公有或者受保護構造器,就不能被子類化
    • 與其他的靜態方法實際上沒有任何區別
  • 靜態工廠方法命名規范

    • valueOf
      • 該方法返回的實例與它的參數具有相同的值,這樣的靜態工廠方法實際上是類型轉換方法
    • of
    • getInstance
      • 返回的實例通過方法的參數來描述,如果沒有參數,則返回唯一的單例
    • newInstance
      • 確保返回的實例都與其他實例不同
    • getType
    • newType

2.遇到多個構造器參數時要考慮用構建器(Builder創建者模式)

靜態工廠和構造器有個共同的局限性,不能很好地擴展到大量可選參數。

處理有大量可選參數的構造器的方式:

  • 重疊構造器
  • JavaBeans 模式
  • Builder 模式
重疊構造器

提供第一個只有必要參數的構造器,第二個構造器有一個可選參數,第三個有兩個可選參數,以此類推,最后一個構造器包含所有可選參數。

public NutritionFact(int servingSize, int servings){}
public NutritionFact(int servingSize, int servings, int calories){}
public NutritionFact(int servingSize, int servings, int calories, int fat){}
public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium){}
...

缺點:重疊構造器模式可行,但是當有許多參數的時候,客戶端代碼會很難編寫和難以閱讀

JavaBeans 模式

另一種替代方法,JavaBeans 模式,調用一個無參構造器來創建對象,然后調用setter方法設置每個必要參數,以及每個相關的可選參數。

NutritionFact cocoCola = new NutritionFact();
cocoCola.setServingSize(240);
cocoCola.setServings(8);
cocoCola.setCalories(100);
cocoCola.setSodium(35);
cocoCola.setCarbohydrate(27);

缺點:

  • 構造過程被分到幾個調用中,構造過程 JavaBean 可能處于不一致的狀態。類無法僅僅通過校驗構造器參數的有效性來保證一致性
  • JavaBean 模式阻止了把類做成不可變的可能,需要確保它的線程安全
Builder 模式

不直接生成想要的對象,客戶端利用多有必要的參數調用構造器(或靜態工廠)得到一個builder對象,然后客戶端再builder 對象上調用類似setter方法,來設置每個相關的可選參數,最后客戶端調用無參的build方法來生成不可變的對象,這個builder是類的靜態成員類。

public class NutritionFacts {
    private final int calories = 0;
    private final int fat = 0;
    private final int sodium = 0;
       
    // 靜態內部類 Builder 對象 
    public static class Builder {
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
    
        // setter方法返回當前builder對象,方便鏈式調用
        public Builder setCalories(int val) {
            calories = val;
        }
          
        public Builder setFat(int val) {
            fat = val;
        }  
        
        public Builder setSodium(int val) {
            sodium = val;
        }
        
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    
    // 傳入 Builder 對象的構造方法
    public NutritionFacts(Builder builder) {
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
    }
}

缺點:需要額外開銷

總結

如果類的構造器或者靜態工廠中具有多個參數,設計這種類時,Builder 模式就是不錯的選擇。

3.用私有構造器或者枚舉類型強化Singleton屬性(單例模式)

Singleton 指僅僅被實例化一次的類。

實現Singleton的方式有很多種

方式一

把構造器保持為私有的,并導出公有的靜態成員,并且靜態成員是個final的

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {...};
    
}

問題:無法抵御通過反射調用私有構造器的攻擊。

方案:可以修改構造器,讓它在要求創建第二個實例的時候拋出異常。

方式二

方式二中,公有成員不再是屬性,而是一個靜態方法getInstance

public class Elvis {
    pvivate static final Elvis INSTANCE = new Elvis();
    private Elvis() {...};
    
    public static Elvis getInstance() {return INSTANCE;}
    
}

問題:如果此類實現了序列化,序列化之后的結果都會創建一個新的實例。
方案:重寫readResolve方法

private Object readResolve() {
    return INSTANCE;
}
方式三

編寫一個包含單個元素的枚舉類型。

public enum Elvis {
    INSTANCE;
}

與公有方法相近,但更加簡潔,無償的提供了序列化機制,并且防止序列到導致多次實例化,并且防止反射的攻擊。

總結

單元素的枚舉類型已經成為實現Singleton的最佳方法。

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

工具類不希望被實例化,實例對它沒有任何意義。

在缺少顯示構造器時,編譯器會自動提供一個公有的,無參的缺省構造器。

可通過創建私有構造器,并構造器中拋出異常,來避免實例化此類。

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

一般來說,最好能重用對象而不是在每次需要的時候就創建一個相同功能的新對象。如果對象是不可變的,那么它就應該始終被重用。

舉例一
String s = new String("mystring")  // 每次都會創建一個新的String實例
String s = "mystring"           // 推薦,保證在同一臺虛擬機中運行的代碼,只要包含相同的字符串字面常量,就會被重用
舉例二:不可變類

對于不可變類,優先使用靜態工廠方法,每次調用可以重用,避免創建不必要的對象。

舉例三

通過靜態初始化器避免在每次調用方法時都會生成一些不必要的對象。

class Person {
    static {
        // 初始化整個類需要用到的不可變可重用對象
    }
    public boolean isBaby() {
        // 這里使用到一些不可變的對象,無需每次都創建,把創建操作放到靜態初始化器中,這里直接使用即可
    }
}

缺點:如果方法沒有被調用,那么初始化工作就沒有必要,可以通過延遲初始化,即把初始化工作放到方法初次調用時。

舉例四

自動裝箱會創建出多余的對象。

sum 聲明為 Long 類型,導致循環內部構造大量大于 Long 實例。

Long sum = 0;
for (long i = 0;i < Interger.MAX_VALUE; i ++) {
    sum += i;
}

要優先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱。

舉例五

通過維護自己的對象池來避免創建對象并不會一種好的做法,除非池中的對象是非常重量級的,現代JVM實現具有高度優化的垃圾回收器,其性能很容易就超過輕量級對象池的性能。

6.消除過期的對象引用

  • 只要類自己管理內存,程序就應該警惕內存泄漏問題
  • 內存泄漏另一個場景來源是緩存
  • 另一個場景是監聽器和其他回調,如果注冊了回調,卻沒有顯式地取消注冊,那么會產生內存泄漏,確保回調立即被當做垃圾回收的最佳方法是只保持它們的弱引用。

7.避免使用終結方法

  • 介紹
    • 終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的
    • 使用終結方法導致行為不穩定,降低性能,以及可移植性問題
    • C++的析構函數可以被用來回收其他的非內存資源,Java 中,一般用try-finally塊來完成類似工作
    • 終結方法的缺點在于不能保證被及時執行,JVM會延遲執行終結方法,所以不要用來關閉已經打開的文件,程序不能依賴終結方法被執行的時間點
    • 不應該依賴終結方法來更新重要的持久狀態
    • System.gc 和 System.runFinalization 增加了終結方法執行的機會,但不能保證終結方法一定會被執行
    • System.runFinalizersOnExit 可以保證終結方法被執行,當然此方法已被廢棄
    • 如果被捕獲的異常在終結方法中被拋出,那么這種異常會被忽略
    • 使用終結方法有非常嚴重的性能損失,即使什么也不做
  • 終止資源(文件或線程資源)
    • 顯示提供一個終止方法,在實例不再需要時,調用此方法
    • 通常與try-finally結構結合,在finally中顯式調用終止方法
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容

  • Effective Java筆記一 創建和銷毀對象 第1條 考慮用靜態工廠方法代替構造器 第2條 遇到多個構造器參...
    圣騎士wind閱讀 388評論 0 2
  • 目錄 第二章 創建和銷毀對象 1 考慮用靜態工廠方法替代構造器 對于代碼來說, 清晰和簡潔是最重要的. 代碼應該被...
    高廣超閱讀 1,460評論 0 12
  • 第一章:Java程序設計概述 Java和C++最大的不同在于Java采用的指針模型可以消除重寫內存和損壞數據的可能...
    loneyzhou閱讀 1,270評論 1 7
  • 感覺時間到了。 該去實現對自己的承諾了。 我不可能把所有東西都抓在手里的。
    JabinW閱讀 229評論 0 0
  • 我有兩條內褲一條紅色一條藍色穿著紅色的,藍色的休息穿著藍色的,紅色的休息沒有哪個更重要
    wuya閱讀 117評論 0 0