Java8の機能インターフェイス
1. 序章
このチュートリアルは、Java 8に存在するさまざまな機能インターフェース、それらの一般的な使用例、および標準JDKライブラリでの使用法のガイドです。
2. Java8のラムダ
Java 8は、ラムダ式の形で強力な新しい構文の改善をもたらしました。 ラムダは、ファーストクラスの言語市民として処理できる無名関数です。 たとえば、メソッドに渡したり、メソッドから返したりすることができます。
Java 8より前は、通常、単一の機能をカプセル化する必要があるすべての場合にクラスを作成していました。 これは、プリミティブ関数表現として機能するものを定義するための多くの不要なボイラープレートコードを意味していました。
記事「ラムダ式と機能インターフェイス:ヒントとベストプラクティス」では、ラムダを操作するための機能インターフェイスとベストプラクティスについて詳しく説明しています。 このガイドでは、java.util.functionパッケージに含まれる特定の機能インターフェイスに焦点を当てています。
3. 機能インターフェイス
すべての機能インターフェイスに、有益な@FunctionalInterfaceアノテーションを付けることをお勧めします。 これにより、インターフェースの目的が明確に伝えられ、注釈付きインターフェースが条件を満たさない場合にコンパイラーがエラーを生成することもできます。
SAM(Single Abstract Method)を使用するすべてのインターフェイスは、機能インターフェイスであり、その実装はラムダ式として扱うことができます。
Java8のdefaultメソッドはabstractではなく、カウントされないことに注意してください。 機能インターフェイスには、複数のdefaultメソッドが含まれている場合があります。 これは、関数のドキュメントを見るとわかります。
4. 機能
ラムダの最も単純で一般的なケースは、ある値を受け取り、別の値を返すメソッドを持つ機能インターフェイスです。 単一の引数のこの関数は、 Function インターフェイスで表されます。このインターフェイスは、引数のタイプと戻り値によってパラメーター化されます。
public interface Function<T, R> { … }
標準ライブラリのFunctionタイプの使用法の1つは、Map.computeIfAbsentメソッドです。 このメソッドは、キーごとにマップから値を返しますが、キーがマップにまだ存在しない場合は値を計算します。 値を計算するには、渡された関数の実装を使用します。
Map<String, Integer> nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());
この場合、関数をキーに適用して値を計算し、マップ内に配置し、メソッド呼び出しからも返します。 W eは、ラムダを、渡された値と戻り値のタイプに一致するメソッド参照に置き換えることができます。
メソッドを呼び出すオブジェクトは、実際には、メソッドの暗黙の最初の引数であることを忘れないでください。 これにより、インスタンスメソッドlength参照をFunctionインターフェイスにキャストできます。
Integer value = nameMap.computeIfAbsent("John", String::length);
Function インターフェースには、デフォルトの compose メソッドもあり、複数の関数を1つに結合して、それらを順番に実行できます。
Function<Integer, String> intToString = Object::toString;
Function<String, String> quote = s -> "'" + s + "'";
Function<Integer, String> quoteIntToString = quote.compose(intToString);
assertEquals("'5'", quoteIntToString.apply(5));
quoteIntToString 関数は、intToString関数の結果に適用されるquote関数の組み合わせです。
5. プリミティブ関数の専門分野
プリミティブ型は汎用型引数にすることはできないため、最もよく使用されるプリミティブ型 double 、 int 、用のFunctionインターフェイスのバージョンがあります。 ] long 、および引数型と戻り型のそれらの組み合わせ:
- 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;
}
これを使用して、shortの配列を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 、
BiFunction には引数と戻り型の両方が生成されますが、ToDoubleBiFunctionなどではプリミティブ値を返すことができます。
標準APIでこのインターフェースを使用する典型的な例の1つは、 Map.replaceAll メソッドです。これにより、マップ内のすべての値を計算値に置き換えることができます。
キーと古い値を受け取るBiFunction実装を使用して、給与の新しい値を計算して返します。
Map<String, Integer> 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 機能インターフェイスは、引数をとらないもう1つのFunction特殊化です。 通常、値の遅延生成に使用します。 たとえば、doubleの値を2乗する関数を定義しましょう。 値自体は受け取りませんが、この値のサプライヤーを受け取ります。
public double squareLazy(Supplier<Double> lazyValue) {
return Math.pow(lazyValue.get(), 2);
}
これにより、Supplier実装を使用してこの関数を呼び出すための引数を遅延生成できます。 これは、引数の生成にかなりの時間がかかる場合に役立ちます。 GuavaのsleepUninterruptiblyメソッドを使用してそれをシミュレートします。
Supplier<Double> lazyValue = () -> {
Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
return 9d;
};
Double valueSquared = squareLazy(lazyValue);
Supplier のもう1つの使用例は、シーケンス生成のロジックを定義することです。 それを示すために、静的な Stream.generate メソッドを使用して、フィボナッチ数のStreamを作成しましょう。
int[] fibs = {0, 1};
Stream<Integer> 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つのフィボナッチ数列で構成されます。
この状態を実装するには、ラムダ内で使用されるすべての外部変数が事実上final である必要があるため、いくつかの変数の代わりに配列を使用します。
Supplier 機能インターフェイスのその他の特殊化には、 BooleanSupplier 、 DoubleSupplier 、 LongSupplier 、およびIntSupplierが含まれます。対応するプリミティブ。
8. 消費者
Supplier とは対照的に、 Consumer は生成された引数を受け入れ、何も返しません。 副作用を表現している機能です。
たとえば、コンソールに挨拶を印刷して、名前のリストにある全員に挨拶しましょう。 List.forEach メソッドに渡されるラムダは、Consumer関数インターフェイスを実装します。
List<String> names = Arrays.asList("John", "Freddy", "Samuel");
names.forEach(name -> System.out.println("Hello, " + name));
Consumer — DoubleConsumer 、 IntConsumer 、 LongConsumer —の特殊なバージョンもあり、これらは引数としてプリミティブ値を受け取ります。 さらに興味深いのは、BiConsumerインターフェースです。 そのユースケースの1つは、マップのエントリを反復処理することです。
Map<String, Integer> 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、で構成されます。 引数の1つは生成され、もう1つはプリミティブ型です。
9. 述語
数理論理学では、述語は値を受け取り、ブール値を返す関数です。
Predicate 機能インターフェイスは、生成された値を受け取り、ブール値を返すFunctionの特殊化です。 Predicate ラムダの一般的な使用例は、値のコレクションをフィルタリングすることです。
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");
List<String> namesWithA = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
上記のコードでは、 Stream APIを使用してリストをフィルタリングし、文字「A」で始まる名前のみを保持します。 述語の実装は、フィルタリングロジックをカプセル化します。
前のすべての例と同様に、プリミティブ値を受け取るこの関数には、 IntPredicate 、 DoublePredicate 、およびLongPredicateバージョンがあります。
10. オペレーター
Operator インターフェースは、同じ値型を受け取り、返す関数の特殊なケースです。 UnaryOperatorインターフェイスは単一の引数を受け取ります。 Collections APIでのユースケースの1つは、リスト内のすべての値を同じタイプの計算値に置き換えることです。
List<String> 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<Integer> values = Arrays.asList(3, 5, 8, 9, 12);
int sum = values.stream()
.reduce(0, (i1, i2) -> i1 + i2);
reduce メソッドは、初期アキュムレータ値とBinaryOperator関数を受け取ります。 この関数の引数は、同じタイプの値のペアです。 関数自体には、同じタイプの単一の値にそれらを結合するためのロジックも含まれています。 渡される関数は結合法則である必要があります。これは、値の集計の順序が重要ではないことを意味します。 次の条件が満たされる必要があります。
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
BinaryOperator 演算子関数の結合法則により、削減プロセスを簡単に並列化できます。
もちろん、プリミティブ値で使用できるUnaryOperatorおよびBinaryOperatorの特殊化、つまり DoubleUnaryOperator 、 IntUnaryOperator 、もあります。 ] LongUnaryOperator 、 DoubleBinaryOperator 、 IntBinaryOperator 、およびLongBinaryOperator。
11. レガシー機能インターフェース
すべての機能インターフェイスがJava8に登場したわけではありません。 以前のバージョンのJavaの多くのインターフェースは、 FunctionalInterface の制約に準拠しており、ラムダとして使用できます。 著名な例には、同時実行APIで使用されるRunnableおよびCallableインターフェースが含まれます。 Java 8では、これらのインターフェースも@FunctionalInterfaceアノテーションでマークされています。 これにより、同時実行コードを大幅に簡素化できます。
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
thread.start();
12. 結論
この記事では、ラムダ式として使用できるJava8APIに存在するさまざまな機能インターフェースについて説明しました。 この記事のソースコードは、GitHubでから入手できます。