Interface es un contrato que define un conjunto de métodos abstractos que una clase debe implementar. En Java y Kotlin, las interfaces son el mecanismo principal de abstracción y polimorfismo. En Java 8+, las interfaces pueden contener métodos default y static, en Kotlin — implementaciones por defecto. Según Google Android Developers (2025), las interfaces se usan en el 90% de los proyectos Android para definir capas de arquitectura — repositorios, UseCases y servicios.
Puntos clave
Interface es un tipo de referencia que contiene métodos abstractos, constantes y métodos por defecto. Una clase que implementa una interfaz debe proporcionar implementación para todos sus métodos abstractos. En Java, una interfaz no puede tener estado (campos de instancia); Kotlin también sigue esta restricción pero admite propiedades con accesores.
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 y 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();
}
// otros métodos
}Repository<T> es una interfaz genérica con métodos CRUD. El método default count() proporciona una implementación por defecto que puede ser sobrescrita. UserRepositoryImpl implementa la interfaz usando EntityManager para el acceso a datos. Este enfoque permite probar la capa de datos mediante un mock de la interfaz sin una base de datos real.
La elección entre una interfaz y una clase abstracta depende de la presencia de estado compartido y las relaciones entre tipos. Una interfaz define un contrato (lo que una clase puede hacer), una clase abstracta define una implementación común (lo que una clase es).
| Criterio | Interface | Abstract Class |
|---|---|---|
| Estado (campos) | Solo constantes static final | Sí, cualquier campo |
| Constructores | No | Sí |
| Herencia múltiple | Sí (implements) | No (extends uno) |
| Modificadores de acceso | public (Java 8-), métodos default | Todos (private, protected, public) |
| Cuándo usar | Contrato para clases no relacionadas | Base común para clases relacionadas |
En Clean Architecture, las interfaces se colocan en la capa interna (domain), y las implementaciones en la capa externa (data). Esto permite mantener la Dependency Rule: la capa externa depende de la interna, pero no al revés. Las clases abstractas se usan más a menudo para métodos plantilla (Template Method pattern).
Las interfaces de Kotlin son más flexibles que las de Java: pueden declarar propiedades abstractas y proporcionar implementaciones de métodos. A diferencia de Java, Kotlin admite la delegación mediante la palabra clave by, lo que reduce poderosamente el boilerplate al implementar el patrón Delegate.
interface ApiService {
val baseUrl: String // propiedad abstracta
suspend fun fetchData(): Result>
fun getEndpoint(path: String): String {
return "$baseUrl/$path" // implementación por defecto
}
}
class RetrofitApiService(
override val baseUrl: String
) : ApiService {
private val client = Retrofit.Builder()
.baseUrl(baseUrl)
.build()
override suspend fun fetchData(): Result>
{
// implementación de solicitud
}
}
// Delegación mediante 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 la interfaz Logger mediante delegación (by). Todas las llamadas a log() se redirigen al objeto logger sin escribir métodos envolventes. Este es un ejemplo de composición que en Java requeriría 5-10 líneas de código boilerplate.
Clean Architecture para Android separa claramente la aplicación en capas. Las interfaces actúan como límites entre capas: Domain define las interfaces de repositorios y UseCases, Data proporciona las implementaciones. Esto permite reemplazar implementaciones sin cambiar la lógica de negocio — una ventaja clave al migrar de Room a Firebase o de REST a GraphQL.
// Domain layer — interfaz (contrato)
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun updateUser(user: User)
}
// Domain layer — use case (depende de la interfaz)
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: String): Result {
return runCatching { repository.getUser(id) }
}
}
// Data layer — implementación
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 es una interfaz en la capa domain. GetUserUseCase depende de la interfaz, no de la implementación. UserRepositoryImpl en la capa data implementa la interfaz, combinando API y base de datos local. Esta arquitectura permite probar UseCases con un repositorio mock sin configurar una base de datos o red.
Los métodos default se añadieron en Java 8 para la extensión evolutiva de interfaces sin romper la compatibilidad hacia atrás. Si ArrayList no implementara el nuevo método stream() añadido a Collection, el código antiguo seguiría funcionando. Los métodos static en interfaces sirven como utilidades relacionadas con la interfaz — una alternativa a las clases utilitarias.
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";
}Los métodos default resuelven el diamond problem: si una clase implementa dos interfaces con el mismo método default, el compilador requiere una sobrescritura explícita. Los métodos static se llaman a través del nombre de la interfaz — Vehicle.of("car"), sin una instancia.
Los errores en el diseño de interfaces conducen a código frágil, complejidad en las pruebas y violaciones de SOLID. Veamos tres problemas comunes.
Una interfaz que contiene 15+ métodos viola el Interface Segregation Principle (ISP). Un ejemplo es el antiguo java.util.Dictionary con 10+ métodos. Solución: dividir en varias interfaces pequeñas — ReadableRepository, WritableRepository, SearchableRepository. Un cliente (servicio) depende solo de los métodos que necesita.
Crear una interfaz para cada clase sin una necesidad real de polimorfismo es el antipatrón Interface overkill. Indicador: la interfaz tiene exactamente una implementación y el proyecto no tiene planes de añadir alternativas. Solución: añadir una interfaz solo cuando aparece una segunda opción de implementación o se necesita un mock para pruebas.
Preguntas frecuentes
Interface define solo un contrato (firmas de métodos), no puede tener estado y admite herencia múltiple. Abstract class puede contener campos, constructores y métodos implementados, pero una clase puede heredar solo una clase abstracta. Desde Java 8, las interfaces han obtenido métodos default y static, reduciendo la brecha.
Sí, en Java y Kotlin las interfaces admiten herencia. public interface AdvancedRepository<T> extends Repository<T>, Pageable es una interfaz que combina otras dos. Una clase que implementa AdvancedRepository debe implementar todos los métodos de ambas interfaces padre. La herencia múltiple solo está permitida para interfaces.
Interfaz funcional es una interfaz con un único método abstracto (SAM — Single Abstract Method). La anotación @FunctionalInterface garantiza esta restricción. Ejemplos: Runnable, Callable, Comparator, Consumer. Las interfaces funcionales son la base de las expresiones lambda de Java 8: () -> System.out.println() implementa Runnable.
Los métodos default permiten añadir nuevos métodos a una interfaz sin modificar todas las clases que la implementan. Por ejemplo, Java 8 añadió stream() a Collection como método default. Sin este mecanismo, foreach(), stream() y otros métodos requerirían cambios en miles de clases del JDK. Default es una forma de extensión compatible hacia atrás.
Kotlin prohíbe la herencia múltiple de clases pero permite la implementación múltiple de interfaces. Si dos interfaces tienen un método con la misma firma e implementación por defecto, el compilador requiere una sobrescritura explícita con la llamada super<InterfaceName>.method(). Esto resuelve el diamond problem a nivel de compilación.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también