Interfaceは、クラスが実装すべき抽象メソッドのセットを定義するコントラクトです。JavaとKotlinにおいて、インターフェースは抽象化とポリモーフィズムの主要なメカニズムです。Java 8+では、インターフェースにdefaultメソッドとstaticメソッドを含めることができ、Kotlinではデフォルト実装を含めることができます。Google Android Developers(2025)によると、Androidプロジェクトの90%でインターフェースを使用してアーキテクチャ層(リポジトリ、UseCase、サービス)を定義しています。
重要なポイント
Interfaceは、抽象メソッド、定数、デフォルトメソッドを含む参照型です。インターフェースを実装するクラスは、そのすべての抽象メソッドの実装を提供する必要があります。Javaでは、インターフェースは状態(インスタンスフィールド)を持つことができません。Kotlinもこの制限に従いますが、アクセサ付きのプロパティをサポートしています。
public interface Repository {
T findById(Long id);
List findAll();
T save(T entity);
void deleteById(Long id);
default long count() {
return findAll().size();
}
}
@Entity
public class UserEntity {
private Long id;
private String email;
// ゲッターとセッター
}
public class UserRepositoryImpl implements Repository {
private final EntityManager em;
public UserEntity findById(Long id) {
return em.find(UserEntity.class, id);
}
public List findAll() {
return em.createQuery("FROM UserEntity", UserEntity.class)
.getResultList();
}
// その他のメソッド
}Repository<T>はCRUDメソッドを持つジェネリックインターフェースです。デフォルトメソッドcount()は、オーバーライド可能なデフォルト実装を提供します。UserRepositoryImplは、データアクセスにEntityManagerを使用してインターフェースを実装します。このアプローチにより、実際のデータベースなしでインターフェースのモックを通じてデータ層をテストできます。
インターフェースと抽象クラスの選択は、共有状態の有無と型間の関係に依存します。インターフェースはコントラクト(クラスができること)を定義し、抽象クラスは共通の実装(クラスが何であるか)を定義します。
| 基準 | Interface | Abstract Class |
|---|---|---|
| 状態(フィールド) | static final定数のみ | はい、任意のフィールド |
| コンストラクタ | なし | あり |
| 多重継承 | 可(implements) | 不可(extendsは一つ) |
| アクセス修飾子 | public(Java 8-)、defaultメソッド | すべて(private、protected、public) |
| 使用するタイミング | 無関係なクラス用のコントラクト | 関連クラス用の共通基底 |
Clean Architectureでは、インターフェースは内側の層(domain)に配置され、実装は外側の層(data)に配置されます。これにより、依存関係ルール(外側の層は内側の層に依存するが、その逆は不可)を維持できます。抽象クラスは、テンプレートメソッド(Template Methodパターン)によく使用されます。
KotlinのインターフェースはJavaよりも柔軟です。抽象プロパティを宣言し、メソッド実装を提供できます。Javaとは異なり、Kotlinはbyキーワードによるデリゲーションをサポートしており、Delegateパターンを実装する際のボイラープレートを大幅に削減します。
interface ApiService {
val baseUrl: String // 抽象プロパティ
suspend fun fetchData(): Result>
fun getEndpoint(path: String): String {
return "$baseUrl/$path" // デフォルト実装
}
}
class RetrofitApiService(
override val baseUrl: String
) : ApiService {
private val client = Retrofit.Builder()
.baseUrl(baseUrl)
.build()
override suspend fun fetchData(): Result>
{
// リクエストの実装
}
}
// byによるデリゲーション
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) = println(message)
}
class UserService(logger: Logger) : Logger by loggerUserServiceは、デリゲーション(by)を介してLoggerインターフェースを実装します。すべてのlog()呼び出しは、ラッパーメソッドを記述せずにloggerオブジェクトに転送されます。これは、Javaでは5〜10行のボイラープレートコードを要するコンポジションの例です。
Clean Architecture for Androidは、アプリケーションを明確に層に分割します。インターフェースは層間の境界として機能します。DomainはリポジトリとUseCaseのインターフェースを定義し、Dataは実装を提供します。これにより、ビジネスロジックを変更せずに実装を置き換えることができ、RoomからFirebase、またはRESTからGraphQLへの移行時の重要な利点となります。
// Domain layer — インターフェース(コントラクト)
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun updateUser(user: User)
}
// Domain layer — use case(インターフェースに依存)
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: String): Result {
return runCatching { repository.getUser(id) }
}
}
// Data layer — 実装
class UserRepositoryImpl(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override suspend fun getUser(id: String): User {
val cached = dao.getUser(id)
if (cached != null) return cached
val remote = api.fetchUser(id)
dao.insertUser(remote)
return remote
}
// updateUser...
}UserRepositoryはdomain層のインターフェースです。GetUserUseCaseは実装ではなくインターフェースに依存します。data層のUserRepositoryImplは、APIとローカルデータベースを組み合わせてインターフェースを実装します。このアーキテクチャにより、データベースやネットワークを設定せずにモックリポジトリでUseCasesをテストできます。
defaultメソッドは、後方互換性を壊さずにインターフェースを進化的に拡張するためにJava 8で追加されました。ArrayListがCollectionに追加された新しいstream()メソッドを実装していなくても、古いコードは引き続き機能します。インターフェースのstaticメソッドは、インターフェースに関連するユーティリティとして機能し、ユーティリティクラスの代替となります。
public interface Vehicle {
void start();
void stop();
default void honk() {
System.out.println("Beep!");
}
static Vehicle of(String type) {
if ("car".equals(type)) return new Car();
return new Bicycle();
}
// 定数
String CATEGORY = "transport";
}defaultメソッドはdiamond problemを解決します。クラスが同じdefaultメソッドを持つ2つのインターフェースを実装する場合、コンパイラは明示的なオーバーライドを要求します。staticメソッドは、インターフェース名を介して呼び出されます — Vehicle.of("car")のように、インスタンスなしで呼び出せます。
インターフェース設計の誤りは、脆弱なコード、テストの複雑さ、SOLID違反につながります。3つの一般的な問題を見てみましょう。
15+のメソッドを含むインターフェースは、Interface Segregation Principle(ISP)に違反します。例として、10+のメソッドを持つ古いjava.util.Dictionaryがあります。解決策:複数の小さなインターフェース(ReadableRepository、WritableRepository、SearchableRepository)に分割します。クライアント(サービス)は必要なメソッドのみに依存します。
ポリモーフィズムの真の必要性なしにすべてのクラスにインターフェースを作成することは、Interface overkillアンチパターンです。兆候:インターフェースに実装が1つだけで、プロジェクトに代替を追加する計画がない。解決策:2番目の実装オプションが出現したとき、またはテスト用のモックが必要になったときにのみインターフェースを追加します。
よくある質問
Interfaceはコントラクト(メソッドシグネチャ)のみを定義し、状態を持てず、多重継承をサポートします。Abstract classはフィールド、コンストラクタ、実装済みメソッドを含められますが、クラスが継承できる抽象クラスは1つだけです。Java 8以降、インターフェースはdefaultメソッドとstaticメソッドを獲得し、ギャップを縮めました。
はい、JavaとKotlinではインターフェースは継承をサポートします。public interface AdvancedRepository<T> extends Repository<T>, Pageableは、他の2つを組み合わせたインターフェースです。AdvancedRepositoryを実装するクラスは、両方の親インターフェースのすべてのメソッドを実装する必要があります。多重継承はインターフェースのみに許可されています。
関数型インターフェースは、単一の抽象メソッド(SAM — Single Abstract Method)を持つインターフェースです。@FunctionalInterfaceアノテーションがこの制限を保証します。例:Runnable、Callable、Comparator、Consumer。関数型インターフェースはJava 8ラムダ式の基盤です:() -> System.out.println()はRunnableを実装します。
defaultメソッドにより、すべての実装クラスを変更せずにインターフェースに新しいメソッドを追加できます。例えば、Java 8はstream()をCollectionにdefaultメソッドとして追加しました。このメカニズムなしでは、foreach()、stream()、その他のメソッドのためにJDKの数千のクラスを変更する必要がありました。Defaultは後方互換性のある拡張方法です。
Kotlinはクラスの多重継承を禁止していますが、インターフェースの多重実装は許可しています。2つのインターフェースが同じシグネチャとデフォルト実装を持つメソッドを持つ場合、コンパイラはsuper<InterfaceName>.method()呼び出しによる明示的なオーバーライドを要求します。これにより、コンパイルレベルでdiamond problemが解決されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。