本ページはプロモーションが含まれています

Java入門

Javaのコンストラクタを理解!基本から実践まで徹底解説

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

「Javaを学び始めたけど、コンストラクタの役割がよくわからない…」

「メソッドと何が違うの?」

「this()とかsuper()とか、似たようなものがあって混乱する」

私がJavaを学び始めた頃、つまずいたのがコンストラクタの概念でした。当時は「おまじないのようなもの」としか理解できず、何度もエラーを出しては頭を抱えたものです。

しかし、実務で多くのコードを書き、レビューをする中で、コンストラクタがオブジェクト指向プログラミングの心臓部とも言えるほど重要な役割を担っていると痛感しました。良いコンストラクタは、バグが少なく、保守性の高いコードを生み出します。

この記事は、過去の私と同じように悩んでいる「Javaのコンストラクタについて知りたい方」に向けて書きました。

この記事を読み終える頃には、あなたは次の状態になっています。

  • コンストラクタの役割と必要性を、自分の言葉で説明できる
  • コンストラクタの正しい書き方を理解し、自分でコードが書ける
  • オーバーロードやthis()を使いこなし、よりスマートなコードを設計できる

Javaの学習を次のステップに進めるために、一緒にコンストラクタをマスターしていきましょう!

コンストラクタとは?Javaの基本をおさらい

Javaにおけるコンストラクタとは、オブジェクトを生成する(インスタンス化する)ときにだけ呼び出される、特別な処理のことです。

主な役割は、オブジェクトが作られた直後に、そのオブジェクトが持つフィールドに初期値を設定すること。これにより、オブジェクトが不完全な状態になるのを防ぎ、必ず初期化された状態で利用できるようになります。

クラスとオブジェクトの関係

コンストラクタを理解するには、まずクラスとオブジェクトの関係をおさらいするのが近道です。

  • クラス: オブジェクトの「設計図」です。例えば、「車」クラスには、色や速度といった属性(フィールド)や、走る・止まるといった操作(メソッド)が定義されます。
  • オブジェクト: クラスという設計図をもとに作られた「実体」です。例えば、「赤い色の時速60kmで走る車」や「青い色の止まっている車」がオブジェクトにあたります。

この設計図(クラス)から実体(オブジェクト)を作り出す工程で、特別な組み立て作業を行うのがコンストラクタの役目なのです。

// 「車」という設計図(クラス)
class Car {
    String color; // 色
    int speed;    // 速度

    // これがコンストラクタ!
    // オブジェクトが作られるときに色と速度を設定する
    Car(String c, int s) {
        color = c;
        speed = s;
        System.out.println("色:" + color + "、速度:" + speed + "の車が作られました。");
    }
}

public class Main {
    public static void main(String[] args) {
        // new Car(...)でコンストラクタが呼び出され、オブジェクトが生成される
        Car myCar = new Car("赤", 60);
        Car yourCar = new Car("青", 0);
    }
}

このコードでは、new Car(...) と書いた瞬間にCarクラスのコンストラクタが呼び出され、それぞれの車の色と速度が設定されています。

メソッドとの違いをわかりやすく解説

コンストラクタはメソッドと形が似ていますが、明確な違いが3つあります。

比較項目コンストラクタメソッド
目的オブジェクトの初期化オブジェクトの振る舞い(操作)
戻り値ない(voidも書かない)ある(voidを含む)
呼び出し方new演算子で自動的に呼ばれるオブジェクト名.メソッド名()で呼び出す

一番大きな違いは、コンストラクタはオブジェクトが生まれる瞬間に、システムによって自動的に一度だけ呼び出される点です。一方で、メソッドは私たちの好きなタイミングで何度でも呼び出すことができます。

コンストラクタは「誕生の儀式」、メソッドは「誕生後の行動」とイメージすると分かりやすいかもしれません。

Javaのコンストラクタの書き方と基本構文

コンストラクタの書き方には、いくつかのルールがあります。ここでは、基本的な構文から、なぜクラス名と同じ名前でなければならないのか、といった理由まで掘り下げてみましょう。

コンストラクタの定義方法

コンストラクタの基本的な構文は以下の通りです。

修飾子 クラス名(引数リスト) {
    // オブジェクトの初期化処理
}
  • 修飾子: publicprivateなどのアクセス修飾子を指定します。
  • クラス名: 必ず、所属するクラスと全く同じ名前にします。
  • 引数リスト: オブジェクトの初期化に必要な値を受け取ります。不要な場合は空にします。

これがJavaの世界で「この部分はコンストラクタです」と示すための厳格なルールです。

コンストラクタ名はクラス名と同じになる理由

コンストラクタ名がクラス名と同じでなければならない理由は、Javaコンパイラ(プログラムを機械がわかる言葉に翻訳する仕組み)が、メソッドとコンストラクタを明確に区別するためです。

もし自由に名前をつけられると、どれが初期化用の特別な処理なのか判断できません。また、戻り値がないという特徴も、この区別に一役買っています。この2つのルールがあるからこそ、new演算子が使われたときに、正確に対応するコンストラクタを呼び出せるのです。

引数あり・なしの違い(デフォルトコンストラクタ)

コンストラクタには、引数がないものと、引数があるものの2種類があります。

引数なしコンストラクタ

class Book {
    String title;

    // 引数なしコンストラクタ
    Book() {
        this.title = "タイトル未設定";
        System.out.println("本が作られました。");
    }
}

引数ありコンストラクタ

class Book {
    String title;

    // 引数ありコンストラクタ
    Book(String title) {
        this.title = title; // 引数で受け取った値で初期化
        System.out.println("「" + title + "」というタイトルの本が作られました。");
    }
}

ここでひとつ、非常に重要な注意点があります。

それは、プログラマがコンストラクタを1つも定義しなかった場合、Javaコンパイラが自動的に引数なしの空のコンストラクタ(デフォルトコンストラクタ)を追加してくれることです。

// コンストラクタを何も書いていない
class Pen {
    String color;
}

// 内部的には、コンパイラが以下を追加してくれる
// Pen() {
// }

しかし、引数ありのコンストラクタを1つでも定義すると、この自動追加は行われません。そのため、引数なしでオブジェクトを作ろうとするとエラーになります。

class Pen {
    String color;

    // 引数ありコンストラクタを定義
    Pen(String color) {
        this.color = color;
    }
}

public class Main {
    public static void main(String[] args) {
        // OK: 引数ありコンストラクタを呼び出す
        Pen redPen = new Pen("赤");

        // エラー!: 引数なしコンストラクタはもう存在しない
        // Pen bluePen = new Pen();
    }
}

この仕様は初心者がつまずきやすいポイントなので、しっかり覚えておきましょう。

複数のコンストラクタを使い分ける「オーバーロード」

Javaでは、同じクラス内に引数の数や型、並び順が異なるコンストラクタを複数定義できます。これをコンストラクタのオーバーロードと呼びます。

オーバーロードを利用すると、オブジェクトを様々な方法で初期化できるようになり、クラスの柔軟性が大きく向上します。

オーバーロードの具体例

例えば、会員情報を管理するMemberクラスを考えてみましょう。

  • IDだけで仮登録したい場合
  • IDと名前で本登録したい場合
  • ID、名前、年齢で詳細に登録したい場合

など、状況によって初期化したい情報が異なります。こんなときにオーバーロードが役立ちます。

class Member {
    long id;
    String name;
    int age;

    // 1. IDだけで初期化するコンストラクタ
    Member(long id) {
        this.id = id;
        this.name = "(ゲスト)"; // 名前はデフォルト値
        this.age = -1;         // 年齢は無効な値
    }

    // 2. IDと名前で初期化するコンストラクタ
    Member(long id, String name) {
        this.id = id;
        this.name = name;
        this.age = -1;
    }

    // 3. ID、名前、年齢で初期化するコンストラクタ
    Member(long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    void printInfo() {
        System.out.println("ID: " + id + ", 名前: " + name + ", 年齢: " + age);
    }
}

引数の数・型が違うとどう動く?

呼び出す側がnew Member(...)と書いたとき、Javaは渡された引数のの組み合わせを自動的に判断し、完全に一致するコンストラクタを選んで実行します。

public class Main {
    public static void main(String[] args) {
        // Member(long) が呼び出される
        Member guest = new Member(101);
        guest.printInfo(); // -> ID: 101, 名前: (ゲスト), 年齢: -1

        // Member(long, String) が呼び出される
        Member user1 = new Member(205, "田中");
        user1.printInfo(); // -> ID: 205, 名前: 田中, 年齢: -1

        // Member(long, String, int) が呼び出される
        Member user2 = new Member(315, "鈴木", 25);
        user2.printInfo(); // -> ID: 315, 名前: 鈴木, 年齢: 25
    }
}

このように、Javaが賢く判断してくれるおかげで、私たちは状況に応じて最適な初期化方法を選ぶことができるのです。

this()を使ったコンストラクタの呼び出し方

オーバーロードは便利ですが、先ほどのMemberクラスの例では、this.id = id;this.name = name; といった同じようなコードが何度も出てきました。

このようなコードの重複をなくし、もっとスマートに記述するための仕組みがthis()です。this()を使うと、同じクラス内にある別のコンストラクタを呼び出すことができます。

this()の基本的な使い方

this()は、コンストラクタの先頭行でのみ使用できます。

先ほどのMemberクラスをthis()を使って書き換えてみましょう。初期化処理を引数が一番多いコンストラクタに集約するのがポイントです。

class Member {
    long id;
    String name;
    int age;

