メソッド参照 ::

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • メソッド参照 String::length はラムダ s -> s.length() の省略記法
  • 種類は次の通り ─ static (Integer::parseInt) / instance (s::length) / クラスメソッド (String::length) / コンストラクタ (ArrayList::new)
  • コードが短くなり意図が明確
  • ラムダで書けるところはほぼ全てメソッド参照にできる

メソッド参照 とは

ラムダ式をさらに短く書ける「メソッド参照 Class::method」の 4 種類と使い分けを学び、Stream + String::toUpperCase で文字列を大文字化して連結するコードを書いてみよう。

ラムダ式の次の一歩、メソッド参照 ::

前のレッスンで FunctionPredicate といった 関数型インターフェース をラムダ式で書く方法を見てきました。たとえば「文字列を大文字に変える」処理は、Function<String, String> f = s -> s.toUpperCase(); のように書けました。よく見ると、このラムダは「引数の s を受け取って、その toUpperCase() を呼んでいるだけ」です。同じことを String::toUpperCase と書けば、もっとシンプルになります。これが本レッスンのテーマである メソッド参照 です。

ラムダ式は「その場で関数を作る」記法、メソッド参照は「すでにあるメソッドを名前で指し示す」記法と覚えると分かりやすいです。指している中身は同じでも、後者の方が短く読みやすくなる場面が多いのが特徴です。

メソッド参照の構文は クラス名やインスタンス名::メソッド名 というたった一行です。:: (コロンを 2 つ並べた記号) が目印で、Java 8 から導入されました。Stream APIOptionalCompletableFuture のような現代の Java ライブラリでは、ラムダ式と並んでメソッド参照を使うのが当たり前になっています。

diagram (will load when visible)

4 種類のメソッド参照

メソッド参照は形が一つしかないように見えて、実は対応するメソッドの種類によって 4 パターンに分かれます。最初に全体像を押さえておくと、後で混乱しません。次の表に整理しました。

種類構文ラムダ式での書き方
static メソッド参照Class::staticMethod(a, b) -> Class.staticMethod(a, b)Integer::parseInt
特定インスタンスのメソッド参照instance::methodx -> instance.method(x)System.out::println
任意インスタンスのメソッド参照Class::instanceMethods -> s.method()String::toUpperCase
コンストラクタ参照Class::newx -> new Class(x)User::new

これらは全部「:: の左右に何を書くか」だけが違います。実行されるのは普通のメソッド呼び出しなので、難しい仕組みが裏で動いているわけではありません。コンパイラがラムダに展開してくれていると思えば気が楽になります。

static メソッド参照

もっとも分かりやすいのが static メソッド参照です。Class::staticMethod という形で、対応するラムダは (args) -> Class.staticMethod(args) です。

Java

