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

Thinking in java 太厚了,我不想看,所以先拿EJ開坑。
Effective Java 和 Thinking in java都是java基礎評分超高的書,所以有必要看一看。
Effective Java這本書被java之父推薦,所以特意買了本正版。

我很希望10年前就擁有這本書。可能有人認為我不需要任何Java方面的書籍,但是我需要這本書。

——Java 之父 James Gosling

當然,這本書是2009年出版的,已經過去N多個年頭,所以帶著“批判”的眼光來瞻仰這本巨作。

ps:中文版的翻譯真是爛的可以,英文厲害的強烈推薦看?英文原版,另外,新手還是先多搬磚,或者去看看java核心技術上下兩卷,這本書不適合0基礎。

0x01 靜態工廠方法代替構造器

用靜態工廠方法代替構造器有什么好處呢?

  1. 靜態工廠方法有方法名稱,可以更確切的針對對象?進行不同的構造如構造素數和正整數,可以在同一個類里放兩個不同的工廠方法,起兩個有意義的名字
  2. 工廠方法不一定每次都會創建一個新對象,如果配合單例,會有更好的性能提升(這個視場景而定)
  3. 可以返回子類型的對象,具有更高的靈活性。
  4. 在創建?參數化實例時,代碼更加簡潔(這個書中使用了泛型作為例子,現在java已經有了類型推斷的能力,所以看情況咯)
//以前
 Map<String,List<String>> m = new HashMap<String,List<String>>();
//現在
Map<String,List<String>> m = new HashMap<>();
 //工廠
public static <K,V> HashMap<K,V> newIncetance(){
    return new HashMap<K,V>();
}

當然工廠方法也有缺點

  1. 如果類中沒有public 或者protected的構造器,就不能子類化
  2. 工廠方法和其他靜態方法沒有區別,所以不能作為特殊的方法對待

0x02 構造器

上面提到的?工廠方法和類的構造函數對多個可選參數?時,劣勢就明顯出來了,一個方法中包含多個參數對調用方是非常不友好的,這時候可以考慮使用構造器(Builder)
放上書中的實例

// Builder Pattern
public 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 {
        // 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 sodium       = 0;
        private int carbohydrate = 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;             return this; }
        public Builder sodium(int val)
            { sodium = val;          return this; }
        public Builder carbohydrate(int val)
            { carbohydrate = val;    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;
    }
}

這樣調用方就可以非常方便的構造出帶有不同屬性的對象了,這些屬性都是可選的,而且可讀性?非常好

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();

但是Builder模式?也不是完美的,創建對象前需要先創建Builder對象,而且Builder代碼相對比較多,并不適合參數少的時候使用。但是一個類如果要考慮以后擴展屬性,最好一開始就使用Builder模式,因為?屬性越多,靜態工廠和構造函數就越難控制。

0x03 單例模式

單例模式一般面試被問到的可能性比較高,什么飽漢餓漢的區別,幾種單例模式的寫法,單例模式的好處自然是很多的,對只需要被實例化一次的類,最好使用單例模式
一般單例模式的類的構造器被私有化,并且加了一重或者兩重判斷來保障線程安全,并且有的寫法還在構造器中添加防止反射強制實例化的代碼。
關于單例模式的幾種寫法我就不寫了,網上有。
Effective Java上介紹的單例模式的代碼

// 單例模式靜態成員變量
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }

    public void leaveTheBuilding() { ... }
}
// 靜態工廠方法
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE }

    public void leaveTheBuilding() { ... }
}
// 枚舉單例模式
public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() { ... }
}

另外為了防止反序列化出假冒的單利的對象,書上說要加上這么一句。具體的牽扯到后面內容,坑以后填。

// readResolve method to preserve sigleton property
private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator.
    return INSTANCE;
}

0x04 私有構造器強化 禁止工具類被實例化

Math類,Arrays類這些工具類是不應該被實例化的,因為里面的方法都是靜態的。并且沒有實例化的意義,實例化反而會浪費內存。所以編寫這類Java類的時候,最好將構造器私有化。
當實例化這些類的時候,應該有異常拋出。

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

0x05 不重復創建對象

書中建議相同功能的對象只需要創建一次就行了,不需要多次創建,另外要盡可能的重用對象,高效運用內存。

String s = new String("stringette"); // 別這么干!

上面的代碼會在常量池創建一個String,再在用構造器在堆里創建一個String,相當于兩次創建,最好是這樣的寫法

String s = "stringette";

另外不變的常亮最好事先加載,不要每次使用對象的時候重新創建。書中用了一個例子,判斷一個人是不是1946年至1964年生的

public class Person {
    private final Date birthDate;

    // 2B程序員的寫法
    public boolean isBabyBoomer() {
        // 沒有必要每次都創建Calendar對象
        Calendar gmtCal = 
            Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1964, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= 0 && 
               birthdate.compareTo(boomEnd)   <  0;
    }
}

很明顯,下面代碼會好很多,如果你看不出來,請回爐重學java

public class Person {
    private final Date birthDate;

    /**
     * 正經程序員的寫法
     */
    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(1964, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(boomStart) >= 0 && 
               birthdate.compareTo(boomEnd)   <  0;
    }
}

在這一節,還講到了拆裝箱對程序性能的影響

public static void main(String[] args) {
    Long sum = 0;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}

因為將long的第一個字母大寫了,導致程序慢了近40秒。
并不是所有創建對象的開銷都很大,但是重復創建是很浪費的,內存就那么大,CPU速度也有上限,無意義的拆裝箱浪費了性能。所以養成好習慣,節約內存。

另外書中說自己維護對象池是個費力不討好的事情,如果能交給GC,請務必交給GC。否則代碼后期維護將是一個很重量級的工作。

0x06 及時丟棄無用對象

首先看一段代碼,是一種棧的實方式

// 這里藏著一個內存泄漏的隱患
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.copayOf(elements, 2 * size + 1);
    }
}

寫C/C++的程序員在對象失去作用的時候,會把對象置空。這個是好辦法,但是java里沒有必要這樣,所以程序員對釋放資源松懈了.
棧彈出元素后,需要解除對這個元素的引用,否則有可能會導致內存泄漏。

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 置空引用
    return result;
}

java的緩存是一個內存泄漏的高發地段,因為緩存的對象會被遺忘,最后即使不用也放在緩存中,所以現在的帶緩存的功能都有一個時間機智,一段時間不用后,自動回收。

另外回調函數也有可能造成內存泄漏。如果一個對象注冊了回調,但是還沒等到回調這個對象就被干掉了,這時候,回調的時候就出現了內存泄漏問題。所以注冊回調的時候最好是弱引用。減少內存泄漏的概率

0x07 盡量別使用finalizer方法

書中說了很多,概括一下就是

  • 使用這個方法不知道什么時候會被調用,甚至不會執行,容易造成內存泄漏
  • 即使被調用了,不一定會讓你得到想要的結果,比如打印異常日志。
  • 如果是多線程,分布式系統,容易導致系統崩潰。(線程被鎖住,宕機)
  • 嚴重的性能損耗
    所以能在對象回收前做完的,不要等到對象失去引用后再做!能不用finalize()方法就不用。

至于好處嘛,finalizer方法的確有兩個優點
當對象的所有者忘記調用前面段落中建議的顯式終止方法時,可以作為保險方法
另一種是對GC不知道的對象進行保險回收操作,比如Native Peer對象。

FileInputStreamFileOutputStreamTimerConnection),都具有終結方法,當它們的close方法未能被調用的情況下,終結方法提供了一層保障。
書上說:

總之,除非是作為安全網,或者是為了終止非關鍵的本地資源,否則請不要使用終結方法。在這些很少見的情況下,既然使用了終結方法,就要記住調用super.finalize。如果終結方法作為安全網,要記得記錄終結方法的非法用法。最后,如果需要把終結方法與公有的非final類關聯起來,請考慮使用終結方法守衛者,以確保即使子類的終結方法未能調用super.finalize,該終結方法也會被執行。

love & peace

轉載請注明出處:https://micorochio.github.io/2017/07/28/reading-effective-java-01/

如若有誤請幫忙指正,謝謝

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容