読者です 読者をやめる 読者になる 読者になる

HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

java.util.ArrayList のインスタンスを使いまわす時はArrayListとして扱うべし

java.util.ArrayList クラスは、いわゆる「可変長配列」を表現するList実装である。
ArrayList (Java 2 Platform SE 5.0)
このクラスも、java.io.ByteArrayOutputStreamやjava.util.HashMapと同様に、内部に配列を持っている。
ByteArrayOutputStreamやHashMapと一つ違うのは、この内部配列の長さを縮めるための手段があること。
ArrayListクラスのtrimToSize()メソッド
但し、注意しなければならないのは、このメソッドはListインタフェースには存在しないということ。Listとして扱ってしまうと、(キャストしない限りは)trimToSize()メソッドを呼ぶことができない、つまり内部配列のサイズを縮めることができないということである。タイトルの「java.util.ArrayListインスタンスを使いまわす時はArrayListとして扱うべし」は、そういうことを言わんとしている。
ついでなので(謎)、同様に検証してみる。
やはり同様に、内部配列を取得するためのメソッドを追加したクラスを作る。

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;

public class ArrayListX<E> extends ArrayList<E> {

    /**
     * <code>serialVersionUID</code> のコメント
     */
    private static final long serialVersionUID = 8201119804011494299L;
    public ArrayListX() {
    }
    public ArrayListX(int initialCapacity) {
        super(initialCapacity);
    }
    public ArrayListX(Collection<E> c) {
        super(c);
    }

    public Object[] getRawBuffer() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        // ArrayListクラスのフィールド'elementData'を無理矢理取得
        Object[] res;
        Field field = this.getClass().getSuperclass().getDeclaredField("elementData");
        boolean oldAccessible = field.isAccessible();
        field.setAccessible(true);
        try {
            res = (Object[]) field.get(this);
        } finally {
            field.setAccessible(oldAccessible);
        }
        return res;
    }
}

検証用のコード。

public class Main {

    /**
     * @param args
     * @throws IllegalAccessException 
     * @throws NoSuchFieldException 
     * @throws IllegalArgumentException 
     * @throws SecurityException 
     */
    public static void main(String[] args) throws SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        System.out.println("java.version = " + System.getProperty("java.version"));
        System.out.println("java.vendor  = " + System.getProperty("java.vendor"));
        System.out.println("os.name      = " + System.getProperty("os.name"));
        dump("initial", null);
        ArrayListX<String> list = new ArrayListX<String>(1024);
        dump("gen array of 1024", list);
        for (int i = 0; i < 5000000; ++i) {
            list.add("hello world");
        }
        dump("add 5M entry", list);
        list.clear();
        dump("clear", list);
        list.add(String.valueOf(0));
        dump("add 1 entry", list);
        list.trimToSize();
        dump("trimToSize", list);
        for (int i = 0; i < 5000000; ++i) {
            list.add("hello world");
        }
        dump("add 5M entry", list);
        list = null;
        dump("set to null", list);
    }

    private static void dump(String label, ArrayListX<String> bout) throws SecurityException, IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        System.out.println("=== " + label + " ===");
        int size = -1;
        int len = -1;
        if (bout != null) {
            size = bout.size();
            len = bout.getRawBuffer().length;
        }
        System.gc();
        System.gc();

        Runtime rt = Runtime.getRuntime();
        long max = rt.maxMemory();
        long total = rt.totalMemory();
        long free = rt.freeMemory();
        long used = total - free;
        System.out.printf("(%10d, %10d) (%10.6f, %10.6f, %10.6f, %10.6f)\n",
                size, len,
                (max / 1024.0 / 1024),
                (total / 1024.0 / 1024),
                (free / 1024.0 / 1024),
                (used / 1024.0 / 1024));
    }
}

そして結果。

java.version = 1.5.0_12
java.vendor  = Sun Microsystems Inc.
os.name      = Windows XP
=== initial ===
(        -1,         -1) ( 63.562500,   1.937500,   1.791595,   0.145905)
=== gen array of 1024 ===
(         0,       1024) ( 63.562500,   1.937500,   1.734856,   0.202644)
=== add 5M entry ===
(   5000000,    5115667) ( 63.562500,  55.417969,  35.704788,  19.713181)
=== clear ===
(         0,    5115667) ( 63.562500,  55.417969,  35.704788,  19.713181)
=== add 1 entry ===
(         1,    5115667) ( 63.562500,  55.417969,  35.704750,  19.713219)
=== trimToSize ===
(         1,          1) ( 63.562500,  50.035156,  49.836655,   0.198502)
=== add 5M entry ===
(   5000001,    5314957) ( 63.562500,  49.671875,  29.198425,  20.473450)
=== set to null ===
(        -1,         -1) ( 63.562500,  44.882813,  44.684364,   0.198448)

結果からも分かるように、ArrayList#trimToSize()メソッドを呼ぶと、内部配列のサイズが現在の要素数にまで縮小される。

但し、これには別の注意点がある。結果を見ると分かると思うが、initialCapacityに1024を指定しているため、内部配列の最初のサイズは1024である。ところが、trimToSize()メソッドを呼ぶと要素数と同じ長さになってしまうため、もはやinitialCapacityで指定した長さはどこかに行ってしまう(まぁ、「初期サイズ」なのだから当たり前といえば当たり前)。何かしらの意図を持ってinitialCapacityの値を指定している場合には注意が必要。