Clases abstractas e interfaces en Java
En esta lección
Con la herencia aprendiste a crear subclases a partir de una clase existente. Pero a veces la clase padre es tan general que no tiene sentido crear objetos de ella: nadie compra “un vehículo”, compra un coche o una moto. Para esos casos Java ofrece las clases abstractas. Y cuando quieres que clases muy distintas compartan una capacidad (poder recargarse, poder alquilarse, poder ordenarse), usas interfaces.
En esta lección construiremos la flota de una empresa de alquiler de vehículos: coches, motos, patinetes eléctricos y coches eléctricos.
Clases abstractas
Todos los vehículos tienen matrícula y velocidad máxima, y todos tienen un número de ruedas y un coste por kilómetro. Pero cada tipo lo calcula de una forma, y un “vehículo genérico” no tiene un valor razonable para esas cosas. La solución es declarar la clase y esos métodos como abstract:
public abstract class Vehiculo {
private String matricula;
protected int velocidadMaxima;
public Vehiculo(String matricula, int velocidadMaxima) {
this.matricula = matricula;
this.velocidadMaxima = velocidadMaxima;
}
public String getMatricula() { return matricula; }
public int getVelocidadMaxima() { return velocidadMaxima; }
// Métodos abstractos: sin cuerpo, terminan en punto y coma
public abstract int numeroRuedas();
public abstract double costePorKm();
// Método normal que usa uno abstracto
public double costeViaje(double km) {
return km * costePorKm();
}
}
Una clase abstracta:
- No se puede instanciar:
new Vehiculo(...)da error. - Puede tener atributos, constructores y métodos normales, igual que cualquier clase.
- Puede tener métodos abstractos: solo la firma, sin cuerpo. Es una promesa: “todas mis subclases tendrán este método”.
Vehiculo v = new Vehiculo("0000 AAA", 100);
// error: Vehiculo is abstract; cannot be instantiated
Subclases concretas
Una subclase que no sea abstracta está obligada a implementar todos los métodos abstractos:
public class Coche extends Vehiculo {
public Coche(String matricula) {
super(matricula, 180);
}
@Override
public int numeroRuedas() { return 4; }
@Override
public double costePorKm() { return 0.12; }
}
Coche coche = new Coche("1234 ABC");
System.out.println(coche.numeroRuedas()); // 4
System.out.println(coche.costeViaje(100)); // 12.0
Si se te olvida alguno, el compilador te avisa:
public class Barco extends Vehiculo { }
// error: Barco is not abstract and does not override abstract method numeroRuedas() in Vehiculo
Fíjate en costeViaje: está escrito una sola vez en Vehiculo, pero funciona con cualquier subclase porque llama a costePorKm(), que cada una define a su manera. La clase abstracta pone el esqueleto y las subclases rellenan los huecos. Y aunque no se puedan crear objetos de ella, una clase abstracta sí tiene constructor: lo usan las subclases con super(...).
Interfaces
Ahora la empresa incorpora vehículos eléctricos. Un patinete eléctrico y un coche eléctrico se recargan; un coche de gasolina, no. ¿Dónde ponemos recargar()?
- En
Vehiculono, porque no todos los vehículos se recargan. - Crear una clase
VehiculoElectricotampoco funciona bien:CocheElectricoya hereda deCoche, y en Java solo hay una superclase.
Una interfaz define un conjunto de métodos que una clase se compromete a tener, sin decir cómo. Es un contrato o una capacidad:
public interface Recargable {
void recargar(int porcentaje);
int getBateria();
}
Los métodos de una interfaz son public y abstract de forma automática, así que no hace falta escribirlo.
implements
Una clase implementa una interfaz con implements y debe escribir todos sus métodos:
public class Patinete extends Vehiculo implements Recargable {
private int bateria;
public Patinete(String matricula, int bateria) {
super(matricula, 25);
this.bateria = bateria;
}
@Override public int numeroRuedas() { return 2; }
@Override public double costePorKm() { return 0.01; }
@Override
public void recargar(int porcentaje) {
bateria = Math.min(100, bateria + porcentaje);
}
@Override
public int getBateria() { return bateria; }
}
Varias interfaces a la vez
Una clase solo puede heredar de una clase, pero puede implementar tantas interfaces como quiera, separadas por comas. Añadimos otra capacidad:
public interface Alquilable {
double precioPorHora();
}
public class CocheElectrico extends Coche implements Recargable, Alquilable {
private int bateria;
public CocheElectrico(String matricula, int bateria) {
super(matricula);
this.bateria = bateria;
}
@Override public double costePorKm() { return 0.04; }
@Override public void recargar(int porcentaje) {
bateria = Math.min(100, bateria + porcentaje);
}
@Override public int getBateria() { return bateria; }
@Override public double precioPorHora() { return 12.5; }
}
Un CocheElectrico es un Coche, es un Vehiculo, es Recargable y es Alquilable. Puedes guardarlo en una variable de cualquiera de esos tipos:
CocheElectrico tesla = new CocheElectrico("5678 DEF", 60);
Recargable r = tesla;
Alquilable a = tesla;
System.out.println(tesla.costeViaje(100)); // 4.0
System.out.println(r.getBateria()); // 60
System.out.println(a.precioPorHora()); // 12.5
A través de r solo ves los métodos de Recargable, y a través de a los de Alquilable. Esta idea se desarrolla en polimorfismo.
Métodos default y static en interfaces
Desde Java 8 una interfaz puede incluir código en dos tipos de métodos.
Métodos default
Un método default tiene cuerpo, y todas las clases que implementan la interfaz lo reciben sin escribirlo (aunque pueden sobrescribirlo):
public interface Recargable {
void recargar(int porcentaje);
int getBateria();
default boolean necesitaCarga() {
return getBateria() < 20;
}
}
Patinete p = new Patinete("PAT-01", 15);
System.out.println(p.necesitaCarga()); // true
p.recargar(90);
System.out.println(p.getBateria()); // 100
System.out.println(p.necesitaCarga()); // false
Los métodos default se inventaron para poder añadir métodos a una interfaz ya publicada sin romper todas las clases que la implementaban.
Métodos static
Un método static de una interfaz es una utilidad relacionada con ella. Se llama con el nombre de la interfaz:
public interface Recargable {
// ...
static String nivel(int bateria) {
return bateria >= 50 ? "alto" : "bajo";
}
}
System.out.println(Recargable.nivel(p.getBateria())); // alto
Constantes
Los atributos de una interfaz son siempre public static final, es decir, constantes:
public interface Recargable {
int CARGA_MAXIMA = 100; // equivale a public static final int
// ...
}
// En Patinete: bateria = Math.min(CARGA_MAXIMA, bateria + porcentaje);
System.out.println(Recargable.CARGA_MAXIMA); // 100
Una interfaz no puede tener atributos normales ni constructores: no guarda estado.
Interfaces funcionales
Una interfaz con un único método abstracto se llama interfaz funcional. Tienen algo especial: se pueden implementar con una lambda, una forma muy corta de escribir una función:
Comparator<String> porLongitud = (x, y) -> Integer.compare(x.length(), y.length());
List<String> nombres = new ArrayList<>(List.of("Luis", "Ana", "Beatriz"));
nombres.sort(porLongitud);
System.out.println(nombres); // [Ana, Luis, Beatriz]
Java trae muchas ya hechas (Comparator, Runnable, Predicate, Function…). Las estudiarás a fondo en lambdas y streams.
Un ejemplo real: Comparable<T>
Java incluye muchas interfaces que tus clases pueden implementar para “encajar” con el resto de la biblioteca estándar. La más usada es Comparable<T>, que define el orden natural de los objetos. Tiene un solo método:
int compareTo(T otro);
Debe devolver:
- Un número negativo si
thisva antes queotro. - Cero si son equivalentes en el orden.
- Un número positivo si
thisva después.
String ya lo implementa (orden alfabético), y por eso Arrays.sort sabe ordenar cadenas:
System.out.println("Ana".compareTo("Bea")); // -1
String[] nombres = {"Luis", "ana", "Bea"};
Arrays.sort(nombres);
System.out.println(Arrays.toString(nombres)); // [Bea, Luis, ana]
(Las mayúsculas van antes que las minúsculas porque se compara el código de cada carácter.)
Para ordenar nuestra flota por velocidad máxima, hacemos que Vehiculo implemente Comparable<Vehiculo>:
public abstract class Vehiculo implements Comparable<Vehiculo> {
// ...
@Override
public int compareTo(Vehiculo otro) {
return Integer.compare(this.velocidadMaxima, otro.velocidadMaxima);
}
@Override
public String toString() {
return getClass().getSimpleName() + " " + getMatricula();
}
}
List<Vehiculo> flota = new ArrayList<>(List.of(
new Coche("1234 ABC"), new Patinete("PAT-01", 15),
new Moto("9999 XYZ"), new CocheElectrico("5678 DEF", 60)));
Collections.sort(flota);
System.out.println(flota);
// [Patinete PAT-01, Moto 9999 XYZ, Coche 1234 ABC, CocheElectrico 5678 DEF]
Integer.compare(a, b) devuelve negativo, cero o positivo según a sea menor, igual o mayor que b. Es más seguro que restar (a - b), que puede desbordarse con números muy grandes. Para invertir el orden, cambia el orden de los argumentos.
¿Clase abstracta o interfaz?
Las dos sirven para definir algo que otras clases completan. La diferencia está en qué representan:
| Clase abstracta | Interfaz | |
|---|---|---|
| Relación | “Es un” (un coche es un vehículo) | “Puede” o “sabe hacer” (es recargable) |
| Cuántas por clase | Solo una (extends) | Todas las que quieras (implements) |
| Atributos | Sí, de cualquier tipo | Solo constantes static final |
| Constructores | Sí | No |
| Métodos con código | Sí, normales | Solo default, static y private |
| Modificadores de métodos | Cualquiera | Públicos (salvo los private auxiliares) |
| Cuándo usarla | Clases muy relacionadas que comparten estado y código | Capacidades que comparten clases sin relación entre sí |
Regla práctica: empieza por una interfaz. Si ves que las clases que la implementan repiten atributos y código, añade una clase abstracta que implemente la interfaz y reúna lo común. Así funcionan muchas partes de la propia biblioteca de Java.
Clases e interfaces selladas (sealed)
Desde Java 17 puedes limitar qué clases pueden heredar de una clase o implementar una interfaz. Se hace con sealed y permits:
public sealed abstract class Vehiculo permits Coche, Moto, Patinete { ... }
public non-sealed class Coche extends Vehiculo { ... } // abierta: CocheElectrico puede heredar
public final class Moto extends Vehiculo { ... } // cerrada
public final class Patinete extends Vehiculo { ... } // cerrada
Cada subclase permitida debe declararse final, sealed o non-sealed; si no, el compilador da el error sealed, non-sealed or final modifiers expected. Y ninguna otra clase puede heredar de Vehiculo.
¿Para qué sirve? El compilador sabe que no existen otros tipos, así que un switch puede cubrirlos todos sin default:
static String tipo(Vehiculo v) {
return switch (v) {
case Coche c -> "coche";
case Moto m -> "moto";
case Patinete p -> "patinete";
};
}
Si mañana añades Furgoneta a permits y olvidas actualizar el switch, no compilará. Es útil cuando el conjunto de tipos está cerrado por naturaleza, como las formas de pago de una tienda.
Errores frecuentes
- Intentar hacer
newde una clase abstracta o de una interfaz. Crea un objeto de una subclase concreta. - No implementar todos los métodos abstractos. O los implementas o declaras la subclase también
abstract. - Reducir la visibilidad al implementar. Los métodos de una interfaz son
public; si en la clase los escribes sinpublic, no compila. - Usar
extendscon una interfaz en una clase (oimplementscon una clase). Clase hereda clase conextends; clase implementa interfaz conimplements; una interfaz sí puede ampliar otra interfaz conextends. - Devolver solo
1o-1“a mano” encompareToy olvidar el caso de igualdad. UsaInteger.compare,Double.compareo elcompareTodeString. - Crear interfaces con veinte métodos. Es mejor varias interfaces pequeñas y concretas (
Recargable,Alquilable) que una enorme.
Resumen
| Concepto | Idea clave |
|---|---|
abstract class | Plantilla que no se instancia; puede tener estado y código |
Método abstract | Sin cuerpo; las subclases concretas deben implementarlo |
interface | Contrato de métodos públicos; sin estado |
implements A, B | Una clase puede implementar varias interfaces |
default | Método con cuerpo en una interfaz, heredado por quien la implementa |
static en interfaz | Utilidad que se llama con el nombre de la interfaz |
| Interfaz funcional | Un solo método abstracto; se puede usar con lambdas |
Comparable<T> | Orden natural con compareTo; permite Collections.sort |
sealed / permits | Limita qué clases pueden heredar (Java 17+) |
En la siguiente lección juntarás todo esto con el polimorfismo: tratar objetos distintos a través de un mismo tipo.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.