読者です 読者をやめる 読者になる 読者になる

【Effective Java】項目41:オーバーロードを注意して利用する

オーバーロード

オーバーロードとは同じ名前・同じ数の引数を持つメソッドに対して、呼び出し側の引数に応じて、対応するメソッドが呼び出される機能のことです。

たとえば、引数の型ごとに型の名前を返すメソッドをオーバーロードを使って実装します。

public class Overload {
    public static String classify(int a) {
        return "int";
    }
    public static String classify(char a) {
        return "char";
    }
    public static String classify(double a) {
        return "double";
    }
}

このように定義すると、オーバーロードの機能によって、呼び出し側引数に応じて異なるメソッドが呼び出されます。

Overload.classify(1); // => "int"
Overload.classify('a'); // => "char"
Overload.classify(1.0); // => "double"

今度は Set, List, Collection のそれぞれの型を出力するメソッドを定義してみます。

public class Overload {
    public static String classify(Set<?> a) {
        return "Set";
    }
    public static String classify(List<?> a) {
        return "List";
    }
    public static String classify(Collection<?> a) {
        return "Collection";
    }
}

これに対して、それぞれの型のインスタンスを Collection 配列に定義し、ループを使って classify メソッドを呼び出します。

Collection<?>[] collections = {
    new HashSet<String>(),
    new ArrayList<BigInteger>(),
    new HashMap<String, String>().values()
};

for (Collection<?> c : collections) {
    System.out.println(Overload.classify(c));
}

collections にはそれぞれ HashSet と ArrayList と HashMap.Values のインスタンスが含まれています。

// 実行結果
"Collection"
"Collection" 
"Collection" 

順に "Set"、"List"、と出力されるように思えますが、実際には "Collection" が3回出力されます。

これはオーバーロードのメソッド解決はコンパイル時に行われることが理由です。 collections は Collection[] なので Collection を引数にとるメソッドが呼び出されます。

オーバーライド

オーバーロードコンパイル時に呼び出しメソッドが決定されました。 一方、オーバーライドは実行時に呼び出しメソッドが決定されます。

たとえば次のようなクラス群があります。

public class A {
    public String name() { return "A"; }
}
public class B extends A {
    public String name() { return "B"; }
}
public class C extends B {
    public String name() { return "C"; }
}

これに対して、オーバーロードの時と同じようにして A 型の配列を定義します。 そして、それぞれのインスタンスに対し name() を呼び出します。

A[] array = {
    new A(),
    new B(),
    new C(),
};

for (A a : as) {
    System.out.println(a.name());
}
// 実行結果
"A"
"B"
"C"

この実行結果は多くの人が意図する通り "A"、"B"、"C" と出力されます。

このようにオーバーロードは静的に、オーバーライドは動的にメソッド呼び出しが決定されます。

オーバーロードの利用

多くの人はオーバーロード時にもオーバーライドのような動的な挙動を期待します。 オーバーロードの利用は混乱を招きます。 そのため、オーバーロードの利用は一般的に避けるべきです。

オーバーロードの代替として引数の型に応じてメソッド名を変更する方法があります。

例えば、ObjectOutputStream には Stream に書き込む write メソッドが用意してあります。 これらはオーバーロードを利用していません。 その変わりに writeBoolean(boolean)、writeInt(int)、writeLong(long) などの名前付シグネチャを使っています。

コンストラクタの場合には異なる名前を利用することはできません。 ただし、static ファクトリーメソッドを提供するという選択肢があります。

自動ボクシングの影響

オーバーロードするメソッドを提供する場合、呼び出されるメソッドが明確であれば問題は起こりにくいです。 例えば、あるメソッドの引数の型を他のメソッドの引数の型にキャストできないのであれば、どちらが呼び出されるかについて混乱することはないでしょう。

一方、相互にキャストできるような引数を持つメソッドがある場合には、実際にどちらが呼び出されるのかについて混乱を招くでしょう。

明示的なキャスト以外にも Java 5 から導入された自動ボクシングが問題を引き起こす場合があります。 例えば Set と List を使った以下のようなコードを考えます。

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

// set と list の内容は?

このコードでは set と list に (-3, -2, -1, 0, 1, 2) を追加してから (0, 1, 2) を削除しようとしています。 しかし、実際には list の方は [-2, 0, 2] となるのです。

list.add(i) が呼び出される時、i は int から Integer に自動ボクシングされて list.add(Object) が呼び出されます。

一方、remove() を呼び出す場合、i は int 型としてそのまま渡されます。 インデックスを指定して要素を削除する list.remove(int) が呼び出されてしまうのです。

もし、これを意図通りにするためには、以下のようにする必要があります。

for (int i = 0; i < 3; i++) {
    set.remove(i);
    list.remove((Integer) i);
}

このように自動ボクシングやキャストなどが絡むオーバーロードメソッドを定義していると、どのメソッドが呼び出されるかを判断するのが難しくなります。 実際、この呼び出し規則はかなり複雑な仕様になっていて、言語仕様上で33ページにもわたります。

この細かい規則を覚えているプログラマはいないでしょう。

新たなインタフェースの追加

既存のクラスに新たなインタフェースを実装するように修正する場合があります。 例えば、String クラスは Java 4 から contentEquals(StringBuffer) メソッドを持っています。 Java 5 では String クラスは contentEquals(CharSequence) メソッドが追加されています。

すなわち、String クラスに contentEquals(StringBuffer) と contentEquals(CharSequence) の二つのオーバーロードメソッドが存在します。 しかし、この二つの contentEquals はまったく同じ処理を行うように実装されています。

public boolean contentEquals(StringBuffer sb) {
    return contentEquals((CharSequence) sb);
}

このようにしておけば contentEquals() の利用者はどのメソッドが呼び出されるかについて悩む必要がなくなります。 もし、やむを得ずオーバーロードを利用する場合には、このように同じ処理を行うようにするべきです。

このようにすれば、実際には二つのメソッドがあること自体を意識する必要すらありません。

一方、Java にはこのアドバイスを守らないメソッドも多くありますが、このようなメソッドは参考にしないべきです。