割と初歩的なことかもしれませんが、なるほどーと思ったのでメモします。
Javaで配列を List<T> に変換したい場合、
String ids[] = new String[]{"a1", "a2", "a3"};
List<String> idList = Arrays.asList(ids);
って書くと思います。
でもこのままだと add ができないので(addメソッドを呼ぶと UnsupportedOperationException がスローされる)、
String ids[] = new String[]{"a1", "a2", "a3"};
List<String> idList = new ArrayList<>(Arrays.asList(ids));
という具合に、生成した ArrayList をさらに ArrayList のコンストラクタに渡して new する、というコードを書くことがあるかと思います。
この記事では、なんで Arrays.asList() で生成した ArrayList は add ができないのに、それを ArrayList のコンストラクタの引数に渡すだけで add できるようになるのか、という疑問について、Java SEのソースコードを見ながら考えてみます。
2つのArrayListクラス
まず、実は ArrayList<E> というクラスはJava SEの中に2つあります。
一つ目が java.util.ArrayList<E> で、二つ目が java.util.Arrays$ArrayList<E> です。名前は同じですが、定義されている場所が違うので全く別のクラスです。
後者の java.util.Arrays$ArrayList<E> は Arrays クラスのprivateな内部クラスですので、普通に new することはできません。
つまり、単純に
List<String> idList = new ArrayList<>();
と書いたときに生成されるのは前者の java.util.ArrayList<E> になります。
そして、
String ids[] = new String[]{"a1", "a2", "a3"};
List<String> idList = Arrays.asList(ids);
と書いた時に生成される idList が後者の java.util.Arrays$ArrayList<E> ということになります。
new ArrayList<>(Arrays.asList("a1", "a2", "a3")) について考える
というわけで、「なんで Arrays.asList() で生成した ArrayList は add ができないのに、それを ArrayList のコンストラクタの引数に渡すだけで add できるようになるのか」という疑問の答えはただ一言「違うクラスだから」です。同じクラスだけど生成のされ方によって add() の挙動が変わる、というわけではなくて、そもそも別クラスで実装も別なわけですね。
では、ここからは2つのクラスの add() メソッドがどのような実装になっているか、また、 new ArrayList<>(Arrays.asList(ids)) と書いたときにどのような処理がされているかについて少し詳しく見ていきます。
java.util.ArrayList<E> の add() メソッド
java.util.ArrayList<E> の add() メソッドはこんな感じです。
transient Object[] elementData; // non-private to simplify nested class access
...省略
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
...省略
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
...省略
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
...省略
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
ここまで引用しておいて雑な説明になりますが、要はインスタンスが保持している配列の空き容量をチェックして、空きがあればそのままその配列に、空きがなければ新しく生成した長さが十分確保された配列に、引数で渡したインスタンスを格納する、という流れになっています。
これなら、普通に考えた場合の「新しく追加するインスタンスをリストの最後に追加する」というイメージままの実装なのではないかと思います。
java.util.Arrays$ArrayList<E> の add() メソッド
一方 java.util.Arrays$ArrayList<E> ですが、こちらは特に add() メソッドについての記述はありません。
ではどこに定義があるのかというと、親クラスの AbstractList<E> に実装があります。
public boolean add(E e) {
add(size(), e);
return true;
}
...省略
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
上記の通り、 AbstractList<E> は単純に UnsupportedOperationException がスローされるようになっており、「add() をサポートしたい子クラスが必ず実装してね」ということになっているようです。
最初に Arrays.asList() で生成した ArrayList は add() を呼ぶと UnsupportedOperationException がスローされる、と書きましたが、まさにここで実装されている処理が実行されているわけですね。
これが、「Arrays.asList() で生成した ArrayList は add() が使えない」の正体です。
new ArrayList<>(Arrays.asList(ids)) と書いたときの処理について
最後に new ArrayList<>(Arrays.asList(ids)) という処理についてです。
java.util.ArrayList のコンストラクタには、以下のように Collection<? extends E> を引数に取るものがあります。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
java.util.Collection は List や Set、 Queue など、すべてのコレクションクラスのインターフェイスです。
add() ができない方の java.util.Arrays$ArrayList も上述の通り (Collection インターフェイスを実装している) AbstractList の子クラスですので、このコンストラクタの引数として渡すことができます。
この中で、渡された Collection を自分の配列にコピーしていますので、 new ArrayList<>(Arrays.asList(ids)) と書けば、最初の ids 配列の要素を持った、 add() が使える java.util.ArrayList のインスタンスを生成することができる、という理屈です。
結局、今まで書いていた通り
String ids[] = new String[]{"a1", "a2", "a3"};
List<String> idList = new ArrayList<>(Arrays.asList(ids));
と書けば良い、という結論なのですが、実際のソースを追いながらどんな理屈になっているのかを探ることでもやもやが一つスッキリし、Java SEの実装を読む練習にもなり、という話でした。
ちなみに、Java SEのソースコードをまだ読んだことがない、という方へ、Java SEのソースコードはJDKの中に含まれる src.zip ファイルを解凍することで読むことができます。ご参考まで。