Javaのインターフェースは「何ができるか(契約)」を決める型、抽象クラスは「共通の状態と手順」を子クラスに引き継ぐための未完成のクラスです。 いちばん大きな違いは、インターフェースは1つのクラスに何個でも実装できるのに対し、抽象クラスは1つしか継承できないこと、そしてインスタンスフィールド(状態)とコンストラクタを持てるのは抽象クラスだけということです。

Java 8以降、インターフェースにもdefaultメソッドで処理を書けるようになり、「違いがわかりにくい」と感じる方が増えました。この記事では、両者の違いを1枚の比較表にまとめたうえで、同じ「通知の送信」を両方式で書いてOpenJDK 21.0.10で実行し、出力を比べます。実装し忘れ・インスタンス化・defaultメソッドの衝突(ダイヤモンド問題)といったコンパイルエラーの全文と直し方、迷ったときの判断フローチャート、sealed interface+record+switchの組み合わせまで紹介します。

インターフェースと抽象クラスの違い早見表

比較項目 インターフェース(interface) 抽象クラス(abstract class)
一言でいうと できること(契約)の宣言 共通部分を持った未完成のクラス
1クラスが持てる数 いくつでもimplementsできる 1つだけextendsできる
インスタンスフィールド(状態) 持てない 持てる
定数 書いたフィールドは自動でpublic static final 自由に宣言できる
コンストラクタ 持てない 持てる(子からsuper(...)で呼ぶ)
抽象メソッド 何も書かなければpublic abstract abstractを付けて宣言
処理を持つメソッド default(Java 8〜)、static(Java 8〜)、private(Java 9〜) 普通のメソッドとして自由に書ける
メソッドのアクセス修飾子 publicかprivateのみ(protected不可) public・protected・パッケージ・privateすべて可
finalメソッド 書けない 書ける(手順を子に変えさせない)
sealed(Java 17〜) sealed interfaceにできる abstract sealed classにできる
実装側にrecord・enumを使えるか 使える 使えない(record・enumはextendsできない)
ラムダ式で実装できるか 抽象メソッドが1つならできる できない
newできるか できない(匿名クラスなら可) できない(匿名クラスなら可)
向いている用途 型の契約、差し替え、複数の役割 共通の状態・手順の共有(テンプレートメソッド)

迷ったら、まずインターフェースで設計し、「共通の状態や手順を持たせたい」ときに抽象クラスを足すのが、Javaでよく使われる考え方です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaインターフェースと抽象クラスの違い|比較表と使い分け」

動作確認した環境

この記事のコードは、OpenJDK 21.0.10でjava -Dstdout.encoding=UTF-8 ファイル名.java(単一ファイル実行、外部ライブラリなし)として実行し、出力をそのまま載せています。 コンパイルエラーはjavacで確認し、英語表示(-J-Duser.language=en)で掲載しています。Java 22〜25の変更点は、この環境では動かせないため、公式(JEP)で確認した内容として注記しています。

インターフェースが自動で付ける修飾子を確かめる

インターフェースでは、書かなくても自動で付く修飾子があります。 比較表の内容をリフレクション(実行中にクラスの情報を調べる仕組み)で確認しました。

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class ModifiersCheck {
    public static void main(String[] args) throws Exception {
        var f = Limits.class.getDeclaredField("MAX_ITEMS");
        System.out.println("interface のフィールド MAX_ITEMS: " + Modifier.toString(f.getModifiers()));
        for (String name : new String[]{"check", "describe", "of", "helper"}) {
            for (Method m : Limits.class.getDeclaredMethods()) {
                if (m.getName().equals(name)) {
                    System.out.println("interface のメソッド " + name + "(): " + Modifier.toString(m.getModifiers())
                        + (m.isDefault() ? " (default)" : ""));
                }
            }
        }
        System.out.println("abstract class のコンストラクタ数: " + Base.class.getDeclaredConstructors().length
            + " / interface のコンストラクタ数: " + Limits.class.getDeclaredConstructors().length);
    }
}

interface Limits {
    int MAX_ITEMS = 100;                 // 何も書かなくても public static final
    boolean check(int n);                // public abstract
    default String describe() { return helper() + MAX_ITEMS; }
    static Limits of() { return n -> n <= MAX_ITEMS; }
    private String helper() { return "上限: "; }
}

abstract class Base {
    protected int value;
    protected Base(int v) { this.value = v; }
}

実行結果(OpenJDK 21.0.10):

interface のフィールド MAX_ITEMS: public static final
interface のメソッド check(): public abstract
interface のメソッド describe(): public (default)
interface のメソッド of(): public static
interface のメソッド helper(): private
abstract class のコンストラクタ数: 1 / interface のコンストラクタ数: 0

int MAX_ITEMS = 100;と書いただけでpublic static final(定数)になっています。 インターフェースのフィールドは、すべての実装クラスで共有される定数であり、オブジェクトごとの値(状態)は持てません。これが「インターフェースは状態を持てない」と言われる理由です。

インターフェースに書けるもの 書き方 自動で付く修飾子 使える版
定数 int MAX = 100; public static final Java 1.0〜
抽象メソッド void send(String m); public abstract Java 1.0〜
defaultメソッド default void f() { ... } public Java 8〜
staticメソッド static X of() { ... } public Java 8〜
privateメソッド private void g() { ... } ― Java 9〜
入れ子の型 record・enum・interfaceなど public static ―

同じ「通知の送信」を両方式で書いて比べる

ここからは、メールとチャットに通知を送る処理を、抽象クラス版とインターフェース版で書き比べます。 どちらも同じ結果を出せますが、「どこに何を書くか」が違います。

抽象クラス版:共通の状態と手順を親が持つ

抽象クラスは、宛先や送信回数といった「状態」と、整形→送信→回数を数える「手順」を親クラスにまとめられます。 子クラスは、違う部分(整形と送信方法)だけを書きます。このように手順の骨組みを親が決める形を、テンプレートメソッドパターンと呼びます。

import java.util.List;

public class AbstractNotifierDemo {
    public static void main(String[] args) {
        List<Notifier> list = List.of(new MailNotifier("taro@example.com"), new ChatNotifier("#general"));
        for (Notifier n : list) {
            n.send("在庫が10件を下回りました");
            n.send("新しいタスクが割り当てられました");
        }
        for (Notifier n : list) {
            System.out.println(n.getClass().getSimpleName() + " の送信回数: " + n.sentCount());
        }
    }
}

/** 抽象クラス版:共通の「状態」と「手順」を親が持つ */
abstract class Notifier {
    private final String destination;   // 状態(インスタンスフィールド)
    private int sentCount;              // 状態(送信回数)

    protected Notifier(String destination) {   // コンストラクタで入力チェック
        if (destination == null || destination.isBlank()) {
            throw new IllegalArgumentException("宛先が空です");
        }
        this.destination = destination;
    }

    /** テンプレートメソッド:手順は親が決め、細部だけ子に任せる */
    public final void send(String message) {
        String body = format(message);
        deliver(destination, body);
        sentCount++;
    }

    protected abstract String format(String message);        // 子が実装
    protected abstract void deliver(String to, String body);  // 子が実装

    public int sentCount() { return sentCount; }
}

class MailNotifier extends Notifier {
    MailNotifier(String address) { super(address); }
    @Override protected String format(String message) { return "件名: お知らせ / 本文: " + message; }
    @Override protected void deliver(String to, String body) { System.out.println("[MAIL to " + to + "] " + body); }
}

class ChatNotifier extends Notifier {
    ChatNotifier(String channel) { super(channel); }
    @Override protected String format(String message) { return ":bell: " + message; }
    @Override protected void deliver(String to, String body) { System.out.println("[CHAT " + to + "] " + body); }
}

実行結果(OpenJDK 21.0.10):

[MAIL to taro@example.com] 件名: お知らせ / 本文: 在庫が10件を下回りました
[MAIL to taro@example.com] 件名: お知らせ / 本文: 新しいタスクが割り当てられました
[CHAT #general] :bell: 在庫が10件を下回りました
[CHAT #general] :bell: 新しいタスクが割り当てられました
MailNotifier の送信回数: 2
ChatNotifier の送信回数: 2

抽象クラス版の読みどころ

  • sentCount(状態)を親が持つので、子クラスは回数を数える処理を書かなくても回数がそろいます。
  • コンストラクタで宛先を検査しているので、宛先が空の通知オブジェクトは作れません。
  • send()をfinalにして手順を固定し、子が変えてよい部分をprotected abstractで限定しています。インターフェースではprotectedもfinalも使えないため、「手順を守らせる」設計は抽象クラスの得意分野です。

インターフェース版:契約だけを決め、状態は実装側が持つ

インターフェースは「sendできること」だけを約束します。 実装クラスは自由に作れるので、recordで書いたり、ラムダ式で書いたり、複数の役割を兼ねたりできます。

import java.util.List;

public class InterfaceNotifierDemo {
    public static void main(String[] args) {
        List<Notifier> list = List.of(
            new MailNotifier("taro@example.com"),
            new ChatNotifier("#general"),
            msg -> System.out.println("[LOG] " + msg)          // 関数型インターフェースならラムダでも書ける
        );
        for (Notifier n : list) {
            n.send("在庫が10件を下回りました");
        }
        // default メソッド:実装クラスに書かなくても使える
        list.get(1).sendAll(List.of("1件目", "2件目"));

        // static メソッド:インターフェース名で呼ぶ
        Notifier console = Notifier.console();
        console.send("static ファクトリから作成");

        // 1つのクラスが複数のインターフェースを実装できる
        CountingMailNotifier c = new CountingMailNotifier("hanako@example.com");
        c.send("A");
        c.send("B");
        System.out.println("CountingMailNotifier は Notifier? " + (c instanceof Notifier)
            + " / Countable? " + (c instanceof Countable) + " / 回数: " + c.count());
    }
}

/** インターフェース版:できること(契約)だけを決める */
interface Notifier {
    void send(String message);                       // 抽象メソッド(public abstract が省略されている)

    default void sendAll(List<String> messages) {    // default メソッド(Java 8〜)
        for (String m : messages) send(decorate(m));
    }

    private String decorate(String m) {              // private メソッド(Java 9〜)
        return "(一括) " + m;
    }

    static Notifier console() {                      // static メソッド(Java 8〜)
        return msg -> System.out.println("[CONSOLE] " + msg);
    }
}

interface Countable {
    int count();
}

/** 状態は実装クラス側が持つ */
record MailNotifier(String address) implements Notifier {
    public void send(String message) { System.out.println("[MAIL to " + address + "] " + message); }
}

record ChatNotifier(String channel) implements Notifier {
    public void send(String message) { System.out.println("[CHAT " + channel + "] " + message); }
}

class CountingMailNotifier implements Notifier, Countable {
    private final MailNotifier delegate;   // 継承ではなく「持つ」(委譲)
    private int count;
    CountingMailNotifier(String address) { this.delegate = new MailNotifier(address); }
    @Override public void send(String message) { delegate.send(message); count++; }
    @Override public int count() { return count; }
}

実行結果(OpenJDK 21.0.10):

[MAIL to taro@example.com] 在庫が10件を下回りました
[CHAT #general] 在庫が10件を下回りました
[LOG] 在庫が10件を下回りました
[CHAT #general] (一括) 1件目
[CHAT #general] (一括) 2件目
[CONSOLE] static ファクトリから作成
[MAIL to hanako@example.com] A
[MAIL to hanako@example.com] B
CountingMailNotifier は Notifier? true / Countable? true / 回数: 2

インターフェース版の読みどころ

  • recordやラムダ式で実装できます。 抽象クラスはrecordから継承できず、ラムダ式でも書けません。3つ目のmsg -> ...は、抽象メソッドが1つだけのインターフェース(関数型インターフェース)だからこそ書ける形です。
  • defaultメソッドsendAllは、実装クラスに何も書かなくても使えます。 内部のdecorateはprivateメソッドなので、外からは見えません。
  • CountingMailNotifierはNotifierとCountableの2つを実装しています。送信回数という状態は、インターフェースではなく実装クラスが持ち、送信そのものはMailNotifierに任せています(委譲)。

2つの方式の比較

観点 抽象クラス版 インターフェース版
共通の状態(送信回数) 親が持つので子は何も書かなくてよい 必要なクラスがそれぞれ持つ
手順の強制 finalで手順を固定できる defaultは上書きできるので強制はできない
実装の自由度 1つの親にしか属せない record・ラムダ・複数の役割が可能
新しい種類の追加 親の作りに合わせる必要がある sendを書くだけで追加できる
テストでの差し替え 親の状態もいっしょに引き継ぐ ラムダ1行で差し替え用の実装を作れる

「共通の状態と手順を持たせたいなら抽象クラス、差し替えやすさと組み合わせやすさを重視するならインターフェース」が、出力から読み取れる違いです。両方を組み合わせて、インターフェースを型として公開し、共通部分を抽象クラスで用意する(ListインターフェースとAbstractListクラスの関係と同じ形)こともよくあります。

使い分けの判断フローチャート

迷ったときは、次の順に確認すると選びやすくなります。

flowchart TD
    A[共通化したいものがある] --> B{インスタンスごとの状態や<br/>コンストラクタでの検査が必要?}
    B -- はい --> C{手順を固定したい・<br/>protected で子だけに見せたい?}
    C -- はい --> D[抽象クラス<br/>テンプレートメソッド]
    C -- いいえ --> E[インターフェース+<br/>状態を持つ実装クラス・委譲]
    B -- いいえ --> F{1つのクラスに<br/>複数の役割を持たせたい?}
    F -- はい --> G[インターフェース]
    F -- いいえ --> H{種類が決まっていて<br/>switch で網羅したい?}
    H -- はい --> I[sealed interface + record]
    H -- いいえ --> J{実装をラムダや<br/>record で書きたい?}
    J -- はい --> G
    J -- いいえ --> K[インターフェースから始め、<br/>共通部分が増えたら抽象クラスを追加]
判断の質問 はい → いいえ →
オブジェクトごとの状態を親で持たせたい? 抽象クラス 次へ
手順をfinalで固定したい? 抽象クラス インターフェース
1クラスに複数の役割を持たせたい? インターフェース 次へ
種類が決まっていて網羅的に分岐したい? sealed interface+record 次へ
ラムダ式・recordで実装したい? インターフェース どちらでも可(まずインターフェース)

よくあるコンパイルエラーと直し方

ここでは、両者を使うときに出やすいコンパイルエラーを、実際のエラー文とともに確認します。 いずれもjavac(OpenJDK 21.0.10)で出力をそのまま載せています。

エラー文(抜粋) 原因 直し方
is not abstract and does not override abstract method 抽象メソッドの実装し忘れ メソッドを実装する、またはクラスをabstractにする
is abstract; cannot be instantiated 抽象クラス・インターフェースをnewした 実装クラスをnewする(または匿名クラス)
inherits unrelated defaults for 2つのインターフェースのdefaultが衝突 オーバーライドしてX.super.method()で選ぶ
attempting to assign weaker access privileges; was public 実装メソッドにpublicを付け忘れ publicを付ける
'{' expected(extends A, B) クラスを2つ継承しようとした 片方をインターフェースにする、委譲にする
cannot find symbol(Square.unit()) インターフェースのstaticメソッドを実装クラス経由で呼んだ Shape.unit()のようにインターフェース名で呼ぶ

エラー1:抽象メソッドの実装し忘れ

interface Shape {
    double area();
    String name();
}

class Square implements Shape {
    private final double side;
    Square(double side) { this.side = side; }
    public double area() { return side * side; }
    // name() を実装し忘れた
}
Forgot.java:6: error: Square is not abstract and does not override abstract method name() in Shape
class Square implements Shape {
^
1 error

エラー文のname()の部分に、実装が足りないメソッド名が出ます。 public String name() { return "正方形"; }を追加すると解消します。実装をさらに子クラスに任せたい場合は、abstract class Square implements Shapeのようにクラス自体を抽象クラスにする方法もあります。

エラー2:抽象クラス・インターフェースを new した

abstract class Animal {
    abstract String sound();
}

interface Shape {
    double area();
}

public class NewAbstract {
    public static void main(String[] args) {
        Animal a = new Animal();
        Shape s = new Shape();
    }
}
NewAbstract.java:11: error: Animal is abstract; cannot be instantiated
        Animal a = new Animal();
                   ^
NewAbstract.java:12: error: Shape is abstract; cannot be instantiated
        Shape s = new Shape();
                  ^
2 errors

インターフェースも同じ「is abstract; cannot be instantiated」になります。 実装クラスをnewするか、その場で中身を書く匿名クラス(new Animal() { String sound() { ... } })を使います。匿名クラスの例は、次のエラー3の修正コードに含めています。

エラー3:default メソッドの衝突(ダイヤモンド問題)

2つのインターフェースが同じ名前のdefaultメソッドを持っていると、どちらを使えばよいか決まらずエラーになります。 クラスの多重継承で起きる「ダイヤモンド問題」の、インターフェース版です。

interface Printer {
    default String hello() { return "Printer"; }
}

interface Scanner {
    default String hello() { return "Scanner"; }
}

class MultiFunctionDevice implements Printer, Scanner {
}
Diamond.java:9: error: types Printer and Scanner are incompatible;
class MultiFunctionDevice implements Printer, Scanner {
^
  class MultiFunctionDevice inherits unrelated defaults for hello() from types Printer and Scanner
1 error

直し方は、クラス側でhello()をオーバーライドし、インターフェース名.super.hello()で使いたい方を呼ぶことです。あわせて、「クラスのメソッドはインターフェースのdefaultより優先される」ルールと、匿名クラスでのnewも確認します。

public class DiamondFixed {
    public static void main(String[] args) {
        System.out.println(new MultiFunctionDevice().hello());
        System.out.println(new OnlyPrinter().hello());
        // 抽象クラス・インターフェースは匿名クラスなら new できる
        Animal cat = new Animal() { String sound() { return "にゃー"; } };
        System.out.println("匿名クラス: " + cat.sound());
    }
}

interface Printer {
    default String hello() { return "Printer"; }
}

interface Scanner {
    default String hello() { return "Scanner"; }
}

class MultiFunctionDevice implements Printer, Scanner {
    @Override
    public String hello() {
        // どちらを使うかを明示する(両方呼ぶこともできる)
        return Printer.super.hello() + " + " + Scanner.super.hello();
    }
}

abstract class Device {
    public String hello() { return "Device(クラス)"; }
}

class OnlyPrinter extends Device implements Printer {
    // クラスのメソッドがインターフェースの default より優先される(クラス優先ルール)
}

abstract class Animal {
    abstract String sound();
}

実行結果(OpenJDK 21.0.10):

Printer + Scanner
Device(クラス)
匿名クラス: にゃー

defaultメソッドの選ばれ方のルールは、次の3つです(Java言語仕様の継承規則を整理したもの)。

優先順位 ルール 実行結果での確認
1 クラス(親クラスを含む)のメソッドが、インターフェースのdefaultより優先 OnlyPrinterはDevice(クラス)
2 親子関係のあるインターフェースどうしなら、より具体的な(子の)インターフェースが優先 (この記事では未掲載)
3 それでも決まらなければコンパイルエラー。クラスでオーバーライドしX.super.m()で選ぶ MultiFunctionDeviceはPrinter + Scanner

エラー4:実装メソッドに public を付け忘れた

interface Shape {
    double area();
}

class Circle implements Shape {
    double area() { return 3.14; }   // public を付け忘れた
}
Weaker.java:6: error: area() in Circle cannot implement area() in Shape
    double area() { return 3.14; }   // public を付け忘れた
           ^
  attempting to assign weaker access privileges; was public
1 error

インターフェースの抽象メソッドは自動でpublicなので、実装側もpublicが必要です。抽象クラスの抽象メソッドなら、親と同じかより広い公開範囲であればよいので、protectedや(同じパッケージなら)修飾子なしでも実装できます。

エラー5:抽象クラスを2つ継承しようとした

abstract class A {}
abstract class B {}
class C extends A, B {}
TwoParents.java:3: error: '{' expected
class C extends A, B {}
                 ^
1 error

extendsの後ろにはクラスを1つしか書けないため、構文エラーになります。 両方の機能が必要なら、片方をインターフェースにする(class C extends A implements B)か、片方のオブジェクトをフィールドに持って処理を任せる(委譲)形にします。

エラー6:インターフェースの static メソッドを実装クラス経由で呼んだ

interface Shape {
    static Shape unit() { return null; }
}
class Square implements Shape {}
public class StaticCall {
    public static void main(String[] args) {
        Shape a = Shape.unit();    // OK
        Shape b = Square.unit();   // 実装クラス経由では呼べない
    }
}
StaticCall.java:8: error: cannot find symbol
        Shape b = Square.unit();   // 実装クラス経由では呼べない
                        ^
  symbol:   method unit()
  location: class Square
1 error

インターフェースのstaticメソッドは実装クラスに引き継がれません。 抽象クラスのstaticメソッドは子クラス名.method()でも呼べるので、ここも両者の違いです。

sealed interface + record + switch で「種類の決まった型」を表す

Java 17で正式になったsealedを使うと、「この型を実装できるのはこのクラスだけ」と限定できます。 Java 21で正式になったswitchのパターンマッチングと組み合わせると、defaultを書かなくても全種類を網羅しているかをコンパイラが確認してくれます。

import java.util.List;

public class SealedShapes {
    // 図形ごとの処理を「外側」に書く:switch パターンで分岐
    static double area(Shape s) {
        return switch (s) {
            case Circle c    -> Math.PI * c.r() * c.r();
            case Rect(double w, double h) -> w * h;           // record パターンで分解
            case Triangle t  -> t.base() * t.height() / 2;
        };                                                    // default 不要(sealed で網羅)
    }

    static String label(Shape s) {
        return switch (s) {
            case Circle c when c.r() == 0 -> "点";
            case Circle c   -> "円 r=" + c.r();
            case Rect r when r.w() == r.h() -> "正方形 " + r.w();
            case Rect r     -> "長方形 " + r.w() + "x" + r.h();
            case Triangle t -> "三角形";
        };
    }

    public static void main(String[] args) {
        List<Shape> shapes = List.of(new Circle(1), new Rect(2, 3), new Rect(4, 4), new Triangle(3, 4), new Circle(0));
        for (Shape s : shapes) {
            System.out.printf("%-12s 面積=%.2f%n", label(s), area(s));
        }
    }
}

sealed interface Shape permits Circle, Rect, Triangle {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}

実行結果(OpenJDK 21.0.10):

円 r=1.0      面積=3.14
長方形 2.0x3.0  面積=6.00
正方形 4.0      面積=16.00
三角形          面積=6.00
点            面積=0.00

sealed interface+recordは、「データの種類が決まっていて、処理をあとから増やしたい」ときに向いています。 一方、同じ図形を抽象クラスで書くと、名前などの共通の状態と、面積を使う共通処理を親に持たせられます。

import java.util.List;

public class AbstractShapes {
    public static void main(String[] args) {
        List<Shape> shapes = List.of(new Circle("円A", 1), new Rect("長方形B", 2, 3));
        for (Shape s : shapes) System.out.println(s.describe());
        System.out.println("作成した図形の数: " + Shape.created());
    }
}

/** 抽象クラス版:共通の状態(名前)と共通処理(describe)を親が持ち、面積の計算だけ子に任せる */
abstract sealed class Shape permits Circle, Rect {
    private static int created = 0;
    private final String name;
    protected Shape(String name) { this.name = name; created++; }
    public abstract double area();
    public final String describe() { return String.format("%s の面積は %.2f", name, area()); }
    static int created() { return created; }
}

final class Circle extends Shape {
    private final double r;
    Circle(String name, double r) { super(name); this.r = r; }
    @Override public double area() { return Math.PI * r * r; }
}

final class Rect extends Shape {
    private final double w, h;
    Rect(String name, double w, double h) { super(name); this.w = w; this.h = h; }
    @Override public double area() { return w * h; }
}

実行結果(OpenJDK 21.0.10):

円A の面積は 3.14
長方形B の面積は 6.00
作成した図形の数: 2
書き方 処理の置き場所 種類を増やすとき 処理を増やすとき
抽象クラス+オーバーライド 各クラスの中(area()) クラスを1つ足すだけ 全クラスにメソッド追加
sealed interface+record+switch 外側のメソッド(area(Shape)) permitsと各switchに追加(漏れはコンパイラが指摘) メソッドを1つ足すだけ

種類を増やしたのに switch を直し忘れたとき

sealedの良さは、書き漏れをコンパイルエラーで教えてくれることです。 case Triangleを書き忘れ、さらにpermitsにないHexagonを追加すると、次のようになりました。

sealed interface Shape permits Circle, Rect, Triangle {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}

class Area {
    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.r() * c.r();
            case Rect r   -> r.w() * r.h();
        };
    }
}

record Hexagon(double side) implements Shape {}
Missing.java:8: error: the switch expression does not cover all possible input values
        return switch (s) {
               ^
Missing.java:15: error: class is not allowed to extend sealed class: Shape (as it is not listed in its 'permits' clause)
record Hexagon(double side) implements Shape {}
^
2 errors

permitsに書いていない型は実装できず、switchで書き漏れがあればコンパイルが通りません。 ここでdefaultを書いてしまうと、書き漏れを検出できなくなるため、sealed型のswitchではdefaultを書かないのがおすすめです。recordパターンやwhenガードの詳しい書き方は「Java switchパターンマッチング」、recordそのものは「Java recordの使い方」の記事で扱っています。

Java 22〜25 での関連する変更(公式で確認)

この環境(Java 21)では動かせないため、以下はOpenJDKの公式ページ(JEP)で確認した内容です。

バージョン 内容 抽象クラス・インターフェースとの関係
Java 22〜24 JEP 447 / 482 / 492(プレビュー) コンストラクタでsuper(...)の前に処理を書けるようにする提案
Java 25 JEP 513「Flexible Constructor Bodies」が正式機能 子クラスのコンストラクタで、super(...)を呼ぶ前に引数の検査などを書ける

Java 21では、子クラスのコンストラクタでsuper(...)より前に処理を書くと、次のエラーになります(実行して確認)。

abstract class Shape {
    protected Shape(String name) { }
}
class Circle extends Shape {
    Circle(String name, double r) {
        if (r < 0) throw new IllegalArgumentException("r < 0");
        super(name);
    }
}
BeforeSuper.java:7: error: call to super must be first statement in constructor
        super(name);
             ^
1 error

Java 25ではJEP 513により、このようにsuper(...)の前で引数を検査する書き方ができると公式に説明されています(この環境では未実行)。Java 21までは、検査をstaticメソッドに切り出してsuper(check(name))のように書くのが一般的です。インターフェースにはコンストラクタがないため、この変更は抽象クラス(と通常のクラス)に関係するものです。

よくある質問

Q. defaultメソッドがあるなら、抽象クラスはもう要らないのですか? A. 役割が違うので、両方使い続けられています。 defaultメソッドは「インターフェースにあとからメソッドを足しても、既存の実装クラスを壊さない」ための仕組みで、インスタンスフィールド(状態)・コンストラクタ・protected・finalメソッドは使えません。共通の状態や固定した手順を持たせたいときは、抽象クラスが向いています。

Q. インターフェースに変数(状態)を持たせることはできますか? A. できません。 int count = 0;と書いても、自動でpublic static finalの定数になります(この記事のリフレクションで確認)。状態が必要なら、実装クラスにフィールドを持たせるか、抽象クラスを使います。

Q. 抽象クラスにコンストラクタを書く意味はありますか? A. あります。 抽象クラス自体はnewできませんが、子クラスのコンストラクタからsuper(...)で呼ばれ、共通のフィールドの初期化や入力チェックを1か所で行えます。この記事のNotifierでは、宛先が空ならIllegalArgumentExceptionを投げています。

Q. インターフェースと抽象クラスを組み合わせてもよいですか? A. よく使われる組み合わせです。 型としてはインターフェースを公開し、共通部分を実装した抽象クラスを用意しておくと、利用者は「抽象クラスを継承して楽をする」か「インターフェースを直接実装して自由に作る」かを選べます。Java標準ライブラリのListとAbstractListがこの形です。

まとめ

  • インターフェースは「できること(契約)」、抽象クラスは「共通の状態と手順を持つ未完成のクラス」
  • インターフェースはいくつでも実装でき、抽象クラスは1つだけ継承できる
  • 状態(インスタンスフィールド)・コンストラクタ・protected・finalメソッドは抽象クラスだけ。インターフェースのフィールドは自動でpublic static final
  • defaultの衝突は、オーバーライドしてX.super.method()で解決。クラスのメソッドはdefaultより優先される
  • record・ラムダ式で実装したい、複数の役割を持たせたいならインターフェース
  • 種類が決まっているならsealed interface+record+switchで、書き漏れをコンパイラに見つけてもらえる
  • 迷ったらインターフェースから始め、共通部分が増えたら抽象クラスを足す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaインターフェースと抽象クラスの違い|比較表と使い分け」

株式会社RJCでは、インターフェースと抽象クラスの使い分けのような設計の考え方を、仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?