Java
StreamAPI

Stream APIと関数インターフェースでカジュアルに宣言型プログラミング

想定読者層

  • Javaエンジニア 
  • Streamをあまり触ったことがない
  • 関数型ってよくわかんない
  • 宣言?煎餅とどっちが美味しいの?

下記について話します

  • 宣言型の側面から...
  • Stream API
  • 関数インターフェース
  • 宣言型プログラミングの考え方の基礎の基礎

下記については話しません

  • 設計・アーキテクチャ
    • イケてなくても直せないことあるじゃん...
    • カジュアルに導入できる部分で闘う

Stream API


そもそもStream APIってなに

  • リスト操作のAPI
  • for文のシンタックスシュガーではない
  • 並列処理を手軽に行える
  • 構文自体が宣言的な文法になっている(これ)
    • ただの「リスト操作API」として使うと勿体無い

Stream APIってどうやって使うの

  • Listから生成したStreamに対して、データを加工する関数を与えていきます
  • 文法が宣言的なので、そのまま言葉として読み上げられます
  • 例を見てみます

例1: 高身長を俺より小さくする処理

personList.stream()
.filter(p -> p.getHeight() >= 180)
.foreach(p -> p.setHeight(156));

人物のリストから,
身長180cm以上の人物を抽出して,
それぞれの身長を156cmにする


例2: ロックマンを買ったメンバ名を抽出

members.stream()
.filter(m -> m.hasMegaman11)
.map(m -> m.getName)
.collect(Collectors.toList)

メンバー一覧から,
ロックマン11を持っているメンバを抽出して,
メンバーオブジェクトから名前に加工して
リストを生成する


関数インターフェース


そもそも関数IFってなに

  • メソッドを一つだけ定義したInterface
  • ラムダ式で簡単に実装できる
  • 正体は匿名クラスのシンタックスシュガー
    • 引数以外の外部変数はfinalしか利用できない
    • = 関数IFはクロージャではありません

関数IFってどうつかうの

  • ラムダ式で実装します
  • メソッド内で使ったり
    • 擬似クロージャみたいに使える
    • 説明変数とかにも便利な気もする
  • 高階関数(メソッド)に渡したりする
    • Streamはこれです
  • 4つの標準関数IFを見てみます

例1: int型の引数を倍にして返す

// Function<引数の型,戻り値の型>
Function<Integer,Integer> dbl = (i) -> i*2;
int result = dbl.apply(2); // 4

例2: 奇数の場合にtrueを返す

Predicate<Integer> isOdd = (i) -> i % 2 == 1;
isOdd.test(1); // true
isOdd.test(2); // false

例3: hydeの身長をセットする

// Consumerは値を返さない
Consumer<AtomicInteger> setHydeHeight = (i) -> i.set(156);
AtomicInteger myHeight = new AtomicInteger(178);
setHydeHeight.accept(myHeight); // hydeの身長は156cm

例4: 引数なしで値を返す関数IF

// Supplierは引数を受け取らない
final int value = 10;
Supplier<Integer> getValue = () -> value * 3;
int num = getValue.get();

で、結局なにができるの?


コードを宣言的に記述できます


宣言的って何だよ


例で理解する宣言型と命令型

  • keyとvalueを持つlistから、値が5以上のデータを取得する
  • 宣言型の例: SQL
    select * from list where value > 5;
  • 命令型の例: Java
List<Data> result = new ArrayList();
for(Data data: list){
  if(data.value > 5) result.add(data);
}

全エンジニアが実現したいこと

  • 業務ロジックをそのままコードにしたい
  • コードを読んで業務ロジックを理解したい
  • ストリームと関数型で近づけるんじゃね?

StreamAPI + 関数IFで宣言的に

  • 例1: 販売可能な食料品一覧の作成
  • 食料品情報の全量が渡される
  • 下記を満たす場合に販売可能とする
    • 賞味期限が当日以降
    • 商品種別ごとに販売可能な条件が異なります
    • 通常商品の場合: 販売可能フラグがtrue
    • 季節商品の場合: 季節内フラグがtrue
    • その他の商品種別: 販売不可

例1: 販売可能な食料品一覧の作成

// 実装をそのまま言葉にします。
// 商品種別ごとの販売可能条件を定義する
// 商品種別が通常商品なら販売可能フラグを条件とする。
// 商品種別が季節商品なら季節内フラグを条件とする。それ以外はfalseを返す。
Predicate<Item> filterEachItemType = 
  i -> i.type == NORMAL ? i.isSalable
     : i.type == SEASON ? i.isInSeason 
     : false;  

// 食料品情報の全量から期限日が当日以降のものを抽出
// 更に商品種別ごとの販売可能条件で抽出する
// 抽出されたデータを販売可能商品リストとする
List<Item> salableList = 
  itemList.stream().filter(i -> i.expire >= today)
                   .filter(filterEachItemType)
                   .collector(Collectors.toList());

例1の命令型バージョン

// 実装をそのまま言葉にします。
// 販売可能商品リストを宣言
// 商品ごとに下記を繰り返し行う
// 賞味期限が当日以前ならスキップ
// 商品種別が通常商品かつ販売可能フラグがtrueなら販売可能商品リストに追加
// 商品種別が季節商品かつ季節内フラグがtrueなら販売可能商品リストに追加
List<Item> salableList = new ArrayList<>();
for(Item item: itemList){
  if(item.expire < today) continue;
  if(item.type == NORMAL && item.isSalable){
    salableList.add(item);
  }else if(item.type == SEASON && item.isInSeason){
    salableList.add(item);
  }
}

メリットまとめ

  • 業務仕様と設計と実装が一致する
    • 仕様バグを減らせる
    • 自然な実装になるので変更,機能追加が容易
    • DDDに繋がっていきます
  • コードから状態が排除される
    • (カウンタやフラグ,一時的な変数など)
    • 業務ロジックに集中できる

とりあえず目指したいこと

  • 完璧な宣言型,関数型は目指さなくてよい
  • ポリシー等は決めず、「コードで仕様を表現する」意識から始めてみる
    • 「形だけ」は絶対に避けたい
    • 詳しい人がちゃんとレビューする
  • その武器としてのStreamや関数IF
  • 「コードで仕様を表現する」でいえば、単純にメソッドを分けて実装を隠蔽するのもいいしね

以上