割と初歩的なことかもしれませんが、なるほどーと思ったのでメモします。
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
ファイルを解凍することで読むことができます。ご参考まで。