Interfaces en Java — práctica con una pasarela de pagos, un carrito y un mando de dispositivos (y la clase abstracta que no pudo con dos padres)

Cuando me pidieron modelar un mando a distancia para una tele, un altavoz y una linterna, pensé: «fácil, clases abstractas». Creé Aparato (que se enciende y se apaga), después ConSonido (que reproduce música) y ConRed (que se conecta al WiFi). Hasta ahí, todo bien. Entonces llegó la tele, que enciende, suena y se conecta, y escribí sin pensar class Television extends Aparato, ConSonido, ConRed. Java me contestó con un seco '{' expected: una clase solo puede tener un padre.

Intenté apañarlo encadenando (ConRed extends ConSonido extends Aparato), pero entonces la linterna, que solo se enciende, acabó heredando un reproducir() que no quería ni sabía hacer. Ahí entendí para qué existen las interfaces: para describir capacidades sueltas que cada clase elige combinar. Esa es la columna vertebral de los tres programas de hoy.

Si ya has leído la teoría de interfaces, este artículo es para aplicarla: una pasarela de pagos que prueba medios de pago en orden, una tienda cuyo carrito acepta cualquier producto y cualquier medio de pago, y un mando que controla dispositivos con capacidades distintas. Son de dificultad media y trabajan con situaciones que se parecen a las de verdad. Cada uno trae el código comentado, la salida y las dudas que suelen surgir.

Antes de empezar

Conviene tener claro:

  • Que una clase implementa una interfaz con implements y debe sobrescribir todos sus métodos abstractos, siempre con public.
  • Que una variable de tipo interfaz (Pagable p) puede guardar cualquier objeto cuya clase la implemente, y solo «ve» los métodos de la interfaz.
  • Que un método default trae un cuerpo que las clases pueden usar o sobrescribir, y que si dos interfaces traen el mismo default, la clase tiene que decidir con Interfaz.super.metodo().
  • Que instanceof sirve para preguntar si un objeto cumple una capacidad antes de usarla.

Si alguno te suena dudoso, repasa la teoría antes de seguir.

Puedes meter todos los ficheros del artículo en una misma carpeta y compilarlos juntos. Los programas 1 y 2 comparten Pagable, Tarjeta y Bizum: el programa 2 reutiliza esas tres clases tal cual. El programa 3 va aparte.

Programa 1: pasarela de pagos

Una tienda online acepta varios medios de pago: Bizum, tarjeta y efectivo en tienda. Cuando llega una compra, la pasarela los prueba en orden y cobra con el primero que pueda. La tarjeta cobra una comisión del 1 %; los otros dos medios, no.

La idea

Todos los medios tienen que poder hacer dos cosas: intentar pagar un importe y decir cómo se llaman. Eso es el contrato, la interfaz Pagable. La pasarela solo conoce ese contrato, así que mañana podrás añadir un nuevo medio sin tocarla.

La comisión es un caso aparte: casi todos los medios no la cobran, pero la tarjeta sí. Si fuera abstracta, Bizum y Efectivo tendrían que escribir return 0 cada uno. Por eso es un método default que devuelve 0 y que la tarjeta sobrescribe.

Antes de programar, piensa en qué necesita saber Pasarela de cada medio: si puede pagar, cómo se llama y cuánto cobra de comisión. Ni una cosa más.

El código

Pagable.java

public interface Pagable {

    boolean pagar(double importe);
    String nombre();

    // Por defecto no hay comisión; quien la cobre, la sobrescribe
    default double comision(double importe) {
        return 0;
    }
}

Tarjeta.java

public class Tarjeta implements Pagable {

    private double limite;

    public Tarjeta(double limite) {
        this.limite = limite;
    }

    @Override
    public double comision(double importe) {
        return importe * 0.01;                      // 1 % de comisión
    }

    @Override
    public boolean pagar(double importe) {
        double total = importe + comision(importe);
        if (total > limite) {
            return false;
        }
        limite -= total;
        return true;
    }

