Interface — is een contract dat een reeks abstracte methoden definieert die een klasse moet implementeren. In Java en Kotlin zijn interfaces het belangrijkste mechanisme voor abstractie en polymorfisme. In Java 8+ kunnen interfaces default en static methoden bevatten, in Kotlin — standaardimplementaties. Volgens Google Android Developers (2025) worden interfaces in 90% van de Android-projecten gebruikt om architectuurlagen te definiëren — repositories, UseCases en services.
Belangrijkste
Interface — is een referentietype dat abstracte methoden, constanten en standaardmethoden bevat. Een klasse die een interface implementeert, moet de implementatie van al zijn abstracte methoden leveren. In Java kan een interface geen toestand hebben (instantievelden), Kotlin volgt deze beperking ook, maar ondersteunt eigenschappen met accessors.
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 en 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();
}
// overige methoden
}Repository<T> — een generieke interface met CRUD-methoden. De default-methode count() biedt een standaardimplementatie die kan worden overschreven. UserRepositoryImpl implementeert de interface met EntityManager voor gegevenstoegang. Deze aanpak maakt het mogelijk om de gegevenslaag te testen via een mock van de interface zonder echte database.
De keuze tussen een interface en een abstracte klasse hangt af van de aanwezigheid van gedeelde toestand en relaties tussen typen. Een interface definieert een contract (wat een klasse kan doen), een abstracte klasse — een gemeenschappelijke implementatie (wat een klasse is).
| Criterium | Interface | Abstract Class |
|---|---|---|
| Toestand (velden) | Alleen static final constanten | Ja, alle velden |
| Constructors | Nee | Ja |
| Meervoudige overerving | Ja (implements) | Nee (extends één) |
| Toegangsmodifiers | public (Java 8-), default methoden | Alle (private, protected, public) |
| Wanneer gebruiken | Contract voor niet-verwante klassen | Gemeenschappelijke basis voor verwante klassen |
In Clean Architecture bevinden interfaces zich in de binnenste laag (domain) en implementaties in de buitenste laag (data). Dit maakt naleving van de Dependency Rule mogelijk: de buitenste laag is afhankelijk van de binnenste, niet andersom. Abstracte klassen worden vaker gebruikt voor sjabloonmethoden (Template Method pattern).
Kotlin-interfaces zijn flexibeler dan Java: ze kunnen abstracte eigenschappen declareren en methodimplementaties bieden. In tegenstelling tot Java ondersteunt Kotlin delegatie via het sleutelwoord by, wat boilerplate aanzienlijk vermindert bij het implementeren van het Delegate-patroon.
interface ApiService {
val baseUrl: String // abstracte eigenschap
suspend fun fetchData(): Result>
fun getEndpoint(path: String): String {
return "$baseUrl/$path" // standaardimplementatie
}
}
class RetrofitApiService(
override val baseUrl: String
) : ApiService {
private val client = Retrofit.Builder()
.baseUrl(baseUrl)
.build()
override suspend fun fetchData(): Result>
{
// implementatie van de aanvraag
}
}
// Delegatie 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 implementeert de Logger-interface via delegatie (by). Alle log()-aanroepen worden doorgestuurd naar het logger-object zonder wrapper-methoden te schrijven. Dit is een voorbeeld van compositie die in Java 5-10 regels boilerplate-code zou vereisen.
Clean Architecture voor Android verdeelt de applicatie duidelijk in lagen. Interfaces fungeren als grenzen tussen lagen: Domain definieert interfaces voor repositories en UseCases, Data levert implementaties. Dit maakt het mogelijk om implementaties te vervangen zonder de bedrijfslogica te wijzigen — een belangrijk voordeel bij migratie van Room naar Firebase of van REST naar GraphQL.
// Domain-laag — interface (contract)
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun updateUser(user: User)
}
// Domain-laag — use case (afhankelijk van interface)
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: String): Result {
return runCatching { repository.getUser(id) }
}
}
// Data-laag — implementatie
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 — interface in de domain-laag. GetUserUseCase is afhankelijk van de interface, niet van de implementatie. UserRepositoryImpl in de data-laag implementeert de interface en combineert API en lokale database. Deze architectuur maakt het mogelijk om UseCases te testen met een mock-repository zonder database- of netwerkconfiguratie.
Default methoden zijn toegevoegd in Java 8 voor evolutionaire uitbreiding van interfaces zonder achterwaartse compatibiliteit te schenden. Als ArrayList de nieuwe stream()-methode die aan Collection was toegevoegd niet had geïmplementeerd, zou oude code blijven werken. Static methoden in interfaces dienen voor hulpprogramma's die bij de interface horen — een alternatief voor utility-klassen.
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";
}Default-methoden lossen het diamond problem op: als een klasse twee interfaces met dezelfde default-methode implementeert, vereist de compiler expliciete overschrijving. Static-methoden worden aangeroepen via de interfacenaam — Vehicle.of("car"), zonder instantie.
Fouten bij het ontwerpen van interfaces leiden tot breekbare code, testproblemen en SOLID-schendingen. Laten we drie veelvoorkomende problemen bekijken.
Een interface met 15+ methoden schendt het Interface Segregation Principle (ISP). Voorbeeld — de oude java.util.Dictionary met 10+ methoden. Oplossing: splitsen in meerdere kleine interfaces — ReadableRepository, WritableRepository, SearchableRepository. De klant (service) is alleen afhankelijk van de methoden die hij nodig heeft.
Het maken van een interface voor elke klasse zonder echte behoefte aan polymorfisme is het antipatroon Interface overkill. Kenmerk: de interface heeft precies één implementatie en er zijn geen plannen om alternatieven toe te voegen. Oplossing: voeg een interface alleen toe wanneer er een tweede implementatievariant ontstaat of behoefte is aan een mock voor tests.
Veelgestelde vragen
Interface definieert alleen het contract (methodesignaturen), kan geen toestand hebben en ondersteunt meervoudige overerving. Abstract class kan velden, constructors en geïmplementeerde methoden bevatten, maar een klasse kan slechts één abstracte klasse erven. Sinds Java 8 hebben interfaces default en static methoden gekregen, waardoor het verschil kleiner is geworden.
Ja, in Java en Kotlin ondersteunen interfaces overerving. public interface AdvancedRepository<T> extends Repository<T>, Pageable — een interface die twee andere combineert. Een klasse die AdvancedRepository implementeert, moet alle methoden van beide ouderinterfaces implementeren. Meervoudige overerving is alleen toegestaan voor interfaces.
Functionele interface — een interface met één abstracte methode (SAM — Single Abstract Method). De annotatie @FunctionalInterface garandeert deze beperking. Voorbeelden: Runnable, Callable, Comparator, Consumer. Functionele interfaces vormen de basis van lambda-expressies in Java 8: () -> System.out.println() implementeert Runnable.
Default methoden maken het mogelijk om nieuwe methoden aan een interface toe te voegen zonder alle implementerende klassen te wijzigen. Java 8 voegde bijvoorbeeld stream() toe aan Collection als default-methode. Zonder dit mechanisme zouden foreach(), stream() en andere methoden duizenden klassen in de JDK hebben moeten wijzigen. Default — een achterwaarts compatibele manier van uitbreiden.
Kotlin verbiedt meervoudige overerving van klassen, maar staat meervoudige implementatie van interfaces toe. Als twee interfaces een methode met dezelfde signatuur en standaardimplementatie hebben, vereist de compiler expliciete overschrijving met een aanroep van super<InterfaceName>.method(). Dit lost het diamond problem op compilatieniveau op.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook