《Effective Java》筆記

2. 創(chuàng)建和銷毀對象

2.1 考慮用靜態(tài)方法代替構(gòu)造器

what

// 靜態(tài)方法
public static Boolean valueOf(boolean b){
    return b ? Boolean.TRUE : Boolean.FALSE;
}
// demo1 
Boolean a1 = new Boolean(true);
Boolean a2 = new Boolean(true);
Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
if (a1 == a2){
  System.out.println("==========1"); //NO
}
if (b1 == b2){
  System.out.println("==========2"); //OK
}
// demo2
 Map<String,List<String>> m = new HashMap<String,List<String>>();
 Map<String,List<String>> m = HashMap.newInstance();
 public static <K,V> HashMap<K,V> newInstance(){
   return new HashMap<K,V>();
 }

why

靜態(tài)方法相比構(gòu)造器優(yōu)點:

  • 有名稱
  • 不必每次調(diào)用時創(chuàng)建一個新的對象(此時可以用==代替equals凑兰,從而提升性能筝家,demo1)
  • 可以返回原返回類型的任何子類型對象媳瞪,從而隱藏實現(xiàn)類使api更簡潔(服務(wù)提供者框架)
  • 創(chuàng)建參數(shù)化類型實例時抒痒,利用類型推導(dǎo)(type inference)代碼更簡潔

缺點:

  • 類如果沒有公有的或受保護(hù)的構(gòu)造器,就無法被子類化

2.2 使用構(gòu)建器代替多個參數(shù)的構(gòu)造器

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;
        }


        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).build();
    }
}

2.3 使用私有構(gòu)造器或枚舉強化Singleton屬性

what

Singleton指僅被實例化一次的類该默,常依賴代表那些本質(zhì)上唯一的組件案糙。

好的單例模式要做到:

  • 防止反射,序列化送粱,高并發(fā)導(dǎo)致的單例失斖使蟆;
  • 能便捷的切換成多例模式

HOW

自己動手實現(xiàn)牛逼的單例模式

2.4 通過私有構(gòu)造器強化不可實例化的能力

防止缺省的構(gòu)造器導(dǎo)致類仍可以實例化抗俄,可以主動實現(xiàn)個無參構(gòu)造器脆丁,并在其中返回異常,這樣做的缺點是導(dǎo)致該類無法子類化

2.5 避免創(chuàng)建不必要的對象

  • 重用不可變對象String s = "a"代替String s = new String("a")
  • 重用已知不會被修改的可變對象动雹,如date
class Person {
    private final Date birthDate;

    public Person(Date birthDate) {
        // Defensive copy - see Item 39
        this.birthDate = new Date(birthDate.getTime());
    }

    // Other fields, methods

