字句スコープとクロージャの使用
多くの開発者はこの誤解を抱いており、ラムダ式を使用するとコードの冗長性が生じ、コードの品質が低下すると信じています。逆に、コードがどれほど複雑になっても、以下で説明するように、簡素化のためにコードの品質に妥協はしません。
前の例ではラムダ式を再利用できましたが、別の文字と一致すると、すぐにコードの冗長性の問題が再発します。まずこの問題をさらに分析してから、字句スコープとクロージャを使用して解決しましょう。
ラムダ式による冗長性
友人からの N または B で始まる文字をフィルターして除外しましょう。上記の例を続けると、作成するコードは次のようになります。
次のようにコードをコピーします。
Final Predicate<String> startsWithN = name -> name.startsWith("N");
Final Predicate<String> startsWithB = name -> name.startsWith("B");
最終的な長いカウントFriendsStartN =
friends.stream()
.filter(startsWithN).count();
最終的な長いカウントFriendsStartB =
friends.stream()
.filter(startsWithB).count();
最初の述語は名前が N で始まるかどうかを決定し、2 番目の述語は名前が B で始まるかどうかを決定します。これら 2 つのインスタンスをそれぞれ 2 つのフィルター メソッド呼び出しに渡します。これは合理的であるように思えますが、2 つの述語は冗長であり、チェック内の文字が異なるだけです。この冗長性を回避する方法を見てみましょう。
冗長性を避けるために語彙スコープを使用する
最初の解決策では、文字を関数のパラメーターとして抽出し、この関数をフィルター メソッドに渡すことができます。これは良い方法ですが、フィルターはすべての関数で受け入れられるわけではありません。パラメーターが 1 つだけある関数のみを受け入れます。そのパラメーターはコレクション内の要素に対応し、渡されるものが述語であることを望みます。
パラメータ (この場合は name パラメータ) が渡されるまで、この文字をキャッシュできる場所があれば幸いです。このような新しい関数を作成してみましょう。
次のようにコードをコピーします。
public static Predicate<String> checkIfStartsWith(最後の文字列文字) {
名前を返します -> name.startsWith(letter);
}
静的関数 checkIfStartsWith を定義しました。この関数は String パラメーターを受け取り、Predicate オブジェクトを返します。このオブジェクトは、後で使用するためにフィルター メソッドに渡すことができます。関数をパラメータとして受け取る前に見た高階関数とは異なり、このメソッドは関数を返します。しかし、これは高次関数でもあり、12 ページの「変化ではなく進化」ですでに述べました。
checkIfStartsWith メソッドによって返される Predicate オブジェクトは、他のラムダ式とは多少異なります。 return name -> name.startsWith(letter) ステートメントでは、name が何であるかを正確に知っており、それはラムダ式に渡されるパラメータです。しかし、変数文字とは正確には何でしょうか?これは匿名関数のドメインの外側にあり、Java はラムダ式が定義されているドメインを見つけて変数文字を検出します。これは字句スコープと呼ばれます。字句スコープは非常に便利なもので、変数を 1 つのスコープにキャッシュして、後で別のコンテキストで使用できるようにします。このラムダ式はスコープ内で変数を使用するため、この状況はクロージャとも呼ばれます。字句範囲のアクセス制限についてですが、31ページの字句範囲の制限を読んでいただけますでしょうか。
語彙範囲に制限はありますか?
ラムダ式では、スコープ内の Final 型、または実際には Final 型のローカル変数にのみアクセスできます。
ラムダ式は、すぐに呼び出すことも、遅延して呼び出すことも、別のスレッドから呼び出すこともできます。競合の競合を避けるために、アクセスするドメイン内のローカル変数は、初期化後に変更することはできません。変更操作を行うとコンパイル例外が発生します。
これを「final」としてマークするとこの問題は解決しますが、Java はこのようにマークすることを強制しません。実際、Java は 2 つのことを考慮しています。 1 つは、アクセスされる変数は、ラムダ式が定義される前に、その変数が定義されているメソッド内で初期化される必要があるということです。第 2 に、これらの変数の値は変更できません。つまり、変数は、そのようにマークされていませんが、実際には Final 型です。
ステートレス ラムダ式は実行時定数ですが、ローカル変数を使用するラムダ式には追加の計算オーバーヘッドがかかります。
filter メソッドを呼び出すときは、次のように checkIfStartsWith メソッドによって返されるラムダ式を使用できます。
次のようにコードをコピーします。
最終的な長いカウントFriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
最終的な長いカウントFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
filter メソッドを呼び出す前に、まず checkIfStartsWith() メソッドを呼び出し、必要な文字を渡します。この呼び出しによりすぐにラムダ式が返され、それをフィルター メソッドに渡します。
高階関数 (この場合は checkIfStartsWith) を作成し、字句スコープを使用することで、コードから冗長性を取り除くことに成功しました。名前が特定の文字で始まるかどうかを繰り返し判断する必要はなくなりました。
リファクタリング、範囲の縮小
前の例では静的メソッドを使用しましたが、コードを混乱させる可能性があるため、変数をキャッシュするために静的メソッドを使用したくありません。この関数の範囲を、それが使用される場所に限定することが最善です。これを実現するには、Function インターフェイスを使用します。
次のようにコードをコピーします。
Final Function<String, Predicate<String>> startsWithLetter = (文字列文字) -> {
Predicate<String> checkStarts = (文字列名) -> name.startsWith(letter);
チェックスタートを返します。
このラムダ式は、元の静的メソッドを置き換えることができ、必要になる前に関数内に配置して定義できます。 startWithLetter 変数は、入力パラメータが String で出力パラメータが Predicate である関数を参照します。
静的メソッドを使用する場合と比較して、このバージョンははるかに単純ですが、引き続きリファクタリングして、より簡潔にすることができます。実際の観点から見ると、この関数は前の静的メソッドと同じであり、両方とも文字列を受け取り、述語を返します。 Predicate を明示的に宣言する代わりに、それを完全にラムダ式に置き換えます。
次のようにコードをコピーします。
Final Function<String, Predicate<String>> startsWithLetter = (文字列文字) -> (文字列名) -> name.startsWith(文字);
混乱は解消されましたが、型宣言を削除してより簡潔にすることもできます。Java コンパイラーはコンテキストに基づいて型推定を行います。改良版を見てみましょう。
次のようにコードをコピーします。
Final Function<String, Predicate<String>> startsWithLetter =
文字 -> 名前 -> 名前.startsWith(letter);
この簡潔な構文に適応するには、ある程度の努力が必要です。目が見えなくなる場合は、まず他の場所を探してください。コードのリファクタリングが完了したので、次のようにそれを使用して元の checkIfStartsWith() メソッドを置き換えることができます。
次のようにコードをコピーします。
最終的な長い countFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
最終的な長いカウントFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
このセクションでは、高階関数を使用します。関数を別の関数に渡す場合に関数内で関数を作成する方法と、関数から関数を返す方法について説明しました。これらの例はすべて、ラムダ式によってもたらされるシンプルさと再利用性を示しています。
ここではFunctionとPredicateの機能を使いこなしてきましたが、両者の違いを見てみましょう。 Predicate は T 型のパラメータを受け取り、対応する判定条件の真または偽を表すブール値を返します。条件付きの判断を行う必要がある場合は、Predicateg を使用してそれを完了できます。 filter のようなメソッドは、フィルター要素が Predicate をパラメーターとして受け取ります。 Funciton は、入力パラメータが T 型の変数である関数を表し、R 型の結果を返します。ブール値のみを返すことができる述語よりも一般的です。入力が出力に変換される限り、Function を使用できるため、map で Function をパラメーターとして使用するのは合理的です。
ご覧のとおり、コレクションから要素を選択するのは非常に簡単です。以下では、コレクションから要素を 1 つだけ選択する方法を紹介します。