    // 3. メインとなるコンストラクタ(処理を集約)
    Member(long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
        System.out.println("3つの引数を持つコンストラクタが動きました。");
    }

    // 2. IDと名前で初期化するコンストラクタ
    Member(long id, String name) {
        // this(...)で、3つの引数を持つコンストラクタを呼び出す
        this(id, name, -1); // 年齢にデフォルト値を渡す
    }

    // 1. IDだけで初期化するコンストラクタ
    Member(long id) {
        // this(...)で、2つの引数を持つコンストラクタを呼び出す
        this(id, "(ゲスト)");
    }
}

このようにthis()を使うと、初期化ロジックが1箇所にまとまります。将来、「会員IDの採番ルールが変わった」といった仕様変更があっても、修正は1箇所で済み、コードの保守性が格段に向上します。

this()とsuper()の違い

this()とよく似たものにsuper()があります。この2つは混同しやすいため、違いをはっきりさせておきましょう。

  • this(): 同じクラス内の、別のコンストラクタを呼び出す。
  • super(): 親クラス(スーパークラス)のコンストラクタを呼び出す。

どちらもコンストラクタの先頭行に記述する必要があるため、this()super()を同じコンストラクタ内で同時に使うことはできません。super()については、次の章で詳しく解説します。

コンストラクタを使うメリットと注意点

コンストラクタを正しく使うことで、コードの品質は大きく向上します。ここでは、そのメリットと、特に継承(クラスの機能を別のクラスが引き継ぐこと)における注意点を解説します。

初期化処理をまとめて管理できる

コンストラクタを使う最大のメリットは、オブジェクト生成時の初期化処理を1箇所に集約できることです。

もしコンストラクタがなければ、newでオブジェクトを作った後、setterメソッドなどを使って一つ一つ値を設定しなければなりません。

// コンストラクタがない場合のコード
Product pc = new Product();
pc.setName("ノートPC");
pc.setPrice(150000); // もしこの行を忘れたら?
pc.setCategory("家電");

この方法では、必要な初期化処理を忘れてしまう可能性があります。価格が設定されていない商品オブジェクトが生まれてしまうと、後の処理でエラー(NullPointerExceptionなど)を引き起こす原因となります。

コンストラクタを使えば、必要な値を引数で受け取ることを強制できるため、初期化漏れのない、安全なオブジェクトの生成を保証できます。

new時に自動で呼ばれる仕組み

コンストラクタは、new演算子でインスタンスを生成する際に必ず自動的に呼び出されます。この「自動的」というのが重要なポイントです。

プログラマが初期化処理の呼び出しを意識する必要がなくなるため、ヒューマンエラーを防ぐことができます。クラスを設計する側は「このクラスのオブジェクトは、必ずこのコンストラクタを通って初期化される」という前提で、安心してロジックを組み立てることが可能です。

継承時の注意点(親クラスの呼び出し)

クラスの継承を使う場合、コンストラクタの動きに注意が必要です。

子クラスのオブジェクトが作られるとき、実はその内部では先に親クラスのオブジェクトが作られています。そのため、子クラスのコンストラクタが動く前に、必ず親クラスのコンストラクタが呼び出されなければなりません。

この呼び出しは、子クラスのコンストラクタの先頭でsuper()と書くことで明示的に行えますが、もし何も書かれていない場合は、コンパイラが自動的に親クラスの引数なしコンストラクタを呼び出すsuper()を挿入します

class Parent {
    Parent() {
        System.out.println("親クラスのコンストラクタが呼ばれました。");
    }
}

class Child extends Parent {
    Child() {
        // コンパイラがここに super(); を自動で追加する
        System.out.println("子クラスのコンストラクタが呼ばれました。");
    }
}

public class Main {
    public static void main(String[] args) {
        new Child();
        // 実行結果:
        // 親クラスのコンストラクタが呼ばれました。
        // 子クラスのコンストラクタが呼ばれました。
    }
}

ここで注意したいのは、親クラスに引数なしのコンストラクタ(デフォルトコンストラクタ)が存在しない場合です。この場合、コンパイラはsuper()を自動挿入できず、コンパイルエラーとなります。

class Parent {
    String name;