    /**
     * 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 isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0
                && birthDate.compareTo(BOOM_END) < 0;
    }
}
  • 優(yōu)先使用基本類型槽卫,而不是裝箱類型
  • 除非是重量級的大對象,如數(shù)據(jù)庫連接池胰蝠,不然沒必要去刻意維護(hù)個對象池來實現(xiàn)重用歼培,因為小對象的創(chuàng)建回收是廉價的震蒋,相比維護(hù)對象池而言(代碼亂,占用內(nèi)存)

2.6 清除過期的對象引用

為什么清除:內(nèi)存泄露躲庄,占用內(nèi)存查剖,導(dǎo)致機(jī)器性能越來越慢,甚至內(nèi)存溢出

清空對象引用應(yīng)該是一種例外噪窘,而不是規(guī)范行為笋庄,應(yīng)當(dāng)最小化變量的作用域。

哪些情況會導(dǎo)致過期引用

  • 自己管理內(nèi)存的類

  • 緩存:一旦把對象引用放到緩存里效览,就容易被遺忘无切。

    只要緩存之外存在對某個鍵的引用,該鍵才有意義丐枉,考慮使用WeakHashMap實現(xiàn)該緩存

  • 監(jiān)聽器和其他回調(diào):如注冊回調(diào)卻沒有顯式取消回調(diào)哆键。確保回調(diào)可以垃圾被垃圾回收的最佳方法是只保存弱引用(weak reference)瘦锹,例如保存成WeakHashMap的鍵

2.7 避免使用finalizer()

3. 對所有對象都通用的方法

4. 類和接口

4.1 使類和成員的可訪問性最小化

信息隱藏:模塊之間只通過他們的api通信籍嘹,一個模塊不需要知道其他模塊的實現(xiàn)細(xì)節(jié)

實例域不能是公有的,否則即放棄了對存儲在這個域中的值進(jìn)行限制的能力弯院,也因此包含公有可變域的類不是線程安全的

長度非零的數(shù)組總是可變的辱士,不要返回它或者將其聲明為公有的靜態(tài)final數(shù)組域。處理辦法是聲明成私有的听绳,然后返回不可變數(shù)組颂碘,或者數(shù)據(jù)的拷貝(clone())

4.2 在公有類中使用訪問方法而不是公有域

如果公有類暴露了數(shù)據(jù)域,那今后想改也晚了椅挣,應(yīng)該調(diào)用它的代碼已經(jīng)遍布客戶端

4.3 使可變性最小化

what

不可變類是其實例不能被修改的類头岔,實例中所有信息都在創(chuàng)建時提供,如String,基本類型的包裝類,BigInteger,BigDecimal.

不可變對象只有一個狀態(tài)鼠证,即被創(chuàng)建時的狀態(tài)

不可變對象是線程安全的峡竣,不需要同步,故可被自由的共享量九,進(jìn)而永遠(yuǎn)不需要保護(hù)性拷貝

不可變對象的缺點是每個不同的值都需要一個單獨的對象

How

為使類成為不可變适掰,遵循5條原則:


不可變類的5條原則.jpg

4.4 復(fù)合優(yōu)于繼承

繼承的缺點:

  • 打破了封裝性,在子類的實現(xiàn)依賴父類的某個特性時會是個問題荠列,其功能可能隨父類發(fā)行版本的變化而失效类浪,如HashSet中addAll()是調(diào)用add()實現(xiàn)的,所以統(tǒng)計add()元素個數(shù)時弯予,只能重寫add()方法
  • 把超類api中的缺陷傳遞到子類

導(dǎo)致問題的另一個原因戚宦,在超類在后續(xù)的發(fā)行方法中會新增加方法:

  • 子類有方法與新加的方法簽名相同但返回類型不同,則編譯報錯
  • 子類有方法與新加的方法簽名且返回類型相同锈嫩,其實現(xiàn)可能不會遵循該新方法的約定

包裝類(復(fù)合受楼,把現(xiàn)有的類變成新類的組件;新類方法中調(diào)用超類中對應(yīng)方法并返回其結(jié)果垦搬,稱為轉(zhuǎn)發(fā)方法)的缺點:

  • 不適用在回調(diào)框架(callback framework),可能會導(dǎo)致SELF問題艳汽,因為被包裝起來的對象并不知道它外面的包裝對象

只有A IS B 時猴贰,才使用繼承

4.5 接口優(yōu)于抽象類

區(qū)別:

  • 抽象類允許包含某些方法的實現(xiàn)而接口不允許;
  • 為實現(xiàn)抽象類定義的類型河狐,必須成為抽象類的子類

抽象類的弊端:

  • 破壞類層次米绕,某個類一旦實習(xí)抽象類,其所有后代都要擴(kuò)展這個新的抽象類馋艺,無論這個后代是否合適
  • 不能Mixin栅干,接口可以實現(xiàn)多個,但類只能繼承一個
  • 無法構(gòu)建非層次結(jié)構(gòu)的類型框架

抽象類優(yōu)點:

  • 可以增加已實現(xiàn)的方法

骨架實現(xiàn)(skeletal implementtation):使用抽象類實現(xiàn)接口,亦被稱作abstractInterface捐祠,可以使程序員很容易提供自己的接口實現(xiàn),對于重要的接口碱鳞,最好提供對應(yīng)的骨架實現(xiàn)類。

通過對你導(dǎo)出的每個重要接口都提供一個抽象的骨架實現(xiàn)類(skeletal implementation)類踱蛀,把接口和抽象類的優(yōu)點結(jié)合起來窿给。接口的作用仍然是定義類型,但是骨架實現(xiàn)類接管了所有與接口實現(xiàn)相關(guān)的工作(可以只實現(xiàn)需要的方法)率拒。

// 應(yīng)用骨架類實現(xiàn)自己的List
public class IntArrays {
    static List<Integer> intArrayAsList(final int[] a) {
        if (a == null)
            throw new NullPointerException();

        return new AbstractList<Integer>() {
            public Integer get(int i) {
                return a[i]; // Autoboxing (Item 5)
            }

            @Override
            public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val; // Auto-unboxing
                return oldVal; // Autoboxing
            }

            public int size() {
                return a.length;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = new int[10];
        for (int i = 0; i < a.length; i++)
            a[i] = i;
        List<Integer> list = intArrayAsList(a);

        Collections.shuffle(list);
        System.out.println(list);
    }
}

4.6 優(yōu)先考慮靜態(tài)成員類

嵌套類:

  • 定義在另一個類內(nèi)部的類崩泡,目的在于為其外圍類服務(wù),如果其可能用在其他類猬膨,應(yīng)該是頂層類角撞。
  • 分類:靜態(tài)成員類、內(nèi)部類(非靜態(tài)成員類勃痴,匿名類靴寂,局部類)

[圖片上傳失敗...(image-c90921-1553657651829)]

5. 泛型

5.1 不要在新代碼中使用原生態(tài)類型

每個泛型都會定義一個原生態(tài)類型(Raw Type),即不帶任何實際類型參數(shù)的泛型名稱召耘。例如,與List<String>對應(yīng)的原生態(tài)類型時List褐隆。原生態(tài)類型就像從類型聲明中刪除了所有泛型信息一樣污它。實際上,原生態(tài)類型List與java平臺沒有泛型之前的接口類型List完全一樣庶弃。

List<Object>是個參數(shù)化類型衫贬,表示可以包含任何對象類型的一個集合;List<?>是一個通配符類型歇攻,表示只能包含某種未知對象類型的一個集合固惯;List是原生態(tài)類型镇辉,它脫離泛型系統(tǒng)忽肛。前兩種是安全的汛骂,可以在編譯時就拋出錯誤图张,最后一種不可以适袜,它是不安全的

原因

  • 無法在編譯時就發(fā)現(xiàn)類型錯誤
  • 可讀性差

java之所以支持原生態(tài)類型是未來兼容疫萤。

參考:java 泛型之不要使用原生態(tài)類型

5.2 消除非受檢警告

如果不是確定沒關(guān)系钓丰,不要置之不理梦鉴,其可能有潛在的CLassCastException風(fēng)險

5.3 列表優(yōu)于數(shù)組

數(shù)組是協(xié)變的娜庇,如果sub是super的子類型匕得,那sub[]就是super[]的子類型,總是在運行時才發(fā)現(xiàn)類型錯誤当娱;

Object[] objectArr = new Long[1];
objectArr[0] = "11";// 運行時報錯

List<Object> objectList = new List<Long>();
//無法通過編譯,因為List不是協(xié)變的溃槐,List<Long>不會成為List<Object>的子類型

數(shù)組是具體化(reified)的科吭,它會在運行時才檢查元素類型約束;而泛型通過擦除(crasure)實現(xiàn),只在編譯時強化類型信息,并在運行時丟棄類型信息(JVM中并沒有泛型)混萝。

總之,數(shù)組是協(xié)變且可具體化的,泛型是不可變且可擦除的雄坪。因此數(shù)組提供了運行時的類型安全绳姨,但沒有編譯時的類型安全,泛型則相反阔挠。所以如果你將泛型和數(shù)組混用飘庄,并在編譯時得到警告或錯誤,用列表代替數(shù)組购撼。

6. 枚舉和注解

6.1 用enum代替int常量

枚舉:通過公有的靜態(tài)final域為每個枚舉常量導(dǎo)出實例的類跪削,是真正的final。

優(yōu)點:

  • 可讀性更好
  • 實例受控迂求,是單例的泛型化碾盐,不會被擴(kuò)展或創(chuàng)建新的實例
  • 編譯時的類型安全
  • 有獨立的命名空間,所以不同枚舉類型可以有同名常量
  • 可添加方法和域揩局,并實現(xiàn)接口

缺點:裝載和初始化枚舉時會占用空間和時間成本

應(yīng)用:

當(dāng)想給常量綁定對應(yīng)的行為時:

  • 使用抽象方法毫玖,(防止新加的常量沒有重寫對應(yīng)的行為)枚舉中的抽象方法必須被它所有常量中的具體方法所覆蓋;
  • 當(dāng)多個常量共享一個行為時,考慮策略枚舉(私有的嵌套枚舉類)
  • 抽象方法可定義在接口中付枫,再由枚舉類實現(xiàn)之烹玉,提高擴(kuò)展性
// 常量綁定抽象方法
public enum Operation {
    PLUS("+") {
        double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        double apply(double x, double y) {
            return x - y;
        }
    };
    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    abstract double apply(double x, double y);

    // Test program to perform all operations on given operands
    public static void main(String[] args) {
        double x = Double.parseDouble("1");
        double y = Double.parseDouble("1");
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
    }
}
// 枚舉綁定行為
public enum Planet {
    MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6);
    private final double mass; // In kilograms
    private final double radius; // In meters
    private final double surfaceGravity; // In m / s^2

    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;

    // Constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() {
        return mass;
    }

    public double radius() {
        return radius;
    }

    public double surfaceGravity() {
        return surfaceGravity;
    }

    public double surfaceWeight(double mass) {
        return mass * surfaceGravity; // F = ma
    }
  
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble("1000");
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p : Planet.values())
            System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
    }
}
// 策略枚舉
enum PayrollDay {
    MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(
            PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(
            PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) {
        this.payType = payType;
    }

    double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
    }

    // The strategy enum type
    private enum PayType {
        WEEKDAY {
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
                        * payRate / 2;
            }
        },
        WEEKEND {
            double overtimePay(double hours, double payRate) {
                return hours * payRate / 2;
            }
        };
        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hrs, double payRate);

        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }

    public static void main(String[] args) {
        System.out.println(PayrollDay.FRIDAY.pay(2,0.3));;
        System.out.println(PayrollDay.SUNDAY.pay(2,0.3));;
    }
}

7. 方法

7.1 檢查參數(shù)有效性

7.2 必要時進(jìn)行保護(hù)性拷貝

// 不安全
public Date start() {
  return start;
}

public Date end() {
  return end;
}
public Period(Date start, Date end) {
  if (start.compareTo(end) > 0)
    throw new IllegalArgumentException(start + " after " + end);
  this.start = start;
  this.end = end;
}
// 攻擊1
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies i
// 攻擊2
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
System.out.println(p);

// 安全
public Date start() {
  return new Date(start.getTime());
}

public Date end() {
  return new Date(end.getTime());
}
public Period(Date start, Date end) {
  this.start = new Date(start.getTime());
  this.end = new Date(end.getTime());
  if (this.start.compareTo(this.end) > 0)
    throw new IllegalArgumentException(start +" after "+ end);
}

注意:

  • 保護(hù)性拷貝是在參數(shù)有效性檢查之前進(jìn)行,因為有效性是針對檢查之后的對象
  • 對參數(shù)類型可以被不可信任方子類化的參數(shù)阐滩,不要使用clone()進(jìn)行保護(hù)性拷貝二打。因為對于非final對象如Date,clone()方法無法保證返回java.util.Date對象掂榔,它可能返回的是惡意的子類實例
  • 只要有可能继效,使用不可變對象作為對象組件,這樣就不需要保護(hù)性拷貝了
  • 保護(hù)性拷貝可能會有性能損失衅疙,如果確定客戶端可以信任莲趣,則在文檔中指明客戶端不可修改受到影響的組件

7.3 關(guān)于方法簽名的設(shè)計

避免參數(shù)超過四個的三種方法:

  • 拆成多個子方法
  • 創(chuàng)建輔助類,保存參數(shù)的分組
  • 使用Builder模式

對于參數(shù)類型饱溢,優(yōu)先使用接口而不是類喧伞,如用Map而不是HashMap;

8. 通用程序設(shè)計

8.1 需要精確的答案绩郎,不要使用float和double

System.out.println(1.03 - .42);//0.6100000000000001
System.out.println();

System.out.println(1.00 - 9 * .10);//0.09999999999999998
System.out.println();

使用BigDecimal,int或long進(jìn)行貨幣運算

需要十進(jìn)制小數(shù)點--BigDecimal

不超過9位十進(jìn)制數(shù)字--int

不超過18位十進(jìn)制數(shù)字--long

9. 異常

9.1 只針對異常情況使用異常

JVM在處理異常情況時可能性能會更低

9.2 對可恢復(fù)的情況使用受檢異常,對編譯錯誤使用運行時異常

9.3 盡量使用標(biāo)準(zhǔn)的異常

常用的異常.jpg

9.4 拋出與所在類層次對應(yīng)的異常

異常轉(zhuǎn)義:高層的實現(xiàn)應(yīng)該捕獲低層的異常潘鲫,同時拋出可以按照高層抽象進(jìn)行解釋的異常

9.5 在異常的構(gòu)造器中保存異常的細(xì)節(jié)信息

10. 并發(fā)

10.1 同步訪問共享的可變數(shù)據(jù)

正確的使用同步,可以保證沒有任何方法會看到對象處于不一致的狀態(tài)肋杖。

將可變數(shù)據(jù)限制在單個線程中溉仑,不然每個讀或者寫操作必須執(zhí)行同步

參考:Java并發(fā)編程:volatile關(guān)鍵字解析

10.2 避免過度同步

在一個被同步的區(qū)域內(nèi)部,不要調(diào)用那些外來的方法状植,如果被設(shè)計成要被覆蓋的方法浊竟,或者客戶端以函數(shù)對象形式提供的方法,因為你無法控制它們津畸。比如我在遍歷一個List振定,在同步的代碼塊內(nèi)是沒問題,但如果調(diào)用了一個外部方法肉拓,導(dǎo)致其回調(diào)刪除了List中的一個元素后频,就會報錯。

在一個被同步的區(qū)域內(nèi)部暖途,盡量少做事情卑惜,把耗時的操作移到外面去

10.3 executor和task優(yōu)于線程

現(xiàn)在關(guān)鍵的抽象不再是Thread,而是工作單元驻售,稱作任務(wù)(task)露久,有Runnable及其近親Callable兩種。執(zhí)行任務(wù)的通用機(jī)制是executor service欺栗。

executor framework 也有個替代java.util.Timer的機(jī)制抱环,即ScheduledThreadPoolExecutor .Timer是單線程的壳快,如果其唯一線程拋出未被捕獲的異常,timer就會停止運行镇草。而executor 支持多線程,并可從拋出為受檢異常的任務(wù)中恢復(fù)瘤旨。

10.4 并發(fā)工具優(yōu)于wait和notify

沒理由在新代碼中使用梯啤,如果不得不使用wait和notify:

  • 始終應(yīng)該使用wait循環(huán)模式來調(diào)用wait方法(即要在while循環(huán)內(nèi)部調(diào)用,調(diào)用wait前后測試條件的成立與否)
  • 一般用notifyAll而不是notify存哲,它可以喚醒所有需要被喚醒的線程因宇,非目標(biāo)的線程會在檢查等待條件后繼續(xù)等待。除非等待狀態(tài)的所有線程都在等待同一個條件祟偷,而每次只喚醒其中一個察滑。

10.5 線程安全的文檔化

當(dāng)一個類的實例或靜態(tài)方法被并發(fā)調(diào)用時,這個類的行為如何修肠,是該類與客戶端建立的約定的重要組成成分贺辰。

線程安全性是有多個級別的,一個類必須在文檔中清楚說明所支持的安全級別嵌施。


線程安全級別.jpg
  • 無條件線程安全類:必須把鎖對象私有化饲化,防止被客戶端訪問
  • 有條件線程安全類:文檔中必須指明哪個調(diào)用序列需要外部同步,還要指明為了執(zhí)行這些序列吗伤,必須獲得哪些鎖

10.6 慎用延遲初始化

延遲初始化降低了初始化類或者創(chuàng)建實例的開銷吃靠,但增加了訪問被延遲初始化域的開銷。

要明確是否延遲足淆,唯一的辦法是測量類在用和不用延遲時的性能差別巢块。

如果多個線程會共享同一個延遲初始化的域,那必須做好同步:

  • 對靜態(tài)域的初始化:
private static class FieldHolder {
  static final FieldType field = computeFieldValue();
}
static FieldType getField() {
  return FieldHolder.field;
}
  • 對只初始化一次的實例域的初始化:
// 雙重檢查
private volatile FieldType field4;

FieldType getField4() {
  FieldType result = field4;
  if (result == null) { // First check (no locking)
    synchronized (this) {
      result = field4;
      if (result == null) // Second check (with locking)
        field4 = result = computeFieldValue();
    }
  }
  return result;
}
  • 對重復(fù)初始化的實例域的初始化:
// 單檢查模式巧号,如果放寬到每個線程可以也初始化一次實例族奢,并且實例是不可變對象,就刪去volatile
private volatile FieldType field5;

private FieldType getField5() {
  FieldType result = field5;
  if (result == null)
    field5 = result = computeFieldValue();
  return result;
}

10.7 不要依賴于線程調(diào)度器

線程調(diào)度器:當(dāng)有多個線程可以運行時裂逐,它決定哪些線程將會運行歹鱼,以及運行多長時間。不同的操作系統(tǒng)采用的策略可能大相徑庭卜高,依賴于線程調(diào)度器的程序得滤,很可能是不可移植的。

進(jìn)而不要依賴Thread.yield或者線程優(yōu)先級赏僧,這些措施僅僅是對調(diào)度器做些暗示匿情。可以使用線程優(yōu)先級提高已能正常工作的程序的質(zhì)量薪缆,但不能用來修正一個原本不能工作的程序秧廉。

要編寫健壯的伞广、響應(yīng)良好的、可移植的多線程程序疼电,最好的辦法是確苯莱可運行線程的平均數(shù)量不明顯多于處理器數(shù)量。注意蔽豺,可運行線程的平均數(shù)量不等于線程總數(shù)量区丑,因為等待的線程并不是可運行的。

10.8 避免使用線程組

11. 序列化

序列化:把一個對象編碼成字節(jié)流

反序列化:從字節(jié)流編碼中重新構(gòu)建對象

一旦對象序列化后修陡,就可以從一臺虛擬機(jī)傳遞到另一臺虛擬機(jī)上沧侥,或者存儲到磁盤,供后續(xù)反序列化使用魄鸦。

參考:

Effective Java--序列化--你以為只要實現(xiàn)Serializable接口就行了嗎
對Java Serializable(序列化)的理解和總結(jié)

11.1 謹(jǐn)慎實現(xiàn)Serializable接口

實現(xiàn)Serializable接口的缺點

1. 類被發(fā)布后宴杀,改變類的靈活性變小

如果一個類實現(xiàn)了Serializable接口,它的字節(jié)流編碼也變成了它導(dǎo)出API的一部分拾因,它的子類都等價于實現(xiàn)了序列化旺罢,以后如果想要改變這個類的內(nèi)部表示法,可能導(dǎo)致序列化形式不兼容盾致。
如果被序列化的類沒有顯示的指定serialVersionUID標(biāo)識(序列版本UID)主经,系統(tǒng)會自動根據(jù)這個類來調(diào)用一個復(fù)雜的運算過程生成該標(biāo)識。此標(biāo)識是根據(jù)類名稱庭惜、接口名稱罩驻、所有公有和受保護(hù)的成員名稱生成的一個64位的Hash字段,若我們改變了這些信息护赊,如增加一個方法惠遏,自動產(chǎn)生的序列版本UID就會發(fā)生變化,等價于客戶端用這個類的舊版本序列化一個類骏啰,而用新版本進(jìn)行反序列化节吮,從而導(dǎo)致程序失敗,類兼容性遭到破壞判耕。

2. 更容易引發(fā)Bug和安全漏洞

一般對象是由構(gòu)造器創(chuàng)建的透绩,而序列化也是一種對象創(chuàng)建機(jī)制,反序列化也可以構(gòu)造對象壁熄。由于反序列化機(jī)制中沒有顯式的構(gòu)造器帚豪,開發(fā)者一般很容易忽略它的存在。
構(gòu)造器創(chuàng)建對象有它的約束條件:不允許攻擊者訪問正在構(gòu)造過程中的對象內(nèi)部信息草丧,而用默認(rèn)的反序列化機(jī)制構(gòu)造對象過程中狸臣,很容易遭到非法訪問,使構(gòu)造出來的對象昌执,并不是原始對象烛亦,引發(fā)程序Bug和其他安全問題诈泼。

3. 隨著類發(fā)行新版本,相關(guān)測試負(fù)擔(dān)加重

當(dāng)一個可序列化的類被修改后煤禽,需要檢查“在新版中序列化一個實例铐达,在舊版本中反序列化”及“在舊版本中序列化一個實例,在新版本反序列化”是否正常檬果,當(dāng)發(fā)布版本增多時娶桦,這種測試量冪級增加。如果開發(fā)者早期進(jìn)行了良好的序列化設(shè)計汁汗,就可能不需要這些測試。

4. 開銷大

序列化對象時栗涂,不僅會序列化當(dāng)前對象本身知牌,還會對該對象引用的其他對象也進(jìn)行序列化。如果一個對象包含的成員變量是容器類等并深層引用時(對象是鏈表形式)斤程,此時序列化開銷會很大角寸,這時必須要采用其他一些手段處理。

無參的構(gòu)造器

若父類沒有實現(xiàn)Serializable忿墅,而子類需要序列化扁藕,需要父類有一個無參的構(gòu)造器,子類要負(fù)責(zé)序列化(反序列化)父類的域疚脐,子類要先序列化自身亿柑,再序列化父類的域。

至于為什么父類要有無參構(gòu)造器棍弄,因為父類沒有實現(xiàn)Serializable接口時望薄,虛擬機(jī)不會序列化父對象,而一個Java對象的構(gòu)造必須先有父對象呼畸,才有子對象痕支,反序列也是構(gòu)造對象的一種方法,所以反序列化時蛮原,為了構(gòu)造父對象卧须,只能調(diào)用父類的無參構(gòu)造函數(shù)作為默認(rèn)的父對象。

11.2 使用自定義的序列化形式

一個理想的序列化形式儒陨,應(yīng)該只包含該對象所表示的邏輯數(shù)據(jù)花嘶,而邏輯數(shù)據(jù)與物理表示法相互獨立。

若一個對象的物理表示法等同于它的邏輯內(nèi)容框全,則可能適合使用默認(rèn)的序列化形式察绷。一般而言,只有自定義的和默認(rèn)的形式基本相同津辩,才考慮使用默認(rèn)的拆撼。如一個表示人名的Name類容劳,從邏輯角度一個名字由姓和名組成,而Name中亦只有firstName和lastName兩個字段闸度,故是可以采用默認(rèn)形式的竭贩。

若一個對象的物理表示法與邏輯數(shù)據(jù)內(nèi)容有實質(zhì)性區(qū)別時,如下面的類:

public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
}

該類邏輯上是一個字符串序列莺禁,但物理意義是雙向鏈表形式留量,使用默認(rèn)序列化有以下4個缺點:
a) 該類導(dǎo)出API被束縛在該類的內(nèi)部表示法上,鏈表類也變成了公有API的一部分哟冬,若將來內(nèi)部表示法發(fā)生變化楼熄,仍需要接受鏈表形式的輸入,并產(chǎn)生鏈?zhǔn)叫问降妮敵觥?br> b) 消耗過多空間:像上面的例子浩峡,序列化既表示了鏈表中的每個項可岂,也表示了所有鏈表關(guān)系,而這是不必要的翰灾。這樣使序列化過于龐大缕粹,把它寫到磁盤中或網(wǎng)絡(luò)上發(fā)送都很慢;
c) 消耗過多時間:序列化邏輯并不了解對象圖的拓?fù)潢P(guān)系纸淮,所以它必須要經(jīng)過一個圖遍歷過程平斩。
d) 引起棧溢出:默認(rèn)的序列化過程要對對象圖執(zhí)行一遍遞歸遍歷,這樣的操作可能會引起棧溢出咽块。

對于StringList類绘面,可以用treansient修飾head和size變量控制其序列化,自定義writeObject,readObject進(jìn)行序列化糜芳。
具體改進(jìn)如下:

public final class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    //此類不再實現(xiàn)Serializable接口
    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }

    private final void add(String s) {
        size++;
        Entry entry = new Entry();
        entry.data = s;
        head.next = entry;
    }

    /**
     * 自定義序列化
     * @param s
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream s) throws IOException{
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    /**
     * 自定義反序列化
     * @param s
     * @throws IOException
     */
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{
        s.defaultReadObject();
        size = s.readInt();
        for (Entry e = head; e != null; e = e.next) {
            add((String) s.readObject());
        }
    }

}

