Java | Java 詳細・モダン文法:ラムダ式・関数型 – コンストラクタ参照

Java Java
スポンサーリンク

コンストラクタ参照を一言でいうと

コンストラクタ参照(ClassName::new)は、

new クラス名(...) を、そのまま“関数”として渡すための短い書き方」

です。

ラムダ式で書くと

() -> new User()
s -> new User(s)
(a, b) -> new User(a, b)
Java

のようになるものを、

User::new
Java

の一言で表現できます。

「インスタンスを“作る処理”を、関数型インターフェース(Supplier, Function, BiFunction など)として渡したい」
というときに使います。


まずは「ラムダと 1 対 1 対応している」感覚をつかむ

引数なしコンストラクタと Supplier<T>

引数なしコンストラクタを持つクラスから始めましょう。

class User {
    String name;
    User() {
        this.name = "no-name";
    }
}
Java

これを「必要になったときに User を作る工場」として渡したいとします。

ラムダ式で書くとこうです。

import java.util.function.Supplier;

Supplier<User> s1 = () -> new User();
User u1 = s1.get();
Java

これをコンストラクタ参照にすると、こう書けます。

Supplier<User> s2 = User::new;
User u2 = s2.get();
Java

User::new

() -> new User()
Java

と完全に同じ意味です。

ここで押さえてほしいのは、

「コンストラクタ参照は“対応するラムダ式を短く書いたもの”に過ぎない」

ということです。

引数 1 つのコンストラクタと Function<A, R>

今度は、引数付きコンストラクタを見てみましょう。

class User {
    String name;
    User(String name) {
        this.name = name;
    }
}
Java

これを「String を渡すと User を作る関数」として扱いたい。

ラムダ式ならこうです。

import java.util.function.Function;

Function<String, User> f1 = name -> new User(name);
User u = f1.apply("Alice");
Java

コンストラクタ参照ならこう。

Function<String, User> f2 = User::new;
User u2 = f2.apply("Bob");
Java

User::new は今度は

name -> new User(name)
Java

というラムダと対応しています。

つまり、

Supplier<User> に代入される User::new
() -> new User()

Function<String, User> に代入される User::new
name -> new User(name)

というように、「左側の関数型インターフェースのシグネチャ」によって、
「どのコンストラクタとして解釈されるか」が決まります。


関数型インターフェースとコンストラクタ参照の組み合わせ

0 引数:Supplier<T> と対応

さきほどの通り、引数なしコンストラクタは Supplier<T> です。

Supplier<User> userFactory = User::new;
User u = userFactory.get();
Java

形としては () -> T です。

1 引数:Function<A, R> と対応

引数 1 つのコンストラクタは Function<引数型, クラス型> に対応します。

class User {
    User(String name) { ... }
}

Function<String, User> f = User::new;
User u = f.apply("Alice");
Java

形としては A -> R です。

2 引数以上:BiFunction や独自の関数型インターフェース

引数 2 つのコンストラクタなら BiFunction<A, B, R> と相性が良いです。

import java.util.function.BiFunction;

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

BiFunction<String, Integer, User> userCreator = User::new;

User u = userCreator.apply("Alice", 20);
Java

コンストラクタが引数 3 つ以上なら、標準の関数型インターフェースには直接対応するものがないので、
自分で @FunctionalInterface を定義しても構いません。


Stream とコンストラクタ参照:現場でよく見るパターン

map(User::new) で「値からオブジェクトを作る変換」

例えば、「名前のリストから User のリストを作る」処理。

import java.util.List;
import java.util.function.Function;

class User {
    final String name;
    User(String name) {
        this.name = name;
    }
    @Override public String toString() { return name; }
}

public class MapConstructorExample {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob");

        List<User> users =
                names.stream()
                     .map(User::new)   // name -> new User(name)
                     .toList();

        System.out.println(users);     // [Alice, Bob]
    }
}
Java

map(User::new) は、内部的には

.map(name -> new User(name))
Java

と同じです。

見慣れてしまえば、「名前から User を作っているんだな」と一瞬で読み取れます。

