整理 Effective Java 書中 Item 52: Use overloading judiciously 心得筆記

主旨

在 Java 中,方法的 overloading(多載) 是一種常見語法糖,但如果使用不當,容易導致令人困惑的行為與難以追蹤的錯誤。尤其當泛型、自動裝箱(autoboxing)、lambda 及 method reference 進入語言後,混淆性更高。本條目要提醒我們:能用不代表該用,overloading 要非常小心設計。

點出問題:overloading 不是動態綁定

先看一段看似沒問題的程式碼:

public class CollectionClassifier {
    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<>(),
            new ArrayList<>(),
            new HashMap<>().values()
        };
        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

你可能以為會輸出:

Set
List
Unknown Collection

但實際輸出卻是:

Unknown Collection
Unknown Collection
Unknown Collection

原因在於 overloading 是編譯期決定的行為,而不是像 overriding 那樣在執行時依照物件實際型別動態 dispatch。這裡 cCollection<?>,所以會選到第三個 classify(Collection<?>),即使實際上是 SetList

範例:Set 和 List 的 remove 陷阱

以下這段程式碼也充滿陷阱:

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);

輸出為:

[-3, -2, -1] [-2, 0, 2]

set.remove(i) 呼叫的是 remove(Object),而 list.remove(i) 呼叫的是 remove(int)(移除第幾個 index),這就是 overloading 帶來的混淆。

修正方式:

list.remove((Integer) i);

明確告訴編譯器你要的是 remove(Object) 這個版本。

劃重點

  • overloading 是靜態決定的,無法依據執行時型別做選擇。
  • 避免讓多個 overload 有相同參數數量,尤其當它們型別容易混淆時(例如 Integer vs int)。
  • 若參數型別無法「徹底區分」,就容易選錯版本。
  • lambda 與 method reference 也會因為參數不明確導致編譯錯誤。
  • 建議改用不同名稱的方法(像是 writeInt() vs writeLong()),比起 overloading 更清楚易懂。

真實世界範例

Java 標準庫的 String.valueOf(char[])String.valueOf(Object),傳入一個 char[] 陣列時會走不同邏輯,如果你不清楚哪個方法會被選中,結果可能出乎意料。

另一個例子是:

ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println); // 編譯錯誤

因為 submit 同時有 RunnableCallable<T> 的 overloading,會讓 method reference 無法決定適用哪個。

小結

建議做法如下:

  • 避免暴露多個參數數量相同的 overloading。
  • 不同方法請使用不同名稱,而非倚賴 overloading。
  • 若一定要 overloading,要確保參數型別彼此「截然不同」。
  • lambdas 與 method references 特別容易踩雷,避免在同樣參數位置 overloading 不同 functional interface。
  • 建構子沒得改名字,可考慮改用 static factory。

Overloading 本身不是錯,但錯在容易造成「以為會跑某個方法,實際卻跑了別的」。為了 API 可預測性與可維護性,慎用 overloading。