Desafio Java

Record vs Classe: comparando imutabilidade e igualdade

Enunciado

Implemente duas versões de uma entidade Coordenada: uma como record e outra como classe tradicional. Ambas devem ter x e y (int), com validação que rejeita valores negativos. No main, crie instâncias de ambas, compare-as com equals e imprima os resultados. Mostre que o record é imutável (tentativa de modificação gera erro de compilação) e que a classe pode ser mutável se fornecer setters.

Requisitos

  • Defina um record CoordenadaRecord com validação no construtor compacto.
  • Defina uma classe CoordenadaClasse com campos privados, construtor com validação, getters, equals, hashCode e toString.
  • No main, crie instâncias de ambos, compare-as com equals e imprima os resultados.
  • Comente no código que a classe poderia ter setters, mas o record não permite modificação após a criação.

Código inicial

public class Main {
    // Defina o record CoordenadaRecord
    // Defina a classe CoordenadaClasse

    public static void main(String[] args) {
        // Crie instâncias, compare e imprima
    }
}

Saída esperada

Record: CoordenadaRecord[x=1, y=2]
Classe: CoordenadaClasse{x=1, y=2}
Record equals? true
Classe equals? true
Record é imutável, não há setters.
Ver dica

Para a classe, lembre-se de sobrescrever equals e hashCode corretamente. Use Objects.equals e Objects.hash para simplificar.

Mostrar solução
import java.util.Objects;

public class Main {
    // Record com validação
    public record CoordenadaRecord(int x, int y) {
        public CoordenadaRecord {
            if (x < 0 || y < 0) {
                throw new IllegalArgumentException("Coordenadas não podem ser negativas");
            }
        }
    }

    // Classe tradicional com validação e imutabilidade (sem setters)
    public static class CoordenadaClasse {
        private final int x;
        private final int y;

        public CoordenadaClasse(int x, int y) {
            if (x < 0 || y < 0) {
                throw new IllegalArgumentException("Coordenadas não podem ser negativas");
            }
            this.x = x;
            this.y = y;
        }

        public int getX() { return x; }
        public int getY() { return y; }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            CoordenadaClasse that = (CoordenadaClasse) o;
            return x == that.x && y == that.y;
        }

        @Override
        public int hashCode() {
            return Objects.hash(x, y);
        }

        @Override
        public String toString() {
            return "CoordenadaClasse{x=" + x + ", y=" + y + "}";
        }
    }

    public static void main(String[] args) {
        CoordenadaRecord rec1 = new CoordenadaRecord(1, 2);
        CoordenadaRecord rec2 = new CoordenadaRecord(1, 2);
        CoordenadaClasse cls1 = new CoordenadaClasse(1, 2);
        CoordenadaClasse cls2 = new CoordenadaClasse(1, 2);

        System.out.println("Record: " + rec1);
        System.out.println("Classe: " + cls1);
        System.out.println("Record equals? " + rec1.equals(rec2));
        System.out.println("Classe equals? " + cls1.equals(cls2));
        System.out.println("Record é imutável, não há setters.");
        // cls1.setX(5); // Descomente para ver que a classe poderia ser mutável se tivesse setters
    }
}

Passo a passo

  1. Definimos o record CoordenadaRecord com dois componentes x e y e um construtor compacto que valida se são não negativos.
  2. Definimos a classe CoordenadaClasse com campos private final int x, y, construtor com a mesma validação, getters, equals, hashCode e toString.
  3. No main, criamos duas instâncias de cada tipo com os mesmos valores.
  4. Imprimimos o toString de cada uma para mostrar a representação.
  5. Comparamos as instâncias com equals e imprimimos os resultados (ambos true).
  6. Comentamos que a classe poderia ter setters, mas o record não permite, reforçando a imutabilidade.

Por que funciona

Records são inerentemente imutáveis e fornecem automaticamente implementações corretas de equals, hashCode e toString. A classe tradicional exige que o programador escreva esses métodos manualmente, o que é propenso a erros. Além disso, records não têm setters, enquanto classes podem ser mutáveis se desejado. A comparação com equals mostra que ambos os tipos podem ser usados como value objects, mas o record é mais conciso e seguro.

Erros comuns

  • Esquecer de sobrescrever equals e hashCode na classe: Isso faz com que a comparação use identidade de referência, não valores.
    Errado: não sobrescrever.
    Correto: sobrescrever como mostrado.
  • Declarar campos não final na classe e não fornecer setters: Isso ainda permite modificação via reflexão, mas não é o ideal. Prefira final para imutabilidade.
  • Tentar modificar um record: Records não têm setters e seus campos são final. Qualquer tentativa de atribuição resulta em erro de compilação.
    Errado: rec1.x = 10;
    Correto: crie um novo record.
  • Usar getClass() != o.getClass() no equals da classe: Isso é correto, mas se a classe for estendida, pode falhar. Alternativa: usar instanceof se a classe não for final.

Outra forma de resolver

Para a classe, você pode usar Objects.equals e Objects.hash para simplificar:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof CoordenadaClasse)) return false;
    CoordenadaClasse that = (CoordenadaClasse) o;
    return x == that.x && y == that.y;
}

Mas a versão com getClass() é mais rigorosa. Records são preferíveis quando a imutabilidade e a concisão são desejadas.

Saída esperada

Record: CoordenadaRecord[x=1, y=2]
Classe: CoordenadaClasse{x=1, y=2}
Record equals? true
Classe equals? true
Record é imutável, não há setters.