ラムダ式と関数型インターフェース:ヒントとベストプラクティス

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は、java.util.functionパッケージのFunctionでそのようなインターフェースをすでに提供しています。

これで、インターフェイスFooを完全に削除し、コードを次のように変更できます。

public String add(String string, Function fn) { return fn.apply(string); }

これを実行するには、次のように記述します。

Function fn = parameter -> parameter + " from lambda"; String result = useFoo.add("Message ", fn);

3. @ FunctionalInterfaceアノテーションを使用します

@FunctionalInterfaceで関数型インターフェースに注釈を付けます最初は、この注釈は役に立たないようです。それがなくても、抽象メソッドが1つしかない限り、インターフェイスは機能するものとして扱われます。

しかし、いくつかのインターフェースを備えた大きなプロジェクトを想像してみてください。すべてを手動で制御するのは困難です。機能するように設計されたインターフェイスは、他の抽象的なメソッドを追加することによって誤って変更され、機能インターフェイスとして使用できなくなる可能性があります。

ただし、@ FunctionalInterfaceアノテーションを使用すると、コンパイラは、機能インターフェイスの事前定義された構造を破壊しようとすると、エラーをトリガーします。また、他の開発者がアプリケーションアーキテクチャを理解しやすくするための非常に便利なツールです。

だから、これを使用してください:

@FunctionalInterface public interface Foo { String method(); }

ただの代わりに:

public interface Foo { String method(); }

4.機能インターフェイスでデフォルトのメソッドを使いすぎないでください

機能インターフェイスにデフォルトのメソッドを簡単に追加できます。これは、抽象メソッド宣言が1つしかない限り、機能インターフェイスコントラクトに受け入れられます。

@FunctionalInterface public interface Foo { String method(String string); default void defaultMethod() {} }

関数型インターフェースは、それらの抽象メソッドが同じ署名を持っている場合、他の関数型インターフェースによって拡張できます。

例えば:

@FunctionalInterface public interface FooExtended extends Baz, Bar {} @FunctionalInterface public interface Baz { String method(String string); default String defaultBaz() {} } @FunctionalInterface public interface Bar { String method(String string); default String defaultBar() {} }

通常のインターフェースと同様に、同じデフォルトの方法で異なる機能インターフェースを拡張することは問題になる可能性があります。

たとえば、defaultCommon()メソッドをBarインターフェイスとBazインターフェイスに追加しましょう。

@FunctionalInterface public interface Baz { String method(String string); default String defaultBaz() {} default String defaultCommon(){} } @FunctionalInterface public interface Bar { String method(String string); default String defaultBar() {} default String defaultCommon() {} }

この場合、コンパイル時エラーが発生します。

interface FooExtended inherits unrelated defaults for defaultCommon() from types Baz and Bar...

これを修正するには、FooExtendedインターフェイスでdefaultCommon ()メソッドをオーバーライドする必要があります。もちろん、このメソッドのカスタム実装を提供することもできます。ただし、親インターフェイスから実装を再利用することもできます

@FunctionalInterface public interface FooExtended extends Baz, Bar { @Override default String defaultCommon() { return Bar.super.defaultCommon(); } }

しかし、注意する必要があります。インターフェイスにデフォルトのメソッドを追加しすぎることは、アーキテクチャ上の決定としてはあまり適切ではありません。これは、下位互換性を損なうことなく既存のインターフェイスをアップグレードするために必要な場合にのみ使用される妥協案と見なす必要があります。

5.ラムダ式を使用して関数型インターフェースをインスタンス化する

コンパイラを使用すると、内部クラスを使用して関数型インターフェイスをインスタンス化できます。ただし、これは非常に冗長なコードにつながる可能性があります。ラムダ式をお勧めします:

Foo foo = parameter -> parameter + " from Foo";

内部クラス上:

Foo fooByIC = new Foo() { @Override public String method(String string) { return string + " from Foo"; } }; 

ラムダ式アプローチは、古いライブラリの適切なインターフェイスに使用できます。RunnableComparatorなどのインターフェースに使用できます。ただし、これ古いコードベース全体を確認してすべてを変更する必要があるという意味ではありません。

6.パラメータとして関数型インターフェースを使用したメソッドのオーバーロードを回避する

衝突を避けるために、異なる名前のメソッドを使用してください。例を見てみましょう:

public interface Processor { String process(Callable c) throws Exception; String process(Supplier s); } public class ProcessorImpl implements Processor { @Override public String process(Callable c) throws Exception { // implementation details } @Override public String process(Supplier s) { // implementation details } }

一見、これは合理的なようです。ただし、ProcessorImplのいずれかのメソッドを実行しようとすると次のようになります。

String result = processor.process(() -> "abc");

次のメッセージでエラーで終了します。

reference to process is ambiguous both method process(java.util.concurrent.Callable) in com.baeldung.java8.lambda.tips.ProcessorImpl and method process(java.util.function.Supplier) in com.baeldung.java8.lambda.tips.ProcessorImpl match

この問題を解決するには、2つのオプションがあります。1つ目は、異なる名前のメソッドを使用することです。

String processWithCallable(Callable c) throws Exception; String processWithSupplier(Supplier s);

2つ目は、手動でキャストを実行することです。これは好ましくありません。

String result = processor.process((Supplier) () -> "abc");

7.ラムダ式を内部クラスとして扱わないでください

内部クラスをラムダ式で本質的に置き換えた前の例にもかかわらず、2つの概念は重要な点で異なります:スコープ。

内部クラスを使用すると、新しいスコープが作成されます。同じ名前の新しいローカル変数をインスタンス化することにより、ローカル変数を囲んでいるスコープから隠すことができます。インスタンスへの参照として、内部クラス内でキーワードthisを使用することもできます。

ただし、ラムダ式はスコープを囲むことで機能します。ラムダの本体内の囲んでいるスコープから変数を非表示にすることはできません。この場合、キーワードthisは、囲んでいるインスタンスへの参照です。

For example, in the class UseFoo you have an instance variable value:

private String value = "Enclosing scope value";

Then in some method of this class place the following code and execute this method.

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; }

If you execute the scopeExperiment() method, you will get the following result: Results: resultIC = Inner class value, resultLambda = Enclosing scope value

As you can see, by calling this.value in IC, you can access a local variable from its instance. But in the case of the lambda, this.value call gives you access to the variable value which is defined in the UseFoo class, but not to the variable value defined inside the lambda's body.

8. Keep Lambda Expressions Short and Self-explanatory

If possible, use one line constructions instead of a large block of code. Remember lambdas should be anexpression, not a narrative. Despite its concise syntax, lambdas should precisely express the functionality they provide.

This is mainly stylistic advice, as performance will not change drastically. In general, however, it is much easier to understand and to work with such code.

This can be achieved in many ways – let's have a closer look.

8.1. Avoid Blocks of Code in Lambda's Body

In an ideal situation, lambdas should be written in one line of code. With this approach, the lambda is a self-explanatory construction, which declares what action should be executed with what data (in the case of lambdas with parameters).

If you have a large block of code, the lambda's functionality is not immediately clear.

With this in mind, do the following:

Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) { String result = "Something " + parameter; //many lines of code return result; }

instead of:

Foo foo = parameter -> { String result = "Something " + parameter; //many lines of code return result; };

However, please don't use this “one-line lambda” rule as dogma. If you have two or three lines in lambda's definition, it may not be valuable to extract that code into another method.

8.2. Avoid Specifying Parameter Types

A compiler in most cases is able to resolve the type of lambda parameters with the help of type inference. Therefore, adding a type to the parameters is optional and can be omitted.

Do this:

(a, b) -> a.toLowerCase() + b.toLowerCase();

instead of this:

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

8.3. Avoid Parentheses Around a Single Parameter

Lambda syntax requires parentheses only around more than one parameter or when there is no parameter at all. That is why it is safe to make your code a little bit shorter and to exclude parentheses when there is only one parameter.

So, do this:

a -> a.toLowerCase();

instead of this:

(a) -> a.toLowerCase();

8.4. Avoid Return Statement and Braces

Braces and return statements are optional in one-line lambda bodies. This means, that they can be omitted for clarity and conciseness.

Do this:

a -> a.toLowerCase();

instead of this:

a -> {return a.toLowerCase()};

8.5. Use Method References

Very often, even in our previous examples, lambda expressions just call methods which are already implemented elsewhere. In this situation, it is very useful to use another Java 8 feature: method references.

So, the lambda expression:

a -> a.toLowerCase();

could be substituted by:

String::toLowerCase;

This is not always shorter, but it makes the code more readable.

9. Use “Effectively Final” Variables

Accessing a non-final variable inside lambda expressions will cause the compile-time error. But it doesn’t mean that you should mark every target variable as final.

According to the “effectively final” concept, a compiler treats every variable as final, as long as it is assigned only once.

It is safe to use such variables inside lambdas because the compiler will control their state and trigger a compile-time error immediately after any attempt to change them.

For example, the following code will not compile:

public void method() { String localVariable = "Local"; Foo foo = parameter -> { String localVariable = parameter; return localVariable; }; }

The compiler will inform you that:

Variable 'localVariable' is already defined in the scope.

This approach should simplify the process of making lambda execution thread-safe.

10. Protect Object Variables from Mutation

One of the main purposes of lambdas is use in parallel computing – which means that they're really helpful when it comes to thread-safety.

The “effectively final” paradigm helps a lot here, but not in every case. Lambdas can't change a value of an object from enclosing scope. But in the case of mutable object variables, a state could be changed inside lambda expressions.

Consider the following code:

int[] total = new int[1]; Runnable r = () -> total[0]++; r.run();

This code is legal, as total variable remains “effectively final”. But will the object it references to have the same state after execution of the lambda? No!

Keep this example as a reminder to avoid code that can cause unexpected mutations.

11. Conclusion

このチュートリアルでは、Java8のラムダ式と関数型インターフェースのいくつかのベストプラクティスと落とし穴を見ました。これらの新機能の有用性と能力にもかかわらず、それらは単なるツールです。すべての開発者は、それらを使用する際に注意を払う必要があります。

この例の完全なソースコードは、このGitHubプロジェクトで入手できます。これはMavenおよびEclipseプロジェクトであるため、そのままインポートして使用できます。