Conceitos essenciais de Java explicados a fundo, um de cada vez.
Como o WeakHashMap envolve cada chave em uma WeakReference para que as entradas sejam removidas automaticamente quando nada fora do mapa segura a chave com uma referência forte, e por que um valor que referencia a própria chave ou uma chave internada anula esse comportamento.
Como o TreeMap mantém as chaves em ordem ascendente através de uma árvore red-black, por que a igualdade de chaves ali é decidida por compareTo() em vez de equals()/hashCode(), e o custo O(log n) que isso paga.
Como o armazenamento em tabela hash do HashMap oferece get/put O(1) em média, sem garantia de ordem de iteração, e como o put() substitui silenciosamente o valor de uma chave já existente.
Como o LinkedHashMap adiciona iteração em ordem de inserção (ou de acesso) sobre a performance de busca do HashMap, e como sobrescrever removeEldestEntry() o transforma em um cache LRU limitado.
Como Comparator desacopla completamente a ordenação de um tipo (diferente do único compareTo() fixo de Comparable), como comparing()/thenComparing() constroem comparators de múltiplas chaves sem um compare() escrito à mão, e o que os algoritmos estáticos de Collections (sort, shuffle, views unmodifiable/synchronized/checked) realmente garantem.
A interface raiz genérica do Collections Framework — o que add, remove e as operações em bloco garantem, e por que várias delas podem lançar exceção em runtime em vez de simplesmente não compilar.
Por que Queue define duas famílias paralelas de métodos — uma que lança exceção em caso de falha, outra que reporta a falha — e o que isso significa para filas vazias e de tamanho fixo.
Como o PriorityQueue ordena elementos por comparator em vez da ordem de inserção, via um heap, e por que iterar sobre ele não visita os elementos em ordem de prioridade da forma como poll() repetido faz.
Como Deque estende Queue para uma fila de duas pontas que também pode funcionar como pilha, e como suas famílias addFirst/addLast, peek e remove se dividem cada uma em uma versão que lança exceção e outra que reporta.
Como o armazenamento em array, sem restrição de capacidade, do ArrayDeque o torna a escolha recomendada pelo JDK para pilha/fila em vez do Stack legado ou LinkedList quando elementos nulos não são necessários.
Como Set proíbe duplicatas, como SortedSet e NavigableSet acrescentam ordenação e buscas por correspondência mais próxima em cima dela, e por que suas range views são apoiadas no set original.
Como o TreeSet mantém os elementos em ordem crescente via uma estrutura de árvore, por que a unicidade ali é decidida por compareTo() em vez de equals(), e o custo O(log n) que isso paga.
Como o armazenamento em tabela hash do HashSet oferece add/contains/remove O(1) em média, sem garantia de ordem de iteração, e o que quebra as buscas quando os campos relevantes para o hash de um elemento mudam.
Como o LinkedHashSet adiciona iteração em ordem de inserção sobre a performance de busca do HashSet, passando uma lista ligada por dentro das entradas da tabela hash.
Como List constrói uma sequência posicional, que permite duplicatas, sobre Collection — acesso indexado, sublistas, ordenação e as listas não modificáveis produzidas por List.of().
As interfaces SequencedCollection/SequencedSet/SequencedMap da JEP 431 dão a toda coleção com uma ordem de encontro real — List, Deque, LinkedHashSet, TreeSet, LinkedHashMap, TreeMap — uma API uniforme de getFirst/getLast/addFirst/addLast/reversed(), excluindo deliberadamente HashSet, HashMap e PriorityQueue, já que nenhuma delas tem uma ordem de encontro estável para expor.
Como o armazenamento baseado em nós do LinkedList inverte o trade-off do ArrayList — acesso indexado O(n) em troca de inserção/remoção baratas — enquanto implementa List, Deque e Queue ao mesmo tempo.
Como o armazenamento em array redimensionável do ArrayList oferece acesso indexado O(1) ao custo de inserções/remoções O(n) fora do final, e como gerenciar sua capacidade diretamente.
O que cada interface faz, por que Iterable te obriga a implementar Iterator, e por que as coleções do JDK são construídas dessa forma.
O VarHandle (JEP 193) permite que um programa aplique acesso com força atômica e volatile a um campo comum ou elemento de array, na força de ordenação de memória escolhida, sem envolvê-lo em uma classe como AtomicInteger.
ThreadLocal dá a cada thread sua própria cópia isolada de uma variável através de um mapa por thread, o que é poderoso mas vaza em threads de pool a menos que seja combinado com remove, e InheritableThreadLocal copia esse valor para threads filhas apenas no momento da criação.
As regras formais de happens-before do Capítulo 17 da JLS que determinam quando uma escrita em uma thread tem visibilidade garantida para uma leitura em outra.
Por que a stack de cada thread — suas variáveis locais, parâmetros e frames de chamada — é privada daquela thread e nunca é fonte de condições de corrida, enquanto referências armazenadas na stack ainda podem apontar para estado compartilhado e sujeito a corrida na heap.
Como thenApply, thenCompose, thenCombine, allOf e os handlers de exceção compõem trabalho assíncrono sem bloquear — e as armadilhas de futures aninhados, common pool e exceções engolidas que vêm junto.
ScopedValue (finalizado no Java 25 pela JEP 506) substitui o slot mutável e propenso a leak por thread do ThreadLocal por um binding imutável cujo tempo de vida é exatamente a extensão dinâmica de uma lambda run()/call() — sem set(), sem remove() para esquecer, e herança automática por subtasks disparadas em um StructuredTaskScope.
StructuredTaskScope (ainda preview no Java 25, JEP 505) transforma um fan-out de subtarefas concorrentes em um bloco try-with-resources que é dono do tempo de vida delas — nenhuma tarefa pode sobreviver ao seu escopo, as tarefas irmãs são canceladas automaticamente em caso de falha, e a operação inteira aparece aninhada em um thread dump em vez de como um pool plano de Futures.
Cobre a fórmula do JCIP para calcular um tamanho ótimo de thread pool e a API do ThreadPoolExecutor para filas de trabalho, políticas de saturação, thread factories e hooks de extensão.
Técnicas concretas para diminuir o impacto de um lock contencioso — reduzir seu escopo, dividi-lo ou fazer striping, evitar campos quentes, e quando trocá-lo por um read-write lock ou um atomic.
Compara o shutdown() gracioso com o shutdownNow() agressivo, o idioma awaitTermination()/close(), poison pills para consumidores baseados em fila, handlers de exceção não capturada, e shutdown hooks da JVM e daemon threads.
Como Thread.interrupt() funciona de fato como uma flag de status cooperativa, por que engolir InterruptedException é o bug de concorrência mais comum, e como cancelar tasks corretamente via política de interrupção e Future.cancel().
Como o put/take bloqueante de BlockingQueue reduz o padrão produtor-consumidor a poucas linhas de código, e por que filas limitadas (bounded) oferecem backpressure real em vez de apenas adiar um OutOfMemoryError.
Por que uma ação composta construída a partir de chamadas thread-safe numa coleção sincronizada ainda pode ter race condition, e como ConcurrentHashMap e CopyOnWriteArrayList resolvem isso com iteração fracamente consistente e métodos de ação composta atômicos em vez disso.
Como deadlocks de ordem de lock acontecem entre threads, e como uma ordem consistente de locks, open calls e tentativas de lock com timeout previnem e diagnosticam esse problema.
A maquinaria interna compartilhada — um campo de estado, uma fila de threads e alguns hooks sobrescrevíveis — sobre a qual ReentrantLock, Semaphore e CountDownLatch são construídos.
Explica por que leituras não sincronizadas entre threads podem ver valores obsoletos ou corrompidos, o que volatile de fato garante, e como publicar um objeto para outra thread com segurança.
Um motor de divisão-e-conquista para paralelismo real em múltiplos núcleos, onde um ForkJoinPool executa RecursiveAction/RecursiveTask divididos recursivamente via work-stealing.
O kit java.util.concurrent — executors e thread pools, Callable/Future, primitivas de coordenação como CountDownLatch e Semaphore, Locks explícitos e variáveis atômicas — como alternativa de mais alto nível ao código artesanal com Thread e synchronized.
Por que Thread.suspend/resume/stop foram descontinuados por corromperem estado compartilhado, o padrão de desligamento cooperativo que os substituiu, e por que virtual threads resolvem um problema diferente (custo de thread) em vez desse mesmo problema de corretude.
String armazena texto como unidades de código UTF-16 de 16 bits, então caracteres fora do Plano Multilíngue Básico, como a maioria dos emojis, precisam de um par substituto (surrogate pair) de dois caracteres para representar um único ponto de código Unicode.
Um mergulho profundo nos Collectors de múltiplos níveis (groupingBy, partitioningBy, toMap, teeing, Collector.of customizado) e na mecânica e nas armadilhas das streams paralelas.
Literais de string são automaticamente internados em um pool compartilhado, então == é true entre dois literais com o mesmo conteúdo mas false contra um new String(...) com conteúdo idêntico — uma consequência direta e previsível de onde cada string veio, não uma peculiaridade da linguagem.
Como métodos default, static e private permitem que uma interface carregue comportamento para que uma API publicada possa crescer sem quebrar quem a implementa, e as regras exatas que o Java usa quando dois defaults herdados colidem.
Desde o JDK 25, um construtor pode executar statements antes de super(...) ou this(...), dividindo seu corpo em um prólogo que valida argumentos e inicializa seus próprios campos e um epílogo que finalmente pode usar o objeto em construção.
Os quatro tipos de classe-dentro-de-classe — nested static, inner, local e anônima — diferem pela quantidade de contexto do enclosing que cada uma captura, e saber qual delas você escreveu revela o que ela alcança, o que mantém vivo em memória, e se uma lambda ou um record diriam a mesma coisa melhor.
O switch statement e a switch expression são duas construções que compartilham uma palavra-chave — a forma com dois-pontos cai através dos casos (fall-through) e não produz nada, enquanto uma expression em forma arrow executa exatamente um ramo, produz um valor via yield quando o ramo é um bloco, e precisa ser comprovadamente exaustiva ou o compilador a rejeita.
Como o text block com aspas triplas escreve SQL, JSON e HTML multi-linha sem escapes de \n ou concatenação com + — e o algoritmo preciso de whitespace incidental por trás disso, onde a própria coluna do delimitador de fechamento decide quanta indentação é removida, espaços finais desaparecem a menos que sejam presos com \s, e terminadores de linha são normalizados para \n. Continua sendo exatamente uma String em runtime, sem interpolação e sem forma de diferenciá-la de um literal comum.
var permite que o compilador infira o tipo estático de uma variável local a partir do seu inicializador — não é tipagem dinâmica nem Object, e combiná-lo com o operador diamond silenciosamente produz ArrayList<Object>.
A resolução de sobrecarga acontece em tempo de compilação, com base no tipo declarado dos argumentos, e não no tipo em runtime — diferente do que ocorre com overriding — e essa diferença molda como as assinaturas de métodos deveriam ser projetadas.
Varargs alocam implicitamente um array em toda chamada e, combinados com generics, podem deixar um ClassCastException escapar do compilador — o motivo, e o que @SafeVarargs de fato promete.
Use corpos de método específicos por constante para comportamento individual por constante, e EnumSet/EnumMap em vez de campos de bits e arrays indexados por ordinal.
Por que uma stream é um conduto preguiçoso e de uso único em vez de uma estrutura de dados, como o formato do pipeline (fonte → operações intermediárias → operação terminal) faz com que filter-then-findFirst pare antecipadamente em vez de varrer tudo, como Collectors e as especializações de stream primitivas funcionam por baixo dos panos, e quando streams paralelas realmente ajudam.
Como uma expressão lambda implementa o único método abstrato de uma interface funcional, as formas de sintaxe com corpo de expressão vs. corpo de bloco, o catálogo java.util.function, a regra effectively-final por trás da captura de variáveis, e os quatro tipos de referência a método.
Por que objetos custam mais que primitivos (identidade, header, alocação no heap, pressão sobre o GC), como Value Classes permitem que a JVM elimine esse overhead para tipos que são apenas seus dados, e por que List<int> ainda não existe por causa do type erasure dos generics.
Como um record reduz uma classe carregadora de dados a uma linha (construtores canônico/compacto, equals/hashCode/toString de graça, imutabilidade rasa), e como classes/interfaces sealed substituem o "qualquer um pode estender isso" por uma lista permits explícita e exaustiva — a combinação que torna seguro modelar hierarquias fechadas.
Como tipos parametrizados movem casts e verificações de incompatibilidade de tipo do runtime para o tempo de compilação, o que tipos limitados e wildcards PECS (extends/super) realmente restringem, e por que erasure significa que só existe, de fato, um arquivo de classe por tipo genérico em tempo de execução.
Como instanceof, switch e record patterns permitem testar a estrutura de um objeto e extrair seus dados em um único passo, em vez de fazer cast depois de checar o tipo.
Por que equals() retorna boolean e compareTo() retorna int, de onde cada um vem (Object vs Comparable), o que significa uma ordenação "consistente com equals", e por que new BigDecimal("1.0") não é igual a new BigDecimal("1.00").
Object.clone() e a interface marcadora Cloneable realizam uma cópia rasa que ignora o construtor, com armadilhas estruturais envolvendo herança e campos final, que copy constructors e static factories evitam.
Data-Oriented Programming separa deliberadamente dados imutáveis e transparentes (records dentro de uma hierarquia sealed) da lógica que atua sobre eles, de modo que um switch exaustivo com record patterns permite ao compilador garantir que toda variante seja tratada.
Optional existe para tornar 'isso pode não ter um valor' parte do tipo de retorno de um método em vez de um null que quem chama precisa lembrar de checar — e por que o próprio JDK desaconselha usá-lo como campo, parâmetro ou dentro de uma coleção, além do anti-padrão isPresent()-então-get() e a armadilha de eagerness orElse-vs-orElseGet que pegam a maioria dos primeiros usos.
Implementar Serializable ignora as checagens de invariantes do construtor da sua classe e expõe seus internos como API permanente — veja por que, e como disciplina no readObject, o padrão serialization proxy e o ObjectInputFilter contêm esse risco.
Como decidir se uma falha deveria sequer ser modelada como exceção, e se um novo tipo de exceção deveria ser checked ou unchecked.
Valide os parâmetros de um método ou construtor logo no topo do corpo e lance imediatamente uma exceção clara, em vez de deixar um argumento inválido se propagar até falhas confusas ou um estado de objeto corrompido.
Estender uma classe que você não controla é frágil porque depende de padrões internos de "auto-uso" não documentados, e composição com forwarding evita esse risco — a mesma limitação de herança única também explica por que interfaces, e não classes abstratas, costumam ser a forma certa de definir um tipo público.
Tornar uma classe verdadeiramente imutável exige mais do que campos final — significa copiar argumentos mutáveis na entrada e campos mutáveis na saída, para que nenhuma outra referência ao estado do objeto jamais escape.
Os contratos documentados por trás dos três métodos mais sobrescritos de Object, por que quebrar a simetria ou pular hashCode() falha silenciosamente em vez de na compilação, e como records satisfazem os três de graça.
Compare as três formas de escrever um singleton em Java — campo público, static factory, enum de um único elemento — e o idioma do construtor privado para classes utilitárias não instanciáveis.
Métodos estáticos nomeados podem substituir ou complementar construtores para criar objetos de forma mais clara, cacheável e flexível quanto a subtipos, e o padrão Builder domina classes com muitos campos opcionais.
O mecanismo estruturado do Java para erros em tempo de execução — try/catch/finally, throw/throws, exceções checked vs. unchecked, subclasses de exceção customizadas, e exceções encadeadas.
Como o AOT cache do Project Leyden guarda classes, perfis de métodos, objetos e (em breve) código compilado de uma execução de treino para encurtar startup e warm-up sem abandonar a JVM dinâmica, e o que cada JEP entrega de fato hoje.
Cobre a divisão de Arrays.sort entre primitivos e Comparator, o comportamento indefinido e silencioso de binarySearch em dados não ordenados, e por que equals/toString precisam de suas variantes "deep" para arrays aninhados.
Explica a hierarquia de referências strong/soft/weak/phantom, o polling via ReferenceQueue, e como o Cleaner substitui o Object.finalize() para limpeza determinística de recursos nativos.
Cobre a arquitetura de providers JCA/JCE e o uso correto de MessageDigest, SecureRandom, Cipher, Signature e KeyStore, incluindo por que hashing simples e Random são substitutos inseguros para hashing de senhas e geração de tokens seguros.
Como a JVM carrega, faz o linking e inicializa classes através de uma hierarquia de delegação parent-first, e o que acontece quando você escreve um class loader customizado ou carrega a mesma classe duas vezes.
String.format/printf/Formatter compartilham o mesmo motor de conversão — width e precision significam coisas diferentes dependendo da conversão, as flags de vírgula/+/espaço/( cobrem a maioria das necessidades reais de formatação, e a indexação de argumentos n$/< permite reutilizar um argumento em vários especificadores sem repeti-lo.
Scanner tokeniza a entrada vinda de System.in, de uma String ou de um arquivo via pares hasNextX()/nextX(), com uma armadilha clássica: nextInt() deixa a quebra de linha final sem consumir, então um nextLine() seguinte lê uma string vazia em vez da próxima linha de verdade.
System.getenv() (ambiente do SO, herdado por processos filhos), System.getProperty() (escopo da JVM, definido via -D) e Runtime (fatos ao vivo como availableProcessors() e Runtime.version()) são três canais distintos para perguntar a uma JVM em que ela está rodando e como foi configurada.
A API nativa de logging do JDK, java.util.logging, raramente é chamada diretamente; código real faz logging através da fachada SLF4J contra um backend trocável (geralmente Logback), usando placeholders {} e MDC para saída estruturada e correlacionável.
O jlink liga módulos do JDK e da aplicação em uma imagem de runtime autocontida contendo só o que é alcançável, com o jdeps descobrindo primeiro a lista real de módulos — distribuindo um runtime Java enxuto em vez de um JDK completo.
O GraalVM reúne um JIT (o compilador Graal) mais o native-image, um compilador ahead-of-time que transforma uma aplicação Java em um executável nativo autocontido — trocando uma premissa de mundo fechado sobre reflection, proxies e recursos por startup quase instantâneo e uma pegada muito menor.
Além da reflection de inspecionar-e-invocar: method handles com verificações de acesso em tempo de lookup, setAccessible contra o InaccessibleObjectException do sistema de módulos e --add-opens, e três formas de fabricar uma classe em runtime — um ClassLoader customizado, o JavaCompiler em memória e a API padrão Class-File do java.lang.classfile.
Usar a API incubadora jdk.incubator.vector para expressar aritmética de arrays como operações SIMD explícitas por lane, em vez de depender do JIT para auto-vetorizar um laço escalar.
Como java.math.BigDecimal e BigInteger oferecem precisão arbitrária e arredondamento explícito onde double e long silenciosamente perdem dinheiro ou estouram.
Como Locale identifica um idioma e uma região, como ResourceBundle expande uma requisição em uma cadeia de arquivos .properties candidatos, e por que o locale padrão da JVM é tentado antes do bundle base, neutro em relação a idioma.
A API java.lang.foreign, padrão desde o JDK 22, permite que Java puro chame funções de bibliotecas nativas e gerencie memória off-heap com ciclos de vida vinculados a arenas, substituindo o código C escrito à mão do JNI.
O JDK ainda não tem suporte a JSON, então trabalhar com JSON significa escolher uma biblioteca e um de três modelos de processamento — data binding, o tree model, ou streaming — todos oferecidos pelo Jackson como o padrão de fato.
O que module-info.java realmente declara — requires, requires transitive, exports, opens, uses/provides — e por que um pacote não exportado é invisível para outros módulos mesmo quando todo tipo dentro dele é público. Cobre o módulo sem nome (onde a maior parte do código de aplicação ainda roda, sem nenhuma dessas garantias), módulos automáticos como ponte de migração, jlink para imagens de runtime mínimas, e os trade-offs honestos: pacotes divididos proibidos, a tensão entre opens e encapsulamento forte, e o quão lenta tem sido a adoção no mundo real.
java.time substituiu o java.util.Date e o Calendar — mutáveis, thread-unsafe e com mês indexado em 0 — por uma família de tipos pequenos e imutáveis, cada um modelando exatamente uma ideia: por que um Instant não tem getYear(), por que Duration e Period genuinamente não podem ser o mesmo tipo, o que \"mais um dia\" significa numa transição de horário de verão, e por que um DateTimeFormatter static final é seguro onde um SimpleDateFormat compartilhado era um bug de produção.
As regras tipográficas do Java (capitalização de pacotes, classes, métodos, campos e parâmetros de tipo) e as regras gramaticais (frases nominais para classes, frases verbais para métodos de ação, is/has para booleanos) que mantêm o código legível e previsível.
Como escrever doc comments (comentários de documentação) eficazes do Javadoc — contratos, tags, descrições resumo e herança — para toda classe, método e campo exportados.
Como um component scan funciona de fato por baixo dos panos: uma varredura recursiva de diretórios sobre arquivos .class compilados, Class.forName + isAnnotationPresent para achar classes marcadas por anotação, e por que frameworks de verdade leem metadados de bytecode via ASM em vez de carregar toda classe candidata.
Como o JShell permite avaliar expressões, declarar variáveis/métodos/classes, e obter feedback imediato sem um wrapper completo de classe/método main — a convenção de variável de resultado, referências antecipadas, os /commands essenciais, e como ele difere do lançamento de código-fonte em arquivo único.
O contrato de ordenação natural de Comparable<T> e sua recomendação de consistência com equals, as regras de idempotência de AutoCloseable vs. Closeable, o StackWalker como o substituto moderno e preguiçoso para inspeção de stack trace, e ProcessBuilder/Process/ProcessHandle para lançar e gerenciar processos do sistema operacional.
Como @Retention/RetentionPolicy controlam se uma annotation sobrevive além do código-fonte, as meta-annotations @Target/@Inherited/@Repeatable, a leitura de annotations em tempo de execução via AnnotatedElement, e as annotations embutidas @Deprecated/@SafeVarargs/@FunctionalInterface/@Documented.
Por que Pattern.compile() e Matcher existem como dois objetos separados (compile uma vez, faça match muitas vezes), a diferença entre find()/matches()/lookingAt(), e como grupos de captura — numerados e nomeados — extraem substrings de um match em vez de apenas testar sim/não.
Explica o modelo de Channel orientado a buffer em java.nio.channels, cobrindo o estado do ByteBuffer, buffers diretos vs. heap, zero-copy e I/O mapeado em memória do FileChannel, sockets não bloqueantes, e leituras/escritas scatter/gather.
URI é sintaxe pura, URL sabe também localizar e abrir um recurso, e URN nomeia um recurso sem dizer onde encontrá-lo — e desde o JDK 20 o JDK direciona a construção via URI.toURL() em vez dos construtores de URL, agora deprecados.
Separar o loop de accept de um ServerSocket da conversa por cliente para que o servidor realmente atenda clientes de forma concorrente, e usar Unix domain sockets (JEP 380, JDK 16) como uma alternativa mais rápida, protegida por permissões de arquivo e estritamente local ao TCP loopback.
O JDK distribui um servidor HTTP oficialmente suportado no módulo jdk.httpserver — HttpServer, HttpExchange, HttpsServer com um SSLContext para TLS, e SimpleFileServer/jwebserver — dando um endpoint HTTP sem dependências que a própria documentação escopa explicitamente para testes, desenvolvimento e depuração, não produção.
Como o estado de sessão é simulado sobre um HTTP sem estado — um ID de sessão aleatório, um round trip Set-Cookie/Cookie e um map do lado servidor indexado por esse ID — além do que falta na versão mínima (expiração, invalidação, armazenamento externalizado) e os atributos de segurança de cookie atuais (Secure, SameSite).
Como ServerSocket/Socket implementam a primitiva accept-connect-read-write por trás de todo web server, e o formato literal, em nível de byte, de uma requisição/resposta HTTP/1.1 (request line, headers, terminador de linha em branco, corpos com Content-Length/chunked, classes de status code).
A separação entre stream de bytes e stream de caracteres (InputStream/OutputStream vs. Reader/Writer), como Closeable/AutoCloseable/Flushable e try-with-resources garantem a limpeza de recursos com exceções suprimidas, buffering e PrintWriter, e como Serializable/ObjectInputFilter transformam um grafo de objetos em bytes protegendo contra ataques de deserialização.
Como o java.nio.file substitui o java.io.File por exceções informativas e específicas em vez de retornos booleanos, adiciona o DirectoryStream para iteração de diretório fechável/filtrável, suporte de primeira classe a links simbólicos e o WatchService para reagir a mudanças no sistema de arquivos em vez de fazer polling.
Como o padrão builder do HttpClient substituiu a API de baixo nível e pouco prática do HttpURLConnection — send() síncrono vs. sendAsync() assíncrono retornando um CompletableFuture — e como o BodyHandlers decide em que formato o corpo da resposta chega.