Java 8の機能的インタフェース
1前書き
この記事は、Java 8に存在するさまざまな機能インターフェース、それらの一般的な使用例、および標準JDKライブラリでの使用法についてのガイドです。
2 Java 8
におけるラムダ
Java 8では、ラムダ式という形で強力な構文上の改善が行われました。ラムダは、例えばメソッドに渡されたり、メソッドから返されたりする、一級言語の市民として扱うことができる無名関数です。
Java 8より前のバージョンでは、通常、単一の機能をカプセル化する必要があるすべてのケースに対してクラスを作成していました。これは、原始的な関数表現として役立つものを定義するための不必要な定型コードがたくさんあることを意味していました。
ラムダ、関数型インタフェース、およびそれらを使った作業のベストプラクティスは、一般的に記事リンク:/java-8-lambda-expressions-tips[“ラムダ式と関数型インタフェース:ヒントとベストプラクティス”]で説明されています。このガイドでは、
java.util.function
パッケージに含まれている特定の機能インタフェースについて説明します。
3機能インターフェース
すべての機能インターフェースは、有益な
@ FunctionalInterface
アノテーションを付けることをお勧めします。これは、このインタフェースの目的を明確に伝えるだけでなく、注釈付きインタフェースが条件を満たさない場合にコンパイラがエラーを生成することを可能にします。
-
SAM(Single Abstract Method)を持つすべてのインタフェースは機能的なインタフェース** であり、その実装はラムダ式として扱うことができます。
Java 8の
default
メソッドは
abstract
ではなく、数えてはいけないことに注意してください。関数型インタフェースは依然として複数の
default
メソッドを持つことができます。
これを観察するには、__Function’s
documentation
をご覧ください。
4関数
ラムダの最も単純で一般的なケースは、ある値を受け取り別の値を返すメソッドを持つ関数型インタフェースです。この単一の引数の関数は、引数の型と戻り値によってパラメータ化される
Function
インタフェースによって表されます。
public interface Function<T, R> { ... }
標準ライブラリの
Function
タイプの使用法の1つは、キーからマップから値を返しますが、キーがマップにまだ存在していない場合は値を計算する
Map.computeIfAbsent
メソッドです。値を計算するために、渡されたFunction実装を使用します。
Map<String, Integer> nameMap = new HashMap<>();
Integer value = nameMap.computeIfAbsent("John", s -> s.length());
この場合、値はキーに関数を適用して計算され、マップ内に配置されてメソッド呼び出しから返されます。ところで、** ラムダは、渡された値と返される値の型に一致するメソッド参照に置き換えることができます。
メソッドが呼び出されるオブジェクトは、実際には、メソッドの暗黙の最初の引数であり、インスタンスメソッドの
length
参照を
Function
インタフェースにキャストできます。
Integer value = nameMap.computeIfAbsent("John", String::length);
Function
インターフェースにはデフォルトの
compose
メソッドもあります。
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
、
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;
}
これを使用して、short配列を2倍のbyte配列に変換できます。
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. Two-Arity関数のスペシャライゼーション
2つの引数を使用してラムダを定義するには、名前に「
Bi」
キーワードを含む追加のインターフェイスを使用する必要があります。
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
の値を二乗する関数を定義しましょう。値そのものではなく、この値の
サプライヤ
を受け取ります。
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);
サプライヤのもう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つのフィボナッチ数列からなります。
この状態を実装するには、2つの変数ではなく配列を使用します。これは、ラムダ内で使用されるすべての外部変数が事実上最終的でなければならないためです。
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
バージョンのもう1つのセットは、2つの引数を受け取る
ObjDoubleConsumer
、
ObjIntConsumer
、および
ObjLongConsumer
で構成されています。
9述語
数学的論理では、述語は値を受け取り、ブール値を返す関数です。
Predicate
機能インターフェースは、汎用値を受け取り、ブール値を返す
Function
の特殊化です。
Predicate
lambdaの典型的な使用例は、値のコレクションをフィルタリングすることです。
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”で始まる名前だけを残します。フィルタリングロジックは
Predicate
実装にカプセル化されています。
前のすべての例と同様に、この関数には、プリミティブ値を受け取る
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
の特殊化もあります。
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に存在するさまざまな機能インターフェースについて説明しました。この記事のソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-8[GitHubで利用可能]です。