Effective Java英文第三版讀書筆記(3) -- 函數最佳實踐

寫在前面

《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。對于聲明為publicprotected的方法使用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類已經內置了這個問題,如果mnullm.signum()將拋出NullPointerException。從Java7開始,使用Objects.requireNonNull方法可以方便地進行空指針檢查。Java9進一步在Object引入了下標檢查checkFromIndexSize,checkFromToIndexcheckIndex

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()。這種方法在有可選參數的場景尤其適用。

對于參數類型

  1. 如果對象是某個接口的實現,則最好用接口類型而不是類類型。例如用 Map 而不是 HashMap。
  2. 除非意義明確,使用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> 是一種不可變的容器類,它包含單個的非nullT 對象,或者什么也不包含(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)

(略)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容