Qual a Diferença entre == e equals() em Java? Um Guia Completo

📖 História: O Bug que Custou uma Tarde Inteira

Era uma sexta-feira à tarde na TechCorp quando Marina, desenvolvedora Java sênior, recebeu uma ligação urgente do Carlos, um desenvolvedor júnior da equipe.

Carlos: “Marina, preciso da sua ajuda! O sistema de autenticação está com um bug estranho. Às vezes funciona, às vezes não. Estou há 3 horas tentando resolver!”

Marina: “Calma, Carlos. Me mostra o código do método de validação.”

Carlos: “Aqui está o problema. Estou comparando as senhas assim:”

public boolean validarSenha(String senhaDigitada, String senhaArmazenada) {
    return senhaDigitada == senhaArmazenada;  // ❌ ERRO!
}

Marina: “Ahá! Encontrei o problema. Você está usando == para comparar Strings. Isso compara referências de memória, não o conteúdo das strings!”

Carlos: “Não entendi… Às vezes funciona!”

Marina: “Exato! Quando as strings vêm do pool de strings (literais), elas podem ter a mesma referência. Mas quando vêm de diferentes fontes, como entrada do usuário ou banco de dados, são objetos diferentes na memória.”

Carlos: “Então como resolvo?”

Marina: “Use o método equals(). Ele compara o conteúdo das strings, não suas referências. Vamos criar um projeto completo para você entender melhor.”

🛠️ Criando o Projeto no IntelliJ IDEA

Passo 1: Novo Projeto Maven

  1. Abra o IntelliJ IDEA (versão 2024.1 ou superior)
  2. File > New > Project
  3. Selecione “Maven Archetype”
  4. Configure o projeto:
    • Name: java-comparacao-demo
    • Location: Escolha seu diretório preferido
    • Language: Java
    • Build System: Maven
    • JDK: 17 ou superior
    • Add sample code: ✅ Marque esta opção

Passo 2: Configuração do pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.exemplo</groupId>
    <artifactId>java-comparacao-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.2</version>
            </plugin>
        </plugins>
    </build>
</project>

📚 Conceitos Fundamentais Detalhados

1. Operador == (Comparação de Referência)

O operador == em Java funciona de forma diferente dependendo do tipo de dados:

🔹 Para Tipos Primitivos:

  • Compara os valores diretamente
  • intdoublebooleanchar, etc.
  • Armazenados na stack (pilha)

🔹 Para Objetos:

  • Compara referências de memória (endereços)
  • Verifica se ambas as variáveis apontam para o mesmo objeto na heap
  • Não considera o conteúdo do objeto

2. Método equals() (Comparação de Conteúdo)

O método equals() é definido na classe Object e pode ser sobrescrito:

🔹 Implementação Padrão:

  • Comporta-se como == (compara referências)
  • Definido em Object.equals()

🔹 Implementação Personalizada:

  • Classes como StringIntegerDouble sobrescrevem o método
  • Permite definir critérios customizados de igualdade
  • Deve seguir o contrato do equals()

3. Pool de Strings – Conceito Crucial

O Java mantém um pool de strings para otimizar memória:

String str1 = "Java";        // Literal - vai para o pool
String str2 = "Java";        // Reutiliza a mesma referência do pool
String str3 = new String("Java"); // Cria novo objeto na heap

💻 Exemplo Prático Completo

Classe Principal: ComparacaoDemo.java

package com.exemplo;

import java.util.Objects;
import java.util.Scanner;

/**
 * Demonstração prática das diferenças entre == e equals() em Java
 * 
 * @author Marina Silva
 * @version 1.0
 */
public class ComparacaoDemo {
    
    public static void main(String[] args) {
        System.out.println("=== DEMONSTRAÇÃO: == vs equals() ===\n");
        
        // Demonstrações individuais
        demonstrarPrimitivos();
        demonstrarStrings();
        demonstrarObjetos();
        demonstrarCasoReal();
        
        System.out.println("\n=== FIM DA DEMONSTRAÇÃO ===");
    }
    
    /**
     * Demonstra comparação com tipos primitivos
     */
    private static void demonstrarPrimitivos() {
        System.out.println("🔢 TIPOS PRIMITIVOS:");
        System.out.println("-------------------");
        
        int numero1 = 42;
        int numero2 = 42;
        int numero3 = 50;
        
        System.out.printf("numero1 = %d, numero2 = %d, numero3 = %d%n", numero1, numero2, numero3);
        System.out.printf("numero1 == numero2: %b%n", numero1 == numero2);
        System.out.printf("numero1 == numero3: %b%n", numero1 == numero3);
        
        double preco1 = 19.99;
        double preco2 = 19.99;
        System.out.printf("preco1 == preco2: %b%n", preco1 == preco2);
        
        System.out.println();
    }
    
    /**
     * Demonstra comparação com Strings
     */
    private static void demonstrarStrings() {
        System.out.println("📝 STRINGS:");
        System.out.println("----------");
        
        // Strings literais (pool)
        String str1 = "Java";
        String str2 = "Java";
        System.out.println("String literais:");
        System.out.printf("str1 == str2: %b%n", str1 == str2);
        System.out.printf("str1.equals(str2): %b%n", str1.equals(str2));
        
        // Strings com new (objetos diferentes)
        String str3 = new String("Java");
        String str4 = new String("Java");
        System.out.println("\nString com new:");
        System.out.printf("str3 == str4: %b%n", str3 == str4);
        System.out.printf("str3.equals(str4): %b%n", str3.equals(str4));
        
        // Comparação entre literal e new
        System.out.println("\nLiteral vs New:");
        System.out.printf("str1 == str3: %b%n", str1 == str3);
        System.out.printf("str1.equals(str3): %b%n", str1.equals(str3));
        
        // Demonstração da identidade dos objetos
        System.out.println("\nIdentidade dos objetos:");
        System.out.printf("str1.hashCode(): %d%n", str1.hashCode());
        System.out.printf("str2.hashCode(): %d%n", str2.hashCode());
        System.out.printf("str3.hashCode(): %d%n", str3.hashCode());
        System.out.printf("System.identityHashCode(str1): %d%n", System.identityHashCode(str1));
        System.out.printf("System.identityHashCode(str3): %d%n", System.identityHashCode(str3));
        
        System.out.println();
    }
    
    /**
     * Demonstra comparação com objetos personalizados
     */
    private static void demonstrarObjetos() {
        System.out.println("👤 OBJETOS PERSONALIZADOS:");
        System.out.println("-------------------------");
        
        // Criando usuários
        Usuario usuario1 = new Usuario("carlos.silva", "Carlos Silva", 28);
        Usuario usuario2 = new Usuario("carlos.silva", "Carlos Silva", 28);
        Usuario usuario3 = usuario1; // mesma referência
        Usuario usuario4 = new Usuario("marina.santos", "Marina Santos", 32);
        
        System.out.println("Usuários criados:");
        System.out.println("usuario1: " + usuario1);
        System.out.println("usuario2: " + usuario2);
        System.out.println("usuario4: " + usuario4);
        
        System.out.println("\nComparações:");
        System.out.printf("usuario1 == usuario2: %b (objetos diferentes, mesmo conteúdo)%n", usuario1 == usuario2);
        System.out.printf("usuario1.equals(usuario2): %b%n", usuario1.equals(usuario2));
        System.out.printf("usuario1 == usuario3: %b (mesma referência)%n", usuario1 == usuario3);
        System.out.printf("usuario1.equals(usuario3): %b%n", usuario1.equals(usuario3));
        System.out.printf("usuario1.equals(usuario4): %b (conteúdo diferente)%n", usuario1.equals(usuario4));
        
        System.out.println();
    }
    
    /**
     * Demonstra caso real - sistema de autenticação
     */
    private static void demonstrarCasoReal() {
        System.out.println("🔐 CASO REAL - SISTEMA DE AUTENTICAÇÃO:");
        System.out.println("-------------------------------------");
        
        // Simula diferentes fontes de string
        String senhaArmazenada = "123456"; // Literal (pool)
        String senhaDigitada = new String("123456"); // Vem de input do usuário
        String senhaInvalida = "654321";
        
        SistemaAutenticacao sistema = new SistemaAutenticacao();
        
        System.out.println("Testando validação de senhas:");
        System.out.printf("Senha armazenada: '%s'%n", senhaArmazenada);
        System.out.printf("Senha digitada: '%s'%n", senhaDigitada);
        
        // Método incorreto (usando ==)
        boolean loginIncorreto = sistema.validarSenhaIncorreto(senhaDigitada, senhaArmazenada);
        System.out.printf("Login com == (INCORRETO): %b%n", loginIncorreto);
        
        // Método correto (usando equals)
        boolean loginCorreto = sistema.validarSenhaCorreto(senhaDigitada, senhaArmazenada);
        System.out.printf("Login com equals() (CORRETO): %b%n", loginCorreto);
        
        // Teste com senha inválida
        boolean loginInvalido = sistema.validarSenhaCorreto(senhaInvalida, senhaArmazenada);
        System.out.printf("Login com senha inválida: %b%n", loginInvalido);
    }
}

/**
 * Classe que representa um usuário do sistema
 */
class Usuario {
    private String username;
    private String nomeCompleto;
    private int idade;
    
    public Usuario(String username, String nomeCompleto, int idade) {
        this.username = username;
        this.nomeCompleto = nomeCompleto;
        this.idade = idade;
    }
    
    /**
     * Implementação correta do equals() seguindo o contrato
     */
    @Override
    public boolean equals(Object obj) {
        // 1. Verificação de referência (otimização)
        if (this == obj) return true;
        
        // 2. Verificação de null
        if (obj == null) return false;
        
        // 3. Verificação de classe
        if (getClass() != obj.getClass()) return false;
        
        // 4. Cast e comparação de campos
        Usuario usuario = (Usuario) obj;
        return idade == usuario.idade &&
               Objects.equals(username, usuario.username) &&
               Objects.equals(nomeCompleto, usuario.nomeCompleto);
    }
    
    /**
     * Implementação do hashCode() - OBRIGATÓRIO quando equals() é sobrescrito
     */
    @Override
    public int hashCode() {
        return Objects.hash(username, nomeCompleto, idade);
    }
    
    @Override
    public String toString() {
        return String.format("Usuario{username='%s', nome='%s', idade=%d}", 
                           username, nomeCompleto, idade);
    }
    
    // Getters
    public String getUsername() { return username; }
    public String getNomeCompleto() { return nomeCompleto; }
    public int getIdade() { return idade; }
}

/**
 * Sistema de autenticação para demonstrar o problema
 */
class SistemaAutenticacao {
    
    /**
     * Método INCORRETO - usa == para comparar strings
     */
    public boolean validarSenhaIncorreto(String senhaDigitada, String senhaArmazenada) {
        return senhaDigitada == senhaArmazenada; // ❌ ERRO!
    }
    
    /**
     * Método CORRETO - usa equals() para comparar strings
     */
    public boolean validarSenhaCorreto(String senhaDigitada, String senhaArmazenada) {
        if (senhaDigitada == null || senhaArmazenada == null) {
            return false;
        }
        return senhaDigitada.equals(senhaArmazenada); // ✅ CORRETO!
    }
}

🧪 Teste Unitário (JUnit 5)

Crie o arquivo src/test/java/com/exemplo/ComparacaoTest.java:

package com.exemplo;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("Testes de Comparação: == vs equals()")
class ComparacaoTest {
    
    private SistemaAutenticacao sistema;
    
