List#subListの罠

String#subStringって結構メジャーなメソッドだと思うんですけど、List#subListって知名度どれくらいかなー。subStringの感覚で使ったら痛い目あうよー・・・ってそんなことどれだけの人が知ってるんだよ!!というお話です。
このシンプルなプログラムの実行結果がどうなるか予想できるだろうか。

List<String> list = new ArrayList<String>();
list.add("dog");
list.add("cat");

List<String> list1 = list.subList(0,1); // dog
List<String> list2 = list.subList(1,2); // cat

list1.add("pig");

System.out.println(list);
System.out.println(list1);
System.out.println(list2);

多分、普通のJavaプログラマなら

[dog, cat]
[dog, pig]
[cat]

っていうでしょう。
しかし、実際はこうなります。

[dog, pig, cat]
[dog, pig]
ConcurrentModificationException発生!

すごいですよね。目を疑いますよね。
ちゃんとJavaソースコードを読んで理解しましたので、ちょっとずつ解説していきます。

List#subList

元凶はここです。
subListの実際のメソッドはAbstractList#subListです。
ちょっと省略して書きますが、このメソッドがどうなっているかというとこんな感じです。

return new SubList<E>(this, fromIndex, toIndex));

勘の良い人はピンときたと思いますが、第一引数にthis渡しちゃってます。List#subListの結果は、String#subStringの時のように、確かに別インスタンスが返ってきているんですが、そいつがもとのリストへの参照を保持しています。

SubList#add

SubList#addメソッドですが、内部の大事なところを抜粋すると

l.add(index+offset, element);

となっています。lは先程の第一引数で渡ってきたリストです。つまり、実際は単に委譲しているだけ。これにより、list1にaddしたつもりが大元のlistに値が追加されてしまいます。あらら。。

ConcurrentModificationException発生のわけ。

この例外が出る時っていうのは、forでリストを回して処理している時に、そのリストに値を追加したり削除したりすると起きます。
その実装はこんな感じ。

private void checkForComodification() {
    if (l.modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

期待している数と実際の数が異なったときに、変な状態になった!例外送出!となるわけです。
で、今回はlist1もlist2もlistへの参照を内部的に持っているわけですが、list1.addを実行したことでlistの数が変わる、つまりlist2の状態が変になった!ということになるわけです。それがList#toStringが呼ばれたときに検出されるので、System.out.println時に例外が出る、と。


会社では新人教育が始まる季節ですが、ネタに使えるかもしれません。

追記(2010.3.13)

id:yoshioriさんにブックマークコメントいただきましたので、試してみました。

String str = "hello";
String substring = str.substring(2);	// llo

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);

value[4] = 'x';

System.out.println(str);
System.out.println(substring);

実行結果は

hellx
llx

でした。
確かにString#substringメソッドは、

return new String(offset + beginIndex, endIndex - beginIndex, value)

となっていて、自分自身が保持しているchar[]のvalueを渡していました。
Stringクラスはイミュータブルなので、今回のようなリフレクションプログラミングをしないと値変更できないため、気づきにくいですが結局はList#subListもString#substringも仕組みは同じなんですね。
いやー、勉強になりました。