12.18 《Effective Java 》系列一

目錄

第二章 創建和銷燬對象 

1 考慮用靜態工廠方法替代構造器
2 遇到多個構造器參數時要考慮用構件器
3 用私有構造器或者枚舉類型強化Singleton屬性
4 通過私有構造器強化不可實例化的能力
5 避免創建不必要的對象
6 消除過期的對象引用
7 避免使用終結方法
第三章 對於對象都通用的方法
8 Equals方法
9 HashCode方法
10 ToString方法
11 Clone方法
12 Comparable接口
第四章 類和接口
13 使類和成員的可訪問性最小化
14 在公有類中使用訪問方法而非共有域
15 支持非可變性
16 複合優先於繼承
17 要麼專門為繼承而設計,並給出文檔說明,要麼禁止繼承
18 接口優於抽象
19 接口只是被用來定義類型
20 類層次優先於標籤類
21 用函數對象表示策略
22 優先考慮靜態成員類
第十章 併發
66 同步訪問共享的可變數據

67 避免過渡同步
68 executor和task優先於線程
69 併發工具優先於wait和notify
70 線程安全性的文檔化
71 甚用延遲初始化
72 不要依賴於線程調度器
73 避免使用線程組

第二章 創建和銷燬對象

1 考慮用靜態工廠方法替代構造器

對於代碼來說, 清晰和簡潔是最重要的.

代碼應該被重用, 而不應該被拷貝

模塊之間的依賴應該儘可能的小.

錯誤應該儘早被檢測出來, 最好是在編譯時刻.

代碼的清晰, 正確, 可用, 健壯, 靈活和可維護性比性能更重要.

編程的藝術應該是先學會基本規則, 然後才能知道在什麼時候打破這些規則.

靜態工廠方法慣用的名稱:

  • valueOf

  • of

  • getInstance

  • newInstance

  • getType

  • newType

類可以提供一個公有的靜態工廠方法,他只是一個返回類的實例的靜態方法。

實例受控類

public static Boolean valueOf(boolean b)
{
return b ? Boolean.TRUE : Boolean.FALSE;
}

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

API可以返回對象,同時又不會使對象的類變成公有的。以這種方式隱藏實現類會使API變得非常簡介。這種結束適用於基於接口的框架(java.util.Collections)

這樣做有幾大優勢。

  • 他們有名稱。

  • 不必再為每次調用他們都創建一個新對象。

  • 他們可以返回原返回類型的任何子類型的對象。

  • 在創建參數化類型實例的時候,他們是代碼變得更加簡潔。

靜態工廠方法的缺點

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

  • 他們與其他的靜態方法實際上沒有任何區別。

2 遇到多個構造器參數時要考慮用構件器

靜態工廠和構造器有個共同的侷限性:他們都不能很好的擴展大量的可選參數。

對於需要大量的可選參數的時候,一向習慣採用重疊構造器模式。

重疊構造器模式可行,但是當有許多參數的時候,客戶端代碼會很難編寫,並且仍然較難以閱讀。

遇到許多構造器參數的時候,還有第二種代替辦法,即JavaBeans模式。

JavaBeans模式自身有著很嚴重的缺點。因為構造過程被分到了幾個調用中,在構造過程中JavaBean可能處於不一致的狀態。

JavaBeans模式阻止了把類變成不可變的可能,這就需要程序員付出額外的努力來確保他的線程安全。

還有第三種替代方法,既能保證向重疊構造器模式那樣的安全性,也能保證像JavaBeans模式那麼好的可讀性。這就是Builder模式。

不直接生成想要的對象,而是讓客戶端利用所有必要的參數調用構造器,得到一個Builder對象。然後客戶端在builder對象上調用類似setter的方法,來設置每個相關的可選參數。

 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 {
private final int servingSize;
private final int servings;
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) {
ccalories = val; return this;
}
public Builder fat(int val) {
fat = val; return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val; return this;
}
public Builder sodium(int val) {
sodium = 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;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).calories(100).build();
}
}

Builder像個構造器一樣,可以對其參數加強約束條件。Build方法可以檢驗這些約束條件。

將參數從builder拷貝到對象中之後,並在對象域而不是在Builder域(39)中對他們進行檢驗,如果違反了任何約束條件,build方法就應該拋出IllegalStateException(60)。異常的詳細信息應該顯示出違反了那些約束條件。

設置參數的builder生成了一個很好的抽象工廠。

public interface Builder {
public T build();
}
NutritionFacts.Builder implements Builder<nutritionfacts>
Tree buildTree(Builder Extends Node> nodeBuilder) {}/<nutritionfacts>

Class.newInstance破壞了編譯時的異常檢查。而Builder接口彌補了這些不足。

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

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

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

或者:

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

工廠方法的優勢之一在於,它提供了靈活性:在不改變其API的前提下,我們可以改變該類是否應該為Singleton的想法。

工廠方法返回該類的唯一實例,但是,他可以很容易的被修改,比如改成每個調用該方法的線程返回一個唯一的實例。第二個優勢與泛型有關(27)。

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

企圖通過將類做成抽象類來強制該類不可被實例化,這是行不通的。該類可以被子類化,並且該子類也可以被實例化。

這要讓這個類包含私有構造器,他就不能被子類化了:

public class UtilityClass {
private UtilityClass() {
throw new AssertionError();
}
}

AssertionError不是必需的,但是它可以避免不小心在類的內部調用構造器。他保證該類在任何情況下都不會被實例化。

5 避免創建不必要的對象

對於同時提供了靜態工廠方法(1)和構造器的不可變類,通常可以使用靜態工廠方法而不是構造器,以避免不必要的對象。

例如,靜態工廠方法Boolean.valueOf(String)幾乎總是優先與構造器Boolean(String)。構造器在每次被調用的時候都會創建一個新的對象,而靜態工廠方法則從來不要求這樣做,實際上也不會這樣做。

public class Person {
private final Date birthDate;

public boolean isBabyBoomer() {
Calendar gmtCal =Calendar.getInstance(TimeZone.getTimeZOne(“GMT”));
gmtCal.set(1946,Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, , 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return brithDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
}
}

下面使用靜態的初始化器:

public class Person {
private final Date brithDate;
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 brithDate.compareTo(BOOM_START) >= 0 && brithDate.compareTo(BOOM_END) < 0;  
}
}

如果改進後的person類被初始化了,他的isBadyBoomer方法卻永遠不會被調用,那就沒有必要初始化BOOM_START和BOOM_END域。

通過延遲初始化(71),即把對這個域的初始化延遲到isBadyBoomer方法第一次被調用的時候進行,則有可能消除這些不必要的初始化工作,但是不建議這樣做。

適配器是指這樣一個對象:把它功能委託給一個後備對象,從而為後備對象提供一個可以替代的接口。由於適配器除了後備對象之外,沒有其他的狀態信息,所以針對給定對象的特定適配器而言,他不需要創建多個適配器實例。(Map接口的keySet)

不要錯誤的認為本條目所介紹的內容暗示著“創建對象代價非常昂貴,我們應該要儘可能的避免創建對象”。相反,由於小對象的構造器製作很少量的顯式工作,所以,小對象的創建和回收動作是非常廉價的,特別是在現代的JVM實現上更是如此。

反之,通過維護自己的對象池來避免創建對象並不是一種好的做法,除非池中的對象時非常重量級的。(數據庫連接池)

6 消除過期的對象引用

public class Stack {
privateObject[] elements;
private int size;
private static final int DEFAULT_INITIAL_CAPACITY =16;
public Stack() {
elements = newObject[DEFAULT_INITIAL_CAPACITy];
}
public void push() {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
returnn elements[--size];
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, size >> 1);
}
}

如果一個棧先是增長,然後再收縮,那麼從棧中彈出來的對象將不會被當做垃圾回收,即使使用棧的程序不在引用這些對象,他們也不會被回收。這是因為,棧的內部維護著對這些對象的過期引用。所謂過期引用,是指永遠也不會被解除的引用。

修復辦法:

publicObject pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}

清空過期引用的另一個好處是,如果他們以後又被錯誤地解除引用,程序就會立即拋出NullPointerException異常,而不是悄悄地錯誤運行下去。

清空對象引用應該是一種例外,而不是一種規範行為。

  • 只要類時自己管理內存,程序員就應該警惕內存洩漏問題。

  • 內存洩漏的另一個常見來源是緩存。(WeakHashMap)。

  • 內存洩漏的第三個常見來源是監聽器和其他回調。

7 避免使用終結方法

終結方法通常是不可預測的,也是很危險的,一般情況下是不必要的。

Java語言規範不僅不保證終結方法會被及時的執行,而且根本就不保證他們會被執行,當一個程序終止的時候,默寫已經無法訪問的對象上的終結方法卻根本沒有被執行,這是完全有可能的。

不應該以來中介方法來更新重要的持久狀態。例如依賴和總結方法來釋放共享資源上的永久鎖,很容易讓整個分佈式系統垮掉。

使用中介方法有一個非常嚴重的性能損失。

現實的種植方法通常與try-fainally結構結合起來使用,以確保及時終止。

Foo foo = new Foo();
try {
// ……
} finally {
foo.terminate();
}

本地對等體是個一個本地對象,普通對象通過本地方法委託給一個本地對象。因為本地對等體不是一個普通的對象,所以垃圾回收期不知道他,當他的Java對等體被回收的時候,他不會被回收。終止方法可以是本地方法,或者他也可以調用本地方法。

那麼終結方法的好處:

  • 當對象的所有者忘記調用前面段落中建議的顯式終止方法時,終止方法可以充當安全網。

  • 在本地方法體並不擁有關鍵資源的前提下,終結方法正式執行回收任務的最合適的工具。

終結方法鏈:

try {
// ……
} finally {
super.finalize();
}

如果終結方法發現資源還未被終止,啫應該在日誌中記錄一條警告,因為這表示客戶端中的一個Bug,應該被修復。

(FileInputStream、FileOutputStream、Timer、Connection),都具有終結方法,這些終結方法充當了安全網。

