← CC2008

Polimorfismo via Interfaces

Semestre 02, 2026

Interfaz

Definición

Una interfaz es un tipo que declara un conjunto de métodos sin implementarlos. Funciona como un contrato, y toda clase que la implementa se compromete a definir esos métodos.


public interface IVolador {
    void volar();
}

Características

Sus métodos son implícitamente public y abstract
No tiene atributos de instancia, solo constantes public static final
No tiene constructores y no se puede instanciar con new
Una clase la implementa con la palabra clave implements
Una clase implementa varias interfaces, pero extiende una sola superclase
Desde Java 8 admite métodos default y static con cuerpo

Herencia e interfaz

Herencia: expresa una relación es-un entre una subclase y su superclase
Interfaz: expresa una capacidad, una relación puede-hacer
Consecuencia

Clases de ramas distintas de la jerarquía pueden implementar la misma interfaz y usarse a través del mismo tipo.

Tipo y habilidad

La lección anterior cerró con el límite de la herencia. Gengar es de tipo fantasma y veneno, y Java no permite que una clase extienda dos superclases.


class Gengar extends Pokemon implements IFantasma, IVeneno { }
Gengar

fantasma + veneno

Dragonite

dragón + volador

La superclase Pokemon


public abstract class Pokemon {
    protected String nombre;
    protected int nivel;
    protected int hp;

    public Pokemon(String nombre, int nivel, int hp) {
        this.nombre = nombre;
        this.nivel = nivel;
        this.hp = hp;
    }

    public abstract void atacar();

    public void mostrarFicha() {
        System.out.println(this.nombre + " | Nv. " + this.nivel
            + " | HP " + this.hp);
    }
}

El punto de partida

1Una superclase
  • Pokemon reúne lo que todo Pokémon comparte, el nombre, el nivel, los puntos de vida y la ficha.
2Subclases por tipo
  • PokemonElectrico y PokemonAgua extienden a Pokemon y definen su propio atacar.
3Interfaces por habilidad
  • Gengar suma sus dos tipos con implements. Dragonite se resuelve con la misma técnica.

Pokémon que vuelan

Problema

Se extenderá la Pokédex con Pokémon de dos tipos. Dragonite es de tipo dragón y volador, y Gyarados es de tipo agua y volador. El juego necesita un movimiento de vuelo que funcione con cualquier Pokémon capaz de volar.

  • Un Pokémon que no vuela, como Pikachu, no debe poder usar ese movimiento, y el error debe detectarse al compilar.
  • El recorrido de los voladores no debe consultar la especie de cada Pokémon.

Restricciones de diseño

1Una sola superclase
  • Dragonite necesita dos tipos y Java permite extender una sola clase.
2Gyarados ya tiene superclase
  • Gyarados extiende a PokemonAgua, así que el vuelo no puede venir de otra superclase.
3Un recorrido sin consultas de tipo
  • El recorrido no pregunta si cada elemento es un Dragonite o un Gyarados.

La capacidad común

Dragonite

extiende a Pokemon

Gyarados

extiende a PokemonAgua

Diseño

Dragonite y Gyarados pertenecen a ramas distintas de la jerarquía, pero ambos vuelan. Esa capacidad común se modela con la interfaz IVolador.

Las interfaces IVolador e IDragon


public interface IVolador {
    void volar();
}

public interface IDragon {
    void usarDragoaliento();
}

La jerarquía con interfaces

classDiagram
    class Pokemon {
        <<abstract>>
        #String nombre
        #int nivel
        #int hp
        +atacar() void
        +mostrarFicha() void
    }
    class PokemonElectrico {
        +atacar() void
    }
    class PokemonAgua {
        +atacar() void
    }
    class Gyarados {
        +volar() void
    }
    class Dragonite {
        +atacar() void
        +usarDragoaliento() void
        +volar() void
    }
    class IVolador {
        <<interface>>
        +volar() void
    }
    class IDragon {
        <<interface>>
        +usarDragoaliento() void
    }
    Pokemon <|-- PokemonElectrico
    Pokemon <|-- PokemonAgua
    Pokemon <|-- Dragonite
    PokemonAgua <|-- Gyarados
    IVolador <|.. Gyarados
    IVolador <|.. Dragonite
    IDragon <|.. Dragonite

