ラムダ式と機能的インタフェース:ヒントとベストプラクティス

ラムダ式と機能的インターフェース:ヒントとベストプラクティス

1. 概要

Java 8は、その主要な機能のいくつかについて、幅広い使用法、パターン、およびベストプラクティスに到達し始めています。 このチュートリアルでは、機能的なインターフェイスとラムダ式を詳しく見ていきます。

参考文献:

Lambdasで使用されるローカル変数が最終または実質的に最終でなければならないのはなぜですか?

Javaがラムダで使用される場合、ローカル変数が効果的に最終である必要がある理由を学びます。

Java 8 – Lambdasとの強力な比較

Java 8のエレガントなソート-ラムダ式は構文糖衣を超えて強力な機能的セマンティクスをJavaにもたらします。

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<T,R>でそのようなインターフェースをすでに提供しています。

これで、インターフェイス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();
    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() {}
}

通常のインターフェイスと同様に、同じデフォルトのメソッドで異なる機能インターフェイスを拡張することには問題があります。 たとえば、インターフェイスBarBazの両方にデフォルトのメソッドdefaultCommon().があるとします。この場合、コンパイル時エラーが発生します。

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

これを修正するには、FooインターフェイスでdefaultCommon()メソッドをオーバーライドする必要があります。 もちろん、このメソッドのカスタム実装を提供できます。 ただし、親インターフェイスの実装の1つを使用する場合(たとえば、Bazインターフェイスから)、次のコード行をdefaultCommon()メソッドの本体に追加します。

Baz.super.defaultCommon();

しかし注意してください。 Adding too many default methods to the interface is not a very good architectural decision.下位互換性を損なうことなく既存のインターフェイスをアップグレードするために、必要な場合にのみ使用される妥協案と見なす必要があります。

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

コンパイラを使用すると、内部クラスを使用して機能的なインターフェイスをインスタンス化できます。 しかし、これは非常に冗長なコードにつながることができます。 ラムダ式を好む必要があります。

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

内部クラスの上:

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

The lambda expression approach can be used for any suitable interface from old libraries.RunnableComparatorなどのインターフェースに使用できます。 However, thisdoesn’t mean that you should review your whole older codebase and change everything.

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.example.java8.lambda.tips.ProcessorImpl
and method process(java.util.function.Supplier)
in com.example.java8.lambda.tips.ProcessorImpl match

この問題を解決するには、2つのオプションがあります。 The first is to use methods with different names:

String processWithCallable(Callable c) throws Exception;

String processWithSupplier(Supplier s);

The second is to perform casting manually.これは推奨されません。

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

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

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

内部クラスを使用すると、新しいスコープが作成されます。 同じ名前で新しいローカル変数をインスタンス化することにより、囲みスコープからローカル変数を隠すことができます。 内部クラス内のキーワード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()メソッドを実行すると、次の結果が得られます。Results: resultIC = Inner class value, resultLambda = Enclosing scope value

ご覧のとおり、ICでthis.valueを呼び出すことにより、そのインスタンスからローカル変数にアクセスできます。 ただし、ラムダの場合、this.value呼び出しでは、UseFooクラスで定義されている変数valueにアクセスできますが、内部で定義されている変数valueにはアクセスできません。ラムダの体。

8. ラムダ式を短く、自明にする

可能であれば、大きなコードブロックの代わりに1行の構成を使用します。 lambdas should be anを覚えておいてくださいexpression, not a narrative.簡潔な構文にもかかわらず、lambdas should precisely express the functionality they provide.

パフォーマンスは大幅に変わらないため、これは主にスタイル上のアドバイスです。 ただし、一般に、このようなコードを理解し、操作する方がはるかに簡単です。

これはさまざまな方法で実現できます。詳しく見てみましょう。

8.1. ラムダのボディでコードのブロックを回避する

理想的な状況では、ラムダは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;
};

However, please don’t use this “one-line lambda” rule as dogma。 ラムダの定義に2行または3行ある場合、そのコードを別のメソッドに抽出することは価値がない可能性があります。

8.2. パラメータタイプの指定は避けてください

ほとんどの場合、コンパイラーはtype inferenceを使用してラムダパラメーターのタイプを解決できます。 したがって、パラメーターへの型の追加はオプションであり、省略できます。

これを行う:

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

これの代わりに:

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

8.3. 単一のパラメーターを括弧で囲むことは避けてください

Lambda構文では、1つ以上のパラメーターを囲むか、パラメーターがまったくない場合にのみかっこが必要です。 そのため、コードが少し短くなり、パラメータが1つしかない場合は括弧を除外しても安全です。

だから、これをしなさい:

a -> a.toLowerCase();

これの代わりに:

(a) -> a.toLowerCase();

8.4. 戻り値と中括弧を避ける

Bracesおよびreturnステートメントは、1行のラムダ本体ではオプションです。 これは、明確さと簡潔さのためにそれらを省略することができることを意味します。

これを行う:

a -> a.toLowerCase();

これの代わりに:

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

8.5. メソッドリファレンスを使用する

非常に多くの場合、以前の例でさえ、ラムダ式は既に他の場所で実装されているメソッドを呼び出すだけです。 この状況では、別のJava 8機能method referencesを使用すると非常に便利です。

だから、ラムダ式:

a -> a.toLowerCase();

で置き換えることができます:

String::toLowerCase;

これは常に短くなるとは限りませんが、コードが読みやすくなります。

9. 「効果的に最終的な」変数を使用する

ラムダ式内の非最終変数にアクセスすると、コンパイル時エラーが発生します。 But it doesn’t mean that you should mark every target variable as final.

effectively final」の概念によれば、コンパイラーは、1回だけ割り当てられる限り、すべての変数を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変数は「事実上最終」のままであるため、このコードは正当です。 しかし、ラムダの実行後、参照するオブジェクトは同じ状態になりますか? No!

予期しない突然変異を引き起こす可能性のあるコードを避けるために、この例を覚えておいてください。

11. 結論

このチュートリアルでは、Java8のラムダ式と関数型インターフェースのいくつかのベストプラクティスと落とし穴を見ました。 これらの新機能のユーティリティとパワーにもかかわらず、これらは単なるツールです。 すべての開発者は、使用中に注意を払わなければなりません。

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