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で利用可能]です。