はじめに:new だけじゃない!Java のオブジェクト生成を見直す

この記事は、「Java で new してばかりで設計が硬直している」「new をやめたいが代案がわからない」「DI コンテナは便利そうだけど中身がイメージできない」という中級者の方を対象にしています。
読み進めることで、new 演算子の背後で何が起きているか、Factory パターンや DI コンテナがどう「生成」を隠蔽するか、リフレクションを使った動的生成の実装パターンまで、一連の「インスタンス生成テクニック」が脳内にマップとして浮かぶようになります。今後、クラス設計やテスト戦略を考える際に「どこで、誰が、どうやって生成するか」を意識したコードが書けるようになるでしょう。

前提知識

  • Java の基本文法(クラス、コンストラクタ、継承、インタフェース)
  • 例外処理の基礎(try-catch 構文)
  • ジェネリクスの記法(型パラメータ <T> が読める程度)

new してもよいが、それだけが手段ではない

Java を学び始めたとき、私たちは「クラス名 変数名 = new コンストラクタ()」という呪文を何度も唱えます。しかし、コードベースが大きくなり、変更に弱い設計を目の当たりにすると「new を減らしたい」「生成ロジックを外だししたい」と考えるはずです。
なぜ new が設計を硬直化させがちなのか、バイトコードレベルで new がどんな命令列を生成するのか、そして new を「置き換える・遅延する・外部化する」ことで得られる柔軟性を概観します。

new 演算子の内部から Factory・DI・リフレクションまで

new が裏でやっている 5 つのステップ

  1. クラスローディング確認
    JVM は対象クラスが既に Method Area へロード済みかをチェック。未ロードなら親デリゲーション・モデルでクラスローダが探索を開始します。

  2. メモリ確保
    ヒープ(通常は Eden 領域)でインスタンス分のメモリを確保。サイズはクラスのフィールド情報から計算済みなので、確保は単純なポインタ移動です。

  3. ゼロクリア
    フィールドのデフォルト値(0, null, false)でメモリを初期化。これにより、未初期化のまま参照されることを防ぎます。

  4. コンストラクタ呼び出し
    <init> としてコンパイルされるコンストラクタが実行されます。ここでフィールドに初値をセットし、不変条件を整えます。

  5. スタックへの参照返却
    スタックのローカル変数にオブジェクト参照が載り、プログラムが利用可能になります。

ステップ1:new をラップする簡易 Factory

Java
public class UserFactory { private UserFactory() {} // ユーティリティクラス化 public static User create(String name, int age) { // バリデーションを集約 if (name == null || name.isBlank()) throw new IllegalArgumentException("name must not blank"); return new User(name, age); } }

メリット
・生成ロジックの切り替えが 1 点で済む(例:キャッシュ返却やサブクラスに差し替え)
・バリデーションが重複しない
・new が隠蔽されるため、テストでモック差替が容易

ステップ2:依存性注入(DI)コンテナに任せる

Spring / CDI / Micronaut など、ほとんどの DI コンテナは「どこでも new しない」ことを前提に設計されています。

  1. インタフェースで依存を定義
Java
public interface PaymentService { void pay(int amount); }
  1. 実装クラスに @Component@Singleton を付与
Java
@Component public class CreditPaymentService implements PaymentService { ... }
  1. 利用側はコンストラクタで要求するだけ
Java
@Service public class OrderService { private final PaymentService payment; public OrderService(PaymentService payment) { this.payment = payment; } }

DI コンテナ起動時、フレームワークがリフレクションでコンストラクタを読み、必要なインスタンスを自動生成・注入します。
生成ルールは@Scope@Beanメソッドでカスタマイズ可能。
単体テスト時はOrderService testTarget = new OrderService(mockPayment);と差し替えられるため、テスタビリティも向上。

ステップ3:リフレクションでクラス名動的生成

設定ファイルや外部ストレージにクラス名を書いておき、実行時に動的に生成したいケースがあります。

Java
String className = props.getProperty("parser.class"); Class<?> clazz = Class.forName(className); Parser parser = (Parser) clazz.getDeclaredConstructor().newInstance();

注意点
getDeclaredConstructor()で非公開コンストラクタも呼べるが、setAccessible(true)が必要
・パフォーマンスは JIT 最適化で改善されるが、ループ内で毎回リフレクションを呼ぶとオーバーヘッドが大きい
・セキュリティマネージャが有効な環境ではReflectPermissionが必要

ハマった点:コンストラクトプロキシと循環依存

Spring で「クラス A がクラス B をコンストラクタ注入、クラス B もクラス A を注入」という循環参照を作ってしまったところ、BeanCurrentlyInCreationExceptionが発生。
根本的に循環依存は設計不良だが、既存コードを大きく書き換えられない場合は、次の 2 択を検討。

  1. セッター注入に切り替えて Spring に循環を解決させる(推奨されない)
  2. インタフェースを 2 つに分割し、双方向が単方向になるように責務を再設計(推奨)

解決策

今回は 2 を採用。A → Bの依存はBServiceインタフェース、B → Aの依存はAServiceインタフェースを別途定義し、それぞれ片方向に注入。結果として循環は解消され、テストも片方ずつスタブ化できるようになった。

まとめ

本記事では、Java における「インスタンス生成」を new 一辺倒から脱却し、Factory パターン、DI コンテナ、リフレクション動的生成という 3 つの実践的テクニックを解説しました。

  • new はバイトコードレベルで 5 ステップを経てメモリ上にオブジェクトを作る
  • 生成ロジックを Factory に閉じ込めることで変更・テストが容易
  • DI コンテナに生成を委ねることで、ライフサイクルと依存方向が宣言的に管理できる
  • リフレクションは動的生成の最終手段だが、パフォーマンス・セキュリティを考慮すべき

この知識を活かし、今後は「どこで誰が生成するか」を設計段階で意識することで、変更に強く、テストしやすいコードが書けるでしょう。次回は「オブジェクトプーリングや Valhalla プロジェクトによる値型」がテーマ予定です。

参考資料