『Javaによる関数型プログラミング』をさらっと読んだ
前半はJavaのラムダ式とStream APIの概要と使い方)、後半は遅延評価・再帰処理・最適化・関数合成についてJavaで紹介してくれる本。 自分がプログラムを書き始めた時には、既にJavaでラムダ式とStream APIが導入されていた。気づけばラムダ式とStream APIを使わずにfor文を使っているコードの方が気持ち悪いなと思うようにくらいには経験を積んだようなので、復習と新たな知見を得るためにさらっと読んだ。
4章の『ラムダ式で設計する』で、ラムダ式で主要な関心を分離するところのテクニックが現場でもよく使えそうで勉強になった(恥ずかしながら本のリファクタリング前のようなメソッドを書いてしまっていた)。JavaScriptで関数を引数で渡して再利用性を高めるテクニックがJava側でも使えることを再認識した。
ラムダ式で主要な関心を分離するサンプルコード
Assetのリストのvalueの合計値を計算するメソッドを実装したい場面を想定
public class Asset { public enum AssetType { BOND, STOCK }; private final AssetType type; private final int value; public Asset(final AssetType assetType, final int asserValue) { this.type = assetType; this.value = asserValue; } public AssetType getType() { return type; } public int getValue() { return value; } } public class AssetUtil { // NOTE: 慣れていないとそれぞれのAssetのtypeごとに合計するメソッドを書いてしまいそう。 // NOTE: fileterに使う関数を引数で渡すことで再利用性を高めることができる public static int totalAssetValues(final List<Asset> assets, final Predicate<Asset> assetSelector) { return assets.stream() .filter(assetSelector) .mapToInt(Asset::getValue) .sum(); } // NOTE: 頻繁に使われるセレクタであれば、Utilに定義しておいて使えるようにしておいてもいいかもしれない? public static Predicate<Asset> ALL_SELECTOR = asset -> true; public static Predicate<Asset> BOND_SELECTOR = asset -> asset.getType() == Asset.AssetType.BOND; public static Predicate<Asset> STOCK_SELECTOR = asset -> asset.getType() == Asset.AssetType.STOCK; }
テストコードはこんな感じ
class AssetUtilTest { final List<Asset> assets = Arrays.asList( new Asset(Asset.AssetType.BOND, 1000), new Asset(Asset.AssetType.BOND, 2000), new Asset(Asset.AssetType.STOCK, 3000), new Asset(Asset.AssetType.STOCK, 4000) ); @Test void assetsの中でassetTypeがBondのものが合計され3_000となること() { int res = AssetUtil.totalAssetValues(assets, asset -> asset.getType() == Asset.AssetType.BOND); assertEquals(3_000, res); } @Test void assetsの中でassetTypeがStockのものが合計され7_000となること() { int res = AssetUtil.totalAssetValues(assets, asset -> asset.getType() == Asset.AssetType.STOCK); assertEquals(7_000, res); } @Test void assetsの全てが合計され10_000となること() { int res = AssetUtil.totalAssetValues(assets, asset -> true); assertEquals(10_000, res); } }
6章の『「遅延させる」ということ』でStreamのメソッド評価順が自分の想像と違ってfor文で書き下すより効率的であることを知ってへ〜となった。 下記のようなコードだと、
1. リストの要素を大文字化 2. リストの要素をチェックして3文字のものを探す 3. リストの最初の要素を返却する
という動きをすると思っていたが、実際の動きは
1. `Brad`を大文字にして、3文字かチェック 2. `Kate`を大文字にして、3文字かチェック 3. `Kim`を大文字にして、3文字かチェック 4. `KIM`を返す
というかなり効率のいい挙動となっていた。 for文を使って素直にこの処理を書こうとすると3回for文を回す処理を書くことになるが、Streamを使うと裏で色々頑張ってくれてかなり効率の良い処理になることが分かった1。
# NOTE: サンプルコードを適当に改変したもの public static void main(String[ ] args) { List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike", "Susan"); Optional<String> a = names.stream() .map(name -> { System.out.println("toUppercase(" + name + ")"); return name.toUpperCase(); }) .filter(name -> { System.out.println("length(" + name + ")"); return name.length() == 3; }) .findFirst(); }
後半の章については、再帰処理の最適化(末尾再帰やメモ化)やparalellStream()
を使うと並列化でき実行時間が短くなる可能性があるといったようなことが書かれていた。
(余談)本書を読んでいて、Utilクラスに実装されたstaticメソッドや定数に対してimport static
を使えば、{クラス名}.{メソッド名}
や{クラス名}.{定数名}
ではなく、{メソッド名}
や{定数名}
だけで済み、各コードの行数が減らせることに今更気づいた。ユニットテストでは当たり前のようにimport static
を使っていたので、なんで気づかなかったのだろうという感じ。ただし、メソッド名や定数名がクラス名なしで明白なもの以外に使うと混乱を招きそう。身近なものだとMath.PI
とかはPI
単独で使っても大丈夫かなと思った。
- 簡潔に書けるからという以外の理由でも、この書き方しましょうと言えるような気がする。↩