    @Override
    public String nombre() {
        return "Tarjeta";
    }
}

Bizum.java

public class Bizum implements Pagable {

    private double saldo;

    public Bizum(double saldo) {
        this.saldo = saldo;
    }

    @Override
    public boolean pagar(double importe) {
        if (importe > saldo) {
            return false;
        }
        saldo -= importe;
        return true;
    }

    @Override
    public String nombre() {
        return "Bizum";
    }
}

Efectivo.java

public class Efectivo implements Pagable {

    private double cartera;

    public Efectivo(double cartera) {
        this.cartera = cartera;
    }

    @Override
    public boolean pagar(double importe) {
        if (importe > cartera) {
            return false;
        }
        cartera -= importe;
        return true;
    }

    @Override
    public String nombre() {
        return "Efectivo";
    }
}

Pasarela.java

public class Pasarela {

    // Prueba los medios EN ORDEN y devuelve el primero que consiga cobrar (o null)
    static Pagable cobrar(Pagable[] medios, double importe) {
        for (Pagable medio : medios) {
            double total = importe + medio.comision(importe);
            if (medio.pagar(importe)) {
                System.out.printf("  %s: cobrado %.2f €%n", medio.nombre(), total);
                return medio;
            }
            System.out.printf("  %s: rechazado%n", medio.nombre());
        }
        return null;
    }

    public static void main(String[] args) {
        Pagable[] medios = { new Bizum(60), new Tarjeta(150), new Efectivo(25) };
        double[] compras = { 45, 120, 300 };

        for (int i = 0; i < compras.length; i++) {
            System.out.printf("Compra %d: %.2f €%n", i + 1, compras[i]);
            Pagable usado = cobrar(medios, compras[i]);
            if (usado == null) {
                System.out.println("  No se ha podido cobrar");
            }
        }
    }
}

Salida

Compra 1: 45,00 €
  Bizum: cobrado 45,00 €
Compra 2: 120,00 €
  Bizum: rechazado
  Tarjeta: cobrado 121,20 €
Compra 3: 300,00 €
  Bizum: rechazado
  Tarjeta: rechazado
  Efectivo: rechazado
  No se ha podido cobrar

Compruébalo a mano:

  • Compra 1 (45 €): Bizum tiene 60 €, así que paga él. Quedan 15 € en Bizum. Sin comisión: 45,00 €.
  • Compra 2 (120 €): Bizum solo tiene 15 € y la rechaza. La tarjeta cobra 120 + 1 % = 121,20 €, que cabe en su límite de 150.
  • Compra 3 (300 €): Bizum tiene 15 €, la tarjeta ya solo tiene 28,80 € de límite y el efectivo 25 €. Ninguno llega, y cobrar devuelve null.

Pasarela.cobrar recibe un Pagable[]. No importa si dentro hay tres clases distintas o veinte: el bucle llama a medio.pagar(...) y Java ejecuta el método del objeto real. Es el polimorfismo con tipo interfaz que viste en la teoría.

Preguntas que te puedes hacer

**¿Por qué comision es default y no abstracto?** Porque la mayoría de medios no cobran comisión. Si fuera abstracto, Bizum y Efectivo estarían obligados a escribir un comision que devuelve 0, y cada medio nuevo tendría que repetirlo. Con default, solo la tarjeta, que es la excepción, lo sobrescribe.

**¿Por qué cobrar recibe Pagable[] y no Tarjeta[]? Porque entonces solo aceptaría tarjetas. Con el tipo interfaz acepta cualquier cosa que cumpla el contrato, hoy y mañana. Es la idea central de las interfaces: programar contra el contrato, no contra la clase**.

**¿Por qué calculamos total antes de llamar a pagar?** Porque pagar devuelve solo un boolean, no cuánto ha cobrado. Para poder imprimir la cantidad con comisión, la calculamos nosotros usando el mismo comision que usa la tarjeta por dentro.

¿Y si el array está vacío o nadie puede pagar? cobrar devuelve null, y el main lo comprueba para escribir «No se ha podido cobrar». Es la forma de decir «no hay resultado» sin lanzar nada.

Usar el resultado de cobrar sin comprobar si es null. Si en la compra 3 escribieras System.out.println(usado.nombre()), el programa se caería con NullPointerException. Siempre que un método pueda devolver null, comprueba antes de usar el resultado.

Pruébalo tú

  1. Crea una clase Cheque que implemente Pagable, con una comisión fija de 2 €, y añádela al array **sin tocar Pasarela**. Fíjate en que no hace falta cambiar nada más: ahí está la gracia.
  2. Cambia el orden del array (Efectivo primero) y averigua qué medio cobra cada compra. ¿Cambia el resultado de la compra 2?

Programa 2: tienda con carrito

Ahora una tienda que vende tres tipos de producto: libros y camisetas (que se envían) y descargas (que no). El carrito suma los precios, añade los gastos de envío solo de lo físico y se puede pagar con cualquier medio del programa anterior.

La idea

Hay dos capacidades distintas y conviene separarlas:

  • Vendible: tiene nombre y precio. Todo lo que está en el carrito lo cumple.
  • Enviable: tiene gastos de envío. Solo lo cumplen los productos físicos.

Una descarga es Vendible pero no Enviable; un libro es las dos cosas. Si metiéramos gastosEnvio dentro de Vendible, la descarga tendría que implementar un método que no le corresponde.

Además, precioFinal es un método default que devuelve el precio completo; la camiseta, que tiene un 10 % de descuento, lo sobrescribe.

Para el total, el carrito recorre todos los artículos y suma precioFinal(). Para el envío, pregunta a cada uno si es Enviable con instanceof.

El código

Este programa reutiliza Pagable, Tarjeta y Bizum del anterior (si compilas en la misma carpeta, ya los tienes).

Vendible.java

public interface Vendible {

    String nombre();
    double precio();

    // Por defecto se paga el precio completo; los productos con descuento lo sobrescriben
    default double precioFinal() {
        return precio();
    }
}

Enviable.java

public interface Enviable {

    double gastosEnvio();
}

Libro.java

public class Libro implements Vendible, Enviable {

    private final String titulo;
    private final double precio;

    public Libro(String titulo, double precio) {
        this.titulo = titulo;
        this.precio = precio;
    }

    @Override
    public String nombre() { return titulo; }

    @Override
    public double precio() { return precio; }

    @Override
    public double gastosEnvio() { return 3.00; }
}

Camiseta.java

public class Camiseta implements Vendible, Enviable {

    private final String modelo;
    private final double precio;

    public Camiseta(String modelo, double precio) {
        this.modelo = modelo;
        this.precio = precio;
    }

    @Override
    public String nombre() { return modelo; }

    @Override
    public double precio() { return precio; }

    @Override
    public double precioFinal() {                  // 10 % de descuento
        return precio * 0.90;
    }

    @Override
    public double gastosEnvio() { return 4.50; }
}

Descargable.java

public class Descargable implements Vendible {     // digital: no es Enviable

    private final String titulo;
    private final double precio;

    public Descargable(String titulo, double precio) {
        this.titulo = titulo;
        this.precio = precio;
    }

    @Override
    public String nombre() { return titulo; }

    @Override
    public double precio() { return precio; }
}

Carrito.java

public class Carrito {

    private final Vendible[] articulos = new Vendible[10];
    private int cantidad = 0;

    public void anadir(Vendible v) {
        articulos[cantidad++] = v;
    }

    public double envio() {
        double envio = 0;
        for (int i = 0; i < cantidad; i++) {
            if (articulos[i] instanceof Enviable e) {     // solo los productos físicos
                envio += e.gastosEnvio();
            }
        }
        return envio;
    }

    public double total() {
        double total = envio();
        for (int i = 0; i < cantidad; i++) {
            total += articulos[i].precioFinal();
        }
        return total;
    }

    public void ticket() {
        for (int i = 0; i < cantidad; i++) {
            System.out.printf("%s · %.2f €%n", articulos[i].nombre(), articulos[i].precioFinal());
        }
        System.out.printf("Envío: %.2f €%n", envio());
        System.out.printf("TOTAL: %.2f €%n", total());
    }

    // El carrito no sabe si paga con tarjeta, Bizum o efectivo: solo que es Pagable
    public boolean pagar(Pagable medio) {
        boolean ok = medio.pagar(total());
        System.out.println("Pago con " + medio.nombre() + ": " + (ok ? "aceptado" : "rechazado"));
        return ok;
    }
}

Tienda.java

public class Tienda {

    public static void main(String[] args) {
        Carrito carrito = new Carrito();
        carrito.anadir(new Libro("Java para FP2", 24.90));
        carrito.anadir(new Camiseta("Camiseta SergioLearns", 20.00));
        carrito.anadir(new Descargable("Curso de Python", 15.00));

        carrito.ticket();

        // Se intenta con varios medios hasta que uno funcione
        Pagable[] medios = { new Tarjeta(50), new Bizum(100) };
        for (Pagable medio : medios) {
            if (carrito.pagar(medio)) {
                break;
            }
        }
    }
}

Salida

Java para FP2 · 24,90 €
Camiseta SergioLearns · 18,00 €
Curso de Python · 15,00 €
Envío: 7,50 €
TOTAL: 65,40 €
Pago con Tarjeta: rechazado
Pago con Bizum: aceptado

Compruébalo a mano:

  • Libro: 24,90 € · Camiseta: 20 € − 10 % = 18,00 € · Descarga: 15,00 €. Suma de productos: 57,90 €.
  • Envío: solo el libro (3,00 €) y la camiseta (4,50 €): 7,50 €. La descarga no cuenta.
  • Total: 57,90 + 7,50 = 65,40 € ✔.
  • Pago: la tarjeta tiene 50 € de límite, y 65,40 € más su comisión no caben → rechazado. Bizum tiene 100 € → aceptado, y el break impide seguir probando.

Preguntas que te puedes hacer

**¿Por qué Enviable es una interfaz aparte y no un método dentro de Vendible?** Porque no todos los vendibles se envían. Una descarga tendría que implementar gastosEnvio() devolviendo 0 o lanzando una excepción, que es justo lo que las interfaces pequeñas y separadas evitan. Cada interfaz, una capacidad.

**¿Por qué usamos instanceof Enviable e y no instanceof Libro || instanceof Camiseta?** Porque mañana puede llegar una Pegatina que también se envíe. Si el carrito pregunta por la capacidad (Enviable), esa nueva clase funciona sin tocar el carrito. Si pregunta por clases concretas, tendrías que acordarte de editarlo cada vez.

**¿Por qué precioFinal es default y no abstracto?** Porque casi todos los productos se venden al precio completo. Solo la camiseta lo sobrescribe para aplicar el descuento. El resto lo hereda sin escribir nada.

**¿Por qué Carrito.pagar recibe un Pagable?** Para que el carrito no sepa si se paga con tarjeta, Bizum o efectivo, y funcione igual con cualquier medio presente o futuro. El carrito depende del contrato, no de ninguna clase concreta.

El array del carrito tiene 10 huecos y anadir no comprueba nada. Si añades el undécimo producto, el programa se cae con ArrayIndexOutOfBoundsException. Cuando uses arrays de tamaño fijo, protege las entradas (o comprueba cantidad < articulos.length antes de añadir).

Pruébalo tú

  1. Crea una clase Pegatina que sea Vendible y Enviable (precio 2 €, envío 1 €) y añádela a la tienda **sin modificar Carrito**. Comprueba que el envío y el total se actualizan solos.
  2. Haz que anadir devuelva un boolean: true si cabía y false si el carrito estaba lleno. Prueba a meter 11 productos.

Programa 3: mando de dispositivos

El último programa es el mando del que te hablaba al principio. Tenemos tres dispositivos: una linterna (solo se enciende y se apaga), un altavoz (además reproduce música) y una televisión (además se conecta a una red y no reproduce nada hasta que la tiene). El mando los controla a todos con las mismas órdenes y cada dispositivo reacciona solo a lo que sabe hacer.

La idea

Tres capacidades, tres interfaces:

  • Encendible: se enciende, se apaga y dice si está encendido.
  • Reproducible: reproduce una pista y dice cuál suena.
  • Conectable: se conecta a una red y dice a cuál.

Cada dispositivo elige su combinación: la linterna implementa una, el altavoz dos y la televisión las tres. Algo que con clases abstractas era imposible.

Encendible y Reproducible tienen ambas un método default estado(). En las clases que implementan las dos hay que resolver el choque, combinando lo que dice cada una.

El Mando guarda un Encendible[], porque todo se enciende y se apaga. Para las órdenes que no son de todos (conectar, reproducir) pregunta primero con instanceof.

El código

Encendible.java

public interface Encendible {

    String nombre();
    void encender();
    void apagar();
    boolean estaEncendido();

    default String estado() {
        return estaEncendido() ? "encendido" : "apagado";
    }
}

Reproducible.java

public interface Reproducible {

    void reproducir(String pista);
    String pistaActual();                            // null si no suena nada

    default String estado() {
        return pistaActual() == null ? "en silencio" : "sonando \"" + pistaActual() + "\"";
    }
}

Conectable.java

public interface Conectable {

    void conectar(String red);
    String redActual();                              // null si no hay red
}

Linterna.java

public class Linterna implements Encendible {        // solo se enciende y se apaga

    private boolean encendida;

    @Override
    public String nombre() { return "Linterna"; }

    @Override
    public void encender() { encendida = true; }

    @Override
    public void apagar() { encendida = false; }

    @Override
    public boolean estaEncendido() { return encendida; }
}

Altavoz.java

public class Altavoz implements Encendible, Reproducible {

    private boolean encendido;
    private String pista;

    @Override
    public String nombre() { return "Altavoz"; }

    @Override
    public void encender() { encendido = true; }

    @Override
    public void apagar() {
        encendido = false;
        pista = null;
    }

    @Override
    public boolean estaEncendido() { return encendido; }

    @Override
    public void reproducir(String pista) {
        if (encendido) {
            this.pista = pista;
        }
    }

    @Override
    public String pistaActual() { return pista; }

    // Las dos interfaces tienen un default estado(): hay que decidir cómo combinarlos
    @Override
    public String estado() {
        return Encendible.super.estado() + ", " + Reproducible.super.estado();
    }
}

Television.java

public class Television implements Encendible, Reproducible, Conectable {

    private boolean encendida;
    private String pista;
    private String red;

    @Override
    public String nombre() { return "Televisión"; }

    @Override
    public void encender() { encendida = true; }

    @Override
    public void apagar() {
        encendida = false;
        pista = null;
    }

    @Override
    public boolean estaEncendido() { return encendida; }

    @Override
    public void conectar(String red) { this.red = red; }

    @Override
    public String redActual() { return red; }

    @Override
    public void reproducir(String pista) {
        if (encendida && red != null) {              // necesita estar encendida Y con red
            this.pista = pista;
        }
    }

    @Override
    public String pistaActual() { return pista; }

    @Override
    public String estado() {
        return Encendible.super.estado() + ", " + Reproducible.super.estado()
                + ", red: " + (red == null ? "sin conectar" : red);
    }
}

Mando.java

public class Mando {

    private final Encendible[] dispositivos;

    public Mando(Encendible[] dispositivos) {
        this.dispositivos = dispositivos;
    }

    public void encenderTodo() {
        for (Encendible d : dispositivos) {
            d.encender();
        }
    }

    public void apagarTodo() {
        for (Encendible d : dispositivos) {
            d.apagar();
        }
    }

