Interface: essenza, contratti in Java e Kotlin per Android

Autore: IT Sectr Pubblicato: 2026-02-18 Tempo di lettura: 9 min

Interface è un contratto che definisce un insieme di metodi astratti che una classe deve implementare. In Java e Kotlin, le interfacce sono il meccanismo principale di astrazione e polimorfismo. In Java 8+, le interfacce possono contenere metodi default e static, in Kotlin — implementazioni predefinite. Secondo Google Android Developers (2025), le interfacce sono utilizzate nel 90% dei progetti Android per definire i livelli architetturali — repository, UseCase e servizi.

Punti chiave

  • Interface è un tipo astratto che definisce firme di metodi senza implementazione (prima di Java 8)
  • Implements è la parola chiave che lega una classe a un'interfaccia; una classe può implementare più interfacce
  • Default method è un metodo con implementazione in un'interfaccia Java, aggiunto in Java 8 per retrocompatibilità
  • Kotlin interface supporta proprietà con getter e implementazioni di metodi, sostituendo abstract class in molti scenari
  • Markup interface è un'interfaccia vuota utilizzata come marcatore (Serializable, Cloneable, RandomAccess)

Cos'è Interface in Java e Kotlin?

Interface è un tipo di riferimento che contiene metodi astratti, costanti e metodi predefiniti. Una classe che implementa un'interfaccia deve fornire l'implementazione per tutti i suoi metodi astratti. In Java, un'interfaccia non può avere stato (campi di istanza); Kotlin segue anch'essa questa restrizione ma supporta proprietà con accessor.

Java
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;
    // getter e setter
}

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();
    }
    // altri metodi
}

Repository<T> è un'interfaccia generica con metodi CRUD. Il metodo default count() fornisce un'implementazione predefinita che può essere sovrascritta. UserRepositoryImpl implementa l'interfaccia utilizzando EntityManager per l'accesso ai dati. Questo approccio consente di testare il livello dati tramite un mock dell'interfaccia senza un database reale.

Interface vs abstract class: quando scegliere cosa

La scelta tra un'interfaccia e una classe astratta dipende dalla presenza di stato condiviso e dalle relazioni tra tipi. Un'interfaccia definisce un contratto (cosa una classe può fare), una classe astratta definisce un'implementazione comune (cosa una classe è).

CriterioInterfaceAbstract Class
Stato (campi)Solo costanti static finalSì, qualsiasi campo
CostruttoriNo
Ereditarietà multiplaSì (implements)No (extends uno)
Modificatori d'accessopublic (Java 8-), metodi defaultTutti (private, protected, public)
Quando usareContratto per classi non correlateBase comune per classi correlate

In Clean Architecture, le interfacce sono posizionate nel livello interno (domain) e le implementazioni nel livello esterno (data). Ciò consente di mantenere la Dependency Rule: il livello esterno dipende da quello interno, ma non viceversa. Le classi astratte sono più spesso utilizzate per metodi template (Template Method pattern).

Interface in Kotlin: proprietà e delegati

Le interfacce Kotlin sono più flessibili di Java: possono dichiarare proprietà astratte e fornire implementazioni di metodi. A differenza di Java, Kotlin supporta la delega tramite la parola chiave by, che riduce potentemente il boilerplate nell'implementazione del pattern Delegate.

Kotlin
interface ApiService {
    val baseUrl: String  // proprietà astratta
    
    suspend fun fetchData(): Result>
    
    fun getEndpoint(path: String): String {
        return "$baseUrl/$path"  // implementazione predefinita
    }
}

class RetrofitApiService(
    override val baseUrl: String
) : ApiService {
    private val client = Retrofit.Builder()
        .baseUrl(baseUrl)
        .build()
    
    override suspend fun fetchData(): Result> {
        // implementazione della richiesta
    }
}

// Delega tramite by
interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println(message)
}

class UserService(logger: Logger) : Logger by logger

UserService implementa l'interfaccia Logger tramite delega (by). Tutte le chiamate log() vengono reindirizzate all'oggetto logger senza scrivere metodi wrapper. Questo è un esempio di composizione che in Java richiederebbe 5-10 righe di codice boilerplate.

Interface in Clean Architecture Android

Clean Architecture per Android separa chiaramente l'applicazione in livelli. Le interfacce fungono da confini tra i livelli: Domain definisce le interfacce di repository e UseCase, Data fornisce le implementazioni. Ciò consente di sostituire le implementazioni senza modificare la logica di business — un vantaggio chiave durante la migrazione da Room a Firebase o da REST a GraphQL.

Kotlin
// Domain layer — interfaccia (contratto)
interface UserRepository {
    suspend fun getUser(id: String): User
    suspend fun updateUser(user: User)
}

// Domain layer — use case (dipende dall'interfaccia)
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: String): Result {
        return runCatching { repository.getUser(id) }
    }
}

// Data layer — implementazione
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 è un'interfaccia nel livello domain. GetUserUseCase dipende dall'interfaccia, non dall'implementazione. UserRepositoryImpl nel livello data implementa l'interfaccia, combinando API e database locale. Questa architettura consente di testare UseCases con un repository mock senza configurare database o rete.

Metodi default e static in Java 8+

I metodi default sono stati aggiunti in Java 8 per l'estensione evolutiva delle interfacce senza rompere la retrocompatibilità. Se ArrayList non implementasse il nuovo metodo stream() aggiunto a Collection, il codice vecchio continuerebbe a funzionare. I metodi static nelle interfacce servono come utility relative all'interfaccia — un'alternativa alle classi di utilità.

Java
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();
    }
    
    // costante
    String CATEGORY = "transport";
}

I metodi default risolvono il diamond problem: se una classe implementa due interfacce con lo stesso metodo default, il compilatore richiede un override esplicito. I metodi static vengono chiamati tramite il nome dell'interfaccia — Vehicle.of("car"), senza un'istanza.

Errori comuni nella progettazione di interfacce

Errori nella progettazione di interfacce portano a codice fragile, complessità di test e violazioni SOLID. Esaminiamo tre problemi comuni.

Interface Pollution — troppi metodi in un'interfaccia

Un'interfaccia contenente 15+ metodi viola l'Interface Segregation Principle (ISP). Un esempio è il vecchio java.util.Dictionary con 10+ metodi. Soluzione: dividere in più interfacce piccole — ReadableRepository, WritableRepository, SearchableRepository. Un client (servizio) dipende solo dai metodi di cui ha bisogno.

Astrazione eccessiva — interfaccia per ogni classe

Creare un'interfaccia per ogni classe senza un reale bisogno di polimorfismo è l'anti-pattern Interface overkill. Indicatore: l'interfaccia ha esattamente un'implementazione e il progetto non ha piani per aggiungere alternative. Soluzione: aggiungere un'interfaccia solo quando appare una seconda opzione di implementazione o serve un mock per i test.

Domande frequenti

In cosa interface si differenzia da abstract class in Java?

Interface definisce solo un contratto (firme dei metodi), non può avere stato e supporta l'ereditarietà multipla. Abstract class può contenere campi, costruttori e metodi implementati, ma una classe può ereditare solo una classe astratta. Da Java 8, le interfacce hanno ottenuto metodi default e static, riducendo il divario.

Un'interfaccia può ereditare un'altra interfaccia?

Sì, in Java e Kotlin le interfacce supportano l'ereditarietà. public interface AdvancedRepository<T> extends Repository<T>, Pageable è un'interfaccia che combina altre due. Una classe che implementa AdvancedRepository deve implementare tutti i metodi di entrambe le interfacce genitore. L'ereditarietà multipla è consentita solo per le interfacce.

Cos'è un'interfaccia funzionale in Java?

Interfaccia funzionale è un'interfaccia con un singolo metodo astratto (SAM — Single Abstract Method). L'annotazione @FunctionalInterface garantisce questa restrizione. Esempi: Runnable, Callable, Comparator, Consumer. Le interfacce funzionali sono la base delle espressioni lambda Java 8: () -> System.out.println() implementa Runnable.

Perché sono necessari i metodi default nelle interfacce Java?

I metodi default consentono di aggiungere nuovi metodi a un'interfaccia senza modificare tutte le classi che la implementano. Ad esempio, Java 8 ha aggiunto stream() a Collection come metodo default. Senza questo meccanismo, foreach(), stream() e altri metodi avrebbero richiesto modifiche a migliaia di classi nel JDK. Default è un modo di estensione retrocompatibile.

Come risolve Kotlin il problema dell'ereditarietà multipla?

Kotlin vieta l'ereditarietà multipla di classi ma consente l'implementazione multipla di interfacce. Se due interfacce hanno un metodo con la stessa firma e implementazione predefinita, il compilatore richiede un override esplicito con la chiamata super<InterfaceName>.method(). Questo risolve il diamond problem a livello di compilazione.

Riepilogo

  • Interface è un contratto che definisce metodi che una classe deve implementare; il meccanismo principale di polimorfismo in Java e Kotlin
  • Java 8+ ha aggiunto metodi default e static alle interfacce, riducendo il divario con le classi astratte
  • Kotlin interfaces supportano proprietà astratte, implementazioni predefinite e delega tramite by
  • Clean Architecture utilizza le interfacce come confini tra i livelli Domain e Data
  • Interface Segregation Principle richiede di dividere interfacce grandi in interfacce specializzate
  • Interfacce funzionali (Single Abstract Method) sono la base delle espressioni lambda e di Stream API
  • Raccomandazione: aggiungi un'interfaccia quando appare una seconda implementazione o serve un mock per i test

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche