本日のお裾分け

日々の開発で得た知識をシェアします。Java/Scala/Ruby/javascript

Java資格取得(OCJP Gold)のための関数型プログラミングまとめ - 後編

Javaの資格(OCJP Gold)取得シリーズ関数型プログラミング後編です。
mrdshinse.hatenablog.com 前編はJava関数型プログラミングをする前提知識として、関数型インターフェース、ラムダ式、組み込みの関数型インターフェースを説明しました。こちら
後編では、関数型プログラミングを使ううえで必要となる知識としてStreamクラスとメソッド、メソッド参照、Optionalクラスを説明するよ。

Streamクラスとメソッド

やんわりしたイメージで言えばStreamってのは券売機にならぶ行列みたいなもんです。
データの集合体って意味では配列とかCollection(SetとかList)の親戚なんだけど、中のデータを扱いやすいメソッドが色々定義されてます。

  • 券売機=何らかの処理
  • 人=データ
  • 行列=Stream

って考えるとイメージしやすい。
あと、行列にどんどん人が並ぶようにStreamは無限のデータを流れ込ますこともできるんです。

Streamの生成方法を4つ紹介します。

//空のStreamを作る
Stream<String> s1 = Stream.empty();
//要素を指定してStreamを作る
Stream<Integer> s2 = Stream.of(1, 2, 3);
//ListからStreamを作る
Stream<String> s3 = Arrays.asList("マキシマム ザ ホルモン", "Crossfaith", "The Bonez").stream();
//無限のStreamを作る
Stream<Integer> s4 = Stream.iterate(1, i -> {return i+1;});

