Effective Java 第3版を読んだので、本書で紹介されていた全90項目のプラクティスを簡潔にまとめる。
https://www.maruzen-publishing.co.jp/item/?book_no=303408

本記事は第7章「ラムダとストリーム」の項目について記載する。

第7章 ラムダとストリーム

項目42 無名クラスよりもラムダを選ぶ

従来では、関数オブジェクトを作成する主な手段として無名クラスを利用していた。

しかし、この手法は冗長であり現在では使われることがない。

そこで、ラムダ式を使い関数型インタフェースを生成することが推奨されている。

List<String> words = Arrays.asList("hogehoge", "huga");
Collections.sort(words,
        (s1, s2) -> Integer.compare(s1.length(), s2.length()));

System.out.println(words); // [huga, hogehoge]

仮引数の型は型推論されるため明示しなくても良いが、コンパイラが型を判断できない場合には明示するべきである。

ラムダによって項目34の定数固有メソッド実装をより簡潔に記載することができる。

public enum Operation {
    PLUS("+", (x, y) -> x + y),
    MINUS("-", (x, y) -> x - y);

    private final String symbol;
    private final IntBinaryOperator op; // intの結果を返す関数型インタフェース型
    
    Operation(String symbol, IntBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override
    public String toString() {
        return symbol; // デフォルトのtoStringは列挙子名を返却する。
    }

    public int apply(int x, int y) {
        return op.applyAsInt(x, y);
    }
}

この場合は、ラムダを使うことによって良い結果を得ることができるが、ラムダは名前とドキュメントの記載ができないので、計算が複雑で数行を超えるようなものであればラムダにするべきでない。

ラムダは1行が利用で、3行が許容できる最大である。

まとめると、ラムダは小さな関数オブジェクトを表現するのに最善である。

項目43 ラムダよりもメソッド参照を選ぶ

Javaには、メソッド参照というラムダよりも簡潔に関数オブジェクトを生成する方法がある。

以下のようなmapメソッド内に記載されたラムダ式で考える。

map(word -> Integer.parseInt(word))

このラムダ式をメソッドの参照に置き換えることで簡潔に記載することができる。

map(Integer::parseInt)

ラムダ式によっては仮引数が存在したほうが可読性が良くなることもあるので、その場合はラムダ式を採用すべき。

ただ、大抵の場合はメソッド参照にすることで短く明瞭なコードにすることができる。

項目44 標準の関数型インタフェースを使う

独自の関数型インタフェースではなく、標準の関数型インタフェースを使うべきである。

標準の関数型インタフェースは、有益なデフォルトメソッドを提供しているので、多くの相互運用の恩恵をもたらす。

java.util.Functionには、43個のインタフェースがあるが、6個の基本インタフェースを覚えておけば良い。

  • UnaryOperator
  • BinaryOperator
  • Predicate
  • Function<T,R>
  • Supplier
  • Consumer

6個の基本インタフェースには、引数の型がint, long, double型の3種類のバージョンがある。

また、Functionインタフェースには、戻り値型が基本データの場合に使うように9種類のバージョンがある。

そして、基本インタフェースは、それぞれ2個の引数を受け取るバージョンが9種類ある。

独自の関数型インタフェースを作成する場合

標準の関数型インタフェースでは仕様を満たせない場合、独自の関数型インタフェースを作成する必要がある。

  • 3つ以上の引数を必要とする関数
  • スローされる例外を独自のものにしたい
  • 特別なデフォルトメソッドから恩恵を受けられる
@FunctionlInterface // 独自の関数型インタフェースに付与する
public interface MyInterface<T> {
    public void hoge();
} 

項目45 ストリームを注意して使う

ストリームAPIは、2つの主要な抽象化を提供している。

  • ストリーム:データ要素の有限、無限なシーケンスを表す。
  • ストリームパイプライン: データ要素に対する複数ステージの計算を表す。ソースのストリーム、中間操作、終端操作から構成される。

ストリームは不適切に使うと悪影響がでる可能性があるため注意して利用する必要がある。

  • 乱用すると可読性と保守性が悪くなるので、極力乱用しないよう努める。
  • ラムダのパラメータに明示的な型が無いため、パラメータの命名が悪いと、ストリームパイプラインの可読性が悪くなる。
  • 複雑な処理を含めるときは、ヘルパーメソッドに処理を分離する。

項目46 ストリームで副作用のない関数を選ぶ

ストリームパイプラインの本質は、副作用のない関数を利用することである。

forEach操作は、計算を行わずに、ストリームによって行われた計算の結果を表示するために利用する。

代わりに、コレクターファクトリも適切に利用する必要がある。

  • toSet
  • toMap
  • groupingBy
  • joining

項目47 戻り値としてStreamよりもCollectionを選ぶ

要素のシーケンスを返却するメソッドを実装する場合、クライアント側で利用するケースを考慮して、戻り値を選択する必要がある。

ストリームパイプラインでしか利用しない場合、ストリームを返却すべき。

ループでしか利用しない場合、Iterableを返却すべき。

ただし、クライアントが柔軟に取り扱いできるためにも、両方に備えるのが最善の選択だ。

IterableのサブタイプであるCollectionインタフェースは、ループアクセスとストリームアクセスの両方に備えているため、戻り値型としてCollectionが適切であると言える。

将来的に、Streamインタフェースの宣言がIterableを拡張するようになった場合、ストリームを返却することも考慮する。

項目48 ストリームを並列化するときは注意を払う

見境なくストリームパイプラインを並列化することで、かえってパフォーマンスが低下することがある。