Interface — to kontrakt określający zestaw abstrakcyjnych metod, które klasa musi zaimplementować. W Javie i Kotlinie interfejsy są głównym mechanizmem abstrakcji i polimorfizmu. W Javie 8+ interfejsy mogą zawierać metody default i static, w Kotlinie — implementacje domyślne. Według Google Android Developers (2025), interfejsy są używane w 90% projektów Android do definiowania warstw architektury — repozytoriów, UseCase i serwisów.
Najważniejsze
Interface — to typ referencyjny, który zawiera abstrakcyjne metody, stałe i metody domyślne. Klasa implementująca interfejs musi dostarczyć implementację wszystkich jego abstrakcyjnych metod. W Javie interfejs nie może mieć stanu (pól instancji), Kotlin również przestrzega tego ograniczenia, ale obsługuje właściwości z akcesorami.
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;
// gettery i settery
}
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();
}
// pozostałe metody
}Repository<T> — interfejs generyczny z metodami CRUD. Metoda domyślna count() dostarcza domyślną implementację, którą można nadpisać. UserRepositoryImpl implementuje interfejs, używając EntityManager do dostępu do danych. Takie podejście pozwala testować warstwę danych przez mock interfejsu bez prawdziwej bazy danych.
Wybór między interfejsem a klasą abstrakcyjną zależy od obecności wspólnego stanu i relacji między typami. Interfejs określa kontrakt (co klasa może robić), klasa abstrakcyjna — wspólną implementację (czym klasa jest).
| Kryterium | Interface | Abstract Class |
|---|---|---|
| Stan (pola) | Tylko stałe static final | Tak, dowolne pola |
| Konstruktory | Nie | Tak |
| Dziedziczenie wielokrotne | Tak (implements) | Nie (extends jeden) |
| Modyfikatory dostępu | public (Java 8-), metody default | Wszystkie (private, protected, public) |
| Kiedy używać | Kontrakt dla niepowiązanych klas | Wspólna baza dla pokrewnych klas |
W Clean Architecture interfejsy znajdują się w wewnętrznej warstwie (domain), a implementacje — w zewnętrznej (data). Pozwala to zachować regułę zależności: warstwa zewnętrzna zależy od wewnętrznej, ale nie odwrotnie. Klasy abstrakcyjne są częściej używane dla metod szablonowych (Template Method pattern).
Interfejsy Kotlina są bardziej elastyczne niż w Javie: mogą deklarować abstrakcyjne właściwości i dostarczać implementacje metod. W przeciwieństwie do Javy, Kotlin obsługuje delegowanie przez słowo kluczowe by, co znacząco redukuje boilerplate przy implementacji wzorca Delegate.
interface ApiService {
val baseUrl: String // abstrakcyjna właściwość
suspend fun fetchData(): Result>
fun getEndpoint(path: String): String {
return "$baseUrl/$path" // domyślna implementacja
}
}
class RetrofitApiService(
override val baseUrl: String
) : ApiService {
private val client = Retrofit.Builder()
.baseUrl(baseUrl)
.build()
override suspend fun fetchData(): Result>
{
// implementacja zapytania
}
}
// Delegacja przez by
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) = println(message)
}
class UserService(logger: Logger) : Logger by loggerUserService implementuje interfejs Logger przez delegowanie (by). Wszystkie wywołania log() są przekierowywane do obiektu logger bez pisania metod-opakowań. To przykład kompozycji, która w Javie wymagałaby 5-10 linii boilerplate-kodu.
Clean Architecture dla Androida wyraźnie dzieli aplikację na warstwy. Interfejsy pełnią rolę granic między warstwami: Domain definiuje interfejsy repozytoriów i UseCase, Data dostarcza implementacje. Pozwala to wymieniać implementacje bez zmiany logiki biznesowej — kluczowa zaleta przy migracji z Room na Firebase lub z REST na GraphQL.
// Warstwa Domain — interfejs (kontrakt)
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun updateUser(user: User)
}
// Warstwa Domain — use case (zależy od interfejsu)
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: String): Result {
return runCatching { repository.getUser(id) }
}
}
// Warstwa Data — implementacja
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 — interfejs w warstwie domain. GetUserUseCase zależy od interfejsu, a nie od implementacji. UserRepositoryImpl w warstwie data implementuje interfejs, łącząc API i lokalną bazę danych. Taka architektura pozwala testować UseCase z mock-repozytorium bez konfiguracji bazy danych lub sieci.
Metody default zostały dodane w Javie 8 do ewolucyjnego rozszerzania interfejsów bez naruszania wstecznej kompatybilności. Gdyby ArrayList nie implementował nowej metody stream() dodanej do Collection, stary kod działałby dalej. Metody static w interfejsach służą jako narzędzia związane z interfejsem — alternatywa dla klas narzędziowych.
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();
}
// stała
String CATEGORY = "transport";
}Metody default rozwiązują problem diamentu (diamond problem): jeśli klasa implementuje dwa interfejsy z tą samą metodą default, kompilator wymaga jawnego nadpisania. Metody static są wywoływane przez nazwę interfejsu — Vehicle.of("car"), bez instancji.
Błędy w projektowaniu interfejsów prowadzą do kruchego kodu, trudności w testowaniu i naruszeń SOLID. Rozważmy trzy częste problemy.
Interfejs zawierający 15+ metod narusza Interface Segregation Principle (ISP). Przykład — stary java.util.Dictionary z 10+ metodami. Rozwiązanie: podzielić na kilka mniejszych interfejsów — ReadableRepository, WritableRepository, SearchableRepository. Klient (serwis) zależy tylko od potrzebnych mu metod.
Tworzenie interfejsu dla każdej klasy bez rzeczywistej potrzeby polimorfizmu to antywzorzec Interface overkill. Oznaka: interfejs ma dokładnie jedną implementację, a w projekcie nie ma planów dodawania alternatyw. Rozwiązanie: dodawać interfejs tylko gdy pojawia się druga wersja implementacji lub potrzeba mocka do testów.
Często zadawane pytania
Interface określa tylko kontrakt (sygnatury metod), nie może mieć stanu i obsługuje dziedziczenie wielokrotne. Abstract class może zawierać pola, konstruktory i zaimplementowane metody, ale klasa może dziedziczyć tylko jedną klasę abstrakcyjną. Od Javy 8 interfejsy otrzymały metody default i static, zmniejszając różnicę.
Tak, w Javie i Kotlinie interfejsy obsługują dziedziczenie. public interface AdvancedRepository<T> extends Repository<T>, Pageable — interfejs łączący dwa inne. Klasa implementująca AdvancedRepository musi zaimplementować wszystkie metody obu rodzicielskich interfejsów. Dziedziczenie wielokrotne jest dozwolone tylko dla interfejsów.
Interfejs funkcyjny — interfejs z jedną metodą abstrakcyjną (SAM — Single Abstract Method). Adnotacja @FunctionalInterface gwarantuje to ograniczenie. Przykłady: Runnable, Callable, Comparator, Consumer. Interfejsy funkcyjne są podstawą wyrażeń lambda w Javie 8: () -> System.out.println() implementuje Runnable.
Metody default pozwalają dodawać nowe metody do interfejsu bez zmiany wszystkich implementujących klas. Na przykład Java 8 dodała stream() do Collection jako metodę default. Bez tego mechanizmu foreach(), stream() i inne metody wymagałyby zmiany tysięcy klas w JDK. Default — to wstecznie kompatybilny sposób rozszerzania.
Kotlin zabrania wielokrotnego dziedziczenia klas, ale zezwala na wielokrotną implementację interfejsów. Jeśli dwa interfejsy mają metodę o tej samej sygnaturze i domyślnej implementacji, kompilator wymaga jawnego nadpisania z wywołaniem super<InterfaceName>.method(). Rozwiązuje to problem diamentu na poziomie kompilacji.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również