コレクションの中に格納された要素にアクセスする時は、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です。
List persons; : for (Iterator it = persons.iterator(); it.hasNext(); ) { Person person = (Person)it.next(); // do something for person. }
Java2 5.0(Tiger)から導入されたジェネリクス機能を用いると、キャストが不要になります。
List<Person> persons; : for (Iterator<Person> it = persons.iterator(); it.hasNext();) { Person person = it.next(); // do something for each person }
Java2 5.0(Tiger)から導入された拡張for文を使用すると、コレクションのイテレーションを簡潔に記述できます。
List persons; : for (Object obj : persons) { Person person = (Person)obj; // do something for each person }
また、Genericsと組み合わせると、キャストが不要になるのでもっと簡潔になります。
List<Person> persons; : for (Person person : persons) { // do something for each person }
Iteratorを使ってコレクションの要素にアクセスする方法はいくぶん手続き的です。コレクションの要素にアクセスを必要とするクライアントの全ての場所に似たコードが散在することになります。これは、コードの一元化を阻害します。"Once and only once"
そこで、クロージャ的にイテレーション処理をコレクション側に一元化する方法を使います。ただし、Java SE 7までのCollection APIにはこのような実装は提供されていません。そこで、別途拡張することになります。クロージャ的な処理は、Javaではinterfaceと匿名クラスを組合せて実現します。ただし、Javaの内部クラスは内部クラスが定義されたメソッドスコープのローカル変数を限定的にしか参照できないので、クロージャと言うのは正しくありません。そこで、「クロージャ的」という表現をこの文書では使っています。
まずは、クロージャ的な処理を記述するためのinterface Blockを定義します。これは、コレクションの要素一つ毎に呼び出されるメソッドexecを宣言しています。
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のインナークラスとした方がよいでしょう。
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では、ラムダ式およびラムダ式の型(関数インタフェース)を引数にとるコレクションのメソッドを使ってクロージャ的にイテレーション処理をコレクション側に委ねることが標準でできるようになりました。
クロージャ的な処理を記述するためのいくつものinterfaceが標準で提供され、それらはアノテーション@functionalInterfaceで修飾されています。また、Collection、Listが実装するインタフェースjava.lang.IterableにforEachメソッドが追加され、主要なコレクションに対してforEachを呼べるようになっています。
後述するCollection、ListのforEachメソッドは引数にjava.util.functions.Consumer型を取ります。これはJava SE 8から新たに追加されました。
package java.util.functions; @FunctionalInterface public interface Consumer<T> { public void accept(T t); }
クロージャ的な処理を記述するためのinterfaceです。Iterableインタフェースは、java.util.Collectionで実装されるので大半のコレクションにforEachメソッドが備わります。
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メソッドが導入され、インタフェースに実装を定義できるようになりました。これは主に後方互換性を維持するために導入されたもので、一度定義したインタフェースにメソッドを追加する際、既存のインタフェース実装クラスを修正しなくて済みます。
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がスローされます。
メソッド名 | 戻り値 |
---|---|
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[]へ変換する例を見てみます。
List persons = ...; : Person[] personArray = new Person[persons.size()]; persons.toArray(personArray);
CollectionクラスのtoArrayメソッドは、引数を取らないものと引数に配列を取るものと2種類存在します。
引数を取らないものは戻り値がObject[]型です。この戻り値で得られたオブジェクトはObject[]以外へのキャストが出来ないため、あまり有用性がありません。
引数にある型の配列を取るものはジェネリクス・メソッドとして宣言されています。戻り値はT[]型です。また、引数に指定した配列のlengthがコレクションの大きさ(size)より等しいかそれ以上であれば、引数の配列にコレクションの各要素を格納してくれます。ただし、引数に指定した配列のlengthがコレクションの大きさより小さいときは、引数に指定した配列型と同じ型の配列を新たにコレクションの要素と同じ長さで生成し、コレクションの各要素を格納して戻り値として返却します。このときは、引数に指定した配列には何も格納しません。
ここで、toArrayメソッドの引数に長さ0の配列を指定すると、必ず戻り値に新規配列が生成されて返却されるようになります。以下にサンプルを挙げます。
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. |