はじめに

オブジェクト指向プログラミングにおいて、継承ポリモーフィズムは最も強力な機能の一つです。継承を活用することで既存のコードを再利用し、ポリモーフィズムによって柔軟で拡張性の高い設計を実現できます。

この記事では、Javaにおける継承の基本概念から始まり、extendsキーワードによるクラスの継承、メソッドのオーバーライド、superthisの使い分け、抽象クラス、そしてポリモーフィズムの概念と活用法まで、段階的に解説します。

継承とは何か

継承(Inheritance)とは、既存のクラスの機能を引き継いで新しいクラスを作成する仕組みです。これにより、コードの重複を避け、階層的なクラス構造を設計できます。

is-a関係で考える

継承はis-a関係(〜は〜の一種である)で表現されます。

flowchart TB
    Animal["Animal(動物)"]
    Dog["Dog(犬)"]
    Cat["Cat(猫)"]
    Bird["Bird(鳥)"]
    
    Animal --> Dog
    Animal --> Cat
    Animal --> Bird
関係 説明
Dog is an Animal 犬は動物の一種である
Cat is an Animal 猫は動物の一種である
Bird is an Animal 鳥は動物の一種である

このように「子クラスは親クラスの一種である」という関係が成り立つ場合に、継承を使用するのが適切です。

継承における用語

継承に関連する用語を整理しておきましょう。

用語 別名 説明
スーパークラス 親クラス、基底クラス 継承元となるクラス
サブクラス 子クラス、派生クラス 継承先となるクラス
継承 拡張 スーパークラスの機能を引き継ぐこと

継承を使うメリット

メリット 説明
コードの再利用 共通の機能を親クラスにまとめ、子クラスで再利用できる
保守性の向上 共通部分の修正が1箇所で済む
拡張性 新しい種類の追加が容易になる
多態性の実現 ポリモーフィズムにより柔軟な処理が可能になる

extendsによるクラスの継承

Javaではextendsキーワードを使用してクラスを継承します。

継承の基本構文

1
2
3
public class サブクラス extends スーパークラス {
    // サブクラス固有のフィールドやメソッド
}

継承の実装例

動物クラスを例に、継承の基本的な使い方を確認しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// スーパークラス(親クラス)
public class Animal {
    private String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    public void eat() {
        System.out.println(name + "が食事をしています");
    }
    
