HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

Generics+Auto-boxing の罠

Auto-boxing が導入されたことで、本来は Collection に格納できない(ラッパークラスによる明示的なラッピングが必要な)プリミティブ型の値を、コード上では明示的なラッピングをしなくても格納できるようになった。
しかし、あるプリミティブ型の引数をとるメソッド、およびそれに対応するラッパークラスを引数にとる同名のメソッドが提供されるクラスを使用する場合は注意が必要。
これは、コレクションクラスに対して Generics の型パラメータにIntegerなどを指定した場合に発生する問題が典型的な例。

import java.util.ArrayList;
import java.util.List;

public class Main {

    /**
     * @param args
     */
    public static void main(String[] args) {
        List<Byte> listByte = new ArrayList<Byte>();
        List<Short> listShort = new ArrayList<Short>();
        List<Integer> listInteger = new ArrayList<Integer>();
        List<Long> listLong = new ArrayList<Long>();
        List<Float> listFloat = new ArrayList<Float>();
        List<Double> listDouble = new ArrayList<Double>();

        listByte.add((byte) 2);
        listByte.add((byte) 4);
        listByte.add((byte) 6);
        listByte.add((byte) 8);

        listShort.add((short) 2);
        listShort.add((short) 4);
        listShort.add((short) 6);
        listShort.add((short) 8);

        listInteger.add(2);
        listInteger.add(4);
        listInteger.add(6);
        listInteger.add(8);

        listLong.add(2L);
        listLong.add(4L);
        listLong.add(6L);
        listLong.add(8L);

        listFloat.add(2F);
        listFloat.add(4F);
        listFloat.add(6F);
        listFloat.add(8F);

        listDouble.add(2.);
        listDouble.add(4.);
        listDouble.add(6.);
        listDouble.add(8.);

        System.out.printf("%-8s: %s\n", "Byte", listByte);
        System.out.printf("%-8s: %s\n", "Short", listShort);
        System.out.printf("%-8s: %s\n", "Integer", listInteger);
        System.out.printf("%-8s: %s\n", "Long", listLong);
        System.out.printf("%-8s: %s\n", "Float", listFloat);
        System.out.printf("%-8s: %s\n", "Double", listDouble);

        // リスト中の要素 '2' を削除するつもり(remove(T) を呼びたい)
        listByte.remove((byte) 2);
        listShort.remove((short) 2);
        listInteger.remove(2);
        listLong.remove(2L);
        listFloat.remove(2F);
        listDouble.remove(2.);
        // リスト中の要素 '8' を削除(こっちは明らかに remove(T) の呼び出し)
        listByte.remove(Byte.valueOf((byte) 8));
        listShort.remove(Short.valueOf((short) 8));
        listInteger.remove(Integer.valueOf(8));
        listLong.remove(Long.valueOf(8L));
        listFloat.remove(Float.valueOf(8F));
        listDouble.remove(Double.valueOf(8));

        System.out.println("=== after removing ===");
        System.out.printf("%-8s: %s\n", "Byte", listByte);
        System.out.printf("%-8s: %s\n", "Short", listShort);
        System.out.printf("%-8s: %s\n", "Integer", listInteger);
        System.out.printf("%-8s: %s\n", "Long", listLong);
        System.out.printf("%-8s: %s\n", "Float", listFloat);
        System.out.printf("%-8s: %s\n", "Double", listDouble);
    }

}

これを実行すると次のようになる。

Byte    : [2, 4, 6, 8]
Short   : [2, 4, 6, 8]
Integer : [2, 4, 6, 8]
Long    : [2, 4, 6, 8]
Float   : [2.0, 4.0, 6.0, 8.0]
Double  : [2.0, 4.0, 6.0, 8.0]
=== after removing ===
Byte    : [2, 4]
Short   : [2, 4]
Integer : [2, 4]
Long    : [4, 6]
Float   : [4.0, 6.0]
Double  : [4.0, 6.0]

いずれも「[4, 6]」または「[4.0, 6.0]」が残ってほしかったのだが‥
Longリスト、Floatリスト、Doubleリストは指定したオブジェクトを削除する remove(Long)、remove(Float)、remove(Double) が呼び出され、コード中のコメントに書いたとおりの動作をするが、Byteリスト、ShortリストとIntegerリストは指定した位置の要素を削除する remove(int) が呼び出される。ByteリストやShortリストではキャストしているけど、意味はなし。
もちろん、DoubleリストやFloatリスト、Longリストでも、型を明確に示す文字(double型の小数点、float型の'F'、long型の'L')がなければ remove(int) の呼び出しとみなされてしまう。
結局、Auto-boxing が導入されたとは言っても、それに頼ってはいけないケースが存在するという‥
まぁ、確かに Java 2 SE 1.4.2 以前では remove(int) と remove(Object) があり、書いたとおりのパラメータに対応するメソッドが呼び出される(つまり remove(2) と書けば、「2」は Object ではない)から、互換性は保たれているわけだけど、Java 2 SE 5.0 から入った人や、Java 2 SE 1.4.2 以前から Java を使っていてもちゃんと意識していないとはまるんじゃないかと。