Java 8での主要な変更点は『ラムダ式』『ストリームAPI』『新しい日付ライブラリ』であるようで、Oracleの公式サイトにおいて解説ページ(
*1)が公開されている。今回はこの中から『ラムダ式』『ストリームAPI』についてみていく。
■ ラムダ式(Lambda Expressions)
Comparator等のインターフェースおいてプログラマが1つのメソッドのみを実装する場合、ソースコードの一部を省略して記述する式をラムダ式と言う。百聞は一見にしかずということで、『リストを降順にソートする』という処理をラムダ式を使う/使わない場合に分けてみていく。
◇ラムダ式を利用しない場合(従来のプログラミング)
package application;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class TestNotLambda {
public static void main(String[] args)
{
// ランダムな数字のリストを作成
ArrayList<Integer> list = new ArrayList<Integer>();
list.add( 5 );
list.add( -77 );
list.add( 24 );
list.add( 3 );
list.add( 987 );
list.add( 56 );
list.add( -9 );
list.add( 0 );
// 整列前のリストを出力
System.out.println("ソート前:");
for ( int i : list){ System.out.println(i); }
// 降順に整列
Collections.sort( list ,
new Comparator<Integer>()
{
public int compare( Integer arg1 , Integer arg2 ){ return arg2 - arg1; }
}
) ;
// 整列後のリストを出力
System.out.println();
System.out.println("ソート後:");
for ( int i : list){ System.out.println(i); }
}
}
実行結果
ソート前:
5
-77
24
3
987
56
-9
0
ソート後:
987
56
24
5
3
0
-9
-77
上記はラムダ式を使わない記述方法である。一応、ソースコードの注目点を解説する。
- 整列はCollection.sort(28行目~)で行っている。sort関数の第1引数にはソートするリストを、第2引数にはソート方法を指定する。ソート方法の指定はComparatorクラスのcompare関数をオーバーライドすることで行う。2つの値(arg1,arg2)が与えられた際に『arg1がarg2より後である場合には正』『arg1がarg2よりも前である場合は負』『arg1とarg2は順不同である場合は0』の値を返すように関数を定義する。
- 29行目~32行目の記述方法は『匿名クラス』と呼ばれ、ここではComparator<Integer>クラスを継承する名前のないクラスを宣言・インスタンス化している。
さて、ラムダ式について話す前に、上記ソースコードの匿名クラスの記述方法について見てみよう。匿名クラスの記述を見ていくといささか記述が冗長に思えてくる。例えば、以下の点である。
new Comparator<integer>() {
public int compare( Integer arg1 , Integer arg2 ){ return arg2 - arg1; }
}
- Collections.sort関数の第2引数であることからComparator<T>クラスであることは明白である(赤字部分)
- listがArrayList<Integer>であることからT=Integerであることも明白である(桃色部分)
- Comparatorで実装すべき関数はcompare(T arg1 , T arg2)だけしかない(青色部分)
プログラマ視点で考えると、『これら黒字以外の部分は文法上、仕方なく記述しているだけで本当は記述を省略したい』という人もいるだろう。この省略したいという願望を叶えたのものこそがラムダ式となる。ラムダ式ではJavaコンパイラが型推論できるコードをプログラマが書く必要がなくなる。
ラムダ式の記述方法は以下の通りである。
( 引数1 , 引数2 , ... ) -> { 式1 ; 式2 ; ... ; return ○○; }
また、引数が1つの場合には丸括弧を省略できるし、式が1つの場合には波括弧とreturnという文字を省略できる。ラムダ式を用いて、上記と同様の処理を記述してみると以下の通りとなる。
◇ラムダ式を利用する場合
package application;
import java.util.ArrayList;
import java.util.Collections;
public class TestLambda {
public static void main(String[] args)
{
// ランダムな数字のリストを作成
ArrayList<Integer> list = new ArrayList<Integer>();
list.add( 5 );
list.add( -77 );
list.add( 24 );
list.add( 3 );
list.add( 987 );
list.add( 56 );
list.add( -9 );
list.add( 0 );
// 整列前のリストを出力
System.out.println("ソート前:");
for ( int i : list){ System.out.println(i); }
// 降順に整列
Collections.sort( list ,
( arg1 , arg2 ) -> arg2 - arg1
);
// 整列後のリストを出力
System.out.println();
System.out.println("ソート後:");
for ( int i : list){ System.out.println(i); }
}
}
※実行結果は『ラムダ式を利用しない場合(従来のプログラミング)』と同様。
27行目の『( arg1 , arg2 ) -> arg2 - arg1』という記述方法がラムダ式となる。記述方法が省略されただけで、処理内容は前述の匿名クラス(Comparator.compare関数の記述)と同様である。
さて、ここまで見るとわかってくると思うが、ラムダ式が利用できる場面は限られている。つまり、Comparator<T>クラスのように『プログラマが1つのメソッドのみを実装する』インターフェースでのみ、ラムダ式が記述が可能である。
この形のインタフェースを特に『関数インターフェース』と呼ぶ。Comparator<T>以外にも、Runnableなど他多数のJavaインターフェースや自作のインターフェースに関しても、『プログラマが1つのメソッドのみを実装する』形になっていれば関数インターフェースである。関数インターフェースであるかはJavaコンパイラが判断するため、プログラム上で指定する必要はない。
ラムダ式は『関数インターフェースを省略して記述した式』と言える。
■ ストリームAPI(Stream API)
Collectionの各要素に対して、処理(関数)を適用するためのインターフェースがストリームAPIである。Iteratorの発展版といった感じ。JavaDocいわく『順次および並列の集約操作をサポートする要素のシーケンス』とのこと。この説明はわかる人には正確に伝わるだろうが、わからない人なのでまったくわからない。
ストリームAPIを使うと、ArrayedList内の要素を並べ替えることはもちろん、リスト内の合計値の取得や、「e」という文字含む要素だけを抜き出して新しいリストを作ることが簡単に実現できるらしい。 ストリームAPIを利用した例を以下に示す。
ストリームAPIの例
package application;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TestStreamAPI {
public static void main(String[] args)
{
// ランダムな数字のリストを作成
List<Integer> list = new ArrayList<Integer>();
list.add( 5 );
list.add( -77 );
list.add( 24 );
list.add( 3 );
list.add( 987 );
list.add( 56 );
list.add( -9 );
list.add( 0 );
// 整列前のリストを出力
System.out.println("ソート前:");
for ( int i : list){ System.out.println(i); }
// 降順に整列
list = list.stream().sorted( ( arg1 , arg2 ) -> arg2 - arg1 ).collect( Collectors.toList() );
// 整列後のリストを出力
System.out.println();
System.out.println("ソート後:");
for ( int i : list){ System.out.println(i); }
// リスト内の正数を抜き出し、各要素を3で割った余りをすべて足す
Integer sum = list.stream().filter( num -> num > 0 )
.map( num -> num % 3 )
.reduce( ( v1 , v2 ) -> v1 + v2 )
.orElseGet( () -> 0 );
System.out.println();
System.out.println("リスト内の正数を抜き出し、各要素を3で割った余りをすべて足した結果:" + sum );
}
}
実行結果
ソート前:
5
-77
24
3
987
56
-9
0
ソート後:
987
56
24
5
3
0
-9
-77
リスト内の正数を抜き出し、各要素を3で割った余りをすべて足した結果:4
ソースコードとしては、31行目まではラムダ式の説明で使ったソースコードをストリームAPIを利用して書き直したもの、31行目以降は『リスト内の正数を抜き出し、各要素を3で割った余りをすべて足す』という新たな処理を行うものである。ソースコードを解説する前に、ストリームAPIの使い方の基本を確認する。ストリームAPIはすべて、次の3つの操作の順番で処理を行う。
- データソースからストリームの生成。コレクション(ArrayListなど)から、ストリームと呼ばれるデータに変換する。
- 中間操作。ストリームに対して処理を行い、新しいストリームを生成する。中間操作は何度でも重ねて実行することができ、先に呼び出した処理が先に実行される。
- 終端操作。中間操作で処理された結果を取得する。終端処理が呼ばれて初めて中間処理が実行される。この仕様により、Javaの実行環境が中間処理を行う最適な処理方法を決定することが可能となっている。
この流れを頭に入れてソースコードを見ていく。
- 28行目では降順のソート処理をストリームAPIで記述している。ListやMapなどのコレクション APIに追加されたstream関数でストリームを作成(ストリームの生成)。その後、sorted関数でソートを実行(中間処理)。最後にcollect関数でList<Integer>型として結果を取得(終端処理)している。
- 36行目~39行目では、いささか複雑な処理を行っている。まずはstream関数でストリームを作成。その後、中間処理としてfilter, map関数を実施している。filter関数ではリスト要素の絞込み(正数に絞込み)を、map関数で要素への処理(3で割った余りを新たなリスト要素にする)している。終端操作としてreduce関数でリスト要素を集約(総和を計算)し、orElseGet関数でInteger型として結果を取得している。
ストリームAPI自体は新しい概念だが、プログラマとしては新しいクラスが発表されたという程度で難しいことはないようだ。データ生成→中間操作→終端操作の流れと、利用できる関数を覚えればとても使いやすそうだと感じた。
最後に、ストリームAPIの主な関数をあげておく。この他の関数や、関数が中間操作か終端操作かはJavaDocに記述があるので、そちらを参照のこと。
JavaDoc( SE 8 インタフェースStream<T> )
◇ストリーム生成
関数 |
内容 |
stream |
Stream型のインスタンスを取得する。ストリームAPIを利用する際、最初に呼び出すメソッド。 |
paralelStream |
streamメソッドのマルチ・スレッド処理版。高速化したい場合に利用する。シングル・スレッド用のStreamに戻す場合にはsequential()メソッドを利用する。 |
◇中間操作
関数 |
内容 |
filter |
Streamの各要素について、条件で絞り込んだ新しいStreamを作成する。条件はPredicate型で指定し、論理積(and)、論理和(or)、排他的論理和(xor)で複数の条件を合成できる |
map |
Streamの各要素について関数を適用した、新しいStream(戻り値のStream)を作成する |
sorted |
Streamの各要素を並び替え、新しいStreamを作成する |
◇終端操作
関数 |
内容 |
forEach |
Streamの各要素に対して、関数を適用する。例えばSystem.out.printlnですべての要素を出力するなどが可能。戻り値はなし。 |
collect |
Stream型を別の型に変換する。 |
reduce |
Streamの各要素について関数を適用し、戻り値(一覧でなく1つの値)を作成する。sum,average,min,max等がreduce関数の例として挙げられる。これら一般的な関数は標準ライブラリに組み込まれている。 |
■ 参考