寫在前面
《Effective Java》原書國內的翻譯只出版到第二版,書籍的編寫日期距今已有十年之久。這期間,Java已經更新換代好幾次,有些實踐經驗已經不再適用。去年底,作者結合Java7、8、9的最新特性,編著了第三版(參考https://blog.csdn.net/u014717036/article/details/80588806)。當前只有英文版本,可以在互聯網搜索到PDF原書。本讀書筆記都是基于原書的理解。
以下是正文部分
函數最佳實踐(Methods)
本章包括:
- 實踐49 校驗參數的有效性(Check parameters for validity)
- 實踐50 必要時,使用保護性拷貝(Make defensive copies when needed)
- 實踐51 仔細設計函數簽名(Design method signatures carefully)
- 實踐52 審慎地使用重載(Use overloading judiciously)
- 實踐53 審慎地使用varargs(Use varargs judiciously)
- 實踐54 返回空集合而不是null(Return empty collections or arrays, not nulls)
- 實踐55 審慎地使用 Optional 作為返回(Return optionals judiciously)
- 實踐56 為所有外部暴露API編寫注釋文檔(Write doc comments for all exposed APIelements)
實踐49 校驗參數的有效性(Check parameters for validity)
在編程時,對于函數入參可能會有一些限制,例如參數作為數組下標不能為負數;參數作為對象引用不能為null。最好的實踐是注釋說明參數限制并在函數入口進行參數檢查,越早發現問題越好。
對于聲明為private
的函數,由于開發者有完全的控制權和訪問與其,在內部使用assert
來校驗即可。注意,只有在啟動java程序時,指定了-ea
或者-enableassertions
參數,assert語句才回發揮實際用途,在校驗失敗時拋出AssertionError
。對于聲明為public
和protected
的方法使用Javadoc的@throws
注釋關鍵詞來注明如果函數調用違背了規則,將拋出的異常。通常,異常的類型為IllegalArgumentException
, IndexOutOfBoundsException,
, NullPointerException
。示例如下:
// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
}
/**
* * Returns a BigInteger whose value is (this mod m). This method * differs from the remainder
* method in that it always returns a * non-negative BigInteger. *
*
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0) throw new ArithmeticException("Modulus <= 0: " + m);
}
注意上述代碼并沒有注明NullPointerException
,這是因為BigInteger
類已經內置了這個問題,如果m
為null
則m.signum()
將拋出NullPointerException
。從Java7開始,使用Objects.requireNonNull
方法可以方便地進行空指針檢查。Java9進一步在Object
引入了下標檢查checkFromIndexSize
,checkFromToIndex
和 checkIndex
this.strategy = Objects.requireNonNull(strategy, "strategy");
如果檢查特別耗時,或者實現上不可行又或者在相關點已經有了檢查,這幾種情況下可以省略參數校驗。
實踐50 必要時,使用保護性拷貝(Make defensive copies when needed)
// Broken "immutable" time period class
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;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
構造一個合理的時間段,起始時間保證早于結束時間,初看該函數是沒問題的。但是注意由于Java在傳遞對象時,使用的是引用拷貝,作為參數Date
在外部的修改會影響類內的引用值。典型的攻擊手法如下:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
對于這個問題,在Java8中可以使用Instant
或者ZonedDateTime
類替換Date
。推而廣之,我們可能在某些API確實需要傳入可變對象,但在類內部希望其值不可再修改。這是,就需要用到保護性性拷貝(defensive copy)技術。上面的缺陷類可以這么修改,注意,這兩句拷貝必須放在函數最開始。為了防止操作時對象在其他線程被修改,拷貝完,再做參數校驗等其他操作。
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
代碼中用的是最原始的構造函數來建立新的對象而不是clone
方法。這是因為clone方法不一定返回java.util.Date
原始對象,它如果在子類中被重寫,可能在拷貝過程中進行某些污染操作,并返回java.util.Date
的子類對象。
解決了第一種修改"不變量"的攻擊,還有第二種等著我們...
// Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
針對這種操作,我們需要對函數返回的不希望被修改的內部可變對象作保護性拷貝。
public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }
實踐51 仔細設計函數簽名(Design method signatures carefully)
首先,函數名要符合編程規范,并且在一個包內部保持風格一致。不要使用過長的函數名,但同時也要保持它的可閱讀性。
不要為了使用方便而抽象出過多的函數。函數太多會增加學習、使用、文檔、測試及維護的負擔。
避免過長的函數參數列表,尤其是很多參數的類型都相同的時候,容易誤用。實踐中,建議參數個數盡量少于5個。下面列出三種解決參數列表過長問題的方法
- 把函數重構成多個函數,每個函數只需要傳入部分參數進行處理
- 將多個參數視為一個對象,創建輔助靜態成員類
- 使用
Builder模式
進行對象創建及函數調用,例如在類的Builder模式
最后是build(),在函數的Builder模式
最后是execute()。這種方法在有可選參數的場景尤其適用。
對于參數類型
- 如果對象是某個接口的實現,則最好用接口類型而不是類類型。例如用
Map
而不是HashMap
。 - 除非意義明確,使用Enum代替Boolean,增加代碼可讀性。
實踐52 審慎地使用重載(Use overloading judiciously)
下面的程序對classify
函數進行了重載,期望能判斷集合的類型。從功能角度,程序設計者的一個預期輸入應當是 "Set List Unknown Collection" ??蓪嶋H上,運行這段程序的輸出是 "Unknown Collection Unknown Collection Unknown Collection" 。
// Broken! - What does this program print?
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));
}
}
重載函數的調用是在編譯時(compile time)決定的,也就是說對于上述程序,運行時(runtime)調用classify
只有一個選擇——classify(Collection<?>)
。
- 重載函數的選擇是靜態的,編譯時確定
- 重寫函數的選擇是動態的,運行時確定
classify
可以修改如下:
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}
對于重載,一個安全、保守的建議是:重載函數的參數個數不能相同(如果函數有varargs,那么久不要進行重載)。Java標準庫的ObjectOutputStream
給出了很好的示例,對于寫操作,它沒有去重載write
,而是針對不同的參數類型,給出了writeBoolean(boolean)
,writeInt(int)
,writeLong(long)
等。稍微放寬一點,至少保證參數類型完全不同且無法相互轉換。例如ArrayList
的兩種構造方法,一個是參數為int,一個是參數為Collection
。這種情況下,使用時基本不會搞混。
另一個誤用的示例如下:
class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list);
}
}
執行main函數將輸出[-3, -2, -1] [-2, 0, 2]
。
實踐53 審慎地使用varargs(Use varargs judiciously)
從原理上講,varargs
會根據入參個數,創建一個相同大小的數組,把所有參數放入數組中,然后再傳遞到方法里面。因此 varargs
可以直接當數組使用,比較方便,但是同時數組的空間分配和初始化又會引入額外的系統開銷,不適用與高性能場景。
static int sum(int... args) {
int sum = 0;
for (int arg : args) sum += arg;
return sum;
}
關于性能問題,一個可能的替代方式是,對于常用場景不使用 varargs
。例如 95% 的情況下,參數不超過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) {}
實踐54 返回空集合而不是null(Return empty collections or arrays, not nulls)
我們常??吹竭@樣的代碼:
/**
* * @return a list containing all of the cheeses in the shop, * or null if no cheeses are
* available for purchase.
*/
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
這種返回 null
的做法,是的“空”這個場景被特殊化。所有外部調用點,都需要針對這種空場景進行處理。一旦外部處理不當,可能會在運行時導致程序出錯。因此,此時應當返回一個空的集合,而不是 null
。返回空集合并不增加多少實際的系統開銷。
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
推薦的方式如下:
//The right way to return a possibly empty collection public
List<Cheese> getCheeses() {
return new ArrayList<>(cheesesInStock);
}
//The right way to return a possibly empty array public
Cheese[] getCheeses() {
return cheesesInStock.toArray(new Cheese[0]);
}
如果實在擔心性能問題,可以用 static
空集合。Java 標準庫中已經集成了 Collections.emptyList
, Collections.emptySet
, Collections.emptyMap
幾種。
// Optimization - avoids allocating empty collections
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheesesInStock);
}
// Optimization - avoids allocating empty arrays
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
實踐55 審慎地 Optional 作為返回(Return optionals judiciously)
在Java8中,Optional<T>
是一種不可變的容器類,它包含單個的非null
的 T
對象,或者什么也不包含(nothing at all)。有了 Optional<T>
,我們處理空返回不用像拋出異常那樣重型化,也不用專門返回 null
特殊處理。
// Returns maximum value in collection as an Optional<E>
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
if (c.isEmpty()) return Optional.empty();
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return Optional.of(result);
}
針對返回 Optional
的函數。調用者可以做的操作包括:
// 收到空值時指定其他返回值
String lastWordInLexicon = max(words).orElse("No words...");
// 收到空值時拋出異常,注意此處使用函數式編程方法,避免了非空時浪費資源創建異常
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
// 直接取內容,可能拋出 NoSuchElementException
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
另外,Optional
提供 isPresent()
方法來驗證是否非空。但是,通常上面幾種用法可以避免使用 isPresent()
,并且更加簡潔。
雖然 Optional
有上述優點。但是并非所有數據類型都適用于它。還有,相比于返回 null
,Optional
的開銷更大。
- 所有集合類都不適用。例如,返回空的List<T>優于返回空的
Optional<List<T>>
。 - 也不要用它來返回一個封裝基礎類型,例如
Optional<Integer>
。 - 不要再數組、集合的key,value上用它
實踐56 為所有外部暴露API編寫注釋文檔(Write doc comments for all exposed APIelements)
(略)