読み込み中...

9ステップで理解するJavaとラムダ式入門|プログラミング入門

Javaとラムダ式を解説するイラスト Java
この記事は約25分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Javaのラムダ式は、関数型インターフェイスに処理を短く渡すための構文です。プログラミング入門の段階では、() ->(x) ->(a, b) ->の形を押さえ、RunnableFunctionPredicateConsumerなどの型と結び付けて読むと理解しやすくなります。

その理解が進むと、Listの加工、StreammapfilterforEachによる反復処理まで、Javaプログラムの見通しが変わります。Java基礎として構文を覚えるだけでなく、ラムダ式使い方、ラムダ式注意点、ラムダ式応用、ラムダ式カスタマイズまで順に扱いるのが基本です。

動作確認環境
  • Java 21 LTS / OpenJDK 21
  • JUnit Jupiter 5.10
📖 この記事で学べること
  • Java基礎として押さえる構文、型、クラスの関係
  • ラムダ式の基本形と関数型インターフェイスの読み方
  • Stream APIを使ったラムダ式応用のコード例
  • 例外処理、変数キャプチャなどのラムダ式注意点
  • メソッド参照やテストを含むラムダ式カスタマイズ

Javaとは

Javaは、クラスを中心に処理を組み立てるオブジェクト指向言語です。ソースコードはjavac.classへ変換され、JVM上で動くため、同じJavaプログラムを複数の環境に配布しやすい設計になっています。

一般に、Java基礎ではclassmainpublicstaticvoidの意味から学びます。その上でintStringなどの型、ifforの制御構文をつかむと、ラムダ式の位置付けも整理しやすくなるのが目安です。

公式情報は、OracleのJava SE 21 Documentationと、言語仕様であるJava Language Specificationが一次情報になります。プログラミング入門では細部を暗記するより、疑問が出たときに公式ドキュメントへ戻れる状態を作るほうが扱いやすいです。

分類代表例Java基礎での役割ラムダ式との関係
intString値の種類を決める引数や戻り値の推論に関わる
クラスclass Main処理のまとまりを作るメソッド内でラムダ式を使う
インターフェイスRunnable実装すべき処理を表す関数型インターフェイスが受け皿になる
コレクションList複数の値を扱うstreamで加工しやすい
例外RuntimeException異常系を表すラムダ式注意点として扱う
テスト@Test期待する動作を固定するラムダ式カスタマイズ後の確認に使う

Javaの特徴

Javaの特徴は、オブジェクト指向、プラットフォーム非依存、型安全性、標準ライブラリの広さにあります。これらは別々の知識ではなく、Javaプログラムを長く保守するための土台としてつながります。

そのため、プログラミング入門でラムダ式から学ぶ場合でも、Objectinterfacepackageimportの考え方を避けて通ると理解が浅くなるのがポイントです。ラムダ式は短く書ける構文ですが、裏側では関数型インターフェイスの抽象メソッドに処理を渡しています。

オブジェクト指向

Javaでは、データと振る舞いをclassにまとめ、必要に応じてnewでインスタンスを作ります。一方、ラムダ式は短い処理そのものを値のように渡す書き方なので、クラス設計を置き換えるものではなく、処理の受け渡しを軽くする構文と理解できるのが一般的です。

プラットフォームに依存しない

Javaのソースコードはバイトコードへ変換され、JVMが実行します。この仕組みによって、OSの違いを吸収しやすくなり、Javaプログラムをサーバー、デスクトップ、開発環境で共有しやすくなります。

安全性

Javaはコンパイル時の型チェック、trycatchによる例外処理、ガベージコレクションを備えているのが現実的です。ラムダ式注意点として例外処理や変数キャプチャがよく挙がるのは、短い構文でもJavaの型規則とスコープ規則から外れないためです。

豊富なライブラリ

Java標準ライブラリには、java.utiljava.util.functionjava.util.streamなど、ラムダ式と相性のよいAPIが含まれます。特にFunctionSupplierBinaryOperatorは、ラムダ式使い方を学ぶときの代表的な型になります。

Javaの基本構文

Java基礎の構文は、変数、演算、条件分岐、繰り返し処理の順で押さえると整理しやすくなると整理できます。これらの構文はラムダ式そのものではありませんが、サンプルコードを読むための前提になります。

変数の宣言

変数はint number = 10;のように、型、名前、値を並べて宣言します。ラムダ式の外側にあるローカル変数を使う場合、その値は実質的に変更されない必要があるため、変数の扱いはラムダ式注意点にも直結すると理解できます。

演算

演算では+-*/を使い、数値や文字列を処理します。たとえばint result = number + 5;は、numberへ5を加えた値をresultへ入れるJavaプログラムです。

条件分岐

ifは条件によって処理を切り替える構文です。if (number > 10)のような条件式は、ラムダ式内でも使えるため、filterに渡す判定処理を読むときにも役立ちます。

繰り返し処理

forwhileは、同じ処理を繰り返すための構文です。一方、Listに対してはforEachstreamを組み合わせる書き方もあり、ラムダ式応用ではこの差が大きな学習テーマになると覚えるとよいでしょう。

Java基礎を補う関連知識として、配列やリストを詳しく学ぶ場合はJava List型完全ガイドが役立ちます。アノテーションやテスト周辺へ進む場合はJavaアノテーションの12選も参照できます。

ラムダ式の基本

ラムダ式は、関数型インターフェイスの抽象メソッドに対して処理を渡す構文です。Java 8以降のJavaプログラムでは、匿名クラスを短く書く用途や、Streamでデータを加工する用途でよく使われますし、ここがポイントです。

基本形は(引数) -> 式または(引数) -> { 文; }です。そのため、ラムダ式使い方の出発点は、左側が入力、右側が処理または戻り値、と読むことになります。

ラムダ式の定義

ラムダ式の->は、引数と本体を分けるラムダ演算子です。引数がない場合は()、引数が一つの場合はx(x)、複数の場合は(x, y)のように書きます。

このとき、型は代入先の関数型インターフェイスから推論されますが、これは押さえたい点です。サンプルコードではBiConsumer<Integer, Integer>を使い、二つのIntegerを受け取ってSystem.out.printlnへ渡します。

(BiConsumer<Integer, Integer>) (x, y) -> {
  System.out.println(x + y);
};

結果: 期待される出力は、渡されたxyの合計値です。

このコード例は式だけを見ると短いものの、実際のJavaプログラムでは変数へ代入するか、メソッドの引数として渡して使います。BiConsumerは戻り値を返さない処理向けなので、合計値を返したい場合はBinaryOperator<Integer>などを選ぶほうが自然です。

ラムダ式の特徴

ラムダ式の特徴は、処理を短く書けること、関数型インターフェイスと結び付くこと、コレクション処理と組み合わせやすいことにあると考えられます。初心者がつまずきやすいのは、ラムダ式そのものが単独で動く関数ではなく、受け皿となる型が必要な点です。

そのため、プログラミング入門ではRunnableのような引数なしの型から始め、PredicateFunctionConsumerへ広げると学習が進みます。ラムダ式応用では、これらの型がStreamのメソッド引数として自然に登場します。

💡 Tips: ラムダ式を読むときは、左側の引数、右側の処理、代入先の型を同時に確認すると迷いにくくなると言えるでしょう。

ラムダ式の詳細な使い方

ラムダ式使い方を具体化するには、引数なし、引数あり、戻り値ありの形を分けて読む必要があります。形を分けると、サンプルコード内の()(a, b)return省略の意味が見えやすくなります。

基本的なラムダ式の書き方

基本的なラムダ式は、パラメータと本体で構成されますし、これが一つの目安です。単一式なら{}returnを省略でき、複数文なら{}の中に処理を並べます。

(a, b) -> a + b

結果: 期待される戻り値は、abを足した値です。

このコード例は、代入先がないため断片として示しています。Javaプログラムとして使う場合は、BinaryOperator<Integer>や独自の関数型インターフェイスに代入して呼び出するのが基本です。

ラムダ式の引数と戻り値

引数と戻り値は、関数型インターフェイスの抽象メソッドで決まります。Runnableは引数も戻り値もなく、Supplier<T>は引数なしで値を返し、Function<T, R>は値を受け取って別の値へ変換します。

具体的には、Predicate<T>は判定、Consumer<T>は消費、BinaryOperator<T>は同じ型の二項演算に向いているのが目安です。この対応を覚えると、ラムダ式使い方が型から逆算できるようになります。

サンプルコード1:ラムダ式の基本形

このサンプルコードは、Runnableに引数なしのラムダ式を代入します。runを呼ぶと、ラムダ式の本体に書いた処理が呼び出される構造です。

Runnable runnable = () -> {
    System.out.println("ラムダ式を実行します");
};
runnable.run();

結果: 期待される出力は「ラムダ式を実行します」です。

その構造は、匿名クラスでrunメソッドを実装する書き方を短くしたものです。Java基礎としてinterfaceの概念を理解していると、ラムダ式がどのメソッドに対応しているかを追いやすくなるのがポイントです。

サンプルコード2:引数を取るラムダ式

引数を取るラムダ式では、左側に受け取る値を並べます。次のコード例では、BinaryOperator<Integer>applyが二つの整数を受け取り、一つの整数を返します。

BinaryOperator<Integer> adder = (a, b) -> a + b;
Integer result = adder.apply(3, 4);
System.out.println("結果: " + result);

結果: 期待される出力は「結果: 7」です。

このサンプルコードでは、adderに処理を代入し、applyで呼び出しているのが一般的です。プログラミング入門では、ラムダ式を代入しただけでは処理が走らず、対応するメソッドを呼んだ時点で評価される点を意識するとよいです。

サンプルコード3:戻り値を返すラムダ式

戻り値を返すラムダ式は、SupplierFunctionでよく使います。単一式の場合、returnを書かなくても式の値が戻り値として扱われます。

Supplier<String> supplier = () -> "Hello, Lambda!";
String message = supplier.get();
System.out.println(message);

結果: 期待される出力は「Hello, Lambda!」です。

このコード例では、getを呼ぶことで文字列が返りますが、覚えておくと役立つでしょう。ラムダ式使い方としては、値を後から取り出したい処理、設定値を遅らせて作りたい処理などでSupplierを選べます。

Javaの分岐や文字列処理を補いたい場合は、Javaエスケープ処理の10ステップマスターガイドも合わせて読むと、Stringの扱いを整理できます。クラス継承との違いを学ぶ場合はJavaでマスターする!オーバーライドのたった7つのステップが関連するのが現実的です。

ラムダ式の応用例

ラムダ式応用で中心になるのは、コレクションの要素を一つずつ処理する場面です。Streamを使うと、抽出、変換、集約を処理の流れとして書けるため、Javaプログラムの意図が読み取りやすくなります。

ただし、短く書けることだけを目的にすると、条件が複雑なラムダ式になりがちです。実装パターンとしてよく見るのは、短い変換はラムダ式、複雑な処理は名前付きメソッドへ切り出してメソッド参照にする分け方です。

ラムダ式とストリームAPI

Streamは、コレクションの要素に対する処理をつなげて表すAPIです。mapは変換、filterは抽出、collectは結果の収集を担います。

その流れでは、ラムダ式が各要素に対する小さな処理を表すると整理できます。公式ドキュメントのStream APIを確認すると、各メソッドがどの関数型インターフェイスを受け取るかを調べられます。

サンプルコード4:リストの操作

リストの各要素を二倍にするJavaプログラムです。numbers.stream()で流れを作り、mapに渡したラムダ式で各要素を変換します。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        List<Integer> doubledNumbers = numbers.stream()
                .map(n -> n * 2)
                .collect(Collectors.toList());

        System.out.println(doubledNumbers); // 出力: [2, 4, 6, 8, 10]
    }
}

