Comparable y Cloneable en Java — ejercicios con libros, jugadores y una plantilla clonada (y la resta que puso al más rico el primero)
Ordenaba cuentas bancarias por saldo y escribí, tan tranquilo, return this.saldo - otra.saldo; dentro de compareTo. Con los datos de clase iba perfecto: 100, 250, 4000… El día que probé con un saldo enorme y un descubierto enorme, la cuenta de dos mil millones salió la primera de una lista que debía ir de menor a mayor. Sin excepción, sin mensaje, sin nada: Java ordenó mal con toda su buena fe, porque la resta se había desbordado y mi compareTo mentía.
Es el tipo de fallo que no se aprende leyendo: se aprende rompiéndolo. Hoy practicamos con tres programas de dificultad creciente, tres fragmentos para depurar (uno es esa resta) y un chuletario para llevarte.
Estos ejercicios usan la teoría de Comparable y Cloneable y repasan lo de la práctica. Sin ellas puedes hacerlos, pero te costará más.
Tabla de Contenidos
Cómo usar este artículo
- Lee el enunciado de un nivel y no mires la solución todavía.
- Escribe tu versión, compílala y compara tu salida con la esperada.
- Si te atascas, mira solo la 🔵 pista y vuelve a intentarlo.
- Cuando termines, compara con la solución y fíjate en por qué está hecha así, no solo en que dé la misma salida.
Ningún ejercicio usa Scanner. Los datos van fijos en el código para que tu salida sea idéntica a la esperada y puedas comprobarla de un vistazo.
Nivel 1 · Básico: libros ordenados por páginas
Enunciado. Crea una clase Libro con título y número de páginas (int), con sus getters y un toString que devuelva, por ejemplo, Platero y yo (160 págs.). Haz que Libro sea Comparable<Libro> con este orden natural: de menos a más páginas.
En una clase N1 con main, crea un array con estos cuatro libros, ordénalo con Arrays.sort e imprímelo:
- El Quijote, 1376 páginas
- Platero y yo, 160 páginas
- La Celestina, 280 páginas
- Fortunata y Jacinta, 1008 páginas
Después, muestra el más corto y el más largo (sin volver a ordenar: usa las posiciones del array ya ordenado).
Salida esperada:
De menos a más páginas: Platero y yo (160 págs.) La Celestina (280 págs.) Fortunata y Jacinta (1008 págs.) El Quijote (1376 págs.) El más corto: Platero y yo (160 págs.) El más largo: El Quijote (1376 págs.)
Una vez ordenado el array, el más corto está en la posición 0 y el más largo en estante.length - 1. Y para comparar enteros, nada de restas: Integer.compare.
Nivel 2 · Intermedio: la clasificación de una liga
Enunciado. Crea una clase Jugador con nombre, puntos y partidos jugados (los tres como atributos finales, con getters) y un toString con el formato Lía 150 pts en 10 partidos. No la hagas Comparable: el criterio de la clasificación será un Comparator aparte.
En N2, declara una constante static final Comparator<Jugador> CLASIFICACION que ordene por:
- Más puntos primero.
- Si empatan, menos partidos jugados primero (quien lo consiguió en menos partidos va por delante).
- Si siguen empatando, por nombre.
Con estos cinco jugadores en una lista, ordénala e imprime el top 3 numerado y el último:
| Nombre | Puntos | Partidos |
|---|---|---|
| Noa | 120 | 10 |
| Iker | 150 | 12 |
| Lía | 150 | 10 |
| Hugo | 90 | 8 |
| Aitana | 120 | 10 |
Salida esperada:
Top 3: 1. Lía 150 pts en 10 partidos 2. Iker 150 pts en 12 partidos 3. Aitana 120 pts en 10 partidos Último: Hugo 90 pts en 8 partidos
comparingInt(...).reversed() invierte solo lo que hay antes del reversed(). Colócalo justo después del primer criterio y añade los demás con thenComparing...: así los puntos van de más a menos pero los partidos, de menos a más.
Nivel 3 · Desafío: una plantilla que se clona de verdad
Enunciado. Un equipo tiene una plantilla de jugadores y un estadio. Queremos poder hacer una copia profunda de la plantilla para probar fichajes sin tocar la original. Crea tres clases, **todas Cloneable** y con clone() público y sin excepción comprobada:
Estadio: nombre y aforo (int, consetAforo).toString:La Isla (5000).Jugador: nombre y puntos (int, con un métodosumar(int)).toString:Noa:10.Plantilla: equipo, lista de jugadores y estadio; métodosfichar(Jugador),getJugadores()ygetEstadio().toString:Tigres [Noa:10, Iker:12] en La Isla (5000).
El clone() de Plantilla debe ser profundo: la copia tiene su propia lista, sus propios jugadores y su propio estadio.
En N3, crea la plantilla «Tigres» con el estadio «La Isla» (5000) y los jugadores Noa (10) e Iker (12). Clónala y, en la copia solamente: ficha a Lía (7), súmale 100 puntos al primer jugador y cambia el aforo a 9000. Imprime las dos plantillas y si el primer jugador de cada una es el mismo objeto.
Salida esperada:
Original: Tigres [Noa:10, Iker:12] en La Isla (5000) Copia: Tigres [Noa:110, Iker:12, Lía:7] en La Isla (9000) ¿Mismo jugador en la posición 0? false
super.clone() te da una Plantilla con la misma lista y el mismo estadio. Reemplaza el campo de la lista por una lista nueva y, dentro de un bucle, añádele j.clone() de cada jugador. El estadio, con estadio.clone().
¿Qué falla aquí?
Tres fragmentos con un fallo cada uno. Antes de leer la explicación, haz dos cosas: predice si compila y, si compila, qué imprime (o qué pasa al ejecutarlo).
Fragmento 1
Cuenta.java
public class Cuenta implements Comparable<Cuenta> {
private final String titular;
private final int saldo; // en céntimos
public Cuenta(String titular, int saldo) {
this.titular = titular;
this.saldo = saldo;
}
@Override
public int compareTo(Cuenta otra) {
return this.saldo - otra.saldo;
}
@Override
public String toString() { return titular + ": " + saldo; }
}
F1.java
import java.util.Arrays;
public class F1 {
public static void main(String[] args) {
Cuenta[] cuentas = {
new Cuenta("Ana", 2_000_000_000),
new Cuenta("Luis", -2_000_000_000),
new Cuenta("Marta", 5)
};
Arrays.sort(cuentas);
System.out.println(Arrays.toString(cuentas));
}
}
Piénsalo antes de seguir: ¿compila? ¿qué imprime? Las cuentas deberían salir de menor a mayor saldo.
Explicación. Compila y no lanza ninguna excepción. Pero el resultado es este:
[Ana: 2000000000, Luis: -2000000000, Marta: 5]
El orden es incorrecto: Ana (dos mil millones) sale la primera y Luis (menos dos mil millones) en medio. La culpa es de la resta. Comparar a Ana con Luis calcula 2 000 000 000 - (-2 000 000 000) = 4 000 000 000, que **no cabe en un int** (el máximo es 2 147 483 647) y se desborda a un número negativo. compareTo le dice a Java que Ana va antes que Luis.
return this.x - otro.x en un compareTo de enteros. Funciona con valores pequeños y falla en silencio con valores grandes o de signo opuesto.
Arreglo. Deja que Java compare, sin restar:
@Override
public int compareTo(Cuenta otra) {
return Integer.compare(this.saldo, otra.saldo);
}
[Luis: -2000000000, Marta: 5, Ana: 2000000000]
Integer.compare(a, b) (o Double.compare) existe precisamente para esto. Es lo mismo que la resta, pero sin desbordamiento.
Fragmento 2
Jugador.java
public class Jugador implements Comparable<Jugador> {
private final String nombre;
private final int puntos;
public Jugador(String nombre, int puntos) {
this.nombre = nombre;
this.puntos = puntos;
}
@Override
public int compareTo(Jugador otro) {
return Integer.compare(this.puntos, otro.puntos);
}
@Override
public String toString() { return nombre + " " + puntos; }
}
F2.java
import java.util.Arrays;
public class F2 {
public static void main(String[] args) {
Jugador[] plantilla = new Jugador[4]; // todavía no hemos fichado al cuarto
plantilla[0] = new Jugador("Noa", 120);
plantilla[1] = new Jugador("Iker", 150);
plantilla[2] = new Jugador("Lía", 90);
Arrays.sort(plantilla);
System.out.println(Arrays.toString(plantilla));
}
}
¿Compila? ¿qué imprime? ¿Qué pasa con la posición que está sin rellenar?
Explicación. Compila bien. Pero new Jugador[4] crea un array de cuatro **referencias a null**, y solo hemos rellenado tres. Arrays.sort intenta comparar todos los elementos, y compara con uno que no existe:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "pivot" is null at java.base/java.util.ComparableTimSort.binarySort(ComparableTimSort.java:262) at java.base/java.util.ComparableTimSort.sort(ComparableTimSort.java:189) at java.base/java.util.Arrays.sort(Arrays.java:1042) at F2.main(F2.java:10)
NullPointerException dentro de Arrays.sort o Collections.sort. El orden natural llama a compareTo sobre los elementos, y un null no tiene métodos. El fallo aparece lejos de donde pusiste el null (aquí, en el new Jugador[4]).
Arreglo. Dos opciones: ordenar solo la parte rellena o decirle a Java qué hacer con los null. La segunda, con un Comparator que los manda al final:
import java.util.Arrays;
import java.util.Comparator;
public class F2Fix {
public static void main(String[] args) {
Jugador[] plantilla = new Jugador[4];
plantilla[0] = new Jugador("Noa", 120);
plantilla[1] = new Jugador("Iker", 150);
plantilla[2] = new Jugador("Lía", 90);
Arrays.sort(plantilla, Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(Arrays.toString(plantilla));
}
}
[Lía 90, Noa 120, Iker 150, null]
Comparator.nullsLast(...) y nullsFirst(...) envuelven cualquier comparador para que sepa qué hacer con los null. Pero el mejor arreglo suele ser que el array no tenga huecos.
Fragmento 3
Jugador.java
public class Jugador implements Cloneable {
protected String nombre;
protected int puntos;
public Jugador(String nombre, int puntos) {
this.nombre = nombre;
this.puntos = puntos;
}
@Override
public Jugador clone() {
return new Jugador(nombre, puntos); // "crear uno igual", ¿no?
}
@Override
public String toString() { return nombre + " " + puntos; }
}
Portero.java
public class Portero extends Jugador {
private int paradas;
public Portero(String nombre, int puntos, int paradas) {
super(nombre, puntos);
this.paradas = paradas;
}
@Override
public Portero clone() {
Portero copia = (Portero) super.clone();
copia.paradas = this.paradas;
return copia;
}
@Override
public String toString() { return super.toString() + " y " + paradas + " paradas"; }
}
F3.java
public class F3 {
public static void main(String[] args) {
Portero original = new Portero("Noa", 120, 35);
Portero copia = original.clone();
System.out.println(copia);
}
}
*¿Compila? ¿qué imprime? Fíjate en cómo se crea la copia en Jugador.clone().*
Explicación. Compila. Pero Jugador.clone() no usa super.clone(): fabrica la copia con new Jugador(...), así que siempre devuelve un Jugador normal, aunque el objeto que se clona sea un Portero. Cuando Portero.clone() intenta convertir ese resultado a Portero, el cast falla:
Exception in thread "main" java.lang.ClassCastException: class Jugador cannot be cast to class Portero (Jugador and Portero are in unnamed module of loader 'app') at Portero.clone(Portero.java:11) at F3.main(F3.java:4)
ClassCastException en un clone() de una subclase. La superclase crea la copia con new en lugar de super.clone(), y la copia pierde el tipo real.
Arreglo. En la superclase, usa super.clone(), que crea un objeto del tipo real del que se está clonando:
@Override
public Jugador clone() {
try {
return (Jugador) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
Noa 120 y 35 paradas
Regla de oro: dentro de cualquier clone(), la primera línea es super.clone(). Si ves un new ahí, desconfía.
Soluciones
Mira esta sección solo cuando hayas intentado el ejercicio. Si tu código es distinto pero da la misma salida y cumple los contratos, también es una solución válida.
Solución del Nivel 1
Libro.java
public class Libro implements Comparable<Libro> {
private final String titulo;
private final int paginas;
public Libro(String titulo, int paginas) {
this.titulo = titulo;
this.paginas = paginas;
}
public String getTitulo() { return titulo; }
public int getPaginas() { return paginas; }
// Orden natural: de menos a más páginas
@Override
public int compareTo(Libro otro) {
return Integer.compare(this.paginas, otro.paginas);
}
@Override
public String toString() {
return titulo + " (" + paginas + " págs.)";
}
}
N1.java
import java.util.Arrays;
public class N1 {
public static void main(String[] args) {
Libro[] estante = {
new Libro("El Quijote", 1376),
new Libro("Platero y yo", 160),
new Libro("La Celestina", 280),
new Libro("Fortunata y Jacinta", 1008)
};
Arrays.sort(estante);
System.out.println("De menos a más páginas:");
for (Libro libro : estante) {
System.out.println(" " + libro);
}
System.out.println("El más corto: " + estante[0]);
System.out.println("El más largo: " + estante[estante.length - 1]);
}
}
Por qué está hecho así:
Integer.compare(this.paginas, otro.paginas)en lugar de una resta: da el mismo resultado sin riesgo de desbordamiento.Arrays.sort(estante)funciona porqueLibroesComparable: sin eso, tendríamos elClassCastExceptionde la teoría.- El más corto y el más largo salen gratis de las posiciones
0ylength - 1. Ordenar una vez es mucho más barato que buscar el mínimo y el máximo por separado.
Solución del Nivel 2
Jugador.java
public class Jugador {
private final String nombre;
private final int puntos;
private final int partidos;
public Jugador(String nombre, int puntos, int partidos) {
this.nombre = nombre;
this.puntos = puntos;
this.partidos = partidos;
}
public String getNombre() { return nombre; }
public int getPuntos() { return puntos; }
public int getPartidos() { return partidos; }
@Override
public String toString() {
return nombre + " " + puntos + " pts en " + partidos + " partidos";
}
}
N2.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class N2 {
// Más puntos primero; si empatan, menos partidos jugados; si siguen empatando, por nombre
static final Comparator<Jugador> CLASIFICACION = Comparator
.comparingInt(Jugador::getPuntos).reversed()
.thenComparingInt(Jugador::getPartidos)
.thenComparing(Jugador::getNombre);
public static void main(String[] args) {
List<Jugador> plantilla = new ArrayList<>(List.of(
new Jugador("Noa", 120, 10), new Jugador("Iker", 150, 12),
new Jugador("Lía", 150, 10), new Jugador("Hugo", 90, 8),
new Jugador("Aitana", 120, 10)));
plantilla.sort(CLASIFICACION);
System.out.println("Top 3:");
for (int i = 0; i < 3; i++) {
System.out.println(" " + (i + 1) + ". " + plantilla.get(i));
}
System.out.println("Último: " + plantilla.get(plantilla.size() - 1));
}
}
Por qué está hecho así:
Jugadorno esComparable: una clasificación es un criterio concreto de una liga, no «el» orden de un jugador. Por eso vive en unComparator.comparingInt(Jugador::getPuntos).reversed()invierte solo los puntos. Los partidos se añaden después conthenComparingInt, así que van de menos a más.- Lía e Iker empatan a 150 puntos; Lía va primero por jugar menos partidos (10 frente a 12). Noa y Aitana empatan a 120 con los mismos partidos, y gana el nombre:
Aitanaantes queNoa… pero en el top 3 solo entra Aitana. Cambia los datos y comprueba que el desempate final funciona. - La constante está en
static final: un comparador no tiene estado, se puede reutilizar y compartir sin riesgo.
Solución del Nivel 3
Estadio.java
public class Estadio implements Cloneable {
private String nombre;
private int aforo;
public Estadio(String nombre, int aforo) {
this.nombre = nombre;
this.aforo = aforo;
}
public void setAforo(int aforo) { this.aforo = aforo; }
@Override
public Estadio clone() {
try {
return (Estadio) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() { return nombre + " (" + aforo + ")"; }
}
Jugador.java
public class Jugador implements Cloneable {
private final String nombre;
private int puntos;
public Jugador(String nombre, int puntos) {
this.nombre = nombre;
this.puntos = puntos;
}
public void sumar(int p) { puntos += p; }
@Override
public Jugador clone() {
try {
return (Jugador) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() { return nombre + ":" + puntos; }
}
Plantilla.java
import java.util.ArrayList;
import java.util.List;
public class Plantilla implements Cloneable {
private String equipo;
private List<Jugador> jugadores = new ArrayList<>();
private Estadio estadio;
public Plantilla(String equipo, Estadio estadio) {
this.equipo = equipo;
this.estadio = estadio;
}
public void fichar(Jugador j) { jugadores.add(j); }
public List<Jugador> getJugadores() { return jugadores; }
public Estadio getEstadio() { return estadio; }
@Override
public Plantilla clone() {
try {
Plantilla copia = (Plantilla) super.clone();
// La lista nueva debe contener COPIAS de los jugadores, no los mismos objetos
copia.jugadores = new ArrayList<>();
for (Jugador j : this.jugadores) {
copia.jugadores.add(j.clone());
}
copia.estadio = this.estadio.clone();
return copia;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
@Override
public String toString() {
return equipo + " " + jugadores + " en " + estadio;
}
}
N3.java
public class N3 {
public static void main(String[] args) {
Plantilla original = new Plantilla("Tigres", new Estadio("La Isla", 5000));
original.fichar(new Jugador("Noa", 10));
original.fichar(new Jugador("Iker", 12));
Plantilla copia = original.clone();
copia.fichar(new Jugador("Lía", 7));
copia.getJugadores().get(0).sumar(100);
copia.getEstadio().setAforo(9000);
System.out.println("Original: " + original);
System.out.println("Copia: " + copia);
System.out.println("¿Mismo jugador en la posición 0? "
+ (original.getJugadores().get(0) == copia.getJugadores().get(0)));
}
}
Por qué está hecho así:
EstadioyJugadorsolo tienen primitivos yString, así que suclone()es el más sencillo:super.clone()y nada más.- En
Plantilla.clone(),super.clone()copia la referencia a la lista y al estadio. Por eso las reemplazamos por una lista nueva y un estadio clonado. Si solo hubiéramos copiado la lista connew ArrayList<>(this.jugadores), la lista sería nueva pero los jugadores seguirían siendo los mismos objetos. - La última línea de la salida lo demuestra:
falsesignifica que el primer jugador de la copia es otro objeto. Por eso sumarle 100 puntos no ha tocado a Noa en la original. - El
try/catchestá dentro de cadaclone()para que quien llame no tenga que manejar la excepción comprobada. Es la receta de la teoría.
Míralo paso a paso en Python Tutor
El fallo de la resta es un buen candidato para verlo en vivo, porque ocurre dentro de una sola línea. Entra en pythontutor.com/java.html, elige Java y pega este programa, que compara los mismos dos números de las dos maneras:
public class Tutor {
static int compararMal(int a, int b) {
return a - b;
}
static int compararBien(int a, int b) {
return Integer.compare(a, b);
}
public static void main(String[] args) {
int grande = 2_000_000_000;
int pequeno = -2_000_000_000;
int mal = compararMal(grande, pequeno);
int bien = compararBien(grande, pequeno);
System.out.println("a - b = " + mal);
System.out.println("Integer.compare = " + bien);
}
}
a - b = -294967296 Integer.compare = 1
Qué observar, paso a paso:
- Al empezar
main, fíjate engrande(2 000 000 000) ypequeno(-2 000 000 000): ambos caben perfectamente en unint. - Entra en
compararMal(a, b). Antes de ejecutar elreturn, calcula tú la resta: debería ser 4 000 000 000, un número positivo. - Al volver a
main, mira el valor demal: es negativo (-294 967 296). Elintse ha desbordado y Java no ha dado ningún aviso. - Después,
compararBiendevuelve1, que es el resultado correcto:grandeva después depequeno. Compara los dos signos y entenderás el fallo del fragmento 1.
Chuletario: Comparable y Cloneable
Lo esencial en 30 segundos
- **
Comparable<T>**:compareTodefine el orden natural de la clase. Negativo = antes, 0 = equivalentes, positivo = después. - **
Comparator<T>: criterio externo**; puedes tener todos los que quieras y encadenarlos. - Nunca restes para comparar: usa
Integer.compare,Double.compareocompareTo. - Un orden que da
0para elementos distintos pierde datos enTreeSetyTreeMap. - **
clone()**:implements Cloneable+public+super.clone()+try/catch. super.clone()es superficial: lo mutable (listas, arrays, otros objetos) hay que duplicarlo a mano.
Sintaxis
| Quiero… | Código | Resultado |
|---|---|---|
| Orden natural de mi clase | class A implements Comparable<A> { public int compareTo(A o) {…} } | sort(lista) funciona |
| Comparar enteros | Integer.compare(this.x, o.x) | Sin desbordamiento |
| Comparar con desempate | if (c != 0) return c; return this.n.compareTo(o.n); | Nunca 0 para distintos |
| Ordenar con criterio externo | lista.sort(comparador) | No hace falta Comparable |
| Criterio por un campo | Comparator.comparing(A::getN) | Orden ascendente |
| Invertir | Comparator.comparingInt(A::getX).reversed() | Descendente |
| Encadenar desempates | ….thenComparing(A::getN) | Segundo criterio |
Orden natural con null al final | Comparator.nullsLast(Comparator.naturalOrder()) | Sin NullPointerException |
| Conjunto ordenado | new TreeSet<>(comparador) | Solo mira el comparador |
| Copiar un objeto | A copia = original.clone(); | Necesita public clone() |
| Copia profunda | copia.lista = new ArrayList<>(this.lista); | Lista propia |
Plantillas de los ejercicios
**compareTo sin restas** (Nivel 1):
@Override
public int compareTo(Libro otro) {
return Integer.compare(this.paginas, otro.paginas);
}
Comparator encadenado (Nivel 2):
static final Comparator<Jugador> CLASIFICACION = Comparator
.comparingInt(Jugador::getPuntos).reversed() // solo invierte los puntos
.thenComparingInt(Jugador::getPartidos)
.thenComparing(Jugador::getNombre);
**clone() profundo** (Nivel 3):
@Override
public Plantilla clone() {
try {
Plantilla copia = (Plantilla) super.clone();
copia.jugadores = new ArrayList<>();
for (Jugador j : this.jugadores) {
copia.jugadores.add(j.clone()); // cada jugador, también copiado
}
copia.estadio = this.estadio.clone();
return copia;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
Errores que más se repiten
| Síntoma | Causa probable |
|---|---|
ClassCastException … cannot be cast to class java.lang.Comparable | La clase no implementa Comparable y la ordenas sin Comparator |
| «is not abstract and does not override abstract method compareTo» | implements Comparable sin escribir compareTo |
| Orden raro, sin excepción, con valores muy grandes | compareTo con una resta que se desborda |
Faltan elementos en un TreeSet/TreeMap | El criterio da 0 para elementos que consideras distintos |
NullPointerException dentro de sort | Hay un null en la lista o el array |
CloneNotSupportedException | Falta implements Cloneable |
| «clone() has protected access in Object» | Falta redefinir clone() como public |
| La copia cambia al original | Copia superficial: lista u objeto compartidos |
ClassCastException en clone() de una subclase | La superclase hace new en vez de super.clone() |
Antes de entregar, pregúntate
- ¿Mi
compareTousa **Integer.compare/Double.compare/compareTo** y no una resta? - ¿Mi criterio de orden distingue a los elementos que son distintos (hay desempate)?
- ¿Mi
clone()empieza con **super.clone()** y nunca connew? - Si hay listas, arrays u objetos mutables dentro, ¿los he duplicado en la copia?
Si quieres repasar el tema desde la base, vuelve a la teoría de Comparable y Cloneable.
Sigue aprendiendoTodos los artículos de Java¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
