Javaでラムダ式を使い始める

Java SE 8より前は、通常、匿名クラスを使用して機能をメソッドに渡していました。この方法ではソースコードが難読化され、理解しにくくなりました。Java 8は、ラムダを導入することでこの問題を解消しました。このチュートリアルでは、最初にラムダ言語機能を紹介し、次にラムダ式とターゲットタイプを使用した関数型プログラミングの詳細を紹介します。また、ラムダがスコープ、ローカル変数、thisandsuperキーワード、およびJava例外とどのように相互作用するかについても学習します。 

このチュートリアルのコード例はJDK12と互換性があることに注意してください。

自分でタイプを見つける

このチュートリアルでは、これまで学習したことのないラムダ以外の言語機能については紹介しませんが、このシリーズでこれまで説明しなかった型を介してラムダをデモンストレーションします。一例はjava.lang.Mathクラスです。これらのタイプは、今後のJava101チュートリアルで紹介します。今のところ、JDK 12APIのドキュメントを読んで詳細を確認することをお勧めします。

ダウンロードコードを取得するこのチュートリアルのサンプルアプリケーションのソースコードをダウンロードします。JavaWorld用にJeffFriesenによって作成されました。

ラムダ:入門書

ラムダ式(ラムダ)は、その後の実行のためのコンストラクタまたはメソッドに渡すことができ、コードのブロック(匿名関数)を記述する。コンストラクターまたはメソッドは、ラムダを引数として受け取ります。次の例を考えてみましょう。

() -> System.out.println("Hello")

この例では、メッセージを標準出力ストリームに出力するためのラムダを識別します。左から右に()、ラムダの正式なパラメーターリストを識別し(例にはパラメーターがありません)、->式がラムダでありSystem.out.println("Hello")、実行されるコードであることを示します。

ラムダは、それぞれが正確に1つの抽象メソッドを宣言する注釈付きインターフェースである関数型インターフェースの使用を簡素化します(ただし、デフォルト、静的、およびプライベートメソッドの任意の組み合わせを宣言することもできます)。たとえば、標準クラスライブラリはjava.lang.Runnable、単一の抽象void run()メソッドを備えたインターフェイスを提供します。この関数型インターフェースの宣言は以下のとおりです。

@FunctionalInterface public interface Runnable { public abstract void run(); }

クラスライブラリは、注釈を付けRunnable@FunctionalInterfaceのインスタンスである、java.lang.FunctionalInterface注釈タイプ。FunctionalInterfaceラムダコンテキストで使用されるインターフェイスに注釈を付けるために使用されます。

ラムダには明示的なインターフェイスタイプはありません。代わりに、コンパイラは周囲のコンテキストを使用して、ラムダが指定されたときにインスタンス化する機能インターフェイスを推測します。ラムダはそのインターフェイスにバインドされます。たとえば、次のコードフラグメントを指定したとします。これは、前のラムダを引数としてjava.lang.ThreadクラスのThread(Runnable target)コンストラクターに渡します。

new Thread(() -> System.out.println("Hello"));

コンパイラは、ラムダThread(Runnable r)を満たす唯一のコンストラクタであるため、ラムダが渡されていると判断します。これはRunnable機能インターフェイスであり、ラムダの空の仮パラメータリストはの空のパラメータリストと()一致run()し、戻り値の型(void)も一致します。ラムダはにバインドされていRunnableます。

リスト1は、この例で遊ぶことができる小さなアプリケーションのソースコードを示しています。

リスト1.LambdaDemo.java(バージョン1)

public class LambdaDemo { public static void main(String[] args) { new Thread(() -> System.out.println("Hello")).start(); } }

リスト1(javac LambdaDemo.java)をコンパイルし、アプリケーション(java LambdaDemo)を実行します。次の出力を確認する必要があります。

Hello

Lambdaは、作成する必要のあるソースコードの量を大幅に簡素化でき、ソースコードをはるかに理解しやすくすることもできます。たとえば、ラムダがない場合は、リスト2のより詳細なコードを指定する可能性があります。これは、を実装する匿名クラスのインスタンスに基づいていますRunnable

リスト2.LambdaDemo.java(バージョン2)

public class LambdaDemo { public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { System.out.println("Hello"); } }; new Thread(r).start(); } }

このソースコードをコンパイルした後、アプリケーションを実行します。前に示したものと同じ出力が見つかります。

LambdasとStreamsAPI

ラムダは、ソースコードを単純化するだけでなく、Javaの機能指向のStreamsAPIでも重要な役割を果たします。これらは、さまざまなAPIメソッドに渡される機能の単位を記述します。

Javaラムダの深さ

ラムダを効果的に使用するには、ラムダ式の構文とターゲットタイプの概念を理解する必要があります。また、ラムダがスコープ、ローカル変数、thisおよびsuperキーワード、および例外とどのように相互作用するかを理解する必要があります。これらのトピックはすべて、次のセクションで説明します。

ラムダの実装方法

ラムダは、Java仮想マシンのinvokedynamic命令とjava.lang.invokeAPIの観点から実装されます。ラムダアーキテクチャについて学ぶために、ビデオLambda:A Peek Under theHoodをご覧ください。

ラムダ構文

すべてのラムダは、次の構文に準拠しています。

( formal-parameter-list ) -> { expression-or-statements }

