Javaには、特定の場面だけに使える「ラムダ式」という記述方法があります。しかし、ラムダ式はJavaで登場が遅れたこともあり、旧式に慣れているJavaエンジニアの方はまだ習得できていないケースもあるでしょう。
そこで、ラムダ式を習得できるように、初歩的な基礎知識から実際のコード作成、実践活用のポイントを解説します。
目次
1.Javaのラムダ式とは
Java8から登場した「ラムダ式」は、クラスの記述を省略して、簡潔にコードを書ける構文のことです。
ここでの省略するクラスとは、「匿名クラス」のことを指します。つまり、匿名クラスを使った関数型インターフェースをさらに簡単にした書き方です。
ラムダ式を構文として取り入れた結果、Javaのコーディングにおいて、ListなどのCollections.sortやフィルタ処理のStream API関連の記述が楽になったのです。
ラムダの登場経緯
プログラムのラムダ式は、数学で使われる「ラムダ計算」をベースに作られています。ラムダ計算とは、「λ(ラムダ)」という記号を使った計算方法のことです。
例えば、「λx. x+1」の場合は、λで「関数を作成する」という意味があり、それを合図として変数(x)を置き、「.(ドット)」で区切って、その右側に処理内容を書きます。つまり、「xに1を足して返す」という計算をする関数を即興で作って、処理を返せるのがラムダ計算です。この場合は、「x=1」のとき「1+1」で「2」を返す関数となります。
匿名クラス(無名クラス)との違い
Javaには、「匿名クラス(無名クラス)」と呼ばれる実装方法が存在します。Java7までクラスの定義を省略したインスタンス化や実装をすることのできる手段として使われていたのです。
しかし、ラムダ式は匿名クラスをさらに簡略化し、匿名クラスを活用しやすくしています。そのため、匿名クラスの発展形がこのラムダ式です。
両者を比較すると、ラムダ式は同じ処理内容で記述量を減らせます。
匿名クラスのコード箇所:
button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("ボタンがクリックされました"); } }); |
ラムダ式のコード箇所:
button.addActionListener(e -> System.out.println("ボタンがクリックされました")); |
ラムダ式で可能なこと
Javaでプログラムを書く場合に、ラムダ式でできることは以下が挙げられます。
メソッドの代わりに、処理内容を直接書ける
CollectionのListなどを操作できる(forEach構文の簡易化など)
Stream APIとの連携処理
GUIのイベントリスナー処理実装
ラムダ式では、メソッドの代わりに、処理内容を直接書くことができます。関数型インターフェースの場合だけという使用条件は付きますが、メソッドの定義が必要なくなるため大きなメリットです。
また、上記は、コーディング作業が楽になるというメリットがあります。全体がコンパクトになって、コードが読みやすくなることにも直結するのです。
2.Javaにおけるラムダ式の基本
ラムダ式の概要を押さえたら、今度はラムダ式の基本となる構文の書き方について解説します。
ラムダ式の文法・構文
ラムダ式は、構文の規則として以下のようにコードを記述します。
(引数) -> {処理する内容} |
引数と処理の間を「->」というアロー演算子の記号でつなげて書く仕様です。
引数なし・1つ(戻り値なし)
以下は、引数なしと引数が1つ(戻り値なし)の構文の書き方です。
引数なし
() -> {System.out.println("こんにちは")}; |
引数が1つ(戻り値なし)
(String s) -> System.out.println(s) |
文字列ではなく、「数字を返して計算したい」「別の目的に使いたい」というときは、返り値を返すために、returnや戻り値のあるインターフェースを使います。ただし、引数が1つの場合は()を省略可能なため、returnが書かれていないことはあります。
引数の受け取りから引き渡し
s -> System.out.println(s) |
ラムダ式を初めて使う方は、「() ->」この形だけでラムダ式と判断するクセをつけると、()がなくてラムダ式を見落としたり、形式の違いに混乱したりします。ラムダ式は、基本的にアロー演算子「->」の有無でチェックします。これだけは省略することができません。
引数が2つ(戻り値なし)
次に、引数が2つの場合は、次のとおりです。()の中に第1引数と第2引数を「,」で区切ります。「->」の後に処理内容を書くルールは、引数が1つの場合と同じです。
(x, y) -> x + y |
ちなみに、ラムダ式が1行なら戻り値を返す場合にreturnの省略も可能です。
複数処理
最後に、複数処理の場合です。x++;で1ずつ追加する場合や条件分岐をする場合の書き方となります。
Function<Integer, Integer> complexProcess = x -> { x = x + 2; x++; return x; }; |
この複数行の複雑処理の場合に、return xは基本的に省略ができません。そのため、returnを含めて整数(<Integer, String>の場合は文字列)の返り値を書く必要があります。
それから、以下はif文で数値と文字列を入れた複数処理の場合です。
Function<Integer, String> isSumLargeEnough = x -> { x++; int sum = x + 2; if (sum >= 10) { return "sumが10以上"; } else { return "sumが10以下"; } }; |
このように、IntegerとStringで異なる型を扱える関数型インターフェースもあります。
ラムダ式の書き方(実行確認の例)
今度は基本的な構文を使った書き方です。構文はあくまでもコード全体の中で部分的に使用するため、ラムダ式の構文だけ書いても実行はできません。
実行環境で確認やデバッグをするためには、メインメソッドや他の部分を含めて全体を設計する必要があります。
例えば、引数なしで戻り値なしの文字出力するコードが以下です。
public class LambdaExample { public static void main(String[] args) { Runnable r = () -> System.out.println("これがラムダ式の書き方です"); r.run(); } } |
それから、「(x)-> x + 2」を表現した実行可能にしたコード例は以下です。
import java.util.function.Function;
public static void main(String[] args) {
} }
public static void main(String[] args) { x -> x + 2; //これだけではエラー発生 System.out.println(addTwo.apply(2)); } } |
ラムダ式の規則では、クラス内で単独では使えず、メソッド・インターフェースや変数に入れることが求められるのです。上の書き方では、Functionの関数型インターフェースを省略したことでエラーが発生します。
ラムダ式の特殊な規則・運用ルール
ラムダ式では構文の形だけでなく、特殊な規則・運用ルールを押さえておくことも必要です。なぜなら、ラムダ式には通常のクラス・メソッドにはない、記述する際の独特な規則が決まっているためです。
例えば、構文の規則だけではわからない以下のような規則・運用ルールです。
関数型インターフェースにしか使えない
「->」の前後で型指定の片方省略は不可(類推できる場合を除く)
1文でも、{}の省略はreturnがある場合に不可(returnは{}の中)
例えば、Javaではラムダ式が関数型インターフェースの利用に限定されます。これに例外はなく、メソッド参照でも不可欠です。
また、省略にはいくつかの規則・運用ルールがあり、類推して型が特定できる場合を除いて、型指定を行った場合は「->」の前後で型指定が必須となります。
加えて、returnはステートメント(文)扱いとなるため、式とはならず、{}の省略ができません。「ラムダ式は簡潔に書けるから」という理由で勝手にルール外の省略をすると実行時にエラーが出ます。
以上、特殊な規則・運用ルールを事前に押さえることが大切です。
関連記事
Java標準ライブラリ&外部ライブラリ一覧ガイド|選び方・使い方・作成方法など解説
Javaでファイルを出力!CSV出力や文字コード指定、ファイル書き込みとレポート作成のテクニック
3.Javaのラムダ式と変数の扱い
Javaのラムダ式では、ローカル変数を扱う際に、外の変数は修飾子をfinalとする必要があります。finalをつけた変数は後で変更ができません。
実質的にfinalな変数のルール
実際に修飾子を付けなくても、変数が不変であればfinalをつけたのと同じ扱いです。これを「実質的にfinal」な変数と呼びます。
import java.util.function.Consumer;
public static void main(String[] args) { String message = "はじめまして、"; //実質的にfinal変数の必要がある
System.out.println(message + " " + s); };
} } |
上記の例では、読み込んだ変数「message」をラムダ式の外で定義しています。これを後で変更してしまうと、コンパイルエラーが発生します。これはJava全体であらかじめ決まっているルールです。
もちろん、ラムダ式の内部で定義して使う変数は問題なく使えます。例えば、数値を内部で使う場合、ラムダ式の中で変数の値を新たに「int a = 10;」と決めてその場で使うことができます。なお、ラムダ式の本体は「->」の右側になります。
もし、ラムダ式の外(前後)で定義していた数値「int a = 5;」を内部で10に変更すると、実質的にfinalな変数ではなくなり、これもまたエラーとなるのです。そのため、定義した変数をインクリメントによって1ずつ追加するなどの操作もラムダ式の変数では原則できません。
配列でインクリメントが使える例外
ただし、その変数が配列やListの場合、参照して数字をラムダ式内でインクリメントなどを使って変更することはできます。ラムダ式で配列を使う場合、直接的に「new int[]{1}」などで配列を定義し直した場合は、エラーとなるのでNGです。
4.Javaのラムダ式の詳細な使い方や使いどころ
次に、Javaでラムダ式を使う際の具体的なコード作成の手順や方法について、使い道のタイプごとに紹介します。
関数タイプの使い方
まずは基本となるFunctionやConsumerなどの関数型インターフェースと一緒に利用する使い方です。代表的なインターフェースは以下の4つです。
Predicate
Function
Consumer
Supplier
Predicateは、引数を1つ取ってbooleanの真偽値を返します。条件の判定やフィルターとしての役割でよく出てくる関数型インターフェースです。
2つ目のFunctionは、型変換など文字列と数値などの異なる型にしたいケースで使われます。
3つ目のConsumerは、外部で動作する(これを「副作用」と呼ぶ)ことを前提とした戻り値のない関数型の代表的なインターフェースです。acceptでオブジェクトを受け取る抽象メソッドを1つ持ちます。
最後のSupplierは、引数を持たないString型などの「T型」の結果を返す役割のある関数型インターフェースです。文字の出力や日付の遅延生成による取得ができます。この遅延生成とは、値が必要なときまで使わずに、あえて処理を遅らせリソースを確保することです。
これらの4つの特徴をあらわしたコード例は以下です。
import java.util.function.*;
public static void main(String[] args) {
Predicate<String> isEmpty = s -> s.isEmpty(); System.out.println("isEmpty(\"\") = " + isEmpty.test(""));
Function<String, Integer> length = s -> s.length(); System.out.println("length(\"Java\") = " + length.apply("Java"));
Consumer<String> print = s -> System.out.println("出力: " + s); print.accept("ラムダ式のConsumer");
Supplier<String> hello = () -> "これがSupplier"; System.out.println(hello.get()); } } |
Javaのラムダ式をStream APIに使う方法
2つ目は、JavaのStream APIでラムダ式を使う方法です。Stream APIは、Collectionなどの要素を格納したインターフェースと一緒に使う方法となります。
代表的なメソッドは以下の4つです。
filter
forEach
sorted
map
これらを踏まえ、各メソッドを扱う場合のListとラムダ式のコード例を取り上げます。
import java.util.*; import java.util.stream.*;
public static void main(String[] args) { List<String> names = Arrays.asList("Ai", "Iyo", "Momo", "Akari");
List<String> filtered = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList()); System.out.println("filterの処理後: " + filtered);
System.out.print("forEachの処理後: "); names.forEach(name -> System.out.print(name + " ")); System.out.println();
List<String> sorted = names.stream() .sorted((a, b) -> b.compareTo(a)) .collect(Collectors.toList()); System.out.println("sorted (desc)の処理後: " + sorted);
List<Integer> lengths = names.stream() .map(name -> name.length()) .collect(Collectors.toList()); System.out.println("map (length)の処理後: " + lengths); } } |
filterのコードでは、名前のリストから、Aで始まる名前だけを出力するフィルター処理しています。forEachのコードでは、名前を横に並べた表示の指示です。sortedのコードは、ソート機能で逆順の指示をしています。mapのコードは、名前の長さを測定して、その昇順で名前を出力するものです。
これらのメソッドで関数の機能を使いつつ、端的にインターフェースを記述して使えます。
Javaのラムダ式をスレッドAPIに使う方法
スレッドAPIでラムダ式を使う場合は、バックグラウンドの処理に関連した並行処理や非同期処理のためのコードで使います。代表的な関数型インターフェースのスレッドAPIは以下の2つです。
Runnable
Callable
Runnableは、マルチスレッドの動作処理を指示するスレッドAPIです。スレッドAPIはスレッド操作のインターフェースにはRunnableのような関数型もあります。Runnableの場合、「Thread.start()」メソッドで処理を開始すると、runメソッドが呼び出される仕組みです。
public class Main { public static void main(String[] args) { Runnable task1 = () -> System.out.println("Runnableで実行中"); new Thread(task1).start(); } } |
上記の例では、Runnableで同時に実行する新規スレッドを1つ作成するコードです。Runnableの構文箇所はわずか2行の記述となります。
ラムダ式を使わない場合、Runnableの記述はかなり増えます。
public class Main { public static void main(String[] args) { Runnable task1 = new Runnable() { @Override public void run() { System.out.println("Runnableで実行中"); } }; new Thread(task1).start(); } } |
ラムダ式が記述を大きく減らしていることがわかるでしょう。
次に、Callableは戻り値や例外処理ができるスレッドAPIです。以下の例では、戻り値を返す場合に、ラムダ式で実装を簡略化しています。
import java.util.concurrent.*;
public static void main(String[] args) throws Exception { Callable<String> task = () -> "Callableの戻り値";
System.out.println(result.get()); } } |
まずCallableのクラスライブラリconcurrentをインポートして、call()メソッドの記述を省略できるラムダ式で書きます。ラムダ式が1行で書ける場合に、戻り値のあるCallableでも式の値が戻り値となるためはreturnの省略も可能です。
イベントの処理
ラムダ式は、イベントの処理のコンパクトな記述にも使えます。
以下は、画面作成とボタンの用意、そしてボタンをクリックすると「ボタンがクリックされました」と表示される簡単なコード例です。
import javax.swing.*;
public static void main(String[] args) { JFrame frame = new JFrame("Lambdaイベント処理"); JButton button = new JButton("クリック");
frame.setSize(300, 150); frame.getContentPane().add(button); frame.setVisible(true); } } |
Swingを活用して、GUIのウインドウやボタンのコード作成の効率を高めています。ちなみに、上記でラムダ式により省略した箇所は、以下のコードを短くしたものです。
button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("ボタンがクリックされました"); } }); |
メソッド参照
ラムダ式には、「メソッド参照」と呼ばれる関数型インターフェースの変数にメソッドを入れて記述を簡略できる方法があります。メソッド参照には構文が用意されており、「::」で区切るのが文法規則です。
クラス名::メソッド名
クラス名をオブジェクト名にする場合もあります。例えば、Consumerの関数型インターフェースで書いた場合は次のとおりです。
Consumer<String> printer = Main::printMessage; |
右辺の「Main(クラス名)::printMessage(メソッド名)」でメッセージ出力の意図を端的に表すことができます。ただし、メソッド参照は、使い方によっては内容が重複して記述が長くなります。
そこで以下の例は、ラムダ式の記述をさらに短くできるケースです。
import java.util.*;
public static void main(String[] args) { List<String> names = Arrays.asList("Akari", "Mika", "Yaeko");
names.forEach(System.out::println); } } |
import java.util.*;
public static void main(String[] args) { List<String> names = Arrays.asList("Akari", "Mika", "Yaeko");
names.forEach(name -> System.out.println(name)); } } |
関連記事
Javaプログラミング能力認定試験とは?詳細や難易度、対策方法について解説
5.Javaのラムダ式でよくある質問
ここからは、Javaのラムダ式の基礎や実践的に利用する際によくある質問に回答します。
Stream APIはラムダ式のほうが良い理由は?
Stream APIでは、ラムダ式で書く方が良いといわれています。コードが簡潔になって、相性も良いからです。
通常、並列処理にはコードが増えるため、ラムダ式なら簡潔になりますし、メソッドチェーンでは、メソッドを並べて記述できるので可読性や保守も向上します。そのため、全体的にラムダ式のほうが匿名クラスや通常クラスで書くよりもメリットが大きくなります。
ベストプラクティスで書くために押さえるポイントは?
ラムダ式でベストプラクティスのコードを作成したいときは、可読性やパフォーマンスを考慮しつつ、デバッグを行いやすいようにコード設計することです。
特殊なルールから外れないように基本を押さえて、メモリリークへの配慮や読み取りの際の共通ルールなども示し合わせておくと、記述を省略するラムダ式では特に有効な方法です。
例えば、複雑な処理を1つにまとめ過ぎない(デバッグ最適化)、外部参照を最小限にする(メモリリーク対策)などです。
ラムダ式で書くべきでないケースは?
ラムダ式は記述が簡略化できる反面、複雑な処理を一度に書くことには向いていません。また、再利用して使う場合にも適しておらず、あくまでもラムダ式はその場限りで使う使い捨てのコードです。
匿名クラスでの利用ということもあり、1度使うと他でもまた新たに作って使う必要があります。そして、初心者に起こりえるのが、ラムダ式を関数型インターフェース以外で使うことです。これはコンパイルエラーとなります。Javaでラムダ式を使うときは必ず、関数型インターフェースと一緒に使うことが前提条件です。
6.まとめ
今回は、Javaのラムダ式について基本構文からコードの書き方、さまざまな使い方まで紹介しました。ラムダ式は関数型インターフェースの匿名クラスで使える記述方法で、通常のメソッドを書く場合よりも短くコードを書けます。
特にStream APIやスレッドAPIでは欠かせない手法です。また、メソッド参照のように、さらに短く書く方法もあります。
ラムダ式の新APIが追加される中で、今後もラムダ式は活躍の場面を広げるための記述スキルとなってくるでしょう。
本記事が皆様にとって少しでもお役に立てますと幸いです。
「フリーランスボード」は、数多くのフリーランスエージェントが掲載するITフリーランスエンジニア・ITフリーランス向けの案件・求人を一括検索できるサイトです。
開発環境、職種、単価、稼働形態、稼働日数など様々な条件から、あなたに最適なフリーランス案件・求人を簡単に見つけることができます。
単価アップを目指す方や、自分の得意なスキルを活かせる案件に参画したい方は、ぜひ「フリーランスボード」をご利用ください。
自身に最適なフリーランスエージェントを探したい方はこちらよりご確認いただけます。