如果子類實現了超類的終結方法,但是忘了手工調用超類的終結方法,防範方法是為每個被終結的對象創建一個附加對象。

把終結方法放在一個匿名類中,該匿名類唯一的用途就是終結他的外圍實例。該匿名類的單個實力被稱為終結方法守衛者。

public class Foo {
private final Object finalizerGuardian = newObject() {
protected void finalize() throw Throwable {
// Finalize outer Foo object
}
}
}

外圍實例在他的私有實例域存放著一個對其終結方法守衛者的唯一引用,因為終結方法守衛與外圍實例可以同時啟動終結過程。當守衛被終結的時候,他執行外圍實例所期望的終結行為,就好像他的終結方法是外圍對象上的一個方法一樣。

第三章 對於對象都通用的方法

8 Equals方法

重寫equals方法規範

  • 自反性

  • 對稱性

  • 傳遞性

  • 一致性:對於任意的應用值x和y,如果對象信息沒有修改,那麼多次調用總是返回true,或false

9 HashCode方法

修改equals總是要修改hashCode

如果兩個對象根據equals方法返回是相等的,那麼調用這兩個對象任一個對象的hashCode方法必須產生相同的結果

為不相等的對象產生不同的散列碼

boolean類型 v ? 0 : 1

byte, char, short類型 (int) v

long類型 (int) (v ^ (v >>> 32))

float類型 Float.floatToIntBits(v)

double類型 Double.doubleToLongBits(v)

Object類型 v == null ? 0 : v.hashCode()

array類型 遞歸調用上述方法

result = 37 * result + n;

10 ToString方法

總是改寫toString()方法

11 Clone方法

Cloneable接口

改變超類中一個受保護的方法的行為

Object的clone方法返回該對象的逐域拷貝,否則拋出一個

CloneNotSupportedException異常

x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)

拷貝一個對象往往會導至創建該類的一個新實例,

但同時他也會要求拷貝內部的數據結構,這個過程中沒有調用構造函數

cone方法是另一個構造函數,必須確保他不會傷害到原始的對象,

並且正確地建立起被克隆對象中的約束關係

clone結構與指向可變對象的final域的正常用法是不兼容的

另一個實現對象拷貝的好辦法是提供一個拷貝構造函數

public Yum(Yum yum)

靜態工廠

public static Yum newInstance(Yum yum)

12 Comparable接口

一個類實現了Comparable接口就表明他的實例具有內在的排序關係

如果想為實現了Comparable接口的類增加一個關鍵特性,請不要

擴展這個類,而是編寫一個不相關的類,其中包含一個域,其類型是的一個類,然後提供一個“視圖”方法返回這個域。

BigDecimal("1.0")
BigDecimal("1.00")

加入到HashMap中,HashMap包含2個元素,通過equals方法比較是不相等的

加入到TreeMap中,TreeMap包含1個元素,通過compareTo方法比較是相等的

第四章 類和接口

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

要區別設計良好的模塊與設計不好的模塊,最重要的因素在於,這個模塊對於外部的其他模塊而言,是否隱藏其內部數據和其他實現細節。

設計良好的模塊會隱藏所有的實現細節,把他的API與他的實現清晰的隔離開來。然後,模塊之間之通過他們的API進行通信,一個模塊不需要知道其他模塊的內部工作情況。這概念被稱為信息隱藏或者封裝,是軟件設計的基本原則之一。

他可以有效地解除組成系統的模塊各模塊之間的耦合關係,使得這些模塊可以獨立地愾發、測試、優化、使用、理解和修改。

Java程序設計語言提供了許多機制來協助信息隱藏。訪問控制機制決定了類、接口和成員可訪問性。

儘可能地使每個類或者成員不被外界訪問。

如果一個報己私有的頂層類,只是在某一個類的內部被用到,就應該考慮使他成為唯一使用他的那個類的私有嵌套類(22)。

實例域決不能是公有的。包含公有可變域的類並不是線程安全的。

長度為非零的數組總是可變的。類具有公有的靜態final數組域,或者返回這種域的訪問方法,這幾乎總是錯誤的。

public static final Thing[] VALUES = { ... };

修正這個問題有兩種方法。可以是共有數組變成私有的,並增加一個公有的不可變列表

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));/<thing>

另一種方法是,可以是數組變成私有的,並添加一個公有方法,他返回私有數組的一個備份。

private static final Thing[] PRIVATE_VALUE = { ... };
public static final Thing[] values() {
return PRIVATE_VALUE.clone();
}

要在這兩種方法之間作出選擇,得考慮客戶端可能怎麼處理這個結果。

出了共有靜態final域的特殊情形之外,公有類都不應該包含共有域。並且要確保共有靜態final域所引用的對象都是不可變的。

14 在公有類中使用訪問方法而非共有域

如果類可以在他所在的包的外部進行訪問,就提供訪問方法,以保留將來改變該類的內部表示法的靈活性。

如果類是包級私有的,或者是私有的嵌套類,直接暴露他的數據域並沒有本質的錯誤。假設這些數據域確實描述了該類所提供的抽象。這種方法比訪問方法的做法更不會產生是覺混亂,無論是在類定義中,還是在使用該類的客戶端代碼中。

