</>waridocu

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.

javaobjecttostringequalshashcodepoo

El problema: dos salidas raras

Estas dos cosas le pasan a todo el mundo la primera vez que trabaja con clases propias:

Ejemplo: los dos resultados que no esperabasjava
Alumno a = new Alumno("Ana", 8.5);
System.out.println(a);

Alumno b = new Alumno("Ana", 8.5);
System.out.println(a == b);
texttext
Alumno@6d06d69c
false

println 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:

javajava
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:

javajava
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 heredadoQué hace la versión de ObjectQué querrías que hiciera
toString()Devuelve NombreClase@códigoHexadecimalDevolver un texto legible con los datos del objeto
equals(Object o)Compara si son el mismo objeto en memoriaComparar 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.

Ejemplo: darle a la clase una representación legiblejava
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 + "}";
    }
}
javajava
Alumno a = new Alumno("Ana", 8.5);
System.out.println(a);
System.out.println("El alumno es: " + a);
texttext
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:

javajava
ArrayList<Alumno> curso = new ArrayList<>();
curso.add(new Alumno("Ana", 8.5));
curso.add(new Alumno("Beto", 6.0));
System.out.println(curso);
texttext
[Alumno{nombre='Ana', nota=8.5}, Alumno{nombre='Beto', nota=6.0}]
Nota

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.

javajava
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);   // true

Cada 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ónQué 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(...)
Advertencia

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ú:

Ejemplo: dos alumnos son el mismo si coinciden nombre y notajava
@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íneaPara qué está
if (this == obj) return true;Atajo: si es literalmente el mismo objeto, no hace falta comparar nada
obj == nulla.equals(null) tiene que dar false, no reventar
getClass() != obj.getClass()Un Alumno no es igual a un Profesor aunque compartan el nombre
(Alumno) objEl 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 ==
Importante

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.

Ejemplo: equals y hashCode siempre juntosjava
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+Insertequals() and hashCode()), y es perfectamente aceptable usar el generado.

Pon a prueba

🎯 ¿Qué imprime este programa?
Alumno tiene equals() sobrescrito comparando nombre y notajava
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));
  • Correcto. == compara referencias y los dos new crearon objetos distintos, así que da false. equals está sobrescrito comparando los campos, y "Ana" y 8.5 coinciden, así que da true. Ese contraste es toda la diferencia entre "el mismo objeto" y "el mismo valor".

  • Sería así si == comparara contenido, pero para objetos compara la dirección en memoria. Dos new distintos nunca dan la misma dirección, por más que los valores coincidan.

  • Sería la salida si equals NO estuviera sobrescrito: heredaría la versión de Object, que hace exactamente lo mismo que ==. Pero aquí sí está sobrescrito comparando campos.

  • Al revés: Object es la firma correcta, porque es la que tiene el método original que se está sobrescribiendo. Recibir un Alumno compilaría igual, pero sería una sobrecarga y no reemplazaría al equals heredado.

💡 Predice la salida

La clase Punto sobrescribe toString() pero no equals(). ¿Qué imprime esto?

javajava
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) porque toString() sí está sobrescrito y println lo usa.
  • false porque equals() no está sobrescrito: se usa el heredado de Object, que compara referencias igual que ==. Dos new distintos, dos objetos distintos.
  • El tercer false es el que hace daño en un programa real: ArrayList.contains recorre la lista preguntando elemento.equals(p2). Como ese equals compara 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.

✏️ Completa toString y equals

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 el código

Ordena las líneas de un equals() bien escrito para una clase Ciudad que se identifica por su nombre.

  1. if (obj == null || getClass() != obj.getClass()) return false;
  2. }
  3. return nombre.equals(otra.nombre);
  4. public boolean equals(Object obj) {
  5. @Override
  6. if (this == obj) return true;
  7. 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.