はじめに

Javaのインターフェースは、クラス間の契約を定義する仕組みです。インターフェースを活用することで、実装の詳細に依存しない疎結合な設計を実現でき、テストしやすく拡張性の高いコードを書くことができます。

この記事では、インターフェースの基本概念から始まり、implementsキーワードによる実装、複数インターフェースの実装、Java 8以降で導入されたデフォルトメソッドとstaticメソッド、そして関数型インターフェースまで、段階的に解説します。

インターフェースとは何か

インターフェース(Interface)とは、クラスが実装すべきメソッドのシグネチャ(名前・引数・戻り値) を定義した型です。インターフェースは「何ができるか」を定義し、「どのように行うか」は実装クラスに委ねます。

契約としてのインターフェース

インターフェースは、異なるクラス間での契約として機能します。

flowchart TB
    subgraph "契約(インターフェース)"
        Interface["Printable<br/>+ print(): void"]
    end
    
    subgraph "実装クラス"
        Document["Document"]
        Image["Image"]
        Report["Report"]
    end
    
    Interface -.->|implements| Document
    Interface -.->|implements| Image
    Interface -.->|implements| Report

この例では、Printableインターフェースが「印刷できる」という契約を定義しています。DocumentImageReportクラスはそれぞれ独自の方法で印刷を実装しますが、利用者側からは統一された方法で扱えます。

can-do関係で考える

継承がis-a関係(〜は〜の一種である)で表現されるのに対し、インターフェースはcan-do関係(〜は〜ができる)で表現されます。

関係 説明
is-a Dog is an Animal 犬は動物の一種である
can-do Dog can Swim 犬は泳ぐことができる
can-do Document can Print ドキュメントは印刷できる

インターフェースを使うメリット

メリット 説明
疎結合 実装の詳細に依存せず、インターフェースを通じてやり取りできる
多重実装 1つのクラスが複数のインターフェースを実装できる
ポリモーフィズム 異なる実装を同じ型として扱える
テスト容易性 モック(代替実装)を簡単に作成できる
拡張性 新しい実装を追加しても既存コードに影響しない

インターフェースの定義と実装

インターフェースの定義方法

インターフェースはinterfaceキーワードを使用して定義します。

1
2
3
4
5
6
7
public interface Printable {
    // 抽象メソッド(public abstractは省略可能)
    void print();
    
    // 定数(public static finalは省略可能)
    int MAX_PAGES = 100;
}

インターフェースのメソッドは暗黙的にpublic abstractであり、フィールドは暗黙的にpublic static finalです。

implementsによる実装

クラスがインターフェースを実装するには、implementsキーワードを使用します。

1
2
3
public interface Printable {
    void print();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Document implements Printable {
    private String content;
    
    public Document(String content) {
        this.content = content;
    }
    
    @Override
    public void print() {
        System.out.println("=== ドキュメント出力 ===");
        System.out.println(content);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Image implements Printable {
    private String filename;
    
    public Image(String filename) {
        this.filename = filename;
    }
    
    @Override
    public void print() {
        System.out.println("=== 画像出力 ===");
        System.out.println("画像ファイル: " + filename);
    }
}

インターフェース型としての利用

インターフェース型の変数に、実装クラスのインスタンスを代入できます。これにより、ポリモーフィズムを活用した柔軟な処理が可能になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {
    public static void main(String[] args) {
        // インターフェース型の変数に実装クラスを代入
        Printable doc = new Document("Hello, World!");
        Printable img = new Image("photo.jpg");
        
        // 同じメソッドを呼び出しても、実装によって動作が異なる
        doc.print();
        // 出力:
        // === ドキュメント出力 ===
        // Hello, World!
        
        img.print();
        // 出力:
        // === 画像出力 ===
        // 画像ファイル: photo.jpg
        
        // 配列やリストで統一的に扱える
        Printable[] printables = {doc, img};
        for (Printable p : printables) {
            p.print();
        }
    }
}

実践例: 支払い処理システム

インターフェースを使用した実践的な例を見てみましょう。

1
2
3
4
5
// 支払い方法を定義するインターフェース
public interface PaymentMethod {
    boolean processPayment(int amount);
    String getPaymentType();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class CreditCard implements PaymentMethod {
    private String cardNumber;
    
    public CreditCard(String cardNumber) {
        this.cardNumber = cardNumber;
    }
    
    @Override
    public boolean processPayment(int amount) {
        System.out.println("クレジットカード決済: " + amount + "円");
        System.out.println("カード番号: ****-****-****-" + 
                           cardNumber.substring(cardNumber.length() - 4));
        return true;
    }
    
    @Override
    public String getPaymentType() {
        return "クレジットカード";
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class BankTransfer implements PaymentMethod {
    private String accountNumber;
    
    public BankTransfer(String accountNumber) {
        this.accountNumber = accountNumber;
    }
    
    @Override
    public boolean processPayment(int amount) {
        System.out.println("銀行振込: " + amount + "円");
        System.out.println("口座番号: " + accountNumber);
        return true;
    }
    
    @Override
    public String getPaymentType() {
        return "銀行振込";
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class PaymentProcessor {
    // インターフェースに依存しているため、どんな支払い方法でも処理できる
    public void executePayment(PaymentMethod method, int amount) {
        System.out.println("支払い方法: " + method.getPaymentType());
        boolean success = method.processPayment(amount);
        if (success) {
            System.out.println("決済完了");
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();
        
        PaymentMethod credit = new CreditCard("1234567890123456");
        PaymentMethod bank = new BankTransfer("123-4567890");
        
        processor.executePayment(credit, 5000);
        System.out.println();
        processor.executePayment(bank, 10000);
    }
}

この設計の利点は、新しい支払い方法(電子マネー、QRコード決済など)を追加する際に、PaymentProcessorクラスを変更する必要がないことです。

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

Javaではクラスの継承は1つに制限されていますが、インターフェースは複数実装できます。これにより、クラスに複数の機能を持たせることが可能です。

複数インターフェースの実装方法

1
2
3
public interface Printable {
    void print();
}
1
2
3
public interface Savable {
    void save(String path);
}
1
2
3
public interface Shareable {
    void share(String recipient);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 複数のインターフェースを実装
public class Report implements Printable, Savable, Shareable {
    private String title;
    private String content;
    
    public Report(String title, String content) {
        this.title = title;
        this.content = content;
    }
    
    @Override
    public void print() {
        System.out.println("=== " + title + " ===");
        System.out.println(content);
    }
    
    @Override
    public void save(String path) {
        System.out.println("レポートを保存: " + path + "/" + title + ".txt");
    }
    
    @Override
    public void share(String recipient) {
        System.out.println("レポートを " + recipient + " に共有しました");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Main {
    public static void main(String[] args) {
        Report report = new Report("月次報告", "売上は前月比120%でした。");
        
        // すべてのインターフェースのメソッドが利用可能
        report.print();
        report.save("/documents");
        report.share("manager@example.com");
        
        // 特定のインターフェース型としても扱える
        Printable printable = report;
        printable.print();
        
        Savable savable = report;
        savable.save("/backup");
    }
}

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

クラスの継承とインターフェースの実装を組み合わせることもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public abstract class Document {
    protected String title;
    
    public Document(String title) {
        this.title = title;
    }
    
    public String getTitle() {
        return title;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 継承とインターフェース実装を組み合わせ
public class PdfDocument extends Document implements Printable, Savable {
    private byte[] data;
    
    public PdfDocument(String title, byte[] data) {
        super(title);
        this.data = data;
    }
    
    @Override
    public void print() {
        System.out.println("PDFを印刷: " + title);
    }
    
    @Override
    public void save(String path) {
        System.out.println("PDFを保存: " + path + "/" + title + ".pdf");
    }
}

インターフェースの継承

インターフェース自体も他のインターフェースを継承(拡張)できます。この場合はextendsキーワードを使用します。

1
2
3
public interface Readable {
    String read();
}
1
2
3
public interface Writable {
    void write(String content);
}
1
2
3
4
5
6
// 複数のインターフェースを継承
public interface ReadWritable extends Readable, Writable {
    // Readable と Writable の両方のメソッドを継承
    // 追加のメソッドを定義することも可能
    void clear();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class TextFile implements ReadWritable {
    private String content = "";
    
    @Override
    public String read() {
        return content;
    }
    
    @Override
    public void write(String content) {
        this.content = content;
    }
    
    @Override
    public void clear() {
        this.content = "";
    }
}

デフォルトメソッドの活用

Java 8から、インターフェースにデフォルトメソッドを定義できるようになりました。デフォルトメソッドは実装を持つメソッドで、実装クラスはこれをオーバーライドするかそのまま使用するかを選択できます。

デフォルトメソッドの基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public interface Logger {
    // 抽象メソッド(実装必須)
    void log(String message);
    
    // デフォルトメソッド(実装済み)
    default void logInfo(String message) {
        log("[INFO] " + message);
    }
    
    default void logWarning(String message) {
        log("[WARNING] " + message);
    }
    
    default void logError(String message) {
        log("[ERROR] " + message);
    }
}
1
2
3
4
5
6
7
public class ConsoleLogger implements Logger {
    // logメソッドのみ実装すればよい
    @Override
    public void log(String message) {
        System.out.println(message);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Main {
    public static void main(String[] args) {
        Logger logger = new ConsoleLogger();
        
        logger.log("通常のログ");
        logger.logInfo("情報メッセージ");    // デフォルトメソッドを使用
        logger.logWarning("警告メッセージ");
        logger.logError("エラーメッセージ");
    }
}

出力結果:

通常のログ
[INFO] 情報メッセージ
[WARNING] 警告メッセージ
[ERROR] エラーメッセージ

デフォルトメソッドの導入背景

デフォルトメソッドが導入された主な理由は、既存のインターフェースに後方互換性を保ちながら新機能を追加するためです。

Java 8でCollectionインターフェースにstream()メソッドが追加された際、デフォルトメソッドとして実装されました。これにより、既存のコレクション実装クラスを変更することなく、Stream APIを利用可能になりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Java 8で追加されたCollectionのデフォルトメソッドの例
public interface Collection<E> extends Iterable<E> {
    // 既存の抽象メソッド
    boolean add(E e);
    // ...
    
    // Java 8で追加されたデフォルトメソッド
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

デフォルトメソッドのオーバーライド

デフォルトメソッドは必要に応じてオーバーライドできます。

1
2
3
4
5
public interface Greeting {
    default String greet(String name) {
        return "Hello, " + name + "!";
    }
}
1
2
3
4
5
6
public class JapaneseGreeting implements Greeting {
    @Override
    public String greet(String name) {
        return "こんにちは、" + name + "さん!";
    }
}
1
2
3
4
5
6
public class FormalGreeting implements Greeting {
    @Override
    public String greet(String name) {
        return "Dear " + name + ", greetings.";
    }
}

菱形継承問題(Diamond Problem)への対処

複数のインターフェースが同じシグネチャのデフォルトメソッドを持つ場合、実装クラスで明示的にどちらを使用するか指定する必要があります。

1
2
3
4
5
public interface InterfaceA {
    default void display() {
        System.out.println("InterfaceAのdisplay");
    }
}
1
2
3
4
5
public interface InterfaceB {
    default void display() {
        System.out.println("InterfaceBのdisplay");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class MultiImplement implements InterfaceA, InterfaceB {
    // 競合するデフォルトメソッドは明示的にオーバーライドが必要
    @Override
    public void display() {
        // 特定のインターフェースのデフォルト実装を呼び出す場合
        InterfaceA.super.display();
        
        // または独自の実装を提供
        System.out.println("MultiImplementのdisplay");
    }
}
1
2
3
4
5
6
7
8
9
public class Main {
    public static void main(String[] args) {
        MultiImplement obj = new MultiImplement();
        obj.display();
        // 出力:
        // InterfaceAのdisplay
        // MultiImplementのdisplay
    }
}

staticメソッドとprivateメソッド

staticメソッド(Java 8以降)

インターフェースにstaticメソッドを定義できます。staticメソッドはインターフェース名を通じて直接呼び出します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public interface MathUtils {
    // staticメソッド
    static int add(int a, int b) {
        return a + b;
    }
    
    static int multiply(int a, int b) {
        return a * b;
    }
    
    static boolean isEven(int number) {
        return number % 2 == 0;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Main {
    public static void main(String[] args) {
        // インターフェース名.メソッド名で呼び出す
        int sum = MathUtils.add(5, 3);
        int product = MathUtils.multiply(4, 7);
        
        System.out.println("合計: " + sum);       // 合計: 8
        System.out.println("積: " + product);     // 積: 28
        System.out.println(MathUtils.isEven(10)); // true
    }
}

staticメソッドは実装クラスに継承されません。必ずインターフェース名を通じて呼び出す必要があります。

privateメソッド(Java 9以降)

Java 9からは、インターフェースにprivateメソッドを定義できるようになりました。privateメソッドは、デフォルトメソッドやstaticメソッド内で共通処理を抽出するために使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface DataValidator {
    // 抽象メソッド
    boolean validate(String data);
    
    // デフォルトメソッド
    default boolean validateEmail(String email) {
        if (!checkNotEmpty(email)) {
            return false;
        }
        return email.contains("@") && email.contains(".");
    }
    
    default boolean validatePhoneNumber(String phone) {
        if (!checkNotEmpty(phone)) {
            return false;
        }
        return phone.matches("\\d{10,11}");
    }
    
    // privateメソッド(Java 9以降)
    private boolean checkNotEmpty(String value) {
        return value != null && !value.trim().isEmpty();
    }
    
    // private staticメソッド
    private static void logValidation(String type, boolean result) {
        System.out.println(type + " validation: " + (result ? "OK" : "NG"));
    }
    
    // staticメソッドからprivate staticメソッドを呼び出す
    static boolean quickValidate(String data) {
        boolean result = data != null && data.length() > 0;
        logValidation("Quick", result);
        return result;
    }
}

メソッドの種類まとめ

メソッドの種類 Java バージョン 修飾子 実装 用途
抽象メソッド 1.0〜 (public abstract) なし 契約の定義
デフォルトメソッド 8〜 default あり 共通実装の提供
staticメソッド 8〜 static あり ユーティリティ機能
privateメソッド 9〜 private あり 内部処理の共通化
private staticメソッド 9〜 private static あり static内部処理の共通化

関数型インターフェース(@FunctionalInterface)

関数型インターフェースとは

関数型インターフェースとは、抽象メソッドを1つだけ持つインターフェースです。Java 8で導入されたラムダ式やメソッド参照と組み合わせて使用します。

1
2
3
4
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
}

@FunctionalInterfaceアノテーションを付けることで、コンパイラが関数型インターフェースの条件を満たしているかチェックしてくれます。

ラムダ式との組み合わせ

関数型インターフェースは、ラムダ式を使用してインスタンス化できます。

1
2
3
4
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {
    public static void main(String[] args) {
        // ラムダ式で実装を提供
        Calculator add = (a, b) -> a + b;
        Calculator subtract = (a, b) -> a - b;
        Calculator multiply = (a, b) -> a * b;
        Calculator divide = (a, b) -> a / b;
        
        System.out.println("5 + 3 = " + add.calculate(5, 3));       // 8
        System.out.println("5 - 3 = " + subtract.calculate(5, 3)); // 2
        System.out.println("5 * 3 = " + multiply.calculate(5, 3)); // 15
        System.out.println("6 / 3 = " + divide.calculate(6, 3));   // 2
    }
}

標準関数型インターフェース

Java標準ライブラリには、よく使われる関数型インターフェースが用意されています。

インターフェース 抽象メソッド 用途
Function<T, R> R apply(T t) 値を変換する
Consumer<T> void accept(T t) 値を消費する
Supplier<T> T get() 値を生成する
Predicate<T> boolean test(T t) 条件を判定する
BiFunction<T, U, R> R apply(T t, U u) 2つの値を変換する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.function.*;

public class Main {
    public static void main(String[] args) {
        // Function: 値を変換
        Function<String, Integer> strLength = str -> str.length();
        System.out.println(strLength.apply("Hello")); // 5
        
        // Consumer: 値を消費(処理するだけで戻り値なし)
        Consumer<String> printer = msg -> System.out.println(msg);
        printer.accept("こんにちは"); // こんにちは
        
        // Supplier: 値を生成
        Supplier<Double> randomValue = () -> Math.random();
        System.out.println(randomValue.get()); // 0.123... (ランダムな値)
        
        // Predicate: 条件判定
        Predicate<Integer> isPositive = n -> n > 0;
        System.out.println(isPositive.test(5));  // true
        System.out.println(isPositive.test(-3)); // false
        
        // BiFunction: 2つの引数を受け取って変換
        BiFunction<Integer, Integer, Integer> max = (a, b) -> a > b ? a : b;
        System.out.println(max.apply(10, 20)); // 20
    }
}

メソッド参照

既存のメソッドを関数型インターフェースのインスタンスとして参照できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // ラムダ式
        names.forEach(name -> System.out.println(name));
        
        // メソッド参照(同等の処理)
        names.forEach(System.out::println);
        
        // staticメソッド参照
        List<String> numbers = Arrays.asList("1", "2", "3");
        numbers.stream()
               .map(Integer::parseInt)  // String -> Integer
               .forEach(System.out::println);
    }
}

関数型インターフェースの実践例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // 条件をPredicate として定義
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Predicate<Integer> isGreaterThan5 = n -> n > 5;
        
        // 偶数をフィルタリング
        List<Integer> evenNumbers = numbers.stream()
                                           .filter(isEven)
                                           .collect(Collectors.toList());
        System.out.println("偶数: " + evenNumbers); // [2, 4, 6, 8, 10]
        
        // Predicateを合成(AND条件)
        List<Integer> evenAndGreaterThan5 = numbers.stream()
                                                   .filter(isEven.and(isGreaterThan5))
                                                   .collect(Collectors.toList());
        System.out.println("偶数かつ5より大きい: " + evenAndGreaterThan5); // [6, 8, 10]
        
        // Predicateを合成(OR条件)
        Predicate<Integer> isOdd = isEven.negate();
        List<Integer> oddNumbers = numbers.stream()
                                          .filter(isOdd)
                                          .collect(Collectors.toList());
        System.out.println("奇数: " + oddNumbers); // [1, 3, 5, 7, 9]
    }
}

インターフェース vs 抽象クラス

インターフェースと抽象クラスは似た用途で使われることがありますが、明確な違いがあります。適切に使い分けることが重要です。

比較表

観点 インターフェース 抽象クラス
多重継承 複数実装可能 単一継承のみ
フィールド 定数(public static final)のみ インスタンス変数を持てる
コンストラクタ 持てない 持てる
アクセス修飾子 メソッドは暗黙的にpublic 任意の修飾子を使用可能
関係性 can-do(〜ができる) is-a(〜の一種である)
用途 機能の契約を定義 共通の基盤と一部の実装を提供

使い分けの指針

flowchart TD
    A[新しい型を設計] --> B{複数のクラスに共通の状態が必要?}
    B -->|はい| C{is-a関係が成り立つ?}
    B -->|いいえ| D[インターフェースを使用]
    C -->|はい| E[抽象クラスを使用]
    C -->|いいえ| D
    E --> F{追加の機能が必要?}
    F -->|はい| G[インターフェースも併用]
    F -->|いいえ| H[抽象クラスのみ]

使い分けの例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 抽象クラス: 動物の共通の状態と振る舞いを持つ
public abstract class Animal {
    protected String name;
    protected int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 共通の実装
    public void sleep() {
        System.out.println(name + "が眠っています");
    }
    
    // 子クラスで実装を強制
    public abstract void makeSound();
}
1
2
3
4
5
6
7
8
// インターフェース: 能力(できること)を定義
public interface Swimmable {
    void swim();
}

public interface Flyable {
    void fly();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 犬: 動物であり、泳げる
public class Dog extends Animal implements Swimmable {
    public Dog(String name, int age) {
        super(name, age);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + "がワンワン吠えます");
    }
    
    @Override
    public void swim() {
        System.out.println(name + "が犬かきで泳いでいます");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 鳥: 動物であり、飛べる
public class Bird extends Animal implements Flyable {
    public Bird(String name, int age) {
        super(name, age);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + "がチュンチュン鳴きます");
    }
    
    @Override
    public void fly() {
        System.out.println(name + "が空を飛んでいます");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// カモ: 動物であり、泳げて、飛べる
public class Duck extends Animal implements Swimmable, Flyable {
    public Duck(String name, int age) {
        super(name, age);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + "がガーガー鳴きます");
    }
    
    @Override
    public void swim() {
        System.out.println(name + "が水面を泳いでいます");
    }
    
    @Override
    public void fly() {
        System.out.println(name + "が飛び立ちます");
    }
}

どちらを選ぶべきか

状況 選択
複数のクラスに共通の状態(フィールド)がある 抽象クラス
複数の無関係なクラスで同じ機能を実装したい インターフェース
既存のクラス階層に機能を追加したい インターフェース
APIの安定性を重視し、後から機能追加の可能性がある インターフェース(デフォルトメソッド活用)
非publicなメンバーが必要 抽象クラス

まとめ

この記事では、Javaのインターフェースについて包括的に解説しました。

概念 ポイント
インターフェース クラスが実装すべき契約(メソッドのシグネチャ)を定義する
implements クラスがインターフェースを実装するためのキーワード
複数実装 1つのクラスが複数のインターフェースを実装できる
デフォルトメソッド Java 8以降、実装を持つメソッドを定義可能(後方互換性の確保)
staticメソッド ユーティリティメソッドをインターフェースに定義可能
privateメソッド Java 9以降、内部処理の共通化に使用
関数型インターフェース 抽象メソッドが1つのインターフェース(ラムダ式と併用)

インターフェースを適切に活用することで、以下のメリットを得られます。

  • 実装の詳細に依存しない疎結合な設計
  • テストしやすいコード(モックの作成が容易)
  • 拡張性の高いアーキテクチャ

インターフェースと抽象クラスは競合するものではなく、組み合わせて使用することで、より柔軟で保守性の高い設計を実現できます。

次のステップとして、パッケージとアクセス制御について学ぶことで、大規模なプロジェクトでもコードを適切に整理・保護できるようになります。

参考リンク