15 支持非可變性

非可變對象本質上是線程安全的,他們不要求同步,並且可以自由共享。

  • 不要提供任何會修改對象的方法

  • 保證沒有可被子類改寫的方法

  • 是所有的域都是final的

  • 訴有的于都成為私有的

  • 保證對於任何可變組件的互斥訪問

對於頻繁用的到的值,為他們提供公有的靜態final常量:

public static final Complex ZORE = new Complex(0, 0);
public static final Complex One = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

這種方可以被進一步擴展。不可變的類可以提供一些靜態工廠,他們把頻繁被請求的實例緩存起來,從而當現有實例可以符合請求的時候,就不必創建新的實例。

不可變類真正唯一的缺點是,對於每個不同的值都需要一個單獨的對象。

對於這種問題有兩種辦法:

  • 先猜測一下會經常用到哪些多步驟的操作,然後將他們作為基本類型提供。如果模個多步驟操作已經作為基本類型提供,不可變的類就可以不必在每個步驟單獨創建一個對象。(例如BigInteger有一個包級私有的可變配套類)

  • 如果無法預測,最好的辦法是提供一個公有的可變配套類。(String類的配套類StringBuilder)。

不僅可以共享非可變對象,甚至也可以共享它們的內部信息

BigInteger中的negate方法

非可變對象為其他對象提供了大量構件

非可變類的缺點是,對於每一個不同的值都要求一個單獨的對象

使一個類成為final的兩種辦法

  • 讓這個類的沒一個方法都成為final的,而不是讓整個類都成為final的。(可擴展)

  • 使其所有的構造方法成為私有的,或者包級私有的,並增加送有靜態工廠來替代個構造函數。(靜態工廠)

public static Complex valueOf(a, b) { return new Complex(a, b); }

擴展則增加靜態方法 static Complex valueOfPolar(a, b) ...

如果當前正在編寫的類,他的安全性依賴於BigInteger的非可變性,那麼你必須檢查

一確定這個參數是不是一個真正的BigInteger,而不是一個不可新的子類實例。

if (arg.getClass() != BigInteger.class) r = new BigInteger(arg.toByteArray());

規則指出,沒有方法會修改對象,並且所有的域必須是final的。

除非有很好的理由要讓一個類成為可變類,否則就應該是非可變的。

實際上這些規則比真正的要求強了一點,為了提高性能實際上應該是沒有一個方法能夠對對象的狀態產生外部可見的改變,然而許多非可變類擁有一個或者多個非final的冗餘域,它們比一些開銷昂貴的計算結果緩存到這些域中,如果將來再次請求這些計算,則直接返回這些被緩存的值,從而節約了從新計算所需的開銷。這總技巧可以很好的工作應為對象是非可變的,他的非可變行保證了這些計算如果被再次執行的話,會產生相同的結果(延遲初始化,String的hashCode)

如果一個類不能被做成非可變類,那麼你仍然應該儘可能地限制它的可變性。

構造函數應該創建完全初始化的對象,所有的約束關係應該在這時候建立起來。

16 複合優先於繼承

與方法調用不同的是,繼承打破了封裝性。

上面的問題都來源於對方法的改寫動作。如果你在擴展一個類的時候,僅僅是增加新的方法,而不改寫已有的方法,你可能會認為這樣做是安全的,但是也並不是完全沒有風險。

有一種辦法可以避免前面提到的所有問題,你不再是擴展一個已有的類,而是在新的類中增加一個私有域,他引用了這個已有的類的一個實例。這種設計被稱作複合。

public class InstrumentedSet extends ForwardingSet {
private int addCount = 0;
public InstrumentedSet(Set s) {
super(s);
}
public boolean add(E e) {
addCount++;
return super.add(e);
}
public boolean addAll(Collection extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}

}
public class ForwardingSet implements Set {
private final Set s;
public ForwardingSet(Set s) {
this.s = s;
}
public void add(E e) { return s.add(e); }
// ......
}

應為原有已有的類邊成了一個新類的一個組成部分。新類中的每個實例方法都可以被調用被包含的已有實例中對應的方法,並返回它的結果。這被稱為轉發,新類中的方法被稱為轉發方法。這樣的到的類會非常穩固,他不依賴於已有類的事現細節。

每一個InstrumentedSet實例都把另一個Set實例包裝起來,所以InstrumentedSet類被稱作包裝類。(Decorutor模式)

包裝類不適合用在回調框架中,在回調框架中,對象把自己的引用傳遞給其他的對象,

已便將來調用回來,因為被包裝起來的對象並不知道他外面的包裝對象,所以他傳遞一個只向自己的引用,回調時繞開了外面的包裝對象這被稱為SELF問題。

只有當子類真正是超類的子類型的時候,繼承才是合適的,對於正在擴展的類,繼承機制會把超類API中的所有缺陷傳播到子類中,而複合技術運允許你設計一個新的API從而隱藏這些缺陷。

17 要麼專門為繼承而設計,並給出文檔說明,要麼禁止繼承

一個類必須通過某種形式提供適當的鉤子,已便能夠進入到它的內部工作流程中,

