インターフェースの基本:契約を定義する

docs

以前の記事では、クラスが「設計図」のようなもので、そこからオブジェクトという「実体」を作るんだ、という話をしましたね。今回はその続きとして、Javaのもうひとつの重要な要素である「インターフェース」について深掘りしていきましょう。

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

インターフェースは一言でいうと「約束事」や「契約」を定義するものです。

「設計図」であるクラスは「どんなものが作れるか」を具体的に示していました。例えば、Carクラスなら「エンジンがあって、タイヤがあって、ハンドルがある」といった具体的な構造が書かれているイメージです。

でも、インターフェースは「何ができるべきか」という行動に焦点を当てます。具体的な実装は書かず、「こういう機能を持ってるべきだよね」ということを宣言するんです。

例えば、Drivable(運転可能)というインターフェースがあったとします。このインターフェースは「運転できるものは、startEngine()(エンジンをかける)とaccelerate()(加速する)という機能を持っていなければならない」と定めます。

Java

// Drivableインターフェースの定義
interface Drivable {
    void startEngine(); // エンジンをかけるという約束
    void accelerate();  // 加速するという約束
}

どうですか?interfaceというキーワードを使って、Drivableという名前のインターフェースを定義しています。中にはメソッドの宣言だけあって、具体的な処理({}の中身)は書かれていませんよね。これがインターフェースの大きな特徴です。

なんでインターフェースが必要なの?

「なんでそんな回りくどいことするの?クラスに直接書けばいいじゃん?」って思うかもしれませんね。インターフェースが必要な理由は大きく分けて2つあります。

1. 共通の行動を強制する

さっきのDrivableの例で考えてみましょう。CarクラスもMotorcycleクラスも「運転できる」という共通の行動を持っていますよね。もしインターフェースがなければ、それぞれのクラスで勝手にstartEngine()とかaccelerate()とかいう名前のメソッドを作ることになります。

もしかしたら、CarクラスではengineStart()MotorcycleクラスではstartTheEngine()なんて、バラバラの名前になってしまうかもしれません。これだと、後から「運転できるもの」をまとめて扱いたいときに困りますよね。

そこでDrivableインターフェースの登場です。CarクラスもMotorcycleクラスもこのDrivableインターフェースを「実装」することで、「startEngine()accelerate()というメソッドを必ず持ちます」という約束を強制できるんです。

Java

// CarクラスがDrivableインターフェースを実装
class Car implements Drivable {
    @Override // インターフェースのメソッドを実装するぞ!という目印
    public void startEngine() {
        System.out.println("車のエンジンがかかりました。ブォン!");
    }

    @Override
    public void accelerate() {
        System.out.println("車が加速します。ビューン!");
    }
}

// MotorcycleクラスもDrivableインターフェースを実装
class Motorcycle implements Drivable {
    @Override
    public void startEngine() {
        System.out.println("バイクのエンジンがかかりました。ドゥルルル!");
    }

    @Override
    public void accelerate() {
        System.out.println("バイクが加速します。ブィーン!");
    }
}

implementsというキーワードを使って、インターフェースを実装していますね。@Overrideは「親のメソッドを上書きするぞ」という目印で、インターフェースのメソッドを実装するときにもよく使われます。

これで、CarMotorcycleも、Drivableという共通の型として扱うことができるようになりました。

Java

public class DrivingSimulator {
    public static void main(String[] args) {
        Drivable myCar = new Car();         // CarオブジェクトをDrivable型として扱う
        Drivable myBike = new Motorcycle(); // MotorcycleオブジェクトをDrivable型として扱う

        System.out.println("--- 車を運転します ---");
        myCar.startEngine();
        myCar.accelerate();

        System.out.println("\n--- バイクを運転します ---");
        myBike.startEngine();
        myBike.accelerate();
    }
}

出力結果:

--- 車を運転します ---
車のエンジンがかかりました。ブォン!
車が加速します。ビューン!

--- バイクを運転します ---
バイクのエンジンがかかりました。ドゥルルル!
バイクが加速します。ブィーン!

myCarmyBikeも型はDrivableなのに、それぞれ固有の動きをしているのがわかるでしょうか?これがインターフェースの持つ「ポリモーフィズム(多様性)」という力の一部です。同じメソッドを呼び出しても、オブジェクトの種類によって異なる振る舞いをします。

vscodeで実行した結果

2. 依存関係を減らす(疎結合にする)

もうひとつの重要な理由は「依存関係を減らす」こと、つまり「疎結合」にすることです。

例えば、あなたのゲームで「キャラクター」が「武器」を使うとします。もしSwordクラスに直接useSword()というメソッドを定義して、Warriorクラスがそれを使うようにしてしまうと、WarriorクラスはSwordクラスに直接「依存」してしまいます。

Java

// インターフェースを使わない例
class Sword {
    public void swing() {
        System.out.println("剣を振り回した!シュパッ!");
    }
}

class Warrior {
    private Sword mySword;

    public Warrior() {
        this.mySword = new Sword(); // WarriorがSwordに依存
    }

    public void attack() {
        mySword.swing();
    }
}

これだと、もし「弓」を使いたくなったらどうでしょう?Warriorクラスを書き換えて、Bowクラスのインスタンスを持つように変更しなければなりません。

Java

// Bowクラスを追加
class Bow {
    public void shootArrow() {
        System.out.println("弓矢を放った!ヒュン!");
    }
}

// Warriorクラスを書き換える必要あり
class Warrior {
    // private Sword mySword;
    private Bow myBow; // SwordからBowに変更

    public Warrior() {
        // this.mySword = new Sword();
        this.myBow = new Bow(); // Bowを生成
    }

    public void attack() {
        // mySword.swing();
        myBow.shootArrow(); // メソッドも変更
    }
}

これは面倒ですよね。

そこでインターフェースの出番です。「使える武器は攻撃できる」という共通の約束を定義します。

Java

// Attackableインターフェースの定義
interface Attackable {
    void attack(); // 攻撃するという約束
}

// SwordクラスがAttackableインターフェースを実装
class Sword implements Attackable {
    @Override
    public void attack() {
        System.out.println("剣を振り回した!シュパッ!");
    }
}

// BowクラスもAttackableインターフェースを実装
class Bow implements Attackable {
    @Override
    public void attack() {
        System.out.println("弓矢を放った!ヒュン!");
    }
}

// WarriorクラスはAttackableインターフェースに依存
class Warrior {
    private Attackable currentWeapon; // 武器はAttackable型

    public Warrior(Attackable weapon) { // コンストラクタで武器を受け取る
        this.currentWeapon = weapon;
    }

    public void useWeapon() {
        currentWeapon.attack(); // どの武器でもattack()メソッドを呼べばOK
    }

    public void changeWeapon(Attackable newWeapon) {
        this.currentWeapon = newWeapon;
        System.out.println("武器を" + newWeapon.getClass().getSimpleName() + "に変更しました。");
    }
}

public class GameCharacter {
    public static void main(String[] args) {
        Warrior arthur = new Warrior(new Sword()); // 剣を持たせてアーサーを生成
        System.out.println("アーサー、攻撃!");
        arthur.useWeapon();

        System.out.println("\nアーサー、弓に持ち替え!");
        arthur.changeWeapon(new Bow()); // 弓に持ち替え
        System.out.println("アーサー、攻撃!");
        arthur.useWeapon();
    }
}

出力結果:

アーサー、攻撃!
剣を振り回した!シュパッ!

アーサー、弓に持ち替え!
武器をBowに変更しました。
アーサー、攻撃!
弓矢を放った!ヒュン!

どうでしょうか?Warriorクラスは、具体的なSwordBowというクラスを知る必要がなくなりました。ただ「Attackableなものなら何でも使える」というだけです。これにより、新しい武器(例えばMagicStaffなど)を追加しても、Warriorクラスを修正する必要がなくなります。これが「疎結合」のメリットです。コードの変更に強い、拡張しやすいプログラムになるんですね。

vscodeで実行した結果

インターフェースのちょっと応用

Java 8以降、インターフェースには「デフォルトメソッド」や「staticメソッド」も書けるようになりました。これはちょっと応用的な話ですが、少しだけ触れておきましょう。

  • デフォルトメソッド: インターフェースを実装するクラスが、そのメソッドを実装しなくても済むように、デフォルトの実装を提供できます。既存のインターフェースに新しいメソッドを追加する際に、過去のクラスを壊さずに機能を追加できるので便利です。
  • staticメソッド: インターフェース自体が持つユーティリティメソッドなどを定義できます。

最初は「メソッドの宣言だけ」という基本をしっかり押さえておけば十分です!

まとめ

インターフェースは「約束事」や「契約」を定義するものです。

  • インターフェースは「何ができるべきか」を宣言し、具体的な実装は書きません。
  • クラスはimplementsキーワードを使ってインターフェースを「実装」し、そのインターフェースが定義したメソッドを必ず提供します。
  • これにより、複数のクラスに共通の行動を強制できます。
  • さらに、具体的なクラスへの依存関係を減らし、柔軟で変更に強い(疎結合な)プログラムを作ることができます。

Javaでプログラムを作る上で、インターフェースは避けて通れない非常に重要な概念です。最初は難しく感じるかもしれませんが、「共通の振る舞いを定義する」「約束を強制する」というイメージを持てば、理解が深まるはずです。


前回の記事:抽象クラスと抽象メソッドを使いこなそう! | ToolDocs

コメント

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