co-machi's diary

コワーキングスペース町田(Co-町)の住人2人が、プログラミング周辺に関するブログします(時には脱線も・・・)

ソースを読んでみよう

Javaのクラスなんかで、今ひとつイメージがわかず、どういう場面で使えば良いのか、ということがロジカルにわかっていないことがある場合、普通だったらそれをどう解消しますか?

Google先生に聞くこともあるでしょう。私はソースを読んでみる、という方法を推薦します。もちろん、全部のソースを読む時間がないかもしれませんが、ポイントさえ押さえれば意外に簡単にできます。

通常、JavaSDKをインストールすると、インストールされたディレクトリーのルートに、src.zipがあり、そこにソース全てあります。解凍すれば

などのファイルが出現します。まさにJavaのクラスであるArrayListのソースがそこにあるわけです。

今回はこのArrayListって、どんなクラスなんだろう、という素朴な疑問に対して、ソースからアプローチしてみます。ただし、1.5以降はArrayListにもジェネリックの考え方が入ったので(それはそれで重要ですが)少々複雑になりがちなので、Javaの1.4のソースを見ていきます。

さてさて、まずはクラスの宣言部分にはAbstractListをextendsして、4つのインターフェースをimplementしていますが、インターフェースの後半3つはマーカーインターフェースです。残りのListはCollectionインターフェースをextendsしたもので、addやcontainsなどのメソッドを定義してあります。

しかし、ここではとりあえず、詳細に突っ込むのはやめてスルーしておきましょう(あまり詳細に突っ込まないのは重要です。そうしないと途中でめげてしまいますw)。

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable

さて、次にまずはコンストラクターから見ましょう。

    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    public ArrayList() {
        this(10);
    }
  1. 通常は引数なしでnewすることが多いので、まずは「this(10)」が実行されます。
  2. そしてArrayList(int initialCapacity)が呼ばれます。
  3. super()が呼ばれていますが、AbstractListのコンストラクターは空なので何もしません。
  4. 引数がマイナスだったらIllegalArgumentExceptionがスローされます。
  5. 最後にObject型の配列を10個分生成してthis.elementDataに代入しています。

つまり(ジェネリックがない時代のという但し書きが付きますが)、ArrayListはObject型の配列を扱うもので、最初は10個分しか配列を生成されないんだということがわかります。

次にaddメソッドを見てみましょう。

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

    public boolean add(Object o) {
        ensureCapacity(size + 1);  // Increments modCount!!
        elementData[size++] = o;
        return true;
    }

    public void ensureCapacity(int minCapacity) {
        modCount++;
        int oldCapacity = elementData.length;
        if (minCapacity > oldCapacity) {
            Object oldData[] = elementData;
            int newCapacity = (oldCapacity * 3)/2 + 1;
            if (newCapacity < minCapacity)
                newCapacity = minCapacity;
                elementData = new Object[newCapacity];
                System.arraycopy(oldData, 0, elementData, 0, size);
            }
        }
    }
  1. addメソッドはObject型として引数を受け取ります。
  2. ensureCapacity(size + 1)を実行するわけですが、これは後で見ましょう。
  3. その後、上でみたthis.elementDataに「現在のオブジェクトの数より1少ない」番目、もし、最初だったら0番目の配列に1.のオブジェクトを格納します。
  4. オブジェクトの格納後に、sizeに1を加えます。
  5. そして、何が何でもtrueを返します(falseが返ることはないんですね)。

さて、上記2の「ensureCapacity」を見ていきましょう。

  1. 配列のsizeに1を加えたものをminCapacityとして受け取ります(つまり次addした時の要素数)。
  2. modCountをインクリメントし(modCountはListを構造的に何回修正したかをカウントしているみたい)
  3. 現在のelementDataの要素数(全ての要素に値があるとは限らない)をoldCapacityに代入し
  4. このoldCapacityが次のaddした場合の要素数が大きくなってしまうと、ArrayIndexOutOfBoundsExceptionが出てしまうので
  5. まずは、もともとの配列をoldData待避しておき、
  6. 新たに配列を作成します。そのサイズはoldCapacityの1.5倍に1を加えたもので、最初だったら(最初は10個作ったので)16個の空の配列を作成することになります。
  7. そして、もともとのデータであるoldDataをelementDataにコピーします。

つまり、配列の個数が足らなくなったら、新たに要素数を増やした配列を作成し、もともとあった要素をコピーするということになります。

これだけちょっぴりソースを見ただけで、次のようなことがわかってきます。

  1. ArrayListは(Java1.4時代)Object型の配列を扱う便利なクラス。
  2. 配列は最初にその要素数を決めなくてはならないので、要素数がケースバイケースで変わってしまうような場合は配列は扱いづらい。なので、ArrayListを利用すると自分で上記の「ensureCapacity」のようなメソッドを定義しなくても済んでしまう(楽ちんw)。
  3. 初期値が10個なので、例えば、常に100個以上の要素になることがわかっている場合は、new ArrayList(120)とかやった方がパフォーマンスが良くなると思われます。なぜならば、コンストラクターで引数を省略すると、10個→16個→25個→38個→58個→88個→133個、とArrayList内の配列の要素数がかわっていくわけですが、その度にコピーをしなくてはならないからです。

これ以外に、ArrayListにはイテレータ(AbstractListに定義されている)など色々便利なメソッドがあり、素の配列を扱うよりも便利が実感できますね。

ジェネリックが導入されて、型が違っていてキャストできないというようなバグもなくなりました。

どうですか、ソースを少し読んだだけでも、イメージがつかめませんか?