java-recipes

ホーム オブジェクト指向設計(OOP) › OOP-01

OOP-01: インターフェース vs 抽象クラスの使い分け

インターフェースと抽象クラスはどちらも「共通の型を定義する仕組み」ですが、 使い分けの判断基準を理解することで、より柔軟で保守しやすい設計ができます。

インターフェース vs 抽象クラス — 何が違うのか

一言で表すと、インターフェースは「何ができるか(契約・能力)」を定義し、 抽象クラスは「何者か(共通の実装・状態)」を定義します。

項目インターフェース抽象クラス
多重実装複数実装できる1クラスにつき1つだけ継承
フィールド持てない(定数のみ)インスタンス変数を持てる
コンストラクタ持てない持てる
共通実装default メソッド(Java 8+)通常のメソッドとして定義
主な用途DAO・Strategy・Observer パターンTemplate Method パターン

使い分けの判断基準

  • インターフェース: 実装を差し替えたい(テスト用のモック、複数の DB 実装など)。複数の「能力」を持たせたい(Printable かつ Saveable など)。
  • 抽象クラス: 共通の状態(フィールド)や実装を持つクラス群をまとめたい。Template Method パターン(処理の骨格を定義してサブクラスに詳細を委ねる)を使いたい。

サンプルコード

InterfaceVsAbstractSample.java
import java.util.Arrays;
import java.util.List;

public class InterfaceVsAbstractSample {

    // === インターフェース: 契約・能力定義(複数実装可) ===

    interface Printable {
        void print(); // 抽象メソッド
    }

    // Java 8+: default メソッド
    interface Saveable {
        void save(String path);

        default void saveToTemp() { // default メソッド: 実装を持てる
            save("/tmp/default.txt");
        }

        static Saveable noOp() { // static メソッド
            return path -> System.out.println("NoOp: " + path);
        }
    }

    // === 抽象クラス: 共通実装・状態を持つ ===

    abstract static class Animal {
        private final String name; // フィールドを持てる

        public Animal(String name) {
            this.name = name;
        }

        // 共通の実装
        public String getName() {
            return name;
        }

        // サブクラスで必ず実装する
        public abstract String sound();

        public void introduce() {
            System.out.println("私は " + name + " です。" + sound() + " と鳴きます。");
        }
    }

    // 具体クラス: 抽象クラスを継承 + インターフェースを複数実装
    static class Dog extends Animal implements Printable, Saveable {
        public Dog(String name) {
            super(name);
        }

        @Override
        public String sound() {
            return "ワン";
        }

        @Override
        public void print() {
            System.out.println("Dog: " + getName());
        }

        @Override
        public void save(String path) {
            System.out.println("Saving Dog to: " + path);
        }
    }

    // DAO パターン: インターフェースで抽象化
    interface UserDao {
        String findById(int id);
        void save(String user);
    }

    // 本番用実装
    static class MySqlUserDao implements UserDao {
        @Override
        public String findById(int id) {
            return "MySQL: User-" + id;
        }

        @Override
        public void save(String user) {
            System.out.println("MySQL: saving " + user);
        }
    }

    // テスト用スタブ
    static class InMemoryUserDao implements UserDao {
        @Override
        public String findById(int id) {
            return "Memory: User-" + id;
        }

        @Override
        public void save(String user) {
            System.out.println("Memory: saving " + user);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== 抽象クラス ===");
        Dog dog = new Dog("ポチ");
        dog.introduce();
        dog.print();
        dog.saveToTemp(); // default メソッド

        System.out.println("\n=== DAO パターン(インターフェースによる抽象化) ===");
        List<UserDao> daos = Arrays.asList(new MySqlUserDao(), new InMemoryUserDao());
        for (UserDao dao : daos) {
            System.out.println(dao.findById(1));
        }
    }
}

Java 8 からインターフェースに default メソッドと static メソッドを定義できるようになりました。これにより、インターフェースに共通の処理を持たせることが可能になっています。

よくあるミス・注意点

⚠️ すべてを抽象クラスで書いて単一継承の制限にはまる

Java は1つのクラスから1つのクラスしか継承できません(単一継承)。 そのため「Animal を継承した Dog はすでに別のクラスを継承できない」という制約が生まれます。 「何ができるか(能力)」を表現する場合は、インターフェースを使うと複数の能力を持たせられます。 例えば Dog extends Animal implements Printable, Saveable のように、 抽象クラスによる「何者か」の定義とインターフェースによる「能力」の定義を組み合わせるのが典型的なパターンです。

⚠️ Java 8 以前の感覚で default メソッドを知らずにユーティリティクラスを乱用する

Java 8 よりも前は、インターフェースに実装を持たせることができませんでした。 そのため、共通処理を XxxUtils のような static メソッドだけを持つユーティリティクラスに書くパターンが多く使われていました。 Java 8 以降はインターフェースに default メソッドを定義できるため、 共通の振る舞いをインターフェース自体に持たせることができます。 ただし、default メソッドは「デフォルトの実装」であり、実装クラスでオーバーライドして変更することもできます。

テストする観点

  • 同じインターフェース(UserDao)の実装を差し替えても、呼び出し側のコードが変わらないこと(DAO パターンの確認)
  • テスト時に InMemoryUserDao に差し替えて、外部 DB なしでテストできること
  • default メソッド(saveToTemp())が正しく動作し、サブクラスでオーバーライドできること
  • 抽象クラスのコンストラクタ(Animal(String name))が継承先でも正しく呼ばれること
  • Dog が Printable と Saveable の両方のインターフェースを正しく実装していること(キャスト確認)

GitHub でソースコードを見る →