概要
コンストラクタの引数が5つ6つと増えていくと、呼び出し側で「何番目の引数が何なのか」が分からなくなります。順序を間違えてもコンパイルが通る型が同じ引数(String が3つ並ぶなど)は特に危険です。Builder パターンは、名前付きメソッドで段階的にフィールドを設定し、最後に build() で不変オブジェクトを生成する構造を作ります。必須フィールドは Builder のコンストラクタで強制し、任意フィールドにはデフォルト値を設定できるため、呼び出し側のコードが自己文書化されます。HTTP リクエストを題材に Builder の基本構造と build() 時の検証(必須漏れ・不正値をここで止める)を示し、Java 標準ライブラリの StringBuilder や HttpClient.newBuilder が同じ構造であることも確認します。
使いどころ
HTTP リクエストの URL・メソッド・ヘッダー・ボディ・タイムアウトを段階的に設定し、不変のリクエストオブジェクトを生成する
メール送信で宛先(必須)・CC・BCC・件名・本文・添付ファイル(任意)を Builder で組み立てる
検索条件オブジェクト(キーワード・日付範囲・ソート順・ページサイズ)を Builder で構築し、条件の組み合わせを柔軟に表現する
コード例
public class BuilderPatternSample {
static class HttpRequest {
private final String url;
private final String method;
private final String body;
private final int timeoutMs;
private final boolean followRedirect;
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.followRedirect = builder.followRedirect;
}
@Override
public String toString() {
return "HttpRequest{url=" + url + ", method=" + method
+ ", timeout=" + timeoutMs + "ms}";
}
static class Builder {
private final String url; // 必須
private String method = "GET"; // 任意(デフォルト値あり)
private String body = "";
private int timeoutMs = 30000;
private boolean followRedirect = true;
public Builder(String url) {
if (url == null || url.isEmpty()) {
throw new IllegalArgumentException("URL は必須");
}
this.url = url;
}
public Builder method(String m) { this.method = m; return this; }
public Builder body(String b) { this.body = b; return this; }
public Builder timeout(int ms) { this.timeoutMs = ms; return this; }
public Builder followRedirect(boolean f) { this.followRedirect = f; return this; }
// 検証は build() に集約する。ここを通らない限り不正なオブジェクトは作れない
public HttpRequest build() {
if (timeoutMs <= 0) {
throw new IllegalArgumentException(
"タイムアウトは正の値を指定: " + timeoutMs);
}
return new HttpRequest(this);
}
}
}
public static void main(String[] args) {
var req = new HttpRequest.Builder("https://api.example.com/users")
.method("POST")
.body("{\"name\":\"田中\"}")
.timeout(5000)
.followRedirect(false)
.build();
System.out.println(req);
// → HttpRequest{url=https://api.example.com/users, method=POST, timeout=5000ms}
// 最小構成: 任意フィールドはデフォルト値のまま
var minimal = new HttpRequest.Builder("https://api.example.com/health")
.build();
System.out.println(minimal);
// → HttpRequest{url=https://api.example.com/health, method=GET, timeout=30000ms}
// 必須フィールド漏れは組み立て時点で止まる
try {
new HttpRequest.Builder("").build();
} catch (IllegalArgumentException e) {
System.out.println("組み立て失敗: " + e.getMessage()); // 組み立て失敗: URL は必須
}
// 不正値も build() で止まる(0 や負のタイムアウトが後工程に流れない)
try {
new HttpRequest.Builder("https://api.example.com")
.timeout(0)
.build();
} catch (IllegalArgumentException e) {
System.out.println("組み立て失敗: " + e.getMessage()); // 組み立て失敗: タイムアウトは正の値を指定: 0
}
}
}Version Coverage
var でメソッドチェーンの記述が簡潔になる。生成対象を record にすれば equals・toString が自動生成される。
// Java 17: var で簡潔に
var req = new HttpRequest.Builder("https://api.example.com")
.method("POST")
.body("{\"name\":\"田中\"}")
.timeout(5000)
.build();Library Comparison
注意点
Builder のフィールドを mutable にしたまま build() 後も変更できると、生成済みオブジェクトの不変性が壊れる。build() 後の Builder 再利用を禁止するか、フィールドを final にする
必須フィールドの検証は build() メソッド内で行うのが確実。Builder のコンストラクタで強制する方法もあるが、引数が増えると結局テレスコーピングコンストラクタの問題が再発する
Builder パターンはフィールドが4つ以上ある場合に有効。2〜3フィールドならコンストラクタやファクトリーメソッドで十分な場合が多い
Lombok の @Builder は便利だが、生成されるコードが見えにくい。チーム内で Lombok の採否が決まっていない場合は手書きの Builder から始めるほうが安全
実務では引数の順序を取り違えてもコンパイルが通るコンストラクタが多く残っている。「String が3つ並んでいる」「null を渡している」といった箇所を見つけたら Builder への置き換えを検討すること。
FAQ
フィールドが4つ以上、または任意フィールドが多い場合は Builder が有効です。2〜3フィールドで全て必須なら通常のコンストラクタで十分です。
record はコンストラクタ引数が全フィールドなので、引数が増えると Builder の恩恵があります。record の中に static な Builder クラスを定義するパターンが実用的です。
Fluent Interface はメソッドチェーンの書き方を指し、Builder はオブジェクト生成のパターンです。Builder は Fluent Interface を使うことが多いですが、概念としては別物です。