PR

Java:クラスの継承(extends)をわかりやすく3分で解説

Java

「継承」とは既存のクラスの機能を再利用し、新しいクラスを作成するための重要なオブジェクト指向プログラミングの概念です。

ザックリいえば、クラスを進化させて新たなクラスを作りますよ!というのが継承です。

参考 Javaのクラスとオブジェクト

「継承」を理解し適切なコーディングを行うことで、コード再利用性の向上、メンテナンスの容易化などが可能になります。このページでは具体的なサンプルコードを用いながら継承の概念を理解し、継承を利用する際のコツや注意点をご説明します。

スポンサーリンク

クラスの継承とは?

継承とは、既存のクラスの機能や特性を引き継いで新しいクラスを作ることを指します。

既存のクラスを「親クラス or スーパークラス」と呼び、そこから派生して作られた新しいクラスを「子クラス or サブクラス」と呼びます。継承を利用すると、親クラスの持つメソッドやフィールドをそのまま使用できるため、コードを一から書く必要がなくなり、非常に便利です。

Java 継承
図1:継承

例えば「動物」という親クラスがあり、その中に「食べる」という動作を定義しているとします。ここで「動物」クラスを継承して「犬」という子クラスを作成すると、「犬」クラスでも「食べる」という動作をそのまま利用することができます。また、「犬」クラスに特有の「吠える」という動作を追加することもできます。

継承を使うことで、共通の機能を親クラスにまとめ個々のクラスにはそのクラス固有の機能だけを追加する、ということができるようになります。こうすることで、コードの再利用性が高まり、変更が必要な場合も親クラスだけを修正すれば良いためメンテナンスが非常に楽になります。

つまるところ、継承はオブジェクト指向プログラミングの超・重要な概念なので、継承をうまく利用できないと、効率的なプログラム作成はできないということになります。

実際にイメージを深めるためには実際のコードを確認していくのが良いので、ここからは早速継承を利用したクラスの定義方法を解説していきます。

継承の構文ルール:extends

Javaで継承を使う際の基本的な構文は非常にシンプル。新しいクラスを定義するときに、既存のクラスを継承するためにextendsキーワードを使用するだけ。

class 子クラス extends 親クラス {
    // 子クラスのフィールドとメソッド
}

具体的なサンプルコードを見てみましょう。まず「動物」という親クラスを作成します。このクラスには「食べる」というメソッドが含まれています。

class Animal {
    void eat() {
        System.out.println("This animal eats.");
    }
}

次にこの「動物」クラスを継承して「犬」という子クラスを作成します。このタイミングでextendsキーワードを使用して、継承元の親クラスを指定します。

class Dog extends Animal {
    void bark() {
        System.out.println("The dog barks.");
    }
}

この例では「犬」クラスは「動物」クラスを継承しています。そのため「犬」クラスは「動物」クラスの「食べる」メソッドをそのまま使用することができます。同時に「犬」クラスには「吠える」という新しいメソッドを利用できるようになります。

実際にこの「犬」クラスを使ってみると、以下のようになります。

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // 継承した「食べる」メソッドを呼び出す
        myDog.bark(); // 新しく追加した「吠える」メソッドを呼び出す
    }
}

// 出力結果
// This animal eats.
// The dog barks.

このように、継承を使うことで親クラスの機能をそのまま再利用しつつ新しい機能を追加することができます。これによりコードの重複を避け、保守性の高いプログラムを作成することができます。

継承の構文は簡潔でわかりやすいため、オブジェクト指向プログラミングを学ぶ上で非常に重要な要素です。継承を正しく理解し、適切に活用することで、効率的で拡張性の高いプログラムを書くことができます。

ここから、継承に関する実践的な応用知識をご説明していきます。

メソッドのオーバーライド

メソッドのオーバーライドとは、親クラスに定義されたメソッドを子クラスで再定義することを指します。→つまり、子クラスは親クラスの基本的な動作を変更したり、拡張したりすることができるということ。

オーバーライドを行うためには、親クラスと同じメソッド名同じ引数リスト、そして同じ戻り値の型(=同じシグニチャのセット)を持つメソッドを子クラスで定義します。また、オーバーライドするメソッドには@Overrideアノテーションを付けることが推奨されます。これは、メソッドのオーバーライドが正しく行われているかをコンパイル時にチェックするためです。

Q
アノテーションとは?
A

Javaプログラムに追加する特別な「注釈」のこと。コードにメタデータ(データに関するデータ)を提供するために使われます。アノテーションを使うと、コードの一部に特定の情報を付加し、その情報を基にコンパイラや実行環境が特定の動作をするように指示できます。

具体的な例を見てみましょう。まず、「動物」という親クラスに「食べる」メソッドを定義します。

class Animal {
    void eat() {
        System.out.println("This animal eats.");
    }
}

次に、「犬」という子クラスでこの「食べる」メソッドをオーバーライドしてみます。

class Dog extends Animal {
    @Override
    void eat() {
        System.out.println("The dog eats dog food.");
    }
}

この例では、「犬」クラスは「動物」クラスの「食べる」メソッドをオーバーライドし、犬がドッグフードを食べるという動作を表現しています。

実際にこの「犬」クラスを使ってみると、以下のようになります。

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // オーバーライドした「食べる」メソッドを呼び出す
    }
}

// 出力結果
// The dog eats dog food.

このように、親クラスの「食べる」メソッドが「This animal eats.」というメッセージを出力するのに対し、子クラスでオーバーライドされた「食べる」メソッドは「The dog eats dog food.」というメッセージを出力します。

メソッドのオーバーライドを利用することで、親クラスの基本的な動作を引き継ぎつつ、子クラスで特定の動作をカスタマイズすることができます。これにより、プログラムの柔軟性が高まり、特定の状況に応じた動作を簡単に実装することができます。

オーバーライドは、オブジェクト指向プログラミングにおける多態性(ポリモーフィズム)を実現するための重要な手法です。より詳しく学習したい方はこちらJavaオーバーライドの基本

親クラスのメソッドを再定義することで、コードの再利用性を維持しつつ、必要に応じて動作を変更できるため、非常に強力な機能と言えます。

superキーワードの使用

superキーワードは、親クラスのメソッドやコンストラクタにアクセスするために使用されます。→つまり、子クラス内で親クラスのメソッドを呼び出したり、親クラスのコンストラクタを明示的に呼び出すことができます。

具体的な例を見てみましょう。まず、いつも通り親クラスに「食べる」メソッドを定義。

class Animal {
    void eat() {
        System.out.println("This animal eats.");
    }
}

次に、子クラスで「食べる」メソッドをオーバーライドし、その中でsuperキーワードを使って親クラスの「食べる」メソッドを呼び出します。

class Dog extends Animal {
    @Override
    void eat() {
        super.eat(); // 親クラスの「食べる」メソッドを呼び出す
        System.out.println("The dog eats dog food."); // 子クラス独自の処理を追加
    }
}

この例では、「犬」クラスの「食べる」メソッドの中でsuper.eat()を呼び出しています。この結果、親クラスの「食べる」メソッドが実行され、その後に子クラス独自の処理が続けて実行されることになります。

実際にこの「犬」クラスを使ってみると、以下の出力が得られます。

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // オーバーライドした「食べる」メソッドを呼び出す
    }
}

// 出力結果
// This animal eats.
// The dog eats dog food.

このサンプルでは、親クラスの「食べる」メソッドが「This animal eats.」というメッセージを出力し、その後に子クラスの「食べる」メソッドが「The dog eats dog food.」というメッセージを出力します。このように、superキーワードを使用することで、親クラスのメソッドを呼び出し、その後に子クラスの追加処理を行うことができます。

さらに、superキーワードは親クラスのコンストラクタ(参考 コンストラクタとは?)を呼び出すためにも使用される場合もあります。親クラスのコンストラクタを呼び出すことで、親クラスの初期化処理を行うことができます。

class Animal {
    Animal() {
        System.out.println("An animal is created.");
    }
}

class Dog extends Animal {
    Dog() {
        super(); // 親クラスのコンストラクタを呼び出す
        System.out.println("A dog is created.");
    }
}

