本連載では、今までJavaの経験はあっても「ラムダ式は、まだ知らない」という人を対象にラムダ式について解説していきます。今回は、Java 8の新機能として、汎用的に使える関数型インターフェースの概要とパターン、Stream APIの特徴と種類3つの注意点についてコード例を交えて解説。
前回の「知っといてムダにならない、Java SE 8の肝となるラムダ式の基本文法」では、Javaでのラムダ式の記述方法について見てきました。
今回はラムダ式の導入によってより効果を発揮するJava 8の新機能について見ていきます。今回のJavaのバージョンアップではラムダ式の導入と共に、多くの関数型インターフェースを扱うAPIが追加されています。
今までJavaをやっている人の中には「Java 8の新しい機能を試してみよう」と思っている人も多いことでしょう。しかし、「ラムダ式」という新しい書式に加えてAPIに数多くのクラスやインターフェースやメソッドが追加されていて、その多さに拒否感を持ってしまう人もいるかと思います。
しかし、多くの新機能は幾つかの基盤となるパターンがあり、多くの細かい機能はそれらを拡張したものになっています。今回は、その中で「汎用的に使える関数型インターフェース」「ラムダ式が活躍するStream APIの概要」に焦点を当てて見ていきます。
Java 8のAPIを見てみると、新しいメソッドの引数に見慣れないものが定義されていることに気付くと思います。Java 8では多くのメソッドの引数に「関数型インターフェース」を持つものが追加されました。そして、これらのメソッドの引数に使われている関数型インターフェースは、Java 8から用意された汎用的に使える関数型インターフェースになります。
ここではまず、これらの汎用的な関数型インターフェースについて見ていきましょう。
Java 8は、汎用的に使える関数型インターフェースは「java.util.function」パッケージで用意しています。Java 8から追加された新しいメソッドで関数型インターフェースを引数に持つメソッドの多くは、引数にこのパッケージで用意した関数型インターフェースを使っています。
java.util.functionパッケージのAPIを見てみると、幾つものインターフェースが追加されていて、うんざりする人がいるかもしれません。しかし、java.util.functionパッケージで用意されている関数型インターフェースは、受け取る引数と返す結果のパターンから大きく分けて4種類用意しています。そして、それらのインターフェース名からどのような処理を行うのかを分かりやすくするため、それらの性質に応じた名前が付けられています。
種類 | 実装するメソッド | 概要 |
---|---|---|
Function<T,R> | R apply(T t) | 実装するメソッドは、引数としてTを受け取り、結果としてRを返すものになる |
Consumer<T> | void accept(T t) | 実装するメソッドは、引数としてTを受け取り、結果を返さず終了するものになる |
Predicate<T> | boolean test(T t) | 実装するメソッドは、引数としてTを受け取り、boolean値を結果として返すものになる |
Supplier<T> | T get() | 実装するメソッドは、何も引数として受け取らず、結果としてTを返すものになる |
また、それらから拡張したものとして次の関数型インターフェースも汎用的に使われます。
種類 | 実装するメソッド | 概要 |
---|---|---|
Operator<T> | T apply(T t) | 実装するメソッドは、引数としてTを受け取り、結果としてTを返すものになる。Functionを拡張したもの |
これらの中でFunction、Consumer、Predicate、Supplierはそれ自身の名前でインターフェースとして用意されていますが、Operatorだけの名前の関数型インターフェースはないので注意してください。その代わり、1つの引数を受け取るOperatorは「UnaryOperator<T>」、2つの同じ型の引数を受け取るものは「BinaryOperator<T>」のように定義されています。
先ほどのパターンに加えてjava.util.functionパッケージでは名前によってその関数型インターフェースの性質を表すように命名されています。この命名パターンは次のようなものがあります。
Interface | 実装するメソッド | 概要 |
---|---|---|
BiConsumer<T,U> | void accept(T t, U u) | 2つの引数(TとU)を受け取り、何も返さず処理を終了 |
BiConsumer<String, BigDecimal> biConsumer = (string, bigDecimal) -> System.out.println("string=" + string + ", bigDecimal=" + bigDecimal); biConsumer.accept("テスト", new BigDecimal("0.5"));
このサンプルでは2つの受け取った引数(stringとbigDecimal)を単純に標準出力しています。実行すると次の結果になります。
string=テスト, bigDecimal=0.5
Interface | 実装するメソッド | 概要 |
---|---|---|
DoublePredicate | boolean test(double value) | 引数としてdouble値を受け取り、結果としてboolean値を返す |
LongSupplier | long getAsLong() | 何も引数として受け取らず、結果としてlong値を返す |
DoublePredicate doublePredicate = value -> value > 3.14; boolean result1 = doublePredicate.test(2.05); System.out.println("doublePredicate.test(2.05) = " + result1); LongSupplier longSupplier = () -> System.currentTimeMillis(); long result2 = longSupplier.getAsLong(); System.out.println("longSupplier.getAsLong() = " + result2);
DoublePredicateではdouble値の引数であるvalueを受け取り、3.14より大きければTrueのboolean値を返し、3.14以下ならFalseのboolean値を返します。
LongSupplierではシステム時刻をミリ秒のlong値で取得し、そのlong値を返しています。
上記のサンプルを実行すると次の結果になります。
doublePredicate.test(2.05) = false longSupplier.getAsLong() = 1394546343441
Interface | 実装するメソッド | 概要 |
---|---|---|
DoubleToIntFunction | int applyAsInt(double value) | double値を受け取り、結果としてint値を返す |
DoubleToIntFunction doubleToIntFunction = value -> new BigDecimal(value).intValue(); int result = doubleToIntFunction.applyAsInt(3.14); System.out.println("result=" + result);
このサンプルではdouble値の引数であるvalueを受け取り、そのvalueの整数部分をint値で取得して返しています。これを実行すると次の結果になります。
result=3
ここまでは、java.util.functionパッケージの関数型インターフェースを見てきましたが、他のパッケージにも汎用的に使える関数型インターフェースはあります。
特に、Java 8の前からあるAPIで関数型インターフェースの特徴を持つものがそれに当たります。ここでは、例としてComparatorを見てみましょう。
種類 | 実装するメソッド | 概要 |
---|---|---|
Comparator<T> | int compare(T t1, T t2) | 引数として2つのTを受け取り、比較した結果としてint値を返すもの。比較結果が同じなら0を返す |
Java 8では新たに導入される機能の中に関数型インターフェースを使って処理を行うものが追加されています。その中で特に注目を浴びているのが配列やCollectionなどの集合体を扱う「Stream API」です。
Stream APIは、集合体を扱うStreamインスタンスに対し用途に合った幾つかのメソッドを追加していき、最終的なメソッドで集計処理の結果を取得します。
例えば、次のサンプルでは【1】で要素を「あ」から始まるもので、絞り【2】で文字列の比較をして最大のものを結果として取得しています。
public static void main(String[] args){ String[] values = {"あか","あお","きいろ","みどり","おれんじ"}; Stream<String> stream = Arrays.stream(values); String result = stream.filter(value -> value.contains("あ")) // 【1】 .max((v1, v2)->v1.compareTo(v2)) // 【2】 .get(); System.out.println("result=" + result); }
このメソッドを追加していく実装方法(パイプライン処理の実装方法)に拒否感を持つ人がいるかもしれません。しかし、実際の処理がメソッド内に隠蔽(いんぺい)されているので、今後Stream APIの処理が改善された際には、JavaのバージョンをアップするだけでStream APIを使って実装した箇所の改善が行えるという利点もあります。
また、近年のハードウェア面での傾向としてCPUが複数のコアを持ち、それぞれのコアで並列に処理を実行するようになってきています。それに伴いStream APIでは並列処理の実装を簡単に行える仕組みが用意されています。
次ページからは、Stream APIについて見ていきましょう。
Copyright © ITmedia, Inc. All Rights Reserved.