Effective Java 3rd [Chapter 7] - ラムダ式とストリーム
ラムダ式とストリーム
Effective Java 第 3 版の個人的メモ
- 項目 42 匿名クラスよりもラムダ式を選ぶ
- 項目 43 ラムダよりもメソッド参照を選ぶ
- 項目 44 標準の関数型インタフェースを使う
- 項目 45 ストリームを注意して使う
- 項目 46 ストリームで副作用のない関数を選ぶ
- 項目 47 戻り値型として Stream よりも Collection を選ぶ
- 項目 48 ストリームを並列化するときは注意を払う
- 参考 URL
Effective Java 3rd 勉強会(11/29)
日時: 2019/11/29 14:00-14:45
参加者(敬称略): 則兼、岩崎、倉元
以下、内容。
1. 項目 42 匿名クラスよりもラムダ式を選ぶ
1.1. 結論
以下の場合は、ラムダ式を使いましょう。
- 数行のコードで済む
- 自分への参照が必要
- 関数型インターフェースの型しか使用しない
それ以外の場合は、匿名クラスを使いましょう。
1.2. 匿名クラスでの実装
Anonymous.java
|
|
1 行で済んでおり、自分への参照は不要なので、ラムダ式の方が良いですね。
そもそも匿名クラスとは、、
以下のように記述される処理のことですね。
|
|
1.3. ラムダ式での実装
Anonymous.java
|
|
Collection.sortの第二引数がComparator
であると認識するのはなぜなのか?
- Collection.sortの仕様は、引数「List
list, Comparator<? super T> c」となっていて、 Comparator
が関数型インタフェースだからラムダ式で表現できる。- 似たような話で、org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#submit()の引数が、
Runnable
とCallable
の識別方法がある。これは、引数の戻り値があるか否かで判断している。 - 戻り値があれば、
Callable
- 戻り値がないと、
Runnable
- 似たような話で、org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#submit()の引数が、
数行で済んでおり、自分への参照はしておらず、Collections.sort
の第 2 引数が関数型インターフェース(Comparator)なので、ラムダ式が向いていますね。
関数型インタフェースとは、、
関数型インターフェースの条件は、大雑把に言って、定義されている抽象メソッドが 1 つだけあるインターフェース。(static メソッドや Object クラスにあるデフォルトメソッドはカウントしない。)
Java8 での関数型インタフェース一覧はOracle Java SE 8 (Uses of Class java.lang.FunctionalInterface)にあります。 3rd party 製のライブラリで@FunctionalInterfaceが付いているインタフェースも、関数型インタフェースですね。
@FunctionalInterface
を付けることで以下のメリットがある。
- コンパイルエラーにしてくれる。
- IDEがもっとよい書き方を提案してくれる。
2. 項目 43 ラムダよりもメソッド参照を選ぶ
2.1. 結論
- メソッド参照の利点:ラムダ式よりも簡潔に書ける。
- ただし、同クラスにあるメソッドを用いる場合は、ラムダ式の方が簡潔に書ける場合がある。
2.2. メソッド参照
メソッド参照とは、、
関数型インターフェース(抽象メソッドが 1 つだけ定義されているインターフェース)の変数にメソッドそのものを代入することが出来る。これを「メソッド参照」と呼ぶ。
MethodRefAndLambdaExpression.java
|
|
メソッド参照の方が、簡潔に記載できていますね。
2.3. メソッド参照について深掘り
メソッド参照には、下記 5 種類がある。
上記の例は、System
クラスのstatic
フィールドout
のprintln
メソッドなので、静的メソッド参照だったんですね。
メソッド参照形式 | コード | 等価ラムダ |
---|---|---|
静的メソッド | TypeName::method |
(args) -> TypeName.method(args) |
非静的メソッド(インスタンス#1 の場合) | instance::method |
(args) -> instance.method(args) |
非静的メソッド(インスタンスなし) | TypeName::method |
(instance, args) -> instance.method(args) |
コンストラクタ#2 | TypeName::new |
(args) -> new TypeName(args) |
配列コンストラクタ | TypeName[]::new |
(int size) -> new TypeName[size] |
#1 :
instance
は、インスタンスへの参照を評価する任意の式です。例えば、getInstance()::method
、this::method
のように書けます。
#2 :
TypeName
が非静的な内部クラスである場合、コンストラクタ参照は外部クラスインスタンスのスコープ内でのみ有効です。(つまり、外からコンストラクタが見れる場合使えますよ。という内容です。)
3. 項目 44 標準の関数型インタフェースを使う
基本的には、標準の関数型インタフェースで事足りる。 しかし、特定の場合に限っては独自の関数インタフェースを定義する必要がある。
3.1. 標準の関数型インタフェース
-
使いやすく、すでに組み込まれている様々なメソッドの恩恵にあずかれる。
-
標準の関数型インタフェースには、43 個あり、主な 6 つは以下である。
-
性能の観点から、プリミティブ型に対応していない関数型インターフェースには、プリミティブ型をボクシングしたオブジェクトは使わないでおきましょう。
3.2. 独自の関数型インタフェース
以下の場合、独自の関数型インタフェースを作成しましょう。
- 広く使われ、説明的な名前によってメリットがある場合
- 強く関係する契約がある場合
- 引数に制約がある場合
- 型の制約
- 2つではなく3つの引数が必要な場合
- 引数に制約がある場合
- 独自のデフォルトメソッドによるメリットがある場合
作成するときは、@FunctionalInterface
を使いましょう。書き方に誤りがある場合、コンパイルエラーにしてくれます。
同じ名前のメソッドで、異なる関数型インタフェースを引数に持つのは、ユーザーにとって曖昧となるのでやめましょう。
例:ExecutorService.submit()
は、異なる関数インタフェース「Runnable
」と「Callable<T>
」を引数に持っているのでよくありません。
3.3. 標準の関数型インタフェースについての余談
各標準の関数型インタフェースについて以下の観点でまとめておきます。
- 従来の書き方
- 匿名クラスでの書き方
- lambda 式を使った書き方
- 呼び出し方
3.3.1. Function
- 従来の書き方
|
|
- 匿名クラスでの書き方
|
|
- lambda 式を使った書き方
|
|
- 呼び出し方
|
|
3.3.2. Consumer
- 従来の書き方
|
|
- 匿名クラスでの書き方
|
|
- lambda 式を使った書き方
|
|
- 呼び出し方
|
|
3.3.3. Supplier
- 従来の書き方
|
|
- 匿名クラスでの書き方
|
|
- lambda 式を使った書き方
|
|
- 呼び出し方
|
|
3.3.4. Predicate
偶奇判定を例に記載します。
- 従来の書き方
|
|
- 匿名クラスでの書き方
|
|
- lambda 式を使った書き方
|
|
- 呼び出し方
|
|
3.3.5. UnaryOperator
String を強調させるメソッドを例に記載します。
- 従来の書き方
|
|
- 匿名クラスでの書き方
|
|
- lambda 式を使った書き方
|
|
- 呼び出し方
|
|
3.3.6. BinaryOperator
足し算メソッドを例に記載します。
- 従来の書き方
|
|
- 匿名クラスでの書き方
|
|
- lambda 式を使った書き方
|
|
- 呼び出し方
|
|
4. 項目 45 ストリームを注意して使う
4.1. 結論
Stream 処理により簡単になる場合は、主に以下である。
いつStream
を使うべきかという、かっちりとしたルールはない。
- 単純な要素の変換
- 要素のフィルタリング処理
- 単純なオペレーションで要素を結びつける処理
- 要素を
Collection
に集約する処理 - 特定の基準を満たす要素を探す処理
次のようなことになるのであれば、使わないでおきましょう。
- Stream 処理ばかりで、複雑になる場合
- char 型の値の Stream 処理
- コードブロック内(
{...}
)で出来ることを捨ててまで、Stream 処理にしない。
4.2. Stream 処理ばかりで、複雑になる場合
4.2.1. 改善前
ファイル内の単語(行単位)を、アナグラムごとにまとめて、指定数以上含まれている場合は出力するプログラム。
アナグラム(anagram)とは、言葉遊びの一つで、単語または文の中の文字をいくつか入れ替えることによって、全く別の意味にさせる遊びである。Wikipedia
|
|
解説が必要なほど、複雑だと思うので、以下に記載します。。。(なんでもかんでもStream
処理が良いという訳ではないということですね。)
|
|
4.2.2. 改善後
ファイル内の単語(行単位)を、アナグラムごとにまとめて、指定数以上含まれている場合は出力するプログラム。
悪い例でややこしかった、文字列ソートの部分をalphabetize
メソッドに切り出しているので、わかりやすくなっていますね。
|
|
4.3. char 型の値の Stream 処理
char
型のStream
処理は直感的でない動きをしうるので、原則としてchar
型の値をStream
処理で扱うべきでない。
|
|
4.4. コードブロック内({...}
)で出来ることを捨ててまで、Stream 処理にしない。
関数型オブジェクトを使用したStream
処理で出来ず、コードブロック内({...}
)で出来ること。
- ローカル変数の使用(ラムダ式では
final
でないと使用できない。) - 内部メソッドの
return
, ループ内でのbreak
/continue
, 例外スロー(ラムダ式では出来ない。)
5. 項目 46 ストリームで副作用のない関数を選ぶ
5.1. 前提
Stream
の最も重要なところは、純粋関数のみによる演算 であること。
純粋関数とは、副作用を持たない関数のことである。副作用とは、変数の再代入や入出力などによってプログラムの状態が変化することを指します。
副作用があることによるデメリット
・全体の流れが分かりづらくなる可能性がある。
・速度が落ち、並列処理時の問題が出る可能性がある。
5.2. 結論
副作用を持たないようにするため、ストリーム処理は以下のようにする。
forEach
は、ストリームの演算の結果を示すためだけに利用する。Collectors
を利用する。
5.3. forEach
は、ストリームの演算の結果を示すためだけに利用する。
ファイルに含まれる単語の頻度を求めるプログラム。
以下は、ストリーム内で、外部の状態(freq
変数)を変更しているため(副作用を持つため)、良くない。
|
|
以下が改善例。
ストリーム内で、外部の状態(freq
変数)を変更しておらず、戻り値を代入している。
|
|
5.4. Collectors
を利用する。
ストリームパイプラインの可読性のために、Collectors のメンバーは static インポートしておくべき。
5.4.1. toSet(), toCollection(), toList()
toSet(), toCollection(), toList()
を利用した例。
|
|
5.4.2. toMap()
toSet(), toCollection(), toList()
以外は、ストリームを Map にするためのものがほとんどである。
toMap()
を使ったサンプルを載せる。
|
|
|
|
5.4.3. groupingBy()
groupingBy()
は、classifier
をもとに要素をカテゴリー分けした Map を生成する。
classifier
には、以下のサンプルのt -> t.length()
や 「項目 45 ストリームを注意して使う」で紹介したalphabetize(word)
が該当する。
|
|
groupingBy()
の 2 つ引数を取るシンプルな例としては以下。
|
|
5.4.4. その他のメソッド
downstream collector
としての利用に特化した以下のメソッドは、collect(counting())
のように利用してはいけない。
Stream
に同様の機能があるため。
- counting()
- 要素数を取得する。
- summing*()
- 入力値を型変換して、合算する。
- averaging*()
- 入力値を型変換して、平均値を算出する。 空ストリームの場合は 0 を返す。
- summarizing*()
- 入力値を型変換して、各種集計値を算出する。
- reducing()
- 値を集約する。
- filtering()
- 条件を満たす値だけ他のコレクターに渡す。
- mapping()
- 値を変換して他のコレクターを呼び出す。
- flatMapping()
- 値を Stream に変換して他のコレクターを呼び出す。
- collectingAndThen()
- 他のコレクターを呼び出した結果を変換する。
- minBy()
- 最小値を取得する。
- maxBy()
- 最大値を取得する。
- joining()
- 結合して String にする。
CollectorsやStreamの利用方法については、こちらのブログ(Java Collector, Java Stream)が入門として(大まかに把握するのに)良さそうである。
6. 項目 47 戻り値型として Stream よりも Collection を選ぶ
6.1. 結論
連続した要素をどの型で返すべきか?については、Collection一択
6.2. 選択肢にはなにがあるか
以下4択。
- ストリーム
- Iterable
- collectionインターフェース
- 配列
6.3. ストリームで返す
StreamはIterableを継承していない。 ストリームを処理する場合には、さらに加工が必要。
for-eachの場合は、Streamのiteratorメソッドを使うしかない。
以下のようにするとコンパイルできないので…
|
|
以下のような対処が必要。
|
|
これを簡素にするクラスをつくってもいいが、このようにユーザコード側が煩雑になることを理解する。
6.4. Iterable で返す
ストリームと同じなので割愛
6.5. collection で返す
CollectionインターフェースはIterableのサブタイプであり、ストリームのメソッドも持っているので、イテレーション処理でもストリーム処理でも対応できる。 そのため、連続した要素を返すメソッドの最適な戻り値型は、たいていの場合、Collectionとなる。
もし返却する連続要素が十分小さければ、ArrayListやHashSetなどのCollectionを実装したものを返せばいいのだが、Collectionとして返却するために、大きな連続要素をメモリに蓄えることはすべきでない(このときは、StreamやIteratorも可。JDBCのResultSetと同じといっても良い。)。
返却する連続要素が、大きいけれど簡潔に表現できるものであれば、特別にcollectionを実装してやることを考える。
6.6. 先輩からいただいたありがたいお言葉
そもそも、複数の値を返却するAPIが、Streamを返却するということに違和感があります。
たいていのAPIは、「XXXという条件のものを全部くれ」という言い方になるので、集合であることが普通です。
Streamだと、その名の通り「流れ」になるので、継続的に流れてくる(なので、前の要素には戻らない)、という意味がでてきます。
集合とは違う概念が混ざってくる認識です。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/stream/package-summary.html
を読んでみると違いが書いています。
7. 項目 48 ストリームを並列化するときは注意を払う
7.1. 結論
気軽に並列化できる点は素晴らしいのですが、その裏の仕組みが変わるわけでは有りません。 並列処理独特の考慮点がなくなるわけではないので、慎重に使いましょう。
7.2. 得意/不得意
7.2.1. 得意
要素1つで独立した操作ができるとき
- Collectionに対して処理する
- ArrayList、HashMap、HashSet、ConcurrentHashMap
- intやlongのrangeも同等
- リダクション操作
- min、max、count、sum
- 短絡評価
- anyMatch、allMatch、noneMatch
- filterも同等
7.2.2. 不得意
他の要素と関係を踏まえた操作のとき
- limit
- collectメソッドによって行われる可変リダクション操作(と書いてあるが、ケースバイケース)
7.2.3. 一例
|
|
paralellの有りなしで、↑のような話だと恩恵を受けやすい。
paralellなしだと3秒、paralellありだと1秒くらい(環境によって差がある)
7.2.4. 並列化すると起きる問題
ストリームだから起きるというわけではなくが、ストリームだから手軽にかけてしまい、ついつい起こしてしまいやすい、ことになる。
一言で言えば、副作用がない処理をする、なのだが、StreamのJavadocには重要なことが書いてある。
8. 参考 URL
- effective java 3rd まとめ
- Effective Java Third Edition 2 版からの更新点 個人的メモ
- Effective Java 第三版、ゲットだぜ!
- Java 関数型インターフェース
- Java メソッド参照・コンストラクター参照
- 【楽チン Java】メソッド参照での呼び出し方
- RIP Tutorial メソッド参照
- Java8 の Function, Consumer, Supplier, Predicate の調査
- [Java]メソッドの引数にメソッドを渡す方法[関数型インターフェース]
- Java8 のラムダ式を理解する
- An example of UnaryOperator in functional Lambda expressions
- Java8 Project Lambda (ラムダ式)
- Collectors.toMap で任意の Map クラスを返す
- Java 8 でグルーピング処理 - List
を Map<K, V> へ変換 - Java 8 | Collectors counting() with Examples
- Java Collector
- Java Stream
- GeeksforGeeks
- Java8 Stream のリダクション操作について
- 短絡評価って本当に処理が速いの? Java における&&と&の違い
- Prime Counting Function
- Stream API の主なメソッドと処理結果の Optional クラスの使い方 (3/4)