Pular para o conteúdo principal

Capítulo 6: Armadilhas Fundamentais do Java

Objetivo

Este capítulo cobre as questões puras de Java mais frequentes em entrevistas. São as bases que todo desenvolvedor Java deve dominar. Um entrevistador as faz para verificar se você entende como o Java funciona na memória.


1. == vs .equals() — A pergunta mais feita​

A regra de ouro​

OperadorCompara o quê?Usar para
==Os endereços de memória (referências)Tipos primitivos (int, boolean, char...)
.equals()O conteúdo (valores)Objetos (String, Integer, List...)

Exemplos com String​

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b); // true ← mesma referência (String Pool)
System.out.println(a == c); // false ← referências diferentes (new cria um novo objeto)
System.out.println(a.equals(c)); // true ← mesmo conteúdo

Por que a == b é true?​

O Java mantém um String Pool (cache) na memória. Quando você escreve "hello" diretamente no código, o Java reutiliza o mesmo objeto:

Memória:
┌──────────────────────────────────┐
│ String Pool │
│ ┌─────────┐ │
│ │ "hello" │ ← a e b apontam aqui
│ └─────────┘ │
└──────────────────────────────────┘

┌──────────────────────────────────┐
│ Heap (heap) │
│ ┌─────────┐ │
│ │ "hello" │ ← c aponta aqui (new = sempre um novo objeto)
│ └─────────┘ │
└──────────────────────────────────┘

Exemplos com Integer (armadilha favorita!)​

Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true ← cache Integer [-128, 127]

Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false ← fora do cache!
System.out.println(a.equals(b)); // true ← mesmo valor
Armadilha de entrevista

O Java faz cache dos Integer de -128 a 127. Acima disso, == retorna false mesmo que os valores sejam idênticos. Por isso, é preciso sempre usar .equals() para comparar objetos.

Boa prática: "CONSTANTE".equals(variável)​

String status = getStatus(); // pode retornar null

// PERIGOSO — NullPointerException se status for null
if (status.equals("ACTIVE")) { ... }

// SEGURO — nunca lança NPE
if ("ACTIVE".equals(status)) { ... }
Frase de entrevista

"== compara referências (endereços de memória), .equals() compara conteúdo. Sempre uso .equals() para objetos. Para strings, coloco a constante à esquerda para evitar NullPointerException."


2. hashCode() — O que é e por que é importante?​

Definição simples​

hashCode() retorna um número inteiro que representa um objeto. É como uma impressão digital: rápido de comparar, mas não 100% único.

O contrato hashCode/equals​

Regra absoluta

Se dois objetos são .equals(), eles DEVEM ter o mesmo hashCode().

Se você sobrescrever .equals(), você deve também sobrescrever hashCode().

// ERRADO — sobrescreve equals sem hashCode
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}
// SEM hashCode() → BUG com HashMap, HashSet!
}

// CERTO — sobrescreve ambos
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}

@Override
public int hashCode() {
return Objects.hash(email);
}
}

Por que isso é crítico? HashMap e HashSet dependem disso!​

User user1 = new User("alice@mail.com");
User user2 = new User("alice@mail.com");

// Sem hashCode correto:
Set<User> set = new HashSet<>();
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 2 ← ERRADO! Deveria ser 1

// Com hashCode correto:
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 1 ← CORRETO

Como o HashMap usa hashCode​

hashCode("alice@mail.com") → 12345

HashMap interno:
┌──────┬──────────────────┐
│ slot │ conteúdo │
├──────┼──────────────────┤
│ ... │ │
│ 12345│ → User("alice") │ ← hashCode determina o slot
│ ... │ │
└──────┴──────────────────┘

1. hashCode() → encontra o slot certo (rápido, O(1))
2. equals() → verifica se é realmente o mesmo objeto

Com Lombok (o mais simples)​

@Data  // gera equals(), hashCode(), toString(), getters, setters
public class User {
private String email;
private String name;
}

// Ou para controlar quais campos usar:
@EqualsAndHashCode(of = {"email"})
public class User {
private String email;
private String name;
}

Com Java Records (o mais moderno)​

public record User(String email, String name) {}
// equals(), hashCode(), toString() são gerados automaticamente!
Frase de entrevista

"hashCode é como uma impressão digital para objetos. O HashMap usa para encontrar o bucket certo em O(1). O contrato diz: se dois objetos são iguais, eles devem ter o mesmo hashCode. Por isso sempre sobrescrevo ambos juntos — ou uso Lombok @Data ou Java Records."


3. toString() — Para depuração​

Sem toString​

User user = new User("Alice", "alice@mail.com");
System.out.println(user);
// → com.example.User@1a2b3c4d ← inútil!

Com toString​

@Override
public String toString() {
return "User{name='%s', email='%s'}".formatted(name, email);
}

System.out.println(user);
// → User{name='Alice', email='alice@mail.com'} ← útil!

Com Lombok ou Record​

@Data       // gera toString() automaticamente
public class User { ... }

// Ou
public record User(String name, String email) {}
// toString() → User[name=Alice, email=alice@mail.com]

4. Tipos primitivos vs Wrapper classes​

PrimitivoWrapperTamanhoValor padrão
intInteger4 bytes0
longLong8 bytes0L
doubleDouble8 bytes0.0
booleanBoolean1 bitfalse
charCharacter2 bytes'\u0000'

Quando usar cada um?​

SituaçãoUsarPor quê
Variáveis locais, cálculosint, doubleMais rápido, sem boxing
Atributos de entidades JPALong, IntegerPode ser null (importante para id ainda não persistido)
Coleções (List, Map)Integer, LongColeções não aceitam primitivos
Parâmetros opcionaisIntegerPode ser null para indicar "não fornecido"

Autoboxing / Unboxing​

// Autoboxing: int → Integer (automático)
Integer a = 42;

// Unboxing: Integer → int (automático)
int b = a;

// ARMADILHA: unboxing de null → NullPointerException!
Integer c = null;
int d = c; // 💥 NullPointerException
Armadilha de entrevista
Integer a = null;
if (a == 0) { ... } // 💥 NullPointerException (unboxing de null)

Sempre verifique null antes de comparar um Wrapper com um primitivo.

Frase de entrevista

"Uso primitivos para variáveis locais e cálculos, e wrappers para IDs de entidades e coleções. O principal perigo é fazer unboxing de null, que lança NullPointerException."


5. final — Três usos diferentes​

ContextoSignificadoExemplo
VariávelA referência não pode mudarfinal List<String> list = new ArrayList<>()
MétodoNão pode ser sobrescrito (override)public final void process()
ClasseNão pode ser herdadapublic final class String

Armadilha: final NÃO torna o objeto imutável​

final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ OK — modifica o CONTEÚDO
list = new ArrayList<>(); // ❌ Erro — tenta mudar a REFERÊNCIA
Memória:
┌─────────────────┐
list (final) ──→ │ ArrayList │
(referência │ ["hello"] │ ← o conteúdo pode mudar
não muda) └─────────────────┘
Frase de entrevista

"final significa que a referência não pode ser reatribuída, mas o objeto ainda pode ser mutado. Para imutabilidade real, uso List.of() ou Collections.unmodifiableList()."


6. static — Pertence à classe, não à instância​

public class Counter {
static int totalCount = 0; // compartilhado entre TODAS as instâncias
int instanceCount = 0; // próprio de cada instância

public Counter() {
totalCount++;
instanceCount++;
}
}

Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();

System.out.println(Counter.totalCount); // 3 (compartilhado)
System.out.println(c3.instanceCount); // 1 (próprio do c3)

static na prática​

UsoExemploBoa prática?
Constantesstatic final int MAX = 100Sim
Métodos utilitáriosMath.max(), Collections.sort()Sim
Variáveis compartilhadasstatic int countAtenção (thread safety)
Classes internasstatic class BuilderSim (Builder pattern)
No Spring Boot

Evite variáveis static mutáveis em beans Spring. Os beans já são singleton por padrão — use campos de instância normais.


7. String — Imutável por design​

Strings são imutáveis​

String s = "hello";
s.toUpperCase();
System.out.println(s); // "hello" ← NÃO modificado!

s = s.toUpperCase();
System.out.println(s); // "HELLO" ← nova String atribuída

Cada modificação cria um novo objeto String.

Concatenação em loop: StringBuilder​

// RUIM — cria N objetos String intermediários → O(n²)
String result = "";
for (int i = 0; i < 10000; i++) {
result += i + ",";
}

// BOM — um único objeto, modificações in-place → O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();

String Pool (cache)​

String a = "hello";          // vai para o String Pool
String b = "hello"; // reutiliza o mesmo objeto do Pool
String c = new String("hello"); // cria um NOVO objeto no Heap

System.out.println(a == b); // true (mesma referência no Pool)
System.out.println(a == c); // false (c está no Heap, não no Pool)

Comparação de Strings​

MétodoUso
.equals()Comparação sensível a maiúsculas/minúsculas
.equalsIgnoreCase()Comparação insensível a maiúsculas/minúsculas
.compareTo()Ordem alfabética (retorna int)
.contains()Verifica se contém uma substring
.startsWith() / .endsWith()Verifica início/fim
.isBlank()Vazio ou apenas espaços (Java 11+)
.isEmpty()Vazio (comprimento 0)
Frase de entrevista

"Strings são imutáveis em Java. Toda modificação cria um novo objeto. Para concatenação em loops, uso StringBuilder. Para comparação, sempre uso .equals(), nunca ==."


8. Optional — Evite NullPointerException​

O problema​

// NullPointerException se findById retornar null
User user = userRepository.findById(1L);
String email = user.getEmail(); // 💥 NPE se user for null

A solução: Optional​

Optional<User> optional = userRepository.findById(1L);

// Abordagem 1: isPresent
if (optional.isPresent()) {
User user = optional.get();
}

// Abordagem 2: orElse (valor padrão)
User user = optional.orElse(new User("anonymous"));

// Abordagem 3: orElseThrow (exceção)
User user = optional.orElseThrow(() ->
new ResourceNotFoundException("User not found with id: 1"));

// Abordagem 4: map (transformar)
String email = optional.map(User::getEmail).orElse("N/A");

Anti-patterns a evitar​

// ❌ RUIM — equivale a um null check
if (optional.isPresent()) {
return optional.get();
} else {
return null;
}

// ✅ CERTO
return optional.orElse(null);

// ❌ RUIM — Optional como parâmetro
public void process(Optional<String> name) { ... }

// ✅ CERTO — Optional apenas como retorno
public Optional<User> findByEmail(String email) { ... }
Frase de entrevista

"Optional representa um valor que pode ou não estar presente. Uso como tipo de retorno, nunca como parâmetro. Prefiro orElseThrow() para valores obrigatórios e map() para transformações."


9. Passagem por valor vs passagem por referência​

Java passa TUDO por valor​

// Primitivos: o VALOR é copiado
void increment(int x) {
x++; // modifica a cópia local
}
int a = 5;
increment(a);
System.out.println(a); // 5 ← não modificado

// Objetos: a REFERÊNCIA é copiada (não o objeto)
void addItem(List<String> list) {
list.add("new"); // modifica o objeto apontado pela cópia da referência
}
List<String> myList = new ArrayList<>();
addItem(myList);
System.out.println(myList); // [new] ← modificado!

// Mas reatribuir a referência não muda nada
void replace(List<String> list) {
list = new ArrayList<>(); // muda a cópia local, não o original
}
replace(myList);
System.out.println(myList); // [new] ← ainda a lista antiga

Esquema de memória​

Chamada: addItem(myList)

main() addItem()
┌──────┐ ┌──────┐
│myList │──┐ ┌────│ list │ ← cópia da referência
└──────┘ │ │ └──────┘
↓ ↓
┌──────────────┐
│ ArrayList │ ← ambos apontam para o MESMO objeto
│ ["new"] │
└──────────────┘
Frase de entrevista

"Java sempre passa por valor. Para primitivos, o valor é copiado. Para objetos, a referência é copiada — então você pode modificar o objeto pela cópia, mas não pode fazer a variável original apontar para outro objeto."


10. Interfaces vs Classes abstratas​

AspectoInterfaceClasse abstrata
Palavra-chaveimplementsextends
Herança múltiplaSim (várias interfaces)Não (uma só classe)
ConstrutorNãoSim
Campos de instânciaNão (apenas static final)Sim
Métodos concretosSim (desde Java 8, default)Sim
Quando usarContrato / capacidade ("pode fazer")Base comum ("é um")
// Interface = contrato (o que o objeto PODE fazer)
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}

class Duck implements Flyable, Swimmable {
public void fly() { ... }
public void swim() { ... }
}

// Classe abstrata = base comum (o que o objeto É)
abstract class Animal {
protected String name;

Animal(String name) { this.name = name; }

abstract void makeSound();

void breathe() { System.out.println(name + " breathes"); }
}

class Dog extends Animal {
Dog(String name) { super(name); }
void makeSound() { System.out.println("Woof!"); }
}
Frase de entrevista

"Uso interfaces para definir contratos (o que um objeto pode fazer) e classes abstratas para compartilhar comportamento comum (o que um objeto é). Desde o Java 8, interfaces podem ter métodos default, o que diminuiu a diferença — mas ainda prefiro interfaces pela flexibilidade e herança múltipla."


Resumo rápido para entrevista​

PerguntaResposta em 1 frase
== vs .equals()== compara referências, .equals() compara conteúdo
hashCode()Impressão digital de um objeto, usada por HashMap/HashSet
Contrato equals/hashCodeSe a.equals(b) então a.hashCode() == b.hashCode()
finalReferência não reatribuível (não imutabilidade do objeto)
staticPertence à classe, não à instância
String imutávelCada modificação cria um novo objeto
OptionalContêiner que pode estar vazio, evita null checks
Passagem por valorJava copia o valor (ou a referência para objetos)
Interface vs AbstrataInterface = contrato, Abstrata = base comum

⬅️ Capítulo 5: Depuração | Capítulo 7: Coleções Java →