Comparable y Cloneable en Java — práctica con un ranking, copias de seguridad y árboles ordenados (y la palabra que desapareció del TreeSet)

Mi primer TreeSet con un Comparator por longitud salió así de bien: le metí siete palabras y me devolvió tres. Revisé el código tres veces buscando el fallo. Revisé la lista de palabras. Hasta sospeché de mi teclado. Java no se había equivocado en nada: simplemente yo le había dicho que dos palabras de la misma longitud eran la misma palabra, y él me había hecho caso.

Esa es la gracia de este tema: Comparable, Comparator y Cloneable son tres herramientas pequeñas que hacen exactamente lo que les pides, aunque no sea lo que querías. Y justo por eso se aprenden practicando: cuando algo sale raro, la causa casi siempre es una definición de «igual» o de «copia» que no era la que tenías en la cabeza.

Si ya has leído la teoría de Comparable y Cloneable, este artículo es para aplicarla: un ranking de alumnos con varios criterios de orden, una comparación entre clone() y el constructor de copia, y un paseo por TreeSet y TreeMap con orden propio. Son programas de dificultad media. Cada uno trae el código comentado, la salida y las dudas que suelen surgir.

Antes de empezar

Conviene tener claro:

  • Que compareTo devuelve un número negativo, cero o positivo, y que debe comparar sin restas (Integer.compare, Double.compare) y con desempate cuando dos elementos distintos pueden empatar.
  • Que Comparable es el orden natural de la clase y Comparator es un criterio externo: puedes tener todos los que quieras y encadenarlos con reversed() y thenComparing().
  • Que clone() necesita implements Cloneable, ser public y empezar por super.clone(), y que esa copia es superficial: las listas y los objetos mutables siguen compartidos.
  • Que TreeSet y TreeMap no usan equals: si el comparador da 0, para ellos son el mismo elemento.

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

Los tres programas son independientes. Crea una carpeta para cada uno y compílalos por separado: los programas 1 y 2 tienen cada uno su propia clase Alumno, y no son la misma.

Programa 1: ranking de alumnos

Una clase de seis alumnos con nombre, nota y curso. Queremos verlos en orden alfabético (el orden por defecto de una lista de clase), después en un ranking por nota y, por último, agrupados por curso.

La idea

El orden natural del alumno será el alfabético, porque es lo que esperaría cualquiera de una lista de clase: eso va en compareTo. Todo lo demás (ranking por nota, por curso) son criterios que no son «el» orden del alumno, así que van fuera, en Comparators que se pueden combinar como piezas.

Y además, un método genérico mayor que solo exige que el tipo sea Comparable: la prueba de que Comparable no sirve solo para ordenar, sino para escribir código que vale para cualquier tipo «comparable».

Antes de programar, piensa en qué cambia entre un criterio y otro: solo el campo por el que se compara. Los Comparator.comparing... están hechos justo para eso.

El código

Alumno.java

public class Alumno implements Comparable<Alumno> {
    private final String nombre;
    private final double nota;
    private final int curso;

    public Alumno(String nombre, double nota, int curso) {
        this.nombre = nombre;
        this.nota = nota;
        this.curso = curso;
    }

    public String getNombre() { return nombre; }
    public double getNota() { return nota; }
    public int getCurso() { return curso; }

    // Orden natural: alfabético por nombre
    @Override
    public int compareTo(Alumno otro) {
        return this.nombre.compareTo(otro.nombre);
    }

    @Override
    public String toString() {
        return nombre + " (" + nota + ", " + curso + "º)";
    }
}

Ranking.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Ranking {

    // Funciona con cualquier tipo que sepa compararse a sí mismo
    static <T extends Comparable<T>> T mayor(List<T> lista) {
        T mejor = lista.get(0);
        for (T elemento : lista) {
            if (elemento.compareTo(mejor) > 0) {
                mejor = elemento;
            }
        }
        return mejor;
    }

    public static void main(String[] args) {
        List<Alumno> clase = new ArrayList<>(List.of(
            new Alumno("Marta", 9.1, 2), new Alumno("Luis", 6.5, 1),
            new Alumno("Ana", 8.0, 2), new Alumno("Pablo", 8.0, 1),
            new Alumno("Eva", 9.1, 1), new Alumno("Sara", 4.8, 2)));

        // 1. Orden natural (compareTo): alfabético
        Collections.sort(clase);
        System.out.println("Alfabético: " + clase);

        // 2. Criterio externo: nota descendente y, si empatan, nombre
        Comparator<Alumno> porNota = Comparator
                .comparingDouble(Alumno::getNota).reversed()
                .thenComparing(Alumno::getNombre);
        clase.sort(porNota);

        System.out.println("\nRanking:");
        for (int i = 0; i < clase.size(); i++) {
            System.out.println((i + 1) + ". " + clase.get(i));
        }

        // 3. Top 3 y extremos con el mismo Comparator
        System.out.println("\nTop 3: " + clase.subList(0, 3));
        System.out.println("Mejor: " + Collections.min(clase, porNota));
        System.out.println("Peor:  " + Collections.max(clase, porNota));

        // 4. Ranking por curso y, dentro de cada curso, por nota
        clase.sort(Comparator.comparingInt(Alumno::getCurso).thenComparing(porNota));
        System.out.println("\nPor curso: " + clase);

        // 5. Un método genérico que solo exige Comparable
        System.out.println("\nÚltimo alfabéticamente: " + mayor(clase));
        System.out.println("Número mayor: " + mayor(List.of(3, 17, 8)));
        System.out.println("Palabra mayor: " + mayor(List.of("pan", "sol", "luna")));
    }
}

Salida

Alfabético: [Ana (8.0, 2º), Eva (9.1, 1º), Luis (6.5, 1º), Marta (9.1, 2º), Pablo (8.0, 1º), Sara (4.8, 2º)]

Ranking:
1. Eva (9.1, 1º)
2. Marta (9.1, 2º)
3. Ana (8.0, 2º)
4. Pablo (8.0, 1º)
5. Luis (6.5, 1º)
6. Sara (4.8, 2º)

Top 3: [Eva (9.1, 1º), Marta (9.1, 2º), Ana (8.0, 2º)]
Mejor: Eva (9.1, 1º)
Peor:  Sara (4.8, 2º)

Por curso: [Eva (9.1, 1º), Pablo (8.0, 1º), Luis (6.5, 1º), Marta (9.1, 2º), Ana (8.0, 2º), Sara (4.8, 2º)]

Último alfabéticamente: Sara (4.8, 2º)
Número mayor: 17
Palabra mayor: sol

Compruébalo a mano:

  • Alfabético: Ana, Eva, Luis, Marta, Pablo, Sara. Las notas no han pintado nada: compareTo solo mira el nombre.
  • Ranking: primero las notas más altas. Eva y Marta tienen 9,1 y Eva va antes por orden alfabético; igual Ana y Pablo con 8,0. Luis (6,5) y Sara (4,8) cierran.
  • Top 3, mejor y peor: el top 3 son los tres primeros del ranking. Como nuestro comparador pone primero al mejor, el mínimo según ese criterio (Collections.min) es el mejor alumno, Eva; y el máximo, el peor, Sara.
  • Por curso: primero los del 1.º (Eva, Pablo, Luis, ordenados por nota) y después los del 2.º (Marta, Ana, Sara).
  • Último alfabéticamente: Sara. Y el mismo método mayor da 17 con enteros y sol con palabras.

subList(0, 3) es una vista de la lista, no una copia: si cambiases la lista original, la vista se vería afectada. Para una lista independiente, new ArrayList<>(clase.subList(0, 3)).

Preguntas que te puedes hacer

**¿Por qué el ranking es un Comparator y no cambio compareTo?** Porque compareTo es el orden por defecto de la clase y solo puede haber uno. Si lo cambiaras a «por nota», el listado alfabético dejaría de existir sin escribir otro criterio. Con Comparator conviven todos.

**¿Por qué Collections.min(clase, porNota) devuelve al mejor alumno?** Porque min devuelve el elemento que va antes según el comparador, y nuestro ranking pone antes al de mejor nota. «Mínimo» no significa «peor», significa «primero en el orden que le des».

**¿Qué significa <T extends Comparable<T>>?** Que mayor acepta cualquier tipo T que sepa compararse con otros de su misma clase. Por eso la misma función vale para Alumno, Integer y String, sin duplicar código.

¿Cómo se combinan dos comparadores? comparingInt(Alumno::getCurso).thenComparing(porNota) ordena por curso y, dentro de cada curso, usa el comparador porNota que ya teníamos. Los comparadores se reutilizan como piezas de construcción.

Llamar a mayor con una lista vacía. lista.get(0) lanza IndexOutOfBoundsException. Antes de coger el primer elemento, comprueba que la lista no esté vacía.

Pruébalo tú

  1. Cambia el ranking para que, en caso de empate de nota, gane el alumno de mayor curso.
  2. Escribe un Comparator que ordene por la longitud del nombre y, si empatan, por nota descendente.

Programa 2: clone() frente al constructor de copia

Un alumno con una lista de notas y una dirección. Queremos copiarlo de tres maneras (copia superficial, clone() profundo y constructor de copia) y ver con qué se queda cada una.

La idea

Una copia hecha con super.clone() solo duplica la caja: la lista de notas y la dirección seguirán siendo las mismas. Para que la copia sea independiente, hay que duplicar a mano lo mutable. Lo vamos a hacer de dos formas y a compararlas, y a ver qué pasa cuando hay herencia (un alumno becado que es un Alumno con un campo más).

La dirección también es Cloneable: así Alumno puede pedirle que se clone a sí misma en lugar de conocer sus campos. La profundidad se decide nivel a nivel.

Antes de leer el código, predice qué pasará en cada bloque de la salida. En el primero, ¿cambiará el original al tocar la copia superficial?

El código

Direccion.java

public class Direccion implements Cloneable {
    private String ciudad;

    public Direccion(String ciudad) { this.ciudad = ciudad; }

    public void setCiudad(String ciudad) { this.ciudad = ciudad; }

    @Override
    public Direccion clone() {
        try {
            return (Direccion) super.clone();   // solo hay un String (inmutable): basta con esto
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

    @Override
    public String toString() { return ciudad; }
}

Alumno.java

import java.util.ArrayList;
import java.util.List;

public class Alumno implements Cloneable {
    private String nombre;
    private List<Double> notas = new ArrayList<>();
    private Direccion direccion;

    public Alumno(String nombre, String ciudad) {
        this.nombre = nombre;
        this.direccion = new Direccion(ciudad);
    }

    // Constructor de copia (copia profunda)
    public Alumno(Alumno otro) {
        this.nombre = otro.nombre;
        this.notas = new ArrayList<>(otro.notas);
        this.direccion = otro.direccion.clone();
    }

    public void anotar(double nota) { notas.add(nota); }
    public Direccion getDireccion() { return direccion; }
    public List<Double> getNotas() { return notas; }

    // clone() superficial: solo lo que hace super.clone()
    public Alumno cloneSuperficial() {
        try {
            return (Alumno) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

    // clone() profundo: super.clone() y después duplicamos lo mutable
    @Override
    public Alumno clone() {
        Alumno copia = cloneSuperficial();
        copia.notas = new ArrayList<>(this.notas);
        copia.direccion = this.direccion.clone();
        return copia;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " " + nombre + " " + notas + " en " + direccion;
    }
}

AlumnoBecado.java

public class AlumnoBecado extends Alumno {
    private double beca;

    public AlumnoBecado(String nombre, String ciudad, double beca) {
        super(nombre, ciudad);
        this.beca = beca;
    }

    public AlumnoBecado(AlumnoBecado otro) {
        super(otro);
        this.beca = otro.beca;
    }

    @Override
    public String toString() {
        return super.toString() + " con beca de " + beca;
    }
}

Copias.java

public class Copias {
    public static void main(String[] args) {
        Alumno ana = new Alumno("Ana", "Telde");
        ana.anotar(8.0);

        // 1. Copia superficial: comparte la lista y la dirección
        Alumno sup = ana.cloneSuperficial();
        sup.anotar(5.0);
        sup.getDireccion().setCiudad("Arucas");
        System.out.println("Original:    " + ana);
        System.out.println("Superficial: " + sup);

        // 2. Las dos copias profundas
        Alumno porClone = ana.clone();
        Alumno porConstructor = new Alumno(ana);
        porClone.anotar(10.0);
        porConstructor.getDireccion().setCiudad("Gáldar");
        System.out.println("\nOriginal:     " + ana);
        System.out.println("Con clone:    " + porClone);
        System.out.println("Con ctor:     " + porConstructor);

        // 3. ¿Qué tipo tiene la copia cuando hay herencia?
        Alumno becado = new AlumnoBecado("Luis", "Agüimes", 1500);
        Alumno copiaClone = becado.clone();
        Alumno copiaCtor = new Alumno(becado);
        System.out.println("\nclone():     " + copiaClone);
        System.out.println("constructor: " + copiaCtor);
    }
}

Salida

Original:    Alumno Ana [8.0, 5.0] en Arucas
Superficial: Alumno Ana [8.0, 5.0] en Arucas

Original:     Alumno Ana [8.0, 5.0] en Arucas
Con clone:    Alumno Ana [8.0, 5.0, 10.0] en Arucas
Con ctor:     Alumno Ana [8.0, 5.0] en Gáldar

clone():     AlumnoBecado Luis [] en Agüimes con beca de 1500.0
constructor: Alumno Luis [] en Agüimes

Compruébalo a mano:

  • Superficial: sup comparte la lista de notas y la dirección con ana. Al añadir un 5,0 y mudarse a Arucas, el original también los tiene. El daño queda hecho para el resto del programa.
  • **clone() profundo:** porClone tiene su propia lista. Le añadimos un 10,0 y el original ([8.0, 5.0]) no se entera.
  • Constructor de copia: porConstructor tiene su propia dirección. Se muda a Gáldar y el original sigue en Arucas.
  • Herencia: becado.clone() devuelve un AlumnoBecado con su beca; new Alumno(becado) crea un Alumno normal y la beca desaparece.
clone()Constructor de copia
Necesita Cloneable y try/catchSíNo
Conserva el tipo real (herencia)Sí (si usas super.clone())No: siempre crea la clase del constructor
Copia profundaA mano, tras super.clone()A mano, campo a campo
Claridad al leerloMenosMás
Campos finalCuesta (los copia el super.clone() pero no los puedes reasignar)Fácil

Preguntas que te puedes hacer

**¿Por qué Direccion.clone() no copia nada a mano?** Porque solo tiene un String, y los String son inmutables: compartirlos no hace daño. Con super.clone() ya es una copia profunda.

**¿Por qué clone() llama a cloneSuperficial() y no directamente a super.clone()?** Podría llamarlo directo. Lo separé para poder demostrar en el mismo programa la copia superficial y la profunda con la misma clase. En código real tendrías solo el clone() profundo.

¿Por qué el constructor de copia pierde la beca? Porque new Alumno(becado) ejecuta el constructor de Alumno, que no sabe nada de becas. El tipo del objeto creado lo decide el new, no el tipo de la variable que le pasas.

¿Cuál uso? No hay un ganador absoluto. Con jerarquías de clases y copias que deben ser del mismo tipo, clone() (o un método copiar() que cada subclase redefine). Con clases simples y ganas de claridad, el constructor de copia.

Copiar una lista con new ArrayList<>(otra) y creer que ya es una copia profunda. La lista es nueva, pero sus elementos siguen siendo los mismos objetos. Con Double (inmutable) no importa; con objetos tuyos mutables, sí.

Pruébalo tú

  1. Quita implements Cloneable de Alumno y mira qué excepción aparece al ejecutar Copias.
  2. Haz que Direccion tenga además una lista de teléfonos. ¿Qué tienes que cambiar para que Alumno.clone() siga siendo profundo?

Programa 3: TreeSet y TreeMap con orden propio

Siete palabras, un comparador por longitud y mucho cuidado con los empates. Aquí está el lugar del crimen de mi anécdota.

La idea

TreeSet y TreeMap mantienen sus elementos ordenados usando compareTo o un Comparator, y eso lo cambia todo: dejan de usar equals y hashCode, y solo miran el resultado de la comparación. Si el comparador da 0, para ellos es el mismo elemento.

Vamos a probar tres cosas: el orden natural de los String, un comparador que pierde palabras y el mismo comparador arreglado con un desempate. Después navegaremos el conjunto y usaremos un TreeMap para agrupar.

Antes de ejecutar, cuenta cuántas palabras de cada longitud hay. Eso ya te dice cuántas sobrevivirán con el comparador «solo longitud».

El código

Palabras.java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.TreeMap;
import java.util.TreeSet;

public class Palabras {
    public static void main(String[] args) {
        List<String> palabras = Arrays.asList("casa", "sol", "mesa", "luna", "pan", "cielo", "árbol");

        // 1. Orden natural de String: alfabético
        TreeSet<String> alfabetico = new TreeSet<>(palabras);
        System.out.println("Alfabético: " + alfabetico);

        // 2. Comparator solo por longitud: ¡empatar es "ser el mismo"!
        TreeSet<String> porLongitud = new TreeSet<>(Comparator.comparingInt(String::length));
        porLongitud.addAll(palabras);
        System.out.println("Solo longitud: " + porLongitud.size() + " de " + palabras.size() + " " + porLongitud);

        // 3. Con desempate no se pierde nada
        TreeSet<String> completo = new TreeSet<>(
                Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
        completo.addAll(palabras);
        System.out.println("Longitud + alfabético: " + completo.size() + " " + completo);

        // 4. Navegar un TreeSet
        System.out.println("\nPrimera: " + completo.first() + ", última: " + completo.last());
        System.out.println("Anterior a \"mesa\": " + completo.lower("mesa"));
        System.out.println("Siguiente a \"mesa\": " + completo.higher("mesa"));
        System.out.println("Hasta \"luna\" (inclusive): " + completo.headSet("luna", true));
        System.out.println("Desde \"mesa\": " + completo.tailSet("mesa"));

        // 5. TreeMap: palabras agrupadas por longitud, siempre ordenadas por clave
        TreeMap<Integer, List<String>> porTamano = new TreeMap<>();
        for (String p : palabras) {
            porTamano.computeIfAbsent(p.length(), k -> new ArrayList<>()).add(p);
        }
        System.out.println("\nAgrupadas: " + porTamano);
        System.out.println("Clave menor y mayor: " + porTamano.firstKey() + " y " + porTamano.lastKey());
        System.out.println("Mayor clave <= 4: " + porTamano.floorKey(4));
        System.out.println("Menor clave >= 5: " + porTamano.ceilingKey(5));
        System.out.println("De mayor a menor: " + porTamano.descendingMap());

        // 6. Claves que se comparan sin mirar mayúsculas
        TreeMap<String, Integer> votos = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (String voto : new String[]{"Sol", "luna", "sol", "SOL", "Luna"}) {
            votos.merge(voto, 1, Integer::sum);
        }
        System.out.println("\nVotos: " + votos);
    }
}

Salida

Alfabético: [casa, cielo, luna, mesa, pan, sol, árbol]
Solo longitud: 3 de 7 [sol, casa, cielo]
Longitud + alfabético: 7 [pan, sol, casa, luna, mesa, cielo, árbol]

Primera: pan, última: árbol
Anterior a "mesa": luna
Siguiente a "mesa": cielo
Hasta "luna" (inclusive): [pan, sol, casa, luna]
Desde "mesa": [mesa, cielo, árbol]

Agrupadas: {3=[sol, pan], 4=[casa, mesa, luna], 5=[cielo, árbol]}
Clave menor y mayor: 3 y 5
Mayor clave <= 4: 4
Menor clave >= 5: 5
De mayor a menor: {5=[cielo, árbol], 4=[casa, mesa, luna], 3=[sol, pan]}

Votos: {luna=2, Sol=3}

Compruébalo a mano:

  • Alfabético: ojo, árbol aparece al final. El orden natural de String compara códigos de carácter y la á tiene un código mayor que la z. No es el orden del diccionario.
  • Solo longitud: 3 de 7. Hay palabras de 3, 4 y 5 letras: tres longitudes, tres elementos. Se queda la primera palabra que entró de cada longitud (sol, casa, cielo).
  • Longitud + alfabético: 7. Con el desempate nadie empata con nadie y no se pierde ninguna.
  • Navegar: lower("mesa") es luna (la anterior) y higher("mesa") es cielo (la siguiente). headSet("luna", true) llega hasta luna incluida; tailSet("mesa") empieza en mesa.
  • **TreeMap:** las claves 3, 4 y 5 salen siempre ordenadas. floorKey(4) es 4 y ceilingKey(5) es 5.
  • Votos: Sol, sol y SOL son la misma clave; los cinco votos se reparten en Sol (3) y luna (2).

lower, higher, floorKey y ceilingKey funcionan aunque la clave que pides no exista: devuelven la más cercana por debajo o por arriba (y null solo si no hay ninguna).

Preguntas que te puedes hacer

**¿Por qué el TreeSet descarta mesa y luna sin avisar?** Porque el comparador dice que compare("mesa", "casa") == 0, y un TreeSet entiende «cero» como «ya lo tengo». No lanza ninguna excepción: añadir devuelve false, y ya está.

¿Por qué con el desempate no se pierde nada? Porque thenComparing(Comparator.naturalOrder()) solo se consulta cuando la longitud empata, y entonces decide el orden alfabético. Dos palabras distintas ya nunca dan 0.

**¿Y la clave que se guarda en el TreeMap con CASE_INSENSITIVE_ORDER? La primera** que entró. Las demás cuentan para esa misma clave, pero no la sustituyen.

**¿Cuándo uso TreeSet en lugar de HashSet? Cuando necesitas los elementos ordenados** o navegar (first, lower, headSet…). Si solo te importa saber si algo está o no, HashSet es más rápido.

Usar un Comparator que no distingue elementos distintos y esperar que el TreeSet guarde los dos. Un TreeSet no mira equals: si compare da 0, el segundo no entra.

Pruébalo tú

  1. Cambia el comparador de longitud para que ordene de mayor a menor longitud (.reversed()) y comprueba cuál de las palabras de cuatro letras sobrevive.
  2. Crea un TreeMap<String, Integer> con String.CASE_INSENSITIVE_ORDER y cuenta cuántas veces aparece cada palabra de un texto, sin distinguir mayúsculas.

Errores típicos en esta práctica

  • Restar para comparar en compareTo: se desborda con valores grandes. Usa Integer.compare o Double.compare.
  • **Un criterio de orden que da 0 para elementos distintos**: en un TreeSet o TreeMap desaparecen datos sin avisar.
  • **Creer que reversed() invierte toda la cadena: solo invierte lo que hay antes** de él.
  • **Llamar a clone() sin implements Cloneable**: CloneNotSupportedException al ejecutar.
  • Olvidar duplicar lo mutable tras super.clone(): la copia comparte la lista y cambia el original.
  • Usar el constructor de copia con herencia y perder los campos de la subclase.

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 el desastre del TreeSet reducido a lo mínimo, con el comparador escrito como una clase normal (más fácil de seguir que una lambda):

import java.util.Comparator;
import java.util.TreeSet;

public class Tutor {
    static class PorLongitud implements Comparator<String> {
        public int compare(String a, String b) {
            return Integer.compare(a.length(), b.length());
        }
    }

    public static void main(String[] args) {
        TreeSet<String> conjunto = new TreeSet<>(new PorLongitud());
        String[] palabras = {"casa", "sol", "mesa", "luna"};
        for (String p : palabras) {
            boolean nueva = conjunto.add(p);
            System.out.println(p + " -> " + nueva + " " + conjunto);
        }
    }
}

Salida esperada:

casa -> true [casa]
sol -> true [sol, casa]
mesa -> false [sol, casa]
luna -> false [sol, casa]

Qué observar paso a paso:

  1. En la primera vuelta (casa), nueva vale true y el conjunto tiene un único elemento.
  2. En la segunda (sol), también true: la longitud es distinta (3 frente a 4) y sol se coloca antes de casa.
  3. En la tercera (mesa), nueva pasa a false. Fíjate en el cambio: el conjunto no crece, porque compare("mesa", "casa") da 0.
  4. La cuarta (luna) repite lo mismo. Al terminar, de cuatro palabras quedan dos.

Por dentro un TreeSet es un árbol, así que Python Tutor puede dibujarlo con más cajas de las que esperas. Céntrate en la variable nueva y en la salida de cada vuelta, que es lo que cuenta la historia.

Resumen

  • Comparable define el orden natural de la clase; Comparator aporta tantos criterios externos como quieras.
  • Los comparadores se encadenan y reutilizan: comparing, reversed, thenComparing.
  • Un criterio de orden es una definición de «igual», no solo de «antes y después»: si da 0, TreeSet y TreeMap lo tratan como el mismo elemento.
  • clone() es superficial: tras super.clone() hay que duplicar a mano listas y objetos mutables.
  • El constructor de copia es más claro, pero pierde el tipo real de la subclase; clone() lo conserva.
  • Los métodos genéricos con <T extends Comparable<T>> valen para cualquier tipo comparable.

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 un criterio nuevo al ranking sin tocar compareTo.
  • [ ] He provocado una copia superficial y he visto cómo cambia el original.
  • [ ] He perdido una palabra en un TreeSet por un empate y la he recuperado con un desempate.
  • [ ] He mirado el programa de Python Tutor y he visto cómo add devuelve false.

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

Siguiente paso: ejerciciosComparable y Cloneable en Java — ejercicios con libros, jugadores y una plantilla clonada (y la resta que puso al más rico el primero)

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

Sigue aprendiendo

Un comentario

Deja una respuesta

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