Saltar al contenido
abstractas-e-interfaces.java · devschool

Clases abstractas e interfaces en Java

Lección 17 de 26 · 11 min de lectura · Actualizado el

En esta lección
  1. Clases abstractas
  2. Interfaces
  3. Métodos default y static en interfaces
  4. Interfaces funcionales
  5. Un ejemplo real: Comparable<T>
  6. ¿Clase abstracta o interfaz?
  7. Clases e interfaces selladas (sealed)
  8. Errores frecuentes
  9. Resumen

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 Vehiculo no, porque no todos los vehículos se recargan.
  • Crear una clase VehiculoElectrico tampoco funciona bien: CocheElectrico ya hereda de Coche, 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 this va antes que otro.
  • Cero si son equivalentes en el orden.
  • Un número positivo si this va 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 abstractaInterfaz
Relación“Es un” (un coche es un vehículo)“Puede” o “sabe hacer” (es recargable)
Cuántas por claseSolo una (extends)Todas las que quieras (implements)
AtributosSí, de cualquier tipoSolo constantes static final
ConstructoresSíNo
Métodos con códigoSí, normalesSolo default, static y private
Modificadores de métodosCualquieraPúblicos (salvo los private auxiliares)
Cuándo usarlaClases muy relacionadas que comparten estado y códigoCapacidades 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 new de 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 sin public, no compila.
  • Usar extends con una interfaz en una clase (o implements con una clase). Clase hereda clase con extends; clase implementa interfaz con implements; una interfaz sí puede ampliar otra interfaz con extends.
  • Devolver solo 1 o -1 “a mano” en compareTo y olvidar el caso de igualdad. Usa Integer.compare, Double.compare o el compareTo de String.
  • Crear interfaces con veinte métodos. Es mejor varias interfaces pequeñas y concretas (Recargable, Alquilable) que una enorme.

Resumen

ConceptoIdea clave
abstract classPlantilla que no se instancia; puede tener estado y código
Método abstractSin cuerpo; las subclases concretas deben implementarlo
interfaceContrato de métodos públicos; sin estado
implements A, BUna clase puede implementar varias interfaces
defaultMétodo con cuerpo en una interfaz, heredado por quien la implementa
static en interfazUtilidad que se llama con el nombre de la interfaz
Interfaz funcionalUn solo método abstracto; se puede usar con lambdas
Comparable<T>Orden natural con compareTo; permite Collections.sort
sealed / permitsLimita 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

[Java] Si Vehiculo es una clase abstracta, ¿qué ocurre con esta línea?
Vehiculo v = new Vehiculo();

[Java] ¿Qué imprime este código?
interface Saludo {
    String nombre();
    default String saludar() { return "Hola, " + nombre(); }
}
class Es implements Saludo {
    public String nombre() { return "Ana"; }
}

System.out.println(new Es().saludar());

[Java] ¿Qué imprime este código?
record Alumno(String nombre, double nota) implements Comparable<Alumno> {
    public int compareTo(Alumno otro) {
        return Double.compare(nota, otro.nota);
    }
}

List<Alumno> lista = new ArrayList<>(List.of(
        new Alumno("Ana", 7), new Alumno("Luis", 5), new Alumno("Eva", 9)));
Collections.sort(lista);
for (Alumno a : lista) System.out.print(a.nombre() + " ");

[Java] ¿Cuál de estas afirmaciones es correcta en Java?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.