【Java】ArrayList のコンストラクタ参照にご用心

例えば、sRGB の10進定数をフィールドに持つ Item があったとして、

public static int RED   = Integer.parseInt("FF0000", 16);  
public static int GREEN = Integer.parseInt("00FF00", 16);  
public static int BLUE  = Integer.parseInt("0000FF", 16);

private record Item(int colorDec, String name) { }

この Item を色別にグルーピングして map に格納する。

private Map<Integer, List<Item>> map = new HashMap<>();

public void put(Item item) {  
    map.computeIfAbsent(item.color, ArrayList::new).add(item);
}

computeIfAbsent にて、キーが存在しない場合は、ArrayList::new で新しいリスト作成して追加。

でも、これは間違いで、やってしまいがち。 見た目も正しそうに見え、しかも正しく動作するので厄介。


メソッド参照を書き下すと、以下のようになり、

map.computeIfAbsent(item.color, (Integer i) -> new ArrayList(i)).add(item);

new ArrayList(int initialCapacity) はリストの初期容量を指定するコンストラクタで、以下のように配列が確保される。

transient Object[] elementData;

public ArrayList(int initialCapacity) {
    ...
    this.elementData = new Object[initialCapacity];
}

上記例では new Object[16711680] と、不要な大量メモリが確保されてしまうこととなる。

なので、以下のように引数なしのコンストラクタを呼ぶのが正しい。

public void put(Item item) {  
    map.computeIfAbsent(item.color, i -> new ArrayList()).add(item);
}

キーが Integer 以外のオブジェクトであれば、コンパイルエラーで気付くことができるが、キーが Integer の場合、意図しないコンストラクタ呼び出しとなり、しかもArrayList内で確保された容量は、ArrayList内でカプセル化され、外部からは知り得ない(リフレクション除く)ので、気付くことはほぼできない。

ので用心が必要。