ラムダ式と機能的インタフェース:ヒントとベストプラクティス
1概要
Java 8が幅広い使用法、パターン、およびベストプラクティスに到達した今、いくつかの見出し機能のために出現し始めました。このチュートリアルでは、関数型インタフェースとラムダ式を詳しく見ていきます。
2標準機能インターフェースを優先
java.util.function
** パッケージに集められた機能インタフェースラムダ式とメソッド参照用のターゲット型を提供するという開発者のニーズ。これらの各インタフェースは一般的で抽象的なので、ほとんどすべてのラムダ式に簡単に適応できます。開発者は、新しい機能インターフェイスを作成する前にこのパッケージを調べてください。
インターフェース
Foo
を考えます。
@FunctionalInterface
public interface Foo {
String method(String string);
}
このインタフェースをパラメータとする
UseFoo
というクラスの__add()メソッド
public String add(String string, Foo foo) {
return foo.method(string);
}
実行するには、次のように書きます。
Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);
よく見ると、
Foo
は1つの引数を受け入れて結果を生成する関数にすぎません。 Java 8はすでにhttps://の
Function <T、R>
にそのようなインタフェースを提供していますdocs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html[java.util.function]パッケージ。
これで、インターフェイス
Foo
を完全に削除し、コードを次のように変更できます。
public String add(String string, Function<String, String> fn) {
return fn.apply(string);
}
これを実行するために、我々は書くことができます:
Function<String, String> fn =
parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", fn);
3
@ FunctionalInterface
アノテーションを使用する
機能インタフェースに
httpsで注釈を付けます。//docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html[@FunctionalInterface].
最初は、この注釈は役に立ちません。それがなくても、あなたのインターフェースはただ一つの抽象メソッドを持つ限り機能的として扱われるでしょう。
しかし、いくつかのインターフェイスを持つ大きなプロジェクトを想像してみてください。すべてを手動で制御するのは難しいです。機能的になるように設計されたインターフェースは、他の抽象メソッドを追加することによって誤って変更され、機能的インターフェースとして使用できなくなる可能性があります。
しかし、
@ FunctionalInterface
アノテーションを使用すると、コンパイラーは、機能インターフェースの事前定義構造を壊そうとする試みに応じてエラーを引き起こします。他の開発者にとってアプリケーションアーキテクチャを理解しやすくするための非常に便利なツールでもあります。
だから、これを使用してください:
@FunctionalInterface
public interface Foo {
String method();
}
ただの代わりに:
public interface Foo {
String method();
}
4関数型インタフェースでデフォルトのメソッドを使いすぎないように
機能インターフェースにデフォルトのメソッドを簡単に追加できます。これは、抽象メソッド宣言が1つだけであれば、機能インターフェース規約では受け入れられます。
@FunctionalInterface
public interface Foo {
String method();
default void defaultMethod() {}
}
抽象メソッドが同じシグネチャを持つ場合、機能インターフェースは他の機能インターフェースによって拡張することができます。例えば:
@FunctionalInterface
public interface FooExtended extends Baz, Bar {}
@FunctionalInterface
public interface Baz {
String method();
default void defaultBaz() {}
}
@FunctionalInterface
public interface Bar {
String method();
default void defaultBar() {}
}
通常のインターフェースと同じように、同じデフォルトの方法で異なる機能インターフェースを拡張することは問題となる可能性があります。たとえば、インタフェース
Bar
と
Baz
の両方にデフォルトのメソッド__defaultCommon()があるとします。この場合、コンパイル時エラーが発生します。
interface Foo inherits unrelated defaults for defaultCommon() from types Baz and Bar...
これを修正するには、
defaultCommon()メソッドを
Foo
インターフェースでオーバーライドする必要があります。もちろん、このメソッドのカスタム実装を提供することもできます。しかし、(
Baz
インターフェースなどから)親インターフェースの実装の1つを使用したい場合は、
defaultCommon()__メソッドの本体に次のコード行を追加します。
Baz.super.defaultCommon();
しかし注意してください。 ** あまりにも多くのデフォルトメソッドをインタフェースに追加することは、あまり良いアーキテクチャ上の決定ではありません。
5ラムダ式を使用して関数型インタフェースをインスタンス化する
コンパイラーは、内部クラスを使用して機能インターフェースをインスタンス化することを可能にします。ただし、これは非常に冗長なコードにつながる可能性があります。あなたはラムダ式を好むべきです:
Foo foo = parameter -> parameter + " from Foo";
内部クラスを超えて:
Foo fooByIC = new Foo() {
@Override
public String method(String string) {
return string + " from Foo";
}
};
-
ラムダ式アプローチは、古いライブラリの適切なインタフェースに使用できます。
ただし、これは
古いコードベース全体を見直してすべてを変更する必要があるという意味ではありません。
6. 関数型インタフェースをパラメータとするメソッドのオーバーロードを避ける
衝突を避けるために異なる名前のメソッドを使用してください。例を見てみましょう。
public interface Adder {
String add(Function<String, String> f);
void add(Consumer<Integer> f);
}
public class AdderImpl implements Adder {
@Override
public String add(Function<String, String> f) {
return f.apply("Something ");
}
@Override
public void add(Consumer<Integer> f) {}
}
一見すると、これは妥当なようです。しかし、__AdderImplのメソッドを実行しようとした場合
String r = adderImpl.add(a -> a + " from lambda");
次のメッセージでエラーで終了します。
reference to add is ambiguous both method
add(java.util.function.Function<java.lang.String,java.lang.String>)
in fiandlambdas.AdderImpl and method
add(java.util.function.Consumer<java.lang.Integer>)
in fiandlambdas.AdderImpl match
この問題を解決するには、2つの選択肢があります。
最初の
は異なる名前のメソッドを使うことです。
String addWithFunction(Function<String, String> f);
void addWithConsumer(Consumer<Integer> f);
-
2番目の** は手動でキャストを実行することです。これは好ましくありません。
String r = Adder.add((Function) a -> a + " from lambda");
7. ラムダ式を内部クラスとして扱わないでください
前の例ではあるが、本質的に内部クラスをラムダ式で置き換えていましたが、2つの概念は重要な点で異なります。それはscopeです。
内部クラスを使用すると、新しいスコープが作成されます。同じ名前の新しいローカル変数をインスタンス化することで、囲んでいるスコープからローカル変数を上書きできます。インスタンスへの参照として、内部クラス内でキーワード
this
を使用することもできます。
ただし、ラムダ式は包含スコープで機能します。ラムダの本体の内側にあるスコープから変数を上書きすることはできません。
この場合、キーワード
this
は囲んでいるインスタンスへの参照です。
たとえば、
UseFoo
クラスにはインスタンス変数__valueがあります。
private String value = "Enclosing scope value";
次に、このクラスのメソッドに次のコードを配置してこのメソッドを実行します。
public String scopeExperiment() {
Foo fooIC = new Foo() {
String value = "Inner class value";
@Override
public String method(String string) {
return this.value;
}
};
String resultIC = fooIC.method("");
Foo fooLambda = parameter -> {
String value = "Lambda value";
return this.value;
};
String resultLambda = fooLambda.method("");
return "Results: resultIC = " + resultIC +
", resultLambda = " + resultLambda;
}
scopeExperiment()
メソッドを実行すると、次のような結果になります。
ご覧のとおり、ICで
this.value
を呼び出すことで、そのインスタンスからローカル変数にアクセスできます。しかし、ラムダの場合、
this.value
callは
UseFoo
クラスで定義されている変数
value
へのアクセスを許可しますが、ラムダの本体内で定義された変数
value
へのアクセスは許可しません。
8ラムダ式を短く、わかりやすくしてください
可能であれば、大きなコードブロックの代わりに1行構成を使用してください。
ラムダは物語ではなく
表現であるべきであることを忘れないでください。
その簡潔な構文にもかかわらず、** ラムダはそれらが提供する機能を正確に表現するべきです。
パフォーマンスはそれほど変わらないので、これは主に文体的なアドバイスです。ただし、一般的に、そのようなコードを理解して使用する方がはるかに簡単です。
これはさまざまな方法で実現できます。詳しく見てみましょう。
8.1. Lambda’s Body
内のコードブロックを回避する
理想的な状況では、ラムダは1行のコードで書かれるべきです。
このアプローチでは、ラムダは自明の構成であり、どのアクションをどのデータで実行するかを宣言します(パラメータ付きラムダの場合)。
大きなコードブロックがある場合、ラムダの機能はすぐにはわかりません。
これを念頭に置いて、次の操作を行います。
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
String result = "Something " + parameter;
//many lines of code
return result;
}
の代わりに:
Foo foo = parameter -> { String result = "Something " + parameter;
//many lines of code
return result;
};
-
ただし、この「1行のラムダ」ルールを教義として使用しないでください** 。ラムダの定義に2〜3行ある場合は、そのコードを別のメソッドに抽出しても意味がありません。
** 8.2. パラメータタイプの指定を避ける
ほとんどの場合、コンパイラは
型推論
を使用して、ラムダパラメータの型を解決できます。したがって、パラメータに型を追加することはオプションであり、省略することができます。
これを行う:
(a, b) -> a.toLowerCase() + b.toLowerCase();
これの代わりに:
(String a, String b) -> a.toLowerCase() + b.toLowerCase();
8.3. 単一のパラメータを括弧で囲まないでください
Lambda構文では、複数のパラメータを囲む場合、またはパラメータがまったくない場合にのみ括弧が必要です。そのため、コードが少し短くなり、パラメータが1つしかない場合は括弧を除外しても安全です。
だから、これをしなさい:
a -> a.toLowerCase();
これの代わりに:
(a) -> a.toLowerCase();
8.4. リターンステートメントとブレースを避ける
-
中括弧
および
return ** ステートメントは、1行のラムダ本体ではオプションです。これは、明確さと簡潔さのためにそれらを省略できることを意味します。
これを行う:
a -> a.toLowerCase();
これの代わりに:
a -> {return a.toLowerCase()};
8.5. メソッド参照の使用
非常に多くの場合、以前の例でも、ラムダ式はすでに他の場所で実装されているメソッドを呼び出すだけです。この状況では、別のJava 8機能を使用することが非常に役立ちます。
-
メソッド参照
** 。
だから、ラムダ式:
a -> a.toLowerCase();
次のように置き換えられます。
String::toLowerCase;
これは必ずしも短いわけではありませんが、コードを読みやすくします。
9 「実質的に最終的な」変数を使用する
ラムダ式の中で最後以外の変数にアクセスすると、コンパイル時エラーが発生します。
しかし、すべてのターゲット変数を
final .
としてマークする必要があるという意味ではありません。
「
事実上の最後
」の概念に従って、コンパイラは、割り当てられている限り、すべての変数を__finalとして扱います一度。
これらの変数をラムダ内で使用するのは安全です。なぜなら、コンパイラはそれらの状態を制御し、それらを変更しようとした直後にコンパイル時エラーを引き起こすからです。
たとえば、次のコードはコンパイルされません。
public void method() {
String localVariable = "Local";
Foo foo = parameter -> {
String localVariable = parameter;
return localVariable;
};
}
コンパイラは次のことを通知します。
Variable 'localVariable' is already defined in the scope.
このアプローチは、ラムダ実行をスレッドセーフにするプロセスを単純化するはずです。
10オブジェクト変数を突然変異から保護する
ラムダの主な目的の1つは、並列コンピューティングでの使用です。つまり、スレッドセーフティに関して言えば、それらは本当に役に立ちます。
「効果的に最終的な」パラダイムはここでは大いに役立ちますが、すべての場合ではありません。ラムダはオブジェクトの値を包含スコープから変更することはできません。
しかし、可変オブジェクト変数の場合、状態はラムダ式の中で変更される可能性があります。
次のコードを見てください。
int[]total = new int[1];
Runnable r = () -> total[0]++;
r.run();
total
変数は「事実上最終」のままなので、このコードは正当です。しかし、参照するオブジェクトはラムダの実行後も同じ状態になりますか?いいえ!
予期しない突然変異を引き起こす可能性のあるコードを避けるために、この例を忘れないでください。
11結論
このチュートリアルでは、Java 8のラムダ式と機能的インタフェースのベストプラクティスと落とし穴について説明しました。これらの新機能の有用性と力にもかかわらず、それらは単なるツールです。すべての開発者はそれらを使用しながら注意を払うべきです。
例の完全な
ソースコード
はhttps://github.com/eugenp/tutorials/tree/master/core-java-8[このGitHubプロジェクト]にあります – これはMavenとEclipseのプロジェクトです。そのままインポートして使用できます。