はじめに

ArrayList#get(int index) の引数indexにArrayListのsizeの範囲を超える数字を入れると、 IndexOutOfBoundsException もしくは ArrayIndexOutOfBoundsException が発生する。indexによって異なる例外が発生する。indexが負数のときがArrayIndexOutOfBoundsExceptionで、index >= sizeのとき(greater than or equal to, gte)がIndexOutOfBoundsExceptionとなる。

発生する例外に違いが出る原因の実装調査と、このような実装になっている根本理由の調査、そしてStack OverFlowで質問して合点が行くまでの経緯をまとめる。

rangeCheckメソッド

indexが負数かgteかで変わる原因を調べる。ArrayList#getの中でまず初めにrangeCheckメソッドを呼び出していた。rangeCheckを呼び出しているのはget以外にもsetとremoveがあったが、どれもまず初めに呼び出していた。

/**
 * Checks if the given index is in range.  If not, throws an appropriate
 * runtime exception.  This method does *not* check if the index is
 * negative: It is always used immediately prior to an array access,
 * which throws an ArrayIndexOutOfBoundsException if index is negative.
 */
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

rangeCheckのJavaDoc及び実装を見ると、indexが負数の時はチェックせず、ArrayListが内部で持つ配列にアクセスした時の検査に任せ、配列アクセス時にArrayIndexOutOfBoundsExceptionを投げるようになっていた。

ArrayIndexOutOfBoundsExceptionはIndexOutOfBoundsExceptionのサブクラスなので、投げられる例外がこのように異なっていても両方ともIndexOutOfBoundsExceptionでcatchできるから問題ないのだが、なぜわざわざ負数のときはチェックせずに配列の検査機構に任せているのかという理由を探ることにした。

負数をチェックしない理由

そもそもJDKの実装で負数をチェックしないのはArrayList導入時からではなく、Java1.4.2からのようだ。
JDK-4819074 : Remove the index < 0 test from RangeCheck in ArrayListで、配列の検査機構でもできる負数のチェックを行うのはredundant、つまり冗長で、頻繁に呼び出されるgetのようなメソッドで不要なチェックをしていることになると主張され、1.4.2になるときに負数チェックを行わないようになっている。

get, setメソッドは頻繁に呼び出されるのでパフォーマンス重視なのはわかるが、Java Language Specificationで、配列の検査機構はindexが負数の時だけでなく、indexと配列のlengthを比べて大なりイコールであった場合もArrayIndexOutOfBoundExceptionを投げると定義されているので、それではArrayListのrangeCheckメソッド自体要らないのではないかと思ってしまった。

Stack OverFlowでの解決

rangeCheckメソッドの存在意義がわからなくなってしまったのでStack OverFlowで聞いてみた。

Why does ArrayList#rangeCheck not check if the index is negative?

回答はすぐに貰えて、しかも当たり前すぎる事実が意識から抜けていたのが悩みの原因だったとわかった。

ArrayListが内部で保持する配列(the backing array)がArrayListの現在のサイズよりも大きいかもしれないから、上限チェックは行わなければならないということだった。

ArrayListは現在のサイズを超えると上限を拡張してくれるので、内部保持の配列のlengthは当然sizeよりも常に等しいか大きい。
size <= lengthなので、配列の検査機構を使ってしまうと、index < lengthは保障されても、index < sizeが保障されなくなってしまう。