Skip to main content

インターフェース

この章で得られるスキル:

  • ✅ 異なるクラスに共通の動作ルールを定義できる
  • ✅ 複数の実装方法を柔軟に切り替えられる
  • ✅ 将来の機能追加に強い設計ができる
  • ✅ チーム開発で役立つ「契約」を定義できる

Step 0: インターフェースがないとどうなる?

まず、インターフェースがない世界で「支払いシステム」を作ってみよう。

状況:

  • オンラインショップで、複数の支払い方法(クレジットカード、銀行振込、電子マネー)に対応したい
  • それぞれの支払い方法は異なる処理を行う
  • しかし、どれも「支払いをする」という共通の振る舞いを持つ

インターフェースがない場合のコード:

// クレジットカード決済クラス class CreditCardPayment { void pay(int amount) { System.out.println("クレジットカードで" + amount + "円を支払いました"); } } // 銀行振込クラス class BankTransferPayment { void pay(int amount) { System.out.println("銀行振込で" + amount + "円を支払いました"); } } // 電子マネークラス class EMoneyPayment { void pay(int amount) { System.out.println("電子マネーで" + amount + "円を支払いました"); } } public class Main { public static void main(String[] args) { // 問題1: 異なる型なので統一的に扱えない CreditCardPayment creditCard = new CreditCardPayment(); BankTransferPayment bankTransfer = new BankTransferPayment(); EMoneyPayment eMoney = new EMoneyPayment(); // 問題2: 支払い方法ごとに異なる処理が必要 creditCard.pay(1000); bankTransfer.pay(2000); eMoney.pay(3000); // 問題3: 配列にまとめられない(型が違うため) // Object[] payments = {creditCard, bankTransfer, eMoney}; // for (Object p : payments) { // p.pay(500); // エラー!Objectにはpayメソッドがない // } // 問題4: 新しい支払い方法を追加するたびに処理を書き換える必要がある } }

問題点:

  1. 統一的に扱えない: 各支払い方法は別々の型なので、共通の型として扱えない
  2. コードの重複: どれも「支払う」という共通の振る舞いを持つのに、個別に処理を書く必要がある
  3. 拡張性が低い: 新しい支払い方法を追加するたびに、既存のコードを大きく変更する必要がある
  4. 保守性が低い: 支払い処理のロジックが散らばり、変更が困難

この問題を解決するのが インターフェース である。


Step 1: インターフェースとは何か

インターフェースの定義

インターフェース とは、クラスが実装すべきメソッドの「仕様」を定義する仕組み である。

比喩:

  • インターフェース = 規格・契約書
  • 実装クラス = 規格を満たす製品

例えば、USB規格があれば、異なるメーカーの機器でも統一的に接続できる。 同様に、Paymentインターフェースがあれば、異なる支払い方法でも統一的に扱える。

インターフェースの特徴

  1. 「何をするか」を定義 し、「どうするか」は実装クラスが決める
  2. 抽象的な型 として機能する(実態を持たない)
  3. 複数のクラスで共通の型 として使える

インターフェースと継承の違い

比較項目継承(extends)インターフェース(implements)
関係性is-a関係(犬 動物 であるcan-do関係(鳥 飛ぶこと ができる
実装親クラスの実装を引き継ぐメソッドの実装を持たない(契約のみ)
複数1つのクラスのみ継承可能複数のインターフェースを実装可能
用途共通の実装を共有共通の振る舞い(能力)を定義

例:

  • 継承: Dog extends Animal(犬は動物である)
  • インターフェース: Bird implements Flyable(鳥は飛べる)

Step 2: インターフェースの基本的な書き方

インターフェースの定義

interface インターフェース名 {
// メソッドの宣言(実装は書かない)
戻り値の型 メソッド名(引数);
}

例: Paymentインターフェース

interface Payment {
void pay(int amount); // 「支払う」という振る舞いを定義
}

インターフェースの実装

インターフェースを実装するには、implementsキーワード を使う。

class クラス名 implements インターフェース名 {
// インターフェースで定義されたメソッドを実装
@Override
public 戻り値の型 メソッド名(引数) {
// 実装内容
}
}

例: CreditCardPaymentクラス

class CreditCardPayment implements Payment {
@Override
public void pay(int amount) {
System.out.println("クレジットカードで" + amount + "円を支払いました");
}
}

インターフェースを使った改善例

実行してみよう:

// 支払いインターフェース interface Payment { void pay(int amount); } // クレジットカード決済 class CreditCardPayment implements Payment { @Override public void pay(int amount) { System.out.println("クレジットカードで" + amount + "円を支払いました"); } } // 銀行振込 class BankTransferPayment implements Payment { @Override public void pay(int amount) { System.out.println("銀行振込で" + amount + "円を支払いました"); } } // 電子マネー class EMoneyPayment implements Payment { @Override public void pay(int amount) { System.out.println("電子マネーで" + amount + "円を支払いました"); } } public class Main { public static void main(String[] args) { // 解決1: Payment型として統一的に扱える Payment payment1 = new CreditCardPayment(); Payment payment2 = new BankTransferPayment(); Payment payment3 = new EMoneyPayment(); // 解決2: 同じ方法で呼び出せる payment1.pay(1000); payment2.pay(2000); payment3.pay(3000); System.out.println("---"); // 解決3: 配列にまとめられる Payment[] payments = { new CreditCardPayment(), new BankTransferPayment(), new EMoneyPayment() }; // 解決4: ループで一括処理できる for (Payment p : payments) { p.pay(500); } } }

改善されたポイント:

  1. 統一的に扱える: Payment型として共通化
  2. コードの重複が減る: ループで一括処理できる
  3. 拡張性が高い: 新しい支払い方法を追加しても、既存コードを変更する必要がない
  4. 保守性が高い: 支払い処理のロジックが明確になる

Step 3: インターフェースのルールと注意点

インターフェースのメソッドのルール

  1. メソッドは自動的にpublic abstractになる

    interface Payment {
    void pay(int amount); // 実際は public abstract void pay(int amount);
    }
  2. 実装クラスではpublicを明示する

    class CreditCardPayment implements Payment {
    @Override
    public void pay(int amount) { // public が必要
    // ...
    }
    }
    重要

    publicを省略するとコンパイルエラーになる。インターフェースのメソッドはpublicなので、実装もpublicでなければならない。

  3. @Overrideアノテーションを付ける

    • インターフェースのメソッドを実装していることを明示
    • タイプミスを防ぐ

インターフェースのフィールド

インターフェースにフィールドを定義すると、自動的に public static final(定数) になる。

interface Payment {
int MAX_AMOUNT = 1000000; // 実際は public static final int MAX_AMOUNT = 1000000;

void pay(int amount);
}

実行してみよう:

interface Payment { // 定数の定義(自動的に public static final) int MAX_AMOUNT = 1000000; String CURRENCY = "JPY"; void pay(int amount); } class CreditCardPayment implements Payment { @Override public void pay(int amount) { if (amount > MAX_AMOUNT) { System.out.println("エラー: 上限額を超えています"); } else { System.out.println("クレジットカードで" + amount + CURRENCY + "を支払いました"); } } } public class Main { public static void main(String[] args) { // 定数は実装クラスやインターフェースから直接アクセスできる System.out.println("支払い上限額: " + Payment.MAX_AMOUNT + Payment.CURRENCY); Payment payment = new CreditCardPayment(); payment.pay(50000); payment.pay(2000000); } }

Step 4: 複数のインターフェースの実装

複数実装の書き方

Javaでは、1つのクラスは 複数のインターフェースを実装 できる(カンマ区切り)。

class クラス名 implements インターフェース1, インターフェース2, インターフェース3 {
// 全てのインターフェースのメソッドを実装
}
ポイント

継承(extends)は1つのクラスのみだが、インターフェース(implements)は複数実装できる。これがインターフェースの大きな利点である。

例: 飛べて泳げるアヒル

実行してみよう:

// 飛べるインターフェース interface Flyable { void fly(); } // 泳げるインターフェース interface Swimmable { void swim(); } // 走れるインターフェース interface Runnable { void run(); } // アヒル: 飛べて泳げる class Duck implements Flyable, Swimmable { String name; Duck(String name) { this.name = name; } @Override public void fly() { System.out.println(name + "が空を飛ぶ"); } @Override public void swim() { System.out.println(name + "が水を泳ぐ"); } } // 犬: 泳げて走れる class Dog implements Swimmable, Runnable { String name; Dog(String name) { this.name = name; } @Override public void swim() { System.out.println(name + "が泳ぐ"); } @Override public void run() { System.out.println(name + "が走る"); } } // トライアスリート: 全部できる class Triathlete implements Flyable, Swimmable, Runnable { String name; Triathlete(String name) { this.name = name; } @Override public void fly() { System.out.println(name + "がパラグライダーで飛ぶ"); } @Override public void swim() { System.out.println(name + "がクロールで泳ぐ"); } @Override public void run() { System.out.println(name + "がマラソンで走る"); } } public class Main { public static void main(String[] args) { Duck duck = new Duck("アヒル"); duck.fly(); duck.swim(); System.out.println("---"); Dog dog = new Dog("ポチ"); dog.swim(); dog.run(); System.out.println("---"); Triathlete athlete = new Triathlete("太郎"); athlete.fly(); athlete.swim(); athlete.run(); System.out.println("---"); // Swimmable型として統一的に扱う Swimmable[] swimmers = {duck, dog, athlete}; System.out.println("全員泳ぎます:"); for (Swimmable s : swimmers) { s.swim(); } } }

ポイント:

  • Duckは2つのインターフェース(Flyable, Swimmable)を実装
  • Dogも2つのインターフェース(Swimmable, Runnable)を実装
  • Triathleteは3つのインターフェース(Flyable, Swimmable, Runnable)を実装
  • Swimmable型として統一的に扱える

Step 5: 継承とインターフェースの組み合わせ

extendsとimplementsの併用

クラスは、1つのクラスを継承 しながら、複数のインターフェースを実装 できる。

class クラス名 extends 親クラス implements インターフェース1, インターフェース2 {
// 実装
}
順序に注意

extendsimplementsの前に書く必要がある。

class Bird extends Animal implements Flyable { }  // ✅ 正しい
class Bird implements Flyable extends Animal { } // ❌ エラー

例: Animalクラスを継承しつつインターフェースを実装

実行してみよう:

// 動物クラス(親クラス) class Animal { String name; int age; Animal(String name, int age) { this.name = name; this.age = age; } void eat() { System.out.println(name + "が食べる"); } void sleep() { System.out.println(name + "が寝る"); } } // インターフェース interface Flyable { void fly(); } interface Swimmable { void swim(); } // 鳥: Animalを継承し、Flyableを実装 class Bird extends Animal implements Flyable { Bird(String name, int age) { super(name, age); } @Override public void fly() { System.out.println(name + "が空を飛ぶ"); } } // アヒル: Animalを継承し、FlyableとSwimmableを実装 class Duck extends Animal implements Flyable, Swimmable { Duck(String name, int age) { super(name, age); } @Override public void fly() { System.out.println(name + "が低空飛行する"); } @Override public void swim() { System.out.println(name + "が水面を泳ぐ"); } } // 魚: Animalを継承し、Swimmableを実装 class Fish extends Animal implements Swimmable { Fish(String name, int age) { super(name, age); } @Override public void swim() { System.out.println(name + "が水中を泳ぐ"); } } public class Main { public static void main(String[] args) { Bird bird = new Bird("スズメ", 1); bird.eat(); // 親クラスのメソッド bird.sleep(); // 親クラスのメソッド bird.fly(); // インターフェースのメソッド System.out.println("---"); Duck duck = new Duck("アヒル", 2); duck.eat(); // 親クラスのメソッド duck.fly(); // インターフェース1のメソッド duck.swim(); // インターフェース2のメソッド System.out.println("---"); Fish fish = new Fish("金魚", 1); fish.eat(); // 親クラスのメソッド fish.swim(); // インターフェースのメソッド System.out.println("---"); // Animal型として統一的に扱う Animal[] animals = {bird, duck, fish}; System.out.println("全員食べます:"); for (Animal a : animals) { a.eat(); } System.out.println("---"); // Swimmable型として統一的に扱う Swimmable[] swimmers = {duck, fish}; System.out.println("泳げる動物だけ泳ぎます:"); for (Swimmable s : swimmers) { s.swim(); } } }

ポイント:

  • 継承で共通の実装(eat, sleep)を共有
  • インターフェースで能力(fly, swim)を定義
  • 1つのオブジェクトを複数の型として扱える(Animal型、Flyable型、Swimmable型)

Step 6: デフォルトメソッド(Java 8以降)

デフォルトメソッドとは

デフォルトメソッド は、インターフェースに 実装を持つメソッド を定義できる機能である(Java 8以降)。

interface インターフェース名 {
// 通常のメソッド(実装を持たない)
void メソッド1();

// デフォルトメソッド(実装を持つ)
default void メソッド2() {
// 実装
}
}

なぜデフォルトメソッドが必要か

問題状況:

  • インターフェースに新しいメソッドを追加したい
  • しかし、既存の実装クラスが多数あり、全てに実装を追加するのは大変

デフォルトメソッドの解決策:

  • インターフェースにデフォルトの実装を提供
  • 実装クラスは必要に応じてオーバーライドできる

例: Greetableインターフェース

実行してみよう:

interface Greetable { // 通常のメソッド(実装を持たない) String getName(); // デフォルトメソッド(実装を持つ) default void greet() { System.out.println("こんにちは、" + getName() + "さん"); } default void bye() { System.out.println("さようなら、" + getName() + "さん"); } } // Person: デフォルトメソッドをそのまま使う class Person implements Greetable { String name; Person(String name) { this.name = name; } @Override public String getName() { return name; } // greet()とbye()は実装しなくてもデフォルトの実装が使われる } // Robot: デフォルトメソッドをオーバーライド class Robot implements Greetable { String name; Robot(String name) { this.name = name; } @Override public String getName() { return name; } // デフォルトメソッドをオーバーライドしてカスタマイズ @Override public void greet() { System.out.println("ピピッ。私は" + getName() + "です。ピピッ。"); } // bye()はデフォルトの実装を使う } public class Main { public static void main(String[] args) { Person person = new Person("太郎"); person.greet(); // デフォルトの実装が使われる person.bye(); // デフォルトの実装が使われる System.out.println("---"); Robot robot = new Robot("R2D2"); robot.greet(); // オーバーライドした実装が使われる robot.bye(); // デフォルトの実装が使われる System.out.println("---"); // Greetable型として統一的に扱う Greetable[] greetables = {person, robot}; for (Greetable g : greetables) { g.greet(); } } }

ポイント:

  • Personはデフォルトメソッドをそのまま使用
  • Robotはgreet()をオーバーライドしてカスタマイズ
  • 実装クラスは必要なメソッドのみオーバーライドすればよい

Step 7: インターフェースの設計原則

can-do関係を考える

インターフェースを使う際は、can-do関係(「〜できる」という関係)が成り立つか確認する。

良い例(can-do関係が成り立つ):

  • Bird can Fly(鳥は飛べる)→ Flyableが適切
  • Duck can Swim(アヒルは泳げる)→ Swimmableが適切
  • Payment can Pay(支払いは支払える)→ Paymentが適切

悪い例(can-do関係が成り立たない):

  • Person can Name(人は名前できる)→ 意味不明
  • Car can Color(車は色できる)→ 意味不明

インターフェース vs 継承の使い分け

使い分けの観点継承を使うインターフェースを使う
関係性is-a関係(本質的な分類)can-do関係(能力・振る舞い)
実装の共有共通の実装を共有したい実装は各クラスで異なる
複数の関係1つの階層に属する複数の能力を持つ
Dog is a AnimalBird can Fly, Duck can Swim

適切な設計のポイント

  1. 単一責任の原則

    • 1つのインターフェースは1つの責任(能力)を持つ
    • 例: Flyableは飛ぶことのみ、Swimmableは泳ぐことのみ
  2. 小さなインターフェースを組み合わせる

    • 大きなインターフェースより、小さなインターフェースを複数実装する方が柔軟
    // ❌ 悪い例: 大きなインターフェース
    interface Animal {
    void fly();
    void swim();
    void run();
    }

    // ✅ 良い例: 小さなインターフェースの組み合わせ
    interface Flyable { void fly(); }
    interface Swimmable { void swim(); }
    interface Runnable { void run(); }

    class Duck implements Flyable, Swimmable { }
  3. インターフェース名の命名規則

    • 能力を表す形容詞: Flyable, Swimmable, Comparable
    • 役割を表す名詞: Payment, Repository, Service


実践的な演習

演習1: 楽器演奏システム

課題: 楽器演奏システムを作成せよ。

要件:

  1. Playableインターフェースを定義
    • play()メソッドを持つ
  2. 3種類の楽器クラスを作成
    • Piano: 「ピアノでドレミを演奏します」と表示
    • Guitar: 「ギターでコードを弾きます」と表示
    • Drum: 「ドラムでリズムを刻みます」と表示
  3. 配列で全ての楽器を管理し、ループで演奏させる

実装してみよう:

// TODO: Playableインターフェースを定義 // TODO: Pianoクラスを実装 // TODO: Guitarクラスを実装 // TODO: Drumクラスを実装 public class Main { public static void main(String[] args) { // TODO: 3つの楽器を作成 // TODO: 配列に格納 // TODO: ループで全ての楽器を演奏 } }
解答例を見る
interface Playable { void play(); } class Piano implements Playable { @Override public void play() { System.out.println("ピアノでドレミを演奏します"); } } class Guitar implements Playable { @Override public void play() { System.out.println("ギターでコードを弾きます"); } } class Drum implements Playable { @Override public void play() { System.out.println("ドラムでリズムを刻みます"); } } public class Main { public static void main(String[] args) { Playable[] instruments = { new Piano(), new Guitar(), new Drum() }; System.out.println("演奏会を開始します:"); for (Playable instrument : instruments) { instrument.play(); } } }

演習2: 通知システム

課題: 複数の通知方法に対応した通知システムを作成せよ。

要件:

  1. Notifiableインターフェースを定義
    • notify(String message)メソッドを持つ
  2. 3種類の通知クラスを作成
    • EmailNotification: 「メールで通知: [メッセージ]」と表示
    • SmsNotification: 「SMSで通知: [メッセージ]」と表示
    • PushNotification: 「プッシュ通知: [メッセージ]」と表示
  3. 全ての通知方法で同じメッセージを送信する

実装してみよう:

// TODO: Notifiableインターフェースを定義 // TODO: EmailNotificationクラスを実装 // TODO: SmsNotificationクラスを実装 // TODO: PushNotificationクラスを実装 public class Main { public static void main(String[] args) { // TODO: 3つの通知方法を作成 // TODO: 配列に格納 // TODO: 全ての通知方法で「システムメンテナンスのお知らせ」を送信 } }
解答例を見る
interface Notifiable { void notify(String message); } class EmailNotification implements Notifiable { @Override public void notify(String message) { System.out.println("メールで通知: " + message); } } class SmsNotification implements Notifiable { @Override public void notify(String message) { System.out.println("SMSで通知: " + message); } } class PushNotification implements Notifiable { @Override public void notify(String message) { System.out.println("プッシュ通知: " + message); } } public class Main { public static void main(String[] args) { Notifiable[] notifiers = { new EmailNotification(), new SmsNotification(), new PushNotification() }; String message = "システムメンテナンスのお知らせ"; System.out.println("通知を送信します:"); for (Notifiable notifier : notifiers) { notifier.notify(message); } } }

演習3: ゲームキャラクターシステム(継承とインターフェースの組み合わせ)

課題: RPGゲームのキャラクターシステムを作成せよ。

要件:

  1. Characterクラス(親クラス)を定義
    • フィールド: name, hp
    • メソッド: attack()
  2. インターフェースを定義
    • Flyable: fly()メソッド
    • Healable: heal(int amount)メソッド
  3. 3種類のキャラクタークラスを作成
    • Warrior: Characterを継承(飛べない、回復できない)
    • Mage: Characterを継承、Healableを実装(飛べない、回復できる)
    • Dragon: Characterを継承、FlyableとHealableを実装(飛べる、回復できる)
  4. 各キャラクターの行動をテストする

実装してみよう:

// TODO: Characterクラスを定義 // TODO: Flyableインターフェースを定義 // TODO: Healableインターフェースを定義 // TODO: Warriorクラスを実装 // TODO: Mageクラスを実装 // TODO: Dragonクラスを実装 public class Main { public static void main(String[] args) { // TODO: 各キャラクターを作成してテスト } }
解答例を見る
class Character { String name; int hp; Character(String name, int hp) { this.name = name; this.hp = hp; } void attack() { System.out.println(name + "が攻撃した"); } void showStatus() { System.out.println(name + " HP: " + hp); } } interface Flyable { void fly(); } interface Healable { void heal(int amount); } class Warrior extends Character { Warrior(String name) { super(name, 100); } } class Mage extends Character implements Healable { Mage(String name) { super(name, 60); } @Override public void heal(int amount) { hp += amount; System.out.println(name + "が" + amount + "HP回復した"); } } class Dragon extends Character implements Flyable, Healable { Dragon(String name) { super(name, 150); } @Override public void fly() { System.out.println(name + "が空を飛んだ"); } @Override public void heal(int amount) { hp += amount; System.out.println(name + "が" + amount + "HP回復した"); } } public class Main { public static void main(String[] args) { Warrior warrior = new Warrior("戦士"); warrior.showStatus(); warrior.attack(); System.out.println("---"); Mage mage = new Mage("魔法使い"); mage.showStatus(); mage.attack(); mage.heal(20); mage.showStatus(); System.out.println("---"); Dragon dragon = new Dragon("ドラゴン"); dragon.showStatus(); dragon.attack(); dragon.fly(); dragon.heal(30); dragon.showStatus(); System.out.println("---"); // Character型として統一的に扱う Character[] party = {warrior, mage, dragon}; System.out.println("パーティ全員が攻撃:"); for (Character c : party) { c.attack(); } System.out.println("---"); // Healable型として統一的に扱う Healable[] healers = {mage, dragon}; System.out.println("回復役が回復:"); for (Healable h : healers) { h.heal(10); } } }

よくある質問(FAQ)

Q1: インターフェースと抽象クラスの違いは?

A:

比較項目インターフェース抽象クラス
複数実装/継承複数実装可能1つのみ継承可能
メソッド抽象メソッド + デフォルトメソッド抽象メソッド + 具象メソッド
フィールド定数のみ(public static final)通常のフィールドも持てる
コンストラクタ持てない持てる
用途能力の定義(can-do)部分的な実装の共有(is-a)

使い分け:

  • インターフェース: 異なるクラス階層で共通の振る舞いを定義したい場合
  • 抽象クラス: 共通の実装を持ち、一部を子クラスに委ねたい場合

Q2: インターフェースのメソッドにpublicを付けなくてもいいのはなぜ?

A: インターフェースのメソッドは 自動的にpublic abstractになる ため、明示的に書かなくてもよい。

interface Payment {
void pay(int amount); // 実際は public abstract void pay(int amount);
}

ただし、実装クラスではpublicを明示する必要がある

class CreditCardPayment implements Payment {
@Override
public void pay(int amount) { // public が必要
// ...
}
}

Q3: デフォルトメソッドはいつ使うべき?

A: 以下の場合にデフォルトメソッドを使うとよい。

使うべき場合:

  1. インターフェースに新しいメソッドを追加するが、既存の実装クラスを壊したくない
    • 既存の実装クラスが多数ある場合に便利
  2. 共通の実装を提供したい
    • 多くの実装クラスで同じ実装になる場合

使わない方がよい場合:

  • インターフェースが複雑になりすぎる場合
  • 抽象クラスの方が適切な場合

Q4: インターフェースを実装し忘れたらどうなる?

A: インターフェースで定義されたメソッドを全て実装しないと、コンパイルエラー になる。

interface Payment {
void pay(int amount);
void refund(int amount);
}

class CreditCardPayment implements Payment {
@Override
public void pay(int amount) {
System.out.println("支払い");
}

// refund()を実装し忘れた
// → コンパイルエラー: CreditCardPayment is not abstract and does not override abstract method refund(int) in Payment
}

エラーを解決するには、全てのメソッドを実装する必要がある。

Q5: インターフェースは実務でどう使われる?

A: 実務では、インターフェースは以下のように活用される。

1. 依存性の注入(DI: Dependency Injection)

interface UserRepository {
User findById(int id);
}

class UserService {
private UserRepository repository;

// インターフェースに依存(具体的な実装には依存しない)
UserService(UserRepository repository) {
this.repository = repository;
}
}

// 本番環境
class DatabaseUserRepository implements UserRepository { }

// テスト環境
class MockUserRepository implements UserRepository { }

2. 標準ライブラリの活用

  • List, Set, Map: コレクションのインターフェース
  • Comparable: 比較可能なオブジェクトのインターフェース
  • Runnable: スレッドで実行可能なタスクのインターフェース

3. フレームワークとの連携

  • Spring Frameworkでは、インターフェースを使って疎結合な設計を実現
  • テストが容易になり、保守性が向上する

まとめ

この章では、インターフェース について学んだ。

学んだ内容

  • インターフェース はクラスが実装すべきメソッドの仕様を定義する仕組みである
  • インターフェースは 「何をするか」を定義 し、「どうするか」は実装クラスが決める
  • implementsキーワード でインターフェースを実装できる
  • 複数のインターフェースを実装 できる(継承は1つのみ)
  • 継承とインターフェースを組み合わせる ことができる(extendsを先に、implementsを後に書く)
  • デフォルトメソッド でインターフェースに実装を持たせられる
  • インターフェースを使う際は can-do関係 が成り立つか確認する
  • 実務では依存性の注入やテスト容易性の向上 に活用される

次のステップ

次の章では、カプセル化 について学ぶ。 フィールドやメソッドのアクセス制御を理解し、より安全で保守性の高いプログラムを作る方法を学ぶ。


演習

インターフェースの説明として最も適切なものを選べ。

正解

D. クラスが実装すべきメソッドの一覧を定義したもの

解説

インターフェースは、クラスが実装すべき メソッドのシグネチャ(名前・引数・戻り値) を定義する。実装の詳細はクラスに任せる「契約」のようなものである。interface キーワードで定義する。

以下のコードの空欄を埋めて、インターフェースの定義を完成させよ。

public
Printable { void print(); }

解答例
interface
解説

インターフェースは interface キーワードで定義する。メソッドは宣言のみで本体は書かない(Java 8以降のdefaultメソッドを除く)。

以下のコードの空欄を埋めて、DocumentクラスがPrintableインターフェースを実装するようにせよ。

class Document
{ public void print() { System.out.println("ドキュメントを印刷します"); } }

解答例
implements Printable
解説

implements キーワードでインターフェースを実装する。実装クラスはインターフェースで宣言されたすべてのメソッドを実装する必要がある。

インターフェースのメソッド定義の特徴として正しいものを選べ。

正解

D. メソッドの処理本体(中身)を書かず、シグネチャだけを定義する

解説

インターフェースのメソッドは シグネチャのみ を定義し、処理の本体 { } は書かない。メソッドは暗黙的に public abstract である。Java 8以降ではdefaultメソッドで本体を持たせることもできる。

Shapeインターフェース(getArea(), getPerimeter() メソッド)を実装するCircleクラスとRectangleクラスを作成せよ。

interface Shape { double getArea(); double getPerimeter(); } class Circle implements Shape { double radius; Circle(double radius) { this.radius = radius; } // ここにgetArea()とgetPerimeter()を実装してください } class Rectangle implements Shape { double width; double height; Rectangle(double width, double height) { this.width = width; this.height = height; } // ここにgetArea()とgetPerimeter()を実装してください } public class Main { public static void main(String[] args) { Circle c = new Circle(5); System.out.println("円の面積: " + c.getArea()); System.out.println("円の周長: " + c.getPerimeter()); Rectangle r = new Rectangle(4, 6); System.out.println("長方形の面積: " + r.getArea()); System.out.println("長方形の周長: " + r.getPerimeter()); } }

インターフェースのすべてのメソッドを実装しないとコンパイルエラーになる。各メソッドに public を付けて実装する。

解説

解答例

interface Shape {
    double getArea();
    double getPerimeter();
}

class Circle implements Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    public double getArea() {
        return 3.14 * radius * radius;
    }

    public double getPerimeter() {
        return 2 * 3.14 * radius;
    }
}

class Rectangle implements Shape {
    double width;
    double height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getArea() {
        return width * height;
    }

    public double getPerimeter() {
        return 2 * (width + height);
    }
}

インターフェースのメソッドをすべて実装する必要がある。実装時は必ず public を付ける。

PrintableとSavableの2つのインターフェースを実装するDocumentクラスを作成せよ。

interface Printable { void print(); } interface Savable { void save(); } // ここにDocumentクラスを作成してください // PrintableとSavableの両方を実装すること public class Main { public static void main(String[] args) { Document doc = new Document("会議資料"); doc.print(); doc.save(); } }

Javaではクラスの継承は1つだけだが、インターフェースは 複数実装 できる。implements A, B のようにカンマで区切る。

解説

解答例

interface Printable {
    void print();
}

interface Savable {
    void save();
}

class Document implements Printable, Savable {
    String content;

    Document(String content) {
        this.content = content;
    }

    public void print() {
        System.out.println("印刷: " + content);
    }

    public void save() {
        System.out.println("保存: " + content);
    }
}

implements インターフェースA, インターフェースB で複数実装できる。Javaのクラスは単一継承だが、インターフェースは多重実装が可能である。

Printable型の配列にDocument, Image, Spreadsheetのオブジェクトを入れ、ループで print() を呼び出せ。

interface Printable { void print(); } class Document implements Printable { // ここにprint()を実装してください } class Image implements Printable { // ここにprint()を実装してください } class Spreadsheet implements Printable { // ここにprint()を実装してください } public class Main { public static void main(String[] args) { // Printable型の配列を作り、各オブジェクトを格納してください // ループでprint()を呼び出してください } }

インターフェース型の変数に、そのインターフェースを実装したクラスのオブジェクトを代入できる。配列やリストで異なるクラスのオブジェクトをまとめて処理できる。

解説

解答例

interface Printable {
    void print();
}

class Document implements Printable {
    public void print() {
        System.out.println("ドキュメントを印刷");
    }
}

class Image implements Printable {
    public void print() {
        System.out.println("画像を印刷");
    }
}

class Spreadsheet implements Printable {
    public void print() {
        System.out.println("スプレッドシートを印刷");
    }
}

インターフェース型の変数は、そのインターフェースを実装するすべてのクラスのオブジェクトを格納できる。これにより異なるクラスのオブジェクトを統一的に扱える。

インターフェースのdefaultメソッドの説明として正しいものを選べ。

Java 8で追加された機能。インターフェースに処理本体を持つメソッドを定義でき、実装クラスでオーバーライドしなくてもよい。

正解

D. 処理本体を持ち、実装クラスでオーバーライドしなくても使えるメソッド

解説

default メソッドはインターフェース内で 処理本体を持つ メソッドである。既存のインターフェースに新しいメソッドを追加しても、既存の実装クラスが壊れないという利点がある。

継承とインターフェースの違いについて正しいものを選べ。

継承はクラス間の「is-a」関係を表す(1つだけ)。インターフェースは「can-do」(能力)を表す(複数可)。

正解

D. 継承は1つだけ、インターフェースは複数実装できる

解説

Javaでは クラスの継承は1つだけ (単一継承)だが、 インターフェースは複数実装 できる。継承は「〜の一種」(is-a)、インターフェースは「〜ができる」(can-do)という関係を表す。