1.はじめに
先日、Oracle認定資格の「Java Gold SE11」に合格しました。
この記事はその振り返り兼、備忘録としての学習記録になります。
第1弾のテーマは「ラムダ式」です✨
私はこれまで業務でJavaを使った経験がなく、今後の使用を見据えてJava Goldを受験しました。中上級者向けの資格ということもあり、
各トピックの理解にはかなり時間を要しましたが、中でも特に苦戦したのがラムダ式です。
Java Silverを受験した際にも一度ラムダ式を学習しましたが、Java未経験者の私には取っつきづらく、理解が追いつかないまま出題数の少なさを理由に深追いせず流してしまいました。(理解することを諦めました😵💫)
ところがJava Goldでは、ラムダ式はもちろん、それを前提としたストリームAPIが頻出トピックとして登場します。合格のためには避けて通れないと痛感し、腰を据えて学習した結果、なんとか理解できるようになりました!🔥
本記事では、一度挫折した私が「どこでつまずいたか」、そして「どう意識したら理解できたか」を踏まえつつ、ラムダ式の概要をまとめていきます。
2.そもそもラムダ式とは
ラムダ式とは、関数型インターフェースの実装を簡潔に書けるようにした構文です。
従来、インターフェースの実装クラスを再利用せず、その場限りで使用する場合は
「匿名クラス」を利用していました。Java 8以降では、ラムダ式を用いることで
「処理(関数)」を直接渡せるようになり、コードの記述量を大幅に削減できます。
※匿名クラスとラムダ式の記述方法の違いについては、後述の「② 関数型インターフェースについて」で解説します。
3.関数型インターフェースについて
関数型インターフェスの特徴は以下の通りです。
☑️抽象メソッドが1つだけ定義されている関数型インターフェース
→ だだしデフォルトメソッドとstaticメソッドはいくつあってもOK。
☑️@FunctionalInterface
アノテーションで明示できる
→ 書かなくても動作はするが、付けておくと抽象メソッドが2つ以上あると
コンパイルエラーになり安全。
☑️ラムダ式で実装を省略できるインターフェース
→ ラムダ式は「関数型インターフェースを実装する処理」を簡潔に書いたもの。
また、単一抽象メソッドを持つ関数型インターフェースを自作することも可能ですが、
java.util.function
パッケージにあらかじめいくつかのパターン添った関数型
インターフェースが定義されています。
試験で出題される主要な関数型インターフェースは以下の通りです。
インターフェース名 | 抽象メソッド | 説明 |
---|---|---|
Supplier<T> | T get() | 引数を受け取らず、戻り値を返す |
Consumer<T> | void accept(T t) | 引数を1つ受け取り、戻り値を返さない |
BiConsumer<T,U> | void accept(T t, U u) | 引数を2つ受け取り、戻り値を返さない | Function<T,R> | R apply(T t) | 引数を1つ受け取り、戻り値を返す |
BiFunction<T,R,U> | R apply(T t) | 引数を2つ受け取り、戻り値を返す |
Predicate<T> | boolean test(T t) | 引数を1つ受け取り真偽値を返す |
BiPredicate<T> | boolean test(T t, U u) | 引数を2つ受け取り真偽値を返す |
4.関数型インターフェースの実装方法
関数型インターフェースを実装する方法としては、以下の3つが挙げられます。
①具象クラスを定義して実装する
→ インターフェースを implements したクラスを定義し、抽象メソッドをオーバーライドして使用する。
再利用性が高く、何度も同じ処理を使いたい場合に有効です。
②匿名クラスを利用する
→ その場限りでクラスを定義せずに、インターフェースを直接オーバーライドして使用する方法。簡便ですが、記述 はやや冗長になりがちです。
③ラムダ式を利用する
→ Java 8 以降で導入された方法で、関数型インターフェースに対して「処理(関数)」を直接渡す形で書けます。
最も簡潔で可読性が高いため、現在は主流の書き方になっています。
ではそれぞれの違いを見ていきましょう!
例1)↓具象クラスを定義して実装した書き方↓
//具象クラスを定義する場合
import java.util.function.Function;
// Functionをimplementsしたクラスを定義
class StringLengthFunction implements Function<String, Integer> {
@Override
public Integer apply(String s) {
return s.length();
}
}
public class Main {
public static void main(String[] args) {
Function<String, Integer> func = new StringLengthFunction();
System.out.println(func.apply("Hello")); // 5が出力される
}
}
例2)↓匿名クラスを利用した書き方↓
//匿名クラスを使用した場合
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, Integer> func = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
};
System.out.println(func.apply("Hello")); // 5が出力される
}
}
例3)↓ラムダ式を利用した書き方↓
//ラムダ式を使用場合
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, Integer> func = s -> s.length();
System.out.println(func.apply("Hello")); // 5が出力される
}
}
比較すると匿名クラスでは数行かかる処理が、ラムダ式なら1行で済むことがわかります。
5.ラムダ式を使用する際の注意点
☑️省略記法について
ラムダ式には記述を簡潔にできるよう省略記法がありますが、いくつか省略ルールがあります。
まず基本構文は以下の通りです。
(引数リスト) -> { 処理 };
⭐️省略可能部分
①引数の型
→ インターフェースの宣言時に引数のは決定しているため、型推論が可能
②引数が1つなら括弧を省略できる
→ 引数がない場合や複数ある場合は省略不可。
③処理が1文の場合波括弧とreturnを省略できる
→ 省略する場合は波括弧とreturnを両方省略しなければならない。
どちらか一方のみを省略した場合はエラーになる。
例1) ↓Function<String, Integer>の場合↓
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
// 基本形(型と括弧とreturnあり)
Function<String, Integer> f1 = (String s) -> { return s.length(); };
// ①引数の型を省略
Function<String, Integer> f2 = (s) -> { return s.length(); };
// ②引数が1つなので括弧も省略
Function<String, Integer> f3 = s -> { return s.length(); };
// ③処理が1文なので {} と return を省略
Function<String, Integer> f4 = s -> s.length();
System.out.println(f4.apply("Hello")); // 5
}
}
例2) ↓エラーになる書き方↓
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
// 引数が複数あるのに括弧を省略するとエラー
Function<String, Integer> f1 = s1, s2 -> s1.length(); //エラー
// 処理が複数(returnが)あるのに {} を省略するとエラー
Function<String, Integer> f2 = s -> return s.length(); //エラー
// return を使う場合は必ず {}
Function<String, Integer> f3 = s -> { return s.length(); }; //正しい
}
}
☑️実質的fainal
ラムダ式内で参照できるローカル変数には制約があります。
参照できるのは「final」または「実質的にfinal」の変数のみ
→「実質的にfinal」とは、再代入していない変数を指します。
なので以下のような場合はエラーとなります。
public class Main {
public static void main(String[] args) {
String msg = "Hello";
Runnable r = () -> System.out.println(msg);
msg = "NG!"; // 再代入しているためコンパイルエラーになる
r.run();
}
}
6.私がラムダ式を理解する上で躓いた点
冒頭でもお話しした通り、私は一度ラムダ式に挫折しました。
その時は以下のようなことを思ってました。
・「なんでメソッドを書かなくても動くの?」
・「引数の a とか b ってなんやねん」
・「結局なにをしているのかわからない、こんがらがる…」
匿名クラスに比べて急にコードが省略されるので、「何が省略されているのか」
が見えにくく、理解が進まないまま「なんか動くけどよくわからない」という
状態に陥っていました😵💫
7.理解するために意識したこと
☑️インターフェースの実装方法を3パターンで比較する
実装クラス、匿名クラス、ラムダ式の3つを並べて「どこが省略されているか」を
対応させて理解する。
☑️ラムダ式は「メソッド」ではなく「処理を引数として渡す仕組み」と捉える
代入できるのは「関数型インターフェース(抽象メソッドが1つだけ)」に限定。
→ 「抽象メソッドに対応する処理だけを渡している」と理解。
☑️省略されている部分を意識する
・メソッド名 → インターフェースが知っているから省略
・引数の型 → コンパイラが推論するから省略
・return / {} → 文法ルールで省略可能
☑️主要な関数型インターフェースを暗記する
・Supplier → 値を供給する
・Consumer → 値を消費する
・Function → 値を変換する
・Predicate → 真偽判定する
また、頭に「Bi」が付くと引数が2つになる。
これらを押さえることで、ラムダ式の適用場面がすぐイメージできるようになりました。
☑️アロー演算子 ->
を「入力 → 出力」としてイメージする
例えば(x, y) -> x + y
は「入力x, yを受け取って、その合計を返す」という流れ。
この視点を持ったことで、ラムダ式を「数式的なマッピング表現」として理解でき、
構文がスッキリ見えるようになりました。
☑️動画を活用する
参考書で読んでいるだけではイメージがつかみにくかったのですが、
YouTubeで動画解説を見たら一気に理解が進みました。
特に 黒本の筆者の方が運営している「優しくないJava」チャンネル のラムダ式の解説はとてもわかりやすくてオススメです!
黒本著者が教える やさしくない!? Java
8.終わりに
ラムダ式は最初こそ取っつきにくい構文ですが、ポイントを押さえて理解すれば大きな得点源になります。
一つひとつ仕組みを丁寧に紐解いていくことで理解が深まり、私自身もJavaGoldの学習を通じて、今では一番得意なトピックになりました。
試験本番でもラムダ式の問題が出てきたときは「よっしゃ!得点源だ!」と思えるほど自信を持てるようになったのは大きな成長でした。
次回は「ストリームAPI」についてまとめる予定です。
最後まで読んでいただき、ありがとうございました!🙌