Es una de las preguntas más frecuentes en entrevistas técnicas de Java. Una que aparece en el 90% de los procesos de selección según datos de CodeGym. Y a pesar de eso, muchos desarrolladores la responden de forma superficial o confunden los casos de uso.
Interfaces y clases abstractas no son simplemente "dos formas de hacer lo mismo". Son herramientas con propósitos distintos que responden a preguntas distintas. Entender cuándo usar cada una es una señal clara de madurez en el diseño de software orientado a objetos.
Si ya tienes claros los 4 pilares de la Programación Orientada a Objetos, este artículo es el paso siguiente natural. Y si quieres ver estos conceptos aplicados en colecciones y procesamiento de datos, échale un vistazo a nuestro artículo sobre Streams en Java.
La pregunta clave antes de decidir
Antes de entrar en la técnica, hay una pregunta conceptual que lo resuelve todo:
¿Estás definiendo lo que un objeto ES, o lo que un objeto PUEDE HACER?
- Si es lo que un objeto ES → clase abstracta
- Si es lo que un objeto PUEDE HACER → interfaz
// Un Perro ES un Animal → clase abstracta
// Un Perro PUEDE nadar → interfaz Nadable
// Un Perro PUEDE ser entrenado → interfaz Entrenable
abstract class Animal { ... } // Perro ES-UN Animal
interface Nadable { void nadar(); } // Perro PUEDE nadar
interface Entrenable { void obedecer(); }
class Perro extends Animal implements Nadable, Entrenable {
public void nadar() { ... }
public void obedecer() { ... }
}¿Qué es una Clase Abstracta?
Una clase abstracta es una clase que no puede instanciarse directamente — no puedes hacer new Animal(). Sirve como plantilla base para otras clases, pudiendo contener tanto métodos abstractos (sin implementación) como métodos concretos (con implementación compartida).
// Clase abstracta — el molde para una jerarquía de clases relacionadas
public abstract class Animal {
// Atributos de instancia — las clases abstractas SÍ tienen estado
private String nombre;
private int edad;
protected double peso; // accesible por subclases
// Constructor — las clases abstractas SÍ tienen constructor
// (lo llaman las subclases con super())
public Animal(String nombre, int edad, double peso) {
this.nombre = nombre;
this.edad = edad;
this.peso = peso;
}
// Método abstracto — OBLIGA a las subclases a implementarlo
// No tiene cuerpo — las subclases deciden el "cómo"
public abstract String hacerSonido();
// Método abstracto — cada animal se mueve de forma diferente
public abstract void moverse();
// Método concreto — implementación compartida por TODAS las subclases
// No necesitan sobreescribirlo (aunque pueden)
public void comer() {
System.out.println(nombre + " está comiendo");
}
public void dormir() {
System.out.println(nombre + " está durmiendo");
}
public void describir() {
System.out.printf("%s — %d años, %.1f kg%n", nombre, edad, peso);
}
// Getters
public String getNombre() { return nombre; }
public int getEdad() { return edad; }
public double getPeso() { return peso; }
}// Subclases concretas — implementan los métodos abstractos
public class Perro extends Animal {
private String raza;
public Perro(String nombre, int edad, double peso, String raza) {
super(nombre, edad, peso); // llama al constructor del padre
this.raza = raza;
}
@Override
public String hacerSonido() {
return "¡Guau!"; // implementación específica del perro
}
@Override
public void moverse() {
System.out.println(getNombre() + " corre en cuatro patas");
}
// Método específico del Perro (no existe en Animal)
public void buscarPelota() {
System.out.println(getNombre() + " busca la pelota");
}
}
public class Aguila extends Animal {
public Aguila(String nombre, int edad, double peso) {
super(nombre, edad, peso);
}
@Override
public String hacerSonido() {
return "¡Squawk!";
}
@Override
public void moverse() {
System.out.println(getNombre() + " vuela a gran velocidad");
}
}
// Uso:
Perro miPerro = new Perro("Rex", 3, 25.5, "Labrador");
miPerro.comer(); // heredado de Animal — "Rex está comiendo"
miPerro.hacerSonido(); // implementado en Perro — "¡Guau!"
miPerro.buscarPelota(); // específico de Perro
// Animal miAnimal = new Animal("X", 1, 1.0); ← Error de compilación
// No se puede instanciar una clase abstracta¿Qué es una Interfaz?
Una interfaz define un contrato de comportamiento: especifica qué debe poder hacer cualquier clase que la implemente, sin importar de qué jerarquía provenga esa clase. Antes de Java 8, las interfaces solo podían tener métodos abstractos y constantes. Desde Java 8, también pueden tener métodos default y static.
// Interfaz — define un contrato de comportamiento
// No impone jerarquía — cualquier clase puede implementarla
public interface Nadable {
// Método abstracto — cualquier implementador DEBE definirlo
void nadar();
// Método default (Java 8+) — implementación opcional que puede sobreescribirse
default void bucear() {
System.out.println("Buceando a poca profundidad");
}
// Método static — no se sobreescribe, se llama con el nombre de la interfaz
static String obtenerMedioNatural() {
return "agua";
}
}
public interface Volable {
void volar();
double obtenerAlturaMaxima();
default void planear() {
System.out.println("Planeando con las corrientes de aire");
}
}
public interface Entrenable {
void aprender(String comando);
void ejecutar(String comando);
default boolean puedeAprender() {
return true;
}
}
// Una clase puede implementar MÚLTIPLES interfaces — herencia múltiple de tipo
public class Pato extends Animal implements Nadable, Volable {
public Pato(String nombre, int edad, double peso) {
super(nombre, edad, peso);
}
@Override
public String hacerSonido() { return "¡Cuac!"; }
@Override
public void moverse() { System.out.println("El pato camina bamboleándose"); }
@Override
public void nadar() {
System.out.println(getNombre() + " nada en el estanque");
}
@Override
public void volar() {
System.out.println(getNombre() + " vuela en formación en V");
}
@Override
public double obtenerAlturaMaxima() { return 100.0; } // metros
// bucear() heredado del default de Nadable — no necesita sobreescribirse
// planear() heredado del default de Volable — no necesita sobreescribirse
}
// Una clase completamente diferente puede implementar la misma interfaz
// Un Pez no tiene NADA en común con un Pato en su jerarquía, pero ambos nadan
public class Pez implements Nadable {
@Override
public void nadar() {
System.out.println("El pez nada con aletas");
}
@Override
public void bucear() { // sobreescribe el default
System.out.println("El pez bucea hasta el fondo del océano");
}
}
// Uso del polimorfismo con interfaces:
List<Nadable> seresQueNadan = Arrays.asList(
new Pato("Donald", 5, 1.5),
new Pez(),
new Perro("Rex", 3, 25.5) // si Perro implementa Nadable
);
seresQueNadan.forEach(Nadable::nadar); // cada uno nada a su maneraLas diferencias técnicas — tabla completa
| Característica | Clase Abstracta | Interfaz |
|---|---|---|
| Instanciación | ❌ No se puede instanciar directamente | ❌ No se puede instanciar directamente |
| Herencia | Una clase solo puede extender UNA clase abstracta | Una clase puede implementar MÚLTIPLES interfaces |
| Estado (atributos) | ✅ Puede tener atributos de instancia con cualquier valor | Solo constantes (public static final) |
| Constructor | ✅ Puede tener constructores | ❌ No puede tener constructores |
| Métodos abstractos | ✅ Puede tener (con abstract) | ✅ Todos los métodos son abstractos por defecto |
| Métodos concretos | ✅ Puede tener (con implementación) | Solo con default o static (Java 8+) |
| Modificadores de acceso | public, protected, private, default | Solo public (implícito) |
| Palabra clave | extends | implements |
| Relación | ES-UN (is-a) — jerarquía | PUEDE-HACER (can-do) — capacidad |
| Propósito | Compartir estado y lógica en una jerarquía | Definir un contrato de comportamiento |
Evolución de las Interfaces: Java 8, 9 y 21
Las interfaces cambiaron drásticamente con Java 8. Muchos artículos desactualizados dicen que "las interfaces solo tienen métodos abstractos" — eso era verdad antes de 2014.
public interface InterfazModerna {
// ── Antes de Java 8 — solo esto ─────────────────────────
// Constante (implícitamente public static final)
int MAX_INTENTOS = 3;
// Método abstracto (implícitamente public abstract)
void metodoObligatorio();
// ── Java 8 — default y static ────────────────────────────
// default: implementación opcional que las clases pueden sobreescribir
// Creados para añadir métodos a interfaces existentes sin romper código
default void metodoConImplementacionOpcional() {
System.out.println("Implementación por defecto");
}
// static: pertenece a la interfaz, no a la implementación
// No se hereda ni se sobreescribe
static String obtenerVersion() {
return "1.0";
}
// ── Java 9 — private ──────────────────────────────────────
// private: métodos auxiliares para los default, no expuestos al exterior
private void logInterno(String mensaje) {
System.out.println("[LOG] " + mensaje);
}
default void operacionCompleja() {
logInterno("Iniciando operación"); // uso del private
realizarOperacion();
logInterno("Operación completada");
}
private void realizarOperacion() {
System.out.println("Ejecutando...");
}
}
// ── Interfaces funcionales (Java 8) ──────────────────────────
// Una interfaz con exactamente UN método abstracto
// Puede usarse con lambdas y referencias a métodos
@FunctionalInterface
public interface Validador<T> {
boolean validar(T valor); // único método abstracto
// Puede tener default y static — sigue siendo funcional
default Validador<T> y(Validador<T> otro) {
return valor -> this.validar(valor) && otro.validar(valor);
}
}
// Usar con lambda:
Validador<String> noVacio = s -> !s.isEmpty();
Validador<String> longOk = s -> s.length() >= 3;
Validador<String> emailOk = s -> s.contains("@");
Validador<String> emailValido = noVacio.y(longOk).y(emailOk);
System.out.println(emailValido.validar("ana@mail.com")); // true
System.out.println(emailValido.validar("")); // false
// Las interfaces funcionales más usadas del JDK:
// Predicate<T> → boolean test(T t)
// Function<T,R> → R apply(T t)
// Consumer<T> → void accept(T t)
// Supplier<T> → T get()
// Comparator<T> → int compare(T a, T b)Cuándo usar Clase Abstracta
✅ 1. Cuando las subclases comparten estado (atributos)
// Si varias clases necesitan los mismos atributos → clase abstracta
public abstract class CuentaBancaria {
// Estado compartido — no se puede poner en una interfaz
private final String numeroCuenta;
private double saldo;
private final String titular;
private final List<String> historial = new ArrayList<>();
public CuentaBancaria(String numeroCuenta, String titular, double saldoInicial) {
this.numeroCuenta = numeroCuenta;
this.titular = titular;
this.saldo = saldoInicial;
}
// Lógica compartida — no se repite en cada subclase
public void depositar(double monto) {
if (monto <= 0) throw new IllegalArgumentException("Monto inválido");
saldo += monto;
historial.add("Depósito: +" + monto);
}
// Método abstracto — cada tipo de cuenta retira de forma diferente
public abstract boolean retirar(double monto);
// Método abstracto — cada cuenta calcula su comisión diferente
public abstract double calcularComisionMensual();
public double getSaldo() { return saldo; }
protected void setSaldo(double saldo) { this.saldo = saldo; }
public List<String> getHistorial() { return Collections.unmodifiableList(historial); }
}
public class CuentaAhorros extends CuentaBancaria {
private final double tasaInteres;
public CuentaAhorros(String numero, String titular, double saldo, double tasa) {
super(numero, titular, saldo);
this.tasaInteres = tasa;
}
@Override
public boolean retirar(double monto) {
if (monto > getSaldo()) {
System.out.println("Saldo insuficiente");
return false;
}
setSaldo(getSaldo() - monto);
return true;
}
@Override
public double calcularComisionMensual() { return 0.0; } // sin comisión
}
public class CuentaCorriente extends CuentaBancaria {
private final double limiteSobregiro;
public CuentaCorriente(String numero, String titular, double saldo, double sobregiro) {
super(numero, titular, saldo);
this.limiteSobregiro = sobregiro;
}
@Override
public boolean retirar(double monto) {
if (monto > getSaldo() + limiteSobregiro) return false;
setSaldo(getSaldo() - monto);
return true;
}
@Override
public double calcularComisionMensual() { return 5.99; }
}✅ 2. Cuando quieres compartir lógica común (Template Method Pattern)
// El Template Method es un patrón donde la clase abstracta define
// el algoritmo general y las subclases implementan los pasos específicos
public abstract class GeneradorInforme {
// El método plantilla — define el algoritmo general
// Es final para que las subclases no cambien el flujo
public final void generarInforme() {
String titulo = obtenerTitulo(); // paso 1: abstracto
List<String> datos = recopilarDatos(); // paso 2: abstracto
String formato = formatear(datos); // paso 3: concreto (compartido)
guardar(titulo, formato); // paso 4: concreto (compartido)
System.out.println("Informe '" + titulo + "' generado");
}
// Los pasos específicos — cada subclase los implementa diferente
protected abstract String obtenerTitulo();
protected abstract List<String> recopilarDatos();
// Pasos compartidos — implementación en la clase base
private String formatear(List<String> datos) {
return String.join("\n", datos);
}
private void guardar(String titulo, String contenido) {
// lógica de guardado común
System.out.println("Guardando: " + titulo);
}
}
public class InformeVentas extends GeneradorInforme {
@Override
protected String obtenerTitulo() { return "Informe de Ventas Q2 2026"; }
@Override
protected List<String> recopilarDatos() {
return Arrays.asList("Ventas: $45,000", "Unidades: 320", "Margen: 32%");
}
}
public class InformeInventario extends GeneradorInforme {
@Override
protected String obtenerTitulo() { return "Estado del Inventario"; }
@Override
protected List<String> recopilarDatos() {
return Arrays.asList("Total: 1,250 productos", "Sin stock: 12", "Crítico: 45");
}
}Cuándo usar Interfaz
✅ 1. Cuando clases no relacionadas necesitan el mismo comportamiento
// Comparable — la interfaz más usada del JDK
// Perro, Producto y Factura no tienen NADA en común
// pero todos pueden ser comparables
public interface Comparable<T> {
int compareTo(T otro);
}
public class Producto implements Comparable<Producto> {
private String nombre;
private double precio;
@Override
public int compareTo(Producto otro) {
return Double.compare(this.precio, otro.precio);
}
}
public class Empleado implements Comparable<Empleado> {
private String nombre;
private double salario;
@Override
public int compareTo(Empleado otro) {
return this.nombre.compareTo(otro.nombre);
}
}
// Ahora ambos funcionan con Collections.sort(), TreeSet, etc.
List<Producto> productos = new ArrayList<>(...);
Collections.sort(productos); // usa compareTo() de Producto
TreeSet<Empleado> empleados = new TreeSet<>(...); // ordenados automáticamente✅ 2. Cuando una clase necesita cumplir múltiples contratos
// Java no permite herencia múltiple de clases
// pero sí de interfaces — clave para el diseño flexible
public interface Serializable { } // puede serializarse
public interface Cloneable { } // puede clonarse
public interface Comparable<T> { ... } // puede compararse
public interface AutoCloseable { // puede cerrarse automáticamente
void close() throws Exception;
}
// Una clase puede cumplir todos los contratos a la vez
public class ConexionBD implements AutoCloseable, Serializable {
@Override
public void close() {
System.out.println("Conexión cerrada");
}
}
// Usado con try-with-resources gracias a AutoCloseable:
try (ConexionBD conn = new ConexionBD()) {
// usar la conexión
} // conn.close() se llama automáticamente✅ 3. Para definir APIs — el contrato entre módulos
// Las interfaces son el contrato entre el que provee y el que consume
// Contrato del repositorio — el servicio solo conoce esta interfaz
public interface ProductoRepositorio {
Optional<Producto> findById(int id);
List<Producto> findAll();
List<Producto> findByCategoria(String categoria);
Producto save(Producto producto);
void deleteById(int id);
}
// Implementación MySQL — el servicio no sabe que existe
public class ProductoRepositorioMySQL implements ProductoRepositorio {
// implementación con MySQL
}
// Implementación en memoria — para tests
public class ProductoRepositorioMemoria implements ProductoRepositorio {
private final Map<Integer, Producto> datos = new HashMap<>();
// implementación con HashMap — sin BD
}
// El servicio solo depende de la interfaz — no de la implementación
public class ProductoServicio {
private final ProductoRepositorio repositorio;
public ProductoServicio(ProductoRepositorio repositorio) {
this.repositorio = repositorio; // inyección de dependencia
}
public List<Producto> obtenerActivos() {
return repositorio.findAll().stream()
.filter(Producto::isActivo)
.collect(Collectors.toList());
}
}
// En producción: usa MySQL
new ProductoServicio(new ProductoRepositorioMySQL());
// En tests: usa la implementación en memoria (rápido, sin BD real)
new ProductoServicio(new ProductoRepositorioMemoria());El patrón combinado — lo mejor de ambos mundos
En la práctica profesional, las soluciones más elegantes combinan interfaces con clases abstractas. La interfaz define el contrato y la clase abstracta proporciona una implementación parcial reutilizable.
// PATRÓN: Interface + AbstractClass + Concrete class
// 1. La INTERFAZ define el contrato completo
public interface ColeccionOrdenada<T extends Comparable<T>> {
void agregar(T elemento);
void eliminar(T elemento);
T obtenerMinimo();
T obtenerMaximo();
List<T> obtenerOrdenados();
int tamaño();
boolean contiene(T elemento);
}
// 2. La CLASE ABSTRACTA implementa lo que es común y deja lo específico
public abstract class ColeccionOrdenadaBase<T extends Comparable<T>>
implements ColeccionOrdenada<T> {
// Estado compartido
protected final List<T> elementos = new ArrayList<>();
// Implementación compartida de los métodos "fáciles"
@Override
public int tamaño() { return elementos.size(); }
@Override
public boolean contiene(T elemento) { return elementos.contains(elemento); }
@Override
public List<T> obtenerOrdenados() {
List<T> copia = new ArrayList<>(elementos);
Collections.sort(copia);
return copia;
}
@Override
public T obtenerMinimo() {
if (elementos.isEmpty()) throw new NoSuchElementException();
return Collections.min(elementos);
}
@Override
public T obtenerMaximo() {
if (elementos.isEmpty()) throw new NoSuchElementException();
return Collections.max(elementos);
}
// Deja para las subclases solo lo que cambia
@Override
public abstract void agregar(T elemento);
@Override
public abstract void eliminar(T elemento);
}
// 3. Las CLASES CONCRETAS solo implementan lo diferente
public class ColeccionSinDuplicados<T extends Comparable<T>>
extends ColeccionOrdenadaBase<T> {
@Override
public void agregar(T elemento) {
if (!contiene(elemento)) { // sin duplicados
elementos.add(elemento);
}
}
@Override
public void eliminar(T elemento) {
elementos.remove(elemento);
}
}
public class ColeccionConDuplicados<T extends Comparable<T>>
extends ColeccionOrdenadaBase<T> {
@Override
public void agregar(T elemento) {
elementos.add(elemento); // permite duplicados
}
@Override
public void eliminar(T elemento) {
elementos.remove(elemento); // elimina la primera ocurrencia
}
}
// Resultado: código mínimo en las clases concretas
// La interfaz garantiza el contrato
// La clase abstracta evita repetición
// Las concretas solo definen lo que las diferenciaInterfaces del JDK que debes conocer
// Las interfaces más usadas en el ecosistema Java
// Comparable<T> — ordenamiento natural
// Implementar cuando los objetos tienen un orden natural (precio, fecha...)
class Producto implements Comparable<Producto> {
public int compareTo(Producto otro) { ... }
}
// Comparator<T> — ordenamiento externo sin tocar la clase
// Útil cuando no controlas la clase o quieres múltiples criterios de orden
Comparator<Producto> porNombre = Comparator.comparing(Producto::getNombre);
Comparator<Producto> porPrecio = Comparator.comparingDouble(Producto::getPrecio);
// Iterable<T> — permite usar el for-each
// Implementar para que tu colección sea iterable con for (X x : miColeccion)
class MiColeccion<T> implements Iterable<T> {
public Iterator<T> iterator() { ... }
}
// AutoCloseable — recursos que se cierran automáticamente
// Usar con try-with-resources
class MiRecurso implements AutoCloseable {
public void close() { ... } // se llama automáticamente al salir del try
}
// Runnable y Callable — para concurrencia
Runnable tarea = () -> System.out.println("Ejecutando en otro hilo");
Callable<Integer> tareaConResultado = () -> calcularAlgo();
// Serializable — para persistencia y transferencia
class MiObjeto implements Serializable {
private static final long serialVersionUID = 1L;
// campos...
}Errores comunes
❌ Usar clase abstracta cuando no hay jerarquía real
// ❌ Clase abstracta para "agrupar" clases que no comparten estado ni lógica
abstract class Forma { abstract double area(); }
class Cuadrado extends Forma { ... }
class Triangulo extends Forma { ... }
// Si Cuadrado y Triangulo no comparten NADA (ni estado ni lógica),
// una interfaz es más apropiada:
// ✅
interface Forma { double area(); double perimetro(); }❌ Ignorar los default methods de las interfaces
// ❌ Duplicar lógica en cada implementación
interface Notificador {
void enviar(String mensaje);
// sin default — cada implementación repite el mismo log
}
// ✅ Usar default para lógica común
interface Notificador {
void enviar(String mensaje);
default void enviarConLog(String mensaje) {
System.out.println("[LOG] Enviando: " + mensaje);
enviar(mensaje);
System.out.println("[LOG] Enviado correctamente");
}
}❌ Confundir "no puedo instanciar" con "no tiene constructor"
// Las clases abstractas SÍ tienen constructor
// Las subclases lo llaman con super()
abstract class Vehiculo {
private String matricula;
public Vehiculo(String matricula) { // ← constructor en clase abstracta
this.matricula = matricula;
}
}
class Coche extends Vehiculo {
public Coche(String matricula) {
super(matricula); // ← llama al constructor del padre
}
}El árbol de decisión — cómo elegir en 4 preguntas
¿Las clases van a compartir ATRIBUTOS DE INSTANCIA (estado)?
SÍ → Clase abstracta (las interfaces no tienen estado)
NO → continúa →
¿Las clases pertenecen a la MISMA JERARQUÍA conceptual?
SÍ → Clase abstracta (relación ES-UN: Perro ES-UN Animal)
NO → continúa →
¿Las clases necesitan cumplir MÚLTIPLES CONTRATOS de comportamiento?
SÍ → Interfaces (una clase puede implementar muchas)
NO → continúa →
¿El comportamiento es INDEPENDIENTE de la jerarquía?
SÍ → Interfaz (Nadable puede aplicar a Pato, Pez, Perro)
NO → Clase abstractaResumen: lo que aprendiste hoy
- ✅ La pregunta clave: ¿el objeto ES algo (clase abstracta) o PUEDE HACER algo (interfaz)?
- ✅ Las clases abstractas pueden tener atributos, constructores y métodos concretos — las interfaces no
- ✅ Una clase puede extender solo UNA clase abstracta pero implementar MÚLTIPLES interfaces
- ✅ Desde Java 8, las interfaces pueden tener métodos
defaultystatic - ✅ Desde Java 9, las interfaces pueden tener métodos
privatepara uso interno - ✅ Las interfaces funcionales tienen un solo método abstracto y se usan con lambdas
- ✅ Usa clase abstracta cuando compartes estado o lógica en una jerarquía de clases relacionadas
- ✅ Usa interfaz para definir contratos que clases no relacionadas pueden cumplir
- ✅ El patrón Interface + AbstractClass + Concrete es el más potente — cada nivel aporta lo suyo
- ✅ Las interfaces son la base del polimorfismo desacoplado y la inyección de dependencias
🧪 ¿Tienes los fundamentos de Java y POO bien sólidos?
Interfaces y clases abstractas son Java intermedio. Para dominarlos necesitas tener claros los conceptos de herencia, polimorfismo y encapsulamiento. Comprueba dónde estás:
👉 Test: Java Básico 👉 Test: POO Conceptos Base
¿Cuál de los dos te resultaba más confuso antes de leer este artículo: las interfaces o las clases abstractas? ¿Ya has tenido que elegir entre los dos en algún proyecto real? Cuéntanos en los comentarios 👇 — respondemos todos. ☕🚀
No hay comentarios todavía. Sé el primero en compartir tu opinión.