import java.util.List; import java.util.stream.Collectors; public class Demo { public static void main(String[] args) { List<String> nums = List.of("1", "2", "3"); List<Integer> parsed = nums.stream() .map(Integer::parseInt) // s -> Integer.parseInt(s) と同じ .collect(Collectors.toList()); System.out.println(parsed); // [1, 2, 3] } }

Integer.parseInt(String)String を受け取って int を返す static メソッドです。Function<String, Integer> の型として Integer::parseInt を渡すと、map の中で parseInt(s) を呼ぶラムダがそのまま完成します。

特定インスタンスのメソッド参照

次は、すでに作られている特定のオブジェクトのメソッドを参照するパターンです。変数::method の形で、よく使うのが System.out::println です。

Java

import java.util.List; public class Demo { public static void main(String[] args) { List<String> names = List.of("Alice", "Bob", "Carol"); names.forEach(System.out::println); // x -> System.out.println(x) と同じ } }

System.out はすでに存在する PrintStream のインスタンスです。System.out::println は「この System.out というオブジェクトの println を、引数だけ渡して呼んでね」という意味になります。forEach の引数は Consumer<T> なので、String を受け取って void を返す println(String) と型が一致します。

任意インスタンスのメソッド参照

少し頭を使うのが、Class::instanceMethod という書き方です。クラス名を書いているのに static メソッドを呼ぶわけではない、というのが最初混乱します。これは「型 Class のレシーバを後から渡して、その上で instanceMethod を呼ぶ」という意味です。

Java

import java.util.List; import java.util.stream.Collectors; public class Demo { public static void main(String[] args) { List<String> words = List.of("hello", "world"); List<String> upper = words.stream() .map(String::toUpperCase) // s -> s.toUpperCase() と同じ .collect(Collectors.toList()); System.out.println(upper); // [HELLO, WORLD] } }

String::toUpperCase は、ラムダで書くと s -> s.toUpperCase() です。s がレシーバ (. の左側) になり、その上で toUpperCase() が呼ばれる、という対応関係です。今回の課題でもまさにこの形を使います。

「クラス名 :: インスタンスメソッド」は、レシーバが引数として渡される、という独特の読み方をします。慣れるまでは「String s を受け取って s.toUpperCase() を呼ぶラムダ」と頭の中で展開すると安全です。

コンストラクタ参照

最後はコンストラクタ参照です。Class::new と書くと、その場で new Class(...) を呼ぶ関数になります。

Java

import java.util.List; import java.util.stream.Collectors; class User { private final String name; User(String name) { this.name = name; } public String toString() { return "User(" + name + ")"; } } public class Demo { public static void main(String[] args) { List<String> names = List.of("Alice", "Bob"); List<User> users = names.stream() .map(User::new) // x -> new User(x) と同じ .collect(Collectors.toList()); System.out.println(users); // [User(Alice), User(Bob)] } }

User::newFunction<String, User> として振る舞います。引数の String がそのままコンストラクタに渡され、戻り値の User がそのまま返ります。配列のコンストラクタ参照 String[]::new も同じノリで、サイズを受け取って配列を作る IntFunction<String[]> になります。

可読性とラムダの境界線

メソッド参照とラムダ式は、できることはほぼ同じです。それでも Stream パイプラインのような短い処理では、メソッド参照の方がノイズが減って読みやすくなります。たとえば次の Before / After を比べてみてください。

Java

// Before: ラムダ式 List<Integer> lens1 = words.stream() .map(s -> s.length()) .collect(Collectors.toList()); // After: メソッド参照 List<Integer> lens2 = words.stream() .map(String::length) .collect(Collectors.toList());

読み手は String::length を見るだけで「Stringlength を取っている」と一瞬で理解できます。一方、ラムダの中で複数の処理を行うとき (s -> s.trim().toUpperCase() など) は、メソッド参照には書き換えられません。ラムダの中身が「メソッドを 1 つ呼ぶだけ」のときに限ってメソッド参照に書き換える、と覚えておけば大きく外しません。

無理にメソッド参照に寄せるとかえって読みにくくなります。たとえば OptionalorElseGet(() -> defaultValue()) のように、引数の数が合わない場面ではラムダのままにする方が安全です。

よくある間違い

メソッド参照は強力ですが、ラムダから書き換えるときにいくつか落とし穴があります。代表的な 3 つを押さえておきましょう。

  • 引数の数が合わないBiFunction<Integer, Integer, Integer> に対して Integer::parseInt を渡すとコンパイルエラーです。parseInt は 1 引数なので、2 引数のインターフェースには使えません。ラムダで書いたときに渡している引数の数と、メソッドの引数の数を必ず一致させることを意識しましょう
  • 戻り値の型が一致しないFunction<String, Integer> のところに Function<String, Long> を返すメソッド参照を渡そうとすると incompatible types のエラーになります。Long::parseLongInteger::parseInt を混同しがちなので、戻り値の型まで揃っているか確認してください
  • コンストラクタ参照を ClassName.new と書く — 正しくは ClassName::new です。.new は内部クラスのインスタンス生成用の別文法なので、ここで使うと意味が変わってしまいます。::new という記号の並びをそのまま覚えるのが安全です

もう一つよくあるのが、String::toUpperCase(s) -> String.toUpperCase(s) と書こうとしてエラーになるパターンです。toUpperCasestatic ではなくインスタンスメソッドなので、String.toUpperCase(s) という呼び出しは存在しません。任意インスタンス参照Class::instanceMethod は、見た目の文法と内部の呼び出され方が違うことに注意してください。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • Stream のメソッドチェーンlist.stream().map(String::trim).filter(String::isEmpty).toList()
  • Comparatorusers.sort(Comparator.comparing(User::getName))
  • CollectorsCollectors.toMap(User::getId, User::getName) のキー / 値抽出
  • 並列処理executor.submit(this::doWork) で自身のメソッドを別スレッドで実行

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • ラムダで書いてある場所x -> x.toString()Object::toString で書ける、修正提案
  • 型推論できないメソッド参照 ─ オーバーロードが多いと String::valueOf がどれか曖昧になる場合があり、ラムダに戻すことも
  • 自身のメソッドを this::method で渡す ─ コンストラクタ参照と区別が付くか、レビューで確認される
  • プライベートメソッドを参照 ─ アクセス権の問題で予期せず public 化していないか

パフォーマンス考慮事項

  • ラムダと同等 ─ JIT が同じバイトコードに最適化
  • キャプチャしないメソッド参照は再利用String::length のような static / クラスメソッド参照はインスタンスが 1 つに集約
  • インスタンスメソッド参照はキャプチャs::lengths を保持するためのオブジェクト生成あり
  • コンストラクタ参照は new と同等ArrayList::new() -> new ArrayList() と同じコスト
この章のポイント

ここまでの要点 メソッド参照 Class::method はラムダの簡潔形。Stream / Comparator / Collectors と相性抜群。キャプチャの有無で GC コストが変わる。

やってみよう

それでは課題に挑戦しましょう。Solution.toUpperJoined(String... arr) を実装します。シグネチャは String... (可変長引数) ですが、メソッドの中では String[] arr とまったく同じように扱えます。やることは次のとおりです。

  1. Arrays.stream(arr) で配列を Stream<String> にする
  2. mapString::toUpperCase を渡して、各要素を大文字に変換する
  3. Collectors.joining(","), ではなく , (区切り文字のみ) で連結する
  4. 結果の Stringreturn する

引数なし (空配列) で呼ばれたときは、joining(",") が空文字 "" を返すので特別な分岐は要りません。Stream パイプラインを素直に組めば自動的に対応できます。

まずはラムダ式で .map(s -> s.toUpperCase()) と書いてみて、それからメソッド参照に置き換えて見比べると理解が深まります。String::toUpperCase という短い表現に置き換えられた瞬間、Stream のコードがぐっと読みやすくなる感覚を掴んでください。次のレッスンではこのメソッド参照を、ComparatorComparator.comparing(User::getName) のような並び替えで活かしていきます。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は Function インターフェース で、Function インターフェース を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. メソッド参照 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. メソッド参照 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Arrays.stream(arr) で配列から Stream<String> を作ること
  2. mapString::toUpperCaseメソッド参照 を渡して大文字化すること (ラムダ s -> s.toUpperCase() でも可だが、メソッド参照が望ましい)
  3. Collectors.joining(",") を使って , 区切りで連結した結果を返すこと

入出力例

test-cases.txt

toUpperJoined("abc", "def")"ABC,DEF" toUpperJoined()"" toUpperJoined("hello")"HELLO" toUpperJoined("Java", "Stream", "Lambda")"JAVA,STREAM,LAMBDA" toUpperJoined("a", "", "b")"A,,B"

ヒント

main.java
main.java
学習モード

メモ

メソッド参照 ::

⌘S で保存