この例では、親クラスのコンストラクタが先に実行され、その後に子クラスのコンストラクタが実行されます。実際にこのクラスを使ってみると、以下のようになります。

public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog(); // コンストラクタを呼び出す
    }
}

// 出力結果
// An animal is created.
// A dog is created.

このように、superキーワードを使うことで、親クラスの初期化処理を確実に行い、子クラスの初期化処理を続けて行うこともできるのがポイント。superキーワードは、親クラスと子クラスの連携をスムーズにし、コードの再利用性と保守性を向上させる重要な機能です。

継承の制限

Javaの継承にはいくつかの制限があります。

ここでは、主な制限として、finalキーワードの使用と、多重継承の禁止について説明します。

finalキーワード

finalキーワードは、クラスやメソッドを継承したりオーバーライドしたりすることを禁止するために利用されます。以下のようにfinalを利用することで継承することを禁ずるため、そのクラスを親クラスとして新しいクラスを作ることはできなくなります。

final class Animal {
    // このクラスは継承できません
}

class Dog extends Animal { // エラー:Animalクラスはfinalなので継承できません
}

同様にメソッドについても、finalを利用することでオーバーライドすることができなくなります。→子クラスでそのメソッドの動作を変更することはできません。

class Animal {
    final void eat() {
        System.out.println("This animal eats.");
    }
}

class Dog extends Animal {
    @Override
    void eat() { // エラー:eatメソッドはfinalなのでオーバーライドできません
        System.out.println("The dog eats dog food.");
    }
}

これにより、重要なクラスやメソッドの動作が変更されないようにすることができます。

多重継承の禁止

Javaではクラスの多重継承が禁止されています。多重継承とは、1つのクラスが複数のクラスを同時に継承することです。多重継承を許可すると、同じメソッドやフィールドが複数の親クラスに存在する場合にどれを使うべきかが曖昧になるため、Javaではこれを避けるために多重継承を禁止しています。

class Animal {
    void eat() {
        System.out.println("This animal eats.");
    }
}

class Pet {
    void play() {
        System.out.println("This pet plays.");
    }
}

class Dog extends Animal, Pet { // エラー:Javaは多重継承をサポートしません
}

この制限を回避するために、Javaではインターフェースを使用することができます。インターフェースを用いることで、多重継承に似た効果を実現することができます。(参考 Javaのインターフェースとは?

interface Eatable {
    void eat();
}

interface Playable {
    void play();
}

class Dog implements Eatable, Playable {
    @Override
    public void eat() {
        System.out.println("The dog eats dog food.");
    }

    @Override
    public void play() {
        System.out.println("The dog plays fetch.");
    }
}

このように、implementsキーワードを使って複数のインターフェースを実装することで、多重継承のような効果を得ることができます。インターフェースはメソッドの宣言のみを持ち、実装は各クラスが行うため、メソッドの競合が発生しません。

これらの制限を理解し、適切に活用することで、Javaの継承機能を正しく使いこなすことができます。finalキーワードとインターフェースを活用して、安全で効率的なプログラムを作成しましょう。

何が継承されて、何が継承されないのか

継承で「何が引き継がれて、何が引き継がれないのか」を整理すると、次のようになります。

■ 継承されるもの

  1. 親クラスのメソッドやフィールド(変数)
    • publicprotectedで定義されているメソッドやフィールドは、子クラスで利用可能です。
    • privateなフィールドやメソッドも、実際のメモリ領域としては親クラス由来のものを持ちますが、子クラスからは直接アクセスできない(見えない)点に注意が必要です。
  2. 親クラスが持つクラス(static)メンバー
    • 静的メソッドや静的フィールド(staticなメンバー)も継承の対象にはなりますが、オーバーライド(上書き)することはできません
    • 「継承されている」というより、クラスローダーを通じて同じ名前空間に含まれているイメージが近いです。

■ 継承されない(直接引き継がれない)もの

  1. コンストラクタ(Constructor)
    • Javaのコンストラクタは子クラスに引き継がれません。
    • 子クラスのコンストラクタが呼ばれるときに、super(...) で親クラスのコンストラクタを指定することで、親クラスの初期化部分を呼び出すだけです。
  2. 親クラスのprivateメンバーへの直接アクセス権
    • 前述のとおり、privateなフィールドやメソッドのメモリ領域は受け継ぐものの、子クラスからは直接アクセスできません。
    • 必要であれば、親クラスにprotectedpublicなメソッドを設けて、子クラスからの利用を可能にします(“ゲッター/セッター”など)。
  • Javaの継承は「親クラスのメソッドやフィールドの“実装”をそのまま使えるようになる」仕組みですが、すべてを無制限に引き継げるわけではありません。
  • 特に、コンストラクタやprivateメンバーはそのまま子クラスで利用できないので「継承後にどうやって初期化処理を呼ぶか」「アクセス可能な範囲はどこまでか」をしっかり理解しておく必要があります。
  • こうした仕組みを正しく把握していないと、「勝手に引き継げるはず」と思っていたのにエラーが起きる、予想と違う動きをするといったトラブルに繋がります。

このように、継承される部分・されない部分を踏まえて、親クラスを変更するときの影響や、子クラスで意図しない動きが起きないか、注意深く設計することが大切です。

継承関係にあるクラスで同名の変数をそれぞれ定義している場合の挙動

継承関係にあるクラスで、親クラスと子クラスが同名の変数(フィールド)をそれぞれ定義している場合、子クラスのフィールドは「親クラスのフィールドを隠す(shadowing/hiding)」動作をします。これはメソッドのオーバーライド(override)とは異なり、変数の場合は「動的ディスパッチ(動的バインディング)」ではなく「静的に(コンパイル時に)どのクラスの変数を使うかが決定される」という点に注意が必要です。

例えば以下のようなコードがあるとします。

class Parent {
    String name = "ParentName";
}

class Child extends Parent {
    String name = "ChildName";
}
  • 親クラス Parentname フィールドがあり、子クラス Child にも同じ名前の name フィールドが定義されている。
  • このとき、子クラス側の name フィールドは親クラスの name フィールドを隠す形になる。

使用例を見ると分かりやすいです:

Child child = new Child();
System.out.println(child.name);            // ChildName が表示される
System.out.println(((Parent) child).name); // ParentName が表示される
  • child.name と書いた場合は、子クラスの name が直接参照されます。
  • しかし (Parent)child という形で親クラス型にキャストすると、あくまで親クラス側の name が見え、"ParentName" が参照されます。
    ※ 実際に同じインスタンスを指していても、コンパイル時に「どのクラス型の変数を使おうとしているか」で参照先が決まるためです。

同名フィールドは上記のように「隠蔽」の関係になるため、通常は同名変数の再定義は推奨されません。開発者が混乱を招くおそれがあり、またメソッドのようにオーバーライドによる動的ディスパッチは行われないため、意図しない変数が参照されてしまう可能性があるからです。もしどうしても継承で同名変数を扱いたい場合には、super.name を使って親クラスのフィールドを明示的に参照するなど、記述を明確に分けて使いましょう。

継承とポリモーフィズムの基本

Javaの継承は、ポリモーフィズム(多態性)を実現します。これにより、あるクラスAを継承したクラスBのインスタンスは、A型の変数で扱うことができ、実際にはBのオーバーライドされたメソッドが呼び出されるなど、柔軟なプログラム設計が可能になります。

以下は、スーパークラスAとそれを継承するサブクラスBのサンプルコードです。

// スーパークラス A
class A {
    public void doSomething() {
        System.out.println("Aの処理");
    }
}

// サブクラス B
class B extends A {
    // Aのメソッドをオーバーライド
    @Override
    public void doSomething() {
        System.out.println("Bの処理");
    }
    
    // B独自のメソッド
    public void uniqueMethod() {
        System.out.println("B特有の処理");
    }
}

public class Main {
    public static void main(String[] args) {
        // BのインスタンスをA型で扱う
        A a = new B();
        a.doSomething();  // 結果: "Bの処理" (オーバーライドされたメソッドが呼ばれる)
        
        // A型の変数からはB独自のメソッドは直接呼べない
        // a.uniqueMethod();  // コンパイルエラーになる

        // Bのメソッドを呼び出すにはキャストが必要
        if (a instanceof B) {
            B b = (B) a;
            b.uniqueMethod();  // 結果: "B特有の処理"
        }
    }
}
  • ポリモーフィズムにより、スーパークラス型の変数でサブクラスのインスタンスを扱えます。
  • オーバーライドされたメソッドは、実際のオブジェクトの型に基づいて実行されるため、柔軟な動作が可能です。
  • B独自のメソッドを利用する際は、必ず安全なキャストを行う必要があります。
  • 参照型と実体型の違いを理解することで、より堅牢なコード設計が実現できます。

注意点

  1. キャストの必要性
    A型の変数は、B独自のメソッドやフィールドに直接アクセスできません。B特有の機能を利用するためには、明示的なキャストが必要です。キャストの際には、instanceof演算子を使用して実際の型を確認することが推奨されます。
  2. ダウンキャストのリスク
    A型の変数にB以外のサブクラスや異なる型のオブジェクトが格納されている場合、無理なキャストはClassCastExceptionを引き起こす可能性があります。安全なキャストのために、必ずinstanceofでチェックしてください。
  3. メソッドのオーバーライド
    サブクラスBがスーパークラスAのメソッドをオーバーライドしている場合、A型の変数でも実際のオブジェクトがBであれば、Bのオーバーライドされたメソッドが呼び出されます。これにより、動的バインディングが実現され、柔軟な振る舞いが可能となります。
  4. 参照型と実体型の違い
    コンパイル時に用いる型(参照型)と、実行時のオブジェクトの型(実体型)は異なる場合があります。メソッドの呼び出しは実体型に基づいて行われますが、参照型で定義されたメソッドやフィールドにしかアクセスできないため、これらの違いを意識した設計が重要です。

このように、Javaの継承とポリモーフィズムはコードの再利用性と拡張性を向上させる一方で、キャストや型チェックといった注意点もあるため、実装時には十分な配慮が必要です。

アップキャスト(upcast)とダウンキャスト(downcast)

アップキャスト(Upcasting)

アップキャストとは、「サブクラス(子クラス)のインスタンスを、スーパークラス(親クラス)として扱う」キャストのことです。
Java では継承関係にある型同士で、サブクラス → スーパークラス 方向の変換は自動で行われます。これを“アップキャスト”と呼ぶことがあります。

参考 キャストとは?

なぜアップキャストが必要なのか

  • サブクラスはスーパークラスを継承しており、スーパークラスのメソッドやフィールドを持っています。
  • あるインスタンスを、より汎用的なスーパークラスの型として扱いたい場合に利用します。
  • 例として、複数のサブクラスをまとめてリストなどに格納したいときに、すべてをスーパークラス型のリストとして保管することができます。
class Animal {
    public void makeSound() {
        System.out.println("Some sound...");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bow wow!");
    }

    public void wagTail() {
        System.out.println("Tail wagging!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();          // Dog型インスタンスを作成
        Animal animal = dog;          // 自動的にアップキャストされる
        animal.makeSound();           // Dogの実装が呼ばれる("Bow wow!"を出力)
    }
}
  • Dog dog = new Dog();Dog 型のインスタンスを生成した後、
  • Animal animal = dog; とすると、Javaが自動的に DogAnimal へキャストする(アップキャスト)。
  • animal.makeSound(); は実行時に DogmakeSound() を呼び出します(動的バインディング)。
  • ただし、animal は型としては Animal とみなされるため、Dog でしか定義されていない wagTail() メソッドは直接呼び出せません

ダウンキャスト(Downcasting)とは

ダウンキャストとは、「スーパークラス型で参照しているインスタンスを、サブクラス型として扱い直す」キャストのことです。
アップキャストと異なり、自動では行われず明示的にキャストを書く必要があります。さらに、実際に指しているインスタンスが本当にサブクラスのオブジェクトでなければ、ClassCastException が発生します。

なぜダウンキャストが必要なのか

  • スーパークラス型の変数で参照されているインスタンスが、特定のサブクラスのオブジェクトだとわかっている場合、そのサブクラス独自のメソッドやフィールドを呼び出したいことがあります。
  • その際、スーパークラス型の変数をサブクラス型として使うためにキャスト(ダウンキャスト)を行います。
class Animal {
    public void makeSound() {
        System.out.println("Some sound...");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bow wow!");
    }
    public void wagTail() {
        System.out.println("Tail wagging!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // アップキャストされている(実態はDog)
        animal.makeSound();        // Bow wow!

        // animal変数をDog型として利用したい
        Dog dog = (Dog) animal;    // ダウンキャスト(明示的にキャストが必要)
        dog.wagTail();            // Dog固有のメソッドを呼び出し
    }
}
  • Animal animal = new Dog(); の時点でインスタンスは Dog ですが、型は Animal で参照しています。
  • Dog dog = (Dog) animal; によって、animal が指している実体を Dog 型へ変換(ダウンキャスト)しています。
  • このとき、もし animal が本当は Cat など別のクラスのインスタンスだった場合、実行時に ClassCastException となります。

アップキャスト (Upcasting)

  • サブクラスのインスタンスをスーパークラス型の変数で扱うこと。
  • キャストの明示は不要。自動的に行われる。
  • スーパークラスにあるメソッドや変数しか呼び出せなくなるが、実装は実際のオブジェクト(サブクラス)側が呼び出される(多態性)。

ダウンキャスト (Downcasting)

  • スーパークラス型の変数が指しているサブクラスのインスタンスを、サブクラス型に戻して使うこと。
  • 明示的にキャストを書く必要がある。
  • 実際に指しているオブジェクトの型がサブクラスでなければ ClassCastException が起きるため、instanceof を使って安全確認するのが一般的。

近年ではJavaに限らずオブジェクト指向プログラミング全般で「継承は必要最小限にとどめ、むしろコンポジション(合成)を優先する」という考え方が定着してきています。これは「継承を使うと、クラス間の結合度が高くなりすぎたり、意図しない依存関係や脆いデザインが生まれやすい」といったデメリットを避けるために、長らく推奨されてきたベストプラクティスです。

ポイント 継承のデメリット

  1. クラスどうしの結び付きが強くなる(密結合)
    • 子クラス(サブクラス)は、親クラス(スーパークラス)の実装に強く依存します。
    • 親クラスを変更すると、子クラスも影響を受ける可能性が高い
    • 小さな変更でも大きく影響が及ぶため、メンテナンスが複雑になります。
  2. 親クラスの意図しない部分まで引き継ぐ場合がある
    • 親クラスに不要なメソッドやフィールドがあっても、子クラスが全て継承してしまいます。
    • 結果として、子クラスが「本来は必要ない機能」まで持ってしまい、コードが煩雑になりがちです。
  3. クラス階層が深くなると可読性が下がる
    • 階層が増えるほど、どこからどのメソッドを継承しているか追いかけるのが難しくなります。
    • バグが起きた際に原因を特定しづらくなるので、保守コストが上がります。
  4. 柔軟性に欠ける
    • 一度「この親クラスを継承する」と決めると、容易には親クラスを変更できません。
    • 新しい機能を追加したくても、継承関係で縛られていて設計の自由度が低くなることがあります。

特にJavaでは、Effective Java(Joshua Bloch著)などの有名な書籍でも「コンポジション(合成)を優先せよ(Prefer composition over inheritance)」という指針が示されており、近年のJava 8以降ではインタフェースにdefaultメソッドが導入されたり、ラムダ式が使えるようになったりすることで、わざわざ継承を使わなくともコードを再利用しやすい仕組みが増えています。

もちろん継承が不必要になったわけではなく、クラス階層に明確なis-a関係があり、拡張ポイントの定義が合理的な場合などでは引き続き有用です。しかし、かつてのように「コードの使い回し=継承」という単純な発想ではなく、継承が本当に適切かどうかを検討し、むしろコンポジションやインタフェースの活用を優先することが一般的な設計指針として広まっています。そういった意味で「できるだけ継承を使わないようにする」傾向は確かに存在するといえます。

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