お盆休み、いかがお過ごしですか。
オリンピックや各地の夏フェス、大小さまざまなイベントも中止や延期になって、ちょっと寂しいですね。
私個人としてはSUPER SONIC(例年のSUMMER SONIC。通称サマソニ。)が延期になったので、今年の夏は始まることなく終わりです。
さて、今回はJava8です。今更です。
Java8が出て暫くしてから、当ブログでJavaの歴史を紹介しました。
今もまだまだ進化や諸々の変化があり2020年8月現在、最新は14だそうです。
なんで今更Java8かと言えば、ここ5年くらい私自身がJava8以降を対象としたプロジェクトに関わりがなかったんですよね。
でも、そろそろJava8以降もちゃんと押さえとかないとヤバいのでは?と思い、恥ずかしながら再入門です。
恥ずかしがってるだけじゃ進まない。「知るは一時の恥、知らぬは一生の恥」です!
という訳で、今回はJava8の機能を改めて確認していこうと思います。
ぶっちゃけ自分用勉強ノートですので、重要な説明はリンク先にあったりします。
事前の整理 *これはJava8以前の話
総称型(Generic type)
List<String> list = new ArrayList<String>();
こんなやつです。<>の中に型を書くことで、このListの中身の要素の型を指定出来ます。
この場合は要素の型がStringだと明示しています。
この<>の中の指定がないとIDEに怒られるので、なんとなく使っている。みたいな人も少し居る印象です。
そういう人は、例えば以下のようにJavaAPIリファレンスのListやArrayListの説明を見てもピンと来ない、もしくは若干の拒否反応を持つでしょう。(経験談)
インターフェースList<E>
クラスArrayList<E>
このEを仮型と呼び、これらがインスタンス化した時に型を固定化している(させる)ことを表しています。
なお、よく見かけるEとかTとかはElemetやType、つまり要素や種類を表しています。
他の仮型として、KはKey、VはValue、NはNumber、RはReturn(戻り値)、SやUは複数のTypeを意味するらしいです。
この辺りを詳しく確認したいという人は、以下のサイトがとても分かりやすいです。
参考URL:https://totech.hateblo.jp/entry/2017/01/08/225019
参考URL:https://nagise.hatenablog.jp/entry/20101105/1288938415
匿名クラス
この後の「ラムダ式」を理解するためには予め匿名クラスをちゃんと理解しておくとスムーズです。
早速わかりやすい例として、Collections.sortメソッドを見てみましょう。下記はJavaAPIリファレンスより。
public static <T> void sort(List<T> list, Comparator<? super T> c)
この記述に面食らった人は一旦落ち着いて、上の総称型で紹介したリンクや、境界ワイルドカードという単語を確認しましょう。
さて、Collections.sortメソッドの第一引数はソートするList、第二引数は比較の条件を定義するComparatorインターフェースとなっています。
List<T>の型が自前のクラスだとすると、そのクラスのどの要素で比較するか?というのをComparatorインターフェースの実装で自由に出来るというものです。
具体的な例:
名前、年齢を持ったEmployeeクラスがあり、それをListに持つことで社員一覧とする。
年齢でソートしたい場合は、Comparatorインターフェース実装したSortByAgeクラスを用意して
そのクラスのインスタンスを使って、以下のようにすれば良いでしょう。
// インターフェースの実装イメージ public static class PersonComparatorByAge implements Comparator { public int compare(Employee o1, Employee o2) { return Integer.compare(o1.getAge(), o2.getAge()); } } // それの利用イメージ Collections.sort(list, new PersonComparatorByAge());
しかし、この年齢ソートが他に使い道が全然なくて、全体で一カ所でしか使わないのが明白だったとする。
それならわざわざクラス実装しなくてよくね?となりますね。こんな時に匿名クラスを使います。
sortの第二引数のComparatorインターフェースの部分に直接クラスの実装を書いちゃうっていう手法。
Collections.sort(list, new Comparator() { public int compare(Employee o1, Employee o2) { return Integer.compare(o1.getAge(), o2.getAge()); } });
このように、インターフェースを実装したクラスを明示的に定義しない方法を匿名クラスといいます。
Java8の新機能 *メイン機能の外堀から埋めよう
キモとなるのは「ラムダ式」と「メソッド参照」、それらを代入する先である「関数型インターフェース」
そして、それらを利用した「Stream」でしょう。
どうやら、これらを導入したいが為の諸々の機能追加がされているようです。
これらの概念も併せてサラっと確認しておきましょう。
なお、Javaプログラマでお世話になってない人は居ないのでは?というくらい有名なhishidamaさんのサイトにて
とても綺麗にまとまってるので、ここを見るのが一番良いと思います。私は今まさにここを見ながら勉強しています。
参考URL:http://www.ne.jp/asahi/hishidama/home/tech/java/uptodate.html#JDK1.8
下に挙げるのは、その中で特に重要と感じたものだけをピックアップしましたが、最終的には全部押さえておく必要があります。
インターフェースのstaticメソッド
Java7までは、インターフェースには抽象メソッド(処理が記述されてないメソッド)しか持てませんでした。
Java8からはインターフェース上にstaticメソッドを定義できるようになりました。
インターフェースのデフォルトメソッド
default句をつけることで、インターフェースのデフォルトメソッドを定義できるようになりました。
これは、このインターフェースを実装したクラス全てで共通で使えるメソッドを定義する機能ということです。
関数型インターフェース *ここからが本番
関数型インターフェースは、ラムダ式やメソッド参照の代入先になれるインターフェースのことです。
定義されている抽象メソッドが1つだけあるインターフェースで、staticメソッドやデフォルトメソッドは含まれていても構いません。
また、Objectクラスのpublicメソッドがインターフェース内に抽象メソッドとして定義されていても、それは無視されます。
例えばComparatorインターフェースには抽象メソッドcompare以外にもpublicなメソッドは存在しますが
それらはstatic、デフォルト、Objectクラスの実装なので除外され、関数型インターフェースとして扱えます。
自作インターフェースを関数型インターフェースとして定義したい場合は、java.lang.FunctionalInterfaceアノテーションを付けるのが良いようです。
FunctionalInterfaceアノテーションを付けていると、関数型インターフェースの条件を満たしていない場合にコンパイルエラーになる為です。
また標準的な関数型インターフェースが用意されているので、ここは一通り目を通しておいた方が良さそうです。
詳しくはこちら:http://www.ne.jp/asahi/hishidama/home/tech/java/functionalinterface.html#h_API
ラムダ式
早い話が、匿名クラスを簡単に書く記法と思えばよさそうです。
再びCollections.sortメソッドを例にすると、これの第二引数はComparatorインターフェースです。
Comparatorインターフェースは関数型インターフェースとして扱えるので、ラムダ式に置き換えることができます。
ラムダ式に置き換えられる中身は唯一の抽象メソッドであるcompareメソッドのオーバーライドということで良さそうですね。
つまり、上で匿名クラスで書いていた以下のようなコードが
Collections.sort(list, new Comparator() { public int compare(Employee o1, Employee o2) { return Integer.compare(o1.getAge(), o2.getAge()); } });
次のように書けるということです。
Collections.sort(list, (o1, o2) -> { return Integer.compare(o1.getAge(), o2.getAge()); });
だいぶスッキリしましたね。
ラムダ式の構文
「引数部 -> 処理部」という形式で表します。
引数部:(型1 引数名1, 型2 引数名2, …) -> 処理部: { 文1; 文2; … return 戻り値; }
例:
(int n, String s) -> { System.out.println(s + n); return n + 1; }
省略構文
引数部の型は代入先の関数型インターフェースで定義されているので省略可能です。
(n, s) -> { System.out.println(s + n); return n + 1; }
引数がひとつであれば、括弧も省略可能です。
n -> { System.out.println(n); return n + 1; }
さらに、処理部の戻り値がvoidや一文の場合は中括弧を省略できます。
この場合はセミコロンも省略しなければいけないようです。
n -> System.out.println(n)
そして引数は型さえあっていればOKなので引数名はなんでもOKです。
ラムダ式を初めて見た時はこの省略しまくった形を見て、面食らっちゃうんですよね。
nってなんやねん!みたいな。
メソッド参照とコンストラクタ参照
関数型インターフェースの変数にメソッドそのものを代入することができます。
「クラス名::メソッド名」とすると、staticメソッドの参照で
「インスタンス変数名::メソッド名」とすると、インスタンスメソッドの参照となります。
基本的にはラムダ式と互換性があります。
使い分けとしてはラムダ式は匿名クラス、メソッド参照は既にあるメソッドの扱いを変えてしまうという感じのようです。
また、コンストラクタも同様に扱え、「クラス名::new」とします。ラムダ式で「() -> new クラス名()」とするのと同じです。
ただし、インスタンスのキャプチャーが違うので注意が必要です。
例によって、詳しくはこちら:http://www.ne.jp/asahi/hishidama/home/tech/java/methodreference.html#h_capture
Stream
複数の値(オブジェクト)に対して何らかの処理(変換や集計)を行う事を分かりやすく記述するためのもの。という認識で良さそうです。
FileInputStreamとかのStreamとは全然関係ありません。
Streamのメソッドは中間処理と終端処理(末端処理)の2種類がありStream内に保持されている値を使った演算は中間処理では行わず、終端処理のメソッドが呼ばれると実際に処理を行うということに注意が必要です。
色々出来るようですが、とりあえずはListに対する便利クラスとしてmap、filter、concat辺りから覚えれば良さそうです。
mapの例:a.txtとb.txtというStringが入ったListをPathのListに変換するというもの
List slist = Arrays.asList("a.txt", "b.txt"); List plist = slist.stream() .map(Paths::get) .collect(Collectors.toList());
filterの例:a,bb,cccというStringが入ったListから、文字長が2のものだけを抽出するというもの
List list = Arrays.asList("a", "bb", "ccc"); List list2 = list.stream() .filter(s -> s.length() == 2) .collect(Collectors.toList()); System.out.println(list2);
concatの例:a,bのStreamとx,yのStreamを結合するというもの
Stream s1 = Stream.of("a", "b"); Stream s2 = Stream.of("x", "y"); Stream s = Stream.concat(s1, s2);
おわりに
大体わかってきましたね。
やっぱりまだまだ構文は難しいと感じますが、便利そうなので積極的に使って慣れましょう!
とにかくパターン的に使って慣れてから、さらに理解を深めるというやり方でも良いと思います。
実践する場所が無いよって人は、ネット上の問題集なんかを解くと良いかもしれませんね。