(2) 如果對象狀態(tài)需要同步飒货,則對象序列化也需要同步

如果選擇使用了默認(rèn)序列化形式,就要使用下列的writeObject方法

private synchronized void writeObject(ObjectOutputStream s) throws IOException {
  s.defaultWriteObject();
}

如果把同步放在writeObject中峭竣,就必須確保它遵守與其他動作相同的鎖排列(lock-ordering)約束條件塘辅,否則由遭遇資源排列(resource-ordering)死鎖的危險

用來兩個private方法來實現(xiàn)自身序列化,這兩個函數(shù)為什么會被調(diào)用到皆撩?

writeObject: 用來處理對象的序列化扣墩,如果聲明該方法,它會被ObjectOutputStream調(diào)用扛吞,而不是默認(rèn)的序列化進(jìn)程呻惕;

readObject: 和writeObject相對應(yīng),用來處理對象的反序列化滥比。

ObjectOutputStream使用反射getPrivateMethod來尋找默認(rèn)序列化的類是否聲明了這兩個方法亚脆,所以這兩個方法必須聲明為private提供ObjectOutputStream使用。虛擬機(jī)會先試圖調(diào)用對象里的writeObject, readObject方法盲泛,進(jìn)行用戶自定義序列化和反序列化濒持,若沒有這樣的方法键耕,就會使用默認(rèn)的ObjectOutputSteam的defaultWriteObject及ObjectInputStream里的defaultReadObject方法。

關(guān)鍵字transient

(1) transient關(guān)鍵字作用是阻止變量的序列化柑营,在變量聲明前加上此關(guān)鍵字屈雄,在被反序列化時,transient的變量值被設(shè)為初始值官套,如int型是0, 對象型是null酒奶;

(2) transient關(guān)鍵字只能修飾變量,而不能修飾方法和類奶赔;

(3) 靜態(tài)變量不管是否被transient修飾惋嚎,均不能被序列化;

(4)defaultWriteObject被調(diào)用時站刑,未被標(biāo)記transient的實例域都會被序列化瘸彤,所以可以加transient的都加上

11.3 保護(hù)性地編寫readObject方法

readObject實際上相當(dāng)于另一個構(gòu)造器(不嚴(yán)格地說,用字節(jié)流作唯一參數(shù)的構(gòu)造器)笛钝,也需要檢查參數(shù)有效性,必要時作保護(hù)性拷貝愕宋。

如下面的類:

public final class Period implements Serializable {
  private Date start;
  private Date end;
  public Period(Date start, Date end) {
    this.start = new Date(start.getTime());//保護(hù)性拷貝
    this.end = new Date(end.getTime());
    if (this.start.compareTo(this.end) > 0) {
      throw new IllegalArgumentException("start bigger end");
    }
  }

