Java
Object, toString y equals
Por qué toda clase hereda de Object sin escribirlo, qué imprime println cuando le pasas un objeto, y por qué comparar con == casi nunca hace lo que esperas.
El problema: dos salidas raras
Estas dos cosas le pasan a todo el mundo la primera vez que trabaja con clases propias:
Alumno a = new Alumno("Ana", 8.5);
System.out.println(a);
Alumno b = new Alumno("Ana", 8.5);
System.out.println(a == b);Alumno@6d06d69c
falseprintln no imprime los datos del alumno sino un código extraño, y dos alumnos con exactamente los mismos valores resultan no ser iguales. Ninguna de las dos cosas es un bug: las dos vienen del mismo lugar, una clase que heredaste sin escribir nada.
Toda clase hereda de Object
En Java, si una clase no escribe extends, hereda automáticamente de Object. Es decir, estas dos líneas significan exactamente lo mismo:
public class Alumno { ... }
public class Alumno extends Object { ... }Y si sí escribe extends, el padre a su vez hereda de Object — o el padre de su padre. Al final de toda cadena de herencia está siempre Object. Por eso Object es el único tipo capaz de guardar literalmente cualquier objeto:
Object cualquiera = new Alumno("Ana", 8.5);
Object otra = "un String";
Object masOtra = new ArrayList<String>();De Object vienen unos pocos métodos que toda clase tiene desde el minuto cero. Los dos que importan ahora:
| Método heredado | Qué hace la versión de Object | Qué querrías que hiciera |
|---|---|---|
toString() | Devuelve NombreClase@códigoHexadecimal | Devolver un texto legible con los datos del objeto |
equals(Object o) | Compara si son el mismo objeto en memoria | Comparar si tienen los mismos valores |
Las dos salidas raras del principio son simplemente esas implementaciones por defecto haciendo su trabajo. La solución en los dos casos es la misma: sobrescribirlas, con la técnica de Herencia y @Override.
toString(): cómo se imprime tu objeto
println no tiene ninguna magia: cuando le pasas un objeto, llama a su toString(). La versión de Object devuelve el nombre de la clase, una @, y el hash code del objeto en hexadecimal — un identificador interno que no le dice nada a nadie.
public class Alumno {
private String nombre;
private double nota;
public Alumno(String nombre, double nota) {
this.nombre = nombre;
this.nota = nota;
}
@Override
public String toString() {
return "Alumno{nombre='" + nombre + "', nota=" + nota + "}";
}
}Alumno a = new Alumno("Ana", 8.5);
System.out.println(a);
System.out.println("El alumno es: " + a);Alumno{nombre='Ana', nota=8.5}
El alumno es: Alumno{nombre='Ana', nota=8.5}La segunda línea muestra algo que sorprende: la concatenación con + también llama a toString(). Lo mismo pasa dentro de una lista:
ArrayList<Alumno> curso = new ArrayList<>();
curso.add(new Alumno("Ana", 8.5));
curso.add(new Alumno("Beto", 6.0));
System.out.println(curso);[Alumno{nombre='Ana', nota=8.5}, Alumno{nombre='Beto', nota=6.0}]Escribir un buen toString() es de las cosas más rentables al depurar: convierte cada System.out.println(objeto) y cada inspección en el debugger en información útil en vez de Alumno@6d06d69c. El formato Clase{campo=valor, campo=valor} es la convención que usa la mayoría de las herramientas de Java.
== compara referencias, no contenido
La segunda sorpresa necesita entender qué guarda realmente una variable de tipo objeto. a no contiene el alumno: contiene una referencia, la dirección donde vive ese alumno en memoria.
Alumno a = new Alumno("Ana", 8.5);
Alumno b = new Alumno("Ana", 8.5);
Alumno c = a;
System.out.println(a == b); // false
System.out.println(a == c); // trueCada new crea un objeto nuevo en un lugar nuevo. a y b apuntan a dos objetos distintos que casualmente tienen los mismos valores, así que sus referencias son distintas y == da false. c = a no copia nada: copia la referencia, de modo que a y c apuntan al mismo objeto y == da true.
La regla completa:
| Comparación | Qué hace == | Qué usar |
|---|---|---|
Dos int, double, char, boolean (primitivos) | Compara los valores. Funciona bien. | == |
Dos objetos (String, Alumno, ArrayList…) | Compara las referencias, casi nunca lo que quieres | .equals(...) |
Esto incluye a String, y es el error más común de todo Java principiante. nombre == "Ana" a veces da true y a veces false según de dónde salió el texto, porque Java reutiliza los literales escritos en el código pero no los String construidos en tiempo de ejecución (por ejemplo, los que devuelve Scanner). Compara siempre con nombre.equals("Ana").
equals(): definir qué significa “iguales”
String ya trae su equals sobrescrito y por eso compara letra por letra. Para tu clase, ese “qué significa iguales” lo decides tú:
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Alumno otro = (Alumno) obj;
return Double.compare(nota, otro.nota) == 0 && nombre.equals(otro.nombre);
}Línea por línea, porque cada una tapa un agujero concreto:
| Línea | Para qué está |
|---|---|
if (this == obj) return true; | Atajo: si es literalmente el mismo objeto, no hace falta comparar nada |
obj == null | a.equals(null) tiene que dar false, no reventar |
getClass() != obj.getClass() | Un Alumno no es igual a un Profesor aunque compartan el nombre |
(Alumno) obj | El parámetro es Object; hay que bajarlo para poder leer sus campos |
nombre.equals(...) | Los campos que son objetos se comparan con equals, no con == |
La firma tiene que ser equals(Object obj) con parámetro Object, no equals(Alumno otro). Si escribes el segundo estás sobrecargando, no sobrescribiendo, y Object.equals sigue vigente para todo el código que no sepa que tu objeto es un Alumno — incluidos ArrayList.contains y compañía. @Override te avisa de esto al instante; sin la anotación, compila y falla en silencio.
hashCode va con equals
Hay una regla que la biblioteca estándar da por sentada: si dos objetos son equals, deben devolver el mismo hashCode(). Estructuras como HashMap y HashSet primero agrupan por hashCode y solo después comparan con equals; si sobrescribes uno sin el otro, un objeto puede “desaparecer” de un HashSet que sí lo contiene.
Escribirlo a mano es innecesario: Objects.hash(...) lo resuelve.
import java.util.Objects;
@Override
public int hashCode() {
return Objects.hash(nombre, nota);
}La regla práctica: sobrescribe siempre los dos juntos, con los mismos campos. Cualquier IDE los genera por ti (en IntelliJ, Alt+Insert → equals() and hashCode()), y es perfectamente aceptable usar el generado.
Pon a prueba
Alumno a = new Alumno("Ana", 8.5);
Alumno b = new Alumno("Ana", 8.5);
System.out.println(a == b);
System.out.println(a.equals(b));La clase Punto sobrescribe toString() pero no equals(). ¿Qué imprime esto?
public class Punto {
private int x, y;
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
// en main:
Punto p1 = new Punto(1, 2);
Punto p2 = new Punto(1, 2);
System.out.println(p1);
System.out.println(p1.equals(p2));
ArrayList<Punto> puntos = new ArrayList<>();
puntos.add(p1);
System.out.println(puntos.contains(p2));Imprime (1, 2), después false, y después false.
(1, 2)porquetoString()sí está sobrescrito yprintlnlo usa.falseporqueequals()no está sobrescrito: se usa el heredado deObject, que compara referencias igual que==. Dosnewdistintos, dos objetos distintos.- El tercer
falsees el que hace daño en un programa real:ArrayList.containsrecorre la lista preguntandoelemento.equals(p2). Como eseequalscompara referencias, la lista contiene un punto (1, 2) y aun así responde que no.
Esto se arregla sobrescribiendo equals (y hashCode junto con él). Es la razón por la que conviene escribir los dos apenas una clase representa un valor que se va a guardar en colecciones.
Producto se identifica por su código. Completa la anotación, el tipo del parámetro de equals y el método que compara Strings correctamente.
public class Producto {
private String codigo;
private double precio;
public String toString() {
return "Producto{codigo='" + codigo + "', precio=" + precio + "}";
}
@Override
public boolean equals( obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Producto otro = () obj;
return codigo.(otro.codigo);
}
}Ordena las líneas de un equals() bien escrito para una clase Ciudad que se identifica por su nombre.
- if (obj == null || getClass() != obj.getClass()) return false;
- }
- return nombre.equals(otra.nombre);
- public boolean equals(Object obj) {
- @Override
- if (this == obj) return true;
- Ciudad otra = (Ciudad) obj;
Siguiente paso
Queda un par de palabras clave que vienes viendo desde el primer programa sin que nadie las explicara del todo: el static de public static void main y el final que aparece en las constantes — ver static y final.