Java | オブジェクト指向:インスタンス生成(new)

Java Java
スポンサーリンク

インスタンス生成の全体像

インスタンス生成は「クラスという設計図から、具体的な“もの”をメモリ上に作る」行為です。Java では new クラス名(引数…) を呼ぶと、必要なメモリが確保され、フィールドが初期化され、コンストラクタが実行されます。生成されたオブジェクトは参照(変数)に入れて使い、不要になればガベージコレクション(GC)が自動で回収します。


コンストラクタと new の基本

コンストラクタの定義と呼び出し

コンストラクタは「初期状態を確立するメソッドの特別版」です。戻り値型は書かず、クラス名と同名で定義します。

public final class User {
    private final String id;
    private String name;

    public User(String id, String name) {       // コンストラクタ
        if (id == null || id.isBlank()) throw new IllegalArgumentException("id");
        this.id = id;
        this.name = name == null ? "" : name.trim();
    }
}
Java

呼び出しは new User("U-1", " Taro ") のように行い、生成直後から「検証された正しい状態」を持ちます。

オーバーロードと共通化(this/super)

複数の初期化パターンがあるなら、コンストラクタをオーバーロードし、共通処理へ集約します。

public User(String id) { this(id, ""); }        // this(...) で別コンストラクタへ委譲
Java

継承時は親の初期化を super(...) で先に呼びます(コンストラクタの先頭で必須)。

public class Employee extends User {
    private final int grade;
    public Employee(String id, String name, int grade) {
        super(id, name);                        // 先に親の初期化
        this.grade = grade;
    }
}
Java

初期化の順序とフィールドの扱い(重要ポイントの深掘り)

new の裏側で起きること

  1. メモリ確保(フィールド分)
  2. フィールドのデフォルト初期化(数値は0、参照はnull、booleanはfalse)
  3. フィールド初期化式の実行(書いていれば)
  4. 親→子の順でコンストラクタ実行(super(...) → 自分)

この順序を理解すると「null のまま使う」「未初期化」の事故を避けられます。

public final class Counter {
    private int value = 10; // 初期化式
    public Counter() { value++; }   // 11 になる
}
Java

不変フィールド(final)と安全性

final フィールドは「コンストラクタ完了までに一度だけ代入」され、以後変更できません。並行性やテストで強力な安全性を提供します。

public final class Token {
    private final String value;
    public Token(String v) { this.value = v; }  // 以後不変
    public String value() { return value; }
}
Java

便利な生成パターンと注意点

ファクトリメソッドとバリデーションの集中

生成規則が複雑なら「名前付きの生成メソッド」に集約します。意図が伝わり、例外方針も一箇所で管理できます。

public final class Email {
    private final String value;
    private Email(String v) { this.value = v; }
    public static Email of(String raw) {
        var v = normalize(raw);
        if (!isValid(v)) throw new IllegalArgumentException("invalid email");
        return new Email(v);
    }
}
Java

ビルダーパターン(多引数・可読性の向上)

引数が多い/オプションだらけなら、順不同で設定できるビルダーが有効です。

public final class Mail {
    private final String to, subject, body;
    private Mail(String to, String subject, String body) { /* ... */ }
    public static class Builder {
        private String to, subject, body;
        public Builder to(String v) { this.to = v; return this; }
        public Builder subject(String v) { this.subject = v; return this; }
        public Builder body(String v) { this.body = v; return this; }
        public Mail build() { return new Mail(to, subject, body); }
    }
}
// 生成
var mail = new Mail.Builder().to("a@b").subject("Hi").body("Hello").build();
Java

new の特殊形(配列/匿名クラス/内部クラス)

  • 配列は new 型[長さ] またはリテラル {...} で作成します。
int[] a = new int[3];      // 0,0,0
String[] s = { "A", "B" }; // リテラル
Java
  • 一度きりの動作差し替えは、匿名クラスで new しながら実装を埋め込めます。
Runnable r = new Runnable() { public void run() { System.out.println("go"); } };
Java
  • 非 static の内部クラスは「外側のインスタンスに紐づく」ため、outer.new Inner() と書きます。
class Outer { class Inner {} }
var o = new Outer();
Outer.Inner in = o.new Inner();
Java

new String の落とし穴

文字列は不変で、リテラルのプール共有が効くため、new String("abc") は不要かつ非効率です。通常は "abc" または演算で十分です。


メモリ・ライフサイクルと GC の基礎

参照と到達可能性

オブジェクトは参照が届く限り生きています。参照が切れ、到達不能になると GC が回収します。

User u = new User("U-1", "A");
u = null;               // 到達不能 → いつかGCにより回収
Java

大量生成や長寿命の参照を持ちすぎるとメモリ圧迫(リーク)を招くため、コレクションからの削除やキャッシュの期限を設計します。

finalize は使わない、try-with-resources を使う

外部リソース(ファイル・ソケット)は「GC任せ」ではなく、明示的にクローズします。

try (var br = java.nio.file.Files.newBufferedReader(java.nio.file.Path.of("data.txt"))) {
    System.out.println(br.readLine());
} // ブロック終端で自動クローズ
Java

例題で身につける

例 1: 生成と検証の一体化

public final class Money {
    private final int amount;
    private final String currency;
    public Money(int amount, String currency) {
        if (amount < 0) throw new IllegalArgumentException("amount>=0");
        if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
        this.amount = amount;
        this.currency = currency;
    }
}
var m = new Money(100, "JPY");      // OK
// var bad = new Money(-1, "JPY");  // 例外で拒否
Java

例 2: 親子の初期化順序

abstract class Base {
    protected final String id;
    public Base(String id) { this.id = id; }
}
final class Item extends Base {
    private final int price;
    public Item(String id, int price) {
        super(id);                 // 先に親
        if (price < 0) throw new IllegalArgumentException();
        this.price = price;
    }
}
Java

例 3: レコードと通常クラスの違い(生成の簡素化)

public record Point(int x, int y) {}
var p = new Point(3, 4); // コンストラクタ・equals・hashCode・toString が自動
Java

通常クラスで同等の機能を作るより、値オブジェクトは record が簡潔で安全です。


よくあるつまずきと回避

コンストラクタで検証しない

不正な初期状態は後で爆発します。必ず検証して、例外で拒否するか、ファクトリで握る。

乱暴な共有(static 多用)

共有状態が増えるとテスト困難・バグ増。生成は注入(コンストラクタ)に集約し、必要な依存だけ渡す。

無駄な生成とコピー

new String や都度 new SimpleDateFormat などの高コスト生成は避け、必要ならスレッド安全な代替(DateTimeFormatter)や再利用を検討。


仕上げのアドバイス(重要部分のまとめ)

new は「メモリ確保→初期化→コンストラクタ」の順でオブジェクトを生み、以後は参照で扱う。初期状態の検証をコンストラクタ(またはファクトリ)に集め、final で不変を保つ。継承時は super(...) を先に呼び、初期化順序を理解する。大量生成・共有状態・クローズ忘れがトラブルの源——ビルダーや record、try-with-resourcesを使い、読みやすく安全な生成を習慣にしよう。

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