O catálogo de pegadinhas clássicas
TL;DR
Esta nota vale ouro. É o catálogo das armadilhas recorrentes que a prova OCP adora — aquelas em que quem programa Java há anos cai justamente por excesso de confiança. Você “sabe” o resultado de cabeça, marca a alternativa óbvia, e a banca contava exatamente com isso. Cada pegadinha aqui tem um snippet curto, o resultado real (com o porquê) e o domínio onde a mecânica de verdade mora, pra você revisar fundo se precisar.
Como usar este catálogo
Revise esta nota inteira na véspera da prova, depois de já ter rodado a mapa objetivo → galho. A diferença entre uma pegadinha e um conceito é o ângulo: o conceito você estuda uma vez e segue; a pegadinha você precisa re-treinar o reflexo, porque seu instinto de programador experiente aponta pra resposta errada. Cada subseção é proposital e curta — leia o snippet, cubra a explicação com a mão, responda mentalmente, e só então confira. Se errar (ou acertar pelo motivo errado), siga o ponteiro do domínio indicado e revise a mecânica de raiz antes de voltar.
A regra de ouro que atravessa metade deste catálogo: == compara identidade de referência, equals compara conteúdo. Boa parte das pegadinhas de tipos e de coleções é só a banca explorando a tentação de usar == onde você deveria usar equals.
Tipos e valores
Integer cache (-128..127)
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true — vêm do cache compartilhado
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false — fora do cache, objetos distintos
System.out.println(c.equals(d)); // true — equals SEMPRE compara valorA JVM mantém um cache de objetos Integer para a faixa -128..127 (autoboxing reaproveita instâncias). Dentro da faixa, == parece funcionar; um número acima e a ilusão quebra. A lição não é decorar a faixa — é nunca comparar wrappers com ==. Use equals. A mesma armadilha vale para Long, Short, Byte e Character.
String pool vs new String / intern
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s2); // true — ambos vêm do pool
System.out.println(s1 == s3); // false — new String() força objeto novo no heap
System.out.println(s1.equals(s3)); // true — conteúdo igual
System.out.println(s1 == s3.intern()); // true — intern() devolve a instância do poolLiterais de String vivem no pool e são deduplicados — por isso s1 == s2. Mas new String("hello") deliberadamente cria um objeto novo, fora do pool, então s1 == s3 é false. intern() consulta o pool e devolve a referência canônica. Mesma moral do Integer cache: para conteúdo, sempre equals.
var com tipos ambíguos (e o que var NÃO pode)
var list = new ArrayList<>(); // inferido como ArrayList<Object>, não <String>!
list.add("hello");
list.add(42); // compila — porque é List<Object>O diamante vazio <> num var infere Object, não o tipo que você “queria”. O var só conhece o que está do lado direito do =.
E var não pode aparecer em vários lugares que a prova adora testar:
// var x; // ERRO — sem inicializador, nada a inferir
// var y = null; // ERRO — null não tem tipo
// var z = () -> 1; // ERRO — lambda precisa de target type explícito
// var a = new int[]{1,2}; // OK, mas: var arr[] = ... // ERRO — notação de array
// var method() { } // ERRO — não serve como tipo de retorno
// public var field; // ERRO — não serve em campo de classevar é só inferência local de variável (dentro de método, for, try-with-resources). Onde o compilador não tem uma expressão concreta pra inferir, ele recusa.
Autoboxing com null → NPE
Integer i = null;
int j = i; // NullPointerException em runtime!Atribuir um Integer a um int dispara unboxing implícito — o compilador insere um i.intValue(). Se a referência for null, isso é um NPE silencioso, sem cast à vista. A prova esconde isso dentro de expressões maiores (ternários, somas com wrappers, Map.get que retorna null).
final com objeto mutável (referência final ≠ conteúdo imutável)
final List<String> list = new ArrayList<>();
list.add("hello"); // OK — só a REFERÊNCIA é final
// list = new ArrayList<>(); // ERRO — não pode reapontar a referênciafinal congela a variável, não o objeto. Você não pode reatribuir list, mas pode mutar à vontade o conteúdo apontado. A banca usa isso pra confundir final com imutabilidade.
Inicialização de arrays (0 / null / false)
int[] a = new int[5]; // [0, 0, 0, 0, 0]
Integer[] b = new Integer[5]; // [null, null, null, null, null]
boolean[] c = new boolean[5]; // [false, false, false, false, false]
String[] d = new String[5]; // [null, null, null, null, null]
double[] e = new double[5]; // [0.0, 0.0, 0.0, 0.0, 0.0]new T[n] zera tudo com o valor default de T: numéricos viram 0/0.0, boolean vira false, referências (incluindo wrappers como Integer) viram null. A pegadinha clássica: assumir que Integer[] começa com zeros — não, começa com null, e iterar somando dá NPE por unboxing.
Orientação a objetos
Override vs overload com autoboxing
class Handler {
void handle(int x) { System.out.println("int"); }
void handle(Integer x) { System.out.println("Integer"); }
void handle(Object x) { System.out.println("Object"); }
}
new Handler().handle(1); // "int" — match exato vence
new Handler().handle((Integer) 1); // "Integer"
new Handler().handle("str"); // "Object"Quando há sobrecargas, o compilador escolhe em compile-time seguindo uma ordem rígida de preferência:
Ordem de resolução de sobrecarga
- Match exato (tipo idêntico)
- Widening (alargamento primitivo:
int→long→double)- Autoboxing (
int→Integer)- Varargs (
int...) — sempre o último recurso
O compilador desce essa escada e para no primeiro nível que tem candidato. Por isso handle(1) chama int (match exato), e não Integer nem Object, mesmo que ambos sirvam. Veja o domínio em Domínio 3 — Orientação a objetos.
Static method hiding (resolvido em compile-time pelo tipo declarado)
class Parent {
static void foo() { System.out.println("Parent"); }
}
class Child extends Parent {
static void foo() { System.out.println("Child"); }
}
Parent p = new Child();
p.foo(); // "Parent" — NÃO é override; é hiding, resolvido pelo TIPO declaradoMétodos static não sofrem override — eles sofrem hiding (ocultação). Não há despacho dinâmico: o método chamado é decidido em compile-time pelo tipo declarado da variável (Parent), não pelo objeto real (Child). Compare com método de instância, que faria o oposto (chamaria Child). Essa inversão é uma das pegadinhas favoritas da prova.
Enum constructor é sempre private
enum Status {
ACTIVE("active"),
INACTIVE("inactive");
private final String label;
Status(String label) { // implicitamente private
this.label = label;
}
}O construtor de um enum é implicitamente private e não pode ser declarado public nem protected — tentar isso é erro de compilação. Faz sentido: as instâncias de um enum são fixas e criadas só pela própria declaração; ninguém de fora pode instanciar. A prova mostra um construtor de enum marcado public e pergunta o resultado (resposta: não compila).
Generics e type erasure (getClass() é igual)
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true!Generics existem só em compile-time. Em runtime, List<String> e List<Integer> são ambos apenas ArrayList — o tipo paramétrico foi apagado (type erasure). Por isso getClass() devolve a mesma Class para os dois, você não pode fazer new T[], e não dá pra testar instanceof List<String>. Isso vive em Domínio 3 — Orientação a objetos.
Fluxo e exceções
try/finally com return (o finally vence)
int method() {
try {
return 1;
} finally {
return 2; // sobrescreve o return do try
}
}
// retorna 2Um return (ou throw) dentro de finally sobrepõe qualquer return ou exceção pendente do try/catch. O try “ia retornar 1”, mas o finally executa antes de a pilha desenrolar e impõe o 2 — inclusive engolindo exceções que estavam a caminho. É código ruim na prática (vários linters acusam), mas a prova pergunta exatamente o valor de retorno.
try-with-resources: ordem reversa de close()
try (var a = new ResourceA();
var b = new ResourceB();
var c = new ResourceC()) {
// uso
}
// Ordem de fechamento: c → b → a (reversa da declaração)Os recursos de um try-with-resources são fechados na ordem inversa em que foram abertos — como uma pilha. O último aberto (c) é o primeiro a fechar. Isso garante que um recurso que depende de outro (ex.: um Writer sobre um Stream) feche antes da sua dependência. A prova testa numerando os recursos e perguntando a sequência impressa pelos close().
Switch case: (fall-through) vs case -> (sem fall-through)
O switch statement com dois-pontos sofre fall-through: sem break, a execução escorre pros próximos cases.
int x = 2;
switch (x) {
case 1: System.out.println("1");
case 2: System.out.println("2");
case 3: System.out.println("3");
default: System.out.println("default");
}
// Imprime: 2, 3, default — escorreu do case 2 até o fimO switch expression com seta (->) não tem fall-through: executa só o ramo casado.
switch (x) {
case 1 -> System.out.println("1");
case 2 -> System.out.println("2");
case 3 -> System.out.println("3");
default -> System.out.println("default");
}
// Imprime apenas: 2A pegadinha é misturar as sintaxes mentalmente e esquecer que os dois-pontos escorrem. Sempre que ver case X: sem break, conte os prints até o próximo break (ou o fim do switch).
Coleções e streams
Arrays.asList tem tamanho fixo
List<String> list = Arrays.asList("a", "b", "c");
list.set(0, "z"); // OK — pode trocar elementos existentes
list.add("d"); // UnsupportedOperationException — tamanho é fixo!
list.remove("a"); // UnsupportedOperationExceptionArrays.asList devolve uma List de tamanho fixo, apoiada no array original. Você pode reordenar e substituir (set), mas não adicionar nem remover — isso lança UnsupportedOperationException. Pra ter uma lista mutável de verdade, embrulhe: new ArrayList<>(Arrays.asList(...)).
Coleções imutáveis: view vs cópia
List<String> list = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(list); // VIEW da original
List<String> copy = List.copyOf(list); // CÓPIA independente
list.add("c");
System.out.println(view.size()); // 3 — a view enxerga a mudança na original!
System.out.println(copy.size()); // 2 — a cópia ficou congeladaCollections.unmodifiableList cria uma view somente-leitura: você não consegue modificá-la pela view, mas ela continua refletindo mudanças feitas na lista subjacente. Já List.copyOf faz uma cópia genuína e independente. A prova explora a diferença mudando a lista original depois de criar as duas e perguntando os tamanhos.
Contrato equals / hashCode (sem hashCode, HashSet/HashMap quebram)
class Person {
String name;
public boolean equals(Object o) { /* compara name */ }
// FALTA hashCode() — BUG silencioso!
}
Set<Person> set = new HashSet<>();
set.add(new Person("Maria"));
set.contains(new Person("Maria")); // false! — hashCode default é por identidadeA regra: se você sobrescreve
equals, tem que sobrescreverhashCode.O contrato exige que objetos iguais por
equalstenham o mesmohashCode. Estruturas baseadas em hash (HashSet,HashMap,HashTable) primeiro localizam o bucket pelohashCodee só depois comparam comequals. Se ohashCodecontinua o default (baseado em identidade do objeto), os doisPerson("Maria")caem em buckets diferentes e nunca se encontram —containsdevolvefalsemesmo comequals“correto”.
Stream consumido 2x (IllegalStateException)
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // ok
stream.forEach(System.out::println); // IllegalStateException — stream has already been operated upon or closedUm Stream é de uso único. Depois de uma operação terminal (forEach, collect, count, reduce…), ele está consumido e qualquer nova operação lança IllegalStateException. Diferente de uma Collection, que você itera quantas vezes quiser. Se precisa reprocessar, recrie o stream (ou guarde o resultado numa coleção). A prova esconde a segunda operação terminal algumas linhas abaixo.
Effectively final em lambdas
int x = 10;
Runnable r = () -> System.out.println(x); // OK — x é effectively final
// x = 20; // se descomentar, ERRO de compilação no lambda acimaUm lambda (ou classe anônima) só pode capturar variáveis locais que sejam final ou effectively final — isto é, atribuídas uma única vez e nunca reatribuídas, mesmo sem a palavra final. Basta uma reatribuição em qualquer ponto do método pra que a variável deixe de ser effectively final e o lambda pare de compilar. Note que a captura é por valor; mutar o conteúdo de um objeto capturado é permitido (mesma lógica do final com objeto mutável, lá em cima).
Veja também
- O mapa objetivo → galho
- Domínio 3 — Orientação a objetos
- Estratégia de estudo e recursos
- Certificação OCP (MOC do galho)