[Java How To Programming] [Home on 246net] [Home on Alles net]
Powered by SmartDoc

コレクションを扱うプログラミングTips

TAKAHASHI, Toru

目次

コレクションを利用するTips

イテレーション

Iteratorを使う

コレクションの中に格納された要素にアクセスする時は、Iteratorを使用することが通例です。その時のコーディングパターンで最も多い書き方は次のようにwhile文を使用するものです。

Iteratorをwhile文で使う
List persons;
    :
Iterator it = persons.iterator();
while (it.hasNext()) {
    Person person = (Person)it.next();
    // do something for person.
}

しかし、while文を使うとイテレータを示すローカル変数itの有効範囲がメソッド全体に渡ってしまいます。本来は、while文の内側に閉じて欲しいところです。また、複数のコレクションを操作する時に、ローカル変数名itを再度使いたいのですが、2度目は変数を宣言しないようにする必要が生じます。

そこで、while文ではなくfor文を使ってIteratorを使用します。

なお、このイディオムの出典はEffective Javaです。

Iteratorをfor文で使う
List persons;
    :
for (Iterator it = persons.iterator(); it.hasNext(); ) {
    Person person = (Person)it.next();
    // do something for person.
}

GenericsなIteratorを使う

Java2 5.0(Tiger)から導入されたジェネリクス機能を用いると、キャストが不要になります。

for文とGenericsの組み合わせ
List<Person> persons;
    :
for (Iterator<Person> it = persons.iterator(); it.hasNext();) {
    Person person = it.next();
    // do something for each person
}

拡張for(foreach)を使う

Java2 5.0(Tiger)から導入された拡張for文を使用すると、コレクションのイテレーションを簡潔に記述できます。

J2SE1.5から導入された拡張for文
List persons;
    :
for (Object obj : persons) {
    Person person = (Person)obj;
    // do something for each person
}

また、Genericsと組み合わせると、キャストが不要になるのでもっと簡潔になります。

拡張for文とGenericsの組み合わせ
List<Person> persons;
    :
for (Person person : persons) {
    // do something for each person
}

クロージャ的なアクセス(Java SE 7以前)

Iteratorを使ってコレクションの要素にアクセスする方法はいくぶん手続き的です。コレクションの要素にアクセスを必要とするクライアントの全ての場所に似たコードが散在することになります。これは、コードの一元化を阻害します。"Once and only once"

そこで、クロージャ的にイテレーション処理をコレクション側に一元化する方法を使います。ただし、Java SE 7までのCollection APIにはこのような実装は提供されていません。そこで、別途拡張することになります。クロージャ的な処理は、Javaではinterfaceと匿名クラスを組合せて実現します。ただし、Javaの内部クラスは内部クラスが定義されたメソッドスコープのローカル変数を限定的にしか参照できないので、クロージャと言うのは正しくありません。そこで、「クロージャ的」という表現をこの文書では使っています。

非ジェネリクスなコード

まずは、クロージャ的な処理を記述するためのinterface Blockを定義します。これは、コレクションの要素一つ毎に呼び出されるメソッドexecを宣言しています。

Blockインタフェースの定義
public interface Block {
    public void exec(Object each);
}

Javaのコレクション実現クラス(ここではArrayList)を拡張して、クロージャ的な処理を持つオブジェクトを受け取るメソッドforEachDoを定義します。このメソッドは、コレクションの要素一つ毎に引数で指定されたクロージャ的オブジェクトのメソッドexecを呼び出します。

クロージャ的対応コレクション
import java.util.ArrayList;
import java.util.Iterator;
public class OrderedCollection extends ArrayList {
    public void forEachDo(Block block) {
        for (Iterator it=iterator(); it.hasNext(); ) {
	    block.exec(it.next());
	}
    }
}

このクロージャ的対応コレクションOrderedCollectionを使う例を見てみましょう。

クロージャ的対応コレクションの利用
OrderedCollection fighters = ...
OrderedCollection bombers = ...
   :
Block eachPrint = new Block() {
    public void exec(Object each) {
        System.out.println(each);
    }
};

fighters.forEachDo(eachPrint);
bombers.forEachDo(eachPrint);

複数のコレクションに対して、各要素を表示するBlockクロージャを1度定義して、それを共通で使用しています。また、コレクションを利用するコード側ではイテレーション処理を記述せずに済みます。

ここでは、クロージャ的なものを汎用的に定義できるよう、Blockインタフェースのexecメソッドは引数の型をObjectにしています。型に安全なコレクションを導入するときは、そのコレクション専用のクロージャインタフェースを定義することもできます。例えば、FlightCollectionというFlight型専用の型に安全なコレクションを定義した場合、execメソッドの引数をFlight型にします。なお、その場合BlockインタフェースはFlightCollectionのインナークラスとした方がよいでしょう。

ジェネリクスなコード

ジェネリクス版Blockインタフェースの定義
public interface Block<E> {
    void exec(E each);
}
ジェネリクス版クロージャ対応コレクションの定義
import java.util.ArrayList;

public class OrderedCollection<E> extends ArrayList<E> {
    public void forEachDo(Block<E> block) {
        for (E each : this) {
            block.exec(each);
        }
    }
}
ジェネリクス版クロージャ対応コレクションの利用
public class Main {

    public static final void main(final String[] args) {
        OrderedCollection<String> fighters = new OrderedCollection<String>();
        fighters.add("Starfighter");
        fighters.add("Tomcat");
        fighters.add("Eagle");
        fighters.add("Phantom");
        fighters.add("Sabre");

        OrderedCollection<String> bombers = new OrderedCollection<String>();
        bombers.add("Lancer");
        bombers.add("Stratofortress");
        bombers.add("Spirit");

        Block<String> eachPrint = new Block<String>() {
            public void exec(String each) {
                System.out.println(each);
            }
        };

        fighters.forEachDo(eachPrint);
        bombers.forEachDo(eachPrint);
    }
}

このイディオムの出典はessential Java Styleです。

Java SE 8のラムダ式およびコレクション操作メソッド

Java SE 8では、ラムダ式およびラムダ式の型(関数インタフェース)を引数にとるコレクションのメソッドを使ってクロージャ的にイテレーション処理をコレクション側に委ねることが標準でできるようになりました。

クロージャ的な処理を記述するためのいくつものinterfaceが標準で提供され、それらはアノテーション@functionalInterfaceで修飾されています。また、Collection、Listが実装するインタフェースjava.lang.IterableにforEachメソッドが追加され、主要なコレクションに対してforEachを呼べるようになっています。

functional interfaceの例、Consumer

後述するCollection、ListのforEachメソッドは引数にjava.util.functions.Consumer型を取ります。これはJava SE 8から新たに追加されました。

Consumerインタフェースの定義
package java.util.functions;

@FunctionalInterface
public interface Consumer<T> {
    public void accept(T t);
}

IterableのforEach

クロージャ的な処理を記述するためのinterfaceです。Iterableインタフェースは、java.util.Collectionで実装されるので大半のコレクションにforEachメソッドが備わります。

java.lang.Iterableインタフェース
package java.lang;

import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

@FunctionalInterface
public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
	for (T t : this) {
	    action.accept(t);
        }
    }
}

Java SE 8からdefaultメソッドが導入され、インタフェースに実装を定義できるようになりました。これは主に後方互換性を維持するために導入されたもので、一度定義したインタフェースにメソッドを追加する際、既存のインタフェース実装クラスを修正しなくて済みます。

Java SE 8のラムダ式およびforEachコレクションの利用

Java SE 8のラムダ式およびforEachコレクションの利用
List<Fighter> fighters = ...
List<Bomber> bombers = ...
fighters.forEach(f -> System.out.println(f));
bombers.forEach(b -> System.out.println(b));

ラムダ式の記述が、Consumerインタフェース実装に変換されます。Consumerインタフェースは1つだけ実装すべきメソッドを定義するfunctional interfaceであり、void accept(T t)が実装すべきメソッドになります。List<Fighter>のforEachはFighter型が適用されるので、実装すべきメソッドの引数はFighter型になります。そこで、引数をf、メソッド実装をSystem.out.println(f)とするラムダ式は、f -> System.out.println(f)となります。

コレクションの様々な表現

不変なコレクション

オブジェクトへ問い合わせメソッドを設けて、戻り値としてコレクションを返す設計があります。ここでオブジェクトが管理しているコレクションへの参照をそのまま戻り値として他のオブジェクトへ渡してしまったら、思わぬ変更をされてしまうことになります。

Iteratorを返せばよい、という案もありますが、Iteratorには、removeメソッドが用意されているため、思わぬ変更を防ぐことはできません。

そこで、java.util.Collectionskクラスに用意されているunmodifiable系メソッドを使って変更できないコレクションを戻り値として他のオブジェクトへ渡すようにします。

不変コレクションの利用
class FlightCoordinator {
    List departures = ...
        :
    public List getDepartureFlights() {
        return Collections.unmodifiableList(departures);
    }
}

この不変なコレクションに対してaddやremoveといったコレクションを変更するようなメソッドを呼び出すと、UnsupportedOperationExceptionがスローされます。

java.util.Collectionsの不変なコレクションへ変換するメソッド
メソッド名 戻り値
unmodifiableCollection(Collection c) Collection
unmodifiableList(List list) List
unmodifiableMap(Map m) Map
unmodifiableSet(Set s) Set
unmodifiableSortedMap(SortedMap m) SortedMap
unmodifiableSortedSet(SortedSet s) SortedSet

同期コレクション

配列とコレクションの相互変換

Javaではデータを保持するときはコレクションを使用することが多いです。しかし、データを他のオブジェクトとの間で受け渡すときには配列を使用することがあります。そこで、オブジェクト内部で保持しているコレクションと配列との変換に関するTipsを見ていきましょう。

コレクションから配列へ

コレクションを配列に変換するには、以下の条件が必要です。

では、Person型を要素に格納しているListをPerson[]へ変換する例を見てみます。

コレクションから配列ヘ変換(1)
List persons = ...;
  :
Person[] personArray = new Person[persons.size()];
persons.toArray(personArray);

CollectionクラスのtoArrayメソッドは、引数を取らないものと引数に配列を取るものと2種類存在します。

引数を取らないものは戻り値がObject[]型です。この戻り値で得られたオブジェクトはObject[]以外へのキャストが出来ないため、あまり有用性がありません。

引数にある型の配列を取るものはジェネリクス・メソッドとして宣言されています。戻り値はT[]型です。また、引数に指定した配列のlengthがコレクションの大きさ(size)より等しいかそれ以上であれば、引数の配列にコレクションの各要素を格納してくれます。ただし、引数に指定した配列のlengthがコレクションの大きさより小さいときは、引数に指定した配列型と同じ型の配列を新たにコレクションの要素と同じ長さで生成し、コレクションの各要素を格納して戻り値として返却します。このときは、引数に指定した配列には何も格納しません。

ここで、toArrayメソッドの引数に長さ0の配列を指定すると、必ず戻り値に新規配列が生成されて返却されるようになります。以下にサンプルを挙げます。

コレクションから配列ヘ変換(2)
List<Person> persons = ...;
  :
Person[] personArray = persons.toArray(new Person[0]);

配列からコレクションへ

配列をコレクションへ変換するときは、java.util.ArraysクラスのasListメソッドを使用します。

コレクションの初期化

初期値を指定して初期化

整数のリストを初期化
List<Integer> numbers = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19);

参考文献

[1]Joshua Bloch. Effective Java. ピアソン・エデュケーション, 2001.
[2]Jeff Langr. essential Java Style : Patterns for Implementation. PRENTICE HALL, 2000.
[3]Brett McLaughlin; David Flanagan. Java 5.0 Tiger. オライリー, 2004.