這樣的形式可以是精心選擇的受保護的方法,也可以是保護域。

當你為了繼承的目的而設計一個有可能被廣泛使用的類時,比需要意識到,對於文檔中所說明的自用模式,以及對於其受保護方法和域所有隱含的實現細節,你實際上已經作出了永久的承諾。這些承諾使得你在後續的版本中要提高這個類的性能,或者增加新功能非常困難,甚至不可能。

構造函數一定不能調用可被改寫的方法。超類的構造函數將會在子類的構造函數運行之前先被調用,如果該改寫版本的方法依賴於子類構造函數所執行的初始化工作,那麼該方法將不會如預期般的執行。

無論是clone還是readObject,都不能他調用一個可改寫的方法,不管是直接地方是,還是間接地方式。

為了繼承而設計一個類,要求對這個類有一些實質的限制。

對於這個問題的最好解決方案是,對於那些並非為了安全地進行子類化而設計和編寫文檔的類,禁止子類化。

  • 2把所有的構造函數變成私有的,並增加一些公有靜態工廠來代替構造函數

消除一個類中可改寫的方法而不改變它的行為,做法如下

  • 把每個可改寫的方法的代碼移到一個私有的輔助方法中,並讓每個可改寫的方法

  • 調用他的私有輔助方法。然後,用直接調用可改寫方法的私有輔助方法來代替可

  • 改寫方法的每個自用調用。

18 接口優於抽象

已有的類可以很容易被更新,已實現新的接口。

接口使得我們可以構造出非層次結構的類型框架。

接口使得安全地增強一個類的功能成為可能。

可以把接口和抽象類的優點結合起來,對於你期望導出的每一個重要接口,都提供一個抽想的骨架實現類。

實現了這個接口的類可以把對於接口方法的調用,轉發到一個內部私有類的實例上,

而這個內部私有類擴展了骨架實現類。這項技術被稱為模擬多重繼承。

編寫一個骨架類相對比較簡單,首先確定那些方法是最為基本的,其他的方法在實現的時候將以他們為基礎。基本方法將是骨架實現類中的抽象方法,必須為接口中所有其他方法提供具體的實現。

抽象類的演化比接口的演化要容易得多。

19 接口只是被用來定義類型

一個類實現了一個接口,就表明客戶可以對這個類的實例實施某些動作。

有一中接口被稱為常量接口,常量接口模式是對接口的不良使用。

要導出常量,可以有幾種選擇方案。如果這些常量被看作一個枚舉類型的成員,

那麼你應該應用一個類型安全枚舉類(21),否則的話,你應該使用一個不可被實例化的工具類。(3)

接口應該使用來定義類型的,他們不應該被用來導出常量。

20 類層次優先於標籤類

標籤類過於冗長、容易出錯,並且效率低下。

class Figure {
enum Shape { RECTANGLE, CIRCLE };
final Shape shape;
double length;
double width;
double radius;
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() { ... }

}

標籤類正是類層次的一種簡單的仿效。

abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
public Circle(double radius) {
this.radius = raidus;
}
public area() { return Math.PI * radius; }
}
class Rectangle extends Figure {
final double length;
final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double area() { return length * width; }
}

這段代碼簡單且清楚,每個類型的實現都配有自己的類,這些類都沒有受到不相關的數據域的拖累。所有的域都是final的。

他們可以用來反應類型之間本質上的層次關係,有助於增強靈活性,並進行更好的編譯時類型檢查。

21 用函數對象表示策略

有些語言支持函數指針、代理、lambda表達式,或者支持類似的機制,允許程序把“調用特殊函數的能力”存儲起來並傳遞這種能力。

Java沒有提供函數指針,但是可以用對象引用實現同樣的功能。調用對象上的方法通常是執行該對象上的某項操作。

我們可以定義這樣一種對象,他的方法執行其他對象上的操作。如果一個類僅僅導出這樣的一個方法,他的實例實際上就等同於一個指向該方法的指針。這樣的實例被稱為函數對象。

class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}

函數指針可以載人一對字符串上被調用。換句話說,StringLengthComparator實例適用於字符串比較操作的具體策略。

作為典型的具體策略類,StringLengthComparator類是無狀態的:他沒有域,這個類的所有實例在功能上都是互相等價的。

我們在設計具體的策略類時,還需要定義一個策略接口:

public interface Comparator {
public int compare(T t1, T t2);
}
Class StringLengthComparator implements Comparator<string> {}/<string>

具體策略類往往使用匿名類聲明。如果他被重複執行,考慮將函數對象存儲到一個私有的靜態final域裡,並重用他。

Class Host {
private static class StrLenCmp implements Comparator<string>, Serializable {
public int compare(String s1, String s2) {

return s1.length() - s2.length();
}
}
public static final Comparator<string> STRING_LENGTH_COMPRATOR = new StrLenCmp();
}/<string>/<string>

宿主類可以到出公有的靜態域(或靜態工廠方法),起類型為策略接口,具體的策略類可以是宿主類的私有嵌套類。(String的不去分大小寫比較)

22 優先考慮靜態成員類

嵌套類是指被定義在另一個類的內部的類。

嵌套類存在的目的應該只是為他的外圍類提供服務。

如果一個嵌套類將來有可能會用於其他的某個環境中,那麼應該是頂層類。

嵌套類有四種:靜態成員類,非靜態成員類,匿名類和局部類。

除了第一種其他三種都被稱為內部類。

靜態成員類的一種通常用法是作為公有的輔助類,僅當與他的外部類一起使用時才有意義。

非靜態成員類的每一個實例都隱含著與外圍類的外圍實例緊密關聯在一起。

在非靜態成員的實例方法內部,調用外圍實例上的方法是有可能的,或者使用經過修飾的this也可以得到一個指向外圍實例的引用。如果一個嵌套類的實例可以在他的外圍類的實例之外獨立存在,那麼這個嵌套類不可能是一個非靜態成員類,在沒有外圍實例的情況下要創建非靜態成員類的實例是不可能的。

非靜態成員應用Iteragor(迭代器)

如果你聲明的成員類不要求訪問外圍實例,那麼請記住把static修飾符放到成員類的聲明中,使他成為一個靜態成員類,而不是一個非靜態成員類。

私有靜態成員類的一種通常方法是用來代表外圍類對象的組件。例如Map實例中的Entry每一對鍵值都與Map關聯但是Entry的方法並不需要訪問外圍類,如果是用非靜態成員來表示,每個Entry中將會包含一個指向Map的引用,只會浪費空間。

匿名類沒有名字,所以他們被實例化之後就不能在對他們進行引用,他不是外圍的一個成員,並不於其他的成員一起被聲明,而是在被使用的點上同時被聲明和實例化。匿名類可以出現在代碼中任何允許表達式出現的地方。匿名類的行為與靜態的或者非靜態的成員類非常類似,取決於他所在的環境:

如果匿名類出現在一個非靜態的環境中,則他有一個外圍實例。

常見用法:

  • 創建一個函數對象。比如Comparator

  • 創建一個過程對象。比如Thread

  • 在一個靜態工廠方法內部。比如intArrayAsList(16條)

  • 在很複雜的類行安全枚舉類型中用於共有靜態final域的初始化器中(21條Operation類)

public class Calculator
{
public static abstract class Operation
{
private final String name;
Operation(String name) { this.name = name; }
public String toString() { return this.name; }
abstract double eval(double x, double y);
public static final Operation PLUS = new Operation("+") {
double eval(double x, double y) { return x + y; }
}
public static final Operation MINUS = new Operation("-") {
double eval(double x, double y) { return x - y; }
}
public static final Operation TIMES = new Operation("*") {
double eval(double x, double y) { return x * y; }
}
public static final Operation DIVIDE = new Operation("/") {

double eval(double x, double y) { return x / y; }
}
}
public double calculate(double x, Operation op, double y) {
return op.eval(x, y)
}
}

如果一個嵌套類需要在單個方法外仍然是可見的,或者太長了不適合放在一個方法內部,那麼應該是用成員類。

如果成員類的每一個實例都需要一個只向起外圍實例的引用,則把成員類做成非靜態的;否則就做成靜態的。

第十章 併發

66 同步訪問共享的可變數據

對象在被創建的時候處於一直的狀態,當有方法訪問他的時候,他就被鎖住了。這些方法觀察到對象的狀態,並且可能會引起狀態轉變,即把對象的一種一致狀態轉換到另一種一致的狀態。

while (!done) i++;

優化後→

if (!done) while (true) i++;

導致狀態的該並永遠不會發生。這種優化稱作提升,正是HotSpot Server VM的工作。結果是個活性失敗。

為了提高性能,在讀寫原子數據的時候,應該避免使用同步。這個建議是非常危險而錯誤的。

雖然語言規範保證了線程在讀取原子數據的時候,不會看到任意的數值,但是他並不保證一個線程寫入的值對於另一個線程是可見的。為了在線程之間進行可靠的通信,也為了互斥訪問,同步是必要的。

如果讀和寫操作沒有都被同步,同步就不會起作用。

private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}

問題在於,增量操作符(++)不是原子的。他在nextSerialNumber域中執行兩項操作:

首先讀取值,然後寫回一個新值,相當於原來的值再加上1

如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取這個域

第二個線程就會與第一個線程一起看到同一個值,並返回相同的序列號。這就是安全性失敗。

修正generateSerialNumber方法的辦法是增加synchronized修飾符、Atomic類或者使用鎖。來確保調用不會交叉存取。

private static final AtomicLong nextSerialNum = new AtomicLong();
publib static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}

只同步共享對象引用的動作。然後其他線程沒有進一步的同步也可以讀取對象,只要他沒有再被修改。

這種對象被稱作事實上不可變的。將這種對象引用從一個線程傳遞到其他的線程被稱作安全發佈。

安全發佈對象引用有許多種方法:

  • 可以將它保存在靜態域中,作為類初始化的一部分;

  • 可以將它保存在volatile域、final域或者通過正常鎖定訪問的域中;或者可以將它放到併發的集合中。

  • 當多個線程共享可變數據的時候,每個讀或者寫書據的線程都必須執行同步。

