コンストラクタ参照を一言でいうと
コンストラクタ参照(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();
JavaUser::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");
JavaUser::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]
}
}
Javamap(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);
}
}
JavaStream.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);
JavaUser::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 と組み合わせてよく使う
「インスタンス生成の方法」を引数として受け取る設計(ファクトリ・遅延生成・テスト差し替え)と相性が良い
という点です。
