52.オーバーロードは気を付けて使うべし
- オーバーロードはコンパイル時に呼び出すメソッドが決められる。
package tryAny.effectiveJava;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class OverloadTest {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> s) {
return "List";
}
public static String classify(Collection<?> s) {
return "Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = { new HashSet<String>(), new ArrayList<String>(),
new HashMap<String, String>().values() };
for (Collection<?> c : collections) {
System.out.println(classify(c));
}
/**
* Collection <br>
* Collection <br>
* Collection
*/
}
}
- 一方、オーバーライドはコンパイル時ではなく、実行時に決められる。
package tryAny.effectiveJava;
import java.util.List;
public class OverrideTest {
public static void main(String[] args) {
List<Wine> l = List.of(new Wine(), new SparklingWine(), new Champagne());
for (Wine w : l) {
System.out.println(w.name());
/**
* wine<br>
* sparkling wine<br>
* champagne
*/
}
}
}
class Wine {
String name() {
return "wine";
}
}
class SparklingWine extends Wine {
@Override
String name() {
return "sparkling wine";
}
}
class Champagne extends SparklingWine {
@Override
String name() {
return "champagne";
}
}
- 混乱を招くようなオーバーロードの使用は避ける。混乱を招くオーバーロードとは何か、ということについては議論の余地があるが、同じ引数の数で、オーバーロードさせるメソッドは作るべきでない、という意見は受け入れられている。
- 同じ引数の数でオーバーロードしたくなったら、違うメソッド名でメソッドを作るべきである。ObjectOutputStream は全てのprimitive型の書き込みメソッドを持っているが、それぞれ writeBoolean、writeInt等、writeをオーバーライドしたりせず、別のメソッドを作っている。
- コンストラクタの場合は、別の名称にすることはできないので、同じ数の引数で複数のコンストラクタを用意することがあるかもしれない。そういった場合に取る引数は、互いに根本的に異なる(radically different)のであれば比較的安全である(キャストできないような関係)。
- Java5になる前は、primitive型と全ての参照型は根本的に異なるものであったが、autoboxingが出現したことによってその前提が覆された。以下のコードでは、直感的には、 [-3, -2, -1] [-3, -2, -1]が表示されそうだが、Listのremoveには、int を引数に取るremoveメソッドがオーバライドされているため、実際には、[-3, -2, -1] [-2, 0, 2]となる。
package tryAny.effectiveJava;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class SetList {
public static void main(String[] args) {
Set<Integer> s = new TreeSet<>();
List<Integer> l = new ArrayList<>();
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);// [-3, -2, -1] [-2, 0, 2]
}
}
- 以下のコードは、
exec.submit(System.out::println);
の部分でコンパイルエラーになる。このsubmitメソッドは、submit(Runnable task);
とsubmit(Callable<T> task);
でオーバーロードされており、コンパイラがどちらを使うか判断できないようになる(判断できない理屈は難しくてわからず。System.out::println
は inexact method referenceであるとかなんとか)。とにかく、混乱を避けるためにも、同じ引数の位置で異なるfunctional interface でオーバーロードするのは避けるようにするべき。
package tryAny.effectiveJava;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Snippet {
public static void main(String[] args) {
new Thread(System.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println); //コンパイルエラー
}
}
- 以前作成したコードの修正等でやむを得ずオーバーロードせねばならない場合には、同じ動作をするよう保証して作る。
// Ensuring that 2 methods have identical behavior by forwarding
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb);
}