溫馨提示:該文章篇幅較長,選擇目錄中的具體項跳轉到目標章節較方便~
引言
Java語言支持四種類型:接口、類、數組、基本類型
接口、類、數組被稱爲引用類型,類實例和數組是對象,而基本類型的值則不是對象。
方法的簽名由它的名稱和所有參數類型組成,簽名不包括它的返回類型。
創建和銷燬對象
第1條:考慮用靜態工廠方法代替構造器
例:
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE ;
}
注意: 靜態工廠方法與設計模式中的工廠方法模式不同。
提供靜態工廠方法而不是公有的構造器的優勢:
靜態工廠方法與構造器不同的第一大優勢在於,它們有名稱。
例如:構造器BigInteger(int, int, Random)返回的BigInteger可能爲素數,如果用名爲BigInteger.probablePrime的靜態工廠方法來表示,顯然更爲清楚。
靜態工廠方法與構造器不同的第二大優勢在於,不必在每次調用它們的時候都創建一個新對象。
例如:不可變類可以使用預先構建好的實例,或者將構建好的實例緩存起來,進行重複利用,從而避免創建不必要的重複對象。
靜態工廠方法與構造器不同的第三大優勢在於,它們可以返回原返回類型的任何子類型的對象。
這項技術適用於基於接口的框架,因爲在這種框架中,接口爲靜態工廠方法提供了自然返回類型。(可參考本人的另一篇博文java服務提供者框架介紹)
靜態工廠方法與構造器不同的第四大優勢在於,在創建參數化類型實例的時候,它們使代碼變得更加簡潔。
例:在調用參數化類的構造器時,即使類型參數很明顯,也必須指明。這通常要求得接連兩次提供類型參數:
Map<String, List<String>> m = new HashMap<String, List<String>>();
隨着類型參數變得越來越長,越來越複雜,這一冗長的說明也很快變得痛苦起來。但是有了靜態工廠方法,編譯器就可以替你找到類型參數,這被稱作type inference(類型推導)。例如,假設HashMap提供了這個靜態工廠:
public static HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
你就可以用下面這句簡潔的代碼代替上面這段繁瑣的聲明:
Map<String, List<String>> m = HashMap.newInstance();
遺憾的是,到發行版本1.6爲止,標準的集合實現如HashMap並沒有工廠方法,但是可以把這些方法放在你自己的工具類中。更重要的是,可以把這樣的靜態工廠放在你自己的參數化的類中。
靜態工廠方法的缺點:
靜態工廠方法的主要缺點在於,類如果不含公有的或者受保護的構造器,就不能被子類化。
例如,要想將Collections Framework中的任何方便的實現類子類化,這是不可能的。但是這樣也許會因禍得福,因爲它鼓勵程序員使用複合(composition),而不是繼承。
靜態工廠方法的第二個缺點在於,它們與其他的靜態方法實際上沒有任何區別。
在API文檔中,它們沒有像構造器那樣在API文檔中明確標識出來,因此,對於提供了靜態工廠方法而不是構造器的類來說,要想查明如何實例化一個類,這是非常困難的。
你可以通過在類或者接口註釋中關注靜態工廠,並遵守標準的命名習慣,來縮小這一劣勢。下面是靜態工廠方法的一些慣用名稱:valueOf, of, getInstance, newInstance, getType, newType
第2條:遇到多個構造器參數時要考慮用構建器
遇到多個構造器參數的解決方案:
一、重疊構造器模式:但是當有許多參數的時候,客戶端代碼會很難編寫,並且仍然較難以閱讀。
二、JavaBeans模式:在構造過程中JavaBean可能處於不一致的狀態;JavaBeans模式阻止了把類做成不可變的可能。
三、Builder模式:不直接生成想要的對象,而是讓客戶端利用所有必要的參數調用構造器(或者靜態工廠),得到一個builder對象。然後客戶端在builder對象上調用類似於setter的方法,來設置每個相關的可選參數。最後,客戶端調用無參的build方法來生成不可變的對象。這個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 carbohydrate = 0;
private int sodium = 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 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).sodium(35).carbohydrate(27).build();
}
}
builder的setter方法返回builder本身,以便可以把調用鏈接起來。
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
Builder模式的確也有它自身的不足。爲了創建對象,必須先創建它的構建器。雖然創建構建器的開銷在實踐中可能不那麼明顯,但是在某些十分注重性能的情況下,可能就是個問題了。Builder模式還比telescoping constructor(重疊構造器)模式更加冗長,因此它只在有足夠參數的時候才使用,比如4個或者更多個參數。但是記住,將來你可能需要添加參數。如果一開始就使用構造器或者靜態工廠,等到類需要多個參數時才添加構建器,就會無法控制,那些過時的構造器或者靜態工廠顯得十分不協調。因此,通常最好一開始就使用構建器。
簡而言之,如果類的構造器或者靜態工廠中具有多個參數,設計這種類時,Builder模式就是種不錯的選擇,特別是當大多數參數都是可選的時候。與使用傳統的telescoping constructor模式相比,使用Builder模式的客戶端代碼將更易於閱讀和編寫,builders也比JavaBeans更加安全。
第3條:用私有構造器或者枚舉類型強化Singleton屬性
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
這種方法在功能上與公有域方法(public static Elvis getInstance()
)相近,但是它更加簡潔,無償地提供了序列化機制,絕對防止多次實例化,即使是在面對複雜的序列化或者反射攻擊的時候(享有特權的客戶端可以藉助於AccessibleObject.setAccessible方法,通過反射機制調用私有構造器。)。雖然這種方法還沒有被廣泛採用,但是單元素的枚舉類型已經成爲實現Singleton的最佳方法。
第4條:通過私有構造器強化不可實例化的能力
很多工具類(utility class)不是要被實例化的:實例沒有任何意義。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
}
AssertionError不是絕對必要,但是它可以避免不小心在類的內部調用構造器。它保證該類在任何情況下都不會被實例化。
第5條:避免創建不必要的對象
一般來說,最好是重用對象而不是在每次需要的時候就創建一個相同功能的新對象。重用方式既更加快速,也更爲流行。如果對象是不可變的(immutable),它就始終可以被重用。
String s = new String("stringette");
//改進
String s = "stringette" ;
除了重用不可變的對象之外,也可以重用那些已知不會被修改的可變對象。
/*
這個類建立了一個模型:其中有一個人,並有一個isBabyBoomer方法,用來檢驗這個人是否爲一個"baby boomer(生育高峯期出生的小孩)",換句話說,就是檢驗這個人是否出生於1946至1964年間:
*/
// Creates lots of unnecessary duplicate objects
import java.util.*;
public class Person {
private final Date birthDate;
public Person(Date birthDate) {
// Defensive copy - see Item 39
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods omitted
// DON'T DO THIS!
public boolean isBabyBoomer() {
// Unnecessary allocation of expensive object
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
}
isBabyBoomer每次被調用的時候,都會新建一個Calendar、一個TimeZone和兩個Date實例,這是沒有必要的。下面的版本使用了一個靜態的初始化器(initializer),避免了這種效率低下的情況:
// Doesn't creates unnecessary duplicate objects
import java.util.*;
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;
}
}
要優先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱。
public class Sum {
// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
}
這段程序算出的答案是正確的,但是比實際情況要更慢一些,只因爲打錯了一個字符。變量sum被聲明成Long而不是long,意味着程序構造了大約2^31個多餘的Long實例(大約每次往Long sum中增加long時構造一個實例)。
不要錯誤地認爲本條目所介紹的內容暗示着”創建對象的代價非常昂貴,我們應該要儘可能地避免創建對象”。相反,由於小對象的構造器只做很少量的顯式工作,所以,小對象的創建和回收動作是非常廉價的,特別是在現代的JVM實現上更是如此。通過創建附加的對象,提升程序的清晰性、簡潔性和功能性,這通常是件好事。
反之,通過維護自己的對象池(object pool)來避免創建對象並不是一種好的做法,除非池中的對象是非常重量級的。真正正確使用對象池的典型對象示例就是數據庫連接池。建立數據庫連接的代價是非常昂貴的,因此重用這些對象非常有意義。而且,數據庫的許可可能限制你只能使用一定數量的連接。但是,一般而言,維護自己的對象池必定會把代碼弄得很亂,同時增加內存佔用(footprint),並且還會損害性能。現代的JVM實現具有高度優化的垃圾回收器,其性能很容易就會超過輕量級對象池的性能。
與本條目對應的是第39條中有關”保護性拷貝(defensive copying)”的內容。本條目提及”當你應該重用現有對象的時候,請不要創建新的對象”,而第39條則說”當你應該創建新對象的時候,請不要重用現有的對象”。注意,在提倡使用保護性拷貝的時候,因重用對象而付出的代價要遠遠大於因創建重複對象而付出的代價。必要時如果沒能實施保護性拷貝,將會導致潛在的錯誤和安全漏洞;而不必要地創建對象則只會影響程序的風格和性能。
第6條:消除過期的對象引用
如果一個棧先是增長,然後再收縮,那麼,從棧中彈出來的對象將不會被當作垃圾回收,即使使用棧的程序不再引用這些對象,它們也不會被回收。這是因爲,棧內部維護着對這些對象的過期引用(obsolete reference)。所謂的過期引用,是指永遠也不會再被解除的引用。
這類問題的修復方法很簡單:一旦對象引用已經過期,只需清空這些引用即可。對於Stack類而言,只要一個單元被彈出棧,指向它的引用就過期了。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
清空對象引用應該是一種例外,而不是一種規範行爲。消除過期引用最好的方法是讓包含該引用的變量結束其生命週期。如果你是在最緊湊的作用域範圍內定義每一個變量,這種情形就會自然而然地發生。
一般而言,只要類是自己管理內存,程序員就應該警惕內存泄漏問題。一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。
內存泄漏的另一個常見來源是緩存。一旦你把對象引用放到緩存中,它就很容易被遺忘掉,從而使得它不再有用之後很長一段時間內仍然留在緩存中。(當所要緩存的項的生命週期是由該鍵的外部引用而不是由值決定時,可用WeakHashMap解決該問題:在 WeakHashMap 中,當某個鍵不再正常使用時,將自動移除其條目;LinkedHashMap類利用它的removeEldestEntry方法可以很容易地清除掉沒用的緩存項)
內存泄漏的第三個常見來源是監聽器和其他回調。如果你在實現的是客戶端註冊回調卻沒有顯式地取消註冊的API,除非你採取某些動作,否則它們就會積聚。確保回調立即被當作垃圾回收的最佳方法是隻保存它們的弱引用(weak reference),例如,只將它們保存成WeakHashMap中的鍵。
第7條:避免使用終結方法
終結方法(finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。
Java語言規範不僅不保證終結函數會被及時地執行,而且根本就不保證它們會被執行。
還有一點:使用終結函數有一個非常嚴重的(Severe)性能損失。在我的機器上,創建和銷燬一個簡單對象的時間大約爲5.6ns。增加一個終結函數使時間增加到了2,400ns。換句話說,用終結函數創建和銷燬對象慢了大約430倍。
如果類的對象中封裝的資源(例如文件或者線程)確實需要終止,應該怎麼做才能不用編寫終結方法呢?只需提供一個顯式的終止方法,並要求該類的客戶端在每個實例不再有用的時候調用這個方法。顯式的終止方法通常與try-finally結構結合起來使用,以確保及時終止。在finally子句內部調用顯式的終止方法,可以保證即使在使用對象的時候有異常拋出,該終止方法也會被執行:
// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
foo.terminate(); // Explicit termination method
}
除非是作爲安全網,或者是爲了終止非關鍵的本地資源,否則請不要使用終結方法。在這些很少見的情況下,既然使用了終結方法,就要記住調用super.finalize (因爲”終結方法鏈(finalizer chaining)”並不會被自動執行)。如果用終結方法作爲安全網,要記得記錄終結方法的非法用法。最後,如果需要把終結方法與公有的非final類關聯起來,請考慮使用終結方法守衛者(把終結方法放在一個匿名的類中,該匿名類的唯一用途就是終結它的外圍實例),以確保即使子類的終結方法未能調用super.finalize,該終結方法也會被執行。
對於所有方法都通用的方法
第8條:覆蓋equals時請遵守通用約定
需要覆蓋equals:如果類具有自己特有的“邏輯相等”概念,而且超類還沒有覆蓋equals以實現期望的行爲,這時我們就需要覆蓋equals方法。
不需要覆蓋equals:用實例受控確保“每個值至多隻存在一個對象”的類。枚舉類型就屬於這種類。對於這樣的類而言,邏輯相同與對象等同是一回事。
equals方法實現等價關係通用約定(equivalence relation):
- 自反性(reflexive):對於任何非null的引用值x,x.equals(x)必須返回true。
- 對稱性(symmetric):對於任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。
- 傳遞性(transitive):對於任何非null的引用值x、y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那麼x.equals(z)也必須返回true。
- 一致性(consistent):對於任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用x.equals(y)就會一致地返回true,或者一致地返回false。
- 非空性(Non-nullity):對於任何非null的引用值x,x.equals(null)必須返回false。
實現高質量equals方法的訣竅:
- 使用==操作符檢查“參數是否爲這個對象的引用”。
- 使用instanceof操作符檢查“參數是否爲正確的類型”。
- 把參數轉換成正確的類型。
- 對於該類的每個“關鍵(significant)”域,檢查參數中的域是否與該對象中對應的域相匹配。
- 當你編寫完成了equals方法之後,應該會問自己三個問題:它是否是對稱的、傳遞的、一致的。
最後的一些告誡:
- 覆蓋equals時總要覆蓋hashcode
- 不要企圖讓equals方法過於智能
- 不要將equals聲明中的Object對象替換爲其他的類型
儘量不要省略@Override。
第9條:覆蓋equals時總要覆蓋hashCode
Object.hashCode的通用約定
- 在引用程序的執行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那麼對這同一個對象調用多次,hashCode方法都必須始終如一地返回同一個整數。在一個應用程序的多次執行過程中,每次執行所返回的整數可以不一致。
- 如果兩個對象根據equals方法比較是相等的,那麼調用這兩個對象中任意一個對象的hashCode方法都必須產生同樣的整數結果。
- 如果兩個對象根據equals方法比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode方法,則不一定要產生不同的整數結果。但是程序員應該知道,給不相等的對象產生截然不同的整數結果,有可能提高散列表(hash table)的性能。
寫完了hashCode方法之後,問問自己“相等的實例是否都具有相等的散列碼”。
不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提高性能。
第10條:始終要覆蓋toString
Object提供的toString方法的實現 : 類的名稱+@+散列碼的無符號十六進制表示
當對象被傳遞給println,printf,字符串連接符(+)以及assert或者被調試器打印出來時,toString方法會被自動調用。
在實現toString的時候,必須要做出一個很重要的決定:是否在文檔中指定返回值的格式。
無論你是否決定指定格式,都應該在文檔中明確地表明你的意圖;無論是否指定格式,都爲 toString返回值中包含的所有信息,提供一種編碼式的訪問途徑。如果沒有提供,即使你已經指明瞭字符串的格式是可以變化的,這個字符串格式也成了事實上的API。
第11條:謹慎地覆蓋clone
Object中clone方法的定義是:
protected native Object clone() throws CloneNotSupportedException;
首先它是保護的,其次它是native(本地)的,也就是說它是通過其他語言編寫的代碼,是看不到源碼的,最後它可能拋出CloneNotSupportedException,在類不支持克隆時。
Cloneable接口:
public interface Cloneable {
}
它其實是空空的,具體方法一個也沒有。那麼它到底做了什麼呢?它決定了Object中受保護的clone方法實現的行爲:如果一個類實現了Cloneable,則Object的clone方法返回該對象的逐域拷貝,否則的話拋出一個CloneNotSupportedException異常。這是接口的一種極端非典型的用法,不值得效仿。
那麼既然實現了Cloneable接口後,就可以調用Object中的clone方法了(Cloneable接口改變了超類中一個受保護的方法的行爲),那我們的目的不就達到了嗎?幹嗎還要改寫clone呢?
Object的clone方法,只能逐域拷貝那些原語類型,對於類僅僅是地址賦值,換句話說,它只是逐域在做 = 操作。這樣並不是完全的克隆,所以我們需要改寫clone方法。
Object中關於克隆的約定:
1) x.clone() != x ,將會爲 true
2) x.clone().getClass() == x.getClass() ,將會爲 true
3) x.clone().equals(x) , 將會爲 true
“將會爲true”,但是這也不是一個絕對要求。拷貝往往會導致創建一個新實例,但同時也會要求拷貝內部的數據結構。這個過程中沒有調用構造函數。
“沒有調用構造函數”和“x.clone().getClass() == x.getClass() ” 的綜合,導致結果就是:如果你改寫一個非final類的clone方法,則應該返回一個通過調用super.clone而得到的對象。這其實也相當於給我們提供了一個改寫clone方法的“處方”:不要使用構造函數來創建類,而是使用超類的clone方法。
如果對象中的域引用了可變的對象,使用上述這種簡單的clone實現可能會導致災難性的後果。
例:
// A cloneable version of Stack
import java.util.Arrays;
public class Stack implements Cloneable {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
public boolean isEmpty() {
return size == 0;
}
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// To see that clone works, call with several command line arguments
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
Stack copy = stack.clone();
while (!stack.isEmpty())
System.out.print(stack.pop() + " ");
System.out.println();
while (!copy.isEmpty())
System.out.print(copy.pop() + " ");
}
}
這樣得到的Stack實例,size域中具有正確的值,但是elements域將引用與原始Stack實例相同的數組。修改原始的實例會破壞被克隆對象中的約束條件,反正亦然。很快你就會發現,這個程序將產生無意義的結果,或者拋出NullPointerException。
爲了使Stack類中的clone方法正常地工作,它必須要拷貝棧的內部信息。
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
// 在elements數組中遞歸地調用clone
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
實際上,clone方法就是另一個構造器,你必須確保它不會傷害到原始的對象,並確保正確地創建被克隆對象中的約束條件。
如果elements域是final的,上述方案就不能正常工作,因爲clone方法是被禁止給elements域賦新值的。clone架構與引用可變對象的final域的正常用法是不相兼容的,除非在原始對象與克隆對象之間可以安全地共享此可變對象。爲了使類成爲可克隆的,可能有必要從某些域中去掉final修飾符。
最好提供某些其他途徑來代替對象拷貝,或者乾脆不提供這樣的功能。一個好的代替方法是提供一個拷貝構造器:
public Yum( Yum yum);
或拷貝工廠:
public static Yum newInstance( Yum yum);
對於一個專門爲了繼承而設計的類,如果你未能提供行爲良好的受保護的clone方法,它的子類就不可能實現Cloneable接口。
第12條:考慮實現Comparable接口
compareTo方法並不是Object類定義的方法。它是Comparable接口中的唯一的一個方法。它和Object類的equals方法類似,只是它允許指定自定義的比較,而不是簡單的相等性比較;並且它是通用的。
類實現了Comparable接口,就表明它的實例具有內在的排序關係。例:爲實現了Comparable接口的對象數組進行排序:Arrays.sort(a);
compareTo方法的通用約定與equals方法的類似:
(sgn表示數學中的signum函數,它根據表達式的值爲負值、零和正值分別返回-1、0、1)
- 實現者必須確保所有的x和y都滿足sgn( x.compareTo ( y )) == -sgn( y.compareTo( x ))。(這也暗示着,當且僅當 y.compareTo( x )拋出異常時,x.compareTo ( y )才必須拋出異常。)
- 實現者還必須確保這個比較關係是可傳遞的:( x.compareTo ( y )>0 && y.compareTo( z )>0 ) 暗示着 x.compareTo ( z )>0
- 最後,實現者必須確保 x.compareTo ( y )==0 暗示着所有的z都滿足( sgn(x.compareTo(z)) == sgn(y.compareTo(z)))
- 強烈建議(x.compareTo(y)==0) == x.equals(y) ,但這並非絕對必要。一般而言,任何實現了Comarable接口的類,若違反了這個條件,應該明確予以說明。推薦這樣的說法:“注意:該類具有內在的排序功能,但是與equals不一致。”
與equals不同的是,在跨越不同類的時候,compareTo可以不做比較:如果兩個被比較的對象引用不同類的對象,compareTo可以拋出ClassCastException異常。
類和接口
第13條:使類和成員的可訪問性最小化
對於公有類的成員,當訪問級別從包級私有變成保護級別時,會大大增強可訪問性。受保護的成員是類的導出的API的一部分,必須永遠得到支持。導出的類的受保護成員也代表了該類對於某個實現細節的公開承諾。受保護的成員應該儘量少用。
除了公有靜態final域的特殊情形之外,公有類都不應該包含公有域。並且要確保公有靜態final域所引用的對象都是不可變的。
第14條:在公有類中使用訪問方法而非公有域
公有類永遠都不應該暴露可變的域。雖然還是有問題,但是讓公有類暴露不可變的域其危害比較小。但是,有時候會需要用包級私有的或者私有的嵌套類來暴露域,無論這個類是可變的還是不可變的。
第15條:使可變性最小化
不可變的類比可變的類更容易設計、實現和使用。它們不容易出錯,且更安全。
爲了使類成爲不可變,要遵循下面五條規則:
- 爲了使類成爲不可變,要遵循下面五條規則:
- 保證類不會被擴展。
- 使所有的域都是final的。
- 使所有的域都成爲私有的。
- 確保對於任何可變組件的互斥訪問。
不可變對象本質上是線程安全的,它們不要求同步。
不可變對象可以自由地共享。
“不可變對象可以被自由的共享”導致的結果是,永遠也不需要進行保護性拷貝。
不僅可以共享不可變對象,甚至也可以共享它們的內部信息。
不可變對象爲其他對象提供了大量的構件。
不可變對象真正唯一的缺點是,對於每一個值都需要一個單獨的對象。
讓不可變對象變成final的另外一個方法就是,讓類的所有構造器都變成私有的或者包級私有的,並添加公有的靜態工廠(static factory)來替代公有構造器。
除非有很好的理由要讓類成爲可變的類,否則就應該是不可變的。
如果類不能被做成是不可變的,仍然應該儘可能地限制它的可變性。
除非有令人信服的理由要使域變成是非final的,否則要使每個域都是final的。
第16條:複合優先於繼承
繼承違背了封裝原則,只有當子類真正是超類的子類型時,才適合用繼承。換句話說,對於兩個類A和B,只有當兩者之間確實存在” is-a “關係的時候,類B才應該擴展類A。
在java平臺類庫中,有許多明顯違反這條原則的地方。例如:棧( Stack )並不是向量( Vector ),所以Stack不應該擴展Vector 。同樣的,屬性列表也不是散列表,所以Properties不應該擴展HashTable。在這兩種情況下,複合模式纔是恰當的。
繼承機制會把超類API中的所有缺陷傳播到子類中,而複合則允許設計新的API來隱藏這些缺陷。
如果子類和超類處在不同的包中,並且超類並不是爲了繼承而設計的,那麼繼承將會導致脆弱性,可以用複合和轉發機制來代替繼承,尤其是當存在適當的接口可以實現包裝類的時候。包裝類比子類更加健壯,而且功能也更加強大。
第17條:要麼爲繼承而設計,並提供文檔說明,要麼就禁止繼承
好的API文檔應該描述一個給定的方法做了什麼工作,而不是描述它是如何做到的。
對於爲了繼承而設計的類,唯一的測試方法就是編寫子類。
爲了允許繼承,類還必須遵守一些其他約束。構造器決不能調用可被覆蓋的方法,無論是直接調用還是間接調用。
如果你決定在一個爲了繼承而設計的類中實現Cloneable或者Serializable接口,就應該意識到,因爲clone和readObject方法在行爲上非常類似於構造器,所以類似的限制也是適用的:無論是clone還是readObject,都不可以調用可覆蓋的方法,不管是以直接的還是間接的方式。
如果你決定在一個爲了繼承而設計的類中實現Serializable,並且該類有一個readResolve或者writeReplace方法,就必須使readResolve或者writeReplace成爲受保護的方法,而不是私有的方法。如果這些方法是私有的,子類將會不聲不響地忽略掉這兩個方法。這正是“爲了允許繼承,而把實現細節變成類的API的一部分“的另一種情形。
對於那些並非爲了安全地進行子類化而設計和編寫文檔的類,要禁止子類化。有兩種辦法可以禁止子類化:把這個類聲明爲final的;把所有的構造器都變成私有的,或者包級私有的,並增加一些公有的靜態工廠來替代構造器。
用”直接調用可覆蓋方法的私有輔助方法“來代替”可覆蓋方法的每個自用調用“。
第18條:接口優於抽象類
接口和抽象類的區別:
- 抽象類允許包含某些方法的實現,接口則不允許。
- 爲了實現由抽象類定義的類型,類必須成爲抽象類的一個子類;任何一個類只要定義了所有必要的方法,並且遵守通用約定,就被允許實現一個接口,而不管這個類處於類層次的哪個位置。
接口的優勢:
- 現有的類可以很容易被更新,以實現新的接口。
- 接口是定義mixin(混合類型)的理想選擇。
- 接口允許我們構造非層次結構的類型框架。
通過對你導出的每個重要接口都提供一個抽象的骨架實現(skeletal implementation)類,把接口和抽象類的優點結合起來。接口的作用仍然是定義類型,但是骨架實現類接管了所有與接口實現相關的工作。按照慣例,骨架實現類被稱爲AbstractInterface。如:AbstractSet。
簡單實現:像骨架實現,它實現了接口,並且是爲了繼承而設計的,但是區別在於它不是抽象的,它是最簡單的可能的有效實現,你可以原封不動地使用,也可以看情況將它子類化。例如: AbstractMap.SimpleEntry。
設計公有的接口要非常謹慎。接口一旦被公開發行,並且已被廣泛實現,再想改變這個接口幾乎是不可能的。
簡而言之,接口通常是定義允許多個實現的類型的最佳途徑。這條規則有個例外,即當演變的容易性比靈活性和功能更爲重要的時候。在這種情況下,應該使用抽象類來定義類型,但前提是必須理解並且可以接受這些侷限性。如果你導出了一個重要的接口,就應該堅決考慮同時提供骨架實現類。最後,應該儘可能謹慎地設計所有的公有接口,並通過編寫多個實現來對它們進行全面的測試。
第19條:接口只用於定義類型
當類實現接口時,接口就充當可以引用這個類的實例的類型。
常量接口:不包含任何方法,只包含靜態的final域,每個域都導出一個常量。使用這些常量的類實現這個接口,以避免用類名來修飾常量名。
例:
// Constant interface antipattern - don not use!
public interface PhysicalConstants {
// Avogadro's number (1/mol)
static final double AVOGADROS_NUMBER = 6.02214199e23;
// Boltzmann constant (J/K)
static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
// Mass of the electron (kg)
static final double ELECTRON_MASS = 9.10938188e-31;
}
常量接口模式是對接口的不良使用。
第20條:類層次優先於標籤類
標籤類過於冗長、容易出錯,並且效率低下。
例:標籤類
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only if shape is CIRCLE
double radius;
// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// Constructor for rectangle
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
第21條:用函數對象表示策略
爲了在java中實現策略模式,要聲明一個接口來表示該策略,並且爲每個具體策略聲明一個實現了該接口的類。當一個具體策略只被使用一次時,通常使用匿名類來聲明和實例化這個具體策略類。當一個具體策略是設計用來重複使用的時候,它的類通常就要被實現爲私有的靜態成員類,並通過公有的靜態final域被導出,其類型爲該策略接口。
//策略接口
public interface Comparator<T> {
public int compare(T t1, T t2);
}
//匿名類實現
Arrays.sort(stringArray,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return 0;
}
});
//私有靜態成員類實現
public class Host {
private static class StrLenCmp implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}
第22條:優先考慮靜態成員類
嵌套類有四種:靜態成員類(static member class)、非靜態成員類(nonstatic member class)、匿名類(anonymous class)和局部類(local class)。除了第一種之外,其他三種都稱爲內部類(inner class)。
如果嵌套類的實例可以在它的外圍類的實例之外獨立存在,這個嵌套類就必須是靜態成員類;在沒有外圍實例的情況下,要想創建非靜態成員類的實例是不可能的。
匿名類可以出現在代碼中任何允許存在表達式的地方。當且僅當匿名類出現在非靜態的環境中時,它纔有外圍實例。但是即使它們出現在靜態的環境中,也不可能擁有任何靜態成員。
匿名類常見用法:動態地創建函數對象(見第21條);創建過程對象,如Runnable,Thread;在靜態工廠方法的內部(見第18條intArrayAsList)。
局部類與其他三種嵌套類中的每一種都有一些共同的屬性。與成員類一樣,局部類有名字,可以被重複地使用。與匿名類一樣,只有當局部類是在非靜態環境中定義的時候,纔有外圍實例,它們也不能包含靜態成員。與匿名類一樣,它們必須非常簡短,以便不會影響到可讀性。
如果一個嵌套類需要在單個方法之外仍然是可見的,或者它太長了,不適合方法內部,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類做成非靜態的;否則就做成靜態的。假設這個嵌套類屬於一個方法的內部,如果你需要在一個地方創建實例,並且已經有了一個預置的類型可以說明這個類的特徵,就把它做成匿名類;否則,就做成局部類。
泛型
第23條:請不要在新代碼中使用原生態類型
原生態類型:不帶任何實際類型參數的泛型名稱。如:與 List<E>
對應的原生態類型是List 。
如果使用原生態類型,就失掉了泛型在安全性和表述性方面的優勢。
泛型有子類型化的規則,List<String>
是原生態類型List的一個子類型,而不是參數化類型List<Object>
的子類型。
不要在新代碼中使用原生態類型,這條規則有兩個例外,兩者都源於“泛型信息可以在運行時被擦除”:
- 在類文字中必須使用原生態類型。如:List.class,String[].class,int.class合法;
List<String>.class
,List<?>.class
不合法。 - 在參數化類型而非無限制通配符類型上使用instanceof操作符是非法的。用無限制通配符代替原生態類型,對instanceof操作符的行爲不會產生任何影響。在這種情況下,
<?>
就顯得多餘了。下面是利用泛型來使用instanceof操作符的首選方法:
if(o instanceof Set){
Set<?> m = Set<?> o;
}
注意,一旦確定這個o是個 Set,就必須將它轉換成通配符類型Set<?>
,而不是轉換成原生態類型Set。這是個受檢的轉換,因此不會導致編譯期警告。
第24條:消除非受檢警告
要儘可能地消除每一個非受檢警告。
如果無法消除警告,同時可以證明引起警告的代碼是類型安全的,(只有在這種情況下才)可以用一個@SuppressWarnings(“unchecked”)註解來禁止這條警告。如果忽略而不是禁止明知道是安全的非受檢警告,那麼當新出現一條真正有問題的警告時,你也不會注意到。新出現的警告就會淹沒在所有的錯誤警告當中。
SuppressWarnings註解可以用在任何粒度的級別中,從單獨的局部變量聲明到整個類都可以。應該始終在儘可能小的範圍中使用SuppressWarnings。
每當使用@SuppressWarnings(“unchecked”)註解時,都要添加一條註釋,說明爲什麼這麼做是安全的。
第25條:列表優先於數組
數組是協變的,如果Sub爲Super的子類型,那麼數組類型Sub[]就是Super[]的子類型;泛型是不可變的,對於任意兩個不同的類型Type1和Type2,
List<Type1>
既不是List<Type2>
的子類型,也不是其超類型。數組是具體化的,在運行時才知道並檢查它們的元素類型約束;泛型則是通過擦除實現的,只在編譯時強化它們的類型信息,並在運行時丟棄(或者擦除)它們的元素類型信息。擦除就是使泛型可以與沒有使用泛型的代碼隨意進行交互。
由於上述這些根本的區別,數組和泛型不能很好地混合使用。例如,創建泛型、參數化類型或者類型參數的數組是非法的。非法的數組創建表達式:new List<E>[]
,new List<String>[]
,new E[]
。
從技術的角度來說,像E
,List<E>
,和List<Sting>
這樣的類型應稱作不可具體化的類型。不可具體化的類型是指其運行時表示法包含的信息比它的編譯時表示法包含的信息更少的類型。唯一可具體化的參數化類型是無限制的通配符類型,如List<?>
和Map<?,?>
。雖然不常用,但是創建無限制通配符類型的數組是合法的。
第26條:優先考慮泛型
使用泛型比使用需要在客戶端代碼中進行轉化的類型來得更安全,也更容易。在設計新類的時候,要確保它們不需要這種轉換就可以使用。這通常意味着要把類做成是泛型的。
第27條:優先考慮泛型方法
泛型方法像泛型一樣,使用起來比要求客戶端轉換輸入參數並返回值的方法來得更安全,也更容易。
第28條:利用有限制通配符來提升API的靈活性
public class Stack<E>{
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
public void pushAll(Iterable<E> src){
for(E e:src){
push(e);
}
}
}
則以下用法出錯:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers=...;
numberStack.pushAll(integers);
因爲參數化類型是不可變的,Iterable<Integer>
不是Iterable<Number>
的子類。
修改:
public void pushAll(Iterable<? extends E> src){
for(E e:src){
push(e);
}
}
E的子類型的集合(包括E):? extends E;
E的超類型的集合(包括E):? super E。
如果參數化類型表示一個T生產者, 就使用<? extends T>
;
如果參數化類型表示一個T消費者, 就使用<? super T>
。
所有的comparable和comparator都是消費者,使用時Comparable<? super T>
優先於Comparable<T>
,Comparator<? super T>
優先於Comparator<T>
。
類型參數和通配符之間具有雙重性,許多方法都可以利用其中一個或者另一個進行聲明。
public static <E> void swap(List<E> list,int i,int j);
public static void swap(List<?> list,int i,int j);
在公共API中,第二種更好一些,因爲它更簡單。
一般來說,如果類型參數只在方法聲明中出現一次,就可以用通配符取代它。如果是無限制的類型參數,就用無限制的通配符取代它;如果是有限制的類型參數,就用有限制的通配符取代它。
但是將swap方法用於第二種聲明會有問題:
public static void swap(List<?> list,int i,int j){
list.set(i,list.set(j,list.get(i)));
}
list的類型爲List<?>
,你不能把null之外的任何值放到List<?>
中。
改進:編寫私有的輔助方法來捕捉通配符類型
public static void swap(List<?> list,int i,int j){
swapHelper(list,i,j);
}
private static <E> void swapHelper(List<E> list,int i,int j){
list.set(i,list.set(j,list.get(i)));
}
swap的這種實現允許我們導出比較好的基於通配符的聲明,同時在內部利用更加複雜的泛型方法。swap方法的客戶端不一定要面對更加複雜的swapHelper聲明,但是它們的確從中受益。
第29條:優先考慮類型安全的異構容器
// Typesafe heterogeneous container
import java.util.*;
public class Favorites {
// Typesafe heterogeneous container pattern - implementation
private Map<Class<?>, Object> favorites =
new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString,
favoriteInteger, favoriteClass.getName());
}
}
存在的限制:
(1) 惡意的客戶端可以輕鬆地破壞Favorites實例的類型安全,只要它以原生態形式使用Class對象。 改進:
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put( type, type.cast(instance) );
}
(2) 不能用在不可具體化的類型中。不完全令人滿意的解決方法:有限制的類型令牌(bounded type token):
// Use of asSubclass to safely cast to a bounded type token
import java.lang.annotation.*;
import java.lang.reflect.*;
public class PrintAnnotation {
// Use of asSubclass to safely cast to a bounded type token
static Annotation getAnnotation(AnnotatedElement element,
String annotationTypeName) {
Class<?> annotationType = null; // Unbounded type token
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(
annotationType.asSubclass(Annotation.class));
}
// Test program to print named annotation of named class
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.out.println(
"Usage: java PrintAnnotation <class> <annotation>");
System.exit(1);
}
String className = args[0];
String annotationTypeName = args[1];
Class<?> klass = Class.forName(className);
System.out.println(getAnnotation(klass, annotationTypeName));
}
}
枚舉和註解
第30條:用enum代替int常量
java的枚舉本質上是int值。
// Enum type with data and behavior
//每個枚舉常量後面括號中的值就是傳遞給構造器的參數
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
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
}
}
java中printf的換行爲%n。
特定於常量的方法實現:
public enum Operation{
PLUS { double apply(double x,double y){ return x+y; } },
MINUS { double apply(double x,double y){ return x-y; } },
TIMES { double apply(double x,double y){ return x*y; } },
DIVIDE { double apply(double x,double y){ return x/y; } };
abstract double apply(double x,double y);
}
下面的Operation覆蓋了toString來返回通常與該操作關聯的字符:
// Enum type with constant-specific class bodies and data
import java.util.*;
public enum Operation {
PLUS("+") {
double apply(double x, double y) { return x + y; }
},
MINUS("-") {
double apply(double x, double y) { return x - y; }
},
TIMES("*") {
double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return 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(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
編寫一個fromString方法,將定製的字符串表示法變回相應的枚舉:
// Implementing a fromString method on an enum type
private static final Map<String, Operation> stringToEnum
= new HashMap<String, Operation>();
static { // Initialize map from constant name to enum constant
for (Operation op : values())
stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}
策略枚舉:
// The strategy enum pattern
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);
}
}
}
如果多個枚舉常量同時共享相同的行爲,則考慮策略枚舉。
第31條:用實例域代替序數
永遠不要根據枚舉的序數導出與它關聯的值(ordinal),而是要將它保存在一個實例域中:
// Enum with integer data stored in an instance field
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
ordinal:大多數程序員都不需要這個方法,它是設計成用來像EnumSet和EnumMap這種基於枚舉的通用數據結構的。除非你在編寫的是這種數據結構,否則最好完全避免使用ordinal方法。
第32條:用EnumSet代替位域
// EnumSet - a modern replacement for bit fields
import java.util.*;
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
// Body goes here
}
// Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
第33條:用EnumMap代替序數索引
// Using a nested EnumMap to associate data with enum pairs
//Map(起始階段,Map(目標階段,過渡階段))
import java.util.*;
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase src;
private final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase,Transition>> m =
new EnumMap<Phase, Map<Phase,Transition>>(Phase.class);
static {
for (Phase p : Phase.values())
m.put(p,new EnumMap<Phase,Transition>(Phase.class));
for (Transition trans : Transition.values())
m.get(trans.src).put(trans.dst, trans);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
// Simple demo program - prints a sloppy table
public static void main(String[] args) {
for (Phase src : Phase.values())
for (Phase dst : Phase.values())
if (src != dst)
System.out.printf("%s to %s : %s %n", src, dst,
Transition.from(src, dst));
}
}
最好不要用序數來索引數組,而要使用EnumMap。如果你所表示的這種關係是多維的,就使用EnumMap<...,EnumMap<...>>
。應用程序的程序員在一般情況下都不使用Enum.ordinal。
第34條:用接口模擬可伸縮的枚舉
// Emulated extensible enum using an interface
public interface Operation {
double apply(double x, double y);
}
// Emulated extensible enum using an interface - Basic implementation
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
// Emulated extension enum - extended implementation
import java.util.*;
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
// Test class to exercise all operations in "extension enum"
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
System.out.println(); // Print a blank line between tests
test2(Arrays.asList(ExtendedOperation.values()), x, y);
}
// test parameter is a bounded type token (Item 29)-有限制的令牌環
private static <T extends Enum<T> & Operation> void test(
Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
// test parameter is a bounded wildcard type (Item 28)-有限制的通配符
private static void test2(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
<T extends Enum<T> & Operation>
確保了Class對象既表示枚舉又表示Operation的子類型。
雖然無法編寫可擴展的枚舉類型,卻可以通過編寫接口以及實現該接口的基礎枚舉類型,對它進行模擬。
第35條:註解優先於命名模式
命名模式:被用來表明有些程序元素需要通過某種工具或者框架進行特殊處理。如:JUnit測試框架原本要求它的用戶一定要用test作爲測試方法名稱的開頭。
缺點:
- 文字拼寫錯誤會導致失敗,且沒有任何提示。
- 無法確保它們只用於相應的程序元素上。
- 沒有提供將參數值與程序元素關聯起來的好方法。
// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
* 只用於無參的靜態方法
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME)元註解表明,test註解應該在運行時保留。如果沒有保留,測試工具就無法知道Test註解。
@Target(ElementType.METHOD)元註解表明,Test註解只在方法聲明中才是合法的:它不能運用到類聲明、域聲明或者其他程序元素上。
標記註解:沒有參數,只是“標註”被註解的元素。
// Program containing marker annotations
public class Sample {
@Test public static void m1() { } // Test should pass
public static void m2() { }
@Test public static void m3() { // Test Should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
沒有用Test註解進行標註的4個方法會被測試工具忽略。
// Annotation type with an array parameter
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method that
* must throw the any of the designated exceptions to succeed.
* 帶參數的註解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
//參數數組
Class<? extends Exception>[] value();
//單個參數
//Class<? extends Exception> value();
}
// Program containing annotations with a parameter
import java.util.*;
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // Should fail (no exception)
// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<String>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
}
測試運行類:
// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class testClass = Class.forName(args[0]);//測試Sample類
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("INVALID @Test: " + m);
}
//捕捉到InvocationTargetException之外的異常,表明是Test註解的無效用法
}
// Array ExceptionTest processing code
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Exception>[] excTypes =
m.getAnnotation(ExceptionTest.class).value();
int oldPassed = passed;
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n",
passed, tests - passed);
}
}
PS:
對象 instanceof 類
類.isInstance(對象)
第36條:堅持使用Override註解
應該在你想要覆蓋超類聲明的每個方法中使用Override註解。
第37條:用標記接口定義類型
標記接口有兩點勝過標記註解:
- 標記接口定義的類型是由被標記類的實例實現的;標記註解則沒有定義這樣的類型。
- 它們可以被更加精確地進行鎖定。
標記接口例子:Serializable接口表明實例可以通過ObjectOutputStream進行處理。
標記註解勝過標記接口的優點:
- 可以通過默認的方式添加一個或者多個註解類型元素,給已被使用的註解類型添加更多的信息。隨着時間的推移,簡單的標記註解類型可以演變成更加豐富的註解類型。這種演變對於標記接口而言則是不可能的,因爲它通常不可能在實現接口之後再給它添加方法。
- 它們是更大的註解機制的一部分。因此,標記註解在那些支持註解作爲編程元素之一的框架中間同樣具有一致性。
總而言之,標記接口和標記註解都各有用處。如果想要定義一個任何新方法都不會與之關聯的類型,標記接口就是最好的選擇。如果想要標記程序元素而非類和接口,考慮到未來可能要給標記添加更多的信息,或者標記要適用於已經廣泛使用了註解類型的框架,那麼標記註解就是正確的選擇。如果你發現自己在編寫的是目標爲ElementType.TYPE的標記註解類型,就要花點時間考慮清楚,它是否真的應該爲註解類型,想想標記接口是否會更加合適。
方法
第38條:檢查方法的有效性
每當編寫方法或者構造器的時候,應該考慮它的參數有哪些限制。應該把這些限制寫到文檔中,並且在每個方法體的開頭處,通過顯式的檢查來實施這些限制。
第39條:必要時進行保護性拷貝
對於參數類型可以被不可信任方子類化的參數,請不要使用clone方法進行保護性拷貝。
// Broken "immutable" time period class
import java.util.*;
public final class Period {
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) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
this.start = start;
this.end = end;
}
// Repaired constructor - makes defensive copies of parameters
// Stops first attack
// 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 start;
}
public Date end() {
return end;
}
// Repaired accessors - make defensive copies of internal fields
// Stops second attack
// public Date start() {
// return new Date(start.getTime());
// }
//
// public Date end() {
// return new Date(end.getTime());
// }
public String toString() {
return start + " - " + end;
}
// Remainder omitted
}
// Two attacks on the internals of an "immutable" period
import java.util.*;
public class Attack {
public static void main(String[] args) {
// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
System.out.println(p);
// Second attack on the internals of a Period instance
start = new Date();
end = new Date();
p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
System.out.println(p);
}
}
有經驗的程序員通常使用Date.getTime()返回的long基本類型作爲內部的時間表示法,而不是使用Date對象引用,主要因爲Date是可變的。
如果類具有從客戶端得到或者返回到客戶端的可變組件,類就必須保護性地拷貝這些組件。如果拷貝的成本受到限制,並且類信任它的客戶端不會不恰當地修改組件,就可以在文檔中指明客戶端的職責是不得修改受到影響的組件,以此來代替保護性拷貝。
第40條:謹慎設計方法簽名
- 謹慎地選擇方法的名稱
- 不要過於追求提供便利的方法
- 避免過長的參數列表。目標是四個參數或者更少。相同類型的長參數序列格外有害。
縮短過長的參數列表的方法:
- 把方法分解成多個方法,每個方法只需要這些參數的一個子集。
- 創建輔助類,用來保存參數的分組。
- 從對象構建到方法調用都採用Builder模式。
對於參數類型,要優先使用接口而不是類。
對於boolean參數,要優先使用兩個元素的枚舉類型。
第41條:慎用重載
對於重載方法的選擇是靜態的,對於被覆蓋方法的選擇是動態的。
// Broken! - What does this program print?
import java.util.*;
import java.math.*;
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
輸出結果爲3個”Unknown Collection”,因爲classify方法被重載了,而要調用哪個重載方法是在編譯時做出決定的。對於for循環中的三次迭代,參數的編譯時類型都是Collection<?>
。
應該避免胡亂地使用重載機制。
安全而保守的策略是,永遠不要導出兩個具有相同參數數目的重載方法。如果方法使用可變參數,保守的策略是根本不要重載它。
第42條:慎用可變參數
可變參數機制通過先創建一個數組,數組的大小爲在調用位置所傳遞的參數數量,然後將參數值傳到數組中,最後將數組傳遞給方法。
// Sample uses of varargs
public class Varargs {
// Simple use of varargs
static int sum(int... args) {
int sum = 0;
for (int arg : args)
sum += arg;
return sum;
}
// The WRONG way to use varargs to pass one or more arguments!
// static int min(int... args) {
// if (args.length == 0)
// throw new IllegalArgumentException("Too few arguments");
// int min = args[0];
// for (int i = 1; i < args.length; i++)
// if (args[i] < min)
// min = args[i];
// return min;
// }
// The right way to use varargs to pass one or more arguments
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs)
if (arg < min)
min = arg;
return min;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
System.out.println(min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
}
}
第43條:返回零長度的數組或集合,而不是null
private final List<Cheese> cheesesInStock = ...;
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
/**
* @return an array containing all of the cheeses in the shop
*/
public Cheese[] getCheeses(){
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
Collection.toArray(T[])的規範保證:如果輸入數組大到足夠容納這個集合,它就將返回這個輸入數組。因此,這種做法永遠也不會分配零長度的數組。
同樣地,集合值的方法也可以做成在每當需要返回空集合時都返回同一個不可變的空集合。Collection.emptySet,emptyList和emptyMap方法提供的正是你所需要的,如下所示:
public List<Cheese> getCheeseList(){
if(cheesesInStock.isEmpty()){
return Collection.emptyList();//Always returns same list
}
else
return new ArrayList<Cheese>(cheesesInStock);
}
返回類型爲數組或集合的方法沒有理由返回null,而是返回一個零長度的數組或集合。
第44條:爲所有導出的API元素編寫文檔註釋
爲了正確地編寫API文檔,必須在每個被導出的類、接口、構造器、方法和域聲明之前增加一個文檔註釋。爲了編寫出可維護的代碼,還應該爲那些沒有被導出的類、接口、構造器、方法和域編寫文檔註釋。
方法的文檔註釋應該簡潔地描述出它和客戶端之間的約定:前提條件、後置條件、副作用、線程安全性。
按慣例,@param,@return或者@throws標籤後面的短語或者子句都不用句點來結束。
Javadoc的{@code}標籤的作用:使該代碼片段以代碼字體呈現;限制HTML標記和嵌套的Javadoc標籤在代碼片段中進行處理。
爲了將多個代碼示例包含在一個文檔註釋中,要使用包在HTML的<pre>
標籤裏面的Javadoc{@code}標籤:<pre>{@code 多個代碼示例}</pre>
。
當”this”被用在實例方法的文檔註釋中時,它應該始終是指方法調用所在的對象。
爲了產生包含HTML元字符的文檔,比如小於號(<),大於號(>)以及“與”號(&),必須採取特殊的動作:用{@literal}標籤將它們包圍起來。除了它不以代碼字體渲染文本之外,其餘方面就像{@code}標籤一樣。例如:
* The triangle inequality is {@literal |x+y|<|x|+|y|}.
文檔註釋在源代碼和產生的文檔中都應該是易於閱讀的。如果無法讓兩者都易讀,產生的文檔的可讀性要優先於源代碼的可讀性。
每個文檔註釋的第一句話成了該註釋所屬元素的概要描述。概要描述必須獨立地描述目標元素的功能。爲了避免混淆,同一個類或者接口中的兩個成員或者構造器,不應該具有同樣的概要描述。特別要注意重載的情形。
注意所期待的概要描述中是否包括句點,因爲句點會過早地終止這個描述。問題在於,概要描述在後面接着空格、跳格或者行終止符的第一個句點(或者在第一個塊標籤處)結束。最好的解決方法是,將討厭的句點以及任何與{@literal}關聯的文本都包含起來。
規範指出,概要描述很少是個完整的句子。對於方法和構造器而言,概要描述應該是個完整的動詞短語(包含任何對象),它描述了該方法所執行的動作;對於類、接口和域,概要描述應該是一個名詞短語,它描述了該類或者接口的實例,或者域本身所代表的事物。
當爲“泛型”或者方法編寫文檔時,確保要在文檔中說明所有的類型參數。當爲”枚舉”類型編寫文檔時,要確保在文檔中說明常量,以及類型,還有任何公有的方法。爲”註解”類型編寫文檔時,要確保在文檔中說明所有成員,以及類型本身。
從Java1.5發行版本開始,包級私有的文檔註釋就應該放在一個稱作package-info.java的文件中,而不是放在package.html中。除了包級私有的文檔註釋之外,package-info.java也可以(單並非必需)包含包聲明和包註解。
類是否是線程安全的,應該在文檔中對它的線程安全級別進行說明;如果類是可序列化的,就應該在文檔中說明它的序列化形式。
Javadoc具有“繼承”方法註釋的能力。如果API元素沒有文檔註釋,Javadoc將會搜索最爲適用的文檔註釋,接口的文檔註釋優先於超類的文檔註釋。也可以利用{@inheritDoc}標籤從超類型中繼承文檔註釋的部分內容。但是繼承機制使用起來需要一些小技巧,並具有一些侷限性。
爲了降低文檔註釋中出錯的可能性,一種簡單的辦法是通過一個HTML有效性檢查器來運行由Javadoc產生的HTML文件。
在文檔註釋內部出現任何HTML標籤都是允許的,但是HTML元字符必須要經過轉義。
通用程序設計
第45條:將局部變量的作用域最小化
java允許你在任何可以出現語句的地方聲明變量。
要使局部變量的作用域最小化,最有力的方法就是在第一次使用它的地方聲明。
幾乎每個局部變量的聲明都應該包含一個初始化表達式。如果你還沒有足夠的信息來對一個變量進行有意義的初始化,就應該推遲這個聲明,直到可以初始化爲止。
第46條:for-each循環優先於傳統的for循環
// Can you spot the bug?
import java.util.*;
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
class Card {
final Suit suit;
final Rank rank;
Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
}
public class NestedIteration {
public static void main(String[] args) {
Collection<Suit> suits = Arrays.asList(Suit.values());
Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<Card>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
// Preferred idiom for nested iteration on collections and arrays
// for (Suit suit : suits)
// for (Rank rank : ranks)
// deck.add(new Card(suit, rank));
}
}
for循環存在的問題:在迭代器上對外部的集合(suits)調用了太多次next方法。
for-each循環不僅讓你遍歷集合和數組,還讓你遍歷任何實現Iterable接口的對象。
有三種常見的情況無法使用for-each循環:
- 過濾-如果需要遍歷集合,並刪除選定的元素,就需要使用顯式的迭代器,以便可以調用它的remove方法。
- 轉換-如果需要遍歷列表或者數組,並取代它部分或者全部的元素值,就需要列表迭代器或者數組索引,以便設定元素的值。
- 平行迭代-如果需要並行地遍歷多個集合,就需要顯式地控制迭代器或者索引變量,以便所有迭代器或者索引變量都可以得到同步前移。
// Same bug as NestIteration.java (but different symptom)!!
import java.util.*;
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
public class DiceRolls {
public static void main(String[] args) {
Collection<Face> faces = Arrays.asList(Face.values());
for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
System.out.println(i.next() + " " + j.next());
//平行迭代的情況
// Preferred idiom for nested iteration on collections and arrays
// for (Face face1 : faces)
// for (Face face2 : faces)
// System.out.println(face1 + " " + face2);
}
}
第47條:瞭解和使用類庫
使用標準類庫的好處:
- 通過使用標準類庫,可以充分利用這些編寫標準類庫的專家的知識,以及在你之前的其他人的使用經驗。
- 不必浪費時間爲那些與工作不太相關的問題提供特別的解決方案。就像大多數程序員一樣,應該把時間花在應用程序上,而不是底層的細節上。
- 它們的性能往往會隨着時間的推移而不斷提高,無需你做任何努力。
- 可以使自己的代碼融入主流。這樣的代碼更易讀、更易維護、更易被大多數的開發人員重用。
在每個重要的發行版本中,都會有許多新的特性被加入到類庫中,所以與這些新特性保持同步是值得的。
第48條:如果需要精確的答案,請避免使用float和double
float和double類型尤其不適合用於貨幣計算,因爲要讓一個float或者double精確地表示0.1(或者10的任何其他負數次方值)是不可能的。正確方法是使用BigDecimal、int或者long進行貨幣計算。
第49條:基本類型優先於裝箱基本類型
基本類型和裝箱基本類型之間的三個主要區別:
- 基本類型只有值,而裝箱基本類型則具有與它們的值不同的同一性。換句話說,兩個裝箱基本類型可以具有相同的值和不同的同一性。
- 基本類型只有功能完備的值,而每個裝箱基本類型除了它對應基本類型的所有功能值之外,還有個非功能值:null。
- 基本類型通常比裝箱基本類型更節省時間和空間。
對裝箱基本類型運用==操作符幾乎總是錯誤的。
當在一項操作中混合使用基本類型和裝箱基本類型時,裝箱基本類型就會自動拆箱。當值爲null時,拆箱會拋出NullPointerException。
裝箱基本類型的合理用處:
- 作爲集合中的元素、鍵和值。
- 在參數化類型中,必須使用裝箱基本類型作爲類型參數。
- 在進行反射的方法調用時,必須使用裝箱基本類型。
第50條:如果其他類型更適合,則儘量避免使用字符串
不應該使用字符串的情形:
- 字符串不適合代替其他的值類型。
- 字符串不適合代替枚舉類型。
- 字符串不適合代替聚集類型。
- 字符串也不適合代替能力表。
第51條:當心字符串連接的性能
爲連接n個字符串而重複地使用字符串連接操作符,需要n的平方級的操作時間。
爲了獲得可以接受的性能,請使用StringBuilder代替String。
性能最差的方式:
public static String concat1(String s1, String s2, String s3, String s4, String s5, String s6) {
String result = "";
result += s1;
result += s2;
result += s3;
result += s4;
result += s5;
result += s6;
return result;
}
最好的方式:
public static String concat2(String s1, String s2, String s3, String s4, String s5, String s6) {
return s1 + s2 + s3 + s4 + s5 + s6;
}
第二種方式Java會自動使用StringBuilder.append()函數來進行連接。
第52條:通過接口引用對象
如果有合適的接口類型存在,那麼對於參數、返回值、變量和域來說,就都應該使用接口類型進行聲明。
如果對象屬於基於類的框架,就應該用相關的基類(往往是抽象類)來引用這個對象,而不是用它的實現類。
第53條:接口優先於反射機制
反射機制允許一個類使用另一個類,即使當前者被編譯的時候後者還根本不存在。然而,這種能力也要付出代價:
- 喪失了編譯時類型檢查的好處,包括異常檢查。
- 執行反射訪問所需要的代碼非常笨拙和冗長。
- 性能損失。反射方法調用比普通方法調用慢了許多。
反射方法只是在設計時被用到。通常,普通應用程序在運行時不應該以反射方式使用對象。
簡而言之,反射機制是一種功能強大的機制,對於特定的複雜系統編程任務,它是非常必要的,但它也有一些缺點。如果你編寫的程序必須要與編譯時未知的類一起工作,如有可能,就應該僅僅使用反射機制來實例化對象,而訪問對象時則使用編譯時已知的某個接口或者超類。
第54條:謹慎地使用本地方法
從歷史上看,本地方法主要有三種用途:
- 提供了“訪問特定於平臺的機制”的能力。
- 提供了訪問遺留代碼庫的能力。
- 可以通過本地語言,編寫應用程序中注重性能的部分,以提高系統的性能。
使用本地方法有一些嚴重的缺點:
- 因爲本地語言是不安全的,所以,使用本地方法的應用程序也不再能免受內存毀壞錯誤的影響。
- 因爲本地語言是與平臺相關的,使用本地方法的應用程序也不再是可自由移植的
- 在進入和退出本地代碼時,需要相關的固定開銷,所以,如果本地代碼只是做少量的工作,本地方法就可能降低性能。
最後一點,需要”膠合代碼“的本地方法編寫起來單調乏味,並且難以閱讀。
總而言之,在使用本地方法之前務必三思。極少數情況下會需要使用本地方法來提高性能。如果你必須要使用本地方法來訪問底層的資源,或者遺留代碼庫,也要儘可能少用本地代碼,並且要全面進行測試。本地代碼中的一個Bug就有可能破壞整個應用程序。
第55條:謹慎地進行優化
三條與優化有關的格言:
很多計算上的過失都被歸咎於效率(沒有必要達到的效率),而不是任何其他的原因——甚至包括盲目地做傻事。(William A. Wulf)
不要去計較效率上的一些小小的得失,在97%的情況下,不成熟的優化纔是一切問題的根源。(Donald E. Knuth)
在優化方面,我們應該遵守兩條規則:
規則1:不要進行優化。
規則2(僅針對專家):還是不要進行優化——也就是說,在你還沒有絕對清晰的未優化方案之前,請不要進行優化。(M.A. Jackson)
總而言之,不要費力去編寫快速的程序——應該努力編寫好的程序,速度自然會隨之而來。在設計系統的時候,特別是在設計API、線路層協議和永久數據格式的時候,一定要考慮性能的因素。在構建完系統之後,要測量它的性能。如果它足夠快,你的任務就完成了。如果不夠快,則可以在性能剖析器的幫助下,找到問題的根源,然後設法優化系統中相關的部分。第一個步驟是檢查所選擇的算法:再多的低層優化也無法彌補算法的選擇不當。必要時重複這個過程,在每次改變之後都要測量性能,直到滿意爲止。
第56條:遵守普遍接受的命名慣例
命名慣例分爲兩大類:字面的、語法的。
異常
第57條:只針對異常的情況才使用異常
異常是爲了在異常情況下使用而設計的,不要將它們用於普通的控制流,也不要編寫迫使它們這麼做的API。
第58條:對可恢復的情況使用受檢異常,對編程錯誤使用運行時異常
Java程序設計語言提供了3種可拋出結構:受檢的異常、運行時異常和錯誤。
第59條:避免不必要地使用受檢的異常
第60條:優先使用標準的異常
重用現有異常的好處:
- 使你的API更加易於學習和使用,因爲它與程序員已經熟悉的習慣用法是一致的。
- 對於用到這些API的程序而言,它們的可讀性更好。
- 異常類越少,意味着內存印跡就越小,裝載這些類的時間開銷也越少。
常用的異常:
異常 | 使用場合 |
---|---|
IllegalArgumentException | 非null的參數值不正確 |
IllegalStateException | 對於方法調用而言,對象狀態不合適 |
NullPointerException | 在禁止使用null的情況下參數值爲null |
IndexOutOfBoundsException | 下標參數值越界 |
ConcurrentModificationException | 在禁止併發修改的情況下,檢測到對象的併發修改 |
UnsupportedOperationException | 對象不支持用戶請求的方法 |
所有錯誤的方法調用都可以被歸結爲非法參數或者非法狀態。
第61條:拋出與抽象相對應的異常
更高層的實現應該捕獲低層的異常,同時拋出可以按照高層抽象進行解釋的異常。這種做法被稱爲異常轉譯(exception translation)。
如果不能阻止或者處理來自更低層的異常,一般的做法是使用異常轉譯,除非低層方法碰巧可以保證它所拋出的所有異常對高層也合適纔可以將異常從低層傳播到高層。異常鏈對高層和低層異常都提供了最佳的功能:它允許拋出適當的高層異常,同時又能捕獲低層的原因進行失敗分析。
第62條:每個方法拋出的異常都要有文檔
始終要單獨地聲明受檢的異常,並且利用Javadoc的@throws標記,準確地記錄下拋出每個異常的條件。如果一個方法可能拋出多個異常類,則不要使用“快捷方式”聲明它會拋出這些異常類的某個超類。
每個方法的文檔應該描述它的前提條件,在文檔中記錄下未受檢的異常是滿足前提條件的最佳做法。
使用Javadoc的@throws標籤記錄下一個方法可能拋出的每個未受檢異常,但是不要使用@throws關鍵字將未受檢的異常包含在方法的聲明中。
如果一個類中的許多方法出於同樣的原因而拋出同一個異常,在該類的文檔註釋中對這個異常建立文檔,而不是爲每個方法單獨建立文檔。
要爲你編寫的每個方法所能拋出的每個異常建立文檔。對於未受檢的和受檢的異常,以及對於抽象的和具體的方法也都一樣。要爲每個受檢異常提供單獨的throws子句,不要爲未受檢的異常提供throws子句。如果沒有爲可以拋出的異常建立文檔,其他人就很難或者根本不可能有效地使用你的類和接口。
第63條:在細節消息中包含能捕獲失敗的信息
異常類型的toString方法應該儘可能多地返回有關失敗原因的信息。
爲了捕獲失敗,異常的細節信息應該包含所有“對該異常有貢獻”的參數和域的值。
爲了確保在異常的細節消息中包含足夠的能捕獲失敗的信息,一種方法是在異常的構造器而不是字符串細節消息中引入這些信息。
第64條:努力使失敗保持原子性
一般而言,失敗的方法調用應該使對象保持在被調用之前的狀態。具有這種屬性的方法被稱爲具有失敗原子性。
實現方法:
- 設計一個不可變的對象。
- 對於在可變對象上執行操作的方法,在執行方法之前檢查參數的有效性。這可以使得在對象的狀態被修改之前,先拋出適當的異常。
//Stack.pop
public Object pop(){
if(size==0)
throw new EmptyStackExcption();
Object result = elements[--size];
elements[size] = null;
return result;
}
- 與上述類似的方法,調整計算處理過程的順序,使得任何可能會失敗的計算部分都在對象狀態被修改之前發生。
- 一種不常用的方法,編寫一段恢復代碼。這種辦法主要用於永久性的(基於磁盤的)數據結構。
- 在對象的一份臨時拷貝上執行操作,當操作完成之後再用臨時拷貝中的結果代替對象中的內容。例如,Collections.sort在執行排序之前,首先把它的輸入列表轉到一個數組中,以便降低在排序的內循環中訪問元素所需要的開銷。這是出於性能考慮的做法,但是,它增加了一項優勢:即使排序失敗,它也能保證輸入列表保持原樣。
雖然一般情況下都希望實現失敗原子性,但並非總是可以做到。
錯誤(相對於異常)通常是不可恢復的,當方法拋出錯誤時,它們不需要努力保持失敗原子性。
第65條:不要忽略異常
空的catch塊會使異常達不到應有的目的,即強迫你處理異常的情況。忽略異常就如同忽略火警信號一樣。至少,catch塊也應該包含一條聲明,解釋爲什麼可以忽略這個異常。
本條目中的建議同樣適用於受檢異常和未受檢的異常。正確地處理異常能夠徹底挽回失敗。只要將異常傳播給外界,至少會導致程序迅速地失敗,從而保留了有助於調試該失敗條件的信息。
併發
第66條:同步訪問共享的可變數據
如果沒有同步,一個線程的變化就不能被其他線程看到。同步不僅可以阻止一個線程看到對象處於不一致的狀態之中,它還可以保證進入同步方法或同步代碼塊的每個線程,都看到由同一個鎖保護的之前所有的修改效果。
Java語言規範保證讀或者寫一個變量是原子的,除非這個變量的類型爲long或者double。
如果讀和寫操作沒有都被同步,同步就不會起作用。
簡而言之,當多個線程共享可變數據的時候,每個讀或者寫數據的線程都必須執行同步。
第67條:避免過度同步
在一個被同步的區域內部,不要調用設計成要被覆蓋的方法,或者是由客戶端以函數對象的形式提供的方法。從包含該同步區域的類的角度來看,這樣的方法是外來的。這個類不知道該方法會做什麼事情,也無法控制它。根據外來方法的作用,從同步區域中調用它會導致異常、死鎖或者數據損壞。
在同步區域之外被調用的外來方法被稱作“開放調用”。除了可以避免死鎖之外,開放調用還可以極大地增加併發性。
通常,你應該在同步區域內做儘可能少的工作。
第68條:executor和task優先於線程
executor:
SingleThreadExecutor
ThreadPoolExecutor
CachedThreadPool
FixedThreadPool
ScheduledThreadPoolExecutor
task:
Runnable
Callable
第69條:併發工具優先於wait和notify
併發集合爲標準的集合接口(如List,Queue和Map)提供了高性能的併發實現。爲了提供高併發性,這些實現在內部自己管理同步。因此,併發集合中不可能排除併發活動;將它鎖定沒有什麼作用,只會使程序的速度變慢。
除非不得已,否則應該優先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者HashTable。只要用併發Map代替老式的同步Map,就可以極大地提升併發應用程序的性能。更一般地,應該優先使用併發集合,而不是使用外部同步的集合。
對於間歇式的定時,始終應該優先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTime更加準確也更加精確,它不受系統的實時時鐘的調整所影響。
使用wait方法的標準模式:
//the standard idiom for using the wait method
synchronized(obj){
while(<condition does not hold>){
obj.wait();//release lock, and requires on wakeup
}
//perform action appropriate to condition
}
始終應該使用while循環模式來調用wait方法;永遠不要在循環之外調用wait方法。循環會在等待之前和之後測試條件。
一般情況下,你應該優先使用notifyAll,而不是使用notify。
第70條:線程安全性的文檔化
一個類爲了可被多個線程安全地使用,必須在文檔中清楚地說明它所支持的線程安全性級別。
常見的幾種線程安全性級別:
- 不可變的——這個類的實例是不變的。(String, Long, BigInteger)
- 無條件的線程安全——這個類的實例是可變的,但是這個類有着足夠的內部同步,所以它的實例可以被併發使用,無需任何外部同步。(Random, ConcurrentHashMap)
- 有條件的線程安全——除了有些方法爲進行安全的併發而需要使用外部同步之外,這種線程安全級別與無條件的線程安全相同。(Collections.synchronized包裝返回的集合,它們的迭代器(iterator)要求外部同步)
- 非線程安全——這個類的實例是可變的。(ArrayList, HashMap)
- 線程對立的——這個類不能安全地被多個線程併發使用,即使所有的方法調用都被外部同步包圍。線程對立的根源通常在於,沒有同步地修改靜態資源。
類的線程安全說明通常放在它的文檔註釋中,但是帶有特殊線程安全屬性的方法則應該在它們自己的文檔註釋中說明它們的屬性。
有條件的線程安全類必須在文檔中指明“哪個方法調用序列需要外部同步,以及在執行這些序列的時候要獲得哪把鎖”。如果你編寫的是無條件的線程安全類,就應該考慮用私有鎖對象來代替同步的方法。這樣可以防止客戶端程序(拒絕服務攻擊,超時地保持公有可訪問鎖)和子類(出於不同的目的而使用相同的鎖,子類和基類可能會“相互絆住對方的腳”)的不同步干擾,讓你能夠在後續的版本中靈活地對併發控制採用更加複雜的方法。
第71條:慎用延遲初始化
延遲初始化就像一把雙刃劍。它降低了初始化類或者創建實例的開銷,卻增加了訪問被延遲初始化的域的開銷。根據延遲初始化的域最終需要初始化的比例、初始化這些域要多少開銷,以及每個域多久被訪問一次,延遲初始化(就像其他的許多優化一樣)實際上降低了性能。
如果域只在類的實例部分被訪問,並且初始化這個域的開銷很高,可能就值得進行延遲初始化。要確定這一點,唯一的辦法就是測量類在用和不用延遲初始化時的性能差別。
在大多數情況下,正常的初始化要優先於延遲初始化。
正常初始化實例域的一個典型聲明:
//normal initialization of an instance field
private final FieldType field = computeFieldValue();
如果利用延遲優化來破壞初始化的循環,就要使用同步訪問方法。
//lazy initialization of instance field - synchronized accessor
private FieldType field;
synchronized FieldType getField(){
if(field == null)
field = computeFieldValue();
return field;
}
如果出於性能的考慮而需要對靜態域使用延遲初始化,就使用 lazy initialization holder class模式。這種模式保證了類要到被用到的時候纔會被初始化:
// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }
當getField方法第一次被調用時,它第一次讀取FieldHolder.field,導致FieldHolder類得到初始化。這種模式的魅力在於,getField方法沒有被同步,並且只執行一個域訪問,因此延遲初始化實際上並沒有增加任何訪問成本。現代的VM將在初始化該類的時候,同步域的訪問。一旦這個類被初始化,VM將修補代碼,以便後續對該域的訪問不會導致任何測試或者同步。
如果出於性能的考慮而需要對實例域使用延遲初始化,就使用雙重檢查模式。這種模式避免了在域被初始化之後訪問這個域時的鎖定開銷。這種模式的背後思想是:兩次檢查域的值,第一次檢查時沒有鎖定,看看這個域是否被初始化了;第二次檢查時有鎖定。域聲明爲volatile很重要。
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) { // First check (no locking)
synchronized(this) {
result = field;
if (result == null) // Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
局部變量result的作用是確保field只在已經被初始化的情況下讀取一次。雖然這不是嚴格要求,但是可以提升性能,並且因爲給低級的併發編程應用了一些標準,因此更加優雅。
有時候,你可能需要延遲初始化一個可以接受重複初始化的實例域。如果處於這種情況,就可以使用單重檢查模式。注意field仍然被聲明爲volatile。
// Single-check idiom - can cause repeated initialization!
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
本條目中的所有初始化方法都適用於基本類型的域,以及對象引用域。(null換成對應的默認初始值)
如果你不在意是否每個線程都重新計算域的值,並且域的類型爲基本類型,而不是long或者double類型,就可以選擇從單重檢查模式的域聲明中刪除volatile修飾符。這種變體稱之爲racy single-check idiom。它加快了某些架構上的域訪問,代價是增加了額外的初始化(訪問該域的每個線程都進行一次初始化)。
第72條:不要依賴於線程調度器
任何依賴於線程調度器來達到正確性或者性能要求的程序,很有可能都是不可移植的。(因爲不同操作系統所採用的調度策略大相徑庭)
線程優先級是Java平臺上最不可移植的特徵。線程優先級可以用來提高一個已經能夠正常工作的程序的服務質量,但永遠不應該用來“修正”一個原本並不能工作的程序。
應該使用Thread.sleep(1)代替Thread.yield來進行併發測試。
第73條:避免使用線程組
線程組並沒有提供太多有用的功能,而且它們提供的許多功能都是有缺陷的。因爲線程組已經過時了,所以根本沒有必要修正。如果你正在設計的一個類需要處理線程的邏輯組,或許就應該使用線程池。
序列化
第74條:謹慎地實現Serializable接口
實現Serializable接口而付出的最大代價是,一旦一個類被髮布,就大大降低了“改變這個類的實現”的靈活性。如果你接受了默認的序列化形式,這個類中私有的和包級私有的實例域都將變成導出的API的一部分,這不符合“最低限度地訪問域”的實踐準則,從而它就失去了作爲信息隱藏工具的有效性。
自動地產生的標識號(serial version UID)會受到類名稱、它所實現的接口的名稱、以及所有公有的和受保護的成員的名稱所影響。因此,如果你沒有聲明一個顯式的序列版本UID,兼容性將會遭到破壞,在運行時導致InvalidClassException異常。
實現Serializable的第二個代價是,它增加了出現Bug和安全漏洞的可能性。無論你是接受了默認的行爲,還是覆蓋了默認的行爲,反序列化機制都是一個“隱藏的構造器”,具備與其他構造器相同的特點。依靠默認的反序列化機制,很容易使對象的約束關係遭到破壞,以及遭受到非法訪問。
實現Serializable的第三個代價是,隨着類發行新的版本,相關的測試負擔也增加了。當一個可序列化的類被修訂的時候,很重要的一點是,要檢查是否可以“在新版本中序列化一個實例,然後在舊版本中反序列化”,反之亦然。因此,測試所需要的工作量與“可序列化的類的數量和發行版本號”的乘積成正比。
每當你實現一個類的時候,都需要權衡一下所付出的代價和帶來的好處。根據經驗,比如Date和BigInteger這樣的值類應該實現 Serializable,大多數的集合類也應該如此。代表活動實體的類,比如線程池,一般不應該實現Serializable。
爲了繼承而設計的類應該儘可能少地去實現Serializable接口,用戶的接口也應該儘可能少地繼承Serializable接口。
在爲了繼承而設計的類中,真正實現了Serializable接口的有Throwable類、Component和HttpServlet抽象類。
如果超類沒有提供可訪問的無參構造器,子類也不可能做到可序列化。因此,對於爲繼承而設計的不可序列化的類,你應該考慮提供一個無參構造器。
給”不可序列化但可擴展的類“增加無參構造器的方法:
// Nonserializable stateful class allowing serializable subclass
import java.util.concurrent.atomic.*;
public abstract class AbstractFoo {
private int x, y; // Our state
// This enum and field are used to track initialization
private enum State { NEW, INITIALIZING, INITIALIZED };
private final AtomicReference<State> init =
new AtomicReference<State>(State.NEW);
public AbstractFoo(int x, int y) { initialize(x, y); }
// This constructor and the following method allow
// subclass's readObject method to initialize our state.
protected AbstractFoo() { }
protected final void initialize(int x, int y) {
if (!init.compareAndSet(State.NEW, State.INITIALIZING))
throw new IllegalStateException(
"Already initialized");
this.x = x;
this.y = y;
// Do anything else the original constructor did
init.set(State.INITIALIZED);
}
// These methods provide access to internal state so it can
// be manually serialized by subclass's writeObject method.
protected final int getX() { checkInit(); return x; }
protected final int getY() { checkInit(); return y; }
// Must call from all public and protected instance methods
private void checkInit() {
if (init.get() != State.INITIALIZED)
throw new IllegalStateException("Uninitialized");
}
// Remainder omitted
}
// Serializable subclass of nonserializable stateful class
import java.io.*;
public class Foo extends AbstractFoo implements Serializable {
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Manually deserialize and initialize superclass state
int x = s.readInt();
int y = s.readInt();
initialize(x, y);
}
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
// Manually serialize superclass state
s.writeInt(getX());
s.writeInt(getY());
}
// Constructor does not use the fancy mechanism
public Foo(int x, int y) { super(x, y); }
private static final long serialVersionUID = 1856835860954L;
}
內部類不應該實現Serializable,因爲內部類的默認序列化形式是定義不清楚的。然而,靜態成員類卻可以實現Serializable接口。
第75條:考慮使用自定義的序列化形式
如果一個對象的物理表示法等同於它的邏輯內容,可能就適合於使用默認的序列化形式。即使你確定了默認的序列化形式是合適的,通常還必須提供一個readObject方法以保證約束關係和安全性。
當一個對象的物理表示法與它的邏輯數據內容有實質性的區別時,使用默認序列化形式會有以下4個缺點:
- 它使這個類的導出API永遠束縛在該類的內部表示法上。
- 它會消耗過多的空間。
- 它會消耗過多的時間。
- 它會引起棧溢出。
// StringList with a reasonable custom serialized form
import java.io.*;
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
// Appends the specified string to the list
public final void add(String s) {
// Implementation omitted
}
/**
* Serialize this {@code StringList} instance.
*
* @serialData The size of the list (the number of strings
* it contains) is emitted ({@code int}), followed by all of
* its elements (each a {@code String}), in the proper
* sequence.
*/
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// Read in all elements and insert them in list
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
private static final long serialVersionUID = 93248094385L;
// Remainder omitted
}
如果所有的實例域都是瞬時的(transient),從技術角度而言,不調用defaultWriteObject和defaultReadObject也是允許的,但是不推薦這樣做。調用defaultWriteObject和defaultReadObject得到的序列化形式允許在以後的發行版本中增加非transient的實例域,並且還能保持向前或者向後兼容性。
在決定將一個域做成非transient的之前,請一定要確信它的值將是該對象邏輯狀態的一部分。如果你正在使用一種自定義的序列化形式,大多數實例域,或者所有的實力域都應該被標記爲transient。
如果你正在使用默認的序列化形式,並且把一個或者多個域標記爲transient,則要記住,當一個實例被反序列化的時候,這些域將被初始化爲它們的默認值。
無論你是否使用默認的序列化形式,如果在讀取整個對象狀態的任何其他方法上強制任何同步,則也必須在對象序列化上強制這種同步。
//writeObject for synchronized class with default serialized form
private synchronized void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
}
不管你選擇了哪種序列化形式,都要爲自己編寫的每個可序列化的類聲明一個顯式的序列版本UID。這樣可以避免序列版本UID成爲潛在的不兼容根源。而且,這樣做也會帶來小小的性能好處(不需要通過計算產生)。
private static final long serialVersionUID = randomLongValue;
第76條:保護性地編寫readObject方法
readObject方法實際上相當於另一個公有的構造器,如同其他的構造器一樣,它也要求注意同樣的所有注意事項。構造器必須檢查其參數的有效性,並且在必要的時候對參數進行保護性拷貝,同樣地,readObject方法也需要這樣做。否則容易受到攻擊。
當一個對象被反序列化的時候,對於客戶端不應該擁有的對象引用,如果哪個域包含了這樣的對象引用,就必須要做保護性拷貝。因此,對於每個可序列化的不可變類,如果它包含了私有的可變組件,那麼在它的readObject方法中,必須要對這些組件進行保護性拷貝。
// readObject method with defensive copying and validity checking
// This will defend against BogusPeriod and MutablePeriod attacks.
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Defensively copy our mutable components
start = new Date(start.getTime());
end = new Date(end.getTime());
// Check that our invariants are satisfied
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after "+ end);
}
同時也要注意到,對於final域,保護性拷貝是不可能的。
編寫出更加健壯的readObject方法的指導方針:
- 對於對象引用域必須保持爲私有的類,要保護性地拷貝這些域中的每個對象。不可變類的可變組件就屬於這一類別。
- 對於任何約束條件,如果檢查失敗,則拋出一個InvalidObjectException異常。這些檢查動作應該跟在所有的保護性拷貝之後。
- 如果整個對象圖在被反序列化之後必須進行驗證,就應該使用ObjectInputValidation接口。
- 無論是直接方式還是間接方式,都不要調用類中任何可被覆蓋的方法。
第77條:對於實例控制,枚舉類型優先於readResolve
如果依賴readResolve進行實例控制,帶有對象引用類型的所有實例域都必須聲明爲transient類型的。
如果將一個可序列化的實例受控的類編寫成枚舉,就可以絕對保證除了所聲明的常量之外,不會有別的實例。
readResolve的可訪問性很重要。如果readResolve方法是受保護的或者公有的,並且子類沒有覆蓋它,對序列化過的子類實例進行反序列化,就會產生一個超類實例,這樣有可能導致ClassCastException異常。
總而言之,你應該儘可能地使用枚舉類型來實施實例控制的約束條件。如果做不到(它的實例在編譯時還不知道),同時又需要一個既可序列化又是實例控制的類,就必須提供一個readResolve方法,並確保該類的所有實例域都爲基本類型,或者是transient的。
第78條:考慮用序列化代理代替序列化實例
爲可序列化的類設計一個私有的靜態嵌套類,精確地表示外圍類的實例的邏輯狀態。這個嵌套類被稱作序列化代理,它應該有一個單獨的構造器,其參數類型就是那個外圍類。這個構造器只從它的參數中複製數據:它不需要進行任何一致性檢查或者保護性拷貝。從設計的角度來看,序列化代理的默認序列化形式是外圍類最好的序列化形式。外圍類及其序列代理都必須聲明實現Serializable接口
// Period class with serialization proxy
import java.util.*;
import java.io.*;
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
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
private Object readResolve() {
return new Period(start, end); // Uses public constructor
}
}
// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
return new SerializationProxy(this);
}
// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream)
throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
}
序列化代理的侷限性:
- 它不能與可以被客戶端擴展的類兼容。( 見第17條)
- 它不能與對象圖中包含循環的某些類兼容:如果你企圖從一個對象的序列化代理的readResolve方法內部調用這個對象中的方法,就會得到一個ClassCastException異常,因爲你還沒有這個對象,只有它的序列化代理。
總而言之,每當你發現自己必須在一個不能被客戶端擴展的類上編寫readObject或者writeObject方法的時候,就應該考慮使用序列化代理模式。要想穩健地將帶有重要約束條件的對象序列化時,這種模式可能是最容易的方法。