    // Solo los que además son Conectable / Reproducible reciben la orden
    public void conectarTodo(String red) {
        for (Encendible d : dispositivos) {
            if (d instanceof Conectable c) {
                c.conectar(red);
            }
        }
    }

    public void sonar(String pista) {
        for (Encendible d : dispositivos) {
            if (d instanceof Reproducible r) {
                r.reproducir(pista);
            }
        }
    }

    public void mostrar() {
        for (Encendible d : dispositivos) {
            System.out.println("  " + d.nombre() + ": " + d.estado());
        }
    }

    public static void main(String[] args) {
        Mando mando = new Mando(new Encendible[] { new Linterna(), new Altavoz(), new Television() });

        System.out.println("Todo encendido, sin red:");
        mando.encenderTodo();
        mando.sonar("Bohemian Rhapsody");
        mando.mostrar();

        System.out.println("Con la red conectada:");
        mando.conectarTodo("WiFi-Casa");
        mando.sonar("Bohemian Rhapsody");
        mando.mostrar();

        System.out.println("Apagado:");
        mando.apagarTodo();
        mando.mostrar();
    }
}

Salida

Todo encendido, sin red:
  Linterna: encendido
  Altavoz: encendido, sonando "Bohemian Rhapsody"
  Televisión: encendido, en silencio, red: sin conectar
Con la red conectada:
  Linterna: encendido
  Altavoz: encendido, sonando "Bohemian Rhapsody"
  Televisión: encendido, sonando "Bohemian Rhapsody", red: WiFi-Casa
Apagado:
  Linterna: apagado
  Altavoz: apagado, en silencio
  Televisión: apagado, en silencio, red: WiFi-Casa

Compruébalo a mano:

  • Todo encendido, sin red: el altavoz suena enseguida; la tele está encendida, pero sin red no reproduce nada, por eso dice «en silencio».
  • Con la red conectada: la orden conectarTodo solo llega a la tele (la única Conectable). Al volver a pedir la pista, ahora sí suena. La linterna no cambia nada: no sabe reproducir ni conectarse.
  • Apagado: apagar borra la pista, así que el altavoz y la tele quedan «en silencio». La tele conserva su red, que es otro dato.

Preguntas que te puedes hacer

**¿Por qué Altavoz tiene que sobrescribir estado()?** Porque implementa dos interfaces con un default estado() y Java no sabe cuál elegir. Con Encendible.super.estado() y Reproducible.super.estado() llamamos a cada versión y las juntamos con una coma.

**¿Por qué Mando guarda Encendible[] y no un array de Object?** Porque con Encendible el mando puede llamar a encender() y apagar() directamente. Con Object tendrías que preguntar por el tipo en todas partes. El tipo interfaz dice justo lo que necesitas ver.

**¿Por qué conectarTodo pregunta instanceof Conectable c?** Porque no todos los dispositivos se conectan. La pregunta evita llamar a un método que la linterna ni conoce, y además deja el mando listo para cualquier dispositivo nuevo que algún día sea Conectable.

¿Por qué la televisión necesita red para sonar? Es una regla de su reproducir, no del contrato: cada clase decide cómo cumple lo que promete. La interfaz solo dice que existe reproducir; lo que hace por dentro es cosa de cada clase.

Si quitas el método estado() de Altavoz, el programa no compila: «types Encendible and Reproducible are incompatible; class Altavoz inherits unrelated defaults for estado()…». Dos default con el mismo nombre en dos interfaces te obligan a decidir en la clase.

Pruébalo tú

  1. Añade una interfaz ConVolumen con subir() y bajar(), hazla implementar por el altavoz y la televisión, y añade un método subirVolumen() al mando que solo afecte a los dispositivos que la cumplan.
  2. Crea una Lampara que implemente solo Encendible y añádela al mando. ¿Tienes que cambiar algo en Mando?

Errores típicos en esta práctica

  • **Olvidar public** al sobrescribir un método de la interfaz: «attempting to assign weaker access privileges; was public».
  • Dejar un método sin implementar: «is not abstract and does not override abstract method…». El mensaje te dice cuál falta.
  • Usar un método propio de la clase a través de una variable de tipo interfaz: «cannot find symbol». Antes, instanceof y conversión.
  • **No resolver el choque de dos default** con el mismo nombre en una clase que implementa ambas interfaces.
  • Hacer un cast sin comprobar con instanceof, que puede acabar en ClassCastException.
  • **Usar el resultado de un método que puede devolver null** sin comprobarlo.

Míralo paso a paso en Python Tutor

Python Tutor ejecuta el código línea a línea y dibuja variables, objetos y referencias. Elige Java. Aquí tienes una versión reducida del carrito del programa 2, con una variable de tipo interfaz, un instanceof y la comprobación de una segunda capacidad. Está escrita con el instanceof y el cast clásicos (sin pattern matching), que es más fácil de seguir.

public class Tutor {
    interface Vendible {
        double precio();
    }
    interface Enviable {
        double envio();
    }
    static class Libro implements Vendible, Enviable {
        public double precio() { return 24.90; }
        public double envio() { return 3.00; }
    }
    static class Descargable implements Vendible {
        public double precio() { return 15.00; }
    }
    public static void main(String[] args) {
        Vendible[] carrito = { new Libro(), new Descargable() };
        double total = 0;
        for (Vendible v : carrito) {
            total += v.precio();
            if (v instanceof Enviable) {
                Enviable e = (Enviable) v;
                total += e.envio();
            }
        }
        System.out.println("Total: " + total);
    }
}

Salida esperada:

Total: 42.9

Qué observar paso a paso:

  1. En Vendible[] carrito = ..., fíjate en que el array contiene dos referencias a objetos distintos, un Libro y un Descargable. Aunque el array es de tipo Vendible, los objetos conservan su clase real.
  2. En la primera vuelta, total += v.precio() entra en el precio() de **Libro**. Después, la condición v instanceof Enviable es verdadera: observa cómo aparece una variable nueva e que apunta al mismo objeto que v (dos nombres, un objeto).
  3. Al ejecutar total += e.envio(), total pasa de 24,9 a 27,9: el libro se envía.
  4. En la segunda vuelta, v apunta al Descargable. La condición v instanceof Enviable da falso, así que se salta el bloque y no se declara e. El total final sale 42,9.

Resumen

  • Una interfaz define un contrato de capacidades; una clase puede cumplir varias a la vez, lo que con la herencia es imposible.
  • Las variables, parámetros y arrays de tipo interfaz permiten escribir código que funciona con cualquier clase presente o futura que cumpla el contrato.
  • default evita repetir lo que casi todos hacen igual; quien sea distinto, lo sobrescribe.
  • instanceof pregunta por una capacidad antes de usarla; así el código no depende de clases concretas.
  • Si dos interfaces traen el mismo default, la clase decide con Interfaz.super.metodo().
  • Una interfaz pequeña y específica (Enviable) es mejor que una grande llena de métodos que no todos usan.

Checklist: ¿lo has practicado?

  • [ ] He compilado y ejecutado los tres programas.
  • [ ] He comprobado a mano al menos una salida de cada programa.
  • [ ] He añadido una clase nueva (Cheque, Pegatina o Lampara) sin tocar el código que la usa.
  • [ ] He provocado el error de default duplicado y sé arreglarlo con Interfaz.super.
  • [ ] He probado instanceof con interfaces y entiendo por qué es mejor que preguntar por clases concretas.
  • [ ] He mirado el programa de Python Tutor y he visto dos variables apuntando al mismo objeto.

Y ahora, a por los ejercicios: ejercicios de interfaces, con tres niveles, tres fragmentos para depurar y un chuletario al final. ¡Nos vemos allí!

Siguiente paso: ejerciciosInterfaces en Java — ejercicios con figuras, una biblioteca y sensores (y el cast que funcionó hasta que llegó una camiseta)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *