EffectiveJava--方法

[b]本章內容:[/b]
1. 檢查參數的有效性
2. 必要時進行保護性拷貝
3. 謹慎設計方法簽名
4. 慎用重載
5. 慎用可變參數
6. 返回零長度的數組或者集合,而不是null
7. 爲所有導出的API元素編寫文檔註釋

[b]1. 檢查參數的有效性[/b]
每當編寫方法或者構造器的時候,應該考慮他的參數有哪些限制。應該把這些限制寫到文檔中,並且在這個方法體的開頭處,通過顯式的檢查來實施這些限制。養成這樣的習慣是非常重要的。
對於公有的方法,要用Javadoc的@throws標籤(tag)在文檔中說明違反參數值限制會拋出異常。手工拋出異常,並且添加@throws註解說明原因 。如下:
/**
* hello.....
* @param m
* @return
* @throws NullPointerException if m is null
* @throws ArithmeticException if m is less than or equals to 0
*/
public BigInteger mod(BigInteger m) {
if(m == null){
throw new NullPointerException("m is null:" + m);
}
if (m.signum() <= 0) {
throw new ArithmeticException("Modulus <= 0: " + m);
}
// Do something
return null;
}
非公有的方法通常應該使用斷言(assertion)來檢查他們的參數。如下:
/**
*
* @param a
* @param offset
* @param length
*/
private static void sort(long[] a,int offset,int length){
assert a != null;
assert offset >= 0 && offset <= a.length;
System.out.println("sort do something");
}
注意以上不同於junit裏的斷言方法:
private static void sort2(long[] a, int offset, int length) {
Assert.assertTrue("a is null", a != null);
Assert.assertTrue(offset >= 0 && offset <= a.length);
System.out.println("sort do something");
}
斷言如果失敗,將會拋出AssertionError,如果它們沒有起到作用,本質上不會有成本開銷,除非通過將-ea(或者-enableassertions)標記傳遞給java解釋器,來啓動他們(一般來說 assert 在開發的時候是檢查程序的安全性的,在發佈的時候通常都不使用assert )。

[b]2. 必要時進行保護性拷貝[/b]
使Java使用起來如此舒適的一個因素在於,它是一門安全的語言。這意味着,它對於緩衝區溢出、數組越界、非法指針以及其他的內存破壞錯誤都自動免疫。
假設類的客戶端會盡其所能來破壞這個類的約束條件,因此你必須保護性的設計程序。 如下代碼:
import java.util.Date;
public final class Period {
private final Date start;
private final Date end;
public Period(Date start,Date end) {
if(start.compareTo(end) > 0){
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start;
this.end = end;
}

public Date start(){
return start;
}

public Date end(){
return end;
}
//remainder omitted
}
這個類看上去沒有什麼問題,時間是不可改變的。然而Date類本身是可變的,因此很容易違反這個約束條件:
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78); // 修改值
System.out.println(period.end());
爲了保護Period實例的內部信息避免受到修改,對於構造器的每個可變參數進行保護性拷貝(defensive copy)是必要的,並且使用備份對象作爲Period實例的組件,而不使用原始的對象:
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(this.start + " after " + this.end);
}
}
用了新的構造器之後,上述的攻擊對於Period實例不再有效。注意,保護性拷貝是在檢查參數的有效性之前進行的,並且有效性檢查是針對拷貝之後的對象,而不是原始對象。 這樣做可以避免在危險階段期間從另一個線程改變類的參數。
對於參數類型可以被不可信任方子類化的參數,請不要使用clone方法進行保護性拷貝。
雖然替換構造器就可以成功地避免上述的攻擊,但是改變Period實例仍然是有可能的,因爲它的訪問方法提供了對其可變內部成員的訪問能力,爲了防止這種攻擊,可以讓訪問方法返回拷貝對象:
public Date start(){
return new Date(start.getTime());
}
public Date end(){
return new Date(end.getTime());
}
參數的保護性拷貝不僅僅針對不可變類。每當編寫編寫方法和構造器時,如果他要允許客戶提供的對象進入到內部數據結構中,則有必要考慮一下,客戶提供的對象是否有可能是可變的,我是否能夠容忍這種可變性。特別是你用到list、map之類連接元素時。 如果答案是否定的,就必須對該對象進行保護性拷貝,並且讓拷貝之後的對象而不是原始對象進入到數據結構中。
在內部組件返回給客戶端的時候,也要考慮是否可以返回一個指向內部引用的數據,解決文字是應該返回保護性拷貝。或者,不使用拷貝,你也可以返回一個不可變對象。
可以肯定的說,上述的真正啓示在於,只要有可能,都應該使用不可變的對象作爲對象內部的組件(注意是不可變對象),這樣就不必再爲保護性拷貝操心。保護性拷貝可能會帶來相關的性能損失,但不一定是。如果類具有從客戶端得到或者返回到客戶端的可變組件,類就必須保護性的拷貝這些組件。如果拷貝的成本受到限制,並且類信任他的客戶端不會進行修改,或者恰當的修改,那麼就需要在文檔中指明客戶端調用者的責任(不的修改或者如何有效修改)。
特別是當你的可變組件的生命週期很長,或者會多層傳遞時,隱藏的問題往往暴漏出來就很可怕。

[b]3. 謹慎設計方法簽名[/b]
(1)謹慎地選擇方法的名稱
(2)不要過於追求提供便利的方法
(3)避免過長的參數列表,目標是四個參數或者更少,如果多於四個了就該考慮重構這個方法了(分解方法、創建輔助類、從對象構建到方法調用都採用Builder模式)。
(4)對於參數類型、要優先使用接口而不是類。如果使用的是類而不是接口,則限制了客戶端只能傳入特定的實現,如果碰巧輸入的數據是以其他的形式存在,就會導致不必要的、可能非常昂貴的拷貝操作。
(5)對語言boolean參數,優先使用兩個元素的枚舉類型。

[b]4. 慎用重載[/b]
下面的例子根據一個集合是Set、List還是其他的集合類型,來對它進行分類:
public class CollectionClassfier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> l) {
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));
}
}
這裏你可能會期望程序打印出Set、List、Unknown Collection,然而實際上卻不是這樣,輸出的結果是3 個"Unknown Collection"。因爲classify方法被重載了,需要調用哪個函數是在編譯期決定的,for中的三次迭代參數的編譯類型是相同的:Collection<?>。對於重載方法的選擇是靜態的,而對於被覆蓋的方法的選擇則是動態的。選擇被覆蓋的方法的正確版本是在運行時進行的,選擇的依據是被調用的方法所在對象的運行時類型。這裏重新說明一下,當一個子類包含的方法聲明與其祖先類中的方法聲明具有同樣的的簽名時,方法就被覆蓋了。如果實例方法在子類中被覆蓋了,並且這個方法是在該子類的實例上被調用的,那麼子類中的覆蓋方法將會執行,而不管該子類實例的編譯時類型到底是什麼。
class Wine{
String name() {return "wine"; }
}
class SparklingWine extends Wine{
@Override String name(){return "sparkling wine"; }
}
class Champagne extends Wine{
@Override String name(){return "Champagne"; }
}
public class Overriding{
public static void main(String[] args){
Wine[] = {new Wine(), new SparklingWine(), new Champagne() };
}
for(Wine wine : wines){
System.out.println(wine.name());
}
}
正如你所預期的那樣,這個程序打印出“wine, sparkling wine, champagne”,當調用被覆蓋的方法時,對象的編譯時類型不會影響到哪個方法將被執行。最爲具體的那個覆蓋版本總是會得到執行。

對於開始的集合輸出類的最佳修正方案是,用單個方法來替換這三個重載的classity方法,如下:
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}

