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 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.
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.
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 è).
| Criterio | Interface | Abstract Class |
|---|---|---|
| Stato (campi) | Solo costanti static final | Sì, qualsiasi campo |
| Costruttori | No | Sì |
| Ereditarietà multipla | Sì (implements) | No (extends uno) |
| Modificatori d'accesso | public (Java 8-), metodi default | Tutti (private, protected, public) |
| Quando usare | Contratto per classi non correlate | Base 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).
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.
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 loggerUserService 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.
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.
// 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.
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à.
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 nella progettazione di interfacce portano a codice fragile, complessità di test e violazioni SOLID. Esaminiamo tre problemi comuni.
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.
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
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.
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.
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.
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.
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
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.
Leggi anche