    public void sleep() {
        System.out.println(name + "が眠っています");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// サブクラス(子クラス)
public class Dog extends Animal {
    private String breed; // 犬種
    
    public Dog(String name, String breed) {
        super(name); // 親クラスのコンストラクタを呼び出す
        this.breed = breed;
    }
    
    public String getBreed() {
        return breed;
    }
    
    // Dog固有のメソッド
    public void bark() {
        System.out.println(getName() + "がワンワン吠えています");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// サブクラス(子クラス)
public class Cat extends Animal {
    private boolean indoor; // 室内飼いかどうか
    
    public Cat(String name, boolean indoor) {
        super(name);
        this.indoor = indoor;
    }
    
    public boolean isIndoor() {
        return indoor;
    }
    
    // Cat固有のメソッド
    public void meow() {
        System.out.println(getName() + "がニャーと鳴いています");
    }
}

継承したクラスの使用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("ポチ", "柴犬");
        Cat cat = new Cat("タマ", true);
        
        // 親クラスから継承したメソッド
        dog.eat();    // ポチが食事をしています
        dog.sleep();  // ポチが眠っています
        
        // 子クラス固有のメソッド
        dog.bark();   // ポチがワンワン吠えています
        
        // 親クラスから継承したメソッド
        cat.eat();    // タマが食事をしています
        
        // 子クラス固有のメソッド
        cat.meow();   // タマがニャーと鳴いています
        
        System.out.println(dog.getBreed()); // 柴犬
        System.out.println(cat.isIndoor()); // true
    }
}

この例では、DogクラスとCatクラスがAnimalクラスを継承しています。両方のサブクラスでeat()sleep()メソッドを使用できるのは、これらがAnimalクラスから継承されているためです。

継承とアクセス修飾子

継承において、スーパークラスのメンバーにアクセスできるかどうかは、アクセス修飾子によって決まります。

アクセス修飾子 同一クラス 同一パッケージ サブクラス その他
private × × ×
(デフォルト) ×
protected ×
public

※ △ = 同一パッケージ内のサブクラスのみアクセス可能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Animal {
    private String name;      // サブクラスから直接アクセス不可
    protected int age;        // サブクラスからアクセス可能
    public String species;    // どこからでもアクセス可能
    
    // privateフィールドへのアクセスはgetterを通じて行う
    protected String getName() {
        return name;
    }
}

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

オーバーライド(Override)とは、スーパークラスで定義されたメソッドをサブクラスで再定義することです。これにより、サブクラス固有の振る舞いを実装できます。

オーバーライドの基本

@Overrideアノテーションを付けることで、コンパイラがオーバーライドの正当性をチェックしてくれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Animal {
    private String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    public void makeSound() {
        System.out.println(name + "が鳴いています");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(getName() + "がワンワン!");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(getName() + "がニャー!");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal("動物");
        Dog dog = new Dog("ポチ");
        Cat cat = new Cat("タマ");
        
        animal.makeSound(); // 動物が鳴いています
        dog.makeSound();    // ポチがワンワン!
        cat.makeSound();    // タマがニャー!
    }
}

@Overrideアノテーションの重要性

@Overrideアノテーションは必須ではありませんが、以下の理由から常に付けることを推奨します。

理由 説明
コンパイルエラーの検出 メソッド名のタイプミスや引数の違いを検出できる
可読性の向上 オーバーライドしていることが明示される
意図の明確化 親クラスのメソッドを上書きする意図を示せる
1
2
3
4
5
6
7
public class Dog extends Animal {
    // 誤り: メソッド名のタイプミス
    @Override
    public void makeSoud() {  // コンパイルエラー!正しくはmakeSound
        System.out.println("ワンワン!");
    }
}

オーバーライドのルール

オーバーライドには以下のルールがあります。

ルール 説明
メソッド名 親クラスと完全に一致する必要がある
引数リスト 親クラスと完全に一致する必要がある
戻り値の型 同じ型、または共変戻り値型(サブタイプ)
アクセス修飾子 親クラスと同じか、より緩いものにする
例外 親クラスと同じか、より狭い範囲の例外をスロー
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Animal {
    protected void display() {
        System.out.println("Animal");
    }
}

public class Dog extends Animal {
    // OK: protectedをpublicに緩和できる
    @Override
    public void display() {
        System.out.println("Dog");
    }
    
    // NG: publicをprivateに制限することはできない
    // @Override
    // private void display() { } // コンパイルエラー
}

superキーワードの使い方

superキーワードは、スーパークラス(親クラス)のメンバーにアクセスするために使用します。

superの3つの用途

用途 構文 説明
コンストラクタ呼び出し super(引数) 親クラスのコンストラクタを呼び出す
メソッド呼び出し super.メソッド名() 親クラスのメソッドを呼び出す
フィールドアクセス super.フィールド名 親クラスのフィールドにアクセスする

親クラスのコンストラクタ呼び出し

サブクラスのコンストラクタでは、必ず親クラスのコンストラクタを呼び出す必要があります。明示的に呼び出さない場合、コンパイラが自動的に引数なしのsuper()を挿入します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Animal {
    private String name;
    private int age;
    
    // 引数なしコンストラクタ
    public Animal() {
        this.name = "不明";
        this.age = 0;
    }
    
    // 引数ありコンストラクタ
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, int age, String breed) {
        super(name, age);  // 親クラスのコンストラクタを明示的に呼び出す
        this.breed = breed;
    }
    
    public Dog(String breed) {
        // super()が暗黙的に呼ばれる(引数なしコンストラクタ)
        this.breed = breed;
    }
    
    public String getBreed() {
        return breed;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
    public static void main(String[] args) {
        Dog dog1 = new Dog("ポチ", 3, "柴犬");
        System.out.println(dog1.getName() + ", " + dog1.getAge() + "歳, " + dog1.getBreed());
        // 出力: ポチ, 3歳, 柴犬
        
        Dog dog2 = new Dog("ゴールデンレトリバー");
        System.out.println(dog2.getName() + ", " + dog2.getAge() + "歳, " + dog2.getBreed());
        // 出力: 不明, 0歳, ゴールデンレトリバー
    }
}

親クラスのメソッド呼び出し

オーバーライドしたメソッド内で、親クラスの元のメソッドを呼び出すことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Animal {
    private String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    public void display() {
        System.out.println("名前: " + name);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, String breed) {
        super(name);
        this.breed = breed;
    }
    
    @Override
    public void display() {
        super.display();  // 親クラスのdisplay()を呼び出す
        System.out.println("犬種: " + breed);  // 追加の情報を表示
    }
}
1
2
3
4
5
6
7
8
9
public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("ポチ", "柴犬");
        dog.display();
        // 出力:
        // 名前: ポチ
        // 犬種: 柴犬
    }
}

