概要
interface と abstract class はどちらも「抽象化」の手段ですが、設計レビューで「なぜ interface ではなく abstract class にしたのか」と問われたとき、明確に答えられるでしょうか。Java 8 で default メソッドが追加されて以降、interface でも実装を持てるようになり、両者の境界はさらに曖昧になりました。この記事では、interface は「契約と能力の宣言」、abstract class は「共通の状態と振る舞いの共有」という原則に立ち返り、DAO パターンでの差し替え容易性、テスト用スタブの挿入、複数インターフェースの同時実装といった実務で直面する場面を通じて、使い分けの判断基準を整理します。Java 17 の record との組み合わせや、Java 21 の sealed interface による型安全な戻り値設計にも触れます。
使いどころ
データアクセス層を interface で抽象化し、本番用 MySQL 実装とテスト用 InMemory 実装を差し替え可能にする
帳票出力の共通処理(ヘッダー・フッター生成)を abstract class にまとめ、個別帳票でボディ部分だけをオーバーライドする
CSV エクスポートと PDF エクスポートの両方に対応するクラスに Exportable interface を複数実装させ、出力先を柔軟に切り替える
コード例
import java.util.List;
public class InterfaceVsAbstractDemo {
interface Printable {
void print();
}
interface Saveable {
void save(String path);
// default メソッド: フォールバック実装を提供
default void saveToTemp() {
save("/tmp/default.txt");
}
}
abstract static class Animal {
private final String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public abstract String sound();
// 共通ロジック: サブクラスは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("保存先: " + path);
}
}
record UserResult(int id, String name, String source) {}
interface UserDao {
UserResult findById(int id);
void save(String user);
}
static class MySqlUserDao implements UserDao {
@Override
public UserResult findById(int id) {
return new UserResult(id, "User-" + id, "MySQL");
}
@Override
public void save(String user) {
System.out.println("MySQL に保存: " + user);
}
}
// テスト用スタブ: interface のおかげで差し替えが容易
static class InMemoryUserDao implements UserDao {
@Override
public UserResult findById(int id) {
return new UserResult(id, "User-" + id, "Memory");
}
@Override
public void save(String user) {
System.out.println("メモリに保存: " + user);
}
}
public static void main(String[] args) {
// abstract class: 共通ロジックの活用
var dog = new Dog("ポチ");
dog.introduce(); // abstract class の共通メソッド
dog.print(); // Printable interface
dog.saveToTemp(); // Saveable default メソッド
// interface: DAO の差し替え
var daos = List.of(new MySqlUserDao(), new InMemoryUserDao());
for (var dao : daos) {
var result = dao.findById(1);
System.out.println(result);
}
}
}Version Coverage
record で DAO の戻り値を簡潔に定義できる。var による型推論と組み合わせると、匿名クラスでの interface 実装も見通しが良くなる。
// Java 17: record で戻り値を簡潔に定義
record UserResult(int id, String name, String source) {}
interface UserDao {
UserResult findById(int id);
void save(String user);
}
// 匿名クラスでも var + record で簡潔に
var dao = new UserDao() {
public UserResult findById(int id) {
return new UserResult(id, "User-" + id, "MySQL");
}
public void save(String user) { /* ... */ }
};Library Comparison
注意点
interface の default メソッドに業務ロジックを詰め込みすぎると、実装クラス側でオーバーライドされていることに気づきにくくなる。default はフォールバック用途に留めるのが安全
abstract class は単一継承のため、後から別の abstract class を追加できない。将来の拡張を考えると、共通状態が不要なら interface を優先するほうが柔軟性が残る
Java 8 以降の interface は static メソッドも持てるが、継承されない点に注意。ユーティリティ的なメソッドを interface に置く場合は、呼び出し側で インターフェース名.メソッド名() と書く必要がある
DAO インターフェースの戻り値を String や Map にすると型安全性が失われる。Java 17 なら record、Java 21 なら sealed interface と組み合わせて戻り値を明示するとコードレビューでの見通しが良くなる
FAQ
2つの interface が同じシグネチャの default メソッドを持つ場合、実装クラスで明示的にオーバーライドしないとコンパイルエラーになります。InterfaceA.super.method() の形で特定の実装を呼び出せます。
共通フィールドを初期化する場合は必要です。サブクラスから super() で呼び出す設計が基本です。状態を持たないなら interface のほうが適切な場合が多いです。
interface のほうがモック作成は容易です。Mockito などのフレームワークも interface ベースが基本で、abstract class のモックには追加設定が必要になることがあります。