Lectura del diagrama

La etiqueta interface marca el contrato
La línea punteada con triángulo indica implementación
La línea continua con triángulo indica herencia
IVolador atraviesa la jerarquía, la implementan clases de dos ramas distintas

Dragonite


public class Dragonite extends Pokemon
        implements IDragon, IVolador {

    public Dragonite(String nombre, int nivel, int hp) {
        super(nombre, nivel, hp);
    }

    @Override
    public void atacar() {
        System.out.println(this.nombre + " usa Hiperrayo.");
    }

    @Override
    public void usarDragoaliento() {
        System.out.println(this.nombre + " usa Dragoaliento.");
    }

    @Override
    public void volar() {
        System.out.println(this.nombre + " vuela sobre el océano.");
    }
}
Dragonite

Decisiones de diseño

Dragonite extiende una sola clase e implementa dos interfaces, separadas por coma
Debe definir atacar y los métodos de las dos interfaces, o no compila
@Override también aplica a los métodos que provienen de una interfaz
Los métodos son public, porque la implementación no puede reducir la visibilidad del contrato

Gyarados


public class Gyarados extends PokemonAgua implements IVolador {

    public Gyarados(String nombre, int nivel, int hp, int presion) {
        super(nombre, nivel, hp, presion);
    }

    @Override
    public void volar() {
        System.out.println(this.nombre + " se eleva sobre las olas.");
    }
}
Gyarados

Una superclase y un contrato

Herencia y contrato

Gyarados hereda atacar y bucear de PokemonAgua y solo agrega volar. Un Gyarados es un PokemonAgua y además cumple el contrato IVolador.

  • PokemonAgua no cambió ni una línea.
  • Squirtle y los demás Pokémon de agua siguen sin poder volar.

Los voladores


ArrayList<IVolador> voladores =
    new ArrayList<>();

voladores.add(new Dragonite(
    "Dragonite", 55, 91));
voladores.add(new Gyarados(
    "Gyarados", 40, 95, 120));

for (IVolador v : voladores) {
    v.volar();
}

// no compila, Pikachu no vuela
voladores.add(new PokemonElectrico(
    "Pikachu", 12, 35, 90));

Lo que muestra el recorrido

La lista se declara con el tipo de la interfaz y acepta cualquier clase que implemente IVolador
Dragonite y Gyarados conviven en la lista aunque sus superclases directas son distintas
Desde una referencia IVolador solo son visibles los métodos del contrato
Cada llamada a volar se resuelve en tiempo de ejecución según el objeto real
PokemonElectrico no implementa IVolador, así que el compilador rechaza a Pikachu

La salida


Dragonite vuela sobre el océano.
Gyarados se eleva sobre las olas.
Resultado

El recorrido no contiene un solo if. Un Pokémon nuevo que implemente IVolador entra a la lista sin modificarlo.

Métodos que reciben una interfaz


public static void usarVuelo(IVolador v, String destino) {
    v.volar();
    System.out.println("Llega a " + destino + ".");
}

usarVuelo(dragonite, "Ciudad Celeste");
usarVuelo(gyarados, "Isla Canela");
usarVuelo(pikachu, "Pueblo Paleta");   // no compila
Extensión

Un Charizard que implemente IVolador funciona con usarVuelo sin modificar el método.

Clase abstracta e interfaz

AspectoClase abstractaInterfaz
Palabra claveextendsimplements
Cuántas por claseunavarias
Atributos de instanciasíno, solo constantes
Constructoressíno
Métodos con cuerposísolo default y static
Relación que expresaes-unpuede-hacer

Criterio de elección

Clase abstracta

Pokemon reúne el estado y el comportamiento que comparte toda la jerarquía, como el nombre, el nivel y la ficha.

Interfaz

IVolador declara una capacidad que tienen solo algunos Pokémon, repartidos en ramas distintas de la jerarquía.

Interfaces de la biblioteca estándar

Java define interfaces que sus propias clases y métodos utilizan de forma polimórfica. Una clase nueva que las implementa se integra con esas herramientas sin modificarlas.

Comparable: define el orden natural de los objetos de una clase
Iterable: permite recorrer un objeto con el for mejorado
Iterator: entrega los elementos de un recorrido uno a la vez
La T es un parámetro de tipo, igual que en ArrayList<Pokemon>

Ordenar por nivel

Se solicita imprimir los Pokémon capturados ordenados de menor a mayor nivel.


Collections.sort(capturados);   // no compila
Por qué falla

Collections.sort no conoce la clase Pokemon ni sabe qué significa que un Pokémon vaya antes que otro. Solo ordena listas cuyos elementos implementen Comparable.

La interfaz Comparable


public interface Comparable<T> {
    int compareTo(T otro);
}
Negativo: this va antes que otro
Cero: ambos ocupan la misma posición en el orden
Positivo: this va después que otro
El valor exacto no importa, solo su signo

Pokemon implementa Comparable


public abstract class Pokemon implements Comparable<Pokemon> {
    protected String nombre;
    protected int nivel;
    protected int hp;

    // constructor, atacar y mostrarFicha sin cambios

    public int getNivel() {
        return this.nivel;
    }

    @Override
    public int compareTo(Pokemon otro) {
        return Integer.compare(this.nivel, otro.getNivel());
    }
}

Un orden para toda la jerarquía

classDiagram
    class Comparable~Pokemon~ {
        <<interface>>
        +compareTo(Pokemon otro) int
    }
    class Pokemon {
        <<abstract>>
        #String nombre
        #int nivel
        #int hp
        +getNivel() int
        +compareTo(Pokemon otro) int
    }
    class PokemonElectrico
    class PokemonAgua
    class Dragonite
    Comparable <|.. Pokemon
    Pokemon <|-- PokemonElectrico
    Pokemon <|-- PokemonAgua
    Pokemon <|-- Dragonite

Decisiones de diseño

Comparable<Pokemon> hace que compareTo reciba un Pokemon y no un Object
Las subclases heredan la implementación, así que un Dragonite se compara con un Squirtle
Integer.compare devuelve el signo correcto sin restar
compareTo invoca getNivel del otro objeto en lugar de leer sus atributos

Collections.sort


ArrayList<Pokemon> capturados = new ArrayList<>();

capturados.add(new PokemonElectrico("Jolteon", 30, 65, 400));
capturados.add(new PokemonElectrico("Pikachu", 12, 35, 90));
capturados.add(new Dragonite("Dragonite", 55, 91));
capturados.add(new PokemonAgua("Squirtle", 10, 44, 60));

Collections.sort(capturados);

for (Pokemon p : capturados) {
    p.mostrarFicha();
}

Squirtle | Nv. 10 | HP 44
Pikachu | Nv. 12 | HP 35
Jolteon | Nv. 30 | HP 65
Dragonite | Nv. 55 | HP 91

Polimorfismo en la biblioteca

Mecanismo

Collections.sort se escribió años antes que la clase Pokemon. Invoca compareTo a través de una referencia Comparable, y se ejecuta la versión que Pokemon definió y que todas sus subclases heredaron.

  • String, Integer y Double ya implementan Comparable.
  • Por eso una lista de nombres se ordena alfabéticamente con la misma llamada.

Desempate por nombre


public String getNombre() {
    return this.nombre;
}

@Override
public int compareTo(Pokemon otro) {
    int porNivel = Integer.compare(this.nivel,
                                   otro.getNivel());
    if (porNivel != 0) {
        return porNivel;
    }
    return this.nombre.compareTo(otro.getNombre());
}
Segundo criterio

El nombre se consulta solo cuando el nivel empata, y reutiliza el compareTo de String.

La resta en compareTo


@Override
public int compareTo(Pokemon otro) {
    return this.nivel - otro.getNivel();
}
Con int

Con valores cercanos a los límites de int la resta se desborda, cambia de signo y el orden queda invertido.

Con double

Un peso de 6.0 kg contra uno de 6.4 kg da -0.4, y la conversión a int lo trunca a cero. Los dos Pokémon se consideran iguales.

El equipo Pokémon

Problema

Un equipo Pokémon admite como máximo seis integrantes. Se requiere una clase Equipo que los almacene en un arreglo privado de tamaño fijo, y el código cliente necesita recorrer el equipo para que cada integrante ataque.

  • El arreglo es private, así que el cliente no puede recorrerlo de forma directa.
  • La primera alternativa es un método que devuelva el arreglo.

Recorrer exponiendo el arreglo


// dentro de Equipo
public Pokemon[] getMiembros() {
    return this.miembros;
}

// en el código cliente, con dos integrantes
Pokemon[] miembros = equipo.getMiembros();
for (int i = 0; i < miembros.length; i++) {
    miembros[i].atacar();   // falla en i = 2
}
miembros[0] = null;         // altera el equipo

Lo que cuesta exponer el arreglo

Las posiciones vacías son null, y recorrer hasta length lanza NullPointerException
El cliente recibe un alias del arreglo y puede modificar el equipo sin pasar por la clase
El cliente debe conocer la cantidad real de integrantes, que es un detalle interno
Si Equipo cambia el arreglo por un ArrayList, todo el código cliente deja de compilar

Cada estructura se recorre distinto

EstructuraRecorrido sin iterador
Arregloíndice hasta la cantidad real
ArrayListget(i) hasta size()
HashSetno tiene índices
Equipodepende de su implementación interna
Consecuencia

Sin un mecanismo común, el código que recorre una colección queda atado a la forma en que esa colección guarda sus datos.

Iterador

Definición

Un iterador es un objeto que recorre una colección y entrega sus elementos uno a la vez, sin revelar cómo están almacenados.

1La colección crea el iterador
  • Equipo conoce su arreglo y su cantidad, y se los entrega al iterador.
2El cliente solo pregunta y pide
  • hasNext indica si quedan elementos y next entrega el siguiente. El cliente no ve índices, null ni el arreglo.

Un mismo protocolo para cualquier colección

El arreglo sigue siendo private y nadie fuera de Equipo lo modifica
El iterador conoce la cantidad real y nunca entrega una posición vacía
Equipo puede cambiar su estructura interna sin romper el código cliente
ArrayList, HashSet y Equipo se recorren con las mismas dos llamadas
for mejorado

El for mejorado usa ese protocolo por dentro, y por eso acepta cualquier objeto que implemente Iterable.

El for mejorado necesita Iterable


for (Pokemon p : equipo) {   // no compila
    p.atacar();
}
Por qué falla

Equipo todavía no implementa Iterable. El for mejorado solo acepta arreglos y objetos que implementen esa interfaz.

Iterable e Iterator


public interface Iterable<T> {
    Iterator<T> iterator();
}

public interface Iterator<T> {
    boolean hasNext();
    T next();
}

Los métodos del contrato

Iterable: la colección que se puede recorrer, su método crea un iterador nuevo
Iterator: el recorrido en curso, sabe en qué posición va
hasNext: indica si quedan elementos, sin avanzar
next: devuelve el siguiente elemento y avanza la posición
next sin elementos restantes lanza NoSuchElementException
remove es default y lanza UnsupportedOperationException si no se sobrescribe

Equipo implementa Iterable


import java.util.Iterator;

public class Equipo implements Iterable<Pokemon> {
    private Pokemon[] miembros;
    private int cantidad;

    public Equipo() {
        this.miembros = new Pokemon[6];
        this.cantidad = 0;
    }

    public void agregar(Pokemon p) {
        this.miembros[this.cantidad] = p;
        this.cantidad++;
    }

    @Override
    public Iterator<Pokemon> iterator() {
        return new IteradorEquipo(this.miembros, this.cantidad);
    }
}

Decisiones de diseño

El arreglo y el contador son private y ninguna clase externa los ve
iterator crea un objeto nuevo en cada llamada, así dos recorridos no se interfieren
El iterador recibe una referencia al mismo arreglo, un alias, no una copia
El cliente no depende de que internamente se use un arreglo

IteradorEquipo implementa Iterator


public class IteradorEquipo implements Iterator<Pokemon> {
    private Pokemon[] miembros;
    private int cantidad;
    private int actual;

    public IteradorEquipo(Pokemon[] miembros, int cantidad) {
        this.miembros = miembros;
        this.cantidad = cantidad;
        this.actual = 0;
    }

    @Override
    public boolean hasNext() {
        return this.actual < this.cantidad;
    }

    @Override
    public Pokemon next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        Pokemon p = this.miembros[this.actual];
        this.actual++;
        return p;
    }
}

Diagrama de clases

classDiagram
    direction LR
    class Iterable~Pokemon~ {
        <<interface>>
        +iterator() Iterator
    }
    class Iterator~Pokemon~ {
        <<interface>>
        +hasNext() boolean
        +next() Pokemon
    }
    class Equipo {
        -Pokemon[] miembros
        -int cantidad
        +agregar(Pokemon p) void
        +iterator() Iterator
    }
    class IteradorEquipo {
        -Pokemon[] miembros
        -int cantidad
        -int actual
        +hasNext() boolean
        +next() Pokemon
    }
    Iterable <|.. Equipo
    Iterator <|.. IteradorEquipo
    Equipo ..> IteradorEquipo : crea

Decisiones del iterador

actual guarda la posición, y vive en el iterador, no en el equipo
hasNext solo consulta, así que se puede llamar varias veces seguidas
next verifica antes de leer y lanza la excepción que exige el contrato
remove no se sobrescribe, y el equipo no admite eliminar durante el recorrido
Iterator y NoSuchElementException se importan de java.util

El for mejorado sobre Equipo


Equipo equipo = new Equipo();
equipo.agregar(new PokemonElectrico("Pikachu", 12, 35, 90));
equipo.agregar(new Dragonite("Dragonite", 55, 91));

for (Pokemon p : equipo) {
    p.atacar();
}
Traducción

El compilador traduce el for mejorado a llamadas sobre las interfaces Iterable e Iterator.

El for mejorado por dentro


Iterator<Pokemon> it = equipo.iterator();
while (it.hasNext()) {
    Pokemon p = it.next();
    p.atacar();
}

Lo que hace cada llamada

equipo.iterator() crea el iterador una sola vez, antes de la primera vuelta
it.hasNext() decide si el ciclo continúa
it.next() entrega el Pokémon de la posición actual y avanza
p.atacar() se resuelve según el objeto real, sin conocer el arreglo ni el índice

La salida


Pikachu lanza 90 V.
Dragonite usa Hiperrayo.
Polimorfismo

El for mejorado depende solo de Iterable e Iterator. Funciona igual con ArrayList, HashSet o Equipo, porque las tres implementan la misma interfaz.

Métodos que reciben Iterable


public static double nivelPromedio(Iterable<Pokemon> grupo) {
    int suma = 0;
    int n = 0;
    for (Pokemon p : grupo) {
        suma += p.getNivel();
        n++;
    }
    return (double) suma / n;
}

nivelPromedio(equipo);       // un Equipo
nivelPromedio(capturados);   // un ArrayList<Pokemon>

Eliminar durante un recorrido

Se solicita liberar a los Pokémon capturados con nivel menor que 15. La lista contiene a Pikachu, Jolteon, Squirtle y Dragonite, en ese orden.


for (Pokemon p : capturados) {
    if (p.getNivel() < 15) {
        capturados.remove(p);
    }
}
ConcurrentModificationException

El for mejorado usa un iterador por debajo, y la lista se modificó sin pasar por él. El iterador detecta el cambio en la siguiente llamada a next.

Eliminar con el iterador


Iterator<Pokemon> it = capturados.iterator();
while (it.hasNext()) {
    Pokemon p = it.next();
    if (p.getNivel() < 15) {
        it.remove();
    }
}
it.remove()

Elimina el último elemento entregado por next y mantiene al iterador sincronizado con la lista. En la lista quedan Jolteon y Dragonite.