インターフェースと抽象クラス: どっちを使う?

docs

「オブジェクト指向って何?」や「クラスって便利!」と感じ始めたあなた、おめでとうございます!Javaプログラミングの次のステップとして、「インターフェース」と「抽象クラス」という、ちょっと混同しやすいけれどとっても重要な概念を掘り下げていきましょう。以前の記事(クラスとオブジェクトの概念 – オブジェクト指向の入り口 | ToolDocs)でクラスの基本を学んだと思いますが、今回はその知識をさらに深める内容です。これらを理解すると、あなたのコードはもっと柔軟に、もっと拡張しやすくなりますよ!

インターフェースって何?

インターフェースは、「こんな機能を持つべきだよね!」という約束事をまとめたものです。イメージとしては、家電製品の取扱説明書にある「こういう操作ボタンがあるよ」という一覧に近いかもしれません。インターフェース自体は何も具体的な処理を持っていません。ただ「こういう名前のメソッドがあるよ」「引数はこれね」「戻り値はこれ」という情報だけを定義します。

たとえば、様々な乗り物があるとして、「移動する」という共通の機能があるとします。

Java

// 移動できるもの、というインターフェース
interface Movable {
    void move(); // 移動するメソッド
    void stop(); // 停止するメソッド
}

このMovableインターフェースは、「move()メソッドとstop()メソッドを実装してね」と宣言しているだけです。具体的な「どうやって移動するのか」「どうやって停止するのか」は、このインターフェースを実装するクラスに任されます。

インターフェースの活用例

私たちの身の回りには、インターフェースの考え方で成り立っているものがたくさんあります。

例1: スマートフォンの充電器

あなたのスマホと充電器を思い出してください。色々なメーカーのスマホがあるけど、Micro USBやUSB-Cの充電器を使えば充電できますよね? これは、スマホ側が「USB-Cで充電する」というインターフェース(約束事)を提供していて、充電器もそのインターフェースに従って作られているからです。充電器の種類(急速充電器、モバイルバッテリーなど)は関係なく、USB-Cというインターフェースに準拠していれば充電できるわけです。

Java

// 充電できるもの、というインターフェース
interface Chargeable {
    void charge(int voltage); // 指定された電圧で充電する
}

// スマートフォンクラス
class SmartPhone implements Chargeable {
    @Override
    public void charge(int voltage) {
        System.out.println(voltage + "Vでスマートフォンを充電中...");
        // スマホ独自の充電処理
    }
}

// モバイルバッテリークラス
class MobileBattery implements Chargeable {
    @Override
    public void charge(int voltage) {
        System.out.println(voltage + "Vでモバイルバッテリーを充電中...");
        // モバイルバッテリー独自の充電処理
    }
}

public class ChargingExample {
    public static void main(String[] args) {
        SmartPhone myPhone = new SmartPhone();
        MobileBattery myBattery = new MobileBattery();

        // どちらもChargeableインターフェースを実装しているので、同じように充電できる
        myPhone.charge(5);
        myBattery.charge(5);
    }
}

このように、Chargeableインターフェースを実装することで、SmartPhoneMobileBatteryも「充電できるもの」として同じように扱えます。

vscodeで実行した結果

例2: テレビのリモコン

テレビのリモコンも、インターフェースの代表例です。どのテレビでも、「チャンネルを変える」「音量を調整する」といった基本的な操作は共通ですよね。これは、テレビが「リモコンからの信号を受け付ける」というインターフェースを提供しているからです。リモコンの種類(純正、汎用)は関係なく、そのインターフェースに従って信号を送ればテレビを操作できます。


抽象クラスって何?

抽象クラスは、「共通の振る舞いはここで定義しつつ、一部は実装をサブクラスに任せる」というクラスです。インターフェースが「約束事」だけだったのに対し、抽象クラスは**「一部の実装」**も持てます。

抽象クラスは、abstractキーワードを使って宣言します。そして、抽象クラスの中にはabstractなメソッドを含めることができます。このabstractなメソッドは、インターフェースのメソッドと同じように、具体的な処理は持ちません。その代わり、抽象クラスを継承するサブクラスが、必ずその抽象メソッドを実装する義務を負います。

先ほどのMovableの例で考えてみましょう。 「乗り物」という抽象的な概念を表現したい場合。

Java

// 乗り物、という抽象クラス
abstract class Vehicle {
    protected int speed; // 速度は共通で持てる

    public Vehicle(int speed) {
        this.speed = speed;
    }

    // 共通の動作として速度を表示するメソッド
    public void displaySpeed() {
        System.out.println("現在の速度: " + speed + "km/h");
    }

    // 移動方法や停止方法は乗り物によって異なるので、抽象メソッドにする
    public abstract void move();
    public abstract void stop();
}

Vehicleクラスは、speedというフィールドとdisplaySpeed()という具体的なメソッドを持っています。一方で、move()stop()abstractなので、実装がありません。

抽象クラスの活用例

抽象クラスは、「共通の機能を持ちつつ、一部の具体的な動作はバリエーションを持たせたい」場合に便利です。

例1: 従業員の種類

会社には、正社員、アルバイト、パートなど、様々な雇用形態の従業員がいます。彼ら全員が「従業員」という共通のカテゴリに属し、名前や社員番号といった共通の情報を持っています。しかし、「給与計算の方法」は雇用形態によって異なりますよね?

Java

// 従業員、という抽象クラス
abstract class Employee {
    protected String name;
    protected String employeeId;

    public Employee(String name, String employeeId) {
        this.name = name;
        this.employeeId = employeeId;
    }

    // 共通のメソッド
    public void displayInfo() {
        System.out.println("名前: " + name + ", 社員番号: " + employeeId);
    }

    // 給与計算は雇用形態によって異なるので抽象メソッドにする
    public abstract double calculateSalary();
}

// 正社員クラス
class FullTimeEmployee extends Employee {
    private double monthlySalary;

    public FullTimeEmployee(String name, String employeeId, double monthlySalary) {
        super(name, employeeId);
        this.monthlySalary = monthlySalary;
    }

    @Override
    public double calculateSalary() {
        return monthlySalary; // 基本給そのまま
    }
}

// アルバイトクラス
class PartTimeEmployee extends Employee {
    private double hourlyWage;
    private int hoursWorked;

    public PartTimeEmployee(String name, String employeeId, double hourlyWage, int hoursWorked) {
        super(name, employeeId);
        this.hourlyWage = hourlyWage;
        this.hoursWorked = hoursWorked;
    }

    @Override
    public double calculateSalary() {
        return hourlyWage * hoursWorked; // 時給 x 労働時間
    }
}

public class EmployeeExample {
    public static void main(String[] args) {
        FullTimeEmployee john = new FullTimeEmployee("ジョン", "001", 300000);
        PartTimeEmployee mary = new PartTimeEmployee("メアリー", "002", 1200, 160);

        john.displayInfo();
        System.out.println("給与: " + john.calculateSalary());

        mary.displayInfo();
        System.out.println("給与: " + mary.calculateSalary());
    }
}

Employee抽象クラスは、nameemployeeIddisplayInfo()といった共通部分を持ちつつ、給与計算のロジックは各サブクラス(FullTimeEmployeePartTimeEmployee)に任せています。

vscodeで実行した結果


インターフェースと抽象クラス、どっちを使えばいいの?

さて、ここが一番混乱しやすいポイントかもしれませんね。簡単に違いをまとめると、

特徴インターフェース抽象クラス
目的「できること」の約束(能力の定義)「〜の一種」という共通基盤(共通の性質と動作)
実装実装を持たない(Java 8以降はデフォルトメソッド可)一部実装を持つことができる
継承・実装implementsで複数実装可能extendsで単一継承のみ
フィールドpublic static finalな定数のみ通常のフィールドを持てる
コンストラクタ持たない持つことができる

こんな感じです。では、どんなときにどちらを使うべきか、見ていきましょう。

インターフェースを使うべき時

  • 「〜できる」という能力を表現したい場合:
    • 例: Printable(印刷できる)、Sortable(並べ替えできる)、Clickable(クリックできる)など。
    • 特定のクラス階層に縛られず、複数のクラスが共通の振る舞いを持つことを保証したいときに最適です。
  • 多重継承を実現したい場合:
    • Javaではクラスの多重継承はできませんが、インターフェースは複数実装できます。
    • 例えば、DogクラスがPetインターフェースとTrainableインターフェースの両方を実装する、といったことが可能です。
  • 機能の契約だけを定義し、実装は完全にサブクラスに任せたい場合:
    • APIを設計する際など、具体的な実装方法を強制せず、機能の存在だけを伝えたい場合に有効です。

抽象クラスを使うべき時

  • 「〜の一種」という共通の基盤を提供したい場合:
    • 例: Shape(図形)、Animal(動物)、Vehicle(乗り物)など。
    • 関連する複数のクラスが、共通のフィールドやメソッド、または一部の共通処理を持つ場合に適しています。
  • 共通の処理を親クラスで実装し、一部の可変部分だけを子クラスに任せたい場合:
    • テンプレートメソッドパターンなど、処理の流れは固定で、一部のステップだけをサブクラスでカスタマイズさせたい場合に便利です。
  • コンストラクタやインスタンス変数(フィールド)を子クラス間で共有したい場合:
    • 共通の初期化処理や、子クラスに継承させたい状態(データ)がある場合に有効です。

まとめ

インターフェースと抽象クラスは、どちらも抽象化という概念を扱うものですが、その目的と使い方が異なります。

  • インターフェースは「何ができるか」という約束事を定義し、クラス間の共通の振る舞いを強制します。これにより、コードの柔軟性と拡張性が高まります。
  • 抽象クラスは「どんなものか」という共通の基盤を提供し、一部の具体的な実装を共有しつつ、特定の処理をサブクラスに任せることで、コードの再利用性を高めます。

どちらを使うべきか迷ったら、

  1. 「その機能をたくさんの異なる種類のクラスに持たせたい?」 → Yesならインターフェースの可能性大。
  2. 「いくつかのクラスに共通の性質や振る舞いがあって、それらをまとめて扱いたいけど、一部の具体的な処理はそれぞれのクラスで変えたい?」 → Yesなら抽象クラスの可能性大。

と考えると良いでしょう。

これらの概念を理解し、適切に使い分けることで、より堅牢で保守しやすいJavaプログラムを書けるようになります。焦らず、少しずつコードを書きながら理解を深めていきましょう!何か疑問があれば、いつでも戻って復習したり、新しい情報を調べたりしてくださいね。

頑張ってください!

コメント

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