結果: 期待される出力は「[2, 4, 6, 8, 10]」です。

このサンプルコードでは、n -> n * 2が各要素の変換規則になります。Java基礎のfor文で書ける処理でも、ラムダ式応用としてStreamに置き換えると、何を作りたいかが先に読めます。

サンプルコード5:フィルタリングとマッピング

抽出と変換を続けるコード例です。filterで長さが条件を満たす文字列だけを残し、mapで大文字へ変換すると理解できます。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("apple", "banana", "cherry", "date", "elderberry");

        List<String> filteredAndMappedWords = words.stream()
                .filter(w -> w.length() > 5)
                .map(w -> w.toUpperCase())
                .collect(Collectors.toList());

        System.out.println(filteredAndMappedWords); // 出力: [BANANA, ELDERBERRY]
    }
}

結果: 期待される出力は「[BANANA, ELDERBERRY]」です。

このコード例では、w -> w.length() > 5が判定、w -> w.toUpperCase()が変換です。ラムダ式使い方として、条件と加工を分けると読みやすく、ラムダ式注意点として一つの式へ詰め込みすぎない判断も必要になります。

⚠️ 注意: Streamの中間操作は、終端操作であるcollectforEachまで到達して初めて評価されます。処理順を追うときは、メソッドチェーン全体を一つの流れとして読みますし、ここを基本と考えるとよいでしょう。

ラムダ式と関数型インターフェイス

関数型インターフェイスは、抽象メソッドを一つだけ持つインターフェイスです。Javaのラムダ式は、この一つの抽象メソッドの実装として扱われます。

具体的には、RunnableならrunPredicateならtestFunctionならapplyが対象です。@FunctionalInterfaceを付けると、抽象メソッドが複数になったときにコンパイル時の検出が働きます。

公式ドキュメントでは、java.util.functionに代表的な関数型インターフェイスがまとまっていると覚えるとよいでしょう。プログラミング入門では、一覧を暗記するより、引数の数、戻り値の有無、判定か変換かで選ぶと整理できます。

匿名クラスで書くと、対象メソッドと処理本体が見えます。その対比を知ると、ラムダ式カスタマイズやメソッド参照へ進んだときにも、何が省略されているかを判断しやすくなると考えられます。

Runnable runnable = new Runnable(){
    @Override
    public void run(){
        System.out.println("匿名クラスを使用したRunnableの実装");
    }
};

結果: 期待される処理は、runnable.run()を呼んだときに「匿名クラスを使用したRunnableの実装」と出力することです。

同じ処理は、ラムダ式で次のように短く書けます。受け皿がRunnableであるため、Javaコンパイラは() -> { ... }runの実装として扱います。

Runnable runnable = () -> {
    System.out.println("ラムダ式を使用したRunnableの実装");
};

結果: 期待される処理は、runnable.run()を呼んだときに「ラムダ式を使用したRunnableの実装」と出力することです。

この比較から、ラムダ式は匿名クラスのすべてを置き換える構文ではなく、単一抽象メソッドの実装を短く書く構文だと分かりますし、ここがポイントです。Javaプログラムで状態を多く持たせたい場合は、通常のクラスやメソッドに分けるほうが扱いやすい場面もあります。

サンプルコード6:関数型インターフェイスの実装

独自の関数型インターフェイスを定義するコード例です。GreetingsayHelloという抽象メソッドを一つだけ持つため、ラムダ式を代入できます。

@FunctionalInterface
interface Greeting {
    void sayHello(String name);
}

結果: 期待される状態は、Greetingがラムダ式を受け取れる関数型インターフェイスとして定義されることです。

そのインターフェイスに、名前を受け取ってあいさつ文を出力する処理を代入すると言えるでしょう。sayHelloを呼ぶと、ラムダ式の本体が評価されます。

Greeting greeting = (name) -> {
    System.out.println("こんにちは、" + name + "さん");
};

greeting.sayHello("山田");

結果: 期待される出力は「こんにちは、山田さん」です。

このサンプルコードは、Java基礎で学ぶインターフェイスとラムダ式を直接つなげています。ラムダ式応用へ進む前に、抽象メソッドのシグネチャとラムダ式の引数が対応している点を確認すると、型エラーの原因を追いやすくなるのが基本です。

ラムダ式の詳細な注意点

ラムダ式注意点の中心は、スコープ、変数キャプチャ、例外処理です。構文が短いため見落とされがちですが、Javaの型規則と実行タイミングを理解していないと、想定と違うJavaプログラムになりやすくなります。

特に押さえたいのは、ラムダ式の外側にあるローカル変数は、finalまたは実質的にfinalである必要がある点です。値を書き換えながら処理したい場合は、設計を見直すか、集約処理としてreducecollectを使うほうが自然です。

スコープと変数キャプチャ

ラムダ式のスコープは、ラムダ式が参照できる名前の範囲を意味します。外側のローカル変数を読むことはできますが、その変数を後から変更すると、実質的にfinalではなくなるためコンパイルエラーになるのが目安です。

その制約は、ラムダ式がいつ評価されるかと関係します。外側の変数が途中で変わる前提になると、処理の意味が追いにくくなるため、Javaはローカル変数のキャプチャに制限を設けています。

ℹ️ 補足: フィールド変数はローカル変数とは扱いが異なるのがポイントです。ただし、ラムダ式内で外部状態を書き換える設計はテストが難しくなりやすいため、値を返す処理へ寄せると読みやすくなります。

サンプルコード7: 例外処理とラムダ式

ラムダ式内で例外を扱う場合、関数型インターフェイスの抽象メソッドが投げられる例外の範囲を確認します。次のコード例では、RuntimeExceptionを投げ、外側のtry-catchで受け取りますが、これは押さえたい点です。

List<String> list = Arrays.asList("a", "b", "c");
try {
    list.forEach(item -> {
        if ("b".equals(item)) {
            throw new RuntimeException("例外が発生しました");
        }
        System.out.println(item);
    });
} catch (RuntimeException e) {
    System.err.println(e.getMessage());
}

結果: 期待される出力は、標準出力に「a」、標準エラーに「例外が発生しました」です。

このサンプルコードでは、forEachに渡したラムダ式の中で条件分岐を行っています。bに到達した時点でRuntimeExceptionが投げられるため、後続のcは通常の流れでは処理されません。

a
例外が発生しました

結果: 期待される出力例は、標準出力の「a」と例外メッセージの「例外が発生しました」です。

ただし、チェック例外を扱う場合は、FunctionConsumerの抽象メソッドがその例外を宣言していないことがあります。その場合は、ラムダ式の内側で処理する、専用の関数型インターフェイスを作る、呼び出し側の設計を変える、といった選択が必要です。

日付や条件判定を扱うJavaプログラムでは、分岐の理解も必要になるのが一般的です。関連する基礎としてJavaでうるう年を判定!初心者でも分かる9ステップ解説を読むと、ifと真偽値の使い方を補えます。

ラムダ式の詳細なカスタマイズ方法

ラムダ式カスタマイズでは、メソッド参照、処理の切り出し、テストのしやすさを考えます。短く書くより、読み手が処理名から意図を理解できる形に整えるほうが、Javaプログラム全体の保守に向きますし、これが一つの目安です。

そのため、ラムダ式応用で処理が長くなったら、名前付きメソッドへ移す判断が有効です。String::toUpperCaseのようなメソッド参照は、既存メソッドで同じ処理を表せるときに使えます。

メソッド参照

メソッド参照は、ラムダ式の本体が既存メソッドの呼び出しだけで表せる場合の書き方です。word -> word.toUpperCase()は、String::toUpperCaseへ置き換えられます。

この書き換えは、ラムダ式カスタマイズの代表例です。ただし、引数の変換や条件分岐を含む場合は、無理にメソッド参照へ寄せるより、読みやすいラムダ式のまま残すほうが分かりやすい場合もあるのが現実的です。

サンプルコード8:メソッド参照の利用

同じ大文字変換を、ラムダ式とメソッド参照で並べたコード例です。処理結果は同じで、表現だけが変わります。

import java.util.List;
import java.util.stream.Collectors;

public class MethodReferenceExample {
    public static void main(String[] args) {
        List<String> words = List.of("apple", "banana", "cherry");

        // ラムダ式を用いた方法
        List<String> upperCaseWordsLambda = words.stream()
                .map(word -> word.toUpperCase())
                .collect(Collectors.toList());

        // メソッド参照を用いた方法
        List<String> upperCaseWordsMethodRef = words.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());

        System.out.println(upperCaseWordsLambda);
        System.out.println(upperCaseWordsMethodRef);
    }
}

結果: 期待される出力は、どちらの行も「[APPLE, BANANA, CHERRY]」です。

このサンプルコードでは、mapに渡す処理だけが異なります。ラムダ式使い方を学ぶ段階では左の形が理解しやすく、ラムダ式カスタマイズでは右の形が短く読めます。

[APPLE, BANANA, CHERRY]
[APPLE, BANANA, CHERRY]

結果: 期待される出力例は、ラムダ式版とメソッド参照版で同じ大文字リストが並ぶ形です。

ラムダ式のテスト

ラムダ式のテストでは、ラムダ式を直接見るより、入力に対して期待する戻り値や副作用を検査すると整理できます。小さなPredicateFunctionなら、通常のメソッドと同じようにassertTrueassertFalseで確認できます。

一方、外部状態を書き換えるラムダ式は、テストが複雑になりやすくなります。そのため、ラムダ式注意点として、副作用を減らし、入力と出力が対応する形に寄せる設計が扱いやすいです。

サンプルコード9:ラムダ式のテスト

JUnit Jupiterを使い、文字列の長さを判定するPredicateをテストすると理解できます。testに渡す文字列を変えることで、真になる場合と偽になる場合を分けています。

import org.junit.jupiter.api.Test;
import java.util.function.Predicate;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class LambdaTest {
    @Test
    public void testLambda() {
        Predicate<String> isLongerThanFive = str -> str.length() > 5;

        assertTrue(isLongerThanFive.test("abcdefg"));
        assertFalse(isLongerThanFive.test("abcd"));
    }
}

結果: 期待される結果は、abcdefgの判定が真、abcdの判定が偽としてテストが通ることです。

このコード例では、Predicate<String>testを通じてラムダ式を評価します。ラムダ式カスタマイズで処理を切り出した後も、同じ入力と期待値を残しておくと、変更後のJavaプログラムを確認しやすくなります。

まとめ

Javaのラムダ式は、関数型インターフェイスに処理を渡すための構文です。プログラミング入門では、() ->(a, b) ->return省略、{}の有無を、型とセットで読むことが出発点になると覚えるとよいでしょう。

その理解を土台にすると、RunnableSupplierBinaryOperatorPredicateの違いが見えてきます。Java基礎のクラス、インターフェイス、型推論が分かるほど、ラムダ式使い方も自然に整理できます。

ラムダ式応用では、Streammapfiltercollectを組み合わせることで、リストの加工を流れとして表せますが、覚えておくと役立つでしょう。一方で、ラムダ式注意点として、変数キャプチャ、例外処理、副作用の扱いを意識する必要があります。

ラムダ式カスタマイズでは、メソッド参照やテストを取り入れると、短さだけでなく読みやすさも調整できます。サンプルコードを写すだけで終えず、引数、戻り値、代入先の型を言葉にしながら読むと、Javaプログラム全体の理解へつながりますし、ここを基本と考えるとよいでしょう。

関連記事

著者: Japanシーモア編集部

Japanシーモアは、Web/IoT/APP/SYS 分野のプログラミング情報を体系的に提供するメディアです。本記事は編集部による執筆とAI支援を組み合わせて制作し、公開前に編集部が校正しています。誤りや改善案がございましたらお問い合わせよりご連絡ください。

※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。