因此,應該避免胡亂地使用重載機制。
一、安全而保守的策略是,永遠不要導出兩個具有相同參數數目的重載方法。比如兩個重載函數均有一個參數,其中一個是整型,另一個是Collection<?>,對於這種情況,int 和Collection<?>之間沒有任何關聯,也無法在兩者之間做任何的類型轉換,否則將會拋出ClassCastException 的異常,因此對於這種函數重載,我們是可以準確確定的。反之,如果兩個參數分別是int 和short,他們之間的差異就不是這麼明顯。
二、如果方法使用可變參數,保守的策略是根本不要重載它。
三、對於構造器,你沒有選擇使用不同名稱的機會,一個類的多個構造器總是重載的,但是構造器也不可能被覆蓋。
四、在Java 1.5 之後,需要對自動裝箱機制保持警惕。演示如下:
public class SetList {
public static void main(String[] args) {
Set<Integer> s = new TreeSet<Integer>();
List<Integer> l = new ArrayList<Integer>();
for (int i = -3; i < 3; ++i) {
s.add(i);
l.add(i);
}
for (int i = 0; i < 3; ++i) {
s.remove(i);
l.remove(i);
}
System.out.println(s + " " + l);
}
}
在執行該段代碼前,我們期望的結果是Set 和List 集合中大於等於的元素均被移除出容器,然而在執行後卻發現事實並非如此,其結果爲:[-3,-2,-1] [-2,0,2]。這個結果和我們的期望還是有很大差異的,爲什麼Set 中的元素是正確的,而List 則不是,是什麼導致了這一結果的發生呢?下面給出具體的解釋:
s.remove(i)調用的是Set 中的remove(E),這裏的E 表示Integer,Java 的編譯器會將i 自動裝箱到Integer 中,因此我們得到了想要的結果。
l.remove(i)實際調用的是List 中的remove(int index)重載方法,而該方法的行爲是刪除集合中指定索引的元素。這裏分別對應第0 個,第1 個和第2 個。
爲了解決這個問題,我們需要讓List 明確的知道,我們需要調用的是remove(E)重載函數,而不是其他的,這樣我們就需要對原有代碼進行如下的修改:
public class SetList {
public static void main(String[] args) {
Set<Integer> s = new TreeSet<Integer>();
List<Integer> l = new ArrayList<Integer>();
for (int i = -3; i < 3; ++i) {
s.add(i);
l.add(i);
}
for (int i = 0; i < 3; ++i) {
s.remove(i);
l.remove((Integer)i); //or remove(Integer.valueOf(i));
}
System.out.println(s + " " + l);
}
}
總結,對於多個具有相同參數數目的方法來說,應該儘量避免重載方法。我們應當保證:當傳遞同樣的參數時,所有重載方法的行爲必須一致。

[b]5. 慎用可變參數[/b]
Java1.5發行版本中增加了可變參數方法,可變參數方法接受0個或者多個指定類型的參數。可變參數機制通過先創建一個數組,數組的大小爲在調用位置所傳遞的參數數量,然後將參數值傳到數組中,最後將數組傳遞給方法,如下:
static int sum(int... args) {
int sum = 0;
for (int arg : args)
sum += arg;
retrun sum;
}
上面的方法可以正常的工作,但是在有的時候,我們可能需要至少一個或者多個某種類型參數的方法,如下:
static int min(int...args) {
if (args.length == 0)
throw new IllegalArgumentException("Too few arguments.");
int min = args[0];
for (int i = 0; i < args.length; ++i) {
if (args[i] < min)
min = args[i];
}
return min;
}
對於上面的代碼主要存在兩個問題,一是如果調用者沒有傳遞參數是,該函數將會在運行時拋出異常,而不是在編譯期報錯。另一個問題是這樣的寫法也是非常不美觀的,函數內部必須做參數的數量驗證,不僅如此,這也影響了效率。將編譯期可以完成的事情推到了運行期。下面提供了一種較好的修改方式,如下:
static int min(int firstArg,int...remainingArgs) {
int min = firstArgs;
for (int arg : remainingArgs) {
if (arg < min)
min = arg;
}
return min;
}
由此可見,當你真正需要讓一個方法帶有不定數量的參數時,可變參數就非常有效。

有的時候在重視性能的情況下,使用可變參數機制要特別小心。可變參數方法的每次調用都會導致進行一次數組分配和初始化。如果確定確實無法承受這一成本,但又需要可變參數的靈活性,還有一種模式可以彌補這一不足。假設確定對某個方法95%的調用會有3 個或者更少的參數,就聲明該方法的5 個重載,每個重載方法帶有0 個至3 個普通參數,當參數的數目超過3 個時,就使用一個可變參數方法:
public void foo() {}
public void foo(int a1) {}
public void foo(int a1,int a2) {}
public void foo(int a1,int a2,int a3) {}
public void foo(int a1,int a2,int a3,int...rest) {}
所有調用中只有5%參數數量超過3 個的調用需要創建數組。就像大多數的性能優化一樣,這種方法通常不恰當,但是一旦真正需要它時,還是非常有用處的。

簡而言之,在定義參數數目不定的方法時,可變參數方法是一種很方便的方式,但是它們不應該過度濫用。如果使用不當,會產生混亂的結果。

[b]6. 返回零長度的數組或者集合,而不是null[/b]
請看如下代碼:
public class CheesesShop {
private final List<Cheese> cheesesInStock = new List<Cheese>();
public Cheese[] getCheeses() {
if (cheesesInStock.size() == 0)
return null;
return cheeseInStock.toArray(null);
}
}
從以上代碼可以看出,當沒有Cheese 的時候,getCheeses()函數返回一種特例情況null。這樣做的結果會使所有的調用代碼在使用前均需對返回值數組做null 的判斷,如下:
public void testGetCheeses(CheesesShop shop) {
Cheese[] cheeses = shop.getCheeses();
if (cheese !=null && Array.asList(cheeses).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
}
對於一個返回null 而不是零長度數組或者集合的方法,幾乎每次用到該方法時都需要這種曲折的處理方式。很顯然,這樣是比較容易出錯的,因爲編寫客戶端程序的程序員可能會忘記寫這種專門的代碼來處理null返回值。如果我們使getCheeses()函數在沒有Cheese 的時候不再返回null,而是返回一個零長度的數組,那麼我的調用代碼將會變得更加簡潔,如下:
public void testGetCheeses2(CheesesShop shop) {
if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
}

有時候會有人認爲:null返回值比零長度數據更好,因爲它避免了分配數組所需要的開銷。這種觀點是站不住腳的,原因有兩點。第一,在這個級別上擔心性能問題是不明智的,除非分析表明這個方法正是造成性能問題的真正源頭。第二,對於不返回任何元素的調用,每次都返回同一個零長度數組是有可能的,因爲零長度數組是不可變的,而不可變對象有可能被自由地共享。
相比於數組,集合亦是如此。

[b]7. 爲所有導出的API元素編寫文檔註釋[/b]
Java語言環境提供了一種被稱爲Javadoc的實用工具,從而使這項任務變得很容易。Javadoc利用特殊格式的文檔註釋,根據源代碼自動產生API文檔。
爲了正確地編寫API文檔,必須在每個被導出的類、接口、構造器、方法和域聲明之前增加一個文檔註釋。如果類是可序列化的,也應該對它的序列化編寫文檔。

方法的文檔註釋, 應該簡潔地描述出它和客戶端之間的約定, 這個約定說明這個方法做了什麼, 而不是說明他是如何完成這項工作的。文檔註釋應該列舉出這個方法的所有前置條件和後置條件,前提條件是客戶端調用這個方法必須要滿足的條件,後置條件是指調用成功之後,哪些條件必須要滿足。一般情況下,前提條件是由@throws標籤針對示受檢的異常所隱含描述的,每個未受檢的異常都對應一個前提違例,當然也可以在@param標記中指定前提條件。除了前提條件和後置條件,還需要描述它們的副作用, 如果有的話。最後,文檔註釋也應該描述類或者方法的線程安全性。
爲了完整地描述方法的約定,方法的文檔註釋應該讓每個參數都有一個@param標籤,以及一個@return標籤(除非爲void),以及對於該方法拋出的每個異常,無論是受檢還是未受檢的,都有一個@throws標籤。
跟在@param標籤和@return標籤後面的文字應該是一個名詞短語,描述了這個參數或者返回值所表示的值。跟在@throws之後的文字應該包含單詞"if" 緊接着是一個名詞短語, 描述了這個異常將在什麼情況下拋出。有時候也會用算術表達式來代替名詞短語。按慣例,@param、@return或者@throws標籤後面的短語或者句子都不用句點來結束。如下:
/**
* 概要描述
*
* 詳細說明
*
* @param xxx
* @return xxx
* @throws xxx
* ({@code index < 0 || index >= this.size()})
*/
E get(int index);
Javadoc工具會把文檔註釋翻譯成HTML,文檔註釋中包含的任意HTML元素都會出現在結果HTML中,但是HTML元字符必須要經過轉義。使用javadoc中的{@code}標籤來代替html中的<code>標籤以代碼字體呈現。還有一種方法是用{@literal xxx}標籤將xxx包圍起來,除了它不以代碼字體渲染文本之外,其餘方面就像{@code}標籤一樣。

每個文檔註釋的第一句話成了該註釋所屬元素的概要描述,不一個類或者接口中的兩個成員或者構造器不應該具有同樣的概要描述。對類或接口而言概要描述應該是一個名詞短語,對於方法和構造器而言, 概要描述應該是一個完整的動詞短語, 描述了該方法所執行的動作
概要描述中的句點會過早的終止這個描述,最好的解決辦法是將句點或其他東西用{@literal}包起來,

還應該在文檔中對類中否是線程安全的,是否可序列化的進行說明。
雖然爲所有導出的API元素提供文檔註釋是必要的,但是這樣做並非永遠就足夠了。對於由多個關聯的類組成的複雜API,通常有必要用一個外部文檔來描述該API的總體結構,對文檔註釋進行補充。如果有這樣的文檔,相關的類或者包文檔註釋就應該包含一個對這個外部文檔的鏈接。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章