superとthisの比較

superthisはどちらもキーワードですが、参照先が異なります。

キーワード 参照先 コンストラクタ呼び出し
this 現在のインスタンス this()
super 親クラスのインスタンス super()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
}

public class Dog extends Animal {
    private String name;  // 親クラスと同名のフィールド(非推奨だが例示用)
    
    public Dog(String animalName, String dogName) {
        super(animalName);
        this.name = dogName;
    }
    
    public void showNames() {
        System.out.println("this.name: " + this.name);   // Dogのname
        System.out.println("super.name: " + super.name); // Animalのname
    }
}

抽象クラスと抽象メソッド

抽象クラス(Abstract Class)は、直接インスタンス化できない「不完全なクラス」です。共通の基盤を提供しながら、具体的な実装はサブクラスに委ねる設計パターンで使用します。

抽象クラスの特徴

特徴 説明
インスタンス化不可 new 抽象クラス()はコンパイルエラー
抽象メソッド 実装を持たないメソッドを定義できる
通常メソッド 実装を持つメソッドも定義できる
継承必須 使用するには継承して具象クラスを作成する

抽象クラスの定義

abstractキーワードを使用して抽象クラスと抽象メソッドを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 抽象クラス
public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // 抽象メソッド: 実装を持たない
    public abstract double getArea();
    
    // 抽象メソッド
    public abstract double getPerimeter();
    
    // 通常のメソッド: 実装を持つ
    public void displayColor() {
        System.out.println("色: " + color);
    }
}

抽象クラスの継承

抽象クラスを継承したサブクラスは、すべての抽象メソッドを実装する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double getArea() {
        return width * height;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * (width + height);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Main {
    public static void main(String[] args) {
        // Shape shape = new Shape("赤"); // コンパイルエラー!抽象クラスはインスタンス化不可
        
        Rectangle rect = new Rectangle("青", 5, 3);
        Circle circle = new Circle("赤", 4);
        
        System.out.println("長方形の面積: " + rect.getArea());       // 15.0
        System.out.println("長方形の周囲長: " + rect.getPerimeter()); // 16.0
        rect.displayColor();  // 色: 青
        
        System.out.println("円の面積: " + circle.getArea());         // 約50.27
        System.out.println("円の周囲長: " + circle.getPerimeter());   // 約25.13
        circle.displayColor(); // 色: 赤
    }
}

抽象クラスを使用する場面

場面 説明
共通機能の提供 複数のサブクラスで共通するコードを親クラスにまとめたい
実装の強制 サブクラスに特定のメソッドの実装を強制したい
テンプレートパターン 処理の骨組みを親クラスで定義し、詳細はサブクラスで実装させたい

テンプレートメソッドパターンの例

抽象クラスの典型的な活用パターンとして、テンプレートメソッドパターンがあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public abstract class DataProcessor {
    // テンプレートメソッド: 処理の流れを定義
    public final void process() {
        readData();
        processData();
        writeData();
    }
    
    // 抽象メソッド: サブクラスで実装
    protected abstract void readData();
    protected abstract void processData();
    protected abstract void writeData();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class CsvProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println("CSVファイルからデータを読み込み");
    }
    
    @Override
    protected void processData() {
        System.out.println("CSVデータを処理");
    }
    
    @Override
    protected void writeData() {
        System.out.println("結果をCSVファイルに書き込み");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class JsonProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println("JSONファイルからデータを読み込み");
    }
    
    @Override
    protected void processData() {
        System.out.println("JSONデータを処理");
    }
    
    @Override
    protected void writeData() {
        System.out.println("結果をJSONファイルに書き込み");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Main {
    public static void main(String[] args) {
        DataProcessor csvProcessor = new CsvProcessor();
        csvProcessor.process();
        // 出力:
        // CSVファイルからデータを読み込み
        // CSVデータを処理
        // 結果をCSVファイルに書き込み
        
        System.out.println();
        
        DataProcessor jsonProcessor = new JsonProcessor();
        jsonProcessor.process();
        // 出力:
        // JSONファイルからデータを読み込み
        // JSONデータを処理
        // 結果をJSONファイルに書き込み
    }
}

ポリモーフィズムの概念

ポリモーフィズム(Polymorphism、多態性)とは、同じ操作(メソッド呼び出し)に対して、オブジェクトの実際の型に応じて異なる振る舞いをする性質です。

ポリモーフィズムのイメージ

flowchart TB
    subgraph 呼び出し側
        Call["animal.makeSound()"]
    end
    
    subgraph 実行時の振る舞い
        Dog["Dogオブジェクト → ワンワン!"]
        Cat["Catオブジェクト → ニャー!"]
        Bird["Birdオブジェクト → ピヨピヨ!"]
    end
    
    Call --> Dog
    Call --> Cat
    Call --> Bird

同じmakeSound()メソッドの呼び出しでも、実際のオブジェクトの型によって異なる動作をします。

ポリモーフィズムの実装

ポリモーフィズムは、親クラス型の変数でサブクラスのインスタンスを参照することで実現します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Animal {
    private String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    public void makeSound() {
        System.out.println(name + "が鳴いています");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(getName() + "がワンワン!");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(getName() + "がニャー!");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(getName() + "がピヨピヨ!");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Main {
    public static void main(String[] args) {
        // 親クラス型の変数でサブクラスのインスタンスを参照
        Animal animal1 = new Dog("ポチ");
        Animal animal2 = new Cat("タマ");
        Animal animal3 = new Bird("ピーちゃん");
        
        // 同じメソッド呼び出しでも、実際の型に応じた動作をする
        animal1.makeSound(); // ポチがワンワン!
        animal2.makeSound(); // タマがニャー!
        animal3.makeSound(); // ピーちゃんがピヨピヨ!
    }
}

配列やコレクションでの活用

ポリモーフィズムは、異なる型のオブジェクトを統一的に扱う場面で特に威力を発揮します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
    public static void main(String[] args) {
        // 異なる型のオブジェクトを同じ配列で管理
        Animal[] animals = {
            new Dog("ポチ"),
            new Cat("タマ"),
            new Bird("ピーちゃん"),
            new Dog("ハチ"),
            new Cat("ミケ")
        };
        
        // すべての動物を鳴かせる
        System.out.println("=== 全員集合! ===");
        for (Animal animal : animals) {
            animal.makeSound();
        }
        
        // 出力:
        // === 全員集合! ===
        // ポチがワンワン!
        // タマがニャー!
        // ピーちゃんがピヨピヨ!
        // ハチがワンワン!
        // ミケがニャー!
    }
}

ポリモーフィズムを使ったメソッド設計

ポリモーフィズムを活用することで、呼び出し側のコードを変更せずに新しい動物を追加できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class AnimalShelter {
    public void feedAll(Animal[] animals) {
        for (Animal animal : animals) {
            System.out.print(animal.getName() + "に餌をあげます。");
            animal.makeSound();  // 各動物固有の鳴き声
        }
    }
    
    public void checkHealth(Animal animal) {
        System.out.println(animal.getName() + "の健康チェック中...");
        animal.makeSound();  // 元気かどうか鳴いてもらう
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
    public static void main(String[] args) {
        AnimalShelter shelter = new AnimalShelter();
        
        Animal[] animals = {
            new Dog("ポチ"),
            new Cat("タマ"),
            new Bird("ピーちゃん")
        };
        
        shelter.feedAll(animals);
        // 出力:
        // ポチに餌をあげます。ポチがワンワン!
        // タマに餌をあげます。タマがニャー!
        // ピーちゃんに餌をあげます。ピーちゃんがピヨピヨ!
        
        System.out.println();
        shelter.checkHealth(new Dog("レオ"));
        // 出力:
        // レオの健康チェック中...
        // レオがワンワン!
    }
}

instanceof演算子と型キャスト

ポリモーフィズムを使用している場合、実行時にオブジェクトの実際の型を確認したい場合があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {
    public static void main(String[] args) {
        Animal[] animals = {
            new Dog("ポチ"),
            new Cat("タマ"),
            new Bird("ピーちゃん")
        };
        
        for (Animal animal : animals) {
            if (animal instanceof Dog) {
                Dog dog = (Dog) animal;  // ダウンキャスト
                System.out.println(animal.getName() + "は犬です");
                // dog.bark(); // Dogクラス固有のメソッドを呼べる
            } else if (animal instanceof Cat) {
                System.out.println(animal.getName() + "は猫です");
            } else if (animal instanceof Bird) {
                System.out.println(animal.getName() + "は鳥です");
            }
        }
    }
}

Java 16以降では、パターンマッチングを使用してより簡潔に記述できます。

1
2
3
4
5
6
7
8
9
// Java 16以降
for (Animal animal : animals) {
    if (animal instanceof Dog dog) {  // パターンマッチング
        System.out.println(dog.getName() + "は犬です");
        // dog.bark(); // そのまま使用可能
    } else if (animal instanceof Cat cat) {
        System.out.println(cat.getName() + "は猫です");
    }
}

継承の設計原則と注意点

継承は強力な機能ですが、適切に使用しないと保守性の低下やバグの原因となります。

Javaの単一継承

Javaでは、クラスは1つのクラスしか継承できません(単一継承)。

1
2
3
4
5
6
7
8
public class A { }
public class B { }

// NG: 複数のクラスを同時に継承することはできない
// public class C extends A, B { } // コンパイルエラー

// OK: 1つのクラスのみ継承可能
public class C extends A { }

複数の機能を持たせたい場合は、インタフェースを使用します(インタフェースは別記事で解説します)。

継承 vs コンポジション

継承を使うべきか、それともコンポジション(委譲)を使うべきかは、重要な設計判断です。

観点 継承 コンポジション
関係性 is-a(〜は〜の一種) has-a(〜は〜を持つ)
結合度 強い(密結合) 弱い(疎結合)
柔軟性 低い(実行時に変更不可) 高い(実行時に変更可能)
使用例 DogはAnimalの一種 CarはEngineを持つ
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 継承(is-a関係): DogはAnimalの一種
public class Dog extends Animal {
    // ...
}

// コンポジション(has-a関係): CarはEngineを持つ
public class Car {
    private Engine engine;  // エンジンを「持っている」
    
    public Car(Engine engine) {
        this.engine = engine;
    }
    
    public void start() {
        engine.start();
    }
}

継承を使用する際のガイドライン

ガイドライン 説明
is-a関係を確認 「サブクラスはスーパークラスの一種」が成り立つか
リスコフの置換原則 サブクラスはスーパークラスの代わりに使用できるか
継承階層は浅く 深い継承階層は理解とメンテナンスを困難にする
protected の使用を検討 サブクラスに公開したいメンバーはprotectedに
finalを検討 継承させたくないクラス・メソッドにはfinalを付ける

finalキーワードによる継承の制限

finalキーワードを使用して、継承やオーバーライドを禁止できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// finalクラス: 継承不可
public final class ImmutableClass {
    // このクラスは継承できない
}

public class Parent {
    // finalメソッド: オーバーライド不可
    public final void criticalMethod() {
        System.out.println("このメソッドは変更できません");
    }
    
    public void normalMethod() {
        System.out.println("このメソッドはオーバーライド可能");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// public class Child extends ImmutableClass { } // コンパイルエラー!

public class Child extends Parent {
    // @Override
    // public void criticalMethod() { } // コンパイルエラー!
    
    @Override
    public void normalMethod() {
        System.out.println("オーバーライドしました");
    }
}

継承のアンチパターン

避けるべき継承の使い方を紹介します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// アンチパターン1: 機能の再利用だけを目的とした継承
// NG: StackはArrayListの一種ではない
public class Stack extends ArrayList {
    // ArrayListの全メソッドが公開されてしまう
}

// 正しい実装: コンポジションを使用
public class Stack<E> {
    private List<E> elements = new ArrayList<>();
    
    public void push(E element) {
        elements.add(element);
    }
    
    public E pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }
}
1
2
3
4
5
6
7
// アンチパターン2: 深すぎる継承階層
// NG: 継承が深すぎると理解が困難
public class A { }
public class B extends A { }
public class C extends B { }
public class D extends C { }
public class E extends D { }  // 5階層目...保守が大変

まとめ

この記事では、Javaにおける継承とポリモーフィズムについて解説しました。

概念 ポイント
継承 extendsで既存クラスの機能を引き継ぎ、コードを再利用する
オーバーライド @Overrideで親クラスのメソッドを再定義し、サブクラス固有の動作を実装する
super 親クラスのコンストラクタやメソッドにアクセスする
抽象クラス abstractで不完全なクラスを定義し、サブクラスに実装を強制する
ポリモーフィズム 親クラス型の変数で異なる型のオブジェクトを統一的に扱う

継承とポリモーフィズムを正しく活用することで、以下のメリットを得られます。

  • コードの重複を削減し、保守性を向上
  • 柔軟で拡張性の高い設計を実現
  • 新しい機能の追加が容易になる

ただし、継承は強力な反面、不適切な使用は設計の複雑化を招きます。「is-a関係が成り立つか」を常に意識し、必要に応じてコンポジションの使用を検討してください。

次のステップとして、インタフェースについて学ぶことで、より柔軟な設計パターンを習得できます。

参考リンク