    // 引数ありコンストラクタしかない
    Parent(String name) {
        this.name = name;
    }
}

class Child extends Parent {
    // エラー!
    // 親クラスのParent(String)を呼び出す super(name) を明示的に書く必要がある
    Child() {
        // 暗黙的なsuper()呼び出しが失敗する
    }
}

このエラーを解決するには、子クラスのコンストラクタで、親クラスのコンストラクタを明示的にsuper("...")のように呼び出す必要があります。継承を使う際は、親クラスのコンストラクタがどうなっているかを常に意識することが重要です。

コンストラクタを使った実践コード例

理論を学んだ後は、実践的なコードで理解を深めましょう。ここでは、よく使われる2つのパターンを紹介します。

ユーザー情報クラスでの初期化例

Webアプリケーションなどで頻繁に登場する、ユーザー情報を扱うUserクラスです。IDや名前など、生成時に必須となる情報はコンストラクタで設定を強制するのが良い設計です。

さらに、フィールドをfinalにすることで、一度設定した値を変更できなくなり、より安全なクラス(不変オブジェクト)になります。

public class User {
    private final long id;      // ID (変更不可)
    private final String name;    // 名前 (変更不可)
    private String email;   // メールアドレス (変更可能)

    /**
     * Userオブジェクトを生成するためのコンストラクタ
     * @param id ユーザーID
     * @param name ユーザー名
     */
    public User(long id, String name) {
        // 値が不正でないかチェックする(ガード節)
        if (id <= 0) {
            throw new IllegalArgumentException("IDは正の数でなければなりません。");
        }
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("名前は空にできません。");
        }

        this.id = id;
        this.name = name;
        this.email = "未設定"; // emailは任意なのでデフォルト値を設定
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void printProfile() {
        System.out.println("--- ユーザー情報 ---");
        System.out.println("ID: " + this.id);
        System.out.println("名前: " + this.name);
        System.out.println("Email: " + this.email);
        System.out.println("--------------------");
    }
}

このクラスでは、コンストラクタ内でIDや名前が不正な値でないかをチェックしています。このように初期化の段階で不正なデータを弾くことで、オブジェクトが常に正しい状態であることを保証できるのです。

オーバーロードとthis()を組み合わせる例

ECサイトの商品クラスItemを例に、オーバーロードとthis()を組み合わせたスマートな実装を見てみましょう。

  • 必須項目:商品ID、商品名、価格
  • 任意項目:商品説明
public class Item {
    private final int id;
    private final String name;
    private final int price;
    private String description;

    /**
     * 全ての情報を指定して初期化するメインのコンストラクタ
     */
    public Item(int id, String name, int price, String description) {
        if (id <= 0 || name == null || name.isEmpty() || price < 0) {
            throw new IllegalArgumentException("必須項目が不正です。");
        }
        this.id = id;
        this.name = name;
        this.price = price;
        this.description = description;
    }

    /**
     * 必須項目だけで初期化するコンストラクタ
     * 商品説明はデフォルト値「説明なし」が入る
     */
    public Item(int id, String name, int price) {
        // this()を使ってメインのコンストラクタを呼び出す
        this(id, name, price, "説明なし");
    }

    public void display() {
        System.out.println(String.format("[%d] %s : %,d円", id, name, price));
        System.out.println("  " + description);
    }
}

呼び出し側のコードは次のようになります。

public class Main {
    public static void main(String[] args) {
        // 必須項目だけでオブジェクトを生成
        Item book = new Item(1001, "Java入門", 3000);

        // 全ての情報を指定してオブジェクトを生成
        Item pc = new Item(2005, "高性能ノートPC", 250000, "最新CPU搭載モデルです。");

        book.display();
        System.out.println();
        pc.display();
    }
}

このように、this()を使うことで初期化ロジックの重複がなくなり、コードが非常にスッキリしました。ロジックの修正が必要になった場合も、修正箇所が1つで済むため、メンテナンスが容易になります。

まとめ:コンストラクタを理解するとJavaの設計力が上がる

今回は、Javaのコンストラクタについて、基本から実践的な使い方までを徹底的に解説しました。

最後に、この記事の要点をまとめます。

  • コンストラクタは、オブジェクト生成時に自動で呼ばれる初期化専門の処理である。
  • クラス名と同じ名前で、戻り値がないのが書き方のルール。
  • オーバーロードで、様々な初期化パターンに対応できる。
  • this()でコンストラクタを連結し、コードの重複をなくせる。
  • コンストラクタは、オブジェクトの状態を正常に保つための重要な仕組み。

Javaのコンストラクタは、単にフィールドに値を代入するだけのものではありません。オブジェクトが「生まれてから一度も不正な状態にならない」ことを保証するための、非常に重要な砦なのです。

コンストラクタを使いこなせるようになると、クラスの設計思想が明確になり、コード全体の安定性や保守性が飛躍的に向上します。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門