1. Interfaz Comparable<T>
¿Alguna vez has ordenado una lista de números o cadenas? ¡Claro que sí! Ahora imagina que tienes una lista de tus propios objetos — por ejemplo, una lista de estudiantes, productos o gatitos. ¿Cómo sabrá Java en qué orden ordenarlos? Justo para eso existe la interfaz Comparable<T>.
Esta interfaz define el «orden natural» de los objetos — es decir, el orden que tiene sentido para ese tipo de datos. Por ejemplo, para números — ascendente; para cadenas — alfabético; para estudiantes — por apellido o por edad (tú eliges).
Cómo está hecho Comparable
La interfaz es muy sencilla: solo tiene un método:
public interface Comparable<T> {
int compareTo(T o);
}
El método compareTo debe devolver:
- un número negativo si el objeto actual es «menor» que el otro;
- 0 si «igual»;
- un número positivo si «mayor».
Ejemplo: ordenamos estudiantes por edad
Añadamos la clase Student e implementemos para ella la interfaz Comparable<Student>:
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// Getters para el ejemplo
public String getName() { return name; }
public int getAge() { return age; }
@Override
public int compareTo(Student other) {
// Ordenamos por edad (ascendente)
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Ahora podemos ordenar fácilmente un array o una lista de estudiantes:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("John", 20));
students.add(new Student("Alice", 18));
students.add(new Student("Charlie", 22));
Collections.sort(students); // ¡Funciona gracias a Comparable!
System.out.println("Estudiantes ordenados:");
for (Student s : students) {
System.out.println(s);
}
}
}
Resultado:
Alice (18)
John (20)
Charlie (22)
Un matiz importante
Si implementas Comparable, intenta que compareTo sea coherente con equals. Es decir, si a.compareTo(b) == 0, entonces a.equals(b) debería ser true. De lo contrario, la ordenación y las colecciones pueden comportarse de forma impredecible, y tendrás motivos para filosofar sobre el sentido de la vida del programador.
2. Interfaz Serializable
La serialización es la capacidad de un objeto para convertirse en una secuencia de bytes (por ejemplo, para guardarse en un archivo o enviarse por la red) y luego restaurarse de nuevo. Imagina que quieres guardar el estado de tu juego o enviar un objeto al servidor — sin serialización, imposible.
En Java existe para ello una interfaz marcadora Serializable. Marcadora significa que no contiene métodos y simplemente «marca» la clase como serializable.
import java.io.Serializable;
public class Student implements Serializable {
private String name;
private int age;
// ... resto del código
}
Cómo serializar un objeto
Para serializar y deserializar se usan las clases ObjectOutputStream y ObjectInputStream. Ejemplo — guardamos un objeto en un archivo y lo leemos de vuelta:
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
Student s = new Student("Diana", 19);
// Guardamos el objeto en un archivo
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
out.writeObject(s);
}
// Leemos el objeto desde el archivo
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat"))) {
Student loaded = (Student) in.readObject();
System.out.println("Cargado: " + loaded);
}
}
}
Nota: Todos los campos del objeto (y de los objetos anidados) también deben ser serializables; de lo contrario habrá un error.
Para qué sirve la interfaz marcadora
La interfaz Serializable no exige implementar métodos — simplemente informa a la JVM: «este objeto se puede serializar». Si olvidas implementarla, el intento de serialización lanzará la excepción NotSerializableException.
3. Otras interfaces importantes de la biblioteca estándar
Interfaz Cloneable
Otra interfaz marcadora. Su cometido es hacer saber a la JVM que el objeto se puede clonar mediante el método Object.clone(). Sin ella, el intento de llamar a clone() lanzará una excepción.
Sin embargo, el clonado en Java tiene trampa. El clonado por defecto es superficial (shallow copy), y a menudo en su lugar es mejor escribir métodos de copia propios.
public class Student implements Cloneable {
private String name;
private int age;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Interfaz AutoCloseable
Esta interfaz contiene un único método close(). Cualquier clase que la implemente puede usarse en la construcción try-with-resources — cierre automático de recursos (por ejemplo, archivos, flujos):
public class MyResource implements AutoCloseable {
@Override
public void close() {
System.out.println("¡Recurso cerrado!");
}
}
public class Main {
public static void main(String[] args) {
try (MyResource res = new MyResource()) {
System.out.println("Trabajando con el recurso");
}
// Aquí se invocará res.close() automáticamente
}
}
Interfaz Iterable<T>
Esta interfaz permite que tu objeto sea «recorrible» en un bucle for-each. Contiene un único método iterator(), que devuelve un objeto Iterator<T>.
public class MyList implements Iterable<String> {
// ... almacenamiento interno
@Override
public java.util.Iterator<String> iterator() {
// Devolvemos un iterador para recorrer los elementos
return ...;
}
}
Todas las colecciones estándar (ArrayList, HashSet, etc.) implementan Iterable, por eso se pueden recorrer con for-each.
Interfaz Comparator<T>
Esta interfaz permite comparar objetos con reglas distintas, sin cambiar los propios objetos. Por ejemplo, ordenar estudiantes por nombre en lugar de por edad.
import java.util.Comparator;
Comparator<Student> byName = new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.getName().compareTo(b.getName());
}
};
En Java moderno esto suele hacerse con expresiones lambda:
Comparator<Student> byName = (a, b) -> a.getName().compareTo(b.getName());
Observer, EventListener
Estas interfaces se usan para implementar los patrones «observador» y «escuchador de eventos» — cuando un objeto reacciona a eventos que ocurren en otro. Por ejemplo, en interfaces gráficas (Swing, JavaFX) los manejadores de botones implementan la interfaz ActionListener.
4. Práctica: implementamos Comparable y serializamos un objeto
Ejemplo 1. Comparable para tu propia clase
Escribamos una clase Book que se pueda ordenar por año de edición:
public class Book implements Comparable<Book> {
private String title;
private int year;
public Book(String title, int year) {
this.title = title;
this.year = year;
}
@Override
public int compareTo(Book other) {
return Integer.compare(this.year, other.year);
}
@Override
public String toString() {
return title + " (" + year + ")";
}
}
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Book> books = Arrays.asList(
new Book("Java para Dummies", 2018),
new Book("Guerra y paz", 1869),
new Book("Harry Potter", 1997)
);
Collections.sort(books);
System.out.println(books);
}
}
Resultado:
[Guerra y paz (1869), Harry Potter (1997), Java para Dummies (2018)]
Ejemplo 2. Serialización de un objeto
import java.io.*;
public class Book implements Serializable {
private String title;
private int year;
// ... constructor, getters, toString
public Book(String title, int year) {
this.title = title;
this.year = year;
}
@Override
public String toString() {
return title + " (" + year + ")";
}
}
public class Main {
public static void main(String[] args) throws Exception {
Book book = new Book("Java para Dummies", 2018);
// Guardamos el objeto en un archivo
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.dat"))) {
out.writeObject(book);
}
// Leemos el objeto desde el archivo
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.dat"))) {
Book loaded = (Book) in.readObject();
System.out.println("Cargado: " + loaded);
}
}
}
5. Tabla: interfaces principales de la biblioteca estándar
| Interfaz | Finalidad | Métodos clave | Ejemplo de uso |
|---|---|---|---|
|
Orden natural de los objetos | |
Ordenación de listas |
|
Comparación personalizada de objetos | |
Ordenación con distintas reglas |
|
Serialización de objetos | — (marcador) | Guardado/carga de objetos |
|
Clonado de objetos | — (marcador) | Creación de copias de objetos |
|
Cierre automático de recursos | |
try-with-resources |
|
Recorrido de elementos en colecciones | |
Bucle for-each |
| Observer / EventListener | Reacción a eventos | |
Gestión de eventos en la UI, patrones |
6. Errores típicos al trabajar con las interfaces estándar
Error n.º 1: No se ha implementado la interfaz, pero se necesita la funcionalidad.
Por ejemplo, olvidaste implementar Serializable y tratas de serializar un objeto — obtendrás NotSerializableException. De forma análoga con Cloneable y la llamada a clone().
Error n.º 2: Incumplir el contrato de Comparable y equals.
Si a.compareTo(b) == 0 pero no se cumple a.equals(b), las colecciones pueden comportarse de forma extraña. Por ejemplo, TreeSet puede «perder» objetos.
Error n.º 3: Copia superficial al clonar.
El método clone() por defecto solo copia la «capa superior» del objeto. Si tienes campos que son referencias a otros objetos, no se copian en profundidad. Esto puede llevar a bugs misteriosos.
Error n.º 4: No usar try-with-resources.
Si una clase implementa AutoCloseable pero no la usas con try-with-resources, corres el riesgo de olvidar cerrar el recurso — y provocar una fuga de memoria o el bloqueo de un archivo.
Error n.º 5: Implementación incorrecta de compareTo o compare.
Si devuelves solo 0 o 1, en lugar de un número negativo/cero/positivo, la ordenación funcionará de forma incorrecta.
GO TO FULL VERSION