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

Java入門

【Javaのextends】継承をわかりやすく解説!使い方から注意点まで解説

トム

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

「Javaの継承ってなんだか難しそう…」

「オブジェクト指向の重要な概念だとは聞くけれど、いまいちピンとこない」

Javaの学習を進める中で、多くの方が「継承」の壁にぶつかります。私自身、10年以上前にJavaを学び始めたとき、この継承の概念を理解するのに苦労した経験があります。しかし、実務でコードを書く中で、継承がどれほどコードを効率的で美しくするかを何度も目の当たりにしてきました。

この記事は、過去の私と同じようにJavaの継承で悩んでいる方に向けて書いています。

この記事を読めば、Javaの継承の基本から、extendssuperの使い方、インターフェースとの違い、そして実務で役立つ設計の考え方まで、網羅的に理解できます。サンプルコードを豊富に使い、初心者の方でもつまずかないように丁寧に解説するので、ぜひ最後まで読んでみてください。あなたのJavaスキルが一段階レベルアップするはずです。

Javaの継承とは?基本の考え方をわかりやすく解説

Javaにおける継承は、オブジェクト指向プログラミングの中核をなす重要な仕組みの1つです。一言でいうと、あるクラスが持つ性質(フィールドやメソッド)を、別のクラスが受け継ぐ機能を指します。

この仕組みを理解することで、コードの再利用性が高まり、より効率的で保守性の高いプログラムを組めるようになります。

継承の定義と目的

継承の主な目的は、コードの重複をなくし、プログラムの構造を整理する点にあります。

たとえば、ゲームに登場する「犬」クラスと「猫」クラスを作るとします。「歩く」「食べる」といった動作は、どちらにも共通する機能です。継承を使わない場合、それぞれのクラスに同じ内容のコードを書く必要があり、非効率ですし、修正があった場合は2箇所を直さなければなりません。

そこで、これらの共通機能をまとめた「動物」クラスを作り、犬クラスと猫クラスが「動物」クラスを継承します。そうすれば、「歩く」「食べる」といったコードは1箇所にまとめられ、プログラム全体がすっきりと見やすくなります。これこそがJava継承の力です。

クラスとスーパークラス(親クラス・子クラス)の関係

継承を語るうえで欠かせないのが、クラス間の関係を示す2つの用語です。

  • スーパークラス(親クラス): 継承元となるクラス。機能を受け継がせる側。基底クラスとも呼ばれます。
  • サブクラス(子クラス): 継承先となるクラス。機能を受け継ぐ側。派生クラスとも呼ばれます。

先ほどの例では、「動物」がスーパークラス(親クラス)であり、「犬」と「猫」がサブクラス(子クラス)にあたります。

サブクラスは、スーパークラスの機能(publicやprotectedで定義されたフィールドやメソッド)をまるで自分のもののように利用できます。さらに、サブクラス独自の機能を追加することも可能です。

継承を使うメリット・デメリット

継承は非常に強力な機能ですが、良い面ばかりではありません。メリットとデメリットを正しく理解して使いこなしましょう。

メリット

  1. コードの再利用性が向上する: 共通の機能をスーパークラスにまとめることで、同じコードを何度も書く必要がなくなる。
  2. 保守性が高まる: 共通機能の修正が必要になった場合、スーパークラスの1箇所を修正するだけで、すべてのサブクラスに修正が反映される。
  3. 多態性(ポリモーフィズム)を実現できる: スーパークラスの型としてサブクラスのオブジェクトを扱えるようになり、柔軟で拡張性の高いプログラムを書きやすくなる。

デメリット

  1. クラス間の結合度が強くなる(密結合): 親クラスの仕様変更が、意図せず子クラスに影響を与えてしまう可能性がある。親子関係が強固になりすぎるため、柔軟性が失われる場合がある。
  2. 構造が複雑になる: 継承関係が何階層にも深くなると、クラスの全体像を把握するのが難しくなり、かえってプログラムが複雑化する恐れがある。

Javaで継承を使う基本構文【extends】

それでは、実際にJavaで継承を利用するための具体的な構文を見ていきましょう。キーワードはextendsです。このキーワードを覚えるだけで、今日から継承を使いこなせます。

基本構文の書き方と使い方

クラスを継承するには、サブクラス(子クラス)の定義時にextendsキーワードに続けてスーパークラス(親クラス)を指定します。

class サブクラス名 extends スーパークラス名 {
    // クラスの定義
}

非常にシンプルです。たったこれだけで、サブクラスはスーパークラスの機能を受け継ぐことができます。

スーパークラスのメソッドを呼び出す方法(superの使い方)

