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

コレクション (List, Set, Map 等) の生成の簡略化

Java SE 7 ではなんかコレクション用のリテラルが入るとか入らないとかあるみたいだけど、個人的には別にそこまで欲しくはない (もし導入されればおいしくいただきますよ?)。
マップリテラルは欲しい。いや、やっぱりそこまで欲しくない (もし導入されれば以下略)。
以下その理由 (一部妄想アリ)。

List および Set リテラル風のメソッド

例えば、こんな感じのクラスを用意して、

public final class CollectionLiterals {
    private CollectionLiterals() {}
    
    private static void throwExceptionIfNull(String name, Object objs) {
        assert name != null : "name is null.";
        if (objs == null)
            // 独自の例外クラスなんで気にしないで><
            // NullPointerExceptionとかIllegalArgumentExceptionに置き換えればいいと思うよ!
            throw ArgumentException.unexpectedNull().butArgIsNull(name);
    }
    
    public static <E> ArrayList<E> arrayList(E...objs) {
        throwExceptionIfNull("objs", objs);
        
        return new ArrayList<E>(Arrays.asList(objs));
    }
    
    public static <E> ArrayList<E> arrayList2(int capacity, E...objs) {
        throwExceptionIfNull("objs", objs);
        
        ArrayList<E> result =
            new ArrayList<E>(capacity < objs.length ? objs.length : capacity);
        result.addAll(Arrays.asList(objs));
        return result;
    }
    
    public static <E> LinkedList<E> linkedList(E...objs) {
        throwExceptionIfNull("objs", objs);
        
        return new LinkedList<E>(Arrays.asList(objs));
    }
    
    // Comparatorを指定しない場合、Comparableを実装しているという制約を付ける
    public static <E extends Comparable<E>> TreeSet<E> treeSet(E...objs) {
        throwExceptionIfNull("objs", objs);
        
        return new TreeSet<E>(Arrays.asList(objs));
    }
    
    public static <E> TreeSet<E> treeSet2(Comparator<E> cmp, E...objs) {
        throwExceptionIfNull("cmp", cmp);
        throwExceptionIfNull("objs", objs);
        
        TreeSet<E> result = new TreeSet<E>(cmp);
        result.addAll(Arrays.asList(objs));
        return result;
    }
    
    // 略
}

あとは使う側でこのクラスのメソッドを static import すれば、こんな感じに使える。

List<Integer> l = arrayList(1, 2, 3, 4, 5);

Java にはいろいろな List や Set の実装があるので、単純な list という名前を使用していない。
かわりに、CollectionLiterals の内部クラスとして、

public static final class ArrayListLiterals {
    private ArrayListLiterals() {}
    
    public static <E> ArrayList<E> list(E...objs) {
        throwExceptionIfNull("objs", objs);
        
        return new ArrayList<E>(Arrays.asList(objs));
    }
    
    public static <E> ArrayList<E> listWithCapacity(int capacity, E...objs) {
        throwExceptionIfNull("objs", objs);
        
        ArrayList<E> result =
            new ArrayList<E>(capacity < objs.length ? objs.length : capacity);
        result.addAll(Arrays.asList(objs));
        return result;
    }
}

public static final class LinkedListLiterals {
    // 略
}

// 後述のMap系のメソッドも入るので、TreeSetLiteralsではなく、TreeCollectionLiterals
public static final class TreeCollectionLiterals {
    // 略
}

を用意することで、これらを static import すれば list という名前が使用できるようにできる。
具体的なコレクションの種類を含むクラスのメソッドを static import する以上、自分がどのコレクションを使いたいかは明示していると見なせるし、CollectionLiterals のメソッドを static import する場合は、メソッド名によりどのコレクションが使いたいか明示している。


ちなみに、arrayList でオーバーロードせずに、arrayList2 やら listWithCapacity やらという名前を使っている理由は、オーバーロードするとやっかいな問題があるから。
例えば、もし arrayList2 を arrayList としていた場合、

ArrayList<String> ss = arrayList(10, "hoge");   // arrayList(int capacity, E...objs)
ArrayList<Integer> is1 = arrayList();   // arrayList(E...objs)
ArrayList<Integer> is2 = arrayList(10); // error
ArrayList<Integer> is3 = arrayList(10, new Integer[0]); // ...
ArrayList<Integer> is4 = arrayList(new Integer[] { 10 });   // ...

is3 や is4 のような記述が必要になるため、全然簡略化にならない。

Queue とか Deque とか

List とか Set と同じようにやればいいさ。

Stack

Stack・・・?あぁ、そんなクラスもありましたねぇ・・・
Stack は使わずに、ArrayDeque を使えばいいと思うよ!

このクラスは通常、スタックとして使われるときは Stack よりも高速で、キューとして使われるときは LinkedList よりも高速です。

Oracle Technology Network for Java Developers

Map は・・・

Map はどうしても微妙になってしまう。

static import したメソッドからのメソッドチェインで
Map<String, Integer> map = hashMap(key("hoge").val(10).key("piyo").val(20));

この方法の欠点は、これを実現するためのコードが長くなってしまう、と言うこと。
あと、そこまでしてメソッドチェインしたいか?というのも謎。


これを実現するためのコードは例えばこんな感じ。

public static <K, V> HashMap<K, V> hashMap(MapBuilder<K, V> builder) {
    throwExceptionIfNull("builder", builder);
    
    // builderのbuildメソッドに対してMapオブジェクトを渡すだけ
    return builder.build(new HashMap<K, V>());
}

// 入り口
public static <K> FirstKey<K> key(K key) {
    return new FirstKey<K>(key);
}

// クラス自身は型パラメータが1つだが、
public static final class FirstKey<K> {
    final K key;
    FirstKey(final K key) {
        this.key = key;
    }

    // メソッドで更にもう一つの型パラメータを追加する
    public <V> Value<K, V> val(V value) {
        List<KeyValue<K, V>> kvs = new ArrayList<KeyValue<K, V>>();
        kvs.add(new KeyValue<K, V>(key, value));
        return new ValueImpl<K, V>(kvs);
    }
}

public static interface Key<K, V> {
    Value<K, V> val(V val);
}

// MapBuilderはpackage privateにしておいて、buildメソッドを外部から隠しておく
// あるいみマーカーインターフェイス
static interface MapBuilder<K, V> {
    <M extends Map<K, V>> M build(M map);
}

// 外部から見えるのはこのインターフェイスが要求するkeyメソッドのみ
public static interface Value<K, V> extends MapBuilder<K, V> {
    Key<K, V> key(K key);
}

static final class KeyValue<K, V> {
    final K key;
    final V value;
    KeyValue(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

static final class KeyImpl<K, V> implements Key<K, V> {
    final K key;
    final List<KeyValue<K, V>> kvs;

    KeyImpl(K key, List<KeyValue<K, V>> kvs) {
        this.key = key;
        this.kvs = kvs;
    }

    public Value<K, V> val(V value) {
        kvs.add(new KeyValue<K, V>(key, value));
        return new ValueImpl<K, V>(kvs);
    }
}

static final class ValueImpl<K, V> implements Value<K, V> {
    final List<KeyValue<K, V>> kvs;

    public ValueImpl(List<KeyValue<K, V>> kvs) {
        this.kvs = kvs;
    }

    public Key<K, V> key(K key) {
        return new KeyImpl<K, V>(key, kvs);
    }
    // すべてを渡されたオブジェクトに対してputしていく
    public <M extends Map<K, V>> M build(M map) {
        assert map != null;
        for (KeyValue<K, V> kv : kvs) {
            map.put(kv.key, kv.value);
        }
        return map;
    }

}
static import したメソッドからのメソッドチェイン + 可変長引数で
Map<String, Integer> map = hashMap(key("hoge").val(10), key("hoge").val(20));

そこまでメソッドチェインにこだわる必要があるのか?と思わなくもない。
下の方法とあんまり変わらないので説明は省略で。
もちろん、上の方法よりも実装は楽 (試してないけど)。

static importしたメソッド + 可変長引数
Map<String, Integer> map = hashMap(kv("hoge", 10), kv("piyo", 20));

やはりというかなんというか、この辺に落ち着くのかな。
Map.Entry の実装は面倒だけど、仕組みとしては超単純。
ただ、警告が出ちゃうのがちょっとアレ。


以下実装例。

private static <M extends Map<K, V>, K, V> M buildMap(M map, Entry<K, V>...es) {
    assert map != null && es != null;

    for (Entry<K, V> e : es) {
        map.put(e.getKey(), e.getValue());
    }
    return map;
}

public static <K, V> HashMap<K, V> hashMap(Entry<K, V>...es) {
    throwExceptionIfNull("es", es);
    
    return buildMap(new HashMap<K, V>(), es);
}

public static <K, V> Entry<K, V> kv(K key, V value) {
    return new KeyValue<K, V>(key, value);
}

static final class KeyValue<K, V> implements Entry<K, V> {
    final K key;
    final V value;

    KeyValue(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
    public V setValue(V value) { throw new UnsupportedOperationException(); }

    @SuppressWarnings("unchecked")
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof Entry))
            return false;
        Entry other = (Entry) obj;
        if (key == null) {
            if (other.getKey() != null)
                return false;
        } else if (!key.equals(other.getKey()))
            return false;
        if (value == null) {
            if (other.getValue() != null)
                return false;
        } else if (!value.equals(other.getValue()))
            return false;
        return true;
    }

    @Override
    public int hashCode() {
        return (getKey() == null ? 0 : getKey().hashCode()) ^
                (getValue() == null ? 0 : getValue().hashCode());
    }
}

ということで

Map 以外のコレクションは別にリテラルとか要らないんだよねぇ・・・
下手にリテラルを導入すると、どの型をデフォルトで使うのか、って問題も出てくるし・・・


ただ、やっぱり Map はもっと簡単に作りたいから、Map のリテラルは・・・欲しい?
うーん、それよりも、Pair というか Tuple というか cons セルがあれば・・・
以下妄想。

// このかわりに
Entry<String, Integer> e1 = new KeyValue<String, Integer>("hoge", 10);
// こう書けるとか
(String:Integer) e2 = "hoge":10;

こんなのがあれば、これと可変長引数で、

Map<String, Integer> map = hashMap("hoge":10, "piyo":20);

こう書ければ Map リテラルも要らないんだけど・・・ね。

あわせて読んでほしい

[http
//nicolas.lehuen.com/index.php/post/2006/10/07/113-java-map-literals-continued:title]:クラス名の HogeHogeLiterals ってのはここから。
[http
//web.archive.org/web/20070330220207/Mac OS X Server]:知っている限りでもっとも古い、可変長引数を使ったコレクションの生成。Map.Entry は使っていない。Web Archive から。
[http
//d.hatena.ne.jp/odz/20061025/1161837641:title]:Map.Entry を使った感じで。
[http
//d.hatena.ne.jp/bleis-tift/20070731/1185812617:title]:何番煎じなんだ、ってエントリ。戻り値の型としてより具体的な型を使っているのは新しい・・・と、信じたい。ちなみに、上記 3 つのエントリは、このエントリにもらったトラックバックで知りました。


この前のエントリのコメント欄で書くって言っちゃったんで、書きました。