HOWTO · Java

Java で 2つのクラスを拡張する

Javaのクラスが拡張できるクラスは1つだけである理由と、インターフェースとコンポジションで複数の型の振る舞いを組み合わせる方法を学びます。

このページの内容

Javaのクラスは、2つのクラスを同時に継承できません。クラスが直接継承できるスーパークラスは1つだけなので、class Child extends First, Second のような宣言はJavaでは無効です。複数の型の振る舞いを組み合わせるには、1つのクラスを継承して任意の数のインターフェースを実装するか、ほかのオブジェクトを保持し、コンポジションによって処理を委譲します。

Javaのクラスが2つのクラスを継承できない理由

Javaのクラスは単一継承を採用しています。各クラスが extends で指定できるスーパークラスは最大1つです。これにより、オブジェクトのクラス継承経路は1つになり、2つの親クラスが同じメンバーを定義している場合に、どちらの実装や状態を適用するかが曖昧になるのを防げます。スーパークラスを2つ列挙したクラス宣言はコンパイラーに拒否されます。これはコンパイル時の制約であり、プログラムの実行中に起きる失敗ではありません。

次のコードは意図的に無効なJava構文であり、コンパイルできません。class Child extends First, Second {} 新しいクラスが本当にその型の特殊な種類であり、その実装を継承すべき場合は、スーパークラスを1つ使います。次のセクションでは、インターフェースの契約で追加の機能を表す方法を説明します。

スーパークラスを1つ継承し、複数のインターフェースを実装する

クラスは、1つのクラスを継承しながら複数のインターフェースを実装できます。スーパークラスは共通の状態や実装を提供でき、各インターフェースはそのクラスが提供すると約束する機能を宣言します。Javaのキーワードの使い分けが重要です。クラスはスーパークラスに extends を使い、インターフェースには implements を使います。インターフェース自体が複数のインターフェースを継承することはできますが、それによってクラスが複数のクラスをスーパークラスに持てるわけではありません。

次の完全な例では、2つのインターフェース契約を定義し、1つのスーパークラスとともに両方を実装します。

public class CatalogItem extends Record implements Printable, Identifiable {
    CatalogItem(String name) {
        super(name);
    }

    @Override
    public void printLabel() {
        System.out.println(name());
    }

    @Override
    public String id() {
        return "item-" + name().toLowerCase();
    }

    public static void main(String[] args) {
        CatalogItem item = new CatalogItem("Book");
        item.printLabel();
        System.out.println(item.id());
    }

}

interface Printable {
    void printLabel();
}

interface Identifiable {
    String id();
}

class Record {
    private final String name;

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

    String name() {
        return name;
    }
}
Book
item-book

CatalogItem は Record の name() 実装を継承し、両方のインターフェースが要求するメソッドを提供します。インターフェースは、互いに無関係なクラス間で型や機能の契約を共有したい場合に役立ちます。ただし、2つ目のスーパークラスのように、状態やクラスの実装を自動的に提供するものではありません。

コンポジションでクラスの振る舞いを組み合わせる

コンポジションは、「~を持つ」という関係を通じてオブジェクトを組み合わせる方法です。クラスが協力オブジェクトへの参照を保持し、そのメソッドを呼び出します。それぞれ独立して役立つクラスの振る舞いを組み合わせたい場合や、協力オブジェクトを交換可能にしたい場合に適しています。包含するクラスは外部に公開する操作を制御でき、呼び出し元が必要とする処理だけを委譲できます。

ここでは SmartDevice が PowerController と MessageSender を保持し、それぞれに処理を委譲します。

public class SmartDevice {
    private final PowerController power = new PowerController();
    private final MessageSender sender = new MessageSender();

    void start() {
        power.powerOn();
    }

    void sendMessage(String message) {
        sender.send(message);
    }

    public static void main(String[] args) {
        SmartDevice device = new SmartDevice();
        device.start();
        device.sendMessage("Ready");
    }

}

class PowerController {
    void powerOn() {
        System.out.println("Power on");
    }
}

class MessageSender {
    void send(String message) {
        System.out.println("Sending: " + message);
    }
}
Power on
Sending: Ready

SmartDevice は PowerController にも MessageSender にもなりません。両方を利用します。規模の大きなプログラムでは、協力オブジェクトをコンストラクター経由で渡したり、インターフェースで表したりできます。こうすると実装を差し替えやすくなり、包含するクラスを単独でテストしやすくなります。

適切な再利用パターンを選ぶ

新しい型が本当に1つのスーパークラスの特殊な種類であり、その実装を継承すべき場合は、クラス継承を選びます。他のクラスと共有する機能を型として約束する必要がある場合は、インターフェースを追加します。振る舞いを部品から組み立てたり、委譲したり、個別に変更したりしたい場合は、コンポジションを使います。

各クラスの責任を明確にし、呼び出し元が必要とする操作だけを公開しましょう。広範なものより小さなインターフェース契約を優先し、共通の協力オブジェクトに属する振る舞いを重複して実装しないようにします。こうした選択によって、要件の変化に応じて関係を理解し、テストし、調整しやすくなります。つまり、Javaのクラスは2つのクラスを継承できません。一般的な再利用のニーズには、スーパークラス1つ、複数のインターフェース契約、そしてコンポジションによる協力オブジェクトで対応できます。