67 避免過渡同步

為了避免活性失敗和安全性失敗,在一個被同步的方法或者代碼中,永遠不要放棄對客戶端的控制。

在一個同步的區域內部,不要調用設計成被覆蓋的方法,或者是由客戶端以函數對象的形式提供的方法(21)。

在同步區域外被調用的外來方法被稱作開放調用。除了可以避免死鎖之外,開放調用還可以極大的增加併發性。

通常,你應該在同步區域內作儘可能少的工作。獲得鎖,檢查共享數據,根據需要轉換數據,然後釋放鎖。如果你必須要執行某個很耗時的動作,則應該設法把這個動作移到同步區域外面,而不違背第66條中的指導方針。

如果你在內部同步了類,就可以使用不同的方法來實現高併發性,例如分拆鎖、分離鎖和非阻塞併發控制。

如果方法修改了靜態域,那麼你也必須同步對這個域的訪問,即使他往往之用於單個線程。

為了避免死鎖和數據破壞,千萬不要從同步區域內部調用外來方法。

68 executor和task優先於線程

ExecutorCompletionService
ScheduledThreadPoolExecutor

如果相讓不知一個線程來處理來自這個隊列的請求,只要調用一個不同的靜態工廠,這個工廠創建了一種不同的executor service,稱作線程池。

69 併發工具優先於wait和notify

concurrent中更高級的工具分成三類:

  • Executor Framwork 執行框架

  • Concurrent Collection 併發容器

  • Synchronizer 同步器

併發集合中不可能排除併發活動;將他所定沒有什麼作用,只會使程序速度更慢。

同步器是一些使線程能夠等待另一個線程的對象,允許他們協調動作。最長用的同步器是CountDownLatch和Semaphore,不常用的是CyclicBarrier和Exchanger。

public static long time(Executor executor, int concurrency, final Runnable action) throws InterruptedException {
final CountDownLatch ready = new CountDownLatch(concurrency);
final CountDownLatch start new CountDownLatch(1);
final CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(new Runnable() {
public void run() {
ready.countDown();
try {
start.await();
action.run();
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
finally {
done.countDown();

}
}
});
}
ready.await();
long startTime = System.nanoTime();
start.countDown();
done.await();
return System.nanoTime() - startTime;
}

對於間歇式的定時,始終應該優先使用System.nanoTime,而不是使用System.currentTimeMillis。System.nanoTime更加準確也更加精確,他不受系統的實時時鐘調整所影響。

始終應該使用wait循環模式來調用wait方法;永遠不要在循環之外調用wait方法。循環會在等待之前和之後測試條件。

等待之前測試條件,當條件已經成立時就跳過等待,這對於確保活性是必要的。如果條件已經成立,並且在線程等待之前,notify方法已被調用,則無法保證該線程從等待中甦醒過來。

等待之後測試條件,如果條件不成立的話繼續等待,這對於確保安全性是必要的。當條件不成立的時候,如果線程繼續執行,則可能會破壞被鎖保護的約束關係。

當條件不成立時,下面一些理由可是一個線程甦醒過來:

  • 另一個線程可能已經的到了鎖,並且從一個線程調用notify那一刻起,到等待線程甦醒過來的這段時間中,得到鎖的線程已經改變了受保護的狀態。

  • 條件並不成立,但是另一個線程可能意外地或惡意的調用了notify。

    通知線程在喚醒等待線程時即使只有某些等待線程的條件已經被滿足,當時通知線程可能仍然調用notifyAll。

  • 在沒有通知的情況下,等待線程也可能會甦醒過來。這稱為偽喚醒。

如果處於等待狀態的所有線程都在等待同一個條件,而每次只有一個線程可以從這個條件中被喚醒,那麼你就應該選擇調用notify,而不是notifyAll。

一般情況下,你應該優先使用notifyAll,而不是使用notify。

70 線程安全性的文檔化

在一個方法聲明中出現synchronized修飾符,這是個實現細節,並不是導出的API的一部分。

一個類為了可悲多個線程安全地使用,必須在文檔中清楚地說明他所支持的線程安全級別。

包括:

  • 不可變的(String Long BigInteger)

  • 無條件的線程安全 (Random ConcurrentHashMap)

  • 有條件的線程安全 (Collections.synchronized)

  • 非線程安全 (ArrayList HashMap)

  • 線程對立的

有條件的線程安全必須指明:哪個方法條用序列需要外部同步,以及在執行這些序列的時候要獲得哪把鎖。

應對於Java併發編程實踐一書中的線程安全註解。在文檔中描述一個有條件的線程安全類要特別小心。

你必須致命那個調用序列需要外部同步,還要指明為了執行這些序列,必須獲得哪一把所。

例外(一個對象代表了另一個對象的一個視圖,用戶通常就必須在後臺對象上同步,以防止其他線程直接修改後臺對象)

Map map = Collections.synchronizedMap(new HashMap);
Set s = map.keySet();
synchronized (map) {
for (K key : s) {
k.method();
}
}