サブクラスでは、スーパークラスのメソッドやコンストラクタを明示的に呼び出したい場合があります。その際に活躍するのがsuperキーワードです。

  • super.メソッド名(): スーパークラスのメソッドを呼び出します。
  • super(): スーパークラスのコンストラクタを呼び出します。

特に、サブクラスのコンストラクタの先頭でsuper()を呼び出し、スーパークラスの初期化処理を行うのは定石パターンです。

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
        System.out.println("Animalクラスのコンストラクタが呼ばれました");
    }

    void cry() {
        System.out.println("動物が鳴きます");
    }
}

class Dog extends Animal {
    Dog(String name) {
        // スーパークラスのコンストラクタを呼び出す
        super(name);
        System.out.println("Dogクラスのコンストラクタが呼ばれました");
    }
}

この例では、Dogクラスのインスタンスを生成すると、まずsuper(name)によってAnimalクラスのコンストラクタが実行され、その後にDogクラスのコンストラクタ内の処理が実行されます。

メソッドオーバーライド(@Override)の仕組み

継承の大きな特徴の1つにメソッドのオーバーライドがあります。オーバーライドとは、スーパークラスで定義されたメソッドを、サブクラスで再定義(上書き)する仕組みです。

たとえば、「動物」クラスのcry()メソッドは「動物が鳴きます」と出力しますが、「犬」クラスでは「ワン!」と鳴いてほしいですよね。このような場合にオーバーライドを使います。

メソッドをオーバーライドする際は、メソッドの前に@Overrideというアノテーションを付けるのが一般的です。これは、コンパイラに対して「このメソッドはオーバーライドしています」と伝えるための目印です。

もしメソッド名や引数の型を間違えていた場合、コンパイラがエラーを教えてくれるため、バグの防止につながります。

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    // Animalクラスのcryメソッドをオーバーライド
    @Override
    void cry() {
        System.out.println(this.name + "は「ワン!」と鳴きます");
    }
}

実際に動かして理解する継承のコード例

ここからは、具体的なコード例を通して、Java継承がどのように機能するのかをさらに深く見ていきます。実際に手を動かしてコードを書いてみると、理解が格段に深まるでしょう。

Animalクラスを継承したDogクラスの例

これまで説明してきたAnimalクラスとDogクラスの完成形と、それを実行するコードを見てみましょう。

// スーパークラス
class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void eat() {
        System.out.println(this.name + "は食事をします");
    }

    void cry() {
        System.out.println("動物が鳴きます");
    }
}

// サブクラス
class Dog extends Animal {
    Dog(String name) {
        super(name); // 親クラスのコンストラクタを呼び出す
    }

    // 独自のメソッドを追加
    void run() {
        System.out.println(this.name + "は走り回ります");
    }

    // 親クラスのメソッドをオーバーライド
    @Override
    void cry() {
        System.out.println(this.name + "は「ワン!」と鳴きます");
    }
}

// 実行クラス
public class Main {
    public static void main(String[] args) {
        Dog myDog = new Dog("ポチ");
        myDog.eat();  // 親クラスのメソッドを呼び出し
        myDog.run();  // 子クラス独自のメソッドを呼び出し
        myDog.cry();  // オーバーライドしたメソッドを呼び出し
    }
}

実行結果:

ポチは食事をします
ポチは走り回ります
ポチは「ワン!」と鳴きます

DogクラスはAnimalクラスを継承しているので、定義していないeat()メソッドを呼び出せています。

また、Dogクラス独自に定義したrun()メソッドも使えますし、cry()メソッドはオーバーライドした内容で実行されているのがわかります。

共通処理をまとめる設計パターンの実例

実務では、データベースへの接続情報やログ出力機能など、多くのクラスで共通して利用したい処理が出てきます。このような共通処理をスーパークラスにまとめて、各クラスがそれを継承する設計は非常によく使われます。

// 共通機能をまとめたスーパークラス
abstract class BaseService {
    void connectDatabase() {
        System.out.println("データベースに接続しました");
    }

    void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

// ユーザー情報を扱うクラス
class UserService extends BaseService {
    void findUser(int userId) {
        connectDatabase(); // 親クラスのメソッドを利用
        log("ユーザーID: " + userId + " を検索します");
        // 検索処理...
    }
}

// 商品情報を扱うクラス
class ProductService extends BaseService {
    void findProduct(int productId) {
        connectDatabase(); // 親クラスのメソッドを利用
        log("商品ID: " + productId + " を検索します");
        // 検索処理...
    }
}

UserServiceProductServiceは、BaseServiceを継承することで、データベース接続やログ出力の機能を自分で実装することなく利用できます。

オーバーライドと多態性(ポリモーフィズム)の関係

継承とオーバーライドを学ぶと、オブジェクト指向のもう1つの重要な柱である多態性(ポリモーフィズム)への扉が開かれます。

多態性とは、同じ型の変数やメソッド呼び出しであっても、実際のインスタンスの種類によって異なる振る舞いをする性質です。

public class Main {
    public static void main(String[] args) {
        // 親クラスの型で子クラスのインスタンスを扱う
        Animal myCat = new Cat("タマ"); // CatクラスもAnimalを継承していると仮定
        Animal myDog = new Dog("ポチ");

        myCat.cry(); // Catクラスでオーバーライドされたメソッドが呼ばれる
        myDog.cry(); // Dogクラスでオーバーライドされたメソッドが呼ばれる
    }
}

実行結果:

タマは「ニャー」と鳴きます
ポチは「ワン!」と鳴きます

myCatmyDogも同じAnimal型として宣言されているにもかかわらず、cry()メソッドを呼び出すと、それぞれのインスタンスの実際のクラス(CatDog)でオーバーライドされたメソッドが実行されます。

このように、1つのインターフェース(ここではcry()メソッド)で、異なる実装を呼び分けられるのが多態性の強力な点です。

継承とインターフェース・抽象クラスの違い

Javaには、継承と似た目的で使われる機能として「インターフェース」と「抽象クラス」があります。これらは混同しやすいため、違いをしっかりと理解しておくことが重要です。

継承(extends)とインターフェース(implements)の使い分け

最大の違いは、Javaではクラスの継承(extends)は1つしかできないのに対し、インターフェースの実装(implements)は複数できる点です。

  • 継承(is-a関係): 「AはBの一種である」という関係(例: 犬は動物の一種である)が成り立つ場合に利用します。クラスの基本的な機能を引き継ぎたいときに適している。
  • インターフェース(can-do関係): 「AはBができる」という関係(例: 人は泳ぐことができる、鳥は飛ぶことができる)が成り立つ場合に利用します。クラスに特定の「機能」や「役割」を追加したいときに使う。

たとえば、「ドローン」クラスは「機械」クラスを継承しつつ、「飛ぶ」「撮影する」という機能を持つインターフェースを実装する、といった設計が可能です。

抽象クラスの特徴と継承の関係

抽象クラスは、インスタンス化できない、不完全なクラスです。抽象メソッド(処理内容が定義されていないメソッド)を持つことができます。

抽象クラスは、継承されることを前提としています。サブクラスで抽象メソッドをオーバーライドし、具体的な処理を実装することで、初めて完全なクラスとして機能します。

インターフェースと似ていますが、抽象クラスはフィールド(変数)を持てたり、実装済みの通常のメソッドも定義できたりする点で異なります。共通の機能や状態を持ちつつ、一部の処理だけをサブクラスに強制したい場合に利用されます。

あわせて読む

継承よりComposition(コンポジション)を選ぶケース

プログラミングの世界には、「継承より移譲(コンポジション)を」という有名な設計原則があります。

継承は親子関係が強固(密結合)になるというデメリットがありました。そこで、あるクラスの機能を利用したい場合に、継承するのではなく、そのクラスのインスタンスをフィールドとして内部に持つ方法がコンポジションです。

class Engine {
    void start() {
        System.out.println("エンジンが始動しました");
    }
}

class Car {
    // 継承ではなく、Engineクラスのインスタンスを内部に持つ
    private Engine engine;

    Car() {
        this.engine = new Engine();
    }

    void start() {
        // Engineの機能を利用する(移譲)
        this.engine.start();
        System.out.println("車が発進します");
    }
}

この設計では、CarEngineの関係が継承よりも疎結合になります。これにより、Engineクラスの変更がCarクラスに与える影響を最小限に抑えられ、より柔軟で再利用性の高い設計が実現できます。

Javaでクラスの多重継承はできない?その理由と代替案

Javaの継承を学んでいると、「複数のクラスを一度に継承できないのだろうか?」という疑問が浮かぶかもしれません。結論からお伝えすると、Javaではクラスの多重継承はサポートされていません。 1つのクラスが継承できる親クラスは、常に1つだけです。

これはJavaの言語仕様における重要な制約であり、プログラムの複雑化を防ぐための意図的な設計になります。この章では、なぜJavaが多重継承を禁止しているのか、そしてその代替策について分かりやすく解説します。

Javaがクラスの多重継承を禁止する理由【ダイヤモンド問題】

Javaがクラスの多重継承を許可しない最大の理由は、「ダイヤモンド問題」と呼ばれる状態を避けるためです。

ダイヤモンド問題とは、あるクラスが、同じスーパークラスを継承する2つの異なるクラスを同時に継承しようとしたときに発生するあいまいさの問題を指します。言葉だけでは少し分かりにくいので、具体的な例で見ていきましょう。

仮に、Javaで多重継承が可能だったとします。

  1. Characterというスーパークラスがあり、attack()メソッドを持っています。
  2. WarriorクラスとWizardクラスが、それぞれCharacterクラスを継承します。そして、両方のクラスでattack()メソッドを独自にオーバーライドします(戦士は剣で攻撃、魔法使いは魔法で攻撃)。
  3. ここで、MagicKnightという新しいクラスが、WarriorWizardの両方を継承しようとします。
// ※これはJavaではコンパイルエラーになるコードです

class Character {
    void attack() {
        System.out.println("キャラクターが攻撃する");
    }
}

class Warrior extends Character {
    @Override
    void attack() {
        System.out.println("剣で攻撃する!");
    }
}

class Wizard extends Character {
    @Override
    void attack() {
        System.out.println("魔法で攻撃する!");
    }
}

// WarriorとWizardを多重継承しようとすると…
class MagicKnight extends Warrior, Wizard { // このような書き方はできません
    // ここで attack() を呼び出すと、
    // Warriorのattack()とWizardのattack()の
    // どちらを呼べばいいのか分からない!
}

このとき、MagicKnightクラスのインスタンスからattack()メソッドを呼び出すと、コンパイラはWarriorattack()Wizardattack()のどちらを実行すればよいのか判断できません。

このように、継承関係がひし形(ダイヤモンド)のようになり、メソッドの呼び出し元が特定できなくなる問題をダイヤモンド問題と呼びます。

この問題を言語仕様レベルで防ぐために、Javaはクラスの多重継承を禁止しているのです。

インターフェースによる多重継承(多重実装)

Javaではクラスの多重継承はできませんが、インターフェースを使うことで多重継承と似た機能を実現できます。 クラスは複数のインターフェースを実装(implements)することが可能です。

インターフェースは、メソッドのシグネチャ(名前、引数、戻り値の型)だけを定義し、具体的な実装は持ちません(Java 8以降のdefaultメソッドを除く)。

そのため、複数のインターフェースを実装しても、どのメソッドを呼び出すかというあいまいさは発生しない仕組みです。実装は常に、実装したクラス側で行われるからです。

先ほどのMagicKnightの例を、インターフェースを使って再設計してみましょう。

// 攻撃できる、という「機能」をインターフェースで定義
interface SwordAttack {
    void attackWithSword();
}

interface MagicAttack {
    void attackWithMagic();
}

// 機能(インターフェース)をクラスに実装する
class MagicKnight implements SwordAttack, MagicAttack {

    @Override
    public void attackWithSword() {
        System.out.println("剣で素早く斬りつける!");
    }

    @Override
    public void attackWithMagic() {
        System.out.println("炎の魔法を唱える!");
    }

    // 両方の技を使う独自のメソッドも作れる
    public void combinedAttack() {
        attackWithSword();
        attackWithMagic();
    }
}

public class Main {
    public static void main(String[] args) {
        MagicKnight knight = new MagicKnight();
        knight.attackWithSword();
        knight.attackWithMagic();
        knight.combinedAttack();
    }
}

実行結果:

剣で素早く斬りつける!
炎の魔法を唱える!
剣で素早く斬りつける!
炎の魔法を唱える!

このように、MagicKnightクラスはSwordAttackMagicAttackという2つの「機能」を実装することで、「剣でも魔法でも攻撃できる」という性質を持ちます。

これは、クラスの継承が「is-a(~の一種である)」の関係を表すのに対し、インターフェースは「can-do(~ができる)」という役割や能力を表すという考え方に基づいています。

まとめ:継承を正しく使いこなすためのポイント

最後に、Javaの継承を使いこなすための重要なポイントをまとめます。

継承を乱用しない設計のコツ

継承は強力なツールですが、万能薬ではありません。使うべき場面を見極めることが肝心です。設計を始める前に、本当に「is-a」の関係が成り立つのかを自問自答しましょう。安易な継承は、将来的にプログラムを複雑にし、保守性を低下させる原因となります。

実務で役立つ継承設計の考え方

実務では、まずインターフェースやコンポジションで実現できないかを検討する癖をつけるのがおすすめです。

これらのほうが、クラス間の結合度を低く保ち、柔軟な設計を維持しやすいためです。そのうえで、クラス間に明確な「is-a」関係があり、共通の基盤を共有するメリットが大きい場合にのみ、継承を選択肢に入れると良いでしょう。

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

トム

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

-Java入門