formal-parameter-list実行時に機能インタフェースの単一抽象メソッドのパラメータと一致している必要があり、正式なパラメータのカンマ区切りリストです。それらの型を省略すると、コンパイラーはラムダが使用されるコンテキストからこれらの型を推測します。次の例を検討してください。

(double a, double b) // types explicitly specified (a, b) // types inferred by compiler

ラムダとvar

Java SE 11以降では、タイプ名をvar。に置き換えることができます。たとえば、を指定できます(var a, var b)

複数の仮パラメータには括弧を指定するか、仮パラメータを指定しない必要があります。ただし、単一の仮パラメーターを指定する場合は、括弧を省略できます(必須ではありません)。(これはパラメーター名にのみ適用されます。タイプも指定する場合は括弧が必要です。)次の追加の例を検討してください。

x // parentheses omitted due to single formal parameter (double x) // parentheses required because type is also present () // parentheses required when no formal parameters (x, y) // parentheses required because of multiple formal parameters

The formal-parameter-list is followed by a -> token, which is followed by expression-or-statements--an expression or a block of statements (either is known as the lambda's body). Unlike expression-based bodies, statement-based bodies must be placed between open ({) and close (}) brace characters:

(double radius) -> Math.PI * radius * radius radius -> { return Math.PI * radius * radius; } radius -> { System.out.println(radius); return Math.PI * radius * radius; }

The first example's expression-based lambda body doesn't have to be placed between braces. The second example converts the expression-based body to a statement-based body, in which return must be specified to return the expression's value. The final example demonstrates multiple statements and cannot be expressed without the braces.

Lambda bodies and semicolons

Note the absence or presence of semicolons (;) in the previous examples. In each case, the lambda body isn't terminated with a semicolon because the lambda isn't a statement. However, within a statement-based lambda body, each statement must be terminated with a semicolon.

Listing 3 presents a simple application that demonstrates lambda syntax; note that this listing builds on the previous two code examples.

Listing 3. LambdaDemo.java (version 3)

@FunctionalInterface interface BinaryCalculator { double calculate(double value1, double value2); } @FunctionalInterface interface UnaryCalculator { double calculate(double value); } public class LambdaDemo { public static void main(String[] args) { System.out.printf("18 + 36.5 = %f%n", calculate((double v1, double v2) -> v1 + v2, 18, 36.5)); System.out.printf("89 / 2.9 = %f%n", calculate((v1, v2) -> v1 / v2, 89, 2.9)); System.out.printf("-89 = %f%n", calculate(v -> -v, 89)); System.out.printf("18 * 18 = %f%n", calculate((double v) -> v * v, 18)); } static double calculate(BinaryCalculator calc, double v1, double v2) { return calc.calculate(v1, v2); } static double calculate(UnaryCalculator calc, double v) { return calc.calculate(v); } }

Listing 3 first introduces the BinaryCalculator and UnaryCalculator functional interfaces whose calculate() methods perform calculations on two input arguments or on a single input argument, respectively. This listing also introduces a LambdaDemo class whose main() method demonstrates these functional interfaces.

The functional interfaces are demonstrated in the static double calculate(BinaryCalculator calc, double v1, double v2) and static double calculate(UnaryCalculator calc, double v) methods. The lambdas pass code as data to these methods, which are received as BinaryCalculator or UnaryCalculator instances.

Compile Listing 3 and run the application. You should observe the following output:

18 + 36.5 = 54.500000 89 / 2.9 = 30.689655 -89 = -89.000000 18 * 18 = 324.000000

Target types

A lambda is associated with an implicit target type, which identifies the type of object to which a lambda is bound. The target type must be a functional interface that's inferred from the context, which limits lambdas to appearing in the following contexts:

  • Variable declaration
  • Assignment
  • Return statement
  • Array initializer
  • Method or constructor arguments
  • Lambda body
  • Ternary conditional expression
  • Cast expression

Listing 4 presents an application that demonstrates these target type contexts.

リスト4.LambdaDemo.java(バージョン4)

import java.io.File; import java.io.FileFilter; import java.nio.file.Files; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitor; import java.nio.file.FileVisitResult; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; public class LambdaDemo { public static void main(String[] args) throws Exception { // Target type #1: variable declaration Runnable r = () -> { System.out.println("running"); }; r.run(); // Target type #2: assignment r = () -> System.out.println("running"); r.run(); // Target type #3: return statement (in getFilter()) File[] files = new File(".").listFiles(getFilter("txt")); for (int i = 0; i  path.toString().endsWith("txt"), (path) -> path.toString().endsWith("java") }; FileVisitor visitor; visitor = new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) { Path name = file.getFileName(); for (int i = 0; i  System.out.println("running")).start(); // Target type #6: lambda body (a nested lambda) Callable callable = () -> () -> System.out.println("called"); callable.call().run(); // Target type #7: ternary conditional expression boolean ascendingSort = false; Comparator cmp; cmp = (ascendingSort) ? (s1, s2) -> s1.compareTo(s2) : (s1, s2) -> s2.compareTo(s1); List cities = Arrays.asList("Washington", "London", "Rome", "Berlin", "Jerusalem", "Ottawa", "Sydney", "Moscow"); Collections.sort(cities, cmp); for (int i = 0; i < cities.size(); i++) System.out.println(cities.get(i)); // Target type #8: cast expression String user = AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty("user.name")); System.out.println(user); } static FileFilter getFilter(String ext) { return (pathname) -> pathname.toString().endsWith(ext); } }