ProjectLambda・6部(Iteration1)
lambda-8-b48-12_jul_2012を基に作成しています。現在の仕様とは違っている点があるかもしれませんので注意してください。
- 即時評価型
- 遅延評価型
- 遅延評価型操作の注意点
次にIterableの強化について見ていきます。
Java8ではコレクション操作をラムダ式等を用いて華麗に書けるようにIterableにいくつかデフォルトメソッドが追加されます。(ラムダ式の類を必要としないメソッドも追加されています。)
この新たなライブラリをIteration1と言います。また、さらに強力なライブラリとしてIteration2と言う物も現在開発されています。
ここではIteration1の範囲から現時点(2012年8月)でIterableに実装されているものを解説します。
なお説明内に出てくるBlock、Blocks、BinaryOperator、Mapper、Mappers、Predicate、Predicates等々はjava.util.functionsパッケージにあります。これらにも便利なメソッドがあるので次回説明します。
まず、Iterableに追加されるメソッドは大きく分けて「遅延評価型」と「即時評価型」に分類できます。
ここでいう遅延評価というのはIterableが返すIteratorのnextなどが呼ばれるまで評価されないという事を意味します。
即時評価型ではIterator#nextなどが呼ばれ遅延評価されていたものが確定します。
T getFirst()[即時評価型]
Iterableの最初の要素を返します。(読んで字のごとくですが^^;)
要素が一つも存在していない場合はNoSuchElementExceptionが投げられます。
仕様(Javadoc)上はこうなのですが、現在の実装では、要素が一つも存在していない場合はnullが返り、最初の要素がnullの場合はぬるぽとなってしまいます。
T getOnly()[即時評価型]
Iterableの要素が一つだけの場合はその要素を返します。
要素が一つも存在していない場合はNoSuchElementExceptionが投げられます。
要素が二つ以上存在する場合はIllegalStateExceptionが投げられます。
getFirstとの違いは要素が二つ以上存在する場合の挙動です。
現在の実装はその要素がnullの場合はぬるぽとなってしまいます。それ以外は仕様通りです。
T getAny()[即時評価型]
Iterableの何番目かの要素を返します。
要素が一つも存在していない場合はNoSuchElementExceptionが投げられます。
getFirstとの違いは何番目の要素が返ってくるのかが保障されていない点です。
ちなみに、現在の実装ではgetFirstを呼び出しているだけです。なので、getFirstと同じで変な挙動です^^;
boolean anyMatch(Predicate<? super T> filter)[即時評価型]
Predicate<T>#boolean test(T t)
→Predicate、Predicates
一つでも適合するかどうかを調べます。
全ての要素に対してfilter#testを適応していき、一つでもtrueを返すような要素があった場合はtrue、そうでなかった場合はfalseを返します。
if (list.anyMatch(n -> n % 2 == 0)) { System.out.println("偶数が存在しています"); }
boolean allMatch(Predicate<? super T> filter)[即時評価型]
Predicate<T>#boolean test(T t)
→Predicate、Predicates
全てが適合するかどうかを調べます。
全ての要素に対してfilter#testを適応していき、全てtrueを返す場合はtrue、そうでなかった場合はfalseを返します。
if (list.allMatch(n -> n > 0)) { System.out.println("すべて正の数です"); }
boolean noneMatch(Predicate<? super T> filter)[即時評価型]
Predicate<T>#boolean test(T t)
→Predicate、Predicates
全てが適合しないかどうかを調べます。
全ての要素に対してfilter#testを適応していき、全てfalseを返す場合はtrue、そうでなかった場合はtrueを返します。
if (list.noneMatch(obj -> obj == null)) { System.out.println("全てnullではありません"); }
「obj -> obj == null」はPredicates#isNullを用いて書けます。
→Predicates#isNull
void forEach(Block<? super T> block)[即時評価型]
Block<T>#void apply(T t)
→Block、Blocks
これは読んで字の如くforEachで先頭の要素から順番にblock#applyを適用していきます。
ある数列リストseqを標準出力に一行ずつ出力する場合、今までは次のように書いていました。
for (int n : seq) { System.out.println(n); }
Java8以降ではforEachを用いて次のように書けます。
seq.forEach(n -> System.out.println(n)); // ラムダ式を使用 seq.forEach(System.out::println); // メソッド参照を使用
<A extends Fillable<? super T>> A into(A target)[即時評価型]
targetに対して全要素を追加します。
FillableというのはJava8から追加されるインターフェースで、addAllメソッドを持っており、Collectionなどによって実装されています。
T reduce(T base, BinaryOperator<T> reducer)[即時評価型]
BinaryOperator<T>#T eval(T left, T right)
→BinaryOperator
baseと呼び出しのIterableの全ての要素から一つの要素に絞り込みます。
例えばある数列リストseq(例:{1, 5, 4, 8, 1})から最高値を取り出す場合はreduceを用いて次のように書きます。
int max = seq.reduce(Integer.MIN_VALUE, Integer::max); // 8
内部的な実装を見てみると
while (iterator.hasNext()) { base = reducer.eval(base, iterator.next()); } return base;
となっていますので、baseの値がreducer#evalの戻り値に変わっていっていることが分かります。
このことを用いてseqの総和を求めるためには
int sum = seq.reduce(0, (n1, n2) -> n1 + n2); // 19
と書けます。
getXXX系のメソッドには最後の要素を取得するメソッドがありませんでしたがreduceを用いて実現することができます。
Integer last = seq.reduce(null, (l, r) -> r);
「(l, r) -> r」では常に後ろの方にある要素を返すようにしているため結果的に最後の要素が取り出されます。*1
<U> U mapReduce(Mapper<? super T, ? extends U> mapper, U base, BinaryOperator<U> reducer)[即時評価型]
Mapper<T, U>#U map(T t)
→Mapper、Mappers
BinaryOperator<T>#T eval(T left, T right)
→BinaryOperator
mapしてreduceします。
iterable.map(mapper).reduce(base, reducer);
と同義になります。
mapについては後述します。
これのUに当たるところがプリミティブ型の物(int, long, doubleのみ)も用意されています。
それぞれのシグネチャは、
int mapReduce(IntMapper<? super T> mapper, int base, IntBinaryOperator reducer) long mapReduce(LongMapper<? super T> mapper, long base, LongBinaryOperator reducer) double mapReduce(DoubleMapper<? super T> mapper, double base, DoubleBinaryOperator reducer)
です。
これらを使用してオートボクシングが無くなるのであれば処理速度の向上を望めるのでそういう時に使用するのかな。
Iterable<T> sorted(Comparator<? super T> comparator)[遅延評価型]
お馴染みのソートです。そして、お馴染みのComparatorです。
Iterable<T> uniqueElements()[遅延評価型]
重複する要素が排除されたIterableを作成します。
Arrays.asList(1, 1, 2, 2, 3, 3, 4, 5).uniqueElements(); // [1, 2, 3, 4, 5]
<U> Iterable<U> map(Mapper<? super T, ? extends U> mapper)[遅延評価型]
Mapper<T, U>#U map(T t)
→Mapper、Mappers
全要素をmapper#mapを用いて変換し、Iterable<U>を作成します。
seqの全要素を100倍した物を作成するにはmapを用いて
Iterable<Integer> seq2 = seq.map(n -> n * 100); // [100, 500, 400, 800, 100]
さらにこれを文字列のリストに変換するには
Iterable<String> seq3 = seq2.map(Object::toString);
と書きます。
「Object::toString」のほかにも「String::valueOf」を利用することもできます。
要素にnullがあった場合は前者はぬるぽになりますが、後者だと"null"に置き換わります。
他にもMappers#stringを用いることもできます。
String str = seq.map(Mappers.string());
Mappers#stringで返されるオブジェクトは、単純に「t -> String.valueOf(t)」とString#valueOfを呼び出しているだけで「String::valueOf」と書いても現在の実装上は同じです。
ちなみに他にもMapperに関するユーティリティーメソッドがMappersによって提供されています。
→Mappers
別の例としては、Studentクラスのインスタンスを格納したリストstudentListがあったとして、このリストから名前のリストへ変換するためには下のように書きます。
List<Student> studentList = ...;
Iterable<String> studentNameList = studentList.map(Student::getName);
<U> Iterable<U> flatMap(Mapper<? super T, ? extends Iterable<U>> mapper)[遅延評価型]
Mapper<T, U>#U map(T t)
→Mapper、Mappers
mapperによって変換されたIterable<U>が一本化されたIterableを返します。
例えば、学校のクラス(SchoolClass)のリストclassesがあり、SchoolClassがそのクラスに属する生徒(Student)のリストを取得するメソッドgetStudentsを持っていたとして、この学校の全生徒のIterableを取得するには、
Iterable<Student> students = classes.flatMap(SchoolClass::getStudents);
と書けます。
Iterable<T> filter(Predicate<? super T> predicate)[遅延評価型]
Predicate<T>#boolean eval(T t)
→Predicate
Predicate#evalがtrueを返すような要素のみのIterable<T>を返します。
seqの要素のうち偶数の物だけのIterableを作成する場合は
Iterable<Integer> seq2 = seq.filter(n -> n % 2 == 0);
と書きます。
Iterable<T> cumulate(BinaryOperator<T> op)[遅延評価型]
BinaryOperator<T>#T eval(T left, T right)
このメソッドは等差数列や等比数列を作成するのに向いています。
どういった動作をするのかは言葉では説明しづらいので上の図を参考にしてください。
例えば1〜10までの等差数列を作成し、1〜nまでの和のIterableを作成するには次の様に書けます。
Iterable<Integer> seq = Arrays.iterable(new Object[10]).map(obj -> 1).cumulate((l, r) -> l + 1); // [1, 2, 3, ..., 10] Iterable<Integer> sums = seq.cumulate((l, r) -> l + r); // [1, 3, 6, ..., 55]
また文字列のカンマ区切りを行うことも可能です。
Iterable<Integer> commaedSums = sums.map(Mappers.string()).cumulate((l, r) -> l + ", " + r); // ["1", "1, 3", ..., "1, 3, 6, 10, 15, 21, 28, 36, 45, 55"]
すべての要素がカンマ区切りされているのは最後の要素なのでreduceを用いて、
String str = commaedSums.reduce((l, r) -> r); // "1, 3, 6, 10, 15, 21, 28, 36, 45, 55"
と得られます。
遅延評価型操作の注意
遅延評価型操作にはいくつか注意しなければならない点があります。
一つ目が各要素の値が即時評価型操作が行われるまで分らないという点です。*2
List<Integer> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { list.add(i); } Iterable<Integer> iterable = list.map(n -> n * 2); // 遅延評価型操作 for (int i = 0; i > list.size(); i++) { list.set(i, 0); } iterable.forEach(System.out::println); // 即時評価型操作: 全部0
このように遅延評価型操作の結果がどのようになるかは即時評価型操作の時の元のIterable(今回はlist)の状態に依存します。
二つ目がそれぞれの遅延評価型操作が即時評価型操作が行われる度に実行されるという点です。
例えば次の例。
Iterable<Integer> iterable = Arrays.asList(0).map( n -> { System.out.println("called map"); return n; }); // 遅延評価型操作 if (iterable.count() == 1) // 即時評価型操作: called map { int n = iterable.getOnly(); // 即時評価型操作: called map }
このように複数回遅延評価型操作が行われていまい、(仮に並列実行されていたとしても)全体として処理速度が遅くなってしまう可能性があります。
これらの問題を解決する場合はintoメソッドを用いて何らかのコレクションに追加して操作を確定しておきます。
List<Integer> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { list.add(i); } List<Integer> doubledList = list.map(n -> n * 2).into(new ArrayList<Integer>()); // 操作が確定する。 for (int i = 0; i < list.size(); i++) { list.set(i, 0); } doubledList.forEach(System.out::println); // 0 2 4 ... 18
Iterable<Integer> iterable = Arrays.asList(0).map( n -> { System.out.println("called map"); return n; }).into(new ArrayList<Integer>()); // 操作が確定する if (iterable.count() == 1) { int n = iterable.getOnly(); }
なお、Java8では実引数でも型推論が働いてダイアモンドオペレータを使用することができるので、into(new ArrayList<>())と書くこともできます。