Java 8の機能インターフェイス
1. 前書き
この記事は、Java 8に存在するさまざまな機能インターフェース、それらの一般的な使用例、および標準JDKライブラリーでの使用法のガイドです。
参考文献:
2. Java8のラムダ
Java 8は、ラムダ式の形式で強力な新しい構文の改善をもたらしました。 ラムダは、たとえばメソッドに渡されたりメソッドから返されたりするなど、一流の言語市民として処理できる匿名関数です。
Java 8より前は、通常、単一の機能をカプセル化する必要があるすべてのケースに対してクラスを作成していました。 これは、プリミティブ関数表現として機能する何かを定義するための多くの不要な定型コードを暗示しています。
ラムダ、関数型インターフェース、およびそれらを操作するためのベストプラクティスは、一般に、記事“Lambda Expressions and Functional Interfaces: Tips and Best Practices”で説明されています。 このガイドでは、java.util.functionパッケージに含まれる特定の機能インターフェイスに焦点を当てています。
3. 機能的インターフェース
すべての機能インターフェイスには、有益な@FunctionalInterfaceアノテーションを付けることをお勧めします。 これは、このインターフェイスの目的を明確に伝えるだけでなく、注釈付きインターフェイスが条件を満たさない場合にコンパイラがエラーを生成することも可能にします。
Any interface with a SAM(Single Abstract Method) is a functional interface、およびその実装はラムダ式として扱われる場合があります。
Java 8のdefaultメソッドはabstractではなく、カウントされないことに注意してください。機能インターフェイスには、複数のdefaultメソッドが含まれている場合があります。 これは、Function’sdocumentationを調べることで確認できます。
4. 関数
ラムダの最も単純で一般的なケースは、1つの値を受け取り、別の値を返すメソッドとの機能的なインターフェースです。 単一の引数のこの関数は、引数の型と戻り値によってパラメーター化されるFunctionインターフェースによって表されます。
public interface Function { … }
標準ライブラリのFunctionタイプの使用法の1つは、キーごとにマップから値を返すが、キーがマップにまだ存在しない場合は値を計算するMap.computeIfAbsentメソッドです。 値を計算するには、渡されたFunction実装を使用します。
Map nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());
この場合、値はキーに関数を適用して計算され、マップ内に配置され、メソッド呼び出しから返されます。 ちなみに、we may replace the lambda with a method reference that matches passed and returned value types。
メソッドが呼び出されるオブジェクトは、実際には、メソッドの暗黙の最初の引数であることに注意してください。これにより、インスタンスメソッドlength参照をFunctionインターフェイスにキャストできます。
Integer value = nameMap.computeIfAbsent("John", String::length);
Functionインターフェースには、デフォルトのcomposeメソッドもあり、複数の関数を1つに結合して、それらを順番に実行できます。
Function intToString = Object::toString;
Function quote = s -> "'" + s + "'";
Function quoteIntToString = quote.compose(intToString);
assertEquals("'5'", quoteIntToString.apply(5));
quoteIntToString関数は、intToString関数の結果に適用されるquote関数の組み合わせです。
5. プリミティブ関数の専門分野
プリミティブ型はジェネリック型の引数にすることはできないため、最もよく使用されるプリミティブ型double、int、long、およびそれらの組み合わせには、Functionインターフェイスのバージョンがあります引数と戻り値の型:
-
IntFunction、LongFunction、DoubleFunction:引数は指定された型であり、戻り値の型はパラメーター化されています
-
ToIntFunction、ToLongFunction、ToDoubleFunction:の戻り値の型は指定された型であり、引数はパラメーター化されます
-
DoubleToIntFunction、DoubleToLongFunction、IntToDoubleFunction、IntToLongFunction、LongToIntFunction、LongToDoubleFunction —引数と戻り値の型の両方がプリミティブ型として定義されています。彼らの名前
たとえば、shortを受け取り、byteを返す関数には、すぐに使用できる関数型インターフェイスはありませんが、独自の関数を作成することを妨げるものはありません。
@FunctionalInterface
public interface ShortToByteFunction {
byte applyAsByte(short s);
}
これで、ShortToByteFunctionで定義されたルールを使用して、shortの配列をbyteの配列に変換するメソッドを記述できます。
public byte[] transformArray(short[] array, ShortToByteFunction function) {
byte[] transformedArray = new byte[array.length];
for (int i = 0; i < array.length; i++) {
transformedArray[i] = function.applyAsByte(array[i]);
}
return transformedArray;
}
これを使用して、ショートの配列をバイトの配列に2を掛けたものに変換する方法を次に示します。
short[] array = {(short) 1, (short) 2, (short) 3};
byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));
byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6};
assertArrayEquals(expectedArray, transformedArray);
6. 2つのアリティ機能の専門分野
2つの引数でラムダを定義するには、名前に「Bi”」キーワードを含む追加のインターフェースを使用する必要があります:BiFunction、ToDoubleBiFunction、ToIntBiFunction、およびToLongBiFunction 。
BiFunctionには引数と戻り値の型の両方が生成されますが、ToDoubleBiFunctionなどではプリミティブ値を返すことができます。
標準APIでこのインターフェースを使用する典型的な例の1つは、Map.replaceAllメソッドです。これにより、マップ内のすべての値を計算値に置き換えることができます。
キーと古い値を受け取るBiFunction実装を使用して、給与の新しい値を計算し、それを返します。
Map salaries = new HashMap<>();
salaries.put("John", 40000);
salaries.put("Freddy", 30000);
salaries.put("Samuel", 50000);
salaries.replaceAll((name, oldValue) ->
name.equals("Freddy") ? oldValue : oldValue + 10000);
7. サプライヤー
Supplier関数型インターフェースは、引数をとらないさらに別のFunction特殊化です。 通常、値の遅延生成に使用されます。 たとえば、doubleの値を2乗する関数を定義しましょう。 値自体ではなく、この値のSupplierを受け取ります。
public double squareLazy(Supplier lazyValue) {
return Math.pow(lazyValue.get(), 2);
}
これにより、Supplier実装を使用して、この関数を呼び出すための引数を遅延生成できます。 これは、この引数の生成にかなりの時間がかかる場合に役立ちます。 GuavaのsleepUninterruptiblyメソッドを使用してそれをシミュレートします。
Supplier lazyValue = () -> {
Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
return 9d;
};
Double valueSquared = squareLazy(lazyValue);
サプライヤの別のユースケースは、シーケンス生成のロジックを定義することです。 それを示すために、静的なStream.generateメソッドを使用して、Streamのフィボナッチ数を作成しましょう。
int[] fibs = {0, 1};
Stream fibonacci = Stream.generate(() -> {
int result = fibs[1];
int fib3 = fibs[0] + fibs[1];
fibs[0] = fibs[1];
fibs[1] = fib3;
return result;
});
Stream.generateメソッドに渡される関数は、Supplier関数型インターフェースを実装します。 ジェネレーターとして役立つためには、Supplierは通常何らかの外部状態を必要とすることに注意してください。 この場合、その状態は最後の2つのフィボナッチ数列から構成されます。
この状態を実装するには、all external variables used inside the lambda have to be effectively finalであるため、いくつかの変数の代わりに配列を使用します。
Supplier関数型インターフェースの他の特殊化には、BooleanSupplier、DoubleSupplier、LongSupplier、およびIntSupplierが含まれ、これらの戻り値の型は対応するプリミティブです。
8. 消費者
Supplierとは対照的に、Consumerは生成された引数を受け入れ、何も返しません。 副作用を表す関数です。
たとえば、コンソールで挨拶を印刷して、名前のリストの全員に挨拶しましょう。 List.forEachメソッドに渡されるラムダは、Consumer関数型インターフェースを実装します。
List names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));
プリミティブ値を引数として受け取るConsumerの特殊なバージョン(DoubleConsumer、IntConsumer、およびLongConsumer)もあります。 さらに興味深いのは、BiConsumerインターフェースです。 その使用例の1つは、マップのエントリを反復処理することです。
Map ages = new HashMap<>();
ages.put("John", 25);
ages.put("Freddy", 24);
ages.put("Samuel", 30);
ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
特殊なBiConsumerバージョンの別のセットは、ObjDoubleConsumer、ObjIntConsumer、およびObjLongConsumerで構成され、2つの引数を受け取ります。1つは生成され、もう1つはプリミティブ型です。
9. 述語
数学的論理では、述語は値を受け取りブール値を返す関数です。
Predicate関数型インターフェースは、生成された値を受け取り、ブール値を返すFunctionの特殊化です。 Predicateラムダの一般的な使用例は、値のコレクションをフィルタリングすることです。
List names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");
List namesWithA = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
上記のコードでは、Stream APIを使用してリストをフィルタリングし、文字「A」で始まる名前のみを保持します。 フィルタリングロジックは、Predicate実装にカプセル化されています。
前のすべての例と同様に、この関数には、プリミティブ値を受け取るIntPredicate、DoublePredicate、およびLongPredicateバージョンがあります。
10. オペレータ
Operatorインターフェースは、同じ値型を受け取り、返す関数の特殊なケースです。 UnaryOperatorインターフェイスは単一の引数を受け取ります。 Collections APIでの使用例の1つは、リスト内のすべての値を同じタイプの計算値で置き換えることです。
List names = Arrays.asList("bob", "josh", "megan");
names.replaceAll(name -> name.toUpperCase());
List.replaceAll関数は、所定の値を置き換えるため、voidを返します。 目的に合わせて、リストの値を変換するために使用されるラムダは、受け取ったものと同じ結果タイプを返さなければなりません。 これが、UnaryOperatorがここで役立つ理由です。
もちろん、name → name.toUpperCase()の代わりに、メソッド参照を使用するだけで済みます。
names.replaceAll(String::toUpperCase);
BinaryOperatorの最も興味深いユースケースの1つは、削減操作です。 すべての値の合計で整数のコレクションを集約するとします。 Stream APIを使用すると、コレクター,を使用してこれを行うことができますが、より一般的な方法は、reduceメソッドを使用することです。
List values = Arrays.asList(3, 5, 8, 9, 12);
int sum = values.stream()
.reduce(0, (i1, i2) -> i1 + i2);
reduceメソッドは、初期アキュムレータ値とBinaryOperator関数を受け取ります。 この関数の引数は、同じ型の値のペアであり、関数自体には、それらを同じ型の単一の値に結合するためのロジックが含まれています。 Passed function must be associative。これは、値の集計の順序が重要ではないことを意味します。 次の条件が満たされる必要があります。
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
BinaryOperator演算子関数の結合法則により、削減プロセスを簡単に並列化できます。
もちろん、プリミティブ値で使用できるUnaryOperatorとBinaryOperatorの特殊化もあります。つまり、DoubleUnaryOperator、IntUnaryOperator、LongUnaryOperator、DoubleBinaryOperatorです。 s、IntBinaryOperator、およびLongBinaryOperator。
11. レガシー機能インターフェイス
すべての機能インターフェースがJava 8で登場したわけではありません。 以前のバージョンのJavaの多くのインターフェースは、FunctionalInterfaceの制約に準拠しており、ラムダとして使用できます。 顕著な例は、同時実行APIで使用されるRunnableおよびCallableインターフェースです。 Java 8では、これらのインターフェースも@FunctionalInterfaceアノテーションでマークされています。 これにより、同時実行コードを大幅に簡素化できます。
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();
12. 結論
この記事では、ラムダ式として使用できるJava 8 APIに存在するさまざまな機能インターフェースについて説明しました。 記事のソースコードはover on GitHubで入手できます。