コレクションフレームワーク 第3回 実践的な使い方

前回では、コレクションフレームワークに関係のあるクラスのうち、具体的な実装クラスについて紹介しました。今回はさらに踏み込んで、コレクションフレームワークと密接な関係のある、各要素にアクセスすためのクラスやソートするためのクラスなど、非常に便利なクラス群を紹介します。

Iterator

Collectionの中に含まれる各要素にアクセスする場合、java.util.Iteratorを使用することになります。

Collectionの中身すべてにアクセス

java.util.Collectionインターフェースには次のようなメソッドがあります。

java.util.Iterator iterator()
コレクションの要素の反復子を取得する。

このメソッドを使用してIteratorを取得することにより、簡単にCollectionの各要素にアクセスできます。

java.util.Iteratorが持つメソッドのうち、特に重要な2つを挙げます。

boolean hasNext()
次の要素が存在するか検査する。
Object next()
次の要素を取得する。

この2つのメソッドを使用すると、配列の各要素にアクセスするのと同様に、Collectionの各要素にアクセス可能です。

import java.util.Collection;
import java.util.Iterator;
import java.util.HashSet;

public class Iteration {
  public static void main(String[] args) {
    Collection c = new HashSet();
    c.add("hoge");
    c.add("foo");
    c.add("bar");
    
    for (Iterator i = c.iterator(); i.hasNext();) {
      String element = (String) i.next();
      System.out.println(element);
    }
  }
}
> java Iteration
foo
bar
hoge

Mapの中身全てにアクセス

MapはCollectionではないため、先ほどと同様の方法ではイテレーションできません。
そこで、entrySet()メソッドを使用して、Collectionに変換してから行います。

import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;

public class MapIteration {
  public static void main(String[] args) {
    Map m = new HashMap();
    m.put("hoge", "123");
    m.put("foo", "456");
    m.put("bar", "789");
    
    for (Iterator i = m.entrySet().iterator(); i.hasNext();) {
      Map.Entry entry = (Map.Entry) i.next();
      String key = (String) entry.getKey();
      String value = (String) entry.getValue();
      System.out.println(key + " => " + value);
    }
  }
}
> java MapIteration
foo => 456
bar => 789
hoge => 123

Collectionのソート

SortedCollection

Listのソート

Listに値を格納していくと、最終的な順序は格納した順番と同じものになります。
この中身をソートしたい場面があるかもしれません。

ソートは速いアルゴリズムを書くのが比較的面倒で、バグを生み出しやすくなります。ここでは、java.util.Collections.sort(List)を使用したソートを紹介します。

import java.util.List;
import java.util.Arrays;
import java.util.Collections;

public class SortList {
  public static void main(String[] args) {
    List l = Arrays.asList(new String[]{
      "hoge", "foo", "bar",
      "hogehoge", "foofoo", "barbar",
      "1", "2", "3",
      "100", "200"
    });
    
    System.out.println(l.toString());
    
    Collections.sort(l);
    System.out.println(l.toString());
  }
}
> java SortList
[hoge, foo, bar, hogehoge, foofoo, barbar, 1, 2, 3, 100, 200]
[1, 100, 2, 200, 3, bar, barbar, foo, foofoo, hoge, hogehoge]

Set/Mapのソート

java.util.HashSetやjava.util.HashMapを使用すると、値またはキーの順序はランダムになります。
これらをソートする一番簡単な方法は、java.util.TreeSetやjava.util.TreeMapを使用するというものです。

import java.util.Set;
import java.util.HashSet;
import java.util.TreeSet;

public class SortSet {
  public static void main(String[] args) {
    Set s = new HashSet();
    s.add("hoge");
    s.add("foo");
    s.add("bar");
    s.add("hogehoge");
    s.add("foofoo");
    s.add("barbar");
    
    System.out.println(s.toString());
    
    s = new TreeSet(s);
    System.out.println(s.toString());
  }
}
> java SortSet
[foo, barbar, bar, hoge, hogehoge, foofoo]
[bar, barbar, foo, foofoo, hoge, hogehoge]

Comparetor

次のプログラムを見てください。

import java.util.List;
import java.util.Arrays;
import java.util.Collections;

public class LexicographicSort {
  public static void main(String[] args) {
    List l = Arrays.asList(new String[]{
      "100", "200",
      "10", "20", "30",
      "1", "2", "3"
    });
    
    Collections.sort(l);
    System.out.println(l.toString());
  }
}
> java LexicographicSort
[1, 10, 100, 2, 20, 200, 3, 30]

結果を見ても分かるように、数値も辞書式配列でソートされるために正しくソートされません。

そこで、java.util.Comparatorというインターフェースを使用して、比較の方法を定義します。

import java.util.Comparator;
import java.util.List;
import java.util.Arrays;
import java.util.Collections;

public class NumericSort {
  public static void main(String[] args) {
    List l = Arrays.asList(new String[]{
      "100", "200",
      "10", "20", "30",
      "1", "2", "3"
    });
    
    Collections.sort(l, new NumericComparator());
    System.out.println(l.toString());
  }
}

class NumericComparator implements Comparator {
  public int compare(Object o1, Object o2) {
    String s1 = (String) o1;
    String s2 = (String) o2;
    return Double.valueOf(s1).compareTo(Double.valueOf(s2));
  }
}
> java NumericSort
[1, 2, 3, 10, 20, 30, 100, 200]

ここで作成したNumericComparatorクラスは、TreeSetやTreeMapのコンストラクタに引数に取ることによって、各要素の順序付けを決めるルールにも使用することができます。

特殊なCollection

Collectionフレームワークに含まれるクラスは、基本的に変更可能でスレッドアンセーフです。
変更不可能なコレクションや、スレッドセーフなコレクションを作成するには、自分でわざわざクラスを作成するまでもなく、簡単な方法で実現できます。

変更不可能なCollection

メソッドからコレクションを返す際に、防御的コピーを毎回とったり、配列に変換したりするのは面倒です。その場合に、変更不可能なコレクションとしてメソッドから返すという方法が考えられます。
そこで、java.util.CollectionsクラスのunmodifiableCollection(Collection)というメソッドを使用すると、現在使用しているCollectionを変更不可能にすることができます。

import java.util.Collections;
import java.util.Collection;
import java.util.ArrayList;

public class Unmodifiable {
  public static void main(String[] args) {
    // modifiable collection
    Collection c = new ArrayList();
    c.add("hoge");
    System.out.println(c);
    c.add("foo");
    System.out.println(c);
    c.add("bar");
    System.out.println(c);
    
    // *** make it unmodifiable ***
    c = Collections.unmodifiableCollection(c);
    
    // ERROR !
    c.add("moge");
    System.out.println(c);
  }
}
> java Unmodifiable
[hoge]
[hoge, foo]
[hoge, foo, bar]
Exception in thread "main" java.lang.UnsupportedOperationException
        at java.util.Collections$UnmodifiableCollection.add(Unknown Source)
        at Unmodifiable.main(Unmodifiable.java:20)

mogeが追加される前に例外が発生していることを確認できます。

このほかにもjava.util.Collectionsには以下のようなメソッドがあります。

スレッドセーフなCollection

次のプログラムを実行した結果を見てみます。

import java.util.List;
import java.util.LinkedList;

public class Loop extends Thread {

  private final List list;
  private static int total = 0;

  private Loop(List list) {
    this.list = list;
  }

  public void run() {
    int count = 0;
    
    // consume
    while (list.remove(null)) {
      count++;
    }
    
    System.out.println("consume " + count + " elements");
    
    synchronized (this.getClass()) {
      total += count;
    }
  }

  public static void main(String[] args) throws InterruptedException {
    // *** thread UNSAFE list ***
    List l = new LinkedList();

    // prepare 100,000 elements
    for (int i = 0; i < 100000; i++)
      l.add(null);

    Thread[] loops = new Thread[] {
       new Loop(l), new Loop(l),
       new Loop(l), new Loop(l),
       new Loop(l)
    };

    // start 5 threads
    for (int i = 0; i < loops.length; i++)
      loops[i].start();

    // wait
    for (int i = 0; i < loops.length; i++)
      loops[i].join();
    
    System.out.println("total " + total + " elements");
  }
}
> java Loop
consume 9201 elements
consume 66894 elements
consume 100000 elements
consume 98501 elements
consume 77424 elements
total 352020 elements

要素は100,000個しか用意しなかったにもかかわらず、352,020個の要素を取り出しています。
これは、コレクションが基本的にスレッドセーフでないために発生する問題です。

そこで、java.util.CollectionsクラスのsynchronizedList(List)というメソッドを使用すると、現在使用しているListをスレッドセーフにすることができます。

先ほどのプログラムのうち、mainメソッドの先頭 - Listを生成する部分で、使用しているListをスレッドセーフなものに変えてみます。

import java.util.Collections;
import java.util.List;
import java.util.LinkedList;

public class SynchronizedLoop extends Thread {

  private final List list;
  private static int total = 0;

  private SynchronizedLoop(List list) {
    this.list = list;
  }

  public void run() {
    int count = 0;

    // consume
    while (list.remove(null)) {
      count++;
    }

    System.out.println("consume " + count + " elements");

    synchronized (this.getClass()) {
      total += count;
    }
  }

  public static void main(String[] args) throws InterruptedException {
    // *** thread SAFE list ***
    List l = Collections.synchronizedList(new LinkedList());

    // prepare 100,000 elements
    for (int i = 0; i < 100000; i++)
      l.add(null);

    Thread[] loops =
      new Thread[] {
        new SynchronizedLoop(l),
        new SynchronizedLoop(l),
        new SynchronizedLoop(l),
        new SynchronizedLoop(l),
        new SynchronizedLoop(l)};

    // start 5 threads
    for (int i = 0; i < loops.length; i++)
      loops[i].start();

    // wait
    for (int i = 0; i < loops.length; i++)
      loops[i].join();

    System.out.println("total " + total + " elements");
  }
}
consume 19691 elements
consume 19702 elements
consume 21211 elements
consume 19698 elements
consume 19698 elements
total 100000 elements

用意した要素の数と、取り出した要素の数が等しくなりました。

このほかにもjava.util.Collectionsには以下のようなメソッドがあります。