    @BeforeEach
    void setUp() {
        sistema = new SistemaAutenticacao();
    }
    
    @Test
    @DisplayName("Deve comparar strings literais corretamente")
    void testComparacaoStringsLiterais() {
        String str1 = "Java";
        String str2 = "Java";
        String str3 = new String("Java");
        
        // Literais têm mesma referência
        assertTrue(str1 == str2, "Strings literais devem ter mesma referência");
        assertTrue(str1.equals(str2), "Strings literais devem ser iguais por conteúdo");
        
        // new String cria objeto diferente
        assertFalse(str1 == str3, "String literal vs new String devem ter referências diferentes");
        assertTrue(str1.equals(str3), "String literal vs new String devem ser iguais por conteúdo");
    }
    
    @Test
    @DisplayName("Deve comparar objetos Usuario corretamente")
    void testComparacaoUsuarios() {
        Usuario user1 = new Usuario("test", "Test User", 25);
        Usuario user2 = new Usuario("test", "Test User", 25);
        Usuario user3 = user1;
        Usuario user4 = new Usuario("other", "Other User", 30);
        
        // Objetos diferentes, mesmo conteúdo
        assertFalse(user1 == user2, "Objetos diferentes devem ter referências diferentes");
        assertTrue(user1.equals(user2), "Objetos com mesmo conteúdo devem ser iguais");
        
        // Mesma referência
        assertTrue(user1 == user3, "Mesma referência deve ser igual com ==");
        assertTrue(user1.equals(user3), "Mesma referência deve ser igual com equals()");
        
        // Conteúdo diferente
        assertFalse(user1.equals(user4), "Objetos com conteúdo diferente devem ser diferentes");
    }
    
    @Test
    @DisplayName("Deve validar sistema de autenticação")
    void testSistemaAutenticacao() {
        String senhaArmazenada = "123456";
        String senhaDigitada = new String("123456");
        String senhaInvalida = "654321";
        
        // Método incorreto pode falhar
        assertFalse(sistema.validarSenhaIncorreto(senhaDigitada, senhaArmazenada),
                   "Método incorreto deve falhar com strings de fontes diferentes");
        
        // Método correto sempre funciona
        assertTrue(sistema.validarSenhaCorreto(senhaDigitada, senhaArmazenada),
                  "Método correto deve funcionar independente da fonte das strings");
        
        // Senha inválida
        assertFalse(sistema.validarSenhaCorreto(senhaInvalida, senhaArmazenada),
                   "Senha inválida deve retornar false");
    }
    
    @Test
    @DisplayName("Deve tratar valores null corretamente")
    void testValoresNull() {
        // Teste com valores null
        assertFalse(sistema.validarSenhaCorreto(null, "123456"),
                   "Senha null deve retornar false");
        assertFalse(sistema.validarSenhaCorreto("123456", null),
                   "Senha armazenada null deve retornar false");
        assertFalse(sistema.validarSenhaCorreto(null, null),
                   "Ambas senhas null devem retornar false");
    }
    
    @Test
    @DisplayName("Deve validar contrato equals() e hashCode()")
    void testContratoEqualsHashCode() {
        Usuario user1 = new Usuario("test", "Test User", 25);
        Usuario user2 = new Usuario("test", "Test User", 25);
        Usuario user3 = new Usuario("other", "Other User", 30);
        
        // Reflexividade
        assertTrue(user1.equals(user1), "equals() deve ser reflexivo");
        
        // Simetria
        assertTrue(user1.equals(user2), "equals() deve ser simétrico");
        assertTrue(user2.equals(user1), "equals() deve ser simétrico");
        
        // Consistência do hashCode
        assertEquals(user1.hashCode(), user2.hashCode(),
                    "Objetos iguais devem ter mesmo hashCode");
        
        // Objetos diferentes podem ter hashCodes diferentes
        assertNotEquals(user1.hashCode(), user3.hashCode(),
                       "Objetos diferentes devem ter hashCodes diferentes");
        
        // Null safety
        assertFalse(user1.equals(null), "equals(null) deve retornar false");
    }
}

📋 Executando o Projeto

  1. Compile o projeto:mvn clean compile
  2. Execute a classe principal:mvn exec:java -Dexec.mainClass="com.exemplo.ComparacaoDemo"
  3. Execute os testes:mvn test
  4. Execute testes com relatório detalhado:mvn test -Dtest=ComparacaoTest

🎯 Boas Práticas e Regras Importantes

1. Contrato do equals()

Ao sobrescrever equals(), deve-se seguir estas regras:

  • Reflexiva: x.equals(x) deve retornar true
  • Simétrica: x.equals(y) deve retornar o mesmo que y.equals(x)
  • Transitiva: Se x.equals(y) e y.equals(z), então x.equals(z)
  • Consistente: Múltiplas chamadas devem retornar o mesmo resultado
  • Null-safe: x.equals(null) deve retornar false

2. Sempre sobrescrever hashCode()

@Override
public int hashCode() {
    return Objects.hash(campo1, campo2, campo3);
}

3. Verificações de Segurança

// Sempre verificar null antes de chamar equals()
if (str1 != null && str1.equals(str2)) {
    // código seguro
}

// Ou usar Objects.equals() para verificação automática de null
if (Objects.equals(str1, str2)) {
    // código seguro
}

4. Comparação de Arrays

int[] array1 = {1, 2, 3};
int[] array2 = {1, 2, 3};

// ❌ INCORRETO - compara referências
System.out.println(array1 == array2);      // false
System.out.println(array1.equals(array2)); // false

// ✅ CORRETO - compara conteúdo
System.out.println(Arrays.equals(array1, array2)); // true

🔍 Casos Especiais e Pegadinhas

1. Autoboxing e Pool de Inteiros

Integer num1 = 127;
Integer num2 = 127;
Integer num3 = 128;
Integer num4 = 128;

System.out.println(num1 == num2); // true (pool -128 a 127)
System.out.println(num3 == num4); // false (fora do pool)

2. Strings e StringBuilder

String str = "Hello";
StringBuilder sb = new StringBuilder("Hello");

// ❌ INCORRETO - tipos diferentes
// str.equals(sb); // false

// ✅ CORRETO - converte para String
str.equals(sb.toString()); // true

🎯 Resumo Final

Aspecto`==``equals()`
**Tipos Primitivos**Compara valoresNão aplicável
**Objetos**Compara referênciasCompara conteúdo
**Strings**Referência (cuidado com pool)Conteúdo
**Performance**Mais rápidoPode ser mais lento
**Personalização**Não pode ser alteradoPode ser sobrescrito

💡 Dicas Finais

  1. Use equals() para comparar conteúdo de objetos
  2. Use == apenas para primitivos ou verificação de referência
  3. Sempre implemente hashCode() quando sobrescrever equals()
  4. Use Objects.equals() para evitar NullPointerException
  5. Teste sempre seus métodos equals() com diferentes cenários
  6. Com JUnit 5, use anotações como @DisplayName para testes mais descritivos
  7. Aproveite as novas assertions do JUnit 5 para mensagens de erro mais claras

Marina: “Agora entendeu, Carlos? O problema era que você estava comparando referências em vez de conteúdo! E veja como o JUnit 5 torna nossos testes mais expressivos e organizados.”

Carlos: “Perfeito! Agora vou usar equals() para comparar strings e implementar corretamente o equals() e hashCode() nas minhas classes. E adorei as melhorias do JUnit 5! Muito obrigado pela explicação detalhada!”

Marina: “De nada! Lembre-se sempre: quando em dúvida sobre comparação de objetos, use equals(). E não se esqueça do hashCode() quando sobrescrever o equals(). O JUnit 5 vai te ajudar muito a criar testes mais robustos e legíveis!”

Rolar para cima