Entrevistas Técnicas
Preparar-se para entrevistas técnicas de nível sênior em Java exige mais do que saber responder perguntas conceituais básicas. É preciso demonstrar profundidade na arquitetura da JVM, detalhes internos de concorrência, otimização de banco de dados, algoritmos clássicos e trade-offs arquiteturais reais. Este módulo revisita as perguntas mais quentes e complexas de entrevistas reais para desenvolvedores experientes, destrinchando o 'como' e o 'porquê' por trás de cada resposta.
String Pool e Imutabilidade — O Quadro de Avisos Compartilhado
Imagine um quadro de avisos em um escritório. Se dez pessoas precisam ler e citar o mesmo documento padronizado, elas não tiram cópias físicas para cada um rabiscar. Elas usam um único documento no quadro. Se alguém precisa alterar um parágrafo do contrato para um caso específico, essa pessoa não altera o documento no quadro — ela faz uma cópia separada e a edita. String em Java funciona exatamente assim com o String Pool. Como Strings são imutáveis, várias referências podem apontar para a mesma String no pool sem medo de que uma alteração afete os outros. Isso economiza memória e garante segurança (como chaves de HashMap ou credenciais de conexão), mas se você tentar concatenar String em um loop usando '+', vai criar milhares de novos rascunhos no chão do escritório. Para isso, use StringBuilder.
Memória JVM e Algoritmos de Garbage Collection
A JVM divide a memória heap em Young Generation (Eden, Survivor spaces S0 e S1) e Old (Tenured) Generation, além da Metaspace (fora do heap, para metadados de classes). Objetos novos nascem no Eden. A maioria morre cedo (Hipótese Geracional). Os sobreviventes vão para S0/S1 e, após atingirem um limiar de idade (tenuring threshold), são promovidos para a Old Gen. Os algoritmos de Garbage Collection evoluíram: (1) Serial: Single-threaded, congela tudo (Stop-The-World - STW). Adequado para CLI leves. (2) Parallel: Multi-threaded na Young/Old, com STW longo. Foco em throughput. (3) G1 (Garbage-First): Divide o heap em regiões e limpa prioritariamente as regiões com mais lixo, equilibrando latência e throughput. (4) ZGC (Z Garbage Collector): Coleta concorrentemente quase sem pausas (STW na casa dos microssegundos), escalando para heaps de terabytes. Ideal para APIs de baixíssima latência.
Investigando Vazamento de Memória (Memory Leak)
public class CacheInseguro {
// Uma referência estática forte impede que os objetos no Map sejam coletados
private static final Map<String, byte[]> cache = new HashMap<>();
public void adicionar(String id, byte[] dados) {
// Sem mecanismo de expiração (TTL) ou limite de tamanho (Eviction)
// Objetos adicionados aqui nunca serão coletados pelo GC, pois
// a referência estática mantém o 'cache' vivo durante toda a vida da JVM.
cache.put(id, dados);
}
// Solução em entrevista:
// 1. Usar WeakHashMap se as chaves puderem ser limpas quando não referenciadas em outro lugar
// 2. Usar uma biblioteca de cache dedicada (Caffeine, Guava) com expiração e tamanho máximo
// 3. Como diagnosticar: Gerar Heap Dump (jmap -dump:live,format=b,file=heap.hprof <pid>)
// e analisar com Eclipse Memory Analyzer (MAT) buscando por "Leak Suspects"
}Thread Pool Sizing — A Cozinha de um Restaurante
Imagine a cozinha de um restaurante. Os garçons trazem pedidos (tarefas) e os cozinheiros (threads) os preparam. Quantos cozinheiros você deve contratar? Se a limitação é o espaço físico no fogão e picadores (tarefas CPU-bound, como criptografia ou processamento de imagem), contratar mais cozinheiros do que bocas de fogão (número de núcleos da CPU) só causará esbarrões e brigas por espaço (context switching) sem aumentar a velocidade total. Mas se os cozinheiros passam a maior parte do tempo esperando entregas de ingredientes de fornecedores externos (tarefas I/O-bound, como chamadas HTTP ou consultas SQL), você pode contratar muito mais cozinheiros (threads) do que bocas de fogão (cores), pois enquanto um espera a cebola chegar, o outro usa o fogão disponível.
Concorrência: synchronized vs ReentrantLock e Deadlocks
Para garantir exclusão mútua e evitar race conditions, Java oferece blocos synchronized e a API java.util.concurrent.locks.ReentrantLock. synchronized é implícito, fácil de ler, liberado automaticamente pelo compilador e otimizado via Biased Locking e Lock Coarsening na JVM. Já ReentrantLock oferece controle explícito: suporta timeouts (tryLock()), interrupção de threads aguardando lock, justiça (fairness - entrega o lock para a thread mais antiga), e locks separados para leitura/escrita (ReentrantReadWriteLock). Em termos de Deadlocks (quando Thread A segura Lock 1 e espera Lock 2, e Thread B segura Lock 2 e espera Lock 1), a prevenção envolve: (1) Adquirir locks sempre na mesma ordem em toda a aplicação. (2) Usar tryLock() com timeout em vez de bloquear indefinidamente. (3) Evitar locks aninhados quando possível.
Orquestração Assíncrona com CompletableFuture
public CompletableFuture<PedidoDTO> buscarPedidoCompleto(String pedidoId) {
// Busca dados do pedido no DB concorrentemente
CompletableFuture<Pedido> pedidoFuture = CompletableFuture
.supplyAsync(() -> db.buscarPedido(pedidoId), executorCustomizado);
// Busca dados do cliente na API externa concorrentemente
CompletableFuture<Cliente> clienteFuture = CompletableFuture
.supplyAsync(() -> apiCliente.buscarCliente(pedidoId), executorCustomizado);
// Combina ambos os resultados quando ambos terminarem
return pedidoFuture.thenCombineAsync(clienteFuture, (pedido, cliente) -> {
return new PedidoDTO(pedido, cliente);
}, executorCustomizado);
// Dica de entrevista: NUNCA use supplyAsync sem passar um Executor customizado
// em produção. Por padrão, ele usa o ForkJoinPool.commonPool(), que é
// compartilhado por toda a JVM e pode sofrer starvation se bloqueado por I/O.
}Índices de Banco de Dados — O Índice Remissivo
Imagine ler um livro técnico de 1000 páginas procurando pela palavra 'Polimorfismo'. Sem um índice, você terá que folhear página por página, do início ao fim (Full Table Scan). Isso é O(N). Se o livro tiver um Índice Remissivo nas páginas finais, ordenado alfabeticamente, você vai direto na letra 'P', encontra a palavra e vê que ela está na página 423. Você faz isso em segundos (Index Scan, geralmente O(log N) usando B-Tree). Mas o índice tem um custo: alguém teve que escrever o índice e, toda vez que o autor adiciona um capítulo novo (INSERT/UPDATE/DELETE), o índice precisa ser reescrito e reordenado. Se você criar índices para todas as palavras do livro, ele ficará pesado demais e lento para ser impresso.
N+1 Query e Níveis de Isolamento SQL
O problema do N+1 ocorre quando o ORM (como Hibernate) executa uma consulta para trazer N registros pais e, em seguida, dispara mais N consultas individuais para carregar os filhos associados de cada registro (ex: listar 50 posts e fazer 50 SELECTs adicionais para os comentários). É mitigado usando JOIN FETCH na query JPQL ou Entity Graphs. Em termos de transações, a norma SQL define quatro níveis de isolamento para evitar anomalias: Read Uncommitted (permite dirty reads), Read Committed (evita dirty reads), Repeatable Read (evita non-repeatable reads), e Serializable (evita todas as anomalias, inclusive phantom reads, mas com alto custo de concorrência).
Profiling de Aplicação Lenta na Prática
// Sintomas de lentidão em produção e como atacar
/*
1. CPU a 100%:
- Sintoma: Loops infinitos, parsing de JSON gigantesco, ou concorrência excessiva (lock contention).
- Ferramenta: Use 'top -H' ou 'async-profiler' para identificar quais threads gastam mais tempo de CPU.
- Análise: Gere um Thread Dump (jstack <pid>) e busque pelo estado 'RUNNABLE' nas threads identificadas.
2. Pausas longas (GC Latency):
- Sintoma: A aplicação trava periodicamente por alguns segundos.
- Análise: Ative os logs do GC (-Xlog:gc*) e analise com ferramentas como GCViewer ou GCeasy.
- Solução: Reduzir alocações temporárias no código crítico, ou migrar do G1 para ZGC se latência for foco.
3. Conflito de Locks (Thread Contention):
- Sintoma: CPU baixa, mas throughput baixo. Threads presas esperando recursos.
- Análise: Thread dump mostrará threads no estado 'BLOCKED' esperando para adquirir monitor.
- Solução: Reduzir escopo do synchronized, migrar para Lock concorrente ou estruturas Lock-free (AtomicInteger).
*/