概要
コンストラクタの引数が5つ6つと増えていくと、呼び出し側で「何番目の引数が何なのか」が分からなくなります。順序を間違えてもコンパイルが通る型が同じ引数(String が3つ並ぶなど)は特に危険です。Builder パターンは、名前付きメソッドで段階的にフィールドを設定し、最後に build() で不変オブジェクトを生成する構造を作ります。必須フィールドは Builder のコンストラクタで強制し、任意フィールドにはデフォルト値を設定できるため、呼び出し側のコードが自己文書化されます。この記事では HTTP リクエストとメールメッセージを題材に Builder パターンの基本構造を示し、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; }
public HttpRequest build() { 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);
// 最小構成(必須フィールドのみ)
var minimal = new HttpRequest.Builder("https://api.example.com/health")
.build();
System.out.println(minimal);
// 標準ライブラリの Builder 例
var sb = new StringBuilder()
.append("Hello, ").append("Builder").append("!");
System.out.println(sb.toString());
}
}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 から始めるほうが安全
FAQ
フィールドが4つ以上、または任意フィールドが多い場合は Builder が有効です。2〜3フィールドで全て必須なら通常のコンストラクタで十分です。
record はコンストラクタ引数が全フィールドなので、引数が増えると Builder の恩恵があります。record の中に static な Builder クラスを定義するパターンが実用的です。
Fluent Interface はメソッドチェーンの書き方を指し、Builder はオブジェクト生成のパターンです。Builder は Fluent Interface を使うことが多いですが、概念としては別物です。