Interface est un contrat définissant un ensemble de méthodes abstraites qu'une classe doit implémenter. En Java et Kotlin, les interfaces sont le mécanisme principal d'abstraction et de polymorphisme. En Java 8+, les interfaces peuvent contenir des méthodes default et static, en Kotlin — des implémentations par défaut. Selon Google Android Developers (2025), les interfaces sont utilisées dans 90% des projets Android pour définir les couches d'architecture — référentiels, UseCases et services.
Points clés
Interface est un type de référence qui contient des méthodes abstraites, des constantes et des méthodes par défaut. Une classe implémentant une interface doit fournir une implémentation pour toutes ses méthodes abstraites. En Java, une interface ne peut pas avoir d'état (champs d'instance) ; Kotlin suit également cette restriction mais prend en charge les propriétés avec accesseurs.
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;
// getters et setters
}
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();
}
// autres méthodes
}Repository<T> est une interface générique avec des méthodes CRUD. La méthode default count() fournit une implémentation par défaut qui peut être redéfinie. UserRepositoryImpl implémente l'interface en utilisant EntityManager pour l'accès aux données. Cette approche permet de tester la couche de données via un mock de l'interface sans base de données réelle.
Le choix entre une interface et une classe abstraite dépend de la présence d'un état partagé et des relations entre types. Une interface définit un contrat (ce qu'une classe peut faire), une classe abstraite définit une implémentation commune (ce qu'une classe est).
| Critère | Interface | Abstract Class |
|---|---|---|
| État (champs) | Uniquement des constantes static final | Oui, tous champs |
| Constructeurs | Non | Oui |
| Héritage multiple | Oui (implements) | Non (extends un) |
| Modificateurs d'accès | public (Java 8-), méthodes default | Tous (private, protected, public) |
| Quand utiliser | Contrat pour classes non liées | Base commune pour classes liées |
Dans Clean Architecture, les interfaces sont placées dans la couche interne (domain) et les implémentations dans la couche externe (data). Cela permet de maintenir la Dependency Rule : la couche externe dépend de la couche interne, mais pas l'inverse. Les classes abstraites sont plus souvent utilisées pour les méthodes template (Template Method pattern).
Les interfaces Kotlin sont plus flexibles que Java : elles peuvent déclarer des propriétés abstraites et fournir des implémentations de méthodes. Contrairement à Java, Kotlin prend en charge la délégation via le mot-clé by, ce qui réduit considérablement le boilerplate lors de l'implémentation du pattern Delegate.
interface ApiService {
val baseUrl: String // propriété abstraite
suspend fun fetchData(): Result>
fun getEndpoint(path: String): String {
return "$baseUrl/$path" // implémentation par défaut
}
}
class RetrofitApiService(
override val baseUrl: String
) : ApiService {
private val client = Retrofit.Builder()
.baseUrl(baseUrl)
.build()
override suspend fun fetchData(): Result>
{
// implémentation de la requête
}
}
// Délégation via by
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) = println(message)
}
class UserService(logger: Logger) : Logger by loggerUserService implémente l'interface Logger par délégation (by). Tous les appels log() sont redirigés vers l'objet logger sans écrire de méthodes wrapper. C'est un exemple de composition qui nécessiterait 5 à 10 lignes de code boilerplate en Java.
Clean Architecture pour Android sépare clairement l'application en couches. Les interfaces agissent comme des frontières entre les couches : Domain définit les interfaces des référentiels et UseCases, Data fournit les implémentations. Cela permet de remplacer les implémentations sans modifier la logique métier — un avantage clé lors de la migration de Room vers Firebase ou de REST vers GraphQL.
// Domain layer — interface (contrat)
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun updateUser(user: User)
}
// Domain layer — use case (dépend de l'interface)
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: String): Result {
return runCatching { repository.getUser(id) }
}
}
// Data layer — implémentation
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 est une interface dans la couche domain. GetUserUseCase dépend de l'interface, pas de l'implémentation. UserRepositoryImpl dans la couche data implémente l'interface, combinant API et base de données locale. Cette architecture permet de tester les UseCases avec un référentiel mock sans configurer de base de données ou de réseau.
Les méthodes default ont été ajoutées en Java 8 pour l'extension évolutive des interfaces sans casser la rétrocompatibilité. Si ArrayList n'implémentait pas la nouvelle méthode stream() ajoutée à Collection, l'ancien code continuerait de fonctionner. Les méthodes static dans les interfaces servent d'utilitaires liés à l'interface — une alternative aux classes utilitaires.
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();
}
// constante
String CATEGORY = "transport";
}Les méthodes default résolvent le diamond problem : si une classe implémente deux interfaces avec la même méthode default, le compilateur exige une redéfinition explicite. Les méthodes static sont appelées via le nom de l'interface — Vehicle.of("car"), sans instance.
Les erreurs dans la conception d'interfaces conduisent à du code fragile, une complexité de test et des violations SOLID. Examinons trois problèmes courants.
Une interface contenant 15+ méthodes viole le Interface Segregation Principle (ISP). Un exemple est l'ancien java.util.Dictionary avec 10+ méthodes. Solution : diviser en plusieurs petites interfaces — ReadableRepository, WritableRepository, SearchableRepository. Un client (service) dépend uniquement des méthodes dont il a besoin.
Créer une interface pour chaque classe sans réel besoin de polymorphisme est l'anti-patron Interface overkill. Indicateur : l'interface a exactement une implémentation et le projet n'a pas de plans pour ajouter des alternatives. Solution : ajouter une interface seulement lorsqu'une deuxième option d'implémentation apparaît ou qu'un mock est nécessaire pour les tests.
Questions fréquentes
Interface définit uniquement un contrat (signatures de méthodes), ne peut pas avoir d'état et prend en charge l'héritage multiple. Abstract class peut contenir des champs, des constructeurs et des méthodes implémentées, mais une classe ne peut hériter que d'une seule classe abstraite. Depuis Java 8, les interfaces ont obtenu des méthodes default et static, réduisant l'écart.
Oui, en Java et Kotlin, les interfaces prennent en charge l'héritage. public interface AdvancedRepository<T> extends Repository<T>, Pageable est une interface combinant deux autres. Une classe implémentant AdvancedRepository doit implémenter toutes les méthodes des deux interfaces parentes. L'héritage multiple n'est autorisé que pour les interfaces.
Interface fonctionnelle est une interface avec une seule méthode abstraite (SAM — Single Abstract Method). L'annotation @FunctionalInterface garantit cette restriction. Exemples : Runnable, Callable, Comparator, Consumer. Les interfaces fonctionnelles sont la base des expressions lambda Java 8 : () -> System.out.println() implémente Runnable.
Les méthodes default permettent d'ajouter de nouvelles méthodes à une interface sans modifier toutes les classes qui l'implémentent. Par exemple, Java 8 a ajouté stream() à Collection comme méthode default. Sans ce mécanisme, foreach(), stream() et d'autres méthodes auraient nécessité des modifications dans des milliers de classes du JDK. Default est une méthode d'extension rétrocompatible.
Kotlin interdit l'héritage multiple de classes mais permet l'implémentation multiple d'interfaces. Si deux interfaces ont une méthode avec la même signature et implémentation par défaut, le compilateur exige une redéfinition explicite avec l'appel super<InterfaceName>.method(). Cela résout le diamond problem au niveau de la compilation.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi