今回のParasoftブログではJava8で追加された新機能をいくつかご紹介していきます。
■ラムダ式
Java8で1番着目されている「ラムダ式」は、関数型インタフェース(実装すべきメソッドを1つだけ持っているインタフェースのこと)のメソッドを実装する際に使うことが出来ます。
基本構文
( 実装するメソッドの引数 ) -> { 処理 }
例えば、int型の変数2つを引数に持ち処理結果を返すようなメソッドは以下のように記述できます。
{ } の中に処理したい内容を記述します。
(int x, int y) -> { return x + y; }
また、引数や戻り値型を型推論してくれるので、次のようにも記述できます。
(x, y) -> { return x + y; }
※複数の引数がある場合に、記述有と記述無を同時に使用することは出来ません。
例をあげると、Comparatorを使ったコレクションのソートを記述するとき、これまでは匿名クラスを使って次のように記述していました。
List<String> list = Arrays.asList("Jtest", "dotTEST", "SOAtest", "Virtualize");
list.sort(new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
int ans = s1.length() - s2.length();
return ans;
}
});
これをJava8で型推論とラムダ式を使うと次のようになります。List<String> list = Arrays.asList("Jtest", "dotTEST", "SOAtest", "Virtualize");
list.sort((s1, s2) -> s1.length() - s2.length());
Java7までの「@Override」でオーバーライドしなければならないメソッドの宣言が必要だったのに比べると、全体の記述量が減り、コードも見やすくなっていることが分かります。Comparatorの他にも次のような関数を使用することが出来ます。
・Function<T, R> … 引数1つ、戻り値あり
・BiFunction<T, U, R> … 引数2つ、戻り値あり
・UnaryOperator<T> … 単項演算子
・BinaryOperator<T> … 二項演算子
Java8の新機能はラムダ式以外にもありますが、ラムダ式はそれらの新機能を有効的に使えるように設計されています。
この次にご紹介する「Stream API」はもちろん、これまでも利用していた「コレクションAPI」もラムダ式を活用するためメソッドが変更されています。
例えば、java.util.Listにはメソッドの引数にラムダ式を記述することが出来ます。
これまでコレクション要素を1つずつ取り出すのにイテレータやカウンタを使用していましたが、ラムダ式を使えば、forEach()の引数にラムダ式を記述することで実現できます。
これまでのイテレータを使った方法:
List<String> list = Arrays.asList("Jtest", "dotTEST", "SOAtest", "Virtualize");
for(Iterator i = list.iterator(); i.hasNext();){
System.out.println(i.next());
}
Java8のラムダ式を使った方法:List<String> list = Arrays.asList("Jtest", "dotTEST", "SOAtest", "Virtualize");
list.forEach(x -> System.out.println(x));
どちらの場合も同じ結果が得られます。Jtest dotTEST SOAtest Virtualize見た目はそこまで大きな変化が見られないかもしれませんが、処理の仕方が大きく変わります。
これまでは本質的に順次処理であり、要素をコレクションに格納されている順に処理しなければいけませんでしたが、ラムダ式を使う場合、処理の対象(ここでは”list”)を指定する必要はあるものの、処理の方法については指定しなくても効率的な順序で処理するためにライブラリで把握しているメモリ内のデータの位置情報を利用して処理され、開発者は正しい答えをより早く得ることができます。
今回はforEach()メソッドを使いましたが、java.util.Listでは以下のメソッドも利用することができます。
| メソッド名 | 処理 |
|---|---|
| forEach() | Listの各要素に対して繰り返し処理を行う |
| sort() | Listの要素をソートする |
| replaceAll() | Listの各要素を置換する |
| removeIf() | 指定した条件に一致する要素をListから削除する |
また、コレクションの1つであるjava.util.Mapでも以下のメソッドで追加/変更が行われています。
| メソッド名 | 処理 |
|---|---|
| forEach() | Mapの各要素に対して繰り返し処理を行う |
| putIfAbsent() | 指定した値が存在しない場合Mapに値を追加する |
| replace() | 指定したキーが存在する場合値を上書きする |
| replaceAll() | Mapの各要素を置換する |
| compute() | 指定したキーでMapに追加する |
ラムダ式の基本的な機能をご紹介いたしましたが、これまでとは全く違ったプログラミング記法で、戸惑う方も出てくるかもしれません。
記述量が減る、処理の追加や変更が行いやすいというメリットはあるものの、慣れるまでは何をしているのか解読しづらいコードが出てくることも想定されます。
ラムダ式のメリットを生かしつつ、誰にでもわかりやすいコーディングを意識する必要もありそうですね。
■Stream API
新規に追加されたAPIとしてjava.util.streamが挙げられます。
このStream API は今回の例に出したようにラムダ式と組み合わせることを想定としているようです。Stream APIを使用すると、コレクションのデータをストリームのソースとして入力し、データを加工、処理する操作をシンプルに記述することが出来ます。
Stream APIではコレクション要素の加工を簡単に行えるようになり、1つずつの処理をfor文でループしながら行う必要があった複数の処理も1文で記述が可能になります。
これまでの方法:
public class StreamAPI {
public static void main(String[] args) {
List<String> list
= Arrays.asList("Jtest", "dotTEST", "SOAtest", "Virtualize");
list = changeUpper(list);
list = sort(list);
output(list);
}
public static List<String> changeUpper(List<String> list) {
List<String> upperList = new ArrayList<>();
for (Iterator i = list.iterator(); i.hasNext();) {
String str = i.next().toString();
upperList.add(str.toUpperCase());
}
return upperList;
}
public static List<String> sort(List<String> list) {
list.sort(new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
int ans = s2.length() - s1.length();
return ans;
}
});
return list;
}
public static void output(List<String> list) {
for (Iterator i = list.iterator(); i.hasNext();) {
System.out.println(i.next());
}
}
}
Stream APIを使った場合:List<String> list = Arrays.asList("Jtest", "dotTEST", "SOAtest", "Virtualize");
list.stream()
.map(s -> s.toUpperCase()) //引数を大文字化
.sorted((a, b) -> b.length() - a.length()) //文字列長で並べ替え
.forEach(System.out::println); //リストの要素数分出力
どちらの場合も同じ結果が得られます。VIRTUALIZE DOTTEST SOATEST JTEST上記の比較でも、Stream APIを使用することでカウンタとして利用するためだけの変数や、複数のメソッド(今回の例は大げさではありますが…)を用意する必要はありません。
また、Stream APIの処理は上から順に実行されますのでデータが加工される流れをソースコード上で確認することも簡単に行えます。さらに、データの流れを意識した処理を記述できるので、一部の処理に変更が入ったとしても簡単に修正ができ、他の処理への影響度もすぐに把握できるので保守開発時のデグレード発生率が下がることも期待できます。
さらに処理の実行タイミングに関して「遅延」という概念が追加されました。
これまでソースコード上の式の値を評価するタイミングは式の値を変数に格納するか、関数に渡した直後でしたが、この「遅延」の概念を利用すると式の値は、その値が必要となった時に初めて評価されるようになります。
例えば以下のコードがある場合、Stream型の変数を評価するための式をskip()で記述した後に、実際に評価されるStreamのデータをListに格納しています。
List<String> list = new ArrayList<>();
Stream<String> st = list.stream().skip(2);
//ListをStreamに変換しつつ、処理を飛ばす要素数を定義
System.out.println("処理開始");
list.add("Jtest");
list.add("dotTEST");
list.add("SOAtest");
list.add("Virtualize");
st.forEach(x -> System.out.println(x));
//Stremを予め定義した(skip()メソッドで)情報で処理
System.out.println("処理終了");
実行結果は以下のようになります。処理開始 SOAtest Virtualize 処理終了この例から、変数stに対するskip()の処理をコードで記述した後、実際に使用する際に実行していることが分かります。
また、Stream APIを使うと、コレクションの並行処理を簡単に実装できるようになったことも大きな変化であると言えます。
parallel()という1つのメソッドをストリームに渡すと、それまで直列的であったコレクションの処理も並列可能なStreamであれば処理が自動的に並列化され、コレクションの大きさが大きければ大きいほど、これまでと比較して処理速度の向上が見込まれます。
parallel()の使用例:
List<String> list = Arrays.asList("Jtest", "dotTEST", "SOAtest", "Virtualize");
list.stream()
.parallel() //並列化を宣言
.map(s -> s.toUpperCase()) //引数を大文字化
.sorted((a, b) -> b.length() - a.length()) //文字列長で並べ替え
.forEach(System.out::println); //リストの要素数分出力
実際に使う際には実現したい処理への影響や効果的な順序等、考慮するべき点はたくさんありますが、「遅延」の概念や並行処理を利用することでJavaの最適化をはかることが出来そうです。■タイプアノテーション
これまではパッケージやクラス、メソッド等の宣言対象に付与していたアノテーションが、Java8からは型の利用に対してもつけられるようになりました。
例えば、変数の型や、ジェネリクスの型パラメータに対してアノテーションをつけることができます。
下の例では変数の型にアノテーションをつけています。
public static void main(String[] args) {
//@NotNull を付与することで、myObj に null が格納されることを許容しない
@NotNull MyObject myObj = getMyObject();
System.out.println(myObj.toString());
}
private static MyObject getMyObject(){
return null;
}
このコードで@NotNullを外した場合、実行時に「java.lang.NullPointerException」が発生します。タイプアノテーションを付与しておくことで、バグを減らすためのチェックをコンパイル時に出来るようになります。
変数だけでなく、以下のような使い方も可能です。
・new @Interned MyObject(); //コンストラクタ
・String str = (@NotNull String)myObj; //キャスト
・List<@NotNull String> list; //ジェネリック型
■まとめ
今回は、Java8の新機能の一部を簡単にご紹介しました。
ご紹介出来なかった機能もたくさんあり、Java8のリリースでJava開発が大きく変わるのは間違いなさそうです。
ラムダ式やStream APIの導入でソースコードの記述が簡略化されることが想定されますが、慣れるまではなかなか大変そうな印象を受けました。
タイプアノテーションを使うとコーディングの段階でバグを減らすチェックを行えるなど品質向上の面でもメリットがありそうですが、記述方法も大きく変わることもあり、コンパイルチェックで出来ること、出来ないことをきちんと判断して、フロー解析などの高度なことはツールを導入するなどうまく活用していく必要があるとも感じました。
Parasoft JtestはJava7には対応しておりますが、Java8の対応も準備中です!!