Javaのラムダ式の書き方を徹底解説!アロー演算子->の基本と使い方
生徒
「Javaのラムダ式って聞いたことあるんですが、難しそうでよくわかりません…」
先生
「ラムダ式は、Java8から追加された機能で、コードをすっきり書ける便利な書き方ですよ。」
生徒
「どうやって書くんですか?関数と何が違うんですか?」
先生
「いい質問ですね。ラムダ式の書き方や使い方、アロー演算子について具体的に見ていきましょう!」
1. ラムダ式とは?Javaで関数のように使える書き方
Javaのラムダ式(Lambda Expression)は、メソッドを1つだけ持つ
関数型インタフェースの実装を、必要な場面でその場でサッと書ける記法です。
無名クラスよりも記述が短く、読みやすさと意図の伝わりやすさがぐっと上がります。
「この引数を受け取ったら、こう処理する」という“動き”を値のように扱える、と考えると掴みやすいです。
たとえばRunnableやComparatorなどに使うと、同じ処理でもコード量が目に見えて減ります。
// まずは無名クラス:動くけれど少し長い
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("こんにちは(無名クラス)");
}
};
// ラムダ式:同じ意味をすっきり表現
Runnable r2 = () -> System.out.println("こんにちは(ラムダ式)");
r1.run();
r2.run();
上の例は「引数なし・戻り値なし」の処理をその場で渡しています。
ラムダ式は基本的に(引数) -> { 処理 }の形。
引数が1つで処理が1行なら、()や{}を省略してより短く書けます。
まずは「無名クラスで書けるところは、ラムダ式で短く書ける」と覚えて、手を動かして違いを体感してみましょう。
2. ラムダ式の基本構文とアロー演算子の意味
Javaのラムダ式は以下のような構文で書きます。
(引数) -> { 実行する処理 }
この中の->(アロー演算子)が「引数」と「処理」をつなぐ役割を持ちます。ラムダ式の読み方は、「引数を受け取って処理を実行する」という意味になります。
たとえば、1つの引数を受け取り、それを2倍にする処理は以下のようになります。
x -> x * 2
3. ラムダ式の具体例:スレッド処理
Javaでスレッドを使うとき、Runnableを使います。従来の無名クラスだと以下のようになります。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("スレッド実行中");
}
});
thread.start();
これをラムダ式で書くと、次のように非常にシンプルになります。
Thread thread = new Thread(() -> System.out.println("スレッド実行中"));
thread.start();
4. ラムダ式と関数型インタフェース
ラムダ式は「関数型インタフェース」があるから使えます。関数型インタフェースとは、抽象メソッドが1つだけのインタフェースのことです。
たとえば、以下のように自分でインタフェースを作ることもできます。
@FunctionalInterface
interface Greet {
void sayHello(String name);
}
このインタフェースを使って、ラムダ式で書くと次のようになります。
Greet greet = (name) -> System.out.println("こんにちは " + name);
greet.sayHello("田中さん");
こんにちは 田中さん
5. ラムダ式とメソッド参照の違いと使い分け
ラムダ式と似た文法に「メソッド参照」があります。これはラムダ式がそのままメソッド呼び出しになる場合に使えます。
// ラムダ式
list.forEach(item -> System.out.println(item));
// メソッド参照
list.forEach(System.out::println);
両方の書き方は同じ動作ですが、可読性や簡潔さの点でメソッド参照が選ばれることもあります。
6. ラムダ式の省略ルール:初心者が覚えておきたいポイント
Javaのラムダ式には、いくつか省略できるルールがあります。
- 引数が1つのときは括弧
()を省略できる - 処理が1行だけなら
{}とreturnを省略できる
// フル構文
(String name) -> { return "Hello " + name; }
// 省略構文
name -> "Hello " + name
このように、ラムダ式は簡潔に書けるのが魅力です。
7. ラムダ式とストリームAPIの相性の良さ
ラムダ式は、JavaのStream APIと一緒に使われることが非常に多いです。データの処理を「宣言的」に書けるようになり、読みやすいコードになります。
List<String> names = Arrays.asList("田中", "鈴木", "佐藤");
names.stream()
.filter(name -> name.startsWith("田"))
.forEach(System.out::println);
田中
このように、ラムダ式はデータの加工やフィルタ処理にぴったりの機能です。
8. ラムダ式が使える代表的な関数型インタフェース
Javaには、標準で使える関数型インタフェースが多数用意されています。以下はよく使われるものです。
Runnable:引数なし、戻り値なしCallable<T>:引数なし、戻り値ありConsumer<T>:引数あり、戻り値なしFunction<T, R>:引数あり、戻り値ありPredicate<T>:条件判定(true/false)
9. ラムダ式を使うときの注意点
ラムダ式は便利ですが、使いすぎると可読性が下がることもあります。また、ラムダ式の中で変数を使うときは、事実上final(再代入しない変数)である必要があります。
String prefix = "こんにちは";
list.forEach(name -> System.out.println(prefix + name));
ここでprefixを後から変更しようとするとコンパイルエラーになります。
まとめ
振り返ってみると、ラムダ式は「かっこいい新文法」ではなく、言いたいことだけを短く書くための道具でした。
アロー演算子で「入力 → 処理」の流れをはっきり示せるので、無名クラスで埋もれていた意図が表に出てきます。
現場では、まず既存のfor文や無名クラスのrun()やcompare()を置き換えるところから始めると、効果を実感しやすいはずです。
ラムダ式を使うときの合い言葉
- 短く書けるなら短く、迷いそうなら丁寧に。 一行で読めないと感じたらすぐブロックへ。
- 役割で選ぶ。 変換は
Function、判定はPredicate、実行はRunnable、消費はConsumer。 - “そのまま呼ぶだけ”はメソッド参照。
System.out::printlnやString::lengthは手癖にしてしまう。 - 外側の変数は再代入しない。 迷ったら別の値にして返す方針で。
小さな置き換え例(実務でよくあるやつ)
// "TODO:" を含む行だけ拾って件数を数える
java.util.List<String> lines = java.util.Arrays.asList(
"fix: refactor module",
"TODO: add error handling",
"docs: update readme",
"TODO: write tests"
);
long todo = lines.stream()
.filter(s -> s.startsWith("TODO:"))
.peek(System.out::println) // 手元で様子を見たいときの“チラ見”
.count();
System.out.println("件数 = " + todo);
「まずは拾う → 必要なら変える → 最後に数える」。この順番で考えると、Stream APIとラムダ式の相性の良さがすぐ分かります。
“あるある”なつまずきと直し方
- つい一行に詰め込みすぎる。 → 条件や例外が増えたら迷わずブロックにし、途中の処理に名前を付ける。
- 無名クラスのクセが抜けない。 → まずは
forEachとComparatorの二箇所だけをラムダ式にする習慣を。 - 型を書くか悩む。 → 基本は推論に任せ、読み手が迷いそうなら型を明示。判断の基準は常に「読みやすさ」。
メソッド参照への自然な置き換え
// 置き換え前
list.forEach(x -> System.out.println(x));
// 置き換え後(意味が同じなら、こちらの方が目に優しい)
list.forEach(System.out::println);
「引数をそのまま渡して呼ぶだけ」になっている瞬間が目印です。map(String::trim)やsorted(String::compareTo)のように、
既存メソッドに名前を任せれば、読む人は処理の内容を一瞬で思い出せます。
現場メモ:読みやすさを守る小技
- 変換の列は「絞る → 変える → 並べる → 集める」の順に並べる。
- 長い条件はメソッドに逃がし、
filter(this::isTarget)のように名札を付ける。 peekはデバッグに便利だが、残しっぱなしにしない。最終的には消す。
仕上げの練習(手を動かして慣れる)
// 名前の前後空白を整え、重複を消し、五十音順で上位三件を表示
java.util.List<String> raw = java.util.Arrays.asList(" 田中", "佐藤", " 佐藤 ", "鈴木", " 田口 ");
raw.stream()
.map(String::trim)
.filter(s -> !s.isEmpty())
.distinct()
.sorted()
.limit(3)
.forEach(System.out::println);
ここでも「順番」がすべてです。後から読む人にとって、工程が上から下へ素直に流れていることが何よりの安心材料になります。
生徒
「一行で書き切ろうとして読みづらくなることが多かったんですが、ブロックに戻していいと分かって肩の力が抜けました。」
先生
「短さより読みやすさが正義です。最初は最小形、複雑になったら丁寧形、この切り替えができれば十分。」
生徒
「メソッド参照のタイミングも掴めました。printlnやlengthは素直に置き換えていきます。」
先生
「その調子。filter・map・sortedの並びも意識して、工程を“見える化”していきましょう。」
生徒
「次は既存コードの無名クラスを少しずつ置き換えてみます。まずはforEachとComparatorから。」
先生
「いい入り口です。小さい成功を積み上げれば、チームのコード全体が読みやすくなりますよ。」