Stream.generate(User::new) でインスタンスを供給し続ける

Supplier としてコンストラクタ参照を渡すと、「新しいインスタンスをどんどん作るストリーム」が簡単に作れます。

import java.util.function.Supplier;
import java.util.stream.Stream;

class User {
    static int seq = 1;
    final int id;
    User() {
        this.id = seq++;
    }
    @Override public String toString() { return "User#" + id; }
}

public class GenerateExample {
    public static void main(String[] args) {
        Supplier<User> userFactory = User::new;

        Stream.generate(userFactory)
              .limit(3)
              .forEach(System.out::println);
    }
}
Java

Stream.generate(User::new) は、内部で new User() を呼び続けます。


コンストラクタ参照が「嬉しい」ポイントを深掘りする

1. 「インスタンスを作るコード」を引数に渡せる

コンストラクタ参照を使うと、

「どのクラスのインスタンスを、どのコンストラクタで作るか」

を “値として” 渡せます。

例えば、「N 回オブジェクトを作るユーティリティ」を書きたい場合。

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class RepeatFactory {
    public static <T> List<T> createList(int n, Supplier<T> factory) {
        List<T> list = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            list.add(factory.get());
        }
        return list;
    }
}
Java

使う側はこう書けます。

List<User> users = RepeatFactory.createList(3, User::new);
Java

User::new の部分を差し替えるだけで、「何を生成するか」を自由に変えられるわけです。

2. テストや差し替えがしやすい

例えば、「本番では RealClient を使うが、テストでは FakeClient を使いたい」という状況。

class Service {
    private final Supplier<Client> clientFactory;

    Service(Supplier<Client> clientFactory) {
        this.clientFactory = clientFactory;
    }

    void doWork() {
        Client client = clientFactory.get();
        // client を使って処理
    }
}
Java

本番コードでは:

Service service = new Service(RealClient::new);
Java

テストコードでは:

Service service = new Service(FakeClient::new);
Java

コンストラクタ参照により、「具象クラスの切り替え」を引数レベルで簡単に行えるようになります。


コンストラクタ参照とオーバーロードの注意点

どのコンストラクタが選ばれるかは「左側の型」で決まる

クラスにコンストラクタが複数ある場合、
どのコンストラクタ参照として解釈されるかは「代入先(または引数)の関数型インターフェースのシグネチャ」で決まります。

class User {
    User() {}
    User(String name) {}
}
Java

このとき:

Supplier<User> s = User::new;          // () -> new User()
Function<String, User> f = User::new;  // (s) -> new User(s)
Java

のように、同じ User::new でも、
どの get/apply に合わせるかでコンストラクタが変わります。

逆に言うと、「どれにも合わない」シグネチャに代入しようとするとコンパイルエラーになります。


「ラムダで書いたらどうなるか」を常に意識する

コンストラクタ参照を読むとき・書くときは、
頭の中で一度「等価なラムダ式」に変換してみると理解が深まります。

User::new を見たら、

Supplier<User> に代入されている → () -> new User()
Function<String, User> に代入されている → s -> new User(s)
BiFunction<String, Integer, User> に代入されている → (name, age) -> new User(name, age)

のように、「どんな関数として振る舞うのか」を即座にイメージできるようになると、
コンストラクタ参照はもう怖くありません。


まとめ:コンストラクタ参照を自分の言葉で整理する

コンストラクタ参照(ClassName::new)は、

new ClassName(...) を、ラムダ式 (...) -> new ClassName(...) の代わりに短く書いたもの」

です。

特に意識しておきたいのは、

引数なし → Supplier<T>() -> new T()
引数 1 つ → Function<A, T>a -> new T(a)
引数 2 つ → BiFunction<A, B, T>(a, b) -> new T(a, b)
map(User::new), Stream.generate(User::new) のように、Stream と組み合わせてよく使う
「インスタンス生成の方法」を引数として受け取る設計(ファクトリ・遅延生成・テスト差し替え)と相性が良い

という点です。

タイトルとURLをコピーしました