Oracle Java のバグっぽいのを見つけた

何の変哲もないように見える以下の Java コード。これは実行するとエラーが発生します。

どんなエラーが発生するかわかるかな?

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

public class Test {
    public static void main(String[] args) {
        List list = Collections.synchronizedList(new ArrayList());
        list.add(list);
        list.remove(list);
    }
}

正解は StackOverflowError (←反転すると見えるよ)でした。

以下はなんでエラーが出ちゃうのかって解説。




ArrayList の remove(Object) はこんな感じ。

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) { // equals が呼び出される
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

リストから指定したオブジェクトを削除するので、equals で比較するのは当然ですね。

呼び出された Collections$SynchronizedList の equals(Object) はこんな感じ。

        public boolean equals(Object o) {
            synchronized(mutex) {return list.equals(o);}
        }

list というのは、リストの実体。o は Collections$SynchronizedList のインスタンス

ここから呼び出された AbstractList の equals(Object) はこんな感じ。

    public boolean equals(Object o) {
        if (o == this)
            return true; // ★
        if (!(o instanceof List))
            return false;

        ListIterator<E> e1 = listIterator();
        ListIterator e2 = ((List) o).listIterator();
        while(e1.hasNext() && e2.hasNext()) {
            E o1 = e1.next();
            Object o2 = e2.next();
            if (!(o1==null ? o2==null : o1.equals(o2))) // リストの中身同士を比較する
                return false;
        }
        return !(e1.hasNext() || e2.hasNext());
    }

このコードの意味は、リストはインスタンスが異なってもリストの中身が全部同じだったら同一と見なす、ということですね。おぉ、コストが高いな。

ここでリストの中身同士を比較した際に、AbstractList 同士で比較した場合なら★の部分で終わるはずなんだけど、片方が Collections$SynchronizedList の皮がかぶっているから始末が悪い。

一応書いておくと、o、o1、o2 はすべて Collections$SynchronizedList のインスタンスだ。ここで Collections$SynchronizedList の equals(Object) に再び戻る。

    public boolean equals(Object o) {
        synchronized(mutex) {return list.equals(o);}
    }

リストの実体と Collections$SynchronizedList のインスタンスを比較、以下繰り返し。


これは、Collections$SynchronizedList#equals(Object) が以下のようになっていれば起こらないエラーだった。

    public boolean equals(Object o) {
        synchronized(mutex) {return this == o ? true : list.equals(o);}
    }

説明も簡単だし、再現性も 100% なので Oracle にバグレポートを出してみた(バグだよね?)。レポート中に「バグの深刻さ」という項目があったけど、「No Impact」しか選べない。