@hishidama さんよりご指摘いただいて、一部修正しました。(9/23) [https://twitter.com/hishidama/status/779203248016101376]

Stream#emptyは何も入っていないStreamを返すし、
Stream#ofで値を指定すれば値の入ったStreamが手に入るし、
List#streamを使えばListをStreamに変換できます。
最後のStream#iterateは無限Streamを生成していて第一引数を初期値として、値を取得するたびに第二引数のUnaryOperatorを実施した結果が返ります。

Streamの作り方がわかったので、次は中の値を操作するメソッドです。
Streamのメソッドには大きく分けて中間処理と、完了処理の2つがあります。
中間処理はStreamを返します。stream.filter(ほげ).map(ほげ).ほげほげと繋いで中の値を操作していきます。
完了処理はStreamを返しません。stream.sorted(ほげ).max()とした時のmaxが完了処理ですね。
完了処理を呼ぶと使用済みストリームとなり、そのストリームはもう使えません。

ここら辺はややこしいけど、使い始めたらわかるかと。。。

とりあえず中間処理は大量にあるんだけど、できる限り一覧にします。(JavaDocまんまだけど)

中間処理の一覧
メソッド 戻り値 概要
filter(Predicate<? super T> predicate) Stream<T> 引数のPredicateに一致するデータのみ返す
distinct() Stream<T> Object#equals(Object o)で重複するデータを除く
limit(long maxSize) Stream<T> 引数の値ぶんだけデータをStreamで返す
skip(long n) Stream<T> 引数の値ぶんだけ要素を破棄して返す
sorted() Stream<T> データをソートして返す。Comparatorを引数にすることも可能。
peek(Consumer<? super T> action) Stream<T> 引数のConsumerを先頭のデータで実行する。Stream自体に変化はない。
map(Function<? super T,? extends R> mapper) <R> Stream<R> 各データに対して引数のFunctionを実行した結果をStreamで返す
flatMap(Function<? super T,? extends Stream<? extends R>> mapper) <R> Stream<R> 各データに対して引数のFunctionを実行した結果をStreamで返す。引数のFunctionが複数のデータを生成しても1つのStreamで返せる。

mapとflatMapの違いは、以下の記事とか読んでくれ。(手抜き)
teratail 【Java】mapとFlatMapの違い

中間処理は以上。
次、完了処理は大量にあるんだけど、(個人的に)よく使うメソッドと試験対策で覚えるべきメソッドに分けて一覧化します。

よく使う完了処理
メソッド 戻り値 概要
collect(Collector<? super T,A,R> collector) <R,A> R Collectorを使ってストリームのデータをMapやListの形に成型して返します。
count() long ストリームのデータの個数を返します。
forEach(Consumer<? super T> action) void ストリームの各データに対して引数のConsumerを実行します。
試験対策で覚えておきたい完了処理
メソッド 戻り値 概要
allMatch(Predicate<? super T> predicate) boolean ストリームのすべてのデータが引数のPredicateに一致するかどうかを返します。
anyMatch(Predicate<? super T> predicate) boolean ストリームのいずれかのデータが引数のPredicateに一致するかどうかを返します。
noneMatch(Predicate<? super T> predicate) boolean 引数のPredicateにに一致するデータがこのストリーム内に存在しないかどうかを返します。
findAny() Optional<T> 何かしら一個のデータをOptionalクラスで返します。
findFirst() Optional<T> ストリームの最初のデータをOptionalクラスで返します。
min(Comparator<? super T> comparator) Optional<T> 指定されたComparatorに従ってこのストリームの最小要素を返します。
max(Comparator<? super T> comparator) Optional<T> 指定されたComparatorに従ってこのストリームの最大要素を返します。
reduce(BinaryOperator<T> accumulator) Optional<T> 結合的な累積関数を使ってこのストリームの要素に対してリダクションを実行し、リデュースされた値が存在する場合はその値を記述するOptionalを返します。

collectの使い方は別の記事でまとめます。(力尽きた)

メソッド参照

メソッド参照は省略記法の1つです。

クラス名::メソッド名

と書くことで該当のメソッドを関数型インターフェースの実装として、取得できます。

全てのpublicメソッド(+コンストラクタ)は上記の記法で書けます。

標準出力(System.out#println)の場合、
引数がString1つに対して戻り値はvoidなので、関数型インターフェースでいうとConsumerとなり、
Consumer<String> c = System.out::println;
と書けます。
ラムダ式だと
Consumer<String> c = s -> System.out.println(s);
なので、ラムダよりも短く書ける事がわかります。

コンストラクタもメソッド参照で書くことがでます。
例えば空のArrayListが取得したい場合、ArrayListのデフォルトコンストラクタは
引数なしの戻り値がArrayListなので、関数型インターフェースでいうとSupplierとなり、
Supplier<ArrayList> s = ArrayList::new;
と書けます。
こっちはラムダ式だとこう。
Supplier<ArrayList> s = () -> new ArrayList<>();
メソッド参照の方がわかりやすいですね。

前の章で紹介したStreamのメソッドと組み合わせると使い方がイメージできます。
文字列リストの文字数を出力する事例。(いけてねぇw)

Java7まで

List<String> names = Arrays.asList("マキシマム ザ ホルモン", "Crossfaith", "The Bonez");
for (final String name : names) {
    System.out.println(name.length());
}

Java8から

Arrays.asList("マキシマム ザ ホルモン", "Crossfaith", "The Bonez").stream()
        .map(String::length)
        .forEach(System.out::println);

ListとかString nameとかローカル変数を定義しなくて済むのが関数型プログラミングのメリットなんですが、
その辺は次の記事にでもまとめます。

Optionalクラス

最後はOptionalクラスです。
こいつはnullサヨナラ要員です。

nullチェックとか無駄だし、そもそもnullってどんな型にも入るし、処理しようとするとException吐くし何だよコイツ。
nullに意味を持たせるコードとかくたばってしまえ。。。

空かもしれないし、何か入ってるかもしれない

Optionalはそんな値を意味しています。

Optional#ofNullableで値を格納します。

System.out.println(Optional.ofNullable(null));
//Optional.empty
System.out.println(Optional.ofNullable("Hello."));
//Optional[Hello.]

何か処理をしたい場合は、

  • Optional#ifPresent(Consumer c)
  • Optional#map(Function f)

を使います。

Optional<String> empty = Optional.ofNullable(null);
Optional<String> filled = Optional.ofNullable("Hello.");

empty.ifPresent(System.out::println); //Optional.emptyの場合は何も処理されない。
filled.ifPresent(System.out::println);//"Hello."が標準出力される。

nullを返すUtilクラスとかあると、こういうことするじゃないですか、、、(そもそもnull返すななんですが、、、)
nullチェックしないで落ちました。乙。みたいな。

final List<String> list = new ArrayList<>();
String value = PropertyUtil.get("HOGE");
if (value != null) {
    String[] hoge = value.split(",");
    for (String s : hoge) {
        list.add(s);
    }
}

nullチェックでインデント下がるのも嫌ですし、
ローカル変数の嵐で気が滅入りそうですね。。。

もし、Optionalで返してくれれば、PropertyUtil#getの結果をローカル変数を定義せずにそのまま処理を書き始められます。

final List<String> list = new ArrayList<>();
PropertyUtil.getOptional("HOGE")
        .ifPresent(value -> {
            Arrays.asList(value.split(",")).stream()
                .forEach(list::add);
        });

ちとはしょりすぎたかな。。。
時間があればまた今度書きなおします。

以上!!!

参考

Java8 Goldの試験対策本(日本語)はこちら。

英語だけどとても丁寧なテキストはこちら