為了避免超時地保持公有可訪問鎖的攻擊,應該使用一個私有鎖對象來代替同步的方法。

private final Object lock = new Object();
public void foo() {
synchronized(lock) {
// ......
}
}

lock域被聲明為final的。這樣可以防止不小心改變他的內容,而導致不同步訪問對象的悲慘結果。

私有鎖對象模式只能用在無條件的線程安全類上。有條件的線程安全類不能使用這種模式。

私有鎖對象模式特別適用於那些專門為繼承而設計的類。

如果這種類使用他的實例作為鎖對象,子類可能很容易在無意種妨礙基類的操作,反之亦然。

71 甚用延遲初始化

延遲初始化是延遲到需要域的值時才能將它初始化的這種行為。

就像大多數的優化一樣,對於延遲初始化,最好建議“除非絕對必要,否則就不要這麼做”。

如果屬性只在類的實例部分被訪問,並且初始化這個屬性的開銷很高。可能就值得進行延遲初始化。

當有多個線程時,延遲初始化是需要技巧的。如果多個線程共享一個延遲初始化的域,採用某種形式的同步是很重要的。

在大多數情況下,正常初始化要優先於延遲初始化。

private final FieldType field = computeFieldValue();

如果利用延遲優化來破壞初始化的循環,就要使用同步訪問方法。

private FieldType field;
synchronized FieldType getField() {
if (field == null) {
field = computeFieldValue();
}

return field;
}

如果出於性能的考慮而需要對靜態域使用延遲初始化,就使用lazy initialization holder class模式。

private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() {
return FieldHolder.field;
}

當個getField方法第一次被調用時,他第一次讀取FieldHolder.field,導致FieldHolder類得到初始化。這種模式的魅力在於,getField方法沒有被同步,並且只能執行一個域訪問,因此延遲初始化實際上並沒有增加任何訪問成本。

如果處於性能的考慮而需要對實例域使用延遲初始化,就使用雙重檢查模式。

這種模式避免了在域被初始化之後訪問這個域時的鎖定開銷。

思想是:兩次檢查域的值(雙重檢查),第一次檢查時沒有鎖定,看看這個域是否被初始化了;第二次檢查時有鎖定。當只有第二次檢查時表明這個域沒有被初始化,才會調用computeFieldValue方法對這個域進行初始化。

private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) {
synchronized (this) {

result = field;
if (result == null) {
field = result = computeFieldValue();
}
}
}
return result;
}

對於需要用到局部變量result可能優點不解。這個變量的作用是確保field只能在已經被初始化的情況下讀取一次。

單重檢查模式:有時候你可能需要延遲初始化一個可以接受重複初始化的實例域。如果處於這種情況,就可以使用雙重檢查慣用法的一個變形,他省去了第二次檢查。

private
volatile FieldType field;
public FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}

當雙重檢查模式或者單重檢查模式應用到數值類型的基本類型域時,就會用0來檢查這個域,而不是用null。

如果你不在意是否每個線程都重新計算域的值,並且域的類型為基本類型,而不是long或者double類型,就可以選擇從單重檢查模式的域聲明中刪除volatile修飾符。他加快了某些架構上的域訪問,代價是增加了額外的初始化訪問該域的每一個線程都要進行一次初始化。

對於實例域,就可以使用雙重檢查模式;對於靜態域,則可以使用惰性初始化;對於可以接受重複初始化的實例域,也可以考慮使用單重檢查模式。

72 不要依賴於線程調度器

任何依賴於線程調度器來達到正確性或者性能要求的程序,很有可能都是不可移植的。

要編寫健壯的、響應良好的、可移植的多線程應用程序,最好的辦法是確保可運行線程的平均數量不明顯多於處理器的數量。

保持可運行線程數儘可能少的主要方法是,讓每個線程做些有意義的工作,然後等待更多有意義的工作。如果線程沒有在做有意義工作,就不應該運行。

線程不應該一直處於忙等得狀態,即反覆地檢查一個共享對象,以等待某些事情發生。

public class SlowCountDownLatch {
private int count;
public SlowCountDownLatch(int count) {
if (count < 0)
throw new IllegalArgumentException();
this.count = count;
}
public void await() {
while (true) {
synchronized (this) {

if (count == 0) return;
}
}
}
public void countDown() {
if (count != 0)
count--;
}
}

如果某一個程序不能工作,是因為某些線程無法向其他線程那樣獲得足夠的CPU時間,那麼,不要企圖通過調用Thread.yield來修正該程序。

對於大多數程序員來說,Thread.yield的唯一用途是在測試期間人為地增加程序的併發性。通過探查程序中更大部分的狀態空間,可以發現一些隱蔽的Bug。

73 避免使用線程組

除了線程、鎖和監視器之外,線程系統還提供了一個基本的抽象,即線程組。

線程組並沒有提供太多有用的功能,而且他們提供的許多功能還都是有缺陷的。


個人介紹:

高廣超:多年一線互聯網研發與架構設計經驗,擅長設計與落地高可用、高性能、可擴展的互聯網架構。

本文首發在 高廣超的簡書博客 轉載請註明!


分享到:


相關文章: