インターフェース

メソッドの契約を定義する仕組み

インターフェースの基本概念

インターフェース(interface)は、クラスが実装すべきメソッドの仕様を定義するもので、クラス間の「契約」の役割を果たします。インターフェースはメソッドの宣言(シグネチャ)のみを含み、実装は含みません。これにより、「何をするか」は定義しても「どのようにするか」は実装クラスに委ねられます。

クラス図

図1: インターフェースのクラス図

基本的なインターフェースの定義と実装

// インターフェースの定義
public interface Drawable {
    void draw();  // 抽象メソッド(abstractキーワードは省略可能)
    
    double getArea();  // 別の抽象メソッド
}

// インターフェースの実装
public class Circle implements Drawable {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    // インターフェースのメソッドを実装
    @Override
    public void draw() {
        System.out.println("円を描画します");
    }
    
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
    
    // 独自のメソッド
    public double getRadius() {
        return radius;
    }
}

// 別のクラスでも同じインターフェースを実装
public class Rectangle implements Drawable {
    private double width;
    private double height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public void draw() {
        System.out.println("四角形を描画します");
    }
    
    @Override
    public double getArea() {
        return width * height;
    }
}

インターフェースの特徴

  • インターフェースのすべてのメソッドは暗黙的に public かつ abstract です(これらのキーワードは省略可能)
  • インターフェースのフィールドは暗黙的に publicstaticfinal(定数)です
  • クラスは複数のインターフェースを実装できます(多重継承に似た効果)
  • インターフェースは他のインターフェースを継承(extends)できます

シーケンス図

図2: インターフェースの動作シーケンス

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

Java 8から、インターフェースに「デフォルトメソッド」を定義できるようになりました。これにより、既存のインターフェースに新機能を追加しても、そのインターフェースを実装している既存のクラスを変更する必要がなくなります。

デフォルトメソッドの例

public interface Vehicle {
    void accelerate();
    void brake();
    
    // デフォルトメソッド
    default void honk() {
        System.out.println("ビープ!");
    }
}

// 実装クラス
public class Car implements Vehicle {
    @Override
    public void accelerate() {
        System.out.println("車が加速します");
    }
    
    @Override
    public void brake() {
        System.out.println("車が減速します");
    }
    
    // honk()メソッドはオーバーライドしなくても使える
}

// 使用例
public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.accelerate();
        car.brake();
        car.honk();  // デフォルト実装が呼び出される
    }
}

静的メソッド (Java 8以降)

Java 8以降、インターフェースに静的メソッドを定義することもできます。これらのメソッドはインターフェース自体に関連付けられ、実装クラスにはインポートされません。

静的メソッドの例

public interface MathOperations {
    // 静的メソッド
    static int add(int a, int b) {
        return a + b;
    }
    
    static int subtract(int a, int b) {
        return a - b;
    }
    
    // 抽象メソッド
    int multiply(int a, int b);
    int divide(int a, int b);
}

// 使用例
public class Calculator implements MathOperations {
    @Override
    public int multiply(int a, int b) {
        return a * b;
    }
    
    @Override
    public int divide(int a, int b) {
        if (b == 0) throw new ArithmeticException("ゼロ除算");
        return a / b;
    }
}

// 静的メソッドの呼び出し
public class Main {
    public static void main(String[] args) {
        // 静的メソッドはインターフェース名で直接呼び出す
        System.out.println("5 + 3 = " + MathOperations.add(5, 3));
        System.out.println("5 - 3 = " + MathOperations.subtract(5, 3));
        
        // インスタンスメソッドは実装クラスのインスタンスで呼び出す
        Calculator calc = new Calculator();
        System.out.println("5 * 3 = " + calc.multiply(5, 3));
        System.out.println("6 / 3 = " + calc.divide(6, 3));
    }
}

privateメソッド (Java 9以降)

Java 9以降では、インターフェース内にprivateメソッドを定義できるようになりました。これにより、インターフェース内のコード重複を減らし、デフォルトメソッドや静的メソッド間での共通処理をカプセル化できます。

privateメソッドの例

public interface Logger {
    void log(String message);
    
    default void logInfo(String message) {
        log(addTimestamp("INFO: " + message));
    }
    
    default void logError(String message) {
        log(addTimestamp("ERROR: " + message));
    }
    
    // privateヘルパーメソッド
    private String addTimestamp(String message) {
        return java.time.LocalDateTime.now() + " " + message;
    }
    
    // privateな静的ヘルパーメソッドも定義可能
    private static String formatMessage(String message) {
        return "[LOG] " + message;
    }
}

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

Javaのクラスは複数のインターフェースを同時に実装できるため、柔軟な設計が可能になります。これは、Javaが直接的なクラスの多重継承を許可していない中での代替手段としても機能します。

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

public interface Flyable {
    void fly();
    double getMaxAltitude();
}

public interface Swimmable {
    void swim();
    double getMaxDepth();
}

// 複数インターフェースの実装
public class Duck implements Flyable, Swimmable {
    @Override
    public void fly() {
        System.out.println("アヒルが飛んでいます");
    }
    
    @Override
    public double getMaxAltitude() {
        return 1000.0;  // メートル
    }
    
    @Override
    public void swim() {
        System.out.println("アヒルが泳いでいます");
    }
    
    @Override
    public double getMaxDepth() {
        return 2.0;  // メートル
    }
}

インターフェース階層

インターフェースは他のインターフェースを継承(extends)することができ、より特化したインターフェースを定義できます。

インターフェース継承の例

public interface Animal {
    void eat();
    void sleep();
}

// Animalを継承したインターフェース
public interface Pet extends Animal {
    void play();
    String getName();
}

// さらに特化したインターフェース
public interface ServiceAnimal extends Animal {
    void work();
    void train();
}

// 実装クラス
public class Dog implements Pet, ServiceAnimal {
    private String name;
    
    public Dog(String name) {
        this.name = name;
    }
    
    // Animalのメソッド
    @Override
    public void eat() {
        System.out.println(name + "がご飯を食べています");
    }
    
    @Override
    public void sleep() {
        System.out.println(name + "が眠っています");
    }
    
    // Petのメソッド
    @Override
    public void play() {
        System.out.println(name + "が遊んでいます");
    }
    
    @Override
    public String getName() {
        return name;
    }
    
    // ServiceAnimalのメソッド
    @Override
    public void work() {
        System.out.println(name + "が仕事をしています");
    }
    
    @Override
    public void train() {
        System.out.println(name + "がトレーニング中です");
    }
}

関数型インターフェース (Java 8以降)

Java 8で導入された関数型インターフェースは、ラムダ式とともに使用される重要な機能です。関数型インターフェースは、@FunctionalInterfaceアノテーションでマークされ、抽象メソッドを1つだけ持つインターフェースです。

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

@FunctionalInterface
public interface Converter {
    T convert(F from);
    
    // デフォルトメソッドはいくつあっても関数型インターフェースの条件を満たす
    default void printInfo() {
        System.out.println("コンバーター");
    }
}

// 使用例
public class Main {
    public static void main(String[] args) {
        // ラムダ式を使用
        Converter stringToInt = (String s) -> Integer.parseInt(s);
        
        // メソッド参照も使用可能
        Converter stringToInt2 = Integer::parseInt;
        
        System.out.println(stringToInt.convert("123"));  // 出力: 123
        System.out.println(stringToInt2.convert("456"));  // 出力: 456
    }
}

インターフェースの実践的な使い方

1. 依存性の分離

インターフェースを使用することで、コードの依存関係を具体的な実装から分離し、テストやメンテナンスが容易になります。

依存性分離の例

// データアクセスのインターフェース
public interface UserRepository {
    User findById(long id);
    List findAll();
    void save(User user);
    void delete(long id);
}

// サービス層(実装に依存せず、インターフェースに依存)
public class UserService {
    private final UserRepository userRepository;
    
    // コンストラクタインジェクション
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User getUser(long id) {
        return userRepository.findById(id);
    }
    
    public List getAllUsers() {
        return userRepository.findAll();
    }
    
    // 他のビジネスロジック...
}

// 実装クラス(本番用)
public class DatabaseUserRepository implements UserRepository {
    // 実際のデータベースアクセス実装
    // ...
}

// 実装クラス(テスト用)
public class InMemoryUserRepository implements UserRepository {
    private Map users = new HashMap<>();
    
    // テスト用のシンプルな実装
    // ...
}

2. 実行時の実装切り替え

インターフェースを使うことで、実行時に異なる実装を柔軟に切り替えることができます。

実行時切り替えの例

public interface NotificationService {
    void sendNotification(String message, String recipient);
}

public class EmailNotification implements NotificationService {
    @Override
    public void sendNotification(String message, String recipient) {
        System.out.println("Eメール送信: " + message + " to " + recipient);
        // 実際のEメール送信ロジック
    }
}

public class SMSNotification implements NotificationService {
    @Override
    public void sendNotification(String message, String recipient) {
        System.out.println("SMS送信: " + message + " to " + recipient);
        // 実際のSMS送信ロジック
    }
}

public class NotificationFactory {
    public static NotificationService createNotificationService(String type) {
        if ("email".equals(type)) {
            return new EmailNotification();
        } else if ("sms".equals(type)) {
            return new SMSNotification();
        } else {
            throw new IllegalArgumentException("Unknown notification type: " + type);
        }
    }
}

// 使用例
public class Main {
    public static void main(String[] args) {
        // 設定やユーザー設定に基づいて実装を選択
        String preferredMethod = getUserPreference();  // "email" または "sms"
        
        NotificationService service = NotificationFactory.createNotificationService(preferredMethod);
        service.sendNotification("重要なお知らせです", "user@example.com");
    }
    
    private static String getUserPreference() {
        // 実際にはデータベースやプロパティファイルから取得
        return "email";
    }
}

インターフェースを使う際のベストプラクティス

  • ISP(インターフェース分離の原則)に従う: 大きなインターフェースよりも、小さく集中したインターフェースを複数作る
  • クリーンなAPIを設計する: メソッド名は明確で一貫性があり、引数と戻り値の型は直感的であるべき
  • 実装ではなくインターフェースに依存する: 具体的なクラスではなく、インターフェースを参照として使用する
  • インターフェースの安定性を確保する: 一度公開したインターフェースを変更すると、それに依存するすべてのコードに影響する
  • 関連するインターフェースをまとめる: 関連する機能のインターフェースは同じパッケージに入れる

まとめ

インターフェースは、Javaのオブジェクト指向設計において中心的な役割を果たします。適切に設計されたインターフェースにより、コードは疎結合で柔軟性が高く、拡張性に優れたものになります。特に、大規模なシステムや複数人での開発では、明確なインターフェース定義が重要です。Java 8以降、デフォルトメソッドや静的メソッドの導入により、インターフェースはさらに強力になりました。