44
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaのListについて

Last updated at Posted at 2015-07-26

#この記事の目的
Javaには便利に使えるコレクションがたくさんあります。ListやSetなんかがその代表だと思いますが、それぞれの実装によって、挿入は早いけどアクセスは遅いなど、いろいろと特徴があると思います。この記事ではJavaの実装を見ながら、そこらへんの基本的なところをおさらいしようと思います。
今回はコレクションでも多分最もよく使うであろうList系、特にArrayListとLinkedListに絞って調べていきます。
ソースコードは
http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/ArrayList.java#ArrayList
および
http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/LinkedList.java#LinkedList.Node
から引用してます。

#ArrayListとLinkedListの違い
中でのデータの持ち方が違います。
ArrayListは内部にObjectの配列を持っていて、要素はそこに格納されます。LinkedListは要素をNodeに格納し、それを連結することで順序付きのコレクションを実現しています。

ArrayListは

ArrayList.java(134行目あたり)
 transient Object[] elementData; // non-private to simplify nested class access

っていうのが要素の実態で、この配列に対して

ArrayList.java(457行目あたり)
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
ArrayList.java(428行目あたり)
public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

こんな感じでaddしたりgetしたりしてます。
add内で呼ばれているensureCapacityInternalで格納している配列のサイズをチェックして、領域を超えるようだったら更にgrowメソッドを呼び出して、配列を拡張しています。デフォルトでは既存のサイズの1.5倍になるみたいです。

一方、LinkedListは内部クラスでこういうのを持ってます。

LinkedList.java(970行目あたり)
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

このノードは次のノードへの参照と前のノードへの参照を保持してます。
で、LinkedList自体はNodeのfirstとlastを保持してます。
こういう感じで頭、もしくはおしりの方から目的のindexまでノードをたって行く形ですね。

LinkedList.java(566行目あたり)
 Node<E> node(int index) {
// assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

こういう特性を持っているので、一般的にアクセス時間はArrayListの方が圧倒的に早いです。ここらへんはArrayListがマーカーインタフェースでRandomAccessを持ってるあたり、明示的ですね。
ただし、ArrayListはリストの途中に要素を挿入する場合、配列をSystem.arraycopyを用いて配列を複製します。なので、そういう時はLinkedListを使いましょう。

#for, 拡張for, foreachなど
Listの要素をすべてたどるのによくforとか使うけど、書き方がいろいろありますよね。ざっと見たところ、下の4つに分けられると思います。

1: for文

for(int i=0; i<list.size(); i++) {
    list.get(i);
}

2: Iterator

Iterator itr = list.iterator();
while(itr.hasNext()){
    itr.next();
}

3: 拡張for文

for(Object o : list) {
}

4: foreach (Java8から)

list.forEach(o -> { });

今回はこの4つの実装、パフォーマンスについても調べました。
1については今更言う必要ないと思います。
2のIteratorの実装ですが、大体はIteratorをimplementsした内部クラスを持っていて、その中で現在位置を保持するcursorを管理してるみたいです。特に、LinkedListの場合はgetで要素にアクセスしようとした場合、リストの先頭(もしくは最後尾)から探索を開始するので、このIteratorを使ったアクセスのほうが圧倒的に早いです。
3のいわゆる拡張for文ですが、]この記事によると、これはこんな風に動作するらしいです。

for(Object o : list) {
    System.out.println(o.toString());
}

これが

Object o;
for(Iterator it = list.iterator(); it.hasNext(); System.out.println(o.toString()) {
    o = it.next();
}

こうなる。
つまり、内部的にIteratorを呼び出してくれています。拡張for文を使うとIteratorを使った呼び出しが簡単に書けるようになります。

4のforEach。これはjava8から追加された書式です。引数にConsumerを受け取って、各要素に適用していきます。
このforEachですが、そもそもはIterableに実装されてます。

Iterable.java(72行目くらい)
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

つまり、デフォルトでは普通に拡張for文使ってますね。
ちなみに、ArrayListではこのforEachがOverrideされて、以下のようになってます。

ArrayList.java(1241行目くらい)
    @Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

並列アクセスの例外処理系を除くと、1の普通のfor文で要素にアクセスする方法をとってることがわかります。ArrayListはRandomAccessなので、Iteratorを使って要素にアクセスするよりも普通にgetを使ったほうがアクセスが早いので、こういった実装になってるんだと思います。

最後に、せっかくなのでこの4つの方法についてパフォーマンスの計測をしました。対象はJava8のLinkedListとArrayList。検証環境はMacBook Air(11inch) 1.7GHz Core i7 / 8GB RAMです。長さを1000に固定したリストについて、それぞれ全要素へのアクセスを30000回繰り返し、その速度を測定しています。
結果は以下のとおりです。

LinkedList ArrayList
for 15020ms 25ms
Iterator 115ms 27ms
拡張for 121ms 22ms
forEach 142ms 101ms

概ね想定通りの値に落ち着いてると思います。Iteratorと拡張forの値がずれてるのが気になりますが、ここらへんは誤差でしょうか??forEachについてはLambdaを呼び出すオーバーヘッドと捉えていいんでしょうか。こういった値になりました。

##コメントを頂いたので追記。
確かにこれだとLinkedList使えねー、てなりますね。でもArrayListよりもLinkedListが有利なケースはあります。それがリストの途中に要素を挿入するケースですね。
というわけで、リストの途中に要素を挿入し、それを30000回繰り返してパフォーマンスも計測してみました。
結果は以下のとおりです。

LinkedList ArrayList
7ms 88ms
というわけで、圧倒的にLinkedListの方が早いという結果になりました。

#まとめ
ArrayListとLinkedList、Javaで最も使う2つのリストについて、基本的な特性を調べました。ついでに、for, 拡張forなどのイテレーションの方法についてもパフォーマンスの計測などを行って、その特性を調べました。

指摘、コメントなどなどありましたら、ぜひおねがいします!

44
39
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?