ArrayListの初期値は必要なのかどうか、性能を踏まえて検証


JtestというJava用の静的解析ツールを実行すると「ArrayListの初期値が設定されてないけど大丈夫?」と言われます。

言ってることは分かります。メモリの確保は時間がかかる処理だし、ArrayListはサイズによって少しずつメモリ上のサイズが増えるもの。なので、初期値設定して初めにがばっとメモリ上に領域を確保しておけば効率化できるのでしょう。

けど、実際初期値ありなしでどれくらい違うのか分からないので試してみました。まずは結論から。

結論

  1. ほとんどの場合、初期値は不要
  2. リストへの追加が非常に多く(例えば1000万個以上追加)、追加処理が処理の大半を占める場合は必要
  3. 実行環境が貧弱(組込)な場合や性能が重視される場合は必要

検証環境

  • Intel Core i7
  • Windows 7
  • JavaSE 1.7

検証

以下のようなコードを用意して実行します。

メソッドで初期値の有無を切り替えて、それら2つの処理時間を出力します。SIZEはまずは1000で。自分の今までの経験だと10000以上追加するようなケースは余りありませんでした。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Main {

  private static int SIZE = 1000;

  public static void main(String[] args) {

    long start;
    long end;

    start = System.nanoTime();
    defaultOff();
    end = System.nanoTime();
    System.out.println("defaultOff : " + ((end - start) / 1000000.0) + " ms");

    start = System.nanoTime();
    defaultOn();
    end = System.nanoTime();
    System.out.println("defaultOn  : " + ((end - start) / 1000000.0) + " ms");

  }

  private static void defaultOff() {
    List<Integer> list = new ArrayList<>();
    insert(list);
  }

  private static void defaultOn() {
    List<Integer> list = new ArrayList<>(SIZE);
    insert(list);
  }

  private static void insert(List<Integer> list) {
    Random r = new Random();
    for (int i = 0; i < SIZE; i++) {
      list.add(r.nextInt());
    }

  }
}

実行結果は以下のような感じ。

defaultOff : 1.890064 ms defaultOn  : 0.740626 ms

これだけ見るとデフォルト設定した方が性能2.5倍で凄い!と思いますが、残念ながらこのプログラムは非常に単純なことしかしていません。重そうな処理は、乱数生成(nextInt)、ボクシング、addだけ。これら2つのメソッドは全く同じことしかしていないので、差があるとしたらArrayListの拡張処理の有無のみです。

forループの後に1000ミリ秒かかる処理があったとします。その場合、上記の実行結果は以下になりました。

defaultOff : 1001.890064 ms defaultOn  : 1000.740626 ms

ArrayListの初期値はほとんど意味がない・・!

そんな訳で、結論1は「ほとんど初期値不要」です。

上記はSIZEが1000でしたが、大量のデータを扱う場合もあるかと思います。自分は今まで余りありませんでしたが、SIZEが10000000(1000万)の場合も検証します。

変更点は以下。

  private static int SIZE = 10000000;

実行結果は以下のようになりました。

defaultOff : 3621.897023 ms defaultOn  : 1436.368276 ms

この場合、仮にforループの後に1000msの処理があったとしても4.6秒と2.4秒で差は大きいです。 ※処理が1000msではなく1000sだったら、結局誤差になってしまいますが・・・

そんな訳で結論2は「リストへの追加が多く、追加処理が大半の場合は初期値必要」です。

結論3は検証環境がなくて、実行できなかったがまあ間違ってはいないと思う。組込系で働いている先輩曰く組込でもJavaはあるらしいし、Androidなんかもそうだし、そういうシーンでは気を付けなければいけないでしょう。

念のためもう1つ検証

ArrayListの初期値省略の場合のデフォルトの長さは10であることを利用します。初期値15のバージョンと初期値なしバージョンのリストに15個の要素を追加する、ということを何度も繰り返します。つまりデフォルトの方は初期値15のバージョンより、リストの伸長処理が1回だけ多くなります。 ※伸長処理で新たに5個分領域確保される

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Main2 {

  private static int ROOP = 1000;
  private static int SIZE = 15;

  public static void main(String[] args) {

    long start;
    long end;

    start = System.nanoTime();
    default10();
    end = System.nanoTime();
    System.out.println("default10 : " + ((end - start) / 1000000.0) + " ms");

    start = System.nanoTime();
    default11();
    end = System.nanoTime();
    System.out.println("default15  : " + ((end - start) / 1000000.0) + " ms");

  }

  private static void default10() {
    for (int i = 0; i < ROOP; i++) {
      List<Integer> list = new ArrayList<>();
      insert(list);
    }
  }

  private static void default11() {
    for (int i = 0; i < ROOP; i++) {
      List<Integer> list = new ArrayList<>(SIZE);
      insert(list);
    }
  }

  private static void insert(List<Integer> list) {
    Random r = new Random();
    for (int i = 0; i < SIZE; i++) {
      list.add(r.nextInt());
    }
  }
}

default10 : 17.488683 ms default15  : 4.336698 ms

差が出る。

予めリストのサイズが分かってるなら初期値設定してもいいかもしれないです。