Java 8 関数インターフェースの例チュートリアルへようこそ。Java は常にオブジェクト指向プログラミング言語でした。オブジェクト指向プログラミング言語ということです。これは、Java プログラミングではオブジェクトを中心にすべてが展開されることを意味します(いくつかのプリミティブ型を除く)。Java には関数だけではなく、クラスと関連付けて使用する必要があるという点があります。
Java 8 関数インターフェース
C++、JavaScript などの他のプログラミング言語を見ると、これらは関数型プログラミング言語と呼ばれます。なぜなら、必要に応じて関数を書き、使用できるからです。これらの言語の一部はオブジェクト指向プログラミングもサポートしています。オブジェクト指向であることは悪いことではありませんが、プログラムに多くの冗長性をもたらします。例えば、Runnable のインスタンスを作成する必要があるとします。通常、以下のように匿名クラスを使用して行います。
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
上記のコードを見ると、実際に役立つ部分は run() メソッド内のコードです。残りのコードはすべて、Java プログラムの構造のためです。Java 8 関数インターフェースとラムダ式は、多くのボイラープレートコードを削除することで、より小さな、よりクリーンなコードを書くのに役立ちます。
Java 8 関数インターフェース
正確に1つの抽象メソッドを持つインターフェースを関数インターフェースと呼びます。@FunctionalInterface
注釈を追加して、インターフェースを関数インターフェースとしてマークできます。これは必須ではありませんが、関数インターフェースに誤って余分なメソッドを追加するのを防ぐために、使用することが最善の方法です。インターフェースが@FunctionalInterface
注釈で注釈付けされており、複数の抽象メソッドを持とうとすると、コンパイラエラーが発生します。Java 8 関数インターフェースの主な利点は、ラムダ式を使用してそれらをインスタンス化し、かさばる匿名クラスの実装を回避できることです。Java 8 コレクション API が書き直され、新しい Stream API が導入されましたが、これには多くの関数インターフェースが使用されています。Java 8 は、java.util.function
パッケージで多くの関数インターフェースを定義しています。Java 8 関数インターフェースのいくつかの有用な例は、Consumer
、Supplier
、Function
、およびPredicate
です。詳細については、Java 8 Stream Exampleで説明しています。java.lang.Runnable
は、run()
という単一の抽象メソッドを持つ関数インターフェースの優れた例です。以下のコードスニペットは、関数インターフェースに関するいくつかのガイダンスを提供しています:
interface Foo { boolean equals(Object obj); }
// equalsはすでに暗黙のメンバー(Objectクラス)です
interface Comparator {
boolean equals(Object obj);
int compare(T o1, T o2);
}
// Comparatorには1つの抽象的でないObjectメソッドしかないため、機能します
interface Foo {
int m();
Object clone();
}
// cloneメソッドがpublicではないため、機能しません
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// 2つのメソッドがありますが、同じシグネチャです
interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Y.mはサブシグネチャであり、戻り値を置換可能です
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// いずれのメソッドもすべての抽象メソッドのサブシグネチャを持っていないため、機能しません
interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class > c); }
interface Z extends X, Y {}
// いずれのメソッドもすべての抽象メソッドのサブシグネチャを持っていないため、機能しません
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// コンパイラエラー: 戻り値を置換可能なメソッドがありません
interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// コンパイラエラー: 異なるシグネチャですが、同じ消去があります
ラムダ式
ラムダ式は、Javaオブジェクト指向世界で関数型プログラミングを視覚化する方法です。オブジェクトはJavaプログラミング言語の基礎であり、関数をオブジェクトなしで作成することはできません。そのため、Java言語ではラムダ式を機能インターフェースと一緒に使用するサポートが提供されています。機能インターフェースには1つの抽象関数しかないため、ラムダ式をメソッドに適用する際に混乱することはありません。ラムダ式の構文は(引数) -> (本文)です。では、上記の無名のRunnableをラムダ式を使ってどのように書くかを見てみましょう。
Runnable r1 = () -> System.out.println("My Runnable");
上記のラムダ式で何が起こっているかを理解しましょう。
- Runnableは、関数型インターフェースです。そのため、ラムダ式を使用してそのインスタンスを作成できます。
- run()メソッドは引数を取らないため、ラムダ式も引数を持ちません。
- if-elseブロックと同様に、メソッド本体に単一のステートメントがあるため、中括弧({})を回避できます。複数のステートメントの場合は、他のメソッドと同様に中括弧を使用する必要があります。
なぜラムダ式が必要なのか
-
コードの行数を減らす ラムダ式を使用する明確な利点の1つは、コードの量が減少することです。ラムダ式を使用して関数型インターフェースのインスタンスを匿名クラスを使用するよりも簡単に作成できる方法を既に見てきました。
-
連続および並列実行のサポート ラムダ式を使用するもう一つの利点は、Stream API の連続および並列操作のサポートを活用できることです。これを説明するために、与えられた数が素数かどうかをテストするメソッドを書く必要がある簡単な例を取り上げてみましょう。従来、次のようにコードを書いていました。コードは完全に最適化されていませんが、例としては十分ですので、ご容赦ください。
//従来の方法 private static boolean isPrime(int number) { if(number < 2) return false; for(int i=2; i<number; i++){ if(number % i == 0) return false; } return true; }
上記のコードの問題は、それが連続的な性質であることです。非常に大きな数の場合、かなりの時間がかかります。コードの別の問題は、多くの出口があることと、読み取りにくいことです。同じメソッドをラムダ式とストリームAPIを使用してどのように書くかを見てみましょう。
//宣言的なアプローチ private static boolean isPrime(int number) { return number > 1 && IntStream.range(2, number).noneMatch( index -> number % index == 0); }
IntStream
は、連続的で並列の集計操作をサポートするプリミティブな int 型の要素のシーケンスです。これは、Stream
の int プリミティブの特殊化です。より読みやすくするために、次のようにメソッドを書くこともできます。private static boolean isPrime(int number) { IntPredicate isDivisible = index -> number % index == 0; return number > 1 && IntStream.range(2, number).noneMatch( isDivisible); }
IntStream
に馴染みがない場合、そのrange()
メソッドは、指定された範囲内(始めを含み、終わりを含まない)の 1 ずつ増加する順序付けされた IntStream を返します。noneMatch()
メソッドは、このストリームの要素がすべて指定された述語と一致しないかどうかを返します。結果を判断するためには、すべての要素に述語を評価する必要がない場合があります。 -
メソッドに動作を渡す ラムダ式を使用して、メソッドの動作を渡す方法を簡単な例で見てみましょう。 与えられた基準に一致する場合、リスト内の数値の合計を計算するメソッドを書かなければならないとします。 Predicate を使用して、以下のようなメソッドを記述することができます。
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i -> i) .sum(); }
サンプルの使用方法:
//すべての数値の合計 sumWithCondition(numbers, n -> true) //すべての偶数の合計 sumWithCondition(numbers, i -> i%2==0) //5より大きいすべての数値の合計 sumWithCondition(numbers, i -> i>5)
-
怠惰な評価による効率向上 ラムダ式を使用するもう一つの利点は、怠惰な評価です。たとえば、範囲3から11の最大の奇数を見つけてその2乗を返すメソッドを書く必要があるとします。通常、このメソッドのコードは次のようになります:
private static int findSquareOfMaxOdd(List<Integer> numbers) { int max = 0; for (int i : numbers) { if (i % 2 != 0 && i > 3 && i < 11 && i > max) { max = i; } } return max * max; }
上記のプログラムは常に順次実行されますが、Stream APIを使用してこれを達成し、怠惰さの利点を得ることができます。どのようにStream APIとラムダ式を使用してこのコードを関数型プログラミングの方法に書き直すかを見てみましょう。
public static int findSquareOfMaxOdd(List<Integer> numbers) { return numbers.stream() .filter(NumberTest::isOdd) // Predicateは関数型インタフェースであり、 .filter(NumberTest::isGreaterThan3) // ラムダ式を使用してそれを初期化しています .filter(NumberTest::isLessThan11) // 無名内部クラスではなく .max(Comparator.naturalOrder()) .map(i -> i * i) .get(); } public static boolean isOdd(int i) { return i % 2 != 0; } public static boolean isGreaterThan3(int i){ return i > 3; } public static boolean isLessThan11(int i){ return i < 11; }
二重コロン(::)演算子に驚いている場合、それはJava 8で導入されたものであり、メソッド参照に使用されます。Javaコンパイラは引数を呼び出されるメソッドにマッピングします。これは、ラムダ式
i -> isGreaterThan3(i)
またはi -> NumberTest.isGreaterThan3(i)
の短縮形です。
ラムダ式の例
以下に、ラムダ式のいくつかのコードスニペットと、それらを説明する短いコメントを提供します。
() -> {} // No parameters; void result
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
// 複雑なブロックボディーで複数の戻り値
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1 // Single inferred-type argument, same as below
x -> x+1 // Parenthesis optional for single inferred-type case
(String s) -> s.length() // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length() // Single inferred-type argument
t -> { t.start(); } // Single inferred-type argument
(int x, int y) -> x+y // Multiple declared-type parameters
(x,y) -> x+y // Multiple inferred-type parameters
(x, final y) -> x+y // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
メソッドとコンストラクタの参照
A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
それはすべてJava 8の関数インターフェースとラムダ式チュートリアルです。これを使用することを強くお勧めします。この構文はJavaに新しいものですので、それを理解するのに時間がかかるでしょう。また、Java 8リリースのすべての改善点と変更点を学ぶためにJava 8の機能もチェックしてください。
Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces