引言
1 代碼應該被重用 ,而不是被拷貝。
2 錯誤應該儘早被檢測出來,最好是在編譯時刻。
3 接口、類、數組是引用類型(對象), 基本類型不是
第二章 創建和銷燬對象
1 考慮用靜態工廠方法代替構造器。
優勢:
1 有名稱(多個 相同簽名 的構造器)
2 不必每次調用它們都創建一個新對象。(可控)
3 可以返回原返回類型的任何子類型的對象。(靈活,可返回一個接口類型,強迫客戶端面向接口編程)
4 靜態工廠方法可以利用 類型推導 簡化 參數化類型實例 的創建。
缺點:
1 類不含public protected的構造器,不能被子類化。
2 Javadoc API 文檔中,沒有特殊標明 靜態工廠方法。 不方便查閱。
一些慣用名稱:
1. valueOf,實際上是類型轉換方法
2. of,valueOf的簡潔替代。
3. getInstance。
4. newInstance。與3相比,確保返回的每個實例都與其他的所有實例不同。
5. getXXXX。
6. newXXXX。
總結:
和構造器比,各有好處。優先使用靜態工廠。
(重用對象的好處 可以用 == 替代 equals,性能更好。靜態工廠參考Boolean.valueOf().)
2 遇到多個構造器參數時,要考慮用Builder
靜態工廠和構造器有個共同的侷限性:它們都不能很好地擴展到大量的可選參數。
Builder模式,既能保證像重疊構造器模式那樣的安全性,也能保證像JavaBeans模式那麼好的可讀性。還可以有多個 可變參數。
Builder模式構建時,被構建類的field一般都是final。
如類的構造器或者靜態工廠中具有多個參數,特別大多數參數時可選的時候,使用Builder模式。
(最好一開始就用Builder,否則兼容以前的構造函數和靜態工廠很煩。)
3 用私有構造器或者枚舉類型強化Singleton屬性
public enum EnumSingleton {
INSTANCE;
EnumSingleton() {
System.out.println("我被創建了");
}
public void metho1() {
System.out.println("單例的方法");
}
}
4 通過私有構造器強化不可實例化的能力
副作用是 使得一個類不能被子類化。可用作常量定義工具類。
5 避免創建不必要的對象
Map的keySet 每次返回的是同一個Set實例。
優先使用基本類型而不是裝箱基本類型,當心無意識的自動裝箱。
並不代表我們要儘可能的避免創建對象,對於小對象而言,它的創建和回收動作是非常廉價的。
反之,通過維護自己的對象池來避免創建對象並不是一種好的做法。
除非池中的對象是非常重量級的。
6 消除過期的對象引用
避免內存泄漏。
只要類是自己管理內存(數組存儲對象引用,及時置位null),程序員就應該警惕內存泄漏問題。
7 避免使用終結方法
終結方法finalizer通常是不可預測的,也是很危險的,一般情況下是不必要的。
它不保證何時執行,是否執行。且會帶來性能損失。
如果需要,提供一個顯式的終止方法。類似InputStream。
顯式的終止方法通常與try-finally結合起來使用,以確保及時終止。在finally子句內調用顯式的終止方法,可以保證即使在使用對象的時候,有異常拋出,該終止方法也會執行。
終結方法的合法用途:
1 使用者忘記調用顯式終止方法,終結方法可以充當 “安全網”,遲一點釋放比不釋放要好。(希望儘可能不要這樣) 。如果終結方法發現資源還未被終止,應該在日誌中記錄一條警告。因爲這是客戶端的一個bug。但是要考慮這種額外的保護是否值得。
四個示例:FileInputStream、FileOutputStream、Timer、Connection。
2 釋放native對象的資源。
還有一點,終結方法鏈不會自動執行。如果類有終結方法,並且子類覆蓋了終結方法,要手動調用超類的終結方法。應該在try中終結子類,在finally中調用超類的finalize().
protected void finalize() throws Throwable {
try {
//finalize自己
} finally {
super.finalize();
}
}
爲了防範子類沒有手工調用超類的終結方法。可以採用匿名內部類實現一個終結方法守衛者。
//終結方法守衛者
private final Object finalizerGuardian = new Object() {
@Override
protected void finalize() throws Throwable {
//在這裏調用外圍實例的finalize
FinalizeTest.this.finalize();
}
};
第三章 對於所有對象都通用的方法
8 覆蓋equals時請遵守通用約定
“值類(value class)”僅僅是一個表示值的類,例如Integer或Date。需要覆蓋equals()。
有一種值類不需要覆蓋,即第一條確保下的“每個值至多隻存在一個對象”的類。枚舉類型就屬於這種類。
equals()方法實現了等價關係。
- 自反性。對於任何非null的引用值x,x.equals(x)必須返回true.(對象必須等於其自身)
- 對稱性。對於任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。(警惕子類父類不同的equals實現時)
- 傳遞性。對於任何非null的引用只x、y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equeals(z)也必須返回true。(優先使用組合而不是繼承,可以避免違反傳遞性 對稱性,否則沒有完美解決方案。 只要超類不能直接創建實例,則不會違反原則)
- 一致性。對於任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)返回值一致。()
- 非空性。對於任何非null的引用值x,x.equals(null)必須返回false。(不用重複判空,只需要用instance 即可)
實現高質量equals方法的訣竅:
- 使用==操作符 檢查”參數是否爲這個對象的引用”。如果是,返回true。這是一種性能優化,適用於比較操作昂貴的情況。
- 使用instanceof操作符檢查“參數是否爲正確的類型”。如果不是,返回false。
- 把參數強轉成正確的類型。
- 對於該類中的每個“關鍵”域進行匹配比較。(float使用Float.compare()比較,double使用Double.compare()比較。 有些filed可能爲null,且合法。 可以用
(field ==null? o.field==null: field.equals(0.field))
。 如果field和o.field通常是相同的對象引用,那麼下面的做法更快一些:( field == o.field || field!=null && field.equlas(0.field))
) - 編寫完equals方法之後,應該去測試 對稱性、傳遞性、一致性。
- 覆蓋equals時,同時覆蓋hashCode。
- 不要將equals()方法聲明中的Object對象替換爲其他的類型。
(public boolean equals(MyClass o)){})
//一個示例
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof EnumSingletonTest)) {
return false;
}
EnumSingletonTest data = (EnumSingletonTest) o;
return data.xxx == xxx
&& dta.bbb == bbb;
}
9 覆蓋equals時 總要覆蓋 hashCode
不覆蓋該方法,會導致該類無法配合HashMap、HashSet和Hashtable一起使用。
因爲equals()相等的話,hashCode()一定要相等。
一個好的散列函數通常傾向於“爲不相等的對象產生不相等的散列碼”。
10 始終要覆蓋 toString
11 謹慎地覆蓋clone
一般,x.clone() != x
,x.clone().getClass() == x.getClass()
, x.clone().equals(x)
(但也不強求)
實際上,clone方法就是另一個構造器;你必須確保它不會傷害到原始的對象,並確保正確地創建被克隆對象中的約束條件。
更好的辦法是類似集合類那樣,提供 轉換構造器 和 轉換工廠。且轉換構造器和工廠可以帶一個接口類型的參數。 例如你有一個HashSet,想得到TreeSet,clone方法無法提供這樣的功能,但是用轉換構造器很容易實現。 new TreeSet(s)。
專家級程序員不建議用clone。
12 考慮實現 Comparable 接口
實現compareTo()方法 。它的約定和equals方法相似。(自反、傳遞、對稱),但它不用進行類型檢查。比較時,順序很重要,先比較重要的,如果產生了非零的結果,則結束比較。
小於返回負整數,等於返回0,大於返回正整數。
牆裂建議,(x.compareTo(y)==0) == (x.equals(y))
用於TreeSet TreeMap,以及工具類Collections、Arrays,內部包含搜索和排序算法。
如果一個域沒有實現Comparable接口,或者需要使用一個非標準的排序關係。可以用Comparator來代替,或者使用已有的Comparator。例如:
public final class A implements Comparable<A>{
private String s ;
public int compareto(A a){
return String.CASE_INSENSITIVE_ORDER_.compare(s,a.s);
}
}
有些時候可以直接用 this.s - other.s 來作爲返回值,但是要確保 最小和最大的可能域值之差小於或等於INTEGER.MAX_VALUE。否則計算結果超過int型溢出時, 結果將相反。
第四章 類和接口
13使類和成員的可訪問性最小化
隱藏內部數據和實現細節。
儘可能的使每個類或者成員不被外界訪問。
對於頂層的(非嵌套的)類和接口,只有兩種可能的訪問級別:包級私有(缺省) 和 公有的。
如果一個包級私有的頂層類活接口 只是在某一個類的內部被用到,就應該考慮使他成爲那個類的私有嵌套類。
然而,降低不必要的共有類的可訪問性,比上一點更重要。
對於成員,依次有:private、包級私有(default)、protected(只有子類和包內可以訪問)、public
protected應該少用,它和public都作爲公開API的一部分。
如果方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於超類中的訪問級別。這樣可以確保任何可使用超類的地方也可以使用子類。
實例域絕不能是公有的。如果域是非final的,則更不應該。如果final域包含可變對象的引用,它引用的對象是可以被修改的,這會導致災難性的後果。
長度非零的數組總是可變的。所以,類具有公有的靜態final數組域,或者返回這種域的訪問方法,幾乎總是錯誤的。因爲客戶端能修改數組中的內容,這是一個安全漏洞。
修正這個問題有兩種方法:
- 訪問權限設爲私有,同時增加一個公有的不可變列表。
private static final A[] PRIVATE_VALUES = {.....};
public static final List<A> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES ));
- 訪問權限設爲私有,增加一個公有方法,返回私有數組的一個備份:
private static final A[] PRIVATE_VALUES = {.....};
public static final A[] values(){
return PRIVATE_VALUES .clone();
}
14 在公有類中使用訪問方法而非公有域
如果類可以在它所在的包的外部進行訪問,就提供訪問方法。
如果類是包級私有的,或者是私有的嵌套類,直接暴漏它的數據域並沒有本質的錯誤。
公有類永遠都不應該暴露可變的域。偶爾會暴漏不可變域。
有時候會暴露包級私有的或者私有的嵌套類的域,無論這個類可變還是不可變。
15 使可變性最小化
不可變類只是其實例不能被修改的類。
每個實例中包含的所有信息都必須在創建該實例的時候就提供,並在對象的整個生命週期內固定不變。例如String、基本類型的包裝類、BigInteger、BigDecimal。
不可變類遵循下面五條規則:
- 不要提供任何會修改對象狀態的方法(改變對象屬性的方法)。
- 保證類不會被擴展。(final或者 私有、包級私有構造器,提供公有靜態工廠)
- 使所有的域都是final的。
- 使所有的域都成爲私有的。
- 確保對於任何可變組件的互斥訪問。確保客戶端無法獲取指向 可變對象的域 的引用。並且永遠不要用客戶端提供的對象引用來初始化這樣的域,也不要從任何訪問方法中返回該對象的引用。
大多數不可變類在修改域時,都是通過返回一個新的對象做的。只對操作數進行運算但不修改它。
這被成爲 函數的 functional做法,與之對應的是過程的procedural 或者 命令式的 imperative,這些方式會導致狀態改變。
例如
public class ImmutableExampleTest {
private final int value;
public ImmutableExampleTest(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public ImmutableExampleTest add(ImmutableExampleTest b) {
return new ImmutableExampleTest(value + b.value);
}
}
不可變對象可以只有一種狀態,即被創建時的狀態。
不可變對象本質上是線程安全的,它們不要求同步。
不可變對象可以被自由地共享。鼓勵客戶端儘可能地重用現有的實例。可以對頻繁用到的值,提供公有的靜態final常量。
不可變對象永遠不需要保護性拷貝。不應該提供clone方法或者拷貝構造器。
不僅可以共享不可變對象,甚至也可以共享它們的內部信息。例如BigInteger類的negate()方法,返回的BigInteger對象中的數組指向原始實例中的同一個內部數組。
不可變對象爲其他對象提供了大量的構件(building blocks)。
不強求所有的域都是final的,但必須保證 :沒有一個方法能夠對對象的狀態產生 外部可見 的改變。可以用非final域去緩存一些昂貴開銷的計算的結果(配合lazy initialization)。
如果不可變類實現了Serializable接口,並且包含一個或多個指向可變對象的域,需要特殊處理。
不可變對象真正唯一的缺點是,對於每個不同的值都需要一個單獨的對象。
如果你執行一個多步驟的操作,並且每個步驟都會產生一個新的對象,除了最後的結果之外其他的對象最終都會被丟棄,此時性能問題就會暴露出來。兩種解決辦法:
- 先猜測一下經常用到哪些多步驟操作,然後將它們作爲基本類型提供。
- 如果無法預測,最好的辦法就是提供一個公有的可變配套類。String類是不可變類,它的可變配套類是StringBuilder。BigInteger對應BigSet。
總結:
儘量將小的值對象,成爲不可變的。認真考慮將較大的值對象做成不可變的,例如String、BigInteger。只有當性能確實需要優化時,才爲不可變的類提供公有的可變配套類。
如果類不能被做成不可變的,仍然應該儘可能地限制它的可變性。除非有令人信服的理由,否則每個域都是final的
不應該提供“重新初始化”方法,它會增加複雜性。
16 複合優先於繼承(組合大於繼承)
繼承是實現代碼重用的有力手段,但它並非永遠是最佳工具。
在包內部使用繼承非常安全,因爲它屬於同一個程序員的控制之下。
對於專門爲繼承而設計、並具有很好的文檔說明的類,繼承也非常安全。
(這裏的繼承代表 實現繼承 ,指的是一個類繼承另一個類。 對於類實現接口、或者接口繼承接口都不屬於這種情況)
與方法調用不同的是,繼承打破了封裝性。
子類依賴於其超類中特定功能的實現細節。超類的實現有可能隨着發行版本的不同而有所變化,如果發生了變化,子類可能遭到破壞,即使子類的代碼完全沒有改變。
只有當子類和超類之間確實存在 “is-a”關係時,才應該用繼承。
繼承機制會把超類API中的所有缺陷傳播到子類中,而複合(裝飾、代理)則允許設計新的API 來隱藏這些缺陷。
17 要麼爲繼承而設計,並提供文檔說明,要麼就禁止繼承。
構造器決不能調用可被覆蓋的方法。不管是直接的還是間接的調用。
如果類是爲了繼承而被設計的,無論實現Cloneable
或Serializable
都不是好主意。因爲它把一些實質性的負擔轉嫁給擴展這個類的程序員的身上。因爲clone()
或readObject()
方法行爲上類似於構造器,所以也不可以調用可覆蓋的方法,不管以直接還是間接的形式。
如果必須從這種類繼承,一種辦法是 確保這個類永遠不會調用它的任何可覆蓋的方法。
18 接口優於抽象類
現有的類可以很容易被更新,以實現新的接口。
接口是定義mixin(混合類型)的理想選擇。
接口允許我們構造非層次(豎向)結構的類型框架。
雖然接口不允許包含方法的實現。但通過對每個重要接口都提供一個抽象的骨架實現(skeletal implementation)類,把接口和抽象類的優點結合起來。接口的作用仍然是定義類型,但是骨架實現類接管了所有與接口實現相關的工作。
按照慣例,骨架實現類稱爲AbstractInterface
,例如AbstractCollection
、AbstractSet
等。
骨架實現上有個小小的不同,就是簡單實現。AbstractMap.SimpleEntry就是個例子,簡單實現類似於骨架實現類,因爲它實現了接口,並且也是爲了繼承而設計的,區別在於它不是抽象的:它是最簡單的可能的有效實現。你可以原封不動地使用,也可以看情況將它子類化。
抽象類比接口有一個明顯的優勢:抽象類的演變比接口的演變要容易的多。
如果在後續的版本中,希望在抽象類中增加新的方法,始終可以增加具體方法,包含合理的默認實現即可。
一般來說,想在公有接口中增加方法,而不破壞實現這個接口的所有現有的類,這是不可能的。可以通過在爲接口增加新方法的同時,也爲骨架實現類增加同樣的新方法,來一定程度上減小破壞。
因此,一個接口一旦公開發行,再修改基本不可能。
總結:
接口通常是定義允許多個實現(多態)的最佳途徑。
如果你想要演變的容易性比功能、靈活性更重要,而且你接受抽象類的侷限性,就用抽象類。
接口->骨架實現類->簡單實現類。
19 接口只用於定義類型
除此之外,爲了任何其他的目的而定義接口是不恰當的。
說白了,定義接口是爲了接口的行爲。
有一種接口是常量接口,這是對接口的不良使用。因爲常量是實現細節,不應該泄漏出去。對類的用戶來說沒有價值,反而會干擾。而且如果非final類實現了常量接口,它的所有子類的命名空間也會被接口中的常量所“污染”。
定義常量應該用枚舉或者不可實例化的工具類。
20 類層次優於標籤類
想到了自己項目裏 羣聊創建模塊。 原本就是一個標籤類,改造成了類層次。
類層次好處:
簡單清晰,沒有樣板代碼,不受無關數據域的拖累,多個程序員可獨立擴展層次結構。而且類層次可以反映類型之間本質上的層次關係,有助於增強靈活性。
標籤類有很多缺點:
- 可讀性差
- 內存佔用增加因爲實例承擔着屬於其他風格的不相關的域。
- 域不能是final的。
總之它過於冗長,容易出錯,效率低下。
其實標籤類是對類層次的一種簡單的效仿。
將標籤類轉化成類層次:
- 爲標籤類的每個方法都定義一個包含抽象方法的抽象類,這裏的每個方法的行爲都依賴於 標籤值。
- 如果有行爲不依賴於標籤值的方法,就將這些方法放在抽象類裏。
- 如果所有的方法都用到了某些域,則將域也放在抽象類裏。
- 爲每種原始標籤書寫相應的子類。
21 用函數對象表示策略
如果一個類,它只有一個方法,這個方法執行其他對象(通過參數傳入)上的操作。
這個類的實例實際上等同於一個指向該方法的指針。
這樣的實例稱之爲函數對象。也是一個具體策略類。
我們在設計具體的策略類時,還需要定義一個策略接口。
總結:函數指針的主要用途就是實現策略模式。
爲了在java中實現這個模式,要聲明一個接口來表示該策略,並且爲每個具體策略實現該接口。
當具體策略只使用一次時,通常用匿名內部類來做。
當重複使用時,通常實現爲某個類私有的靜態成員類,並通過 公有 靜態 final 域 被導出,域類型是策略接口類型。
22 優先考慮靜態成員類
嵌套類指定義在另一個類內部的類。目的是 只爲它的外圍類服務。
如果嵌套類將來可能用於其他的環境中,它就應該是頂層類。
嵌套類四種:
- 靜態成員類(優先)
- 非靜態成員類
- 匿名類(不能擁有靜態成員,如果只有一處創建的地方)
- 局部類(有多處創建的地方)
後三種被稱爲內部類。
靜態成員類是最簡單的一種嵌套類,最好把它看作是普通的類,只是碰巧被聲明在另一個類的內部而已。
如果聲明內部類不要求訪問外圍實例,就要聲明稱 靜態成員類。
內部類會包含一個額外的指向外圍對象的引用,保存這份引用要消耗時間和空間,也會影響GC.
匿名類的常見用法:
- 動態創建函數對象。(21條)
- 創建過程對象,比如Runnable、Thread。
- 靜態工廠方法的內部。
第五章 泛型
出錯之後應該儘快發現,最好是在編譯時就發現。
23 請不要在新代碼中使用原生態類型、
List<T>
對應的原生態類型就是List
,如果使用原生態類型,就丟掉了泛型在安全性和表述性方面的所有優勢。它只是爲了兼容遺留代碼的。
無限制的通配符類型:List<?>
,等於List
,它們和List<Object>
都不相等。
在類文字 中必須使用原生態類型。List.class String[].class ini.class
都合法,List<String>.class 和List<?>.class
都不合法。
24 消除非受檢警告
儘可能地消除每一個非受檢警告。 如果消除所有警告,則可以確保代碼是類型安全的。意味着在運行時不會出現ClassCastException異常。
如果無法消除警告,同時可以證明引起警告的代碼是類型安全的,只有在這種情況之下,纔可以用一個@SuppressWarnings(“unchecked”)註解來禁止這條警告。
應該在儘可能小的範圍裏使用該註解。並添加註釋解釋爲什麼這麼做是安全的。
25 列表優先於數組
數組與泛型相比,有兩個重要的不同:
一 數組是協變的,泛型是不可變的。
如果Sub是Super的子類型,那麼Sub[]就是Super[]的子類型。
然而泛型則不是。對於任意兩個不同的類型Type1,Type2,List<Type1>和List<Type
>不可能是子類型或父類型的關係。
實際上,這不是泛型的缺陷,反而是數組的缺陷:
//fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";//Throws ArrayStoreException
//Won't compile
List<Object> listObject = new ArrayList<Long>();//Incompatible types
上述代碼,無論哪種方法,都不能將String放入Long容器中。但是利用列表,可以在編譯時發現錯誤。
二 數組是具體化的 在運行時才知道自己的類型,泛型通過擦除,在運行時丟棄類型信息
一般來說,數組和泛型不能很好地混合使用。
如果混合使用出現了編譯時錯誤,應該用列表替代數組。
26 優先考慮泛型
不能直接創建 new T[16],可以通過創建 (T[])new Object[16];強轉。也可以在 取出數據時強轉。
有些泛型如ArrayList,必須在數組上實現。
爲了提升性能,其他泛型如HashMap也在數組上實現。
27 優先考慮泛型方法
靜態工具方法尤其適用於泛型化。
泛型方法一個顯著特徵,可以配合 類型推導,無需明確指定類型參數的值。
28 利用 有限制通配符 來提升API的靈活性
PECS表示,producer(取)-extends,consumer(存)-super。
還要記住所有的comparable和comparator都是消費者。
爲了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入參數上使用通配符類型。
如果某個輸入參數 即是生產者又是消費者,則通配符類型對你沒有好處,需要嚴格的類型匹配。
不要用通配符作爲返回類型。
如果類的用戶必須考慮通配符類型,類的API或許就會出錯。
顯式的類型參數:
Set<Number> numbers = Union.<Number>union(integers,doubles);
一個複雜的例子:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
類型參數和通配符之間具有雙重性,許多方法都可以利用其中一個或者另一個進行聲明。
一般來說,如果類型參數只在方法聲明中出現一次,就可以用通配符取代它。
在公共API中,推薦第二種。
例如:
//類型參數和通配符之間具有雙重性,許多方法都可以利用其中一個或者另一個進行聲明
//一般來說,如果類型參數只在方法聲明中出現一次,就可以用通配符取代它。
public static <E> void swap1(List<E> list, int i, int j) {
}
public static void swap2(List<?> list, int i, int j) {
}
public static <E> void swap3(E list, int i, int j) {
}
//這個是錯誤的, 因爲E不是類型參數
/* public static void swap4(? list, int i, int j) {
}*/
29 優先考慮 類型安全 的 異構容器
類型安全:當你向它請求String時,它從來不會返回一個Integer給你
異構容器:不像普通的map,它的所有key都是不同類型的。
泛型最常用於集合。
當一個類的字面文字(Class<T>
)被用在方法中,來傳達編譯時和運行時的類型信息時,就被稱作type token
。
註解API廣泛利用了有限制的類型令牌。 被註解的元素,本質上是個類型安全的異構容器。
第六章 枚舉和註解
1.5中增加了 兩個新的引用類型:枚舉類型、註解類型。
30 用enum代替 int 常量。
枚舉易讀,提供了編譯時的類型安全。
允許添加任意的方法和域(本質上是個類)。
如果枚舉具有普適性,應該成爲一個頂層類。如果用在特定頂層類中,應該成爲該頂層類的一個成員類。
但枚舉有小小的性能缺點,裝載和初始化枚舉時會有空間和時間的成本。
31 用實例域 代替序數(ordinal())
永遠不要根據枚舉的序數導出與它關聯的值,而是要將它保存在一個實例域中。
最好完全避免ordinal方法的使用。
32 用EnumSet 代替位域
位域有更多缺點,打印時翻譯位域更困難,遍歷位域也困難。
EnumSet內部用單個long保存,(枚舉類型小於等於64個)
枚舉類型要用在集合中,所以沒有理由用位域來表示它。
用EnumSet,簡潔、性能也有優勢。
public static void main(String[] args) {
EnumSet<Style> bold = EnumSet.of(Style.BOLD, Style.ITALIC);
System.out.println(bold);
}
enum Style {
BOLD, ITALIC, UNDERLINE;
}
//[BOLD, ITALIC]
33 用EnumMap 代替序數(ordinal())索引
按Enum分組索引,key是Enum。
最好不要用序數來索引數組,而要使用EnumMap。
34 用接口模擬可伸縮的枚舉
用接口模擬可伸縮枚舉有個小小的不足,無法從一個枚舉類型繼承到另一個枚舉類型。
雖然無法便攜可擴展的枚舉類型,卻可以通過編寫接口以及實現該接口的基礎枚舉類型,對它進行模擬
35註解優先於 命名模式
命名模式缺點:
- 文字拼寫錯誤會導致失敗,且沒有任何提示。
- 無法確保它們只用於相應的程序元素上。
- 沒有提供將參數值與程序元素關聯起來的好辦法。
36堅持使用Override註解
37用標記接口定義類型
標記接口是沒有包含方法聲明的接口,而只是知名一個類實現了具有某種屬性的接口。
例如Serializable。
如果想要定義類型,一定要使用接口
第七章方法
38檢查參數的有效性
應該在文檔中清楚的指明參數的限制 以及 拋出的異常。
並在方法體的開頭檢查參數。
這是“應該在發生錯誤之後儘快檢測出錯誤”原則的具體情形。
檢查構造器參數的有效性是非常重要的。
39必要時進行保護性拷貝
編寫一些面對客戶的不良行爲時仍能保持健壯性的類,是非常值得的。
對構造器的每個可變參數進行保護性拷貝是必要的。
保護性拷貝是在檢查參數的有效性(上一條38)之前進行的,並且有效性檢查是針對拷貝之後的對象,而不是針對原始的對象。 這樣做可以避免在“危險階段”(從檢查參數開始,直到拷貝參數之間的時間段)期間從另一個線程改變類的參數。
對於參數類型可以被不可信任方子類化的參數,請不要使用clone方法進行保護性拷貝。
返回可變內部域的保護性拷貝
總結:
如果拷貝的成本受到限制,並且類信任它的客戶端不會不恰當的修改組件,就可以在文檔中說明客戶端的職責,代替保護性拷貝。
40謹慎設計方法簽名
本條目是若干API設計技巧的總結。
- 謹慎地選擇方法的名稱。易於理解、風格一致、大衆認可,可參考Java類庫。
- 不要過於追求提供便利的方法。方法太多會使類難以學習、使用、文檔化、測試和維護。爲每個動作提供一個功能齊全的方法,只有當一項操作被經常用到的時候,才考慮爲它提供快捷方式,如果不確定,還是不提供快捷爲好。
- 避免過長的參數列表。目標是四個參數或更少。 相同類型的長參數序列格外有害。(拆分、輔助類、Builder)
- 對於參數類型,要優先使用接口而不是類。
- 對於boolean參數,要優先使用兩個元素的枚舉類型。易於閱讀和編寫,更易於後期添加更多的選項。
41 慎用重載
重載是發生在編譯時的,所以嚴格的說,它並不是多態。
要調用哪個重載方法是在編譯時做出決定的。
對於重載方法的選擇是靜態(編譯時的對象類型決定)的,對於覆蓋的方法的選擇則是動態(運行時的對象類型決定)的。
避免胡亂地使用重載機制。
安全而保守的策略是,永遠不要導出兩個具有相同參數數目的重載方法。
如果方法使用可變參數,保守的策略是根本不要重載它。
例如,考慮ObjectOutputStream類,對於不同類型,提供諸如writeBoolean(boolean)、writeInt(int)方法,好處還可以提供對應的readBoolean()readInt()方法。
導致混亂主要是由於 參數是 有繼承關係的類型。應該避免 同一組參數只許經過類型轉換就可以被傳遞給不同的重載方法。
對於每一對重載方法,至少有一個對應的參數在兩個重載方法中具有“根本不同”的類型。
但是當自動裝箱和泛型成了Java語言的一部分之後,謹慎重載顯得更加必要了。
例如List的remove()方法。如果配合Integer,會搞不清是要按下標刪除還是刪除對應的元素。
如果被迫違反本規則,要保證讓更具體化的重載方法把調用轉發給更一般化的重載方法。
例如:
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence)sb);
}
42慎用可變參數
可變參數方法,接受,0個或多個指定類型的參數。
可變參數也會影響性能,每次方法調用都會導致數組進行一次分配和初始化。
43返回零長度的數組或者集合,而不是null
除非分析表明這個方法正是造成性能問題的真正源頭。
且返回同一零長度數組是可能的,因爲零長度數組不可變,可以被自由共享(15條)。
不可變的空集合:Collections.emptySet(),Collections.emptyList(),Collections.emptyMap(),
44爲所有導出的API元素編寫文檔註釋
第八章 通用程序設計
45將局部變量的作用域最小化
本條目與13條(使類和成員的可訪問性最小化)本質上是類似的。
- 在第一次使用它的地方聲明。
- 如果在循環終止之後不再需要循環變量的內容,for循環優先於while循環
- 使方法小而集中。
46for-each循環優先於傳統的for循環
集合數組,以及任意實現Iterable接口的對象,都能用for-each。
嵌套迭代時,優勢更明顯。
三種常見情況無法使用for-each:
- 過濾—-遍歷集合時要刪除選定的元素,需要用迭代器。
- 轉換—-遍歷列表或數組時,需要取代它部分或者全部的元素值。需要用迭代器或者數組索引。
- 平行迭代—–故意的嵌套迭代(很少使用)
47:瞭解和使用類庫
48:如果需要精確的答案,請 避免使用 float和double
它們尤其不適合於貨幣就算。
使用BigDecimal、int、long進行貨幣計算
49:基本類型優先於裝箱基本類型
它們的區別:
- 基本類型只有值,裝箱基本類型是引用
- 基本類型只有值,而裝箱基本類型還有null
- 基本類型通常更節省時間、空間
對裝箱基本類型運用 == 操作符,幾乎總是錯誤的。
當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型就會 自動拆箱。 null被拆箱就拋出NPE。
合理使用裝箱基本類型的地方:
- 集合中的元素、鍵、值。因爲基本類型不能放在集合中。
- 參數化類型中
- 反射的方法調用時
50:如果其他類型更適合,則儘量避免使用字符串。
- 字符串不適合代替其他的值類型。(從網絡接受一段數據)
- 字符串不適合代替枚舉類型
- 字符串不適合代替聚集類型。例如
String key = className + "#" + i.next();
應該用一個類來描述這個數據集,通常是一個私有的靜態成員類。 - 字符串也不適合代替能力表。
51:當心字符串連接的性能
爲連接n個字符串而重複地使用字符串連接操作符,需要n的平方級的時間。
52:通過接口引用對象
如果有合適的接口類型存在,對於 參數、返回值、變量、域來說,都應該使用接口類型進行聲明。
如果沒有,則使用類層次結構中提供了必要功能的最基礎的類。
使得程序更靈活。
53:接口優先於反射機制
缺點:
- 喪失了編譯時類型檢查的好處
- 反射的代碼長
- 性能損失
54:謹慎使用本地方法
使用本地方法來提高性能的做法不值得提倡。
如果本地代碼只是做少量的工作,本地方法就可能降低性能。
55:謹慎地進行優化
不要因爲性能而犧牲合理的架構。要努力編寫好的程序而不是快的程序。
好的程序體現了信息隱藏,只要有可能可以改變單個決策,而不會影響到系統其他部分。
努力避免那些限制性能的設計決策。
爲了獲得好的性能而對API進行包裝,這是一種非常不好的想法。(性能因素可能隨着版本時間而被解決,但是API會一直留在那裏限制自己)
構建完系統之後,要測量性能。利用各種工具,找出瓶頸。優化前、優化後也要測量。
56:遵守普遍接受的命名慣例
類型參數(泛型),通常是 T 任意類型,E 集合元素,KV鍵值,X異常。
方法常用動詞、動詞短語。