  private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    start = new Date(start.getTime());//保護(hù)性拷貝
    end = new Date(end.getTime());
    if (this.start.compareTo(this.end) > 0) {
      throw new IllegalArgumentException("start bigger end");
    }
  }
}

readObject方法不可以調(diào)用可以被覆蓋的方法玻靡,因為被覆蓋的方法將在子類的狀態(tài)被反序列化之前先運行,這樣程序很可能會crash.

11.4 對于實例控制中贝,枚舉類型優(yōu)先于readObsolve

采用readObsolve方法實現(xiàn)單例序列化

對于下面的單例

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

通過序列化工具囤捻,可以將一個類的單例的實例對象寫到磁盤再讀回來,從而有效獲得一個實例邻寿。如果想要單例實現(xiàn)Serializable蝎土,任何readObject方法,不管顯示還是默認(rèn)的绣否,它會返回一個新建的實例誊涯,這個新建實例不同于該類初始化時創(chuàng)建的實例。從而導(dǎo)致單例獲取失敗蒜撮。但序列化工具可以讓開發(fā)人員通過readResolve來替換readObject中創(chuàng)建的實例暴构,即使構(gòu)造方法是私有的。在反序列化時段磨,新建對象上的readResolve方法會被調(diào)用取逾,返回的對象將會取代readObject中新建的對象。
具體方法是在類中添加如下方法就可以保證類的Singleton屬性:

//該方法忽略了被反序列化的對象苹支,只返回該類初始化時創(chuàng)建的那個Elvis實例
private Object readResolve() {
  return INSTANCE;
} 

由于Elvis實例的序列化形式不需要包含任何實際的數(shù)據(jù)砾隅,因此該類的所有的類成員(field)、帶有對象引用類型的實例域都應(yīng)該被transient修飾债蜜。

采用枚舉實現(xiàn)單例序列化

采用readResolve的一些缺點:

  1. readResolve的可訪問性需要控制好晴埂,否則很容易出問題究反。如果readResolve方法是受保護(hù)或是公有的,且子類沒有覆蓋它邑时,序列化的子類實例進(jìn)行反序列化時奴紧,就會產(chǎn)生一個超類實例,這時可能導(dǎo)致ClassCastException異常晶丘。
  2. readResolve需要類的所有實例域都用transient來修飾,除非它們都是基本數(shù)據(jù)類型黍氮,否則可能被攻擊。
    而將一個可序列化的實例受控類用枚舉實現(xiàn)浅浮,可以保證除了聲明的常量外沫浆,不會有別的實例。
    所以如果一個單例需要序列化滚秩,最好用枚舉來實現(xiàn):
public enum Elvis implements Serializable {
  INSTANCE;
  private String[] favriteSongs = {"test", "abc"};//如果不是枚舉专执,需要將該變量用transient修飾
}

11.5 考慮用序列化代理代替序列化實例

public final class Period implements Serializable {
    private final Date start;
    private final Date end;

    /**
     * @param start
     *            the beginning of the period
     * @param end
     *            the end of the period; must not precede start
     * @throws IllegalArgumentException
     *             if start is after end
     * @throws NullPointerException
     *             if start or end is null
     */
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(start + " after " + end);
    }

    public Date start() {
        return new Date(start.getTime());
    }

    public Date end() {
        return new Date(end.getTime());
    }

    public String toString() {
        return start + " - " + end;
    }

    // Serialization proxy for Period class - page 312
    private static class SerializationProxy implements Serializable {
        private final Date start;
        private final Date end;

        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }

        private static final long serialVersionUID = 234098243823485285L; // Any
                                                                            // number
                                                                            // will
                                                                            // do
                                                                            // (Item
                                                                            // 75)

        // readResolve method for Period.SerializationProxy - Page 313
        private Object readResolve() {
            return new Period(start, end); // Uses public constructor
        }
    }

    // writeReplace method for the serialization proxy pattern - page 312
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // readObject method for the serialization proxy pattern - Page 313
    private void readObject(ObjectInputStream stream)
            throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }
}
public class MutablePeriod {
    // A period instance
    public final Period period;

    // period's start field, to which we shouldn't have access
    public final Date start;

    // period's end field, to which we shouldn't have access
    public final Date end;

    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);

            // Serialize a valid Period instance
            out.writeObject(new Period(new Date(), new Date()));

            /*
             * Append rogue "previous object refs" for internal Date fields in
             * Period. For details, see "Java Object Serialization
             * Specification," Section 6.4.
             */
            byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
            bos.write(ref); // The start field
            ref[4] = 4; // Ref # 4
            bos.write(ref); // The end field

            // Deserialize Period and "stolen" Date references
            ObjectInputStream in = new ObjectInputStream(
                    new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }

    public static void main(String[] args) {
        MutablePeriod mp = new MutablePeriod();
        Period p = mp.period;
        Date pEnd = mp.end;

        // Let's turn back the clock
        pEnd.setYear(78);
        System.out.println(p);

        // Bring back the 60s!
        pEnd.setYear(69);
        System.out.println(p);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市郁油,隨后出現(xiàn)的幾起案子本股,更是在濱河造成了極大的恐慌,老刑警劉巖桐腌,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拄显,死亡現(xiàn)場離奇詭異,居然都是意外死亡案站,警方通過查閱死者的電腦和手機(jī)躬审,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟆盐,“玉大人承边,你說我怎么就攤上這事∈遥” “怎么了博助?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長痹愚。 經(jīng)常有香客問我翔始,道長,這世上最難降的妖魔是什么里伯? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任城瞎,我火速辦了婚禮,結(jié)果婚禮上疾瓮,老公的妹妹穿的比我還像新娘脖镀。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布蜒灰。 她就那樣靜靜地躺著弦蹂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪强窖。 梳的紋絲不亂的頭發(fā)上凸椿,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機(jī)與錄音翅溺,去河邊找鬼脑漫。 笑死,一個胖子當(dāng)著我的面吹牛咙崎,可吹牛的內(nèi)容都是我干的优幸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼褪猛,長吁一口氣:“原來是場噩夢啊……” “哼网杆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伊滋,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤碳却,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后笑旺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體追城,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年燥撞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迷帜。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡物舒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出戏锹,到底是詐尸還是另有隱情冠胯,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布锦针,位于F島的核電站荠察,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奈搜。R本人自食惡果不足惜悉盆,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望馋吗。 院中可真熱鬧焕盟,春花似錦、人聲如沸宏粤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至来农,卻和暖如春鞋真,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沃于。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工涩咖, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人揽涮。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓抠藕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蒋困。 傳聞我的和親對象是個殘疾皇子盾似,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容