Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Java 25

Por Vinícius Dias

Esta versão do texto assume Java 25 (a versão de suporte longo lançada em setembro de 2025) ou mais recente. A seção “Instalação” do capítulo 1 traz as instruções para instalar ou atualizar o JDK com o SDKMAN, e o restante do capítulo explica como as versões do Java funcionam.

O formato HTML deste livro é gerado com o mdBook. O livro aceita contribuições da comunidade em seu repositório no GitHub, acessível também pelo ícone no topo da página.

Sobre esta edição

A versão 1.0 deste livro foi inteiramente gerada por inteligência artificial, sob direção humana. A geração não foi livre: o texto foi produzido sob um contrato de estilo explícito, com verificação mecânica a cada capítulo, e todos os capítulos passaram por extensa revisão humana, que definiu o escopo, a ordem, o domínio dos exemplos e corrigiu o que a máquina errou. O registro fica aqui por honestidade com quem lê: nenhuma parte do texto finge uma origem que não tem.

Com as contribuições da comunidade e com escrita minha, porque sempre gostei de escrever, virá a versão 2.0, mais humanizada e mais revisada. A 1.0, ainda assim, precisava vir ao mundo: eu legitimamente senti falta de aprender Java por um livro atualizado, e não encontrei no mercado uma abordagem que atendesse a todas as minhas necessidades pessoais, que bem podem ser as suas.

Boa sorte a todos que vão aprender com este livro.

Prefácio

Este é um livro de Java para quem parte do zero. Ele não assume que o leitor já programou em outra linguagem, não assume vocabulário prévio de computação e não assume ferramenta nenhuma além de um computador com terminal. O ponto de chegada é o núcleo da linguagem, o que se costuma chamar de Java core: a linguagem em si, a orientação a objetos como o Java a pratica, a biblioteca padrão nas partes que todo programa usa, testes, e a capacidade de ler e escrever um programa de verdade. O livro é uma experiência completa em si: termina com um sistema real, construído do zero e funcionando no terminal, e nada nele depende de um passo seguinte. Quem quiser seguir adiante encontra o caminho preparado, porque os frameworks de aplicação, como o Spring, são o rumo mais comum das carreiras Java, e o penúltimo capítulo constrói à mão o mecanismo central deles. Quem não quiser não perde nada: esse mesmo capítulo vale por si, como técnica de organizar programas grandes.

São vinte e cinco capítulos e cinco apêndices. A ordem não é temática, é de dependência: cada capítulo assume todos os anteriores, cada termo técnico é definido uma única vez, no capítulo que o apresenta, e usado livremente dali em diante. Por isso a leitura é sequencial, e pular um capítulo cobra o preço nos seguintes. Os apêndices são os únicos desviáveis: dependem do livro inteiro, mas nada no livro depende deles. O primeiro leva o projeto do livro para um banco de dados com JDBC, e é o único ponto em que o livro assume conhecimento de fora, a saber, SQL e um banco configurado; ele declara isso na própria abertura, junto com material de referência para quem precisar. Os três seguintes constroem interfaces gráficas com JavaFX. O último é um guia de leitura do Java legado: as grafias antigas, como System.out.println e a data da era anterior, que continuam por toda parte no código existente e que o leitor precisa reconhecer sem estranhar.

O mercadinho

Este livro tem um fio condutor: um mercadinho de bairro, com produtos e preços, estoque, lotes e validades, vendas, a caderneta de fiado e relatórios. Ele começa no capítulo 7, quando a linguagem passa a ter peças para construir um sistema, cresce capítulo a capítulo dali em diante e culmina no capítulo 25 como um programa completo de terminal; nos apêndices, vai para um banco de dados e ganha tela.

O mercadinho não é o único exemplo do livro, e não tenta ser. Cada conceito é ensinado com o exemplo que o mostra com mais clareza, venha ele de onde vier. O que o mercadinho garante é a acumulação: em cada capítulo, do 7 ao 24, o material novo também é aplicado ao sistema que já existe, e é nessa aplicação que aparece o que exemplos avulsos não mostram. Exemplo desconexo nasce limpo e morre na mesma página; num sistema que continua, uma escolha apressada no capítulo 7 cobra seu preço no capítulo 15, e consertá-la exige mexer em código que já existia. Aprender a conviver com isso é parte do que o livro ensina.

Como ler este livro

O livro fala com o leitor em notações fixas, e vale conhecê-las antes do primeiro capítulo.

Blocos de código aparecem de dois tipos. Os de fonte Java mostram o conteúdo de um arquivo, e são sempre completos ou têm a omissão marcada. Os de terminal mostram uma sessão real: as linhas que começam com $ são o que se digita (sem o $), e as linhas sem $ são a resposta da máquina, copiada sem edição. Mensagens de erro, em particular, aparecem por extenso, porque aprender a lê-las é conteúdo do livro, não ruído.

Em pontos escolhidos aparecem diagramas: caixas ligadas por setas com rótulos, lidas na direção das setas. Um diagrama resume o que a prosa acabou de dizer, nunca a substitui, e capítulo sem necessidade de diagrama não tem nenhum.

Além do texto corrido, quatro caixas aparecem ao longo dos capítulos. Cada uma se apresenta abaixo com a mesma aparência que terá lá dentro:

Esta caixa mostra um trecho de código e faz uma pergunta, em geral sobre o que será impresso, antes de revelar a resposta. Ela só funciona com participação: pare, decida a resposta de verdade, de preferência por escrito, e só então continue. Acertar confirma o modelo mental; errar vale mais ainda, porque expõe exatamente a peça que estava montada errado, no momento em que corrigi-la ainda é barato. A resposta vem sempre logo depois da caixa. Ler a pergunta e deslizar para a resposta desperdiça o instrumento.

Esta caixa marca comportamento da linguagem que parece uma coisa e faz outra, produzindo defeito silencioso: o programa roda, não avisa, e o resultado está errado. Na vida prática, é dessa família que saem os defeitos que atravessam testes e chegam a quem usa o programa, e por isso a caixa é cobrada nos exercícios.

Esta caixa é contexto: explica um porquê, um mecanismo interno, uma história. Muda o entendimento, não muda o que se escreve. Pode ser pulada sem quebrar a sequência do livro e nenhum exercício depende dela.

Esta caixa acompanha as raras comparações do livro com coisas de fora da computação. Toda analogia mente a partir de algum ponto; a caixa declara o ponto. O livro prefere explicação direta a analogia, e quando abre essa exceção, o limite vem junto.

Cada capítulo abre com um problema concreto e fecha com duas seções fixas. A Prática traz exercícios sem resposta publicada, de propósito: o critério de acerto (o programa compila? imprime o que a previsão dizia?) está na máquina do leitor, e exercício com gabarito ao lado treina conferência, não construção. A Ficha resume em tabelas os comandos e termos do capítulo, para consulta rápida quando um capítulo posterior os reutilizar.

O que é preciso ter

Um computador com Windows, Linux ou macOS, um terminal, um editor de texto qualquer e um JDK da versão 25 ou mais nova; o capítulo 1 explica o que é um JDK, de onde baixar e como conferir a versão. Antes do capítulo 14, nenhum outro programa é necessário.

Terminal é o programa de conversa em texto com o sistema: comandos digitados, respostas impressas, e é nele que tudo neste livro compila e roda. No macOS ele vem instalado com o nome Terminal; no Linux, com Terminal em algum lugar do nome, variando por distribuição; no Windows, o PowerShell faz esse papel até o capítulo 1 recomendar o WSL, que traz o terminal Linux para dentro do Windows.

Um ambiente integrado de desenvolvimento vai ser útil mais adiante, mas os primeiros capítulos são feitos deliberadamente no terminal. IDE, de integrated development environment, é o nome desses programas que reúnem editor, compilador e depurador, como o IntelliJ IDEA e o Eclipse. A IDE esconde exatamente os comandos que os primeiros capítulos ensinam, e quem aprende primeiro o que a ferramenta esconde depois a usa entendendo o que ela faz; o contrário não acontece.

O livro usa Java 25 porque é a versão de suporte longo mais recente na escrita, e porque um recurso dela, explicado no capítulo 2, deixa os primeiros programas do tamanho que programas de primeiro capítulo deveriam ter tido desde sempre.

A linguagem, a plataforma e as versões

89 c8
01 d0
c3

Um processador da família x86, a que equipa a maior parte dos computadores de mesa e dos servidores, recebe ordens nesse formato. Cada linha acima é uma instrução: a primeira copia um valor de um lugar para outro dentro do processador, a segunda soma dois valores, a terceira devolve o resultado ao ponto que pediu a soma. Esse formato tem nome: código de máquina, e é a única coisa que um processador executa. Tudo o que um computador faz, de abrir uma página a calcular um boleto ou gravar um arquivo, termina, em algum nível, em sequências como essa.

A própria escrita dessas linhas, porém, usa duas convenções que precisam ser desfeitas antes de qualquer outra coisa, porque as duas reaparecem pela vida inteira de quem programa. Um processador é um circuito elétrico, e um circuito distingue com segurança apenas dois estados: tensão presente ou tensão ausente. Toda informação que um computador guarda, transmite ou executa é, por isso, uma sequência desses dois estados, anotados no papel como 0 e 1. Cada posição de uma sequência dessas é um bit, a menor unidade de informação que existe; a escrita que usa apenas os símbolos 0 e 1 chama-se notação binária. Em notação binária, as três instruções da abertura têm a forma que existe de fato dentro da máquina:

10001001 11001000
00000001 11010000
11000011

Ler, escrever e conferir zeros e uns em quantidade é impraticável para uma pessoa, e por isso quase nenhum material técnico os mostra assim. A abertura usou a notação hexadecimal: um sistema de escrita de números com dezesseis símbolos (os algarismos de 0 a 9, seguidos das letras de a a f) no lugar dos dez algarismos da notação decimal, a da vida cotidiana. A escolha do dezesseis tem motivo: quatro bits admitem 2 × 2 × 2 × 2 = dezesseis combinações, uma para cada símbolo, de modo que cada símbolo hexadecimal corresponde exatamente a quatro bits, e um par de símbolos descreve um grupo de oito bits, chamado byte, a unidade em que quase tudo em computação é contado, de tamanho de arquivo a memória. O 89 da primeira linha da abertura e o 10001001 da primeira linha acima são o mesmo número: cento e trinta e sete, em notação decimal. Entre as três escritas muda a notação, nunca a quantidade. E a quantidade, aqui, nem é o que interessa: o processador não a trata como número, e sim como ordem a cumprir. Notação de número volta ao livro no capítulo 3, quando os valores que um programa manipula ganham forma escrita em Java.

Duas propriedades do código de máquina explicam por que ninguém constrói sistemas escrevendo essas linhas diretamente. A primeira é a escala: somar dois números são três instruções; emitir uma nota fiscal são milhões, e um texto de milhões de números não pode ser lido, revisado nem corrigido por uma pessoa. A segunda é a dependência do processador: cada família tem seu próprio conjunto de instruções, e as três linhas acima, levadas a um processador ARM, o dos celulares e de parte dos notebooks atuais, não significam nada. Um programa escrito assim vale para uma família de máquina e precisa ser reescrito para cada outra.

Uma linguagem de programação existe para atravessar essa distância. É uma notação de texto com regras exatas, desenhada para dois leitores ao mesmo tempo: a pessoa que escreve e mantém o texto, e o programa que traduz esse texto para uma forma que a máquina executa. Na linguagem deste livro, uma soma se escreve assim:

total = preco + multa;

O texto escrito em uma linguagem de programação chama-se código-fonte. O programa que lê código-fonte, confere se ele segue as regras da linguagem e produz a forma executável chama-se compilador.

“Regras exatas” tem um sentido preciso, e é ele que separa uma linguagem de programação de uma língua natural. Para cada texto possível, ou o texto pertence à linguagem e tem um único significado, ou não pertence, e então o compilador o recusa, apontando onde parou de entender. Não existe interpretação aproximada, não existe “deu para entender o que se quis dizer”. A consequência prática aparece no primeiro dia e não vai embora: o computador executa o que está escrito, não o que se pretendia escrever. Um texto quase correto não produz um programa quase correto; produz uma recusa do compilador ou, pior, um programa que faz, sem avisar ninguém, exatamente a coisa errada que está escrita.

O que é Java

Java é uma linguagem de programação publicada em 1995 pela Sun Microsystems, empresa comprada pela Oracle em 2010. Hoje a linguagem é desenvolvida em um projeto de código aberto (isto é, com o código-fonte publicado, para qualquer um ler e propor mudança), o OpenJDK, do qual participam Oracle, Amazon, Microsoft, Red Hat e outras empresas, e evolui por um processo público de propostas: qualquer mudança na linguagem começa como um documento numerado, discutido em aberto antes de virar código.

O nome, porém, cobre mais do que a notação do texto. O que se instala e se usa sob o nome Java são três coisas. A primeira é a linguagem propriamente dita: as regras que dizem o que é um texto Java válido e o que cada construção significa. A segunda é a biblioteca padrão: um conjunto extenso de código pronto, distribuído junto com a linguagem, para as tarefas que quase todo programa tem, como imprimir no terminal, ler e gravar arquivos, medir tempo e conversar com a rede. Boa parte do trabalho de escrever Java é chamar essa biblioteca, e boa parte deste livro é apresentá-la.

A terceira é a plataforma de execução, e é ela que distingue Java da descrição genérica dada até aqui. O compilador de Java não traduz o código-fonte para o código de máquina de um processador específico. Traduz para uma forma intermediária chamada bytecode: instruções definidas em especificação, não em silício, que cumprem o mesmo papel do código de máquina sem pertencer a nenhum processador real. Quem executa bytecode é a JVM (Java Virtual Machine, a máquina virtual Java): um programa instalado em cada máquina, que lê essas instruções e as cumpre usando o processador que houver ali. Existe uma JVM para x86 com Windows, outra para ARM com macOS, e assim por diante, todas lendo o mesmo bytecode. A consequência é o que define a plataforma: o mesmo programa Java, compilado uma vez, roda em qualquer sistema que tenha uma JVM; o que muda de uma máquina para outra é a JVM instalada nela, não o programa. O capítulo 2 põe as duas peças para funcionar no terminal, com os comandos que compilam e executam.

Essa arquitetura, somada a um compromisso incomum de compatibilidade (código compilado há vinte anos continua rodando nas versões atuais), explica onde Java está: em sistemas bancários, no comércio eletrônico de grande porte, em ferramentas de infraestrutura que sustentam outros sistemas, e na origem dos aplicativos Android, que usam a linguagem com outra plataforma de execução. Java é uma escolha frequente para sistemas que precisam funcionar por décadas e ser mantidos por pessoas que não estavam lá quando eles começaram. As decisões de projeto da linguagem, inclusive as que geram reclamação, fazem sentido lidas contra esse requisito.

O JDK e as distribuições

Desenvolver em Java exige instalar um único conjunto de programas: o JDK, sigla de Java Development Kit. Dele fazem parte o compilador de Java, a JVM e a biblioteca padrão. Instalado o JDK, o terminal passa a ter o comando java, e pedir a versão a ele é a primeira verificação a fazer em qualquer máquina:

$ java -version
openjdk version "25.0.4" 2026-07-15 LTS
OpenJDK Runtime Environment Corretto-25.0.4.7.1 (build 25.0.4+7-LTS)
OpenJDK 64-Bit Server VM Corretto-25.0.4.7.1 (build 25.0.4+7-LTS, mixed mode, sharing)

O número no começo da primeira linha é a versão, 25 neste caso. O nome Corretto, nas linhas seguintes, identifica a distribuição, e entender por que existem várias exige apresentar a origem comum de todas. OpenJDK é o projeto de código aberto onde a plataforma Java é desenvolvida: o código-fonte do compilador, da JVM e da biblioteca padrão mora ali, e é ali que as propostas públicas citadas na seção anterior viram código. O OpenJDK é também a implementação-padrão da plataforma, a chamada reference implementation: quando a especificação de uma versão fica pronta, é o código dele que a realiza e contra o qual as demais são conferidas.

Só que código aberto não é código pronto para usar: alguém precisa compilá-lo para cada sistema, testar, assinar o resultado e, sobretudo, continuar publicando correções por anos, e esse trabalho contínuo custa dinheiro. Cada empresa com interesse em Java o financia à sua maneira e distribui o resultado, que é o que se chama de distribuição. A Amazon mantém o Corretto porque roda Java em escala nos próprios servidores; a Microsoft faz o mesmo para a nuvem dela; a Oracle vende suporte sobre o Oracle JDK; a Eclipse Foundation publica o Temurin como distribuição comunitária, sem dono comercial; a Azul vive de suporte ao seu Zulu. Todas partem do mesmo código de origem, e por isso a linguagem, a biblioteca padrão e o comportamento dos programas são os mesmos em qualquer uma; o que muda é quem publica correções, por quanto tempo e sob quais termos de licença. Para este livro, qualquer distribuição serve, e trocar de uma para outra não muda uma linha do que vem pela frente.

Instalação

Cada distribuição tem instalador próprio, e qualquer um funciona. Este livro recomenda um caminho único, válido para Linux e macOS: o SDKMAN, um gerenciador que instala, lista e troca versões de JDK pelo terminal. Dois comandos o instalam, e um terceiro instala o JDK:

$ curl -s "https://get.sdkman.io" | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
$ sdk install java 25.0.4-amzn

O identificador 25.0.4-amzn nomeia a correção 25.0.4 do Corretto, a distribuição da Amazon, exatamente a instalação da máquina em que este livro roda: instalado por esse caminho, java -version responde as mesmas linhas do exemplo acima. O número exato muda a cada trimestre; o comando abaixo mostra os identificadores disponíveis no dia, e qualquer um que comece com 25, ou mais novo, atende ao livro:

$ sdk list java
================================================================================
Available Java Versions for Linux 64bit
================================================================================
 Vendor        | Use | Version  | Dist | Status     | Identifier
--------------------------------------------------------------------------------
 Corretto      | >>> | 25.0.4   | amzn | installed  | 25.0.4-amzn
               |     | 21.0.9   | amzn |            | 21.0.9-amzn
 Java.net      |     | 26.ea.36 | open |            | 26.ea.36-open
 Temurin       |     | 25.0.4   | tem  |            | 25.0.4-tem
               |     | 21.0.9   | tem  |            | 21.0.9-tem
...

A lista real é bem mais longa, com a omissão marcada acima. A primeira coluna traz a distribuição; a última, o identificador que o sdk install java recebe. A linha marcada com >>> é a instalação que o comando java usa no momento. Identificadores com ea no meio, como 26.ea.36, são montagens de acesso antecipado (early access) da versão ainda em desenvolvimento, e não atendem ao livro. Quando houver mais de um JDK na máquina, o SDKMAN também faz a troca: sdk default java, seguido de um identificador, passa a apontar o comando java para a instalação escolhida.

No Windows, a recomendação é instalar o WSL (Windows Subsystem for Linux), que põe um Linux completo dentro do Windows, e seguir o caminho acima dentro dele. Um comando faz a instalação, em um PowerShell aberto como administrador (o > faz as vezes do $ no PowerShell):

> wsl --install

Depois de reiniciar a máquina, o terminal do WSL é um terminal Linux comum. A recomendação tem um motivo que vai além deste capítulo: os comandos de terminal deste livro, como os de listar arquivos, mover e criar pastas que o capítulo 2 já usa, são os do mundo Unix, e as saídas mostradas vêm de um sistema assim. Nada disso é obrigatório: o JDK existe para Windows puro e o Java é o mesmo lá. Mas quem estiver em Linux, WSL ou macOS verá na tela exatamente o que o livro mostra.

Como as versões funcionam

Até 2017, uma versão nova de Java saía quando ficava pronta, em intervalos irregulares: quase cinco anos separam a versão 6 da 7; a 8 é de 2014, a 9 de 2017. Desde a versão 9 o calendário é fixo: uma versão nova em março e outra em setembro, todos os anos, estejam prontas as novidades que estiverem; o que não entrou espera a versão seguinte. O número da versão, portanto, mede posição no calendário, não tamanho de mudança: a distância entre a 24 e a 25 pode ser maior ou menor do que entre a 23 e a 24.

Novidade de linguagem raramente estreia pronta. Entra primeiro como prévia (preview, o nome que aparece na documentação em inglês): disponível para teste, sujeita a mudar de forma, desligada a menos que se peça para ligá-la. Depois de uma ou mais rodadas de prévia, a novidade é finalizada e passa a valer sem pedido nenhum. Este livro se apoia em um recurso que percorreu esse caminho e foi finalizado na versão 25; essa é a razão da exigência de versão feita na prática deste capítulo.

Desde a versão 17, a cada dois anos a versão de setembro recebe a marca LTS, de long-term support, suporte de longo prazo; antes dela o intervalo era maior, como a tabela da ficha registra. Uma versão LTS continua recebendo correções de erros e de segurança por anos; uma versão comum para de recebê-las seis meses depois de lançada, quando a seguinte chega. São LTS as versões 11 (2018), 17 (2021), 21 (2023) e 25 (2025), e também a 8, de 2014, anterior ao calendário atual e ainda viva em muitos sistemas antigos. Empresas que mantêm sistemas em produção em geral ficam nas LTS e pulam de uma para a outra; as versões intermediárias são usadas por quem quer testar as novidades no semestre em que saem. Este livro usa a versão 25.

As correções chegam como números acrescentados ao da versão: 25.0.1, 25.0.2, publicadas de três em três meses. Elas consertam defeitos e fecham brechas de segurança, mas não acrescentam recurso nenhum. O primeiro número é o único que decide o que o código-fonte pode usar.

Nem toda máquina é a deste livro. Num computador herdado de outra pessoa, de um estágio ou de um laboratório, o comando de verificação responde isto:

$ java -version
openjdk version "21.0.8" 2025-07-15 LTS
OpenJDK Runtime Environment Temurin-21.0.8+9 (build 21.0.8+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.8+9 (build 21.0.8+9-LTS, mixed mode)

Três perguntas, todas respondíveis com o que este capítulo apresentou: essa instalação ainda recebe correções? Ela atende ao que este livro exige? E o que precisa acontecer para que atenda — a instalação existente se transforma, ou não?

A primeira resposta é sim: 21 é a LTS de setembro de 2023, e a própria saída mostra a correção 21.0.8, publicada em julho de 2025, sinal de que a versão segue mantida. A segunda resposta é não: o livro exige a versão 25 ou mais nova, e essa instalação está quatro versões e dois anos atrás dela. Receber correções e estar atualizado são coisas diferentes: a saída acima é de uma máquina bem cuidada e, ainda assim, insuficiente aqui. A terceira: instalar um JDK da versão 25 ou mais nova, e nada se transforma sozinho. É uma segunda instalação, e o comando java do terminal precisa passar a apontar para ela. Uma máquina com JDK em dia para o sistema que ela roda pode estar anos atrasada para este livro, e o número na primeira linha é o que decide.

Numeração antiga. Até a versão 8, o número público vinha precedido de “1.”: java -version imprimia 1.8.0_292 para o que todo mundo chamava de Java 8. A grafia foi abandonada na versão 9, mas continua aparecendo em máquinas antigas e em material antigo. 1.8 e 8 são a mesma versão.

Prática

  1. Rode java -version na máquina em que o livro será acompanhado e anote três coisas: o número da versão, se ela é LTS e se atende ao que o livro pede. Se o comando não existir ou a versão for menor que 25, instale uma distribuição da 25 ou mais nova e repita a verificação.

  2. Sem consultar nada além do calendário fixo descrito neste capítulo, calcule qual número de versão estará recém-lançado em outubro do ano que vem e em que ano sai a primeira LTS depois da 25. Depois confira no site do OpenJDK.

  3. Escolha duas distribuições e compare, nas páginas de cada uma, até quando cada uma promete correções para a versão 25. Anote a diferença.

  4. Explique por escrito, em um parágrafo, por que o mesmo programa Java compilado roda em um processador x86 e em um ARM, dizendo qual peça muda de uma máquina para a outra e qual permanece a mesma.

Ficha do capítulo

TermoDefinição
código de máquinainstruções numéricas que um processador executa; específicas de cada família de processador
bita menor unidade de informação; vale 0 ou 1
bytegrupo de oito bits; a unidade comum de contagem em computação
notação bináriaescrita de números com dois símbolos, 0 e 1
notação hexadecimalescrita de números com dezesseis símbolos; cada um corresponde a quatro bits
linguagem de programaçãonotação de texto com regras exatas, escrita por pessoas e traduzida por programa
código-fonteo texto escrito em uma linguagem de programação
compiladorprograma que lê código-fonte, confere as regras da linguagem e produz a forma executável
bytecodeforma intermediária gerada pelo compilador de Java; igual em qualquer máquina
JVMmáquina virtual Java: programa instalado em cada máquina, que executa bytecode
biblioteca padrãocódigo pronto para tarefas comuns, distribuído junto com a linguagem
JDKconjunto instalável que reúne o compilador, a JVM e a biblioteca padrão
OpenJDKprojeto de código aberto onde o JDK é desenvolvido; a implementação-padrão (reference implementation) da plataforma
distribuiçãoempacotamento do OpenJDK publicado e mantido por uma empresa
SDKMANgerenciador que instala, lista e troca versões de JDK pelo terminal
WSLWindows Subsystem for Linux: um Linux completo dentro do Windows
prévia (preview)novidade disponível para teste, sujeita a mudança, desligada a menos que se peça
LTSversão com correções por anos; as demais recebem correções por seis meses
Versão LTSLançamento
82014
112018
172021
212023
252025

Hello world e como o Java executa

Um arquivo de três linhas é suficiente para pôr um programa Java de pé:

void main() {
    IO.println("Olá, mundo.");
}

O texto vai salvo em um arquivo chamado Ola.java, e um único comando no terminal o executa:

$ java Ola.java
Olá, mundo.

Esse é o programa inteiro: uma linha impressa e o fim. A partir dele, este capítulo provoca de propósito as duas falhas que vão acompanhar o leitor pelo livro inteiro. Trocar uma letra dentro de println produz uma mensagem que cita o arquivo, a linha e a coluna do problema. Mover um arquivo para uma subpasta produz outra mensagem, que não cita linha nenhuma e reclama de um nome. As duas se parecem com “não rodou”, e é exatamente aí que mora a dificuldade: elas vêm de programas diferentes, acontecem em momentos diferentes e se consertam de jeitos diferentes. Quem trata as duas como a mesma coisa procura o defeito no lugar errado, às vezes por horas. Separar esses dois momentos é o trabalho deste capítulo, e é o que torna legíveis as mensagens de erro de todos os capítulos seguintes.

As três linhas, uma a uma

A primeira linha declara um método. Um método é um bloco de código que tem nome: escreve-se o bloco uma vez e ele pode ser executado, pelo nome, quantas vezes forem necessárias. Declarar um método é escrevê-lo; chamar um método é fazer a execução entrar nele. As chaves { e } marcam onde o corpo do método começa e onde termina: tudo o que estiver entre elas é o que acontece quando o método for chamado, e nada fora delas pertence a ele.

O nome main não foi escolha deste livro. main é o método por onde todo programa Java começa: de tudo o que um arquivo declara, é esse o nome que a execução procura e chama primeiro, e o programa termina quando esse método termina. Um arquivo pode declarar outros métodos com outros nomes, mas a porta de entrada é sempre essa, e um dos exercícios deste capítulo mostra o que acontece quando ela não existe.

A palavra void, escrita antes do nome, declara que main não devolve nada a quem o chamou. Um método pode produzir um resultado e entregá-lo ao ponto que o chamou, para que a execução continue dali com esse resultado em mãos; void é a marca de que este método não entrega resultado nenhum. Como um método devolve, e o que muda no código de quem chama, é assunto do capítulo 4.

A linha do meio é uma chamada. IO.println é um método pronto: vem com o Java, dentro da biblioteca padrão, e está disponível em qualquer arquivo como este sem nenhuma linha extra. Ele recebe um argumento, que é o valor escrito entre os parênteses e entregue ao método no momento da chamada, e escreve esse valor na saída padrão, seguido de uma quebra de linha. Saída padrão é o canal por onde um programa de terminal escreve seu resultado comum; é o que o terminal mostra sem que ninguém peça nada. As aspas duplas marcam onde o texto a imprimir começa e onde termina, e não aparecem na saída: o programa imprime Olá, mundo., não as aspas em volta. O ln no fim do nome é a quebra de linha. Existe também IO.print, sem o ln, que escreve o mesmo valor e deixa a saída parada na mesma linha. A diferença parece miúda até o primeiro exercício em que duas mensagens saem grudadas uma na outra.

Resta explicar por que essas três linhas bastam como arquivo. Arquivo-fonte compacto é o nome do arquivo .java que declara métodos diretamente, como este. Antes da versão 25, o mesmo programa exigia linhas de moldura em volta do método, e praticamente todo material publicado até hoje, de livros a respostas em fórum, mostra essas linhas. O recurso que as dispensa passou por rodadas de prévia e foi finalizado na versão 25; dela em diante, o compilador fornece a moldura quando ela não está escrita. O que essa moldura declara, e por que ela deixa de ser dispensável assim que um programa passa de um arquivo, é assunto do capítulo 7. Há também uma segunda forma de escrever main, que recebe o que foi digitado no terminal depois do nome do programa; essa forma é o capítulo 5.

Duas linhas de um arquivo-fonte podem nunca chegar ao programa. Tudo o que vem depois de // até o fim da linha, e tudo o que estiver entre /* e */, é comentário: texto para quem lê o arquivo, que o compilador descarta por inteiro e que não influencia a execução de nenhuma forma. Os exercícios deste capítulo pedem descrições por escrito, e o próprio arquivo .java, em comentários, é um lugar razoável para elas.

A versão do JDK decide se o arquivo compila

O calendário de versões do capítulo 1 cobra aqui sua primeira consequência prática. O arquivo-fonte compacto só existe, como recurso finalizado, da versão 25 em diante. Em um JDK anterior, o compilador recusa a primeira linha do arquivo, e a recusa não menciona main nem IO, o que a torna difícil de ligar à causa verdadeira. A mesma recusa pode ser reproduzida em um JDK novo, mandando o compilador seguir as regras de uma versão antiga:

$ javac --release 24 Ola.java
Ola.java:1: error: implicitly declared classes are not supported in -source 24
void main() {
^
  (use -source 25 or higher to enable implicitly declared classes)
1 error

Vale conhecer essa mensagem de véspera, porque ela ensina uma regra de leitura: mensagem estranha apontando para a primeira linha de um arquivo que está correto é, quase sempre, sintoma de versão, não de código. Conferir a versão instalada antes de procurar defeito no arquivo evita o desencontro:

$ java -version
openjdk version "25.0.4" 2026-07-15 LTS
OpenJDK Runtime Environment Corretto-25.0.4.7.1 (build 25.0.4+7-LTS)
OpenJDK 64-Bit Server VM Corretto-25.0.4.7.1 (build 25.0.4+7-LTS, mixed mode, sharing)

Da saída interessa o número no começo da primeira linha, que precisa ser 25 ou maior; o resto identifica a distribuição, um Corretto nesta máquina, e não muda nada do que este livro faz. A moldura que o capítulo 7 apresenta elimina a exigência do arquivo-fonte compacto, mas não a da versão: IO.println também nasceu no 25, e os programas do livro seguem pedindo esse mínimo do começo ao fim.

Dois programas, dois momentos

O comando da abertura, java Ola.java, fazia dois trabalhos de uma vez, e enquanto os dois dão certo não há como perceber que são dois. Estes comandos os separam:

$ javac Ola.java
$ ls
Ola.class  Ola.java
$ java Ola
Olá, mundo.

javac é o compilador de Java: o compilador do capítulo 1, agora com nome e comando próprios. Ele lê o texto de Ola.java, confere se aquilo é Java válido e grava o resultado da tradução em um arquivo novo, o Ola.class. O conteúdo desse arquivo não é texto Java nem código de máquina de processador algum. É bytecode: até aqui uma ideia descrita no papel, daqui em diante um arquivo concreto no disco, que pode ser copiado, movido e apagado como qualquer outro. A extensão .class vem do nome de uma declaração que o capítulo 7 apresenta.

Quem lê e executa esse arquivo é a JVM. O comando java inicia uma, entrega a ela o bytecode indicado e sai do caminho: dali em diante, quem está rodando é o programa. As duas ferramentas, javac para traduzir e java para executar, vêm juntas no JDK, ao lado da biblioteca padrão onde mora IO.println.

A tradução em duas etapas é o mecanismo por trás da portabilidade prometida no capítulo 1. O Ola.class gerado aqui roda sem alteração em qualquer máquina que tenha uma JVM da mesma versão ou mais nova, seja ela x86 ou ARM, Windows, Linux ou macOS. O que se instala em cada máquina é a JVM certa para ela; o bytecode entregue é o mesmo em todas. Um compilador que traduzisse direto para o código de máquina produziria um programa mais rápido de iniciar e preso a uma família de processador; o desenho do Java troca esse arranque pela portabilidade.

O nome que aparece no comando java também sai desse desenho. Para um arquivo-fonte compacto, o compilador dá ao bytecode gerado o mesmo nome do arquivo: Ola.java vira Ola.class, e é Ola, sem extensão nenhuma, que se escreve depois de java. Renomear o arquivo antes de compilar muda as duas coisas juntas.

Os dois caminhos:

flowchart TB
    F["Ola.java<br/>código-fonte"]

    F -->|"javac Ola.java"| C["Ola.class<br/>bytecode em disco"]
    C -->|"java Ola"| E["execução na JVM"]

    F -->|"java Ola.java<br/>compila e executa na memória"| E

O erro que não deixa nada para trás

O compilador recusa o que não entende, e recusar quer dizer não gravar nada:

$ javac Ola.java
Ola.java:2: error: cannot find symbol
    IO.printn("Olá, mundo.");
      ^
  symbol:   method printn(String)
  location: class IO
1 error
$ ls
Ola.java

Esse é um erro de compilação: o compilador aponta arquivo, linha, coluna e o que não reconheceu, e o diretório continua sem nenhum Ola.class. A consequência vale ser dita por inteiro: um erro de compilação nunca alcança quem usa o programa, porque não há programa a entregar. Tudo o que o compilador consegue pegar, ele pega antes de existir qualquer coisa executável, e esse é o motivo de tanta coisa neste livro ser desenhada para transformar enganos em erros de compilação.

Duas leituras dessa mensagem valem virar hábito. A primeira: cannot find symbol significa que o nome escrito não existe no lugar em que foi procurado; na prática, um erro de digitação ou um nome que ainda não foi declarado. A segunda: quando o compilador imprime várias mensagens de uma vez, a primeira costuma ser a causa real e as demais costumam ser consequência dela. Corrigir a de cima e compilar de novo economiza mais tempo do que tentar entender todas de uma vez. Dois nomes nas linhas de detalhe da mensagem ainda não pertencem ao leitor; os capítulos 5 e 7 os apresentam, e as linhas que importam por enquanto são as três primeiras.

Nem toda recusa aponta a falta no lugar em que ela está. Retirar o ponto e vírgula do fim da linha produz esta mensagem:

$ javac Ola.java
Ola.java:2: error: ';' expected
    IO.println("Olá, mundo.")
                             ^
1 error

A coluna apontada é o fim da linha 2, não o começo da linha 3, porque o compilador só percebe a ausência quando termina de ler o que veio antes. As instruções simples, como a chamada da linha 2, terminam em ponto e vírgula, e as chaves marcam onde o corpo do método começa e acaba: essas duas regras respondem pela maior parte dos erros de compilação da primeira semana, e as mensagens delas nem sempre apontam para onde o dedo iria.

A outra família é a do erro de execução: o que o compilador não tem como prever e que só aparece do lançamento em diante, quando a JVM já está no comando. Alguns acontecem antes mesmo da primeira instrução do programa, como o da previsão a seguir; outros, no meio do trabalho. Os capítulos seguintes apresentam os erros de execução mais comuns, um a um, à medida que as construções que os provocam aparecem; o que este capítulo mostra é o primeiro da lista, e ele nasce de uma pergunta simples: como a JVM encontra o bytecode que deve executar?

O diretório tem o fonte e o bytecode. O bytecode vai para uma subpasta e o comando é repetido sem nenhuma outra mudança:

$ ls
Ola.class  Ola.java
$ mkdir saida
$ mv Ola.class saida/
$ java Ola

Ola.java continua ali, intacto, e o programa nele está correto. O que aparece no terminal?

O terminal responde isto:

Error: Could not find or load main class Ola
Caused by: java.lang.ClassNotFoundException: Ola

O fonte estar no diretório não ajuda em nada, porque java Ola não lê fonte nenhum: ele executa bytecode já pronto. E Ola, nesse comando, não nomeia um arquivo; nomeia o que a JVM tem de encontrar. Onde ela procura é o classpath: a lista de lugares em que a JVM busca bytecode. Quando ninguém diz nada, essa lista tem um item só, o diretório atual. O arquivo saiu do diretório atual, então saiu da lista, e a JVM não tem por onde continuar.

Dizer onde procurar resolve:

$ java -cp saida Ola
Olá, mundo.

-cp, que também se escreve -classpath, substitui a lista inteira pelo que vem depois dele. Vale gravar o formato da mensagem: could not find or load quase nunca quer dizer que o código não existe. Quer dizer que ele não está em nenhum dos lugares da lista. Procurar o defeito dentro do fonte, nesse caso, é procurar onde não está, e essa confusão responde por boa parte das primeiras horas perdidas de quem começa. A partir do capítulo 14, quem monta o classpath deixa de ser quem digita o comando e passa a ser a ferramenta de construção do projeto; até lá, ele é digitado à mão, e digitá-lo à mão algumas vezes é o que torna compreensível o que a ferramenta fará depois.

Dois comandos, o mesmo diretório, saídas diferentes. O arquivo é compilado com javac Ola.java; em seguida, no editor, o texto do fonte é trocado por “Tchau, mundo.”; e os dois comandos rodam:

$ java Ola
Olá, mundo.
$ java Ola.java
Tchau, mundo.

java Ola.java compila o fonte na memória e executa o que acabou de compilar, sem gravar nada em disco. java Ola ignora o fonte e executa o Ola.class que encontrar no classpath, que é o de antes da edição. O sintoma é uma correção que não faz efeito: o código muda, o comando roda sem erro nenhum, e a saída continua a antiga. Compilar antes de executar, sempre, é o que elimina essa classe de engano.

JIT. A JVM começa interpretando o bytecode uma instrução por vez e, nos trechos que se repetem muito, traduz esse bytecode para instruções do processador da máquina e guarda o resultado para as próximas passagens. Esse tradutor interno chama-se JIT, de just-in-time. É por isso que um mesmo trecho de código pode ficar mais rápido depois de alguns milhares de execuções, sem que nada tenha mudado no programa.

Prática

  1. Escreva um arquivo-fonte compacto que imprima três linhas, uma por chamada de IO.println, e execute-o sem compilar antes. Depois troque a última chamada por IO.print e descreva por escrito a diferença na saída.

  2. Compile o arquivo com javac, apague o .java e execute o programa. Escreva em uma frase o que esse resultado prova sobre o que a JVM precisa ter em mãos.

  3. Escreva IO.printn no lugar de IO.println e compile. Anote arquivo, linha e coluna citados. Depois execute o mesmo fonte com java direto e compare as duas mensagens: o que é igual, o que muda e por quê.

  4. Compile, crie a subpasta saida, mova para lá o arquivo de bytecode e faça o programa rodar sem devolver o arquivo ao diretório atual.

  5. Reproduza a armadilha: no mesmo diretório e sem editar nada entre um comando e outro, obtenha duas saídas diferentes. Escreva a menor regra de trabalho que torna esse engano impossível.

  6. Escreva um arquivo-fonte compacto cujo único método se chame principal em vez de main. Descubra em qual dos dois momentos o problema aparece e explique por que ele aparece nesse momento e não no outro.

Ficha do capítulo

ComandoO que faz
java Ola.javacompila em memória e executa; não grava bytecode
javac Ola.javagrava Ola.class e não executa nada
java Olaprocura o bytecode de nome Ola no classpath e executa
java -cp saida Olamesma coisa, procurando em saida em vez do diretório atual
TermoDefinição
métodobloco de código com nome, que pode ser chamado por esse nome
mainmétodo por onde a execução do programa começa
voidmarca de que o método não devolve nada a quem o chamou
argumentovalor escrito entre parênteses na chamada e entregue ao método
IO.printlnmétodo pronto que escreve o argumento na saída padrão e quebra a linha
saída padrãocanal por onde o programa escreve seu resultado comum no terminal
arquivo-fonte compactoarquivo .java que declara métodos diretamente, sem moldura em volta
javaco compilador de Java; grava bytecode e não executa nada
classpathlista de lugares onde a JVM procura bytecode; por omissão, o diretório atual
erro de compilaçãorecusa do compilador; cita arquivo e linha, e nada é gravado
erro de execuçãofalha que o compilador não prevê; encontrada pela JVM, do lançamento em diante

Variáveis, primitivos e operadores

Quatro linhas dentro de main, salvas em Maioridade.java:

void main() {
    int idade = 17;
    int ano = 2026;
    int maioridade = ano + 18 - idade;
    IO.println(maioridade);
}
$ java Maioridade.java
2027

O capítulo 2 imprimia o que já estava escrito no arquivo. Este programa é diferente: o valor 2027 não aparece em linha nenhuma do fonte. Ele nasceu de uma conta feita durante a execução, sobre valores guardados com nome nas duas primeiras linhas. Guardar valores e calcular com eles é o assunto deste capítulo, e as regras desse jogo têm consequências práticas: duas delas produzem resultados errados sem aviso nenhum, e este capítulo as provoca de propósito.

Variáveis

A linha int idade = 17; tem três partes: um tipo, um nome e um valor. Uma variável é um nome que guarda um valor durante a execução; escrever a linha acima é declarar a variável, e o sinal = faz a atribuição: calcula o que está à direita e guarda no nome à esquerda. O = de Java não é a igualdade da matemática, é uma ordem com direção. A linha seguinte é legal e útil:

idade = idade + 1;

Lida como igualdade, a linha é absurda; lida como ordem, é rotina: calcule idade + 1 com o valor atual, guarde o resultado de volta em idade. A variável passa a valer 18. Declarar duas vezes o mesmo nome no mesmo trecho, por outro lado, é erro de compilação: a declaração acontece uma vez, e as atribuições seguintes usam só o nome.

O tipo, primeira palavra da declaração, diz que espécie de valor o nome guarda e o que se pode fazer com ele. int guarda números inteiros; guardar outra coisa ali é recusado na hora:

$ javac Maioridade.java
Maioridade.java:2: error: incompatible types: possible lossy conversion from double to int
    int idade = 17.5;
                ^
1 error

O compilador leu o tipo declarado, conferiu o valor e recusou antes de existir programa. Essa conferência é o serviço que os tipos prestam: um engano de espécie de valor vira erro de compilação, com arquivo e linha, em vez de virar resultado errado em execução. Boa parte do desenho de Java existe para empurrar enganos nessa direção.

Oito primitivos, cinco em uso

Os tipos mais simples da linguagem chamam-se primitivos: neles, a variável guarda o próprio valor, direto. Existem oito; este livro trabalha com cinco, e os outros três ficam registrados na ficha do capítulo por completude.

Cada primitivo ocupa um número fixo de bits na memória, e é esse número que determina quantos valores cabem no tipo. O capítulo 1 apresentou o bit como a menor unidade de informação, com dois estados possíveis; combinar bits multiplica possibilidades: 2 bits formam 4 combinações, 3 bits formam 8, e cada bit acrescentado dobra o total, de modo que N bits distinguem 2^N valores. Um int ocupa 32 bits, o que dá 2³² combinações, cerca de 4,29 bilhões; metade delas fica com os negativos, e daí vem o intervalo de ±2,1 bilhões. Um long ocupa 64 bits, um double também 64, um char 16, e o boolean é o único cujo tamanho a linguagem não define, deixando a escolha para a JVM. Como oito bits formam um byte, os mesmos tamanhos aparecem por aí como 4 bytes para int e 8 para long.

int guarda inteiros de −2.147.483.648 a 2.147.483.647. Um valor escrito diretamente no código, como o 17 e o 2026 da abertura, chama-se literal. Java aceita o separador _ dentro de um literal numérico, e ele só existe para olhos humanos: 2_147_483_647 é o mesmo número. As notações do capítulo 1 também valem como literal: 0x1F é a escrita hexadecimal e 0b11111 a binária do mesmo 31 decimal. A base muda a escrita no fonte; o valor guardado é um só.

long guarda inteiros de até cerca de ±9,2 quintilhões (9,2 × 10¹⁸), e seu literal leva o sufixo L: 8_000_000_000L. É o tipo das contagens que passam dos dois bilhões, como milissegundos acumulados ou habitantes do planeta.

double guarda números com parte fracionária, escritos com ponto, não com vírgula: 19.90. O nome do formato é ponto flutuante, e a última seção deste capítulo mostra o que esse formato esconde.

boolean guarda apenas dois valores, true e false. Ele quase nunca é escrito como literal no dia a dia: nasce das comparações, apresentadas logo adiante, e comanda as decisões do capítulo 4.

char guarda um único caractere, entre aspas simples: 'A'. Aspas simples e aspas duplas não se trocam: "A", com aspas duplas, é um valor de outra espécie, apresentada no capítulo 5. Por dentro, char é um número. Cada caractere de cada escrita do mundo tem um código na tabela Unicode, a tabela que dá um número a cada caractere, e 'A' é o 65. Essa natureza numérica reaparece daqui a duas seções.

Expressões e operadores

Uma expressão é qualquer trecho de código que produz um valor: 17 é uma expressão, idade é uma expressão, ano + 18 - idade é uma expressão. Um operador é o símbolo que combina valores dentro de uma expressão. Os aritméticos são cinco: +, -, * para multiplicar, / para dividir e % para o resto da divisão. A precedência é a da escola: *, / e % antes de + e -, e parênteses decidem qualquer outra ordem. Em caso de dúvida, parênteses resolvem: não custam nada e eliminam a ambiguidade para o próximo leitor.

Um rateio simples:

void main() {
    int total = 7;
    int pessoas = 2;
    IO.println(total / pessoas);
    IO.println(total % pessoas);
}

Duas linhas de saída. Quais valores aparecem?

3
1

A divisão entre dois int produz um int: a parte fracionária é descartada, sem arredondamento (7 dividido por 2 daria 3,5; o resultado é 3, e 3,9 viraria 3 do mesmo jeito). O % entrega o que a divisão descartou como resto inteiro: 2 cabe 3 vezes em 7, sobra 1. O custo de não saber disso é concreto: média de avaliações, porcentual de desconto e rateio de conta saem errados, sem erro nenhum na tela, sempre que os dois lados da divisão são inteiros. O conserto aparece na seção de promoção, logo adiante.

Três atalhos completam o conjunto. Os operadores compostos aplicam a conta sobre a própria variável: total += 2 soma 2 a total, e -=, *=, /= seguem o padrão. total++ soma 1 e total-- subtrai 1; este livro os usa como instrução isolada, que é o uso que não guarda surpresa.

As comparações produzem boolean: == pergunta se dois valores são iguais, != se são diferentes, e <, <=, >, >= comparam ordem. IO.println(idade >= 18) imprime false para a idade da abertura. Vale fixar a diferença de papéis: = guarda, == compara. Combinar comparações entre si, com “e”, “ou” e “não”, pede operadores próprios, que chegam no capítulo 4 junto das estruturas que decidem.

Promoção e casting

Misturar tipos numa expressão é permitido, com uma regra fixa, e é ela que conserta o rateio da previsão: na divisão total / 2.0, um lado é int e o outro é double; antes da conta, o int é convertido para double, a divisão acontece entre dois double e o resultado é 3.5, com a fração preservada. Essa conversão automática do tipo mais estreito para o mais largo da expressão chama-se promoção, e segue a largura dos tipos: int promove para long, e ambos promovem para double. O char entra nas contas como o número que ele é: 'A' + 1 vale 66, um int.

A conversão no sentido contrário nunca é automática; ela existe, mas precisa ser pedida por escrito, com o tipo de destino entre parênteses:

double preco = 3.9;
int inteiro = (int) preco;
char letra = (char) 66;

Esse pedido chama-se casting. (int) 3.9 vale 3: o casting de double para int descarta a parte fracionária, sem arredondar. (char) 66 vale 'B', o caminho de volta da tabela Unicode. E um casting de long para int com valor grande demais corta o que não cabe, em silêncio: o compilador aceita porque a ordem foi explícita, e a responsabilidade pelo corte passa a ser de quem escreveu.

Quando a conta estoura

Uma multiplicação inocente:

void main() {
    int populacao = 2_000_000_000;
    int dobro = populacao * 2;
    IO.println(dobro);
}
-294967296

O programa compila, roda e imprime um número negativo. Nenhuma mensagem de erro aparece, em nenhum dos dois momentos.

O nome disso é overflow. Um int vive em 32 bits, e 32 bits comportam exatamente os valores do intervalo dado na seção dos primitivos; uma conta que passa do máximo dá a volta e continua a contagem do outro extremo, no lado negativo. A JVM não trata isso como erro: é o comportamento definido, o programa segue, e o valor errado se espalha pelas contas seguintes. É a primeira das duas surpresas prometidas na abertura, e aparece em lugares previsíveis: contadores que crescem sem parar, quantias em centavos somadas aos milhões, multiplicações de valores já grandes.

A correção é usar um tipo mais largo, com um detalhe que engana: trocar só o tipo da variável de destino não basta. long dobro = populacao * 2 imprime o mesmo valor errado, porque a conta entre dois int acontece em int, e o estrago já está feito quando o resultado é guardado. populacao * 2L resolve: com um long na expressão, a promoção sobe a conta inteira para long antes de multiplicar.

Complemento de dois. Nos 32 bits de um int, o bit mais alto indica o sinal, e os negativos são representados de um jeito que faz a soma funcionar com um circuito só, chamado complemento de dois. Somar 1 ao maior positivo produz o padrão de bits do menor negativo; por isso o estouro “dá a volta” em vez de parar o programa.

O que o ponto flutuante esconde

Uma soma de uma linha:

void main() {
    IO.println(0.1 + 0.2);
}

O que aparece no terminal?

0.30000000000000004

Esta é a segunda surpresa prometida na abertura. Um double guarda o número em binário, a notação do capítulo 1, e nem todo número decimal tem escrita binária exata. A notação binária também aceita casas depois da vírgula, cada uma valendo a metade da anterior: 0,1 em binário é um meio, 0,01 é um quarto, 0,11 é três quartos. Um décimo está para essa escrita como um terço está para a decimal: 1/3 em decimal vira 0,333… sem fim, e 1/10 em binário vira uma sequência periódica sem fim. O que cabe nos 64 bits de um double é a aproximação mais próxima; as sobras de 0.1 e de 0.2, invisíveis na impressão de cada um, aparecem na soma. Frações cujo denominador é potência de dois, como 0.5 e 0.25, são exatas, e por isso 0.5 + 0.25 imprime 0.75 sem ruído.

Duas regras práticas saem daí, e as duas voltam nos exercícios. Primeira: comparar double com == não é confiável, porque dois caminhos de cálculo que deveriam dar no mesmo valor podem diferir na última casa da aproximação. Segunda: dinheiro não se guarda em double; um sistema que soma centavos aproximados espalha diferenças de um centavo que ninguém consegue rastrear. Como dinheiro se representa em Java é assunto do capítulo 7, junto do sistema que vai precisar disso. O lugar do double é a medida física, peso, distância, temperatura, onde o valor já nasce aproximado por natureza.

var

Quando o valor inicial já deixa o tipo evidente, a palavra var declara a variável sem repeti-lo:

var total = 7;
var preco = 19.90;

O compilador olha o valor inicial e infere o tipo: total é int, preco é double. Isso se chama inferência de tipo, e vem com duas regras. A declaração com var exige o valor inicial na mesma linha, porque é dele que o tipo sai; e o tipo inferido é fixo dali em diante, exatamente como se estivesse escrito, de modo que total = 1.5 continua sendo recusado. var não muda o que a variável é; muda só quanto se digita. Nestes primeiros capítulos o livro escreve os tipos por extenso, para eles ficarem visíveis; var volta a aparecer no capítulo 17, quando os nomes de tipo crescem.

Prática

  1. Escreva um programa que guarde uma temperatura em graus Celsius num double e imprima a conversão para Fahrenheit, calculada como a temperatura vezes 9, dividida por 5, mais 32. Confira com um valor conhecido: 100 °C são 212 °F.

  2. Guarde duas avaliações inteiras, 7 e 8, e imprima a média. Obtenha primeiro o resultado errado, com divisão inteira; depois conserte usando promoção, sem mudar o tipo das duas variáveis.

  3. Parta de um int valendo 2.147.483.647, some 1 e imprima. Depois refaça a conta de modo a obter o valor correto, e explique por escrito em que ponto da linha a correção agiu.

  4. Imprima 0.1 + 0.2 e depois 0.5 + 0.25. Explique por escrito, apoiando-se na comparação com 1/3 em decimal, por que uma soma sai com ruído e a outra não.

  5. Imprima 'a', depois 'a' + 1, depois (char) ('a' + 1). Descreva o tipo do valor em cada uma das três impressões e o papel do casting na última.

  6. Escreva o valor 255 como literal decimal, hexadecimal e binário, guarde os três em variáveis e imprima os três, provando que a base muda a escrita e não o valor.

Ficha do capítulo

TipoBitsGuardaLiteral de exemplo
int32inteiros até ±2,1 bilhões42, 2_147_483_647, 0x2A, 0b101010
long64inteiros até cerca de ±9,2 × 10¹⁸8_000_000_000L
double64ponto flutuante19.90, 0.5
booleana JVM decidetrue ou falsetrue
char16um caractere (código Unicode)'A'
byte, short, float8, 16, 32versões menores de int e double; raras fora de arquivo e rede
OperadorO que faz
+ - * / %aritmética; / entre inteiros descarta a fração, % dá o resto
+= -= *= /=aplica a conta sobre a própria variável
++ --soma ou subtrai 1, como instrução
== != < <= > >=comparações; produzem boolean
(tipo)casting: conversão explícita, cortando o que não couber
TermoDefinição
variávelnome que guarda um valor durante a execução
declaraçãolinha que cria a variável, com tipo e nome
atribuiçãoo =: calcula a direita e guarda no nome da esquerda
tipoa espécie de valor que o nome guarda e as operações válidas sobre ele
primitivotipo cujo valor é guardado diretamente na variável
literalvalor escrito diretamente no código
expressãotrecho de código que produz um valor
operadorsímbolo que combina valores numa expressão
promoçãoconversão automática para o tipo mais largo da expressão
castingconversão explícita, escrita como (tipo), por conta de quem pede
overflowconta que passa do limite do tipo e dá a volta, sem aviso
ponto flutuanteformato binário aproximado do double
vardeclaração cujo tipo o compilador infere do valor inicial
inferência de tipodedução do tipo pelo compilador a partir do valor inicial

Fluxo de controle e decomposição

Um programa de cinema, salvo em Ingresso.java, precisa responder se um ingresso é de meia-entrada:

void main() {
    int idade = 16;
    if (idade < 18) {
        IO.println("Meia-entrada.");
    } else {
        IO.println("Inteira.");
    }
}
$ java Ingresso.java
Meia-entrada.

Todos os programas dos capítulos anteriores executavam as mesmas linhas, na mesma ordem, em toda execução. Este não: uma das duas impressões nunca acontece, e qual delas roda depende de um valor. Escolher o caminho da execução, repetir trechos e dar nome a pedaços do programa são as três habilidades deste capítulo, e com elas os programas deixam de ser listas de ordens para virar comportamento.

As construções que fazem isso têm nome de família: estrutura de controle é toda construção da linguagem que determina a ordem em que as instruções executam. Na ausência delas vale o fluxo sequencial, uma instrução após a outra, de cima para baixo, que foi o regime de todos os programas até aqui. As estruturas de controle quebram essa sequência de três maneiras: por seleção, escolhendo um entre dois ou mais caminhos, que é o território do if e do switch deste capítulo; por repetição, voltando a um trecho enquanto uma condição valer, que é o território dos laços; e por desvio, abandonando o fluxo corrente, como fazem o return e as palavras de controle de laço. Qualquer programa, em qualquer linguagem dessa família, se descreve com sequência, seleção, repetição e desvio, e o resto do capítulo apresenta as formas que Java dá a cada uma.

if, else e a condição

if executa um bloco somente quando uma condição vale. Condição é uma expressão de valor boolean, exatamente o que as comparações produzem: idade < 18 vale true ou false, e o if consulta esse valor. O bloco entre chaves logo após o if roda no caso true; o bloco do else, que é opcional, roda no caso false. Encadear decisões é escrever else if:

if (nota >= 90) {
    IO.println("A");
} else if (nota >= 70) {
    IO.println("B");
} else {
    IO.println("C");
}

As comparações acontecem de cima para baixo e a primeira condição verdadeira ganha: uma nota 95 imprime só A, porque o else if nem chega a ser avaliado. A ordem das faixas, portanto, faz parte da lógica, e invertê-la muda o programa.

As chaves são opcionais quando o corpo é uma única instrução, e este livro as escreve sempre. O motivo cabe numa armadilha, mais adiante.

Combinando condições

O capítulo 3 deixou uma promessa: os operadores que combinam decisões. São três. && é o “e”: a expressão inteira só vale true com os dois lados true. || é o “ou”: basta um lado true. ! é o “não”: inverte o valor que vem depois.

boolean meia = idade < 18 || idade >= 60;
boolean pagaInteira = !meia;

&& e || avaliam apenas o necessário, e isso tem nome: curto-circuito. Quando o lado esquerdo já decide o resultado, o lado direito nem é avaliado: um && com esquerda false já é false, um || com esquerda true já é true, e a execução segue sem tocar no resto. Não é só economia; é uma técnica de proteção. A divisão inteira por zero derruba o programa com um erro de execução chamado ArithmeticException, e o curto-circuito permite blindar a conta na própria condição:

if (pessoas != 0 && total / pessoas > 100) {
    IO.println("Rateio alto.");
}

Com pessoas valendo zero, o lado esquerdo é false, a divisão nunca acontece e o programa segue. Invertida a ordem dos dois lados, a mesma linha derruba o programa. Em curto-circuito, a ordem dos operandos é parte da correção, não do estilo.

Decisão em forma de valor

Duas construções decidem produzindo um valor, em vez de executar blocos. O operador ternário escolhe entre duas expressões:

int desconto = idade < 18 ? 50 : 0;

Lê-se: se a condição vale, o valor é o do meio; senão, o do fim. Ele serve para escolhas curtas dentro de uma atribuição. Encadear ternários dentro de ternários compila, mas obriga o leitor a resolver as condições de cabeça para descobrir qual valor sai; este livro não os encadeia.

Para escolher entre vários casos a partir de um mesmo valor, existe o switch expression (a expressão switch):

int minutosDeTreino = switch (dia) {
    case 1, 7 -> 60;
    case 6 -> 30;
    default -> 45;
};

O valor entre parênteses é comparado com cada case; a seta aponta o valor produzido; casos podem ser agrupados com vírgula; e default cobre todo o resto. O switch expression exige que algum caminho exista para qualquer valor possível, e com um int na entrada isso obriga o default: sem ele, erro de compilação. Essa exigência é uma proteção, e ela cresce de importância no capítulo 12, quando a entrada do switch passa a ser um tipo com lista fechada de valores. Material antigo mostra um switch de outro formato, com dois-pontos no lugar da seta e uma regra de continuação em que a execução atravessa também os casos seguintes por omissão; este livro usa apenas a forma nova.

Laços

Um laço repete um bloco enquanto uma condição valer. O while é a forma crua:

int restantes = 3;
while (restantes > 0) {
    IO.println(restantes);
    restantes--;
}
IO.println(0);
3
2
1
0

A condição é conferida antes de cada volta, inclusive a primeira: um while com condição inicial false não roda nenhuma vez. E uma condição que nunca vira false não para nunca; o programa fica preso, sem erro e sem saída, até ser morto no terminal com Ctrl+C.

Quando a repetição tem contador, começo e passo, o for empacota as três partes na primeira linha:

for (int volta = 1; volta <= 5; volta++) {
    IO.println(volta * 7);
}

A primeira parte declara e inicia o contador, a segunda é a condição conferida antes de cada volta, a terceira roda ao fim de cada volta. O trecho imprime a tabuada do 7 até 35. Dentro de qualquer laço valem duas palavras de controle, definidas aqui de passagem: break abandona o laço na hora, e continue pula direto para a próxima volta.

Um contador fracionário, somando de um décimo em um décimo até chegar a um:

void main() {
    double x = 0.0;
    while (x != 1.0) {
        x += 0.1;
    }
    IO.println("Cheguei.");
}

O programa compila e roda. O que acontece no terminal?

Nada, para sempre. O capítulo 3 mostrou que 0.1 não tem escrita binária exata; somado dez vezes, o acumulado passa perto de 1.0 sem valer exatamente 1.0, a comparação != nunca vira false, e o laço não termina. Nenhuma mensagem aparece, porque nenhum erro aconteceu: o programa está fazendo o que está escrito. A regra prática: contador de laço é inteiro; o double entra na conta de dentro, nunca no controle da repetição.

Métodos próprios e decomposição

Até aqui, todo código deste livro mora em main. Um arquivo-fonte compacto aceita outros métodos ao lado dele:

int precoDoIngresso(int idade) {
    if (idade < 18 || idade >= 60) {
        return 20;
    }
    return 40;
}

void main() {
    IO.println(precoDoIngresso(16));
    IO.println(precoDoIngresso(30));
}
20
40

Três novidades moram aí. int idade, entre os parênteses da declaração, é um parâmetro: uma variável que nasce a cada chamada, já valendo o argumento que veio de fora. O argumento é o valor entregue na chamada; o parâmetro é o nome que o recebe na declaração. A palavra return devolve um valor ao ponto que chamou e encerra o método naquele instante: na primeira chamada acima, o return 20 roda e o return 40 nem é alcançado. E o int antes do nome do método declara o tipo do valor devolvido, ocupando a posição onde main escreve void, a marca de quem não devolve nada.

O nome disso tudo é decomposição: partir um programa em métodos pequenos, cada um com um trabalho nomeado. O ganho não é estético. Um cálculo que existe num único lugar é corrigido num único lugar, e um método com nome honesto documenta o programa melhor do que comentário.

Cada método tem uma assinatura: o nome mais os tipos dos parâmetros, na ordem. É a assinatura que identifica o método, e por isso dois métodos podem ter o mesmo nome com parâmetros diferentes, o que se chama sobrecarga:

int dobro(int valor) {
    return valor * 2;
}

double dobro(double valor) {
    return valor * 2;
}

Com os dois métodos dobro acima no arquivo:

void main() {
    IO.println(dobro(21));
    IO.println(dobro(21.0));
    IO.println(dobro('a'));
}

Três linhas de saída. Quais, e qual dobro atende cada chamada?

42
42.0
194

O compilador escolhe pela assinatura que casa com o argumento: 21 é int e vai para o primeiro método, 21.0 é double e vai para o segundo. A terceira chamada é o caso interessante: não existe dobro de char, e o compilador converte o argumento por alargamento, na mesma direção da promoção: char cabe em int, e cabe também em double. Entre as versões capazes de receber o valor, vence a de tipo mais estreito: a de int é chamada com 97, o código de 'a', e devolve 194. A escolha acontece na compilação, em silêncio, e uma sobrecarga nova pode mudar para onde chamadas antigas vão. Sobrecarga é ferramenta boa para operações realmente iguais sobre tipos diferentes, e má para qualquer outra coisa.

Escopo

Uma variável não existe no programa inteiro. Ela nasce na linha da declaração e morre no fim do bloco onde nasceu, e esse alcance chama-se escopo. O contador declarado na primeira parte de um for vive só dentro do laço; a variável declarada dentro de um if vive só ali; um parâmetro vive só no corpo do seu método. Usar o nome fora do escopo reproduz o cannot find symbol da primeira semana: para o compilador, fora do bloco o nome simplesmente não existe.

O escopo é o que torna a decomposição segura. Dois métodos podem declarar variáveis de mesmo nome sem conflito, porque cada nome vive no seu corpo; quem escreve um método não precisa saber que nomes os outros usam. Em programas de um arquivo isso parece pouco; num sistema, é o que permite que partes escritas por pessoas diferentes convivam.

Recursão

Um método pode chamar a si mesmo, e isso se chama recursão:

int somaAte(int n) {
    if (n == 1) {
        return 1;
    }
    return n + somaAte(n - 1);
}

somaAte(4) devolve 4 + somaAte(3), que devolve 3 + somaAte(2), e assim até somaAte(1), que devolve 1 sem se chamar de novo: 10 ao todo. A linha do n == 1 é o caso base, a saída da repetição, e é a parte que não pode faltar. Sem caso base, as chamadas se acumulam na pilha de chamadas, a estrutura em que a JVM guarda as variáveis de cada chamada em andamento, até o limite dela, e o programa cai com um erro de execução chamado StackOverflowError; o aprofundamento abaixo detalha o mecanismo. Todo problema recursivo tem uma versão com laço, e este livro usa laços na maior parte do tempo.

A pilha de chamadas. A JVM guarda as variáveis de cada chamada de método em andamento numa estrutura que cresce a cada chamada e encolhe a cada return, a pilha de chamadas. É ela que dá a cada chamada as suas próprias variáveis, inclusive nas mil chamadas simultâneas de uma recursão. Recursão sem caso base enche a pilha até o limite, e o nome do erro, “estouro de pilha” em inglês, descreve exatamente isso.

Um ponto e vírgula a mais, depois da condição:

void main() {
    int saldo = -50;
    if (saldo >= 0);
    {
        IO.println("Saldo disponível.");
    }
}
Saldo disponível.

O saldo é negativo e a mensagem sai mesmo assim, sem erro nenhum.

O ; logo após o if é uma instrução vazia, válida, e é ela o bloco do if. As chaves seguintes formam um bloco solto, que roda sempre. O compilador não reclama porque nada está errado para ele; o programa apenas não diz o que parece dizer. O mesmo vale para o while: com x positivo, while (x > 0); gira em silêncio para sempre, porque o corpo vazio nunca altera x. É por acidentes dessa família que este livro escreve chaves mesmo em blocos de uma linha, e o hábito vale a pena desde o primeiro dia.

Prática

  1. Escreva um método que receba um ano e devolva true para ano bissexto: divisível por 4 e não por 100, exceto quando divisível por 400. Imprima o resultado para 2024, 2025, 2100 e 2400.

  2. Com um for de 1 a 100, imprima cada número, substituindo múltiplos de 3 por Fizz, múltiplos de 5 por Buzz e múltiplos de ambos por FizzBuzz. Decida a ordem das condições e explique por escrito por que ela importa.

  3. Escreva duas sobrecargas de um método maior: uma que compara dois int e uma que compara dois double. Depois chame maior(3, 4.5) e explique por escrito qual versão atendeu e por quê.

  4. Reproduza o laço infinito do contador double e conserte de duas formas diferentes: com contador inteiro e com comparação de ordem no lugar da comparação de igualdade. Escreva qual das duas correções é a melhor e o motivo.

  5. Escreva um método recursivo que conte de n até zero, imprimindo cada valor. Retire o caso base, execute, anote o nome do erro e explique por que ele é um erro de execução, e não de compilação.

  6. Escreva um switch expression que receba o número de um mês e devolva quantos dias ele tem num ano comum, agrupando os meses de mesma duração. Fevereiro fica com 28.

Ficha do capítulo

ConstruçãoO que faz
if / else if / elseexecuta o primeiro bloco cuja condição vale
cond ? a : boperador ternário: escolhe um valor pela condição
switch (v) { case ... -> ... }switch expression: escolhe um valor entre casos
while (cond) { }repete enquanto a condição valer; confere antes de cada volta
for (início; cond; passo) { }laço com contador empacotado
break / continueabandona o laço / pula para a próxima volta
return expr;devolve o valor e encerra o método
TermoDefinição
estrutura de controleconstrução que determina a ordem de execução das instruções; seleção, repetição ou desvio
condiçãoexpressão boolean consultada por if, laços e afins
ifexecuta um bloco somente com a condição valendo
curto-circuito&& e || não avaliam o lado direito quando o esquerdo decide
operador ternáriodecisão em forma de expressão: cond ? a : b
switch expressionescolha entre vários casos, produzindo um valor
laçorepetição de um bloco controlada por condição
parâmetrovariável do método que recebe o argumento da chamada
returndevolve um valor e encerra o método no ato
assinaturanome do método mais os tipos dos parâmetros
sobrecargamétodos de mesmo nome com assinaturas diferentes
escopoo trecho do programa em que um nome existe
recursãométodo que chama a si mesmo, com caso base obrigatório

Referências, objetos, String e arrays

Nenhum programa deste livro, até aqui, recebeu uma informação sequer de quem o executa. Este recebe:

void main() {
    String nome = IO.readln("Qual é o seu nome? ");
    IO.println("Bem-vindo, " + nome + ".");
}
$ java Recepcao.java
Qual é o seu nome? Ana
Bem-vindo, Ana.

IO.readln escreve a pergunta, espera a resposta terminar com Enter e devolve o que foi digitado. O valor devolvido é um texto, e texto em Java tem tipo: String. A segunda linha monta a saudação com o operador +, que entre textos tem outro significado, apresentado adiante. Duas linhas, e o programa passou a depender do que quem o executa digita. O preço é que String não é como os tipos que vieram antes, e a diferença entre as duas famílias de tipos é o assunto que dá nome a este capítulo, além de ser a origem de metade dos defeitos difíceis de quem começa.

String: o texto como valor

Um literal de String é o texto entre aspas duplas, presente desde o "Olá, mundo." do capítulo 2. A novidade é que esse valor carrega comportamento próprio: um String é um objeto, um valor composto que reúne dados e métodos, e os métodos se chamam com um ponto sobre o valor:

void main() {
    String curso = "Java 25";
    IO.println(curso.length());
    IO.println(curso.toUpperCase());
    IO.println(curso.charAt(0));
    IO.println(curso.substring(0, 4));
}
7
JAVA 25
J
Java

length() devolve a quantidade de caracteres; toUpperCase() devolve a versão em maiúsculas; charAt(0) devolve o char da posição zero, porque posições em Java começam do zero, um fato que volta em força na seção de arrays; substring(0, 4) devolve o trecho da posição 0 até antes da 4. Os parênteses vazios de length() são a primeira chamada sem argumento do livro: chamar sem entregar valor nenhum é permitido, e os parênteses continuam obrigatórios, porque são eles que fazem a chamada. Nenhum desses métodos altera o texto original; os que produzem texto devolvem outro String, e a seção de imutabilidade retoma essa propriedade.

O + entre um String e qualquer outro valor produz um String novo com os dois emendados, e isso se chama concatenação: "Bem-vindo, " + nome produziu "Bem-vindo, Ana". Números entram na emenda já convertidos para texto: "Total: " + 7 é o texto "Total: 7".

Arrays: vários valores sob um nome

Guardar as avaliações de um filme em variáveis nota1, nota2, nota3 para de funcionar na décima nota. Um array guarda uma sequência de valores do mesmo tipo sob um único nome:

void main() {
    int[] notas = { 8, 10, 7 };
    IO.println(notas.length);
    IO.println(notas[0]);
    notas[0] = 9;
    IO.println(notas[0]);
}
3
8
9

int[] é o tipo “array de int”, e as chaves com valores são o inicializador de array, uma escrita que só vale junto da declaração. Cada posição é lida e gravada pelo índice entre colchetes, contado a partir do zero: num array de 3 posições, os índices válidos são 0, 1 e 2. length, sem parênteses, informa o tamanho, fixado na criação e imutável dali em diante. Um array também nasce sem valores escolhidos, com a palavra new e o tamanho: new int[10] cria dez posições valendo zero, o valor padrão de int. A próxima seção mostra o que new faz.

Índice fora da faixa é um dos erros de execução clássicos da linguagem. Pedir notas[3] ao array de três posições acima produz:

$ java Notas.java
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at Notas.main(Notas.java:4)

A mensagem de ArrayIndexOutOfBoundsException entrega o índice pedido, o tamanho real e a linha. Ela costuma nascer de um laço com condição <= onde deveria ser <, porque o último índice válido é o tamanho menos um.

Percorrer um array combina com o for clássico, e existe uma forma dedicada a “visitar todos”, o laço for-each, que dispensa o índice quando a posição não importa:

int soma = 0;
for (int nota : notas) {
    soma += nota;
}

Lê-se: para cada nota em notas. A cada volta, a variável recebe o valor da posição seguinte, do primeiro ao último.

Referências: o que a variável guarda de verdade

Duas variáveis, um inicializador de array, uma alteração:

void main() {
    int[] a = { 10, 20, 30 };
    int[] b = a;
    b[0] = 99;
    IO.println(a[0]);
}

A alteração foi feita através de b. O que imprime a leitura através de a?

99

Uma variável de tipo primitivo guarda o próprio valor; uma variável de tipo objeto, como String e arrays, não guarda o objeto: guarda o endereço dele, e esse endereço se chama referência. Quando int[] b = a executa, o que se copia é o endereço, não o array: passa a haver um array e dois nomes para ele, e mutação através de um nome é visível através do outro. O compilador não avisa nada, porque nada está errado; apenas é assim que referências funcionam.

flowchart LR
    a["a"] --> obj["array: 99, 20, 30"]
    b["b"] --> obj

Os objetos em si vivem numa região da memória chamada heap, criados pelo new ou pelo inicializador, e as variáveis vivem na pilha de chamadas; o que elas guardam de um objeto é só a referência para lá. Cada objeto criado tem identidade: é ele mesmo, distinto de qualquer outro, mesmo que outro objeto tenha conteúdo idêntico. Dois arrays criados com { 10, 20, 30 } duas vezes são dois objetos; a e b acima são dois nomes para um só.

Um método que só deveria calcular:

int menorNota(int[] notas) {
    int menor = notas[0];
    for (int i = 0; i < notas.length; i++) {
        if (notas[i] < menor) {
            menor = notas[i];
        }
        notas[i] = 0; // limpa o rascunho, acreditando ser uma cópia
    }
    return menor;
}

void main() {
    int[] notas = { 8, 9, 7 };
    IO.println(menorNota(notas));
    IO.println(notas[0]);
}
7
0

O método devolveu o menor valor, mas a segunda impressão mostra o array de main alterado: a limpeza, escrita na crença de que o parâmetro era uma cópia, zerou o array original.

Quando o tipo de um parâmetro é um objeto, o que ele recebe é uma cópia da referência, e portanto aponta para o mesmo objeto de quem chamou: o que o método altera, o chamador vê. Não há erro em nenhum momento, e o defeito aparece longe da causa, em qualquer código que use o array depois. A disciplina que evita essa família de bugs é dupla: métodos que calculam não alteram o que recebem, e métodos que alteram dizem isso no nome.

Igualdade: == compara identidade, equals compara conteúdo

Um cofre com senha:

void main() {
    String digitada = IO.readln("Senha: ");
    if (digitada == "abracadabra") {
        IO.println("Cofre aberto.");
    } else {
        IO.println("Senha errada.");
    }
}
$ java Cofre.java
Senha: abracadabra
Senha errada.

A senha digitada está correta, o programa compila, roda e nega.

Entre referências, == pergunta se os dois lados apontam para o mesmo objeto, a identidade, e não se os conteúdos coincidem. O texto digitado é um objeto novo, criado na leitura; o literal "abracadabra" é outro; conteúdos iguais, objetos distintos, == falso. A pergunta certa para conteúdo tem nome: equals.

if (digitada.equals("abracadabra")) {

A regra de bolso do capítulo: primitivos se comparam com ==; objetos, com equals. O perigo desta armadilha é ela fingir que funciona: comparando dois literais iguais no mesmo teste rápido, o == chega a valer true, e o aprofundamento abaixo explica o porquê; o defeito só aparece com texto vindo de fora, longe do teste que “provou” que estava certo.

O pool de literais. Literais de String idênticos no código são guardados uma vez só, numa área chamada pool, e por isso "a" == "a" vale true: são o mesmo objeto. Texto construído em execução, como o devolvido por IO.readln, nasce fora do pool. É uma economia de memória da JVM, não uma regra de igualdade em que se possa apoiar.

Imutabilidade e StringBuilder

Nenhum método de String altera o texto: todos devolvem um String novo, e o original permanece intacto. Essa propriedade se chama imutabilidade, e é uma decisão deliberada da linguagem:

String curso = "java";
curso.toUpperCase();
IO.println(curso);
java

A segunda linha não é uma ordem para maiúsculas: é uma expressão cujo resultado, um String novo, foi jogado fora. Para ficar com o resultado, guarda-se o valor devolvido: curso = curso.toUpperCase(). A imutabilidade tem um preço quando o texto cresce aos pedaços: cada + cria um objeto novo, e mil voltas de concatenação criam mil objetos intermediários. Para montagem em etapas existe o StringBuilder, um objeto de texto que aceita alteração:

StringBuilder relatorio = new StringBuilder();
for (int nota : notas) {
    relatorio.append(nota);
    relatorio.append(" ");
}
IO.println(relatorio);

new StringBuilder() cria o objeto vazio, append acrescenta no fim, e IO.println o aceita diretamente. Como qualquer objeto vira texto na hora de imprimir é assunto do capítulo 10. A regra prática: concatenação com + para emendas pontuais; StringBuilder para montagem dentro de laço.

null e a entrada que vira número

Uma referência pode não apontar para objeto nenhum, e o literal desse estado é null. Chamar qualquer método através de uma referência nula derruba o programa com um erro de execução: o NullPointerException. O programa abaixo, salvo em Cadastro.java, o provoca:

void main() {
    String nome = null;
    IO.println(nome.length());
}
$ java Cadastro.java
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
	at Cadastro.main(Cadastro.java:3)

A mensagem diz qual chamada falhou e sobre o quê: o <local1> é a variável nome, cujo nome o compilador descarta na tradução, restando a posição dela. A linha aponta o local da queda; a causa verdadeira, porém, costuma estar antes, no ponto que deixou a referência nula. Nos programas deste livro, null quase não aparece por enquanto; ele volta no capítulo 17, quando buscas passam a poder terminar sem encontrar nada.

IO.readln devolve sempre String, inclusive quando o usuário digita um número: "25" é texto, e "25" + 1 é a concatenação "251", não a soma 26. A ponte para a aritmética é Integer.parseInt, que converte o texto num int:

void main() {
    String resposta = IO.readln("Sua idade: ");
    int idade = Integer.parseInt(resposta);
    IO.println(idade + 1);
}

Texto que não é um número derruba a conversão com um erro de execução chamado NumberFormatException, cuja mensagem mostra o texto recusado. O que fazer para o programa sobreviver a uma entrada malformada, em vez de cair, é assunto do capítulo 13. O nome Integer, e a família de que ele faz parte, é assunto do capítulo 17; por ora ele é o endereço onde mora a conversão.

A segunda forma de main

O capítulo 2 prometeu uma forma de main que recebe o que foi digitado no terminal depois do nome do programa. As peças deste capítulo a destravam:

void main(String[] args) {
    int a = Integer.parseInt(args[0]);
    int b = Integer.parseInt(args[1]);
    IO.println(a + b);
}
$ java Soma.java 2 3
5

args é um array de String com os argumentos da linha de comando, na ordem digitada: args[0] é "2", args[1] é "3", e args.length diz quantos vieram. Executado sem argumentos, o programa cai com o ArrayIndexOutOfBoundsException deste capítulo, pedindo o índice 0 de um array de tamanho zero; conferir args.length antes de ler é o hábito que evita a queda. As duas formas de main convivem na linguagem, e a JVM prefere a que declara o array quando as duas existem no arquivo.

Prática

  1. Escreva um programa que pergunte o nome e imprima três linhas: o nome em maiúsculas, a quantidade de caracteres e a primeira letra. Use apenas métodos apresentados neste capítulo.

  2. Reproduza a previsão dos dois nomes com um array seu e desfaça o compartilhamento: crie um segundo array do mesmo tamanho com new e copie os valores com um laço, provando com impressões que a alteração num deles parou de afetar o outro.

  3. Escreva um método media que receba um array de int e devolva a média como double, sem alterar o array recebido, usando a promoção do capítulo 3 para não cair na divisão inteira. Imprima a média de { 7, 8, 10 }.

  4. Reproduza a armadilha do cofre com ==, conserte com equals e depois quebre de novo: compare dois literais iguais com == e explique por escrito, citando o pool, por que esse caso engana.

  5. Com StringBuilder e um laço, monte numa única linha os números de 1 a 10 separados por vírgula, sem vírgula sobrando no fim.

  6. Escreva um programa que receba dois números pela linha de comando e imprima a soma, e que, executado com menos de dois argumentos, imprima uma instrução de uso em vez de cair. Anote qual erro de execução ele sofria antes da sua proteção.

Ficha do capítulo

ChamadaO que faz
texto.length()quantidade de caracteres
texto.toUpperCase()devolve a versão em maiúsculas
texto.charAt(i)o char da posição i
texto.substring(a, b)o trecho da posição a até antes de b
a.equals(b)compara conteúdo, não identidade
montagem.append(x)acrescenta ao fim do StringBuilder
Integer.parseInt(texto)converte o texto num int
IO.readln(pergunta)escreve a pergunta e devolve a linha digitada
new int[n]array de n posições com o valor padrão do tipo
TermoDefinição
objetovalor composto que reúne dados e métodos, criado durante a execução
referênciao endereço de um objeto; é o que a variável de tipo objeto guarda
newcria um objeto e devolve a referência para ele
heapregião da memória onde os objetos vivem
identidadecada objeto é ele mesmo, distinto até de outro de conteúdo igual
nullreferência que não aponta para objeto nenhum
NullPointerExceptionerro de execução ao usar uma referência nula
Stringo tipo do texto; objeto imutável
equalscomparação de conteúdo entre objetos
concatenaçãoo + entre String e outro valor, produzindo String novo
imutabilidadepropriedade do objeto que nunca muda após criado
StringBuilderobjeto de texto alterável, para montagem em etapas
Integer.parseIntconverte String em int; recusa texto malformado
IO.readlnlê uma linha do terminal e a devolve como String
arraysequência de tamanho fixo de valores do mesmo tipo
índiceposição num array, contada do zero
ArrayIndexOutOfBoundsExceptionerro de execução por índice fora da faixa
laço for-eachpercorre todos os valores sem usar índice
String[] argsforma de main que recebe os argumentos da linha de comando

Math e Random

Com decisões, laços e entrada de teclado, um jogo de adivinhação está quase ao alcance: o programa pensa num número, a pessoa chuta, o programa responde se o segredo é maior ou menor. Falta uma peça que nenhum código dos capítulos anteriores produz: um número que ninguém escolheu.

void main() {
    Random sorteio = new Random();
    IO.println(sorteio.nextInt(1, 101));
    IO.println(sorteio.nextInt(1, 101));
}
$ java Sorteio.java
73
12
$ java Sorteio.java
41
89

Duas execuções, quatro valores diferentes. Sob o nome Random, a biblioteca padrão oferece um gerador de números: new Random() cria um, do mesmo jeito que new StringBuilder() criou o objeto de texto do capítulo 5, e cada chamada de nextInt(1, 101) devolve um inteiro de 1 a 100. O que exatamente é o nome Random, e por que new Random() tem essa forma, é assunto do capítulo 7; este capítulo é sobre usá-lo bem, e sobre o punhado de contas prontas que mora sob o nome Math.

Sortear números

nextInt existe em duas formas. Com dois argumentos, nextInt(inicio, fim) devolve um inteiro do início até antes do fim: nextInt(1, 101) produz de 1 a 100, nunca 101. Com um argumento, nextInt(n) devolve de 0 até antes de n: nextInt(6) produz de 0 a 5. Nas duas formas o limite superior fica de fora, no mesmo espírito dos índices de array do capítulo 5, que vão de zero até o tamanho menos um. Além dos inteiros, nextDouble() devolve um double de 0 até antes de 1, e nextBoolean() devolve true ou false em cara ou coroa.

Um dado de seis faces para um jogo de tabuleiro:

void main() {
    Random dado = new Random();
    for (int volta = 0; volta < 5; volta++) {
        IO.println(dado.nextInt(6));
    }
}
2
0
5
1
3

O programa compila, roda e imprime cinco lançamentos de aparência razoável. O dado, porém, está viciado de um jeito que nenhuma dessas execuções denuncia.

nextInt(6) produz de 0 a 5: este dado tem uma face zero que não existe e nunca tira seis. Nenhum erro acontece, os valores parecem plausíveis, e o defeito só seria flagrado por quem contasse milhares de lançamentos. O limite de fora é, com Random, a fonte clássica dos erros de off-by-one, o engano de uma unidade num limite, e a correção tem duas grafias equivalentes: dado.nextInt(6) + 1 ou dado.nextInt(1, 7). Na dúvida, a forma de dois argumentos deixa a intenção escrita: de 1 até antes de 7.

Pseudoaleatório e a semente

Dois geradores, criados com o mesmo argumento 42:

void main() {
    Random primeiro = new Random(42);
    Random segundo = new Random(42);
    IO.println(primeiro.nextInt(1, 101));
    IO.println(primeiro.nextInt(1, 101));
    IO.println(segundo.nextInt(1, 101));
    IO.println(segundo.nextInt(1, 101));
}

Quatro sorteios. Que relação existe entre as duas primeiras linhas e as duas últimas?

31
64
31
64

As duas sequências são idênticas, e idênticas em toda execução, em qualquer máquina. Os números de Random não são sorteados: são calculados, um a partir do anterior, numa sequência completamente determinada pelo valor inicial. Por isso o nome técnico é número pseudoaleatório, e o valor que determina a sequência inteira chama-se semente. new Random(42) fixa a semente em 42; new Random(), sem argumento, tira a semente do relógio e de outras fontes variáveis da máquina, e é isso que faz cada execução parecer um sorteio novo.

Semente fixa parece derrotar o propósito, e é o contrário: é a ferramenta de trabalho. Um defeito que só aparece com certos valores sorteados é um defeito que some quando se tenta observá-lo; com a semente anotada, a execução inteira se repete à vontade, valor por valor. O mesmo vale para comparar duas versões de um programa sob os mesmos dados. A reprodutibilidade volta ao livro no capítulo 15, quando os programas passam a ser testados automaticamente.

De onde saem os números. Um gerador clássico guarda um valor interno e, a cada pedido, o transforma com multiplicação, soma e resto, devolvendo um pedaço do resultado. A sequência é longa o bastante para parecer sorteio, mas quem conhece o valor interno prevê todos os próximos números. Para sorteio valendo prêmio ou segurança existe SecureRandom, um gerador da mesma família de uso cuja saída não se prevê; para jogos, simulações e testes, Random basta e é mais rápido.

Math: contas prontas

Sob o nome Math a biblioteca padrão reúne dezenas de contas prontas, chamadas pelo próprio nome, sem new, do mesmo jeito que IO.println. O porquê dessa diferença em relação a Random é assunto do capítulo 7. Este livro usa um punhado delas, e declara desde já que não pretende cobrir as demais; a lista completa vive na documentação oficial.

void main() {
    IO.println(Math.max(7, 12));
    IO.println(Math.min(7, 12));
    IO.println(Math.abs(-15));
    IO.println(Math.round(2.5));
}
12
7
15
3

max e min escolhem entre dois valores, abs descarta o sinal, e round arredonda para o inteiro mais próximo, devolvendo long; nos empates, arredonda para cima, e por isso 2.5 foi a 3, enquanto Math.round(-2.5) daria −2. round é o arredondamento que o casting do capítulo 3 não faz: (int) 2.9 trunca para 2, Math.round(2.9) vai a 3. Na ficha ficam ainda pow e sqrt, para potência e raiz, à disposição de quem precisar; nenhum exercício deste livro exige matemática além da aritmética.

Tudo junto: o jogo

As peças dos capítulos 3 a 6 montam o jogo completo da abertura:

void main() {
    Random sorteio = new Random();
    int segredo = sorteio.nextInt(1, 101);
    int chute = 0;
    while (chute != segredo) {
        chute = Integer.parseInt(IO.readln("Chute um número de 1 a 100: "));
        if (chute < segredo) {
            IO.println("O segredo é maior.");
        } else if (chute > segredo) {
            IO.println("O segredo é menor.");
        }
    }
    IO.println("Acertou!");
}
$ java Jogo.java
Chute um número de 1 a 100: 50
O segredo é maior.
Chute um número de 1 a 100: 75
O segredo é menor.
Chute um número de 1 a 100: 62
Acertou!

Quinze linhas: variáveis e conversão do capítulo 3 e do 5, decisão e laço do capítulo 4, leitura do teclado do 5, sorteio deste. É o primeiro programa do livro que ninguém executa duas vezes igual, e a prática abaixo o estica em todas as direções.

Prática

  1. Corrija o dado da armadilha e prove a correção com força bruta: lance-o um milhão de vezes num laço, conte num array de contadores quantas vezes cada face saiu e imprima as contagens. Faces com contagens próximas, e nenhuma face zero, encerram a discussão.

  2. Simule mil lançamentos de moeda com nextBoolean e imprima quantas caras e quantas coroas saíram. Rode três vezes e observe a variação em torno da metade.

  3. Acrescente ao jogo um contador de tentativas e imprima-o na vitória. Depois limite a sete tentativas, encerrando com a revelação do segredo quando elas acabarem.

  4. Rode o jogo com semente fixa e jogue duas partidas idênticas, provando que o segredo se repete. Explique por escrito por que essa versão é melhor para demonstrar o jogo numa aula e pior para jogar de verdade.

  5. Sem usar Math, escreva um método que devolva o maior de três valores int, usando apenas o capítulo 4. Depois reescreva em uma linha com duas chamadas de Math.max e compare a legibilidade das duas versões por escrito.

Ficha do capítulo

ChamadaO que faz
new Random()cria um gerador com semente tirada do relógio
new Random(semente)cria um gerador de sequência reprodutível
nextInt(a, b)inteiro de a até antes de b
nextInt(n)inteiro de 0 até antes de n
nextDouble()double de 0 até antes de 1
nextBoolean()true ou false
Math.max(a, b) / Math.min(a, b)o maior / o menor de dois valores
Math.abs(x)valor sem sinal
Math.round(x)inteiro mais próximo, como long
Math.pow(a, b) / Math.sqrt(x)potência e raiz quadrada
TermoDefinição
Randomgerador de números pseudoaleatórios; a sequência é determinada pela semente
número pseudoaleatóriovalor calculado em sequência determinada, com aparência de sorteio
sementevalor inicial que determina toda a sequência do gerador
Mathconjunto de contas prontas da biblioteca padrão, chamadas pelo nome

Classes, construtores e encapsulamento

O mercadinho prometido no prefácio abre as portas neste capítulo, e a primeira tentativa de registrar o estoque dele usa o que o livro tem até aqui:

void main() {
    String[] nomes = { "Arroz 5kg", "Café 500g", "Sabão em pó" };
    int[] estoques = { 40, 25, 12 };
    IO.println(nomes[1] + ": " + estoques[1] + " unidades");
}
$ java Estoque.java
Café 500g: 25 unidades

Funciona, e é uma armadilha armada. Os dados de um produto estão espalhados em dois arrays que só se correspondem pela posição: o café é o nome de índice 1 e o estoque de índice 1, e nada além de disciplina mantém esse acordo. Basta remover um produto de um array e esquecer o outro, ou ordenar os nomes sem ordenar os estoques, para o café passar a exibir o estoque do sabão, sem erro de compilação e sem erro de execução. A raiz do problema é que “produto” não existe no programa; existem pedaços de produto em lugares que o compilador não sabe que se relacionam. Este capítulo cria tipos próprios, e com eles o mercadinho começa a virar sistema.

Um tipo novo: a palavra-chave class

A palavra-chave class declara uma classe: a definição de um tipo de objeto, com os dados que cada objeto carrega e os métodos que operam sobre esses dados. Os dados declarados na classe chamam-se campos:

class Produto {
    String nome;
    int estoque;
}

void main() {
    Produto cafe = new Produto();
    cafe.nome = "Café 500g";
    cafe.estoque = 25;
    IO.println(cafe.nome + ": " + cafe.estoque + " unidades");
}

O capítulo 5 apresentou objetos prontos, como String e StringBuilder; aqui, pela primeira vez, o tipo é nosso. Cada new Produto() cria uma instância: um objeto independente desse tipo, com os próprios valores nos campos. Duas instâncias de Produto são dois objetos no heap, cada um com seu nome e seu estoque, e a variável cafe guarda a referência de um deles. Nome, estoque e o que mais o produto tiver agora viajam juntos, e o desalinhamento dos arrays paralelos deixa de ser possível: não há mais duas listas para dessincronizar.

Construtor e this

Criar o objeto vazio e preencher campo a campo, como acima, deixa um intervalo perigoso: entre o new e a última atribuição existe um produto pela metade, visível a qualquer código que rode no meio. O construtor fecha esse intervalo. Construtor é o método especial que roda no new, tem o mesmo nome da classe, não declara tipo de retorno e recebe o necessário para o objeto nascer completo:

Um construtor escrito às pressas:

class Produto {
    String nome;
    int estoque;

    Produto(String nome, int estoque) {
        nome = nome;
        estoque = estoque;
    }
}

void main() {
    Produto cafe = new Produto("Café 500g", 25);
    IO.println(cafe.nome);
}

O programa compila. O que a impressão mostra?

null

Dentro do construtor existem dois nome: o parâmetro e o campo. Quando dois escopos sobrepostos declaram o mesmo nome, o mais interno vence, uma regra nova deste capítulo chamada sombreamento, e nome = nome fala do parâmetro duas vezes, atribuindo-o a ele mesmo. O campo nunca é tocado e fica com o valor padrão: campo de objeto nasce valendo zero, false ou null, conforme o tipo, ao contrário da variável local, que o compilador recusa usar sem valor. Nenhum aviso, porque a linha é válida. A palavra this resolve: ela é a referência da própria instância em construção, e this.nome aponta sem ambiguidade para o campo:

Produto(String nome, int estoque) {
    this.nome = nome;
    this.estoque = estoque;
}

Invariantes e a recusa no nascimento

Um produto de estoque negativo não descreve coisa nenhuma do mercadinho; se um objeto assim circular pelo sistema, cada relatório que o somar sai errado. A condição que todo objeto válido de um tipo sustenta, do nascimento em diante, chama-se invariante, e o construtor é o lugar de defendê-la:

Produto(String nome, int estoque) {
    if (nome == null || nome.isBlank()) {
        throw new IllegalArgumentException("Produto precisa de nome.");
    }
    if (estoque < 0) {
        throw new IllegalArgumentException("Estoque não pode ser negativo: " + estoque);
    }
    this.nome = nome;
    this.estoque = estoque;
}

A palavra throw dispara um erro de execução no ponto do problema, e IllegalArgumentException é o erro padrão da biblioteca para argumento que viola as regras de quem recebe; a mensagem entre parênteses aparece na tela com a linha do disparo. O ganho é de localização: sem a defesa, o estoque negativo entra sem aviso e o efeito aparece longe, num relatório qualquer; com ela, o programa cai no instante e na linha em que o valor ruim tentou entrar, com o valor impresso. O mecanismo completo por trás do throw, incluindo como um programa reage a ele em vez de cair, é o capítulo 13; isBlank, usado ali, apenas pergunta se o texto está vazio ou só com espaços.

Encapsulamento

A defesa do construtor tem um furo: qualquer código pode escrever cafe.estoque = -8 depois do nascimento, por engano ou por atalho. A solução é controlar o acesso. Os modificadores de acesso definem quem enxerga cada membro da classe, o nome coletivo de campos, métodos e construtores: private restringe à própria classe, public libera para todo o programa. Campo fica private; o que o resto do programa pode fazer vira método public, e passa pela regra:

class Produto {
    private final String nome;
    private int estoque;

    Produto(String nome, int estoque) {
        // validações do construtor, como antes
        this.nome = nome;
        this.estoque = estoque;
    }

    public String nome() {
        return nome;
    }

    public int estoque() {
        return estoque;
    }

    public void baixar(int quantidade) {
        if (quantidade <= 0 || quantidade > estoque) {
            throw new IllegalArgumentException("Baixa inválida: " + quantidade);
        }
        estoque -= quantidade;
    }
}

Isso é encapsulamento: esconder a representação interna de um objeto e expor apenas operações que mantêm as invariantes. O estoque continua existindo, mas o único caminho até ele agora valida cada baixa; o cafe.estoque = -8 passa a ser recusado pelo compilador em qualquer classe de arquivo próprio, porque o campo é invisível fora de Produto. Uma ressalva de laboratório: no arquivo-fonte compacto, em que tudo divide um arquivo, as classes dali de dentro se enxergam por inteiro, private incluído, e a recusa só vale com cada classe no seu arquivo, que é o formato da última seção e de todo o livro daqui em diante. O final no campo nome acrescenta outra trava, do próprio compilador: o modificador final marca o que recebe valor uma vez e nunca mais, e produto que muda de nome não existe neste domínio. A regra prática do capítulo: campo private sempre; public é decisão, tomada método a método, e cada método public é uma promessa de comportamento que o resto do sistema vai usar.

Dinheiro entra no mercadinho

Falta o preço, e o capítulo 3 deixou dito que double não serve: centavos aproximados espalham diferenças que ninguém rastreia. Existem duas soluções honestas. A primeira é guardar centavos em inteiros: int precoEmCentavos, com 1990 valendo R$ 19,90. É exata, rápida, muito usada em sistemas de pagamento, e o nome do campo carrega a unidade para ninguém somar centavos com reais. A segunda é o BigDecimal, o tipo da biblioteca padrão para números decimais exatos, e é a escolha deste livro, por dois motivos: os valores aparecem no código como aparecem na etiqueta, e o tipo é um objeto imutável de método em método, o que exercita exatamente o que este capítulo ensina.

BigDecimal preco = new BigDecimal("19.90");
BigDecimal total = preco.multiply(new BigDecimal(3));
IO.println(total);
59.70

BigDecimal nasce de new, como qualquer objeto, e o argumento do construtor é um String com o valor exato. As contas são métodos: add soma, subtract subtrai, multiply multiplica, e todos devolvem um BigDecimal novo, porque o tipo é imutável como o String; comparações de ordem usam o método compareTo, que devolve negativo, zero ou positivo. No arquivo compacto, BigDecimal resolve sem linha extra; o import que a moldura exige aparece na última seção.

O construtor de BigDecimal também aceita um double:

BigDecimal errado = new BigDecimal(0.1);
BigDecimal certo = new BigDecimal("0.1");
IO.println(errado);
IO.println(certo);
0.1000000000000000055511151231257827021181583404541015625
0.1

O new BigDecimal(0.1) não criou o valor um décimo: criou o retrato exato da aproximação binária do double, com todas as casas. O double já chega contaminado, e o BigDecimal apenas fotografa. Em código de dinheiro, BigDecimal nasce de String ou de inteiros, nunca de double; essa regra é curta e a violação passa em qualquer teste que não imprima as casas todas.

static: o membro que pertence ao tipo

Dois capítulos deixaram uma diferença sem explicação: Random exige new Random(), e Math.max se chama direto pelo nome. A resposta é o modificador static. Um membro estático pertence à classe, não a cada instância: existe uma única cópia, acessada pelo nome do tipo, sem new. Math é uma classe cujos métodos são todos estáticos, porque max e abs não dependem de nenhum estado guardado; Random precisa de instância porque cada gerador carrega o próprio estado, a semente em andamento. IO.println e Integer.parseInt seguem o mesmo desenho de Math. Em Produto, o uso típico de static é a constante compartilhada por todas as instâncias:

static final int ESTOQUE_MAXIMO = 10_000;

static final com nome em maiúsculas é a grafia consagrada de constante. A regra prática: método estático para cálculo que não depende de instância; todo o resto, membro de instância. Estado mutável em campo estático é uma única variável compartilhada pelo programa inteiro, e os defeitos disso aparecem no capítulo 22.

A moldura, enfim

O capítulo 2 prometeu explicar as linhas que o arquivo-fonte compacto dispensa. Todas as palavras delas agora têm dono:

import java.math.BigDecimal;

public class Loja {
    public static void main(String[] args) {
        Produto cafe = new Produto("Café 500g", 25);
        IO.println(cafe.nome());
    }
}

public class Loja declara a classe visível a todo o programa, no arquivo Loja.java, com o nome do arquivo amarrado ao da classe pública; é dessa declaração que a extensão .class do bytecode tira o nome. O main com public static é a forma tradicional, esperada por décadas de ferramentas e presente em praticamente todo projeto existente: static dispensa instância para a partida, e public o expõe ao lançador. O Java 25 flexibilizou esse protocolo junto com o arquivo compacto, e formas mais enxutas de main também valem; o livro escreve a tradicional na moldura, porque é a que o leitor vai encontrar. O import declara de onde vem um tipo de fora do arquivo: BigDecimal mora no pacote java.math, e pacote é o espaço de nomes que agrupa classes relacionadas, declarado com package na primeira linha de um arquivo e refletido em pastas homônimas no disco. Os capítulos até o 13 mantêm as classes sem declaração de pacote, para javac *.java e java Loja continuarem diretos; a organização completa em pastas chega com a ferramenta do capítulo 14.

A moldura que o compilador escrevia. No arquivo-fonte compacto, o compilador gera uma classe implícita em volta dos métodos soltos e importa de uma vez os tipos principais da biblioteca padrão; é por isso que IO.println e Random sempre funcionaram sem import. A moldura escrita não acrescenta poder nenhum: torna explícito o que era fornecido, e passa a ser necessária quando o programa tem mais de uma classe pública em arquivos próprios.

Prática

  1. Escreva a classe Produto completa deste capítulo, com nome, estoque e preço em BigDecimal, invariantes no construtor e campos private. Monte na estrutura da última seção, cada classe em seu arquivo, tente violar cada invariante a partir do main e anote o que o compilador recusa e o que o construtor recusa.

  2. Acrescente a Produto um método repor(int quantidade) com a validação que ele merece, e um método valorEmEstoque() que devolva o preço multiplicado pelo estoque, em BigDecimal.

  3. Reproduza a armadilha do new BigDecimal(0.1) e conserte. Depois some 0.10 dez vezes com BigDecimal e imprima, comparando com a soma de 0.1 dez vezes em double do capítulo 3.

  4. Escreva uma classe Caixa com um campo private BigDecimal para o total do dia e um método registrar(Produto produto, int quantidade) que baixa o estoque e acumula o valor da venda. Imprima o total após três vendas.

  5. Refaça o programa de abertura, com os arrays paralelos substituídos por um array de Produto, e escreva em um parágrafo qual classe de erro ficou impossível na nova versão.

  6. Converta o jogo de adivinhação do capítulo 6 para a moldura completa: uma classe pública com main, em arquivo próprio, compilada com javac e executada com java.

Ficha do capítulo

TermoDefinição
classe (class)declaração de um tipo de objeto: os campos e os métodos dele
campodado declarado na classe, presente em cada objeto
instânciaum objeto de uma classe, criado por new
construtormétodo de nome igual ao da classe, executado pelo new; sem tipo de retorno
thisa referência da própria instância
invariantecondição que todo objeto válido do tipo sustenta sempre
throwdispara um erro de execução no ponto do problema
IllegalArgumentExceptionerro padrão para argumento que viola as regras de quem recebe
modificador de acessopublic (visível a todos) e private (só a própria classe)
encapsulamentoesconder a representação e expor operações que mantêm as invariantes
modificador finalcampo que recebe valor uma vez e nunca mais
static / membro estáticomembro que pertence à classe, único, acessado pelo nome do tipo
representação de dinheirocentavos em inteiros, ou BigDecimal; nunca double
BigDecimaldecimal exato e imutável; nasce de String, opera por métodos
pacoteespaço de nomes que agrupa classes; declarado com package
importdeclara de onde vem um tipo usado no arquivo

Herança, polimorfismo e composição

O mercadinho vende café em pacote fechado e queijo fatiado na hora: um tem preço por unidade, o outro por quilo. Com o capítulo 7, a saída natural seria uma segunda classe, ProdutoPorPeso, copiando de Produto o nome, as validações e tudo o mais, e mudando só o cálculo do preço. A cópia funciona no primeiro dia e começa a cobrar no segundo: cada correção em uma classe precisa ser lembrada na outra, e a que for esquecida diverge em silêncio. A linguagem tem um mecanismo para “é igual àquela, exceto em”: a herança.

class Produto {
    private final String nome;
    private final BigDecimal preco;

    Produto(String nome, BigDecimal preco) {
        // validações do capítulo 7
        this.nome = nome;
        this.preco = preco;
    }

    public String nome() {
        return nome;
    }

    public BigDecimal preco() {
        return preco;
    }

    public BigDecimal precoPara(int quantidade) {
        return preco.multiply(new BigDecimal(quantidade));
    }

    public String etiqueta() {
        return nome + ", R$ " + preco;
    }
}

class ProdutoPorPeso extends Produto {
    ProdutoPorPeso(String nome, BigDecimal precoPorQuilo) {
        super(nome, precoPorQuilo);
    }

    @Override
    public String etiqueta() {
        return super.etiqueta() + " o quilo";
    }
}

O estoque do capítulo 7 sai de cena nestes trechos, para cada exemplo mostrar só o que muda; na classe do projeto ele continua onde estava, com as validações.

extends, superclasse e subclasse

Herança é o mecanismo em que uma classe estende outra, recebendo os campos e os métodos dela e podendo acrescentar os seus ou redefinir os herdados. A palavra extends declara a relação: Produto é a superclasse, ProdutoPorPeso é a subclasse. Uma instância de ProdutoPorPeso é um Produto no sentido pleno: carrega os campos da superclasse e responde a todos os métodos dela.

O que se herda tem fronteiras precisas, e três delas evitam surpresa. Primeira: construtores não são herdados. Cada classe declara os seus, e o construtor da subclasse abre com super(...), a chamada ao construtor da superclasse, obrigatória antes de qualquer outra coisa; é por ela que as validações do capítulo 7 continuam protegendo o nascimento, e um ProdutoPorPeso de nome vazio é recusado pela mesma linha que recusa um Produto de nome vazio. Segunda: campo private da superclasse existe dentro do objeto da subclasse, mas continua invisível até para ela; quem o acessa são os métodos da própria superclasse, e a subclasse os usa como qualquer outro código usa. Terceira: a herança pode ser proibida. O modificador final, aplicado a uma classe, veta o extends (String é uma classe final, e é em parte por isso que a imutabilidade dela é uma garantia, não um costume); aplicado a um método, veta a sobrescrita daquele método, deixando o resto da classe aberto. Uma classe que não foi desenhada para ser estendida declara isso com final, e o compilador passa a defender a decisão.

Sobrescrita e @Override

Redefinir na subclasse um método herdado chama-se sobrescrita, e é o que etiqueta() fez: a versão de ProdutoPorPeso substitui a herdada, podendo aproveitá-la por dentro com super.etiqueta(), a chamada da versão da superclasse. A linha @Override em cima do método é uma anotação: uma marca no código, lida pelo compilador e por ferramentas, que não muda o comportamento. Essa anotação específica pede ao compilador que confira se o método de fato sobrescreve algo, e a armadilha abaixo mostra o que acontece sem essa conferência.

O queijo custa R$ 39,80 o quilo, e a subclasse calcula o preço por gramas:

class ProdutoPorPeso extends Produto {
    // construtor como antes

    public BigDecimal precoPara(long gramas) {
        return preco().multiply(new BigDecimal(gramas))
                      .divide(new BigDecimal(1000));
    }
}

void main() {
    Produto queijo = new ProdutoPorPeso("Queijo minas", new BigDecimal("39.80"));
    IO.println(queijo.precoPara(250));
}
9950.00

Duzentos e cinquenta gramas de queijo saíram por nove mil, novecentos e cinquenta reais: o programa cobrou 250 unidades de R$ 39,80.

O método novo recebe long; o herdado recebe int. Assinaturas diferentes não sobrescrevem: convivem, como sobrecarga. Pela variável queijo, de tipo Produto, a versão de long nem é visível, e a chamada vai direto à herdada, que multiplica por unidade; mesmo por uma variável do subtipo, o argumento 250, um int, prefere a versão herdada de int, pela regra do tipo mais estreito do capítulo 4. Nenhum caminho comum chega ao método novo, nenhum erro aparece, e a diferença só existe na conta do cliente. Com @Override escrito sobre o método, o compilador teria recusado na hora, avisando que precoPara(long) não sobrescreve nada. Daí a regra, curta: toda sobrescrita carrega @Override, sempre, porque a anotação transforma esse engano silencioso em erro de compilação.

Polimorfismo e despacho dinâmico

A herança paga o preço da hierarquia quando o código trata tudo pelo tipo da superclasse:

Um carrinho misto:

void main() {
    Produto[] carrinho = {
        new Produto("Café 500g", new BigDecimal("19.90")),
        new ProdutoPorPeso("Queijo minas", new BigDecimal("39.80"))
    };
    for (Produto item : carrinho) {
        IO.println(item.etiqueta());
    }
}

A variável do laço tem tipo Produto nas duas voltas. Qual etiqueta() roda para o queijo?

Café 500g, R$ 19.90
Queijo minas, R$ 39.80 o quilo

A do queijo, com “o quilo” no fim. Isso é polimorfismo: referências do tipo da superclasse apontando para objetos de subtipos variados, com cada chamada de método atendida pela versão do objeto real, não pela do tipo da variável. A escolha acontece durante a execução, objeto a objeto, e tem nome: despacho dinâmico. É o inverso da sobrecarga do capítulo 4: a sobrecarga é resolvida pelo compilador, olhando o tipo escrito do argumento; a sobrescrita é resolvida pela JVM, na hora, olhando o objeto real. Os dois mecanismos convivem na mesma chamada, cada um decidindo a sua metade.

Para o mercadinho, a consequência prática vale ser dita por inteiro. O laço do carrinho foi escrito conhecendo só Produto, e continua correto para subtipos que ainda não existem: no dia em que o mercadinho passar a vender produto com desconto de validade próxima, a classe nova entra, sobrescreve o cálculo, e o caixa, o carrinho e os relatórios somam certo sem uma linha editada. Código que ganha comportamento novo sem ser tocado é o que o polimorfismo compra, e é a diferença entre acrescentar uma classe e caçar todos os if do sistema.

Object, casts e instanceof

Toda classe sem extends estende Object, a superclasse de todas: Produto estende Object sem que ninguém tenha pedido, e a cadeia completa do queijo é Object, depois Produto, depois ProdutoPorPeso. Duas consequências saem desse desenho. A primeira: uma variável do tipo Object aceita referência para qualquer objeto, o que dá à biblioteca padrão um jeito de escrever código que serve para todos os tipos de uma vez; o custo e o conserto desse truque são o assunto do capítulo 16. A segunda: Object declara métodos que toda classe herda, entre eles o equals conhecido do capítulo 5, e o capítulo 10 é inteiro sobre o que esses métodos prometem e como sobrescrevê-los bem.

Guardar um ProdutoPorPeso numa variável Produto, como o carrinho fez, é um upcast: a conversão implícita para o tipo mais geral, sempre segura, porque a subclasse responde a tudo que a superclasse promete. O caminho de volta é o downcast, escrito como o casting do capítulo 3, e ele é uma aposta:

Produto item = carrinho[1];
ProdutoPorPeso porPeso = (ProdutoPorPeso) item;

Se o objeto real não for um ProdutoPorPeso, a linha derruba o programa com um erro de execução chamado ClassCastException, na hora do cast. A pergunta que evita a queda é o instanceof, que responde true quando o objeto real é do tipo perguntado:

if (item instanceof ProdutoPorPeso) {
    ProdutoPorPeso porPeso = (ProdutoPorPeso) item;
    // trato especial
}

Vale registrar o sinal de alerta: um código que enfileira instanceof para tratar cada subtipo de um jeito está refazendo à mão o que o despacho dinâmico faz sozinho, e cada subtipo novo exige lembrar de mais um if. Quando o comportamento varia por tipo, o lugar dele é um método sobrescrito; o capítulo 9 dá a essa ideia a forma definitiva.

Composição e delegação

Nem toda relação entre classes é “é um”. O carrinho de compras do mercadinho tem produtos, e tem-um se modela guardando referências, o que se chama composição:

class Carrinho {
    private final Produto[] itens;
    private final int[] quantidades;
    // construtor e validações omitidos

    public BigDecimal total() {
        BigDecimal soma = BigDecimal.ZERO;
        for (int i = 0; i < itens.length; i++) {
            soma = soma.add(itens[i].precoPara(quantidades[i]));
        }
        return soma;
    }
}

Carrinho não estende coisa nenhuma: contém. E o total() não calcula preço de produto; pede a cada item que calcule o seu, via precoPara, e soma. Encaminhar trabalho para um objeto contido chama-se delegação, e o polimorfismo continua valendo dentro dela: o item por peso responde com a conta por peso. Os dois arrays lado a lado reabrem, de propósito, a dívida da abertura do capítulo 7: aqui dentro, escondidos pelo encapsulamento e mantidos por uma classe só, eles têm um dono; ainda assim são dívida, e o capítulo 11 dá ao par produto-quantidade a forma definitiva. BigDecimal.ZERO, de passagem, é uma constante static final da própria classe, irmã das constantes da seção de static.

A escolha entre herança e composição tem uma regra que envelheceu bem: herança só quando cada subclasse é um caso genuíno da superclasse, no domínio, e o resto é composição. Herança amarra forte: tudo que a superclasse muda, as subclasses sentem. Composição depende só do essencial: o Carrinho conhece apenas as promessas públicas de Produto. Na dúvida entre as duas, composição, e o arrependimento é menor.

Como a JVM despacha. Cada classe carregada tem uma tabela com o endereço da versão de cada método que vale para ela; sobrescrever um método troca a entrada na tabela da subclasse. Uma chamada polimórfica consulta a tabela do objeto real, e é por isso que o despacho custa quase nada e não depende de quantos subtipos existem.

Prática

  1. Implemente Produto e ProdutoPorPeso completos, com o preço por gramas correto na subclasse, @Override em toda sobrescrita e as validações do capítulo 7. Imprima a etiqueta e o preço de 250 gramas de um queijo de R$ 39,80 o quilo.

  2. Reproduza a armadilha da sobrecarga acidental: remova o @Override, troque o tipo do parâmetro e documente a conta errada. Devolva o @Override e anote a mensagem exata do compilador.

  3. Acrescente uma terceira subclasse, ProdutoComDesconto, que sobrescreva precoPara aplicando um percentual de desconto recebido no construtor, com validação do percentual. Ponha os três tipos num mesmo array e some o carrinho sem nenhum instanceof.

  4. Escreva um trecho que provoque ClassCastException de propósito, depois o conserte com instanceof, e por fim explique por escrito por que a versão do exercício 3 é melhor que as duas.

  5. Modele com composição um Combo de café da manhã: um nome próprio e um conjunto de produtos, com total() delegando o preço a cada um. Escreva em um parágrafo por que Combo extends Produto seria uma modelagem pior.

Ficha do capítulo

TermoDefinição
herança (extends)uma classe estende outra, herdando campos e métodos
superclasse / subclassea classe estendida / a classe que estende
superchamada ao construtor ou ao método da superclasse
sobrescritaredefinição, na subclasse, de um método herdado; mesma assinatura
anotaçãomarca no código lida por ferramentas; @Override confere a sobrescrita
polimorfismoreferências do tipo base atendidas pela versão do objeto real
despacho dinâmicoa escolha do método pelo objeto, em execução
Objectsuperclasse de todas as classes
upcastconversão implícita para o tipo mais geral; sempre segura
downcastcast para o subtipo; sujeito a ClassCastException
instanceofpergunta se o objeto real é do tipo dado
ClassCastExceptionerro de execução de um downcast errado
composiçãoobjeto que contém referências a outros (“tem-um”)
delegaçãoencaminhar trabalho ao objeto contido

Interfaces e abstração

O caixa do mercadinho aceita dinheiro, cartão e fiado, e cada forma altera o valor final da compra de um jeito: no dinheiro o mercadinho dá 5% de desconto, no cartão repassa a taxa da máquina, no fiado o valor apenas vai para a caderneta. A herança do capítulo 8 não modela isso bem, e vale tentar para ver a recusa. Cartão não é um caso de dinheiro, então Cartao extends Dinheiro mente sobre o domínio e herda um desconto que não existe no cartão. Uma superclasse Pagamento com o cálculo dentro também não fecha, porque não há cálculo comum para herdar: cada forma tem a própria aritmética e nenhum campo compartilhado. A herança serve quando há implementação comum e parentesco genuíno; aqui não há nem um nem outro. O que os três compartilham é só uma promessa: dado o valor da compra, cada um sabe dizer o valor final. Promessa sem implementação comum tem uma construção própria na linguagem:

interface MeioDePagamento {
    BigDecimal valorFinal(BigDecimal compra);
}

class Dinheiro implements MeioDePagamento {
    @Override
    public BigDecimal valorFinal(BigDecimal compra) {
        return compra.multiply(new BigDecimal("0.95"));
    }
}

class Cartao implements MeioDePagamento {
    @Override
    public BigDecimal valorFinal(BigDecimal compra) {
        return compra.multiply(new BigDecimal("1.02"));
    }
}

interface, implements e o contrato

Uma interface declara um conjunto de métodos que um tipo promete oferecer, sem dizer como: só as assinaturas e os tipos de retorno, sem corpo. Os métodos de uma interface são public por definição, e uma classe adere à promessa com implements, ficando obrigada pelo compilador a dar corpo a cada método declarado; esquecer um é erro de compilação, não descuido possível. Ao contrário do extends, que aceita uma superclasse só, uma classe pode implementar quantas interfaces fizerem sentido: duas promessas de mesma assinatura pedem a mesma coisa, e a ressalva dos métodos com corpo aparece na seção do default. Cada interface é um papel, e um mesmo tipo pode exercer vários: o ProdutoPorPeso do capítulo 8 pode implementar um contrato Pesavel sem deixar de ser um Produto, e cada trecho do sistema o enxerga pelo papel que lhe interessa.

Uma interface também é um tipo, com tudo que isso implica desde o capítulo 3: existe variável do tipo MeioDePagamento, parâmetro, retorno e array desse tipo, e guardar um Dinheiro numa variável assim é um upcast, sempre seguro. A única coisa que não existe é new MeioDePagamento(), porque não há implementação para instanciar; toda referência de tipo interface aponta para um objeto de alguma classe concreta.

A palavra para o que a interface captura é contrato: a promessa observável de um tipo, o que se pode chamar e o que cada chamada garante, separada de qualquer detalhe de como se cumpre. MeioDePagamento é um contrato de uma cláusula: entra o valor da compra, sai o valor final. Dinheiro e Cartao são dois cumpridores com aritméticas próprias, e o resto do sistema não tem por que saber qual é qual.

O contrato obriga os dois lados. Quem chama só pode contar com o que a interface promete, e nada do que souber por fora; quem implementa deve honrar a promessa por inteiro, inclusive as partes que o compilador não confere. O compilador garante a assinatura, mas uma implementação que devolvesse null como valor final estaria tecnicamente compilando e concretamente quebrando o contrato, e derrubaria o caixa com um NullPointerException longe da causa. Contrato bem escrito diz também essas cláusulas de comportamento, nem que seja em comentário sobre a interface, e o capítulo 15 mostra como transformá-las em verificação executável.

Programar contra o contrato

O ganho aparece no caixa:

class Caixa {
    public BigDecimal fechar(BigDecimal compra, MeioDePagamento meio) {
        return meio.valorFinal(compra);
    }
}

O parâmetro é do tipo da interface, e o polimorfismo vale igual para ela: a chamada meio.valorFinal(compra) é despachada para a classe do objeto real. O Caixa compila sem conhecer Dinheiro nem Cartao, e é essa ignorância deliberada que tem nome: acoplamento é o grau em que um trecho de código depende dos detalhes de outro, e programar contra a interface deixa o acoplamento no mínimo necessário. Quando o mercadinho adotar Pix, a classe nova implementa MeioDePagamento e o Caixa a atende sem uma linha editada, no mesmo espírito do carrinho de compras.

O caminho oposto é o sinal de alerta registrado no capítulo 8, e vale ver o custo dele por extenso:

Um caixa escrito antes das interfaces, decidindo por tipo:

public BigDecimal fechar(BigDecimal compra, Object meio) {
    if (meio instanceof Cartao) {
        return compra.multiply(new BigDecimal("1.02"));
    }
    return compra.multiply(new BigDecimal("0.95"));
}

O mercadinho adota Pix e alguém escreve a classe Pix. O programa compila e todas as vendas seguem passando. O que acontece com uma compra de R$ 100,00 paga em Pix?

Sai por R$ 95,00. O Pix não casa com o instanceof de Cartao, escorrega para o ramo final, e leva o desconto que era do dinheiro. Nenhum erro em nenhum momento: o prejuízo de 5% em cada venda no Pix aparece no fechamento do mês, longe da causa. A cascata de tipos exige que cada tipo novo lembre de se apresentar em cada cascata do sistema; o contrato inverte a obrigação, porque quem chega já traz o próprio comportamento. É a mesma lição do polimorfismo, agora sem exigir parentesco de classe nenhum. O fiado, terceiro meio da abertura, é o cumpridor mais simples do contrato, devolvendo a própria compra, e o registro na caderneta, com o limite dele, é assunto que o capítulo 13 retoma:

class Fiado implements MeioDePagamento {
    @Override
    public BigDecimal valorFinal(BigDecimal compra) {
        return compra;
    }
}

Método default

Contrato publicado é compromisso: no dia em que MeioDePagamento ganhar um método novo, toda classe que o implementa quebra de uma vez, com um erro de compilação por implementador. Para evoluir sem quebrar, uma interface pode dar corpo padrão a um método, com a palavra default:

interface MeioDePagamento {
    BigDecimal valorFinal(BigDecimal compra);

    default String nomeNoRecibo() {
        return "Pagamento";
    }
}

class Pix implements MeioDePagamento {
    @Override
    public BigDecimal valorFinal(BigDecimal compra) {
        return compra;
    }

    @Override
    public String nomeNoRecibo() {
        return "Pix";
    }
}

Dinheiro e Cartao continuam como estavam, sem saber que nomeNoRecibo existe. O trecho abaixo roda:

MeioDePagamento[] meios = { new Pix(), new Dinheiro() };
for (MeioDePagamento meio : meios) {
    IO.println(meio.nomeNoRecibo());
}

O que aparece para cada um dos dois?

Pix
Pagamento

Quem sobrescreveu responde com a própria versão; quem não sobrescreveu responde com o corpo default, e continua compilando como antes da mudança. Com corpo na interface, o conflito volta a ser possível: uma classe que implemente duas interfaces com o mesmo método default é obrigada pelo compilador a sobrescrever e decidir. O método default existe para evolução de contrato publicado, e não para virar depósito de lógica: interface que acumula corpos está fazendo o trabalho da construção da próxima seção.

Classe abstrata

Entre a interface, que não carrega implementação de estado nenhuma, e a classe concreta, que carrega tudo, existe o meio-termo. Os cartões do mercadinho, crédito e débito, têm taxas diferentes, mas o cálculo é o mesmo e o campo da taxa também; só a descrição de cada um varia. Uma classe abstrata guarda esse miolo comum:

abstract class CartaoBase implements MeioDePagamento {
    private final BigDecimal taxa;

    CartaoBase(BigDecimal taxa) {
        this.taxa = taxa;
    }

    @Override
    public final BigDecimal valorFinal(BigDecimal compra) {
        return compra.multiply(BigDecimal.ONE.add(taxa));
    }

    public abstract String bandeira();
}

class CartaoDeCredito extends CartaoBase {
    CartaoDeCredito() {
        super(new BigDecimal("0.02"));
    }

    @Override
    public String bandeira() {
        return "crédito";
    }
}

Classe abstrata é a classe que não pode ser instanciada: new CartaoBase() é erro de compilação, porque ela existe para ser estendida. Ela pode ter tudo que classe tem, campos, construtores e métodos concretos, e mais uma coisa que classe concreta não pode: o método abstrato, declarado sem corpo, como bandeira(), que obriga cada subclasse a fornecer a própria versão. BigDecimal.ONE, de passagem, é a constante 1 da própria classe, irmã do ZERO do capítulo 8, e o final em valorFinal usa a regra do capítulo 8 para congelar o cálculo que as subclasses não devem alterar.

A régua para escolher entre as três construções cabe em três linhas. Interface quando o que existe em comum é só a promessa: é o caso de MeioDePagamento, e é por onde se começa. Classe abstrata quando implementações irmãs compartilham código e estado de verdade, como os cartões. Classe concreta para o que se instancia. Na prática as três convivem: a interface no topo, uma abstrata opcional no meio onde a duplicação doeu, concretas embaixo.

Por que o default existe. Interfaces não aceitavam corpo nenhum nas primeiras versões de Java, e evoluir as interfaces centrais da biblioteca padrão era impossível sem quebrar o mundo inteiro. O método default entrou na versão 8 exatamente para isso, e boa parte dos métodos que os capítulos 17 a 19 usam chegou por essa porta, em interfaces que existiam desde os anos noventa.

Prática

  1. Implemente MeioDePagamento com Dinheiro, Cartao e Pix, o Caixa programado contra a interface e uma volta de impressões que feche a mesma compra pelos três meios, exibindo nome do recibo e valor final. A impressão sai com casas de sobra, porque multiply soma as casas dos dois lados; registre o fato e siga, que o conserto, setScale, aparece no capítulo 11.

  2. Reproduza a armadilha da cascata: escreva a versão com instanceof, acrescente o Pix sem tocar nela e documente o valor errado. Depois escreva em um parágrafo onde exatamente a versão com interface elimina a categoria do erro, e não só o caso.

  3. Acrescente CartaoDeDebito com taxa de 1% à hierarquia de CartaoBase, sem duplicar o cálculo. Explique por escrito por que valorFinal é final na base e o que uma subclasse ganharia ou quebraria se pudesse sobrescrevê-lo.

  4. Declare um método novo em MeioDePagamento sem corpo default, anote quantos erros de compilação aparecem e onde; depois transforme-o em default e observe o que muda. Registre a regra que você tirou disso.

  5. Modele com interface um segundo contrato do mercadinho: Pesavel, com um método que devolve o peso em gramas, implementado por ProdutoPorPeso e por uma nova classe CestaBasica. Escreva um método que receba um array de Pesavel e some o peso, sem instanceof.

Ficha do capítulo

TermoDefinição
interfaceconjunto de métodos que um tipo promete, sem implementação
implementsadesão de uma classe a uma interface; corpo obrigatório para cada método
contratoa promessa observável de um tipo, separada do como
acoplamentograu em que um trecho depende dos detalhes de outro
método defaultmétodo de interface com corpo padrão; permite evoluir contrato publicado
classe abstrataclasse não instanciável, feita para ser estendida; aceita campos e código
método abstratométodo sem corpo em classe abstrata; sobrescrita obrigatória

O contrato equals/hashCode/toString

O mercadinho recebe a segunda remessa de café da semana, e o sistema cria o objeto do produto de novo, a partir da nota do fornecedor:

void main() {
    Produto primeira = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"));
    Produto segunda = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"));
    IO.println(primeira.equals(segunda));
}
false

Mesmo código de barras, mesmo nome, mesmo preço, e equals responde false. Para o estoque, isso significa café duplicado no relatório: a busca que pergunta “esse produto já existe?” com equals nunca encontra o que procura, porque cada remessa cria um objeto novo. O capítulo 5 avisou que equals compara conteúdo, e a frase era verdadeira para String; para as classes nossas, ainda não, e este capítulo explica o porquê e o conserto.

O equals que rodou ali é o herdado de Object, e a versão de Object compara identidade, como o ==: só responde true para o mesmíssimo objeto. String responde por conteúdo porque a classe String sobrescreve equals; Produto ainda não sobrescreveu, e igualdade por conteúdo é uma decisão que cada classe toma por si. Tomá-la direito exige conhecer as cláusulas.

O contrato de equals

A documentação de Object define o que toda sobrescrita de equals deve cumprir, e o conjunto se chama contrato de equals, contrato no sentido exato do capítulo 9: promessas que quem chama pode assumir e quem implementa deve honrar. São cinco cláusulas, três delas com nome de propriedade matemática.

Reflexividade: todo objeto é igual a si mesmo, x.equals(x) responde true. Simetria: a resposta não depende da ordem, x.equals(y) e y.equals(x) respondem o mesmo. Transitividade: se x é igual a y e y é igual a z, então x é igual a z. Consistência: enquanto os objetos não mudam, a resposta não muda entre chamadas. E a quinta: comparação com null responde false, sem derrubar o programa.

Nenhuma dessas cláusulas é conferida pelo compilador, e essa é a característica que muda o jogo: o compilador confere a assinatura e mais nada, e todo o resto do sistema, da busca do estoque às estruturas do capítulo 17, assume as cláusulas como verdadeiras sem perguntar. Um equals que viola o contrato não produz erro em lugar nenhum; produz comportamento errado nos lugares que confiaram nele, que é a definição de bug difícil.

As cláusulas parecem óbvias até a primeira implementação torta violar uma. A simetria, por exemplo, quebra quando Produto aceita se comparar com String de código de barras: produto.equals("7891...") diria true, e "7891...".equals(produto) diria false, porque String não conhece Produto. Buscas que funcionam numa direção e falham na outra são o sintoma clássico, e a raiz é sempre uma cláusula violada.

Para o mercadinho, a decisão de domínio vem antes do código: dois produtos são o mesmo produto quando têm o mesmo código de barras. Nome e preço podem divergir entre remessas, o código não. A implementação canônica:

public class Produto {
    private final String codigoDeBarras;
    private final String nome;
    private final BigDecimal preco;

    // construtor com as validações de sempre

    @Override
    public boolean equals(Object outro) {
        if (this == outro) {
            return true;
        }
        if (!(outro instanceof Produto)) {
            return false;
        }
        Produto produto = (Produto) outro;
        return codigoDeBarras.equals(produto.codigoDeBarras);
    }
}

A primeira conferência resolve o caso comum, mesmo objeto, sem trabalho. O instanceof cobre o null e qualquer tipo estranho de uma vez, porque null instanceof Produto responde false sem erro. O downcast vem protegido pela pergunta anterior, e a última linha delega a comparação ao equals de String, que já cumpre o contrato. Cada linha dessa forma tem função, e a assinatura tem uma exigência que a armadilha abaixo torna inesquecível: o parâmetro é Object, não Produto.

Uma versão que parece mais limpa, com o tipo certo no parâmetro:

public boolean equals(Produto outro) {
    return codigoDeBarras.equals(outro.codigoDeBarras);
}

void main() {
    Produto primeira = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"));
    Produto segunda = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"));
    Object registro = primeira;
    IO.println(primeira.equals(segunda));
    IO.println(registro.equals(segunda));
}
true
false

A chamada direta respondeu true; a mesma pergunta, feita através de uma variável Object, respondeu false.

equals(Produto) não sobrescreve equals(Object): assinatura diferente é sobrecarga, o mesmo acidente do capítulo 8, e as duas versões passam a conviver. A chamada através da variável Object resolve para o equals(Object) herdado, o de identidade, e responde false. O defeito é intermitente: nos testes diretos, com variáveis Produto, tudo funciona; dentro das estruturas do capítulo 17, que guardam tudo por referências genéricas, a igualdade some. @Override pega o engano na hora, porque equals(Produto) não sobrescreve nada, e é mais uma vez a diferença entre bug silencioso e erro de compilação.

hashCode e o código de hash

equals obriga a sobrescrever um segundo método de Object. O código de hash de um objeto é um int que o acompanha, devolvido pelo método hashCode: as sobrescritas corretas o derivam dos mesmos campos que o equals compara, e o herdado de Object deriva da identidade, não do conteúdo. A cláusula que amarra os dois métodos é curta: objetos iguais segundo equals devem ter o mesmo código de hash. O contrário não é exigido: objetos diferentes podem partilhar um hash, o que se chama colisão de hash, e colisões são inevitáveis, porque há mais conteúdos possíveis do que valores de int.

O propósito do hash é velocidade de busca: estruturas que o capítulo 17 apresenta usam o código de hash para saltar direto à vizinhança do objeto, em vez de comparar com todos, e só usam equals para o desempate final entre vizinhos. Quem sobrescreve equals sem sobrescrever hashCode deixa os dois em desacordo: os cafés iguais das duas remessas ficam com hashes herdados diferentes, a estrutura procura cada um na vizinhança do próprio hash, e o produto guardado nunca é encontrado, sem erro nenhum. O capítulo 17 reproduz esse desaparecimento ao vivo; aqui fica a regra que o evita: sobrescreveu um, sobrescreve o outro, derivando o hash dos mesmos campos que o equals compara.

@Override
public int hashCode() {
    return codigoDeBarras.hashCode();
}

String já sabe calcular o próprio hash, e delegar a ele cumpre a cláusula: mesmo código de barras, mesmo hash, sempre. Quando a igualdade olha mais de um campo, o utilitário Objects.hash(campo1, campo2), do pacote java.util, combina os hashes de todos numa chamada, e a regra continua a mesma: os campos do hashCode são exatamente os campos do equals. Um hash também pode ser ruim sem estar errado: devolver sempre 42 cumpre a cláusula à risca, porque iguais têm o mesmo hash, e destrói a velocidade, porque tudo colide com tudo e a busca degenera em comparar com todos. Correção é obrigação; espalhamento é qualidade, e delegar aos hashes dos próprios campos costuma entregar os dois.

toString

O terceiro método de Object responde pela forma em texto do objeto:

IO.println(new Produto("7891000100103", "Café 500g", new BigDecimal("19.90")));
Produto@6f2b958e

Sem sobrescrita, toString devolve o nome da classe, um arroba e um número em hexadecimal derivado da identidade; o número muda de objeto para objeto e de execução para execução, e a saída acima é a de uma execução, não a de todas. No arquivo-fonte compacto ainda sai o nome qualificado pela classe implícita do arquivo, como Caixa$Produto@.... Serve para distinguir objetos e para nada mais. Sobrescrever muda a impressão inteira, porque IO.println chama toString de qualquer objeto que recebe, e essa é a resposta da promessa sobre o StringBuilder impresso direto no capítulo 5:

@Override
public String toString() {
    return "Produto[" + codigoDeBarras + ", " + nome + ", R$ " + preco + "]";
}

A regra de uso: toString existe para depuração e registro, para gente lendo saída de programa, e não para dado que outro código interprete. Um formato de verdade, recibo ou relatório, é método próprio com nome próprio; o toString fica livre para mudar sem quebrar ninguém. O retorno do investimento vem na primeira sessão de caça a defeito: a mensagem de uma IllegalArgumentException que concatena o produto, ou um IO.println(produto) jogado no meio do caixa para inspecionar, imprime Produto[7891000100103, Café 500g, R$ 19.90] em vez de um arroba com hexadecimal, e a diferença entre os dois é a diferença entre ler o problema e caçá-lo. Vale sobrescrever toString em toda classe de domínio, mesmo quando nenhum requisito pede.

O caso BigDecimal

O mesmo preço, escrito com e sem o zero final:

BigDecimal a = new BigDecimal("2.50");
BigDecimal b = new BigDecimal("2.5");
IO.println(a.equals(b));
IO.println(a.compareTo(b));

Duas linhas de saída. Quais?

false
0

Para equals, os dois são diferentes; para compareTo, o método de comparação do capítulo 7, valem o mesmo. BigDecimal guarda o valor e a quantidade de casas, e o equals dele compara as duas coisas: 2.50 tem duas casas, 2.5 tem uma, e a igualdade estrita recusa. É uma decisão documentada da classe, não um defeito, e a consequência prática cabe numa regra: igualdade de dinheiro se pergunta com compareTo(...) == 0, nunca com equals. Levar essa regra para dentro de um equals nosso exige cuidado com a cláusula do hash: um equals que iguala 2.5 a 2.50 precisa de um hashCode que também os iguale, e o caminho seguro é normalizar a escala do campo na entrada, fixando as casas com setScale, para equals e hash enxergarem o mesmo valor. O sintoma de esquecer é o de sempre nesta família: valores que são o mesmo preço na etiqueta e objetos que se dizem diferentes no código.

Hash e mutação. O código de hash é calculado a partir dos campos; se um campo que participa do hash mudar depois que o objeto entrou numa estrutura de busca, o objeto fica arquivado na vizinhança do hash antigo e passa a ser procurado na do novo, sumindo sem sair do lugar. É um dos motivos de os campos de identidade, como o código de barras, serem final, e de os tipos feitos para chave serem imutáveis por inteiro.

Prática

  1. Complete o Produto deste capítulo com equals, hashCode e toString sobrescritos, e reproduza a abertura obtendo true para as duas remessas.

  2. Reproduza a armadilha da sobrecarga: mantenha equals(Produto), mostre o par de saídas divergentes através de Produto e de Object, e depois anote a mensagem do compilador ao pôr @Override na versão errada.

  3. Quebre a simetria de propósito: faça Produto.equals aceitar String de código de barras e demonstre x.equals(y) diferente de y.equals(x). Desfaça e escreva em uma frase qual cláusula estava violada.

  4. Sobrescreva equals sem hashCode, imprima os hashes das duas remessas iguais e explique por escrito qual cláusula do contrato ficou violada e onde isso vai doer no capítulo 17.

  5. Escreva um método mesmoPreco(Produto outro) que compare os preços com compareTo, e demonstre um par de produtos com equals de preço false e mesmoPreco true.

Ficha do capítulo

TermoDefinição
contrato de equalsas cláusulas que toda sobrescrita de equals deve cumprir
reflexividadex.equals(x) é true
simetriax.equals(y) e y.equals(x) respondem o mesmo
transitividadeiguais a um mesmo terceiro são iguais entre si
hashCodedevolve o código de hash; iguais por equals têm o mesmo hash
código de hashint que acompanha o objeto; sobrescrito, deriva dos campos do equals
colisão de hashobjetos diferentes com o mesmo hash; permitida e inevitável
toStringa forma em texto do objeto; usada por IO.println; para depuração
Regra prática
parâmetro do equalssempre Object, com @Override
equals e hashCodesobrescreve um, sobrescreve o outro, sobre os mesmos campos
dinheiroigualdade com compareTo(...) == 0; campo BigDecimal em equals pede escala normalizada

Records e semântica de valor

O código de barras merece deixar de ser um String solto: como texto, ele aceita qualquer conteúdo, e a validação de treze dígitos fica espalhada por quem lembrar dela. A ferramenta dos capítulos 7 e 10 resolve, ao preço de um ritual completo:

public class CodigoDeBarras {
    private final String digitos;

    public CodigoDeBarras(String digitos) {
        if (digitos == null || !digitos.matches("\\d{13}")) {
            throw new IllegalArgumentException("Código de barras inválido: " + digitos);
        }
        this.digitos = digitos;
    }

    public String digitos() {
        return digitos;
    }

    @Override
    public boolean equals(Object outro) {
        if (this == outro) {
            return true;
        }
        if (!(outro instanceof CodigoDeBarras)) {
            return false;
        }
        CodigoDeBarras codigo = (CodigoDeBarras) outro;
        return digitos.equals(codigo.digitos);
    }

    @Override
    public int hashCode() {
        return digitos.hashCode();
    }

    @Override
    public String toString() {
        return "CodigoDeBarras[" + digitos + "]";
    }
}

Trinta e seis linhas, todas corretas, todas já justificadas por este livro, e todas dizendo uma única ideia: um código de barras é treze dígitos, e dois códigos com os mesmos dígitos são o mesmo código. O mercadinho vai precisar do mesmo ritual para item de venda, para valor em dinheiro, para endereço de entrega, e cada repetição manual é uma chance nova de errar como a armadilha da sobrecarga mostrou: um parâmetro Produto onde devia ser Object, um campo esquecido no hashCode, um toString desatualizado depois que um campo entrou. Ritual previsível que se repete e dá margem a erro é trabalho de compilador, e a linguagem o absorveu. O matches, novidade pontual, pergunta se o texto casa com um padrão; o \\d{13} descreve “treze dígitos”, e a notação completa desses padrões fica fora deste livro, com o essencial registrado na ficha.

record

public record CodigoDeBarras(String digitos) { }

Uma linha. A palavra record declara um tipo cuja definição é a lista entre parênteses, os componentes do record, e o compilador gera o resto do ritual: um campo private final por componente; o construtor canônico, que recebe todos os componentes na ordem declarada; um método de acesso por componente, com o mesmo nome dele, digitos(); e equals, hashCode e toString sobre todos os componentes, este último no formato Nome[componente=valor], cumprindo o contrato de equals por inteiro. Um record é imutável por construção: não existe forma de alterar um componente depois do new, e a imutabilidade deixa de ser disciplina para ser garantia.

O ganho de leitura vale tanto quanto o de digitação. A declaração de um record é a forma dos dados, inteira, numa linha: quem abre o arquivo sabe que não há estado escondido, que não há mutação possível, que a igualdade é por conteúdo e cobre todos os componentes. Numa classe comum, cada uma dessas propriedades exige ler a classe inteira para confirmar; no record, elas são consequência da palavra-chave, e a leitura do tipo se resume à leitura da lista.

A abertura do capítulo 10, refeita com records:

void main() {
    CodigoDeBarras primeira = new CodigoDeBarras("7891000100103");
    CodigoDeBarras segunda = new CodigoDeBarras("7891000100103");
    IO.println(primeira.equals(segunda));
    IO.println(primeira);
}

O que imprimem as duas linhas, sem nenhum método escrito à mão?

true
CodigoDeBarras[digitos=7891000100103]

Igualdade por conteúdo e impressão legível, de fábrica, porque o compilador gerou as sobrescritas que antes se escreviam à mão. Saber escrevê-las à mão continua sendo o que separa usar o record de entendê-lo: o record não muda as regras do contrato, muda quem digita.

Construtor compacto

A linha única perdeu a validação dos treze dígitos, e recuperá-la não exige voltar ao ritual. O construtor compacto é um corpo de construtor escrito sem repetir a lista de parâmetros, que roda antes de os componentes serem atribuídos:

public record CodigoDeBarras(String digitos) {
    public CodigoDeBarras {
        if (digitos == null || !digitos.matches("\\d{13}")) {
            throw new IllegalArgumentException("Código de barras inválido: " + digitos);
        }
    }
}

Sem parênteses depois do nome: essa é a grafia do compacto. Dentro dele, os nomes dos componentes são os parâmetros recebidos, e o que o corpo pode fazer é validar, como acima, ou normalizar, reatribuindo ao parâmetro (digitos = digitos.strip(), com strip removendo espaços das pontas) antes de a atribuição automática acontecer. As invariantes do capítulo 7 voltam inteiras, com uma linha de cerimônia a menos, e valem para todo caminho de criação, porque todo caminho passa pelo canônico.

Um record também aceita construtores adicionais, com outras assinaturas, desde que cada um comece delegando ao canônico com this(...). É a porta para conveniências como aceitar o código de barras com espaços de nota fiscal, ou um item de venda com quantidade implícita de um:

public ItemDeVenda(Produto produto) {
    this(produto, 1);
}

A delegação obrigatória garante que a validação do compacto rode sempre, venha o objeto do caminho que vier; não existe caminho de criação que contorne a validação do canônico.

Semântica de valor

O nome do que o record materializa é semântica de valor: um tipo tem semântica de valor quando seus exemplares valem pelo conteúdo, e dois exemplares de mesmo conteúdo são intercambiáveis em qualquer uso. É o regime dos primitivos, o 7 de uma conta é o 7 de outra, e o oposto do regime de identidade, em que cada objeto é ele mesmo. O ritual da abertura constrói semântica de valor à mão, sobrescrevendo o contrato; o record a declara de nascença.

A régua para o mercadinho separa os tipos em duas famílias. Têm semântica de valor os que descrevem: código de barras, um item de venda com produto e quantidade, um valor em dinheiro. Têm identidade os que vivem: o Produto com estoque que sobe e desce continua classe, porque duas remessas do mesmo café são o mesmo produto com histórias diferentes, e estado mutável não cabe em record. A regra prática: dado imutável que vale pelo conteúdo vira record; entidade com ciclo de vida vira classe. Na dúvida, a pergunta é “faz sentido trocar um pelo outro de mesmo conteúdo?”, e a resposta sim aponta o record. No código profissional, os records se concentraram exatamente nesses papéis: retratos de dados que atravessam fronteiras, a linha de um relatório, o resultado de uma consulta, a resposta que um sistema envia a outro, todos dados que nascem prontos, viajam e não mudam no caminho. O capítulo 12 apresenta a forma de abrir um record em padrões, componente a componente.

Records participam do resto da linguagem sem regalias: aceitam métodos próprios, membros static e implements de interface; o que não aceitam é extends, nem para estender nem para serem estendidos, porque herança de estado e valor imutável não se misturam bem, e campos de instância fora da lista de componentes também não existem. Um item de venda mostra o formato típico, componente mais método derivado:

public record ItemDeVenda(Produto produto, int quantidade) {
    public ItemDeVenda {
        if (quantidade <= 0) {
            throw new IllegalArgumentException("Quantidade inválida: " + quantidade);
        }
    }

    public BigDecimal subtotal() {
        return produto.precoPara(quantidade);
    }
}

Um record para as pesagens do dia da balança do queijo:

public record PesagensDoDia(int[] gramas) { }

void main() {
    PesagensDoDia manha = new PesagensDoDia(new int[] { 250, 400 });
    PesagensDoDia copia = new PesagensDoDia(new int[] { 250, 400 });
    IO.println(manha.equals(copia));
    IO.println(manha);
}
false
PesagensDoDia[gramas=[I@76ed5528]

O mesmo conteúdo deu false, e a impressão saiu ilegível.

O equals gerado compara cada componente com o equals do componente, e o equals de array é o herdado de Object, identidade, pelo capítulo 5: dois arrays de mesmo conteúdo são objetos diferentes. O toString do array é o herdado também, daí o [I@ com hexadecimal. E há um furo pior: o array é mutável, e alguém com a referência pode alterar as pesagens por fora, minando a promessa de valor imutável. A regra que fecha as três frestas de uma vez: componente de record deve ser imutável e ter igualdade por conteúdo, como String, BigDecimal, primitivos e outros records. Sequência de valores dentro de record espera o tipo certo de recipiente, que o capítulo 17 apresenta.

Duas notas fecham a regra do componente. ItemDeVenda carrega um Produto, entidade mutável, sem violar o que importa: a igualdade de Produto se apoia no código de barras final, e o equals e o hash do item não se movem quando o estoque muda. Componente pode ser entidade cuja igualdade é estável; o que não pode é igualdade instável ou por identidade. E componente BigDecimal herda a igualdade estrita de escala do capítulo 10: dois records com 95.0 e 95.00 no mesmo componente não se igualam. Dinheiro que participa da igualdade de um record pede escala fixada na entrada, no construtor compacto, com setScale(2), o método de BigDecimal que fixa a quantidade de casas.

Acessor sem get. O record consolidou a grafia digitos() para leitura de componente, mas quase todo código anterior a ele segue a convenção getDigitos(), dos tempos em que ferramentas descobriam propriedades pelo prefixo. As duas grafias convivem no mundo real e dizem a mesma coisa; o apêndice de legado volta ao assunto.

Prática

  1. Escreva o record CodigoDeBarras com validação e normalização no construtor compacto, e prove com impressões: igualdade por conteúdo, recusa de código curto, espaços removidos das pontas.

  2. Converta Produto para carregar um CodigoDeBarras em vez de String, delegando equals e hashCode ao record, e refaça a abertura do capítulo 10 com a versão nova.

  3. Escreva ItemDeVenda completo e um main que monte três itens, imprima cada um e some os subtotais com BigDecimal. Compare a legibilidade da impressão gerada com a que você escreveria à mão.

  4. Reproduza a armadilha do componente array e conserte da forma primitiva disponível: guarde as pesagens como String no formato “250;400” e converta ao ler. Anote o que essa gambiarra custa e o que o capítulo 17 promete no lugar dela.

  5. Decida, para cada tipo do mercadinho até aqui, record ou classe: Produto, CodigoDeBarras, ItemDeVenda, Carrinho, Caixa, MeioDePagamento e implementações. Justifique cada decisão em uma linha, pela régua da semântica de valor.

Ficha do capítulo

TermoDefinição
recordtipo declarado pela lista de componentes; imutável, com o ritual gerado
componente de recordcada item da lista; vira campo final, acessor e parte do equals
construtor canônicoo construtor com todos os componentes, na ordem declarada
construtor compactocorpo sem lista de parâmetros, para validar e normalizar antes da atribuição
semântica de valorexemplares valem pelo conteúdo; iguais são intercambiáveis
Regra prática
record × classedado imutável que vale pelo conteúdo × entidade com ciclo de vida
componenteimutável e com igualdade por conteúdo; array não
\d{13} em matchespadrão de texto “treze dígitos”; a notação completa fica fora do livro

Sealed types, enums e pattern matching

A categoria de cada produto do mercadinho decide o corredor da prateleira e o relatório em que a venda entra, e a primeira versão a guarda como texto:

Produto cafe = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"), "mercearia");

if (cafe.categoria().equals("Mercearia")) {
    IO.println("Corredor 3");
}

O if nunca acha o corredor, porque a categoria foi gravada com minúscula e comparada com maiúscula. String aceita qualquer conteúdo: “mercearia”, “Mercearia”, “merceária” e um erro de digitação são quatro valores diferentes e todos válidos, e o compilador não tem como saber que o domínio só reconhece meia dúzia de categorias. O problema é de tipo: categoria não é texto livre, é uma escolha numa lista fechada, e a linguagem tem uma declaração exatamente para isso.

enum: a lista fechada de valores

public enum Categoria {
    MERCEARIA, HORTIFRUTI, LIMPEZA, BEBIDAS;
}

Um enum é um tipo cujos valores possíveis são declarados um a um, na própria definição, e acabou: não existe quinto valor de Categoria, nem errado de digitação, nem fora da lista. Cada nome declarado é uma constante de enum, um objeto único daquele tipo, criado pela JVM; Categoria.MERCEARIA é o mesmo objeto em qualquer ponto do programa, e por isso enum se compara com == sem o risco do capítulo 5: identidade e conteúdo coincidem quando cada valor existe uma vez só. O campo do produto vira private final Categoria categoria, o construtor recebe o tipo em vez de texto, e o erro da abertura morre na compilação: new Produto(..., "mercearia") nem compila, porque String não é Categoria.

O pagamento prometido no capítulo 4 vem no switch. Com um int na entrada, o default era obrigatório, porque o compilador não conhecia os valores possíveis; com um enum, ele conhece todos:

public int corredor(Categoria categoria) {
    return switch (categoria) {
        case MERCEARIA -> 3;
        case HORTIFRUTI -> 1;
        case LIMPEZA -> 4;
        case BEBIDAS -> 2;
    };
}

Sem default, e compila. Essa é a exaustividade: a garantia, conferida pelo compilador, de que o switch cobre todos os casos possíveis do valor. Ela parece detalhe até a lista crescer, e a armadilha abaixo mostra os dois lados.

Um switch de enum escrito com default, por hábito:

public int corredor(Categoria categoria) {
    return switch (categoria) {
        case MERCEARIA -> 3;
        case HORTIFRUTI -> 1;
        default -> 4;
    };
}

Meses depois, o mercadinho ganha padaria, e PADARIA entra no enum. O programa inteiro recompila sem um aviso. Onde o pão vai parar?

No corredor 4, junto com a limpeza. O default engoliu o caso novo: para o compilador, PADARIA está coberta, e a exaustividade que ele conferiria foi desligada por quem escreveu default. Sem o default, a recompilação falharia em cada switch que esqueceu a padaria, com a lista exata dos lugares a atualizar, que é o comportamento desejável quando o domínio cresce. A regra que sai daí: switch sobre enum não leva default; cobrir todos os casos por extenso é o que mantém o compilador de guarda.

Constantes de enum aceitam dados e comportamento próprios, porque enum é uma classe com nascimento controlado. O corredor pertence mais à categoria do que ao método solto acima:

public enum Categoria {
    MERCEARIA(3), HORTIFRUTI(1), LIMPEZA(4), BEBIDAS(2);

    private final int corredor;

    Categoria(int corredor) {
        this.corredor = corredor;
    }

    public int corredor() {
        return corredor;
    }
}

O construtor de enum roda uma vez por constante, na carga do tipo, e não é chamável com new: as constantes só nascem na lista do topo. Campos e métodos seguem as regras normais dos capítulos 7 em diante.

sealed: hierarquia fechada

O enum fecha uma lista de valores; falta fechar uma lista de tipos. O caixa do capítulo 9 devolve o resultado de um pagamento, e resultado não é um valor único: aprovado carrega o valor cobrado, recusado carrega um motivo. São tipos diferentes com dados diferentes, e a hierarquia aberta do capítulo 8 deixaria qualquer um criar um resultado novo que o resto do sistema não trata. A palavra sealed fecha a hierarquia:

public sealed interface Resultado permits Aprovado, Recusado { }

public record Aprovado(BigDecimal valorCobrado) implements Resultado { }

public record Recusado(String motivo) implements Resultado { }

Uma interface ou classe sealed declara em permits a lista completa de quem pode implementá-la ou estendê-la; qualquer tipo fora da lista é erro de compilação. Cada tipo permitido declara o próprio destino: records e classes final encerram o ramo; um tipo permitido pode seguir fechado, declarando-se sealed com a própria lista; e non-sealed reabre o ramo para herança livre, o que é raro e deliberado. O casamento com o capítulo 11 é natural: os ramos de uma hierarquia selada costumam ser records, dados imutáveis com o contrato pronto, e o conjunto descreve “um resultado é isto ou aquilo, com estes dados em cada caso”.

Pattern matching

Falta consumir o resultado, e é aqui que entra o recurso que amarra o capítulo. Pattern matching (casamento de padrões) é comparar um valor contra um padrão que, ao casar, já extrai as partes. A forma mais simples é o padrão de tipo, que aposenta o par instanceof-e-cast do capítulo 8:

if (resultado instanceof Recusado recusado) {
    IO.println("Recusado: " + recusado.motivo());
}

O instanceof com uma variável ao lado pergunta e converte num passo só: casando, recusado nasce já com o tipo certo, no escopo do if, e o downcast manual desaparece junto com o risco de ClassCastException. Sobre uma hierarquia selada, o switch com padrões de tipo ganha a exaustividade do enum:

public String linhaDoRecibo(Resultado resultado) {
    return switch (resultado) {
        case Aprovado aprovado -> "Pago: R$ " + aprovado.valorCobrado();
        case Recusado recusado -> "Recusado: " + recusado.motivo();
    };
}

Sem default, porque permits disse ao compilador que a lista acabou; a mesma regra da armadilha do enum vale inteira aqui. E quando o ramo é um record, o padrão de registro (record pattern) desestrutura os componentes na própria cláusula:

return switch (resultado) {
    case Aprovado(BigDecimal valor) -> "Pago: R$ " + valor;
    case Recusado(String motivo) -> "Recusado: " + motivo;
};

Aprovado(BigDecimal valor) casa com um Aprovado e já entrega o componente na variável valor, sem acessor e sem variável intermediária: é a resposta à promessa deixada no capítulo 11.

O mercadinho passa a aceitar estorno, e Estornado entra na hierarquia:

public sealed interface Resultado permits Aprovado, Recusado, Estornado { }

public record Estornado(BigDecimal valorDevolvido) implements Resultado { }

Nenhum switch do sistema foi tocado. O que acontece na recompilação?

Cada switch sobre Resultado sem o caso novo vira um erro de compilação, com a mensagem the switch expression does not cover all possible input values; quem aponta os lugares a atualizar é a lista dos próprios erros, cada um com arquivo e linha. É a mesma mecânica da armadilha do enum, agora a favor: o compilador entrega a lista completa dos pontos do sistema que precisam aprender o que fazer com um estorno, e nada compila até todos decidirem. A dupla sealed e switch exaustivo transforma “esquecemos de tratar um caso”, o defeito silencioso clássico das cascatas do capítulo 9, em tarefa listada pelo compilador.

Tipos de soma. A dupla “hierarquia fechada de records” mais “switch exaustivo com desestruturação” reproduz em Java o que outras linguagens chamam de tipos de soma ou uniões etiquetadas: o dado é um entre poucos formatos conhecidos, e o consumo é obrigado a tratar todos. Enum é o caso degenerado, soma de valores sem dados; sealed com records é a forma geral.

Prática

  1. Converta a categoria do Produto para o enum com corredor e refaça a abertura do capítulo: mostre o erro de compilação ao tentar passar texto e o corredor certo saindo de categoria().corredor().

  2. Reproduza a armadilha: switch de enum com default, categoria nova, pão no corredor errado. Depois remova o default e anote a mensagem exata do compilador para cada switch desatualizado.

  3. Implemente Resultado com os três ramos, faça o Caixa do capítulo 9 devolver Resultado em vez de valor cru (recusando pagamento acima de um limite, por exemplo) e escreva linhaDoRecibo com padrões de registro.

  4. Acrescente um quarto ramo, EmAnalise, sem dados. Decida entre record vazio e outra forma, siga os erros de compilação até o sistema inteiro tratá-lo, e conte quantos lugares o compilador listou por você.

  5. Volte ao exercício 3 do capítulo 8 e reescreva o trecho que usava instanceof com cast para a forma com padrão de tipo, comparando as duas versões por escrito.

Ficha do capítulo

TermoDefinição
enumtipo com a lista fechada e nomeada de valores possíveis
constante de enumcada valor declarado; objeto único, comparável com ==
exaustividadegarantia do compilador de que o switch cobre todos os casos
sealed / permitshierarquia fechada, com a lista completa de subtipos declarada
non-sealedreabre um ramo de hierarquia selada para herança livre
pattern matchingcomparar contra um padrão que, casando, extrai as partes
padrão de tipoinstanceof Tipo nome: pergunta e converte num passo
padrão de registrocase Tipo(componentes): casa e desestrutura o record
Regra prática
switch sobre enum ou sealedsem default, para o compilador cobrar os casos novos
ramos de hierarquia seladade preferência records, imutáveis e com contrato pronto

Exceções

O caixa do mercadinho pergunta a quantidade e o operador, com pressa, digita o nome do número:

void main() {
    String resposta = IO.readln("Quantidade: ");
    int quantidade = Integer.parseInt(resposta);
    IO.println("Registrado: " + quantidade);
}
$ java Caixa.java
Quantidade: duas
Exception in thread "main" java.lang.NumberFormatException: For input string: "duas"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:64)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Caixa.main(Caixa.java:3)

O programa caiu, e um caixa que cai por erro de digitação não serve para o balcão. O capítulo 5 prometeu que sobreviver a isso era assunto para este capítulo, e a promessa vence agora. Desde o capítulo 2 o livro provoca erros de execução e lê as mensagens deles; o que faltava era o mecanismo por trás, e ele muda a pergunta: não “como evitar toda queda”, mas “quem decide o que acontece quando algo dá errado”.

A exceção é um objeto

Tudo que este livro chamou de erro de execução tem um nome próprio na linguagem: exceção. Uma exceção é um objeto, dos comuns, criado no instante do problema: carrega uma mensagem, o tipo que classifica o problema (NumberFormatException, NullPointerException e as demais conhecidas) e um retrato da pilha de chamadas do momento. O throw é quem lança esse objeto, e a biblioteca faz o mesmo por dentro: o parseInt da abertura executa um throw new NumberFormatException(...) quando o texto não é número.

Lançada, a exceção interrompe o fluxo normal no ato: a linha seguinte não roda. Ela então sobe pela pilha de chamadas, encerrando cada método no caminho, à procura de alguém disposto a tratá-la; essa subida chama-se propagação. Quando ninguém trata e a propagação passa do main, a JVM encerra o programa e imprime o que a abertura mostrou. Toda queda que o livro provocou até aqui foi exatamente isto: uma exceção que propagou até o fim sem encontrar tratador.

A transcrição da queda tem nome, stack trace: o retrato da pilha, impresso de cima para baixo do ponto do problema até o main. Lê-se assim: a primeira linha dá o tipo e a mensagem; cada linha at é um método que estava em andamento, com arquivo e linha; as de java.base são o interior da biblioteca, com números de linha que variam de uma versão de JDK para outra, e a primeira linha com um arquivo nosso, Caixa.java:3, aponta onde o nosso código entrou na história. Em stack traces longos, achar a linha do próprio programa é o primeiro gesto da leitura.

try e catch

Tratar é declarar-se disposto a receber a exceção:

void main() {
    int quantidade = -1;
    while (quantidade < 0) {
        String resposta = IO.readln("Quantidade: ");
        try {
            quantidade = Integer.parseInt(resposta);
        } catch (NumberFormatException erro) {
            IO.println("Não entendi \"" + resposta + "\". Digite um número.");
        }
    }
    IO.println("Registrado: " + quantidade);
}
$ java Caixa.java
Quantidade: duas
Não entendi "duas". Digite um número.
Quantidade: 2
Registrado: 2

O bloco try delimita o trecho vigiado; o catch declara o tipo de exceção que aceita e recebe o objeto numa variável, como um parâmetro. Dando tudo certo, o catch é ignorado; lançada uma exceção do tipo declarado dentro do try, a execução salta o resto do bloco e entra no catch, e o programa segue vivo depois dele. O laço em volta transforma o tratamento em política: pergunta de novo até vir número. Um try aceita vários catch, um por tipo, avaliados na ordem, e o polimorfismo vale aqui: catch (Exception erro) apanha qualquer exceção do livro, porque todas descendem de Exception. Existe uma família irmã, Error, das falhas da própria JVM, como o StackOverflowError do capítulo 4; ela fica fora dessa rede, e é melhor assim, porque não há tratamento sensato para a pilha estourada. A armadilha adiante mostra por que mesmo a rede das exceções costuma ser larga demais.

A propagação atravessando dois métodos:

int converter(String texto) {
    IO.println("antes do parse");
    int valor = Integer.parseInt(texto);
    IO.println("depois do parse");
    return valor;
}

void main() {
    try {
        IO.println("vou converter");
        int quantidade = converter("duas");
        IO.println("converti: " + quantidade);
    } catch (NumberFormatException erro) {
        IO.println("não deu: " + erro.getMessage());
    }
    IO.println("caixa segue aberto");
}

Cinco IO.println no fonte. Quais rodam, e em que ordem?

vou converter
antes do parse
não deu: For input string: "duas"
caixa segue aberto

O “depois do parse” e o “converti” nunca rodam: a exceção nasceu dentro de parseInt, encerrou converter no meio e continuou subindo até o catch do main, pulando tudo que estava entre o ponto do lançamento e o tratador. Depois do catch, o fluxo normal volta. A propagação atravessa quantos métodos houver, e é isso que permite tratar o erro longe de onde ele nasce, no nível que tem contexto para decidir; o método getMessage, usado no tratador, devolve a mensagem que o criador da exceção escreveu.

Um tratamento escrito para “não deixar o caixa cair de jeito nenhum”:

BigDecimal total = BigDecimal.ZERO;
for (ItemDeVenda item : itens) {
    try {
        total = total.add(item.subtotal());
    } catch (Exception erro) {
    }
}
IO.println("Total do dia: " + total);

O caixa nunca cai. O fechamento do dia bate com o dinheiro na gaveta?

Não há como saber, e esse é o dano. O catch vazio engole qualquer exceção sem registrar nada: um item com produto nulo, um subtotal que estourou uma validação, qualquer defeito vira uma venda silenciosamente ausente do total, e a primeira notícia é a diferença no caixa, dias depois, sem pista. Engolir exceção é o bug silencioso em estado puro, e a rede catch (Exception ...) agrava, porque apanha até os erros de programação que deveriam derrubar e ser corrigidos. As duas regras que evitam o buraco: captura-se o tipo mais específico que se sabe tratar, e todo catch faz alguma coisa, nem que seja registrar e relançar. Cair com stack trace é melhor do que errar em silêncio: a queda tem endereço, o silêncio não.

Checked, unchecked e throws

Nem toda exceção é igual perante o compilador, e é a divisão que decide o que ele exige de quem chama. As exceções unchecked são RuntimeException e tudo que descende dela, a família inteira das conhecidas deste livro: NullPointerException, ArrayIndexOutOfBoundsException, NumberFormatException, IllegalArgumentException, ArithmeticException, ClassCastException. O compilador não exige nada sobre elas, porque em geral denunciam erro de programação, e a correção é consertar o código, não tratar. As checked são as demais descendentes de Exception: o compilador obriga cada método em que elas podem nascer ou passar a escolher entre tratar com catch ou declarar com throws que as deixa propagar:

public class LimiteDeFiadoException extends Exception {
    public LimiteDeFiadoException(String mensagem) {
        super(mensagem);
    }
}

public void vender(ItemDeVenda item) throws LimiteDeFiadoException {
    if (devendo.add(item.subtotal()).compareTo(limite) > 0) {
        throw new LimiteDeFiadoException("Fiado estouraria o limite de R$ " + limite);
    }
    devendo = devendo.add(item.subtotal());
}

Criar exceção própria é estender Exception, para checked, ou RuntimeException, para unchecked, repassando a mensagem com o super. A caderneta de fiado acima escolheu checked de propósito: estourar o limite não é bug, é uma resposta possível do domínio, e quem chama vender é obrigado pelo compilador a decidir na hora o que o mercadinho faz, recusa a venda, oferece outro pagamento, chama o dono. O throws na assinatura é o aviso público dessa obrigação, parte do contrato do método no sentido do capítulo 9.

A régua deste livro, num assunto que divide opiniões há décadas: unchecked para violação de regra que o chamador tinha como respeitar (argumento inválido, estado impossível), checked para condição esperável do domínio que o chamador precisa decidir, e com parcimônia, porque cada throws se espalha pelas assinaturas acima. Grande parte do código moderno usa quase só unchecked; o encontro inevitável com as checked da biblioteca acontece no capítulo 21, quando o programa tocar arquivos.

finally

O bloco finally, acoplado ao try, roda sempre: com sucesso, com exceção tratada, com exceção propagando.

try {
    IO.println(Integer.parseInt(resposta));
} catch (NumberFormatException erro) {
    IO.println("entrada inválida");
} finally {
    IO.println("tentativa encerrada");
}

O uso legítimo é limpeza do que precisa acontecer aconteça o que acontecer, tipicamente devolver um recurso ao sistema. Para o caso mais comum dessa limpeza, fechar o que foi aberto, a linguagem tem uma escrita dedicada que o capítulo 21 apresenta junto com os arquivos, e é ela que o código atual usa; o finally fica para as limpezas que não são fechamento e para ler o código dos outros.

O preço de lançar. Criar uma exceção custa ordens de grandeza mais que um if, porque o retrato da pilha é capturado no new. É um dos motivos de exceção não servir como desvio de fluxo comum: if decide caminho esperado, exceção sinaliza o excepcional. A regra de bolso: se o chamador vai tratar em todo uso normal, provavelmente era um retorno, não uma exceção.

Prática

  1. Blinde o caixa interativo por completo: quantidade não numérica pergunta de novo, quantidade zero ou negativa idem, com mensagens distintas, e o registro só sai com valor válido. Decida onde cada validação mora e por quê.

  2. Reproduza a previsão com três métodos aninhados em vez de dois, prevendo a saída antes de rodar. Depois remova o catch e compare o stack trace impresso com a sua previsão da propagação.

  3. Reproduza a armadilha do catch vazio com um item defeituoso no meio de três, mostre o total errado, e conserte duas vezes: registrando o erro e seguindo, e deixando propagar. Escreva quando cada política é a certa para um caixa de verdade.

  4. Implemente a caderneta de fiado com LimiteDeFiadoException e um main que venda até estourar o limite, tratando a exceção com uma mensagem ao operador. Depois converta a exceção para unchecked, observe o que o compilador deixa de exigir, e escreva qual das duas versões você defenderia na revisão de código do mercadinho.

  5. Escreva um método que provoque, de propósito, três exceções unchecked diferentes deste livro, conforme o argumento recebido, e um main com três catch específicos que identifique cada uma. Acrescente um quarto caso que nenhum catch cubra e descreva o que acontece.

Ficha do capítulo

TermoDefinição
exceçãoobjeto que descreve um problema; lançado, interrompe o fluxo
propagaçãoa subida da exceção pela pilha, encerrando métodos, até um tratador
stack traceo retrato da pilha no momento do lançamento, impresso na queda
try / catchtrecho vigiado e tratador por tipo de exceção
finallybloco que roda sempre, com ou sem exceção
throwsdeclara que o método deixa a exceção checked propagar
checkedo compilador obriga a tratar ou declarar; condição esperável do domínio
uncheckedRuntimeException e descendentes; em geral, erro de programação
RuntimeExceptiona raiz da família unchecked
Regra prática
capturao tipo mais específico que se sabe tratar
catch vazionunca; registrar, reagir ou relançar
exceção própriaestende Exception (checked) ou RuntimeException (unchecked)

Maven e Gradle

O mercadinho já soma uma dúzia de classes, e o ritual de colocá-lo de pé cresceu junto:

$ javac -d saida *.java
$ java -cp saida Loja

Funciona, e três incômodos moram aí. O curinga *.java alcança uma pasta só: no momento em que os fontes se organizarem em subpastas, os pacotes do capítulo 7, o comando deixa arquivos para trás. O classpath, digitado desde o capítulo 2, precisa ser combinado entre todos que rodam o projeto, em toda máquina. E o terceiro incômodo é o maior: nenhum código de fora entra. A ferramenta de testes do capítulo 15, ou qualquer outra peça pronta do ecossistema, chegaria como um arquivo a baixar, guardar em algum lugar e acrescentar ao classpath, à mão, para cada projeto e cada máquina. Sistemas reais não vivem assim; eles usam uma ferramenta de construção, e este capítulo instala a do resto do livro.

O que uma ferramenta de construção faz

Uma ferramenta de construção é o programa que transforma o código-fonte, junto do código de fora de que ele depende, num pacote pronto para distribuir, sempre do mesmo jeito, em qualquer máquina. Ela descobre os fontes sozinha, baixa o código de fora que o projeto declarar, monta o classpath, compila na ordem certa, roda verificações e empacota o resultado. O que era uma sequência de comandos combinada entre pessoas vira um arquivo guardado junto do projeto, e “funciona na minha máquina” deixa de ser mistério: a construção é a mesma em todas.

As duas ferramentas dominantes no mundo Java são Maven e Gradle. Este livro apresenta as duas e adota o Maven do próximo capítulo em diante; os motivos fecham o capítulo, junto com o mapa para ler projetos Gradle. A instalação segue o caminho do capítulo 1:

$ sdk install maven
$ mvn -version
Apache Maven 3.9.11
...

A primeira linha da resposta confirma a instalação; as seguintes, omitidas acima, mostram qual JDK a ferramenta enxerga, que precisa ser o 25 do livro.

Maven: convenção e pom.xml

O Maven não pergunta onde estão os fontes: ele decide, e essa é a primeira ideia da ferramenta. A convenção de diretórios é o leiaute fixo de pastas que o Maven espera:

mercadinho/
├── pom.xml
└── src/
    ├── main/
    │   └── java/        ← código do sistema
    └── test/
        └── java/        ← código de teste, a partir do capítulo 15

Tudo que a ferramenta produz vai para target/, uma pasta descartável que nunca se edita nem se versiona. A convenção vale por contrato social: qualquer pessoa que já viu um projeto Maven sabe onde está cada coisa neste, sem ler documentação nenhuma.

O único arquivo a escrever é o pom.xml, a descrição do projeto:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <groupId>br.com.mercadinho</groupId>
    <artifactId>mercadinho</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

A escrita entre < e > é XML, um formato de texto para dados aninhados, e o essencial dela se lê no exemplo: cada <coisa> abre e </coisa> fecha, e o conteúdo vai no meio. As três linhas do meio identificam o projeto: groupId é o dono, escrito como um domínio de internet de trás para a frente, br.com.mercadinho para quem controla mercadinho.com.br, convenção que garante nome único no mundo sem cadastro central, porque só o dono do domínio o usaria; artifactId é o nome; version é a versão. As properties travam a versão do Java, a 25 exigida desde o capítulo 1, e a codificação dos fontes. Com isso e os fontes movidos para src/main/java, a construção inteira vira:

$ mvn package
[INFO] BUILD SUCCESS
$ java -cp target/classes Loja

mvn compile compila tudo para target/classes; mvn package compila e ainda empacota. O pacote gerado, target/mercadinho-1.0.jar, é um jar: o formato de distribuição do Java, um arquivo único que embrulha o bytecode e metadados, feito para entrar no classpath de outros projetos. O nome sai da coordenada do pom.xml, e o capítulo 21 mostra a variante executável com um único comando.

A armadilha do capítulo 2, revisitada com ferramenta. O projeto foi empacotado, o preço do café mudou no fonte, e o operador roda:

$ mvn package
[INFO] BUILD SUCCESS
$ # o preço muda em Produto.java, no editor
$ java -cp target/classes Loja

A saída mostra o preço novo ou o antigo?

O antigo. O Maven automatiza a construção, não a adivinha: target/classes guarda o bytecode do último mvn package, e editar fonte não recompila nada, exatamente como no capítulo 2. A diferença é que o hábito protetor ficou barato: um único comando reconstrói o projeto inteiro, sempre, e “editou, rodou mvn, executou” vira o ciclo padrão de trabalho.

Dependências

O ganho que justifica a cerimônia é declarar código de fora:

<dependencies>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.11.0</version>
    </dependency>
</dependencies>

O trio grupo, artefato e versão é a coordenada: o endereço único de um artefato publicado, onde artefato é qualquer pacote construído e distribuído, tipicamente um jar. Declarada a coordenada, o mvn package baixa o artefato do repositório, o servidor público que hospeda artefatos publicados, sendo o Maven Central o repositório padrão do ecossistema, guarda uma cópia local e o coloca no classpath de compilação e de execução. O exemplo acima traz uma biblioteca de conversão de dados apenas para mostrar a forma; a primeira dependência que o mercadinho vai realmente usar chega no capítulo 15, com a ferramenta de testes.

Junto da dependência declarada vêm as dela: se o artefato pedido depende de outros, o Maven os baixa também, e esses são as dependências transitivas. É o que torna o ecossistema utilizável, ninguém declara a árvore inteira à mão, e é também uma porta de surpresas: duas dependências que pedem versões diferentes do mesmo artefato obrigam a ferramenta a escolher uma, e o comando mvn dependency:tree imprime a árvore completa quando a escolha precisar de auditoria. A regra prática: declarar o que o código importa diretamente, deixar o transitivo para a ferramenta, e olhar a árvore quando algo cheirar a versão trocada.

Uma classe nova, Promocao.java, criada por engano na raiz do projeto, ao lado do pom.xml, em vez de dentro de src/main/java:

$ mvn package
[INFO] BUILD SUCCESS

A construção passa limpa. Onde está a classe?

Em lugar nenhum: fora da convenção, o arquivo é invisível para o Maven, que compilou o resto e declarou sucesso, porque dele nada foi pedido sobre um arquivo que ele não enxerga. O sintoma aparece depois, como cannot find symbol em quem usar Promocao, ou como um jar sem a classe. A convenção de diretórios não é sugestão: é o contrato de descoberta dos fontes, e arquivo fora dela simplesmente não existe para a construção. O mesmo vale para os testes, na pasta deles.

Gradle, e a escolha do livro

O Gradle constrói sobre os mesmos conceitos, convenção de diretórios idêntica, coordenadas idênticas, os mesmos repositórios, trocando o XML por um script de configuração:

plugins {
    java
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.code.gson:gson:2.11.0")
}

O arquivo é mais curto, o modelo de execução é mais flexível, e projetos grandes o adotam por desempenho de construção; o preço é um script programável onde o Maven tem um documento fixo, e mais de um jeito de fazer cada coisa. Este livro adota o Maven daqui em diante por três motivos práticos: é o formato que o leitor mais vai encontrar em projetos e em respostas na internet, o pom.xml se lê sem conhecer linguagem de script nenhuma, e tudo que o livro ensinar sobre ele se traduz para Gradle trocando a grafia, porque os conceitos deste capítulo são os mesmos nas duas ferramentas. Quem cair num projeto Gradle lê o build.gradle.kts com o vocabulário daqui: coordenada, repositório, dependência, convenção.

O repositório local. Tudo que o Maven baixa fica em ~/.m2/repository, organizado por coordenada, e cada artefato é baixado uma vez por máquina, não por projeto. A primeira construção de um projeto novo é lenta e as seguintes são rápidas por causa desse cache; apagar a pasta é seguro e custa só o novo download.

Prática

  1. Converta o mercadinho para a estrutura Maven: crie o pom.xml deste capítulo, mova os fontes para src/main/java, rode mvn package e execute com java -cp target/classes. Guarde o pom.xml junto do código, e confirme que target/ é descartável apagando-o e reconstruindo.

  2. Reproduza a previsão: mude um preço, execute sem reconstruir, veja o valor antigo, reconstrua e veja o novo. Escreva a regra de trabalho em uma frase.

  3. Reproduza a armadilha da classe fora da convenção e a conserte. Depois liste, com ls target/classes, o que a construção produziu, e confira o conteúdo do jar com jar tf target/mercadinho-1.0.jar.

  4. Declare a dependência de exemplo do capítulo, rode mvn package e depois mvn dependency:tree. Identifique na árvore o que você declarou e o que veio por transitividade, e localize os arquivos correspondentes em ~/.m2/repository.

  5. Troque o maven.compiler.release para 21 e reconstrua. Anote o erro, explique-o com o capítulo 2, e desfaça.

Ficha do capítulo

ComandoO que faz
mvn compilecompila os fontes de src/main/java para target/classes
mvn packagecompila e empacota o jar em target/
mvn dependency:treeimprime a árvore de dependências, transitivas incluídas
jar tf arquivo.jarlista o conteúdo de um jar
TermoDefinição
Mavenferramenta de construção adotada pelo livro; descrita pelo pom.xml
Gradleferramenta equivalente com script de configuração; mesmos conceitos
convenção de diretóriosleiaute fixo (src/main/java, src/test/java, target/) que a ferramenta espera
artefatopacote construído e distribuído; tipicamente um jar
coordenadaendereço único de um artefato: grupo, nome e versão
repositórioservidor que hospeda artefatos; o central é o padrão do ecossistema
dependência transitivadependência trazida por outra dependência
jararquivo único com bytecode e metadados, pronto para o classpath

JUnit 5 e o hábito de testar

Toda correção do mercadinho, até aqui, foi conferida do mesmo jeito: rodar o programa e olhar a saída. O método funciona para um arquivo e morre de cansaço num sistema: a cada mudança no Caixa, alguém precisaria reexecutar à mão a venda em dinheiro, a venda no cartão, o fiado no limite, o desconto, o troco, e ninguém reexecuta tudo, então as quebras passam. A conferência manual deixa de acontecer conforme o sistema cresce; a saída é escrever programas que conferem programas.

Um teste unitário é um método que executa um pedaço pequeno do sistema, uma unidade, e confere o resultado contra o esperado, falhando sozinho quando eles divergem. O JUnit é a ferramenta que descobre esses métodos, roda todos e imprime o placar, e é a primeira dependência de verdade do pom.xml, exatamente como o capítulo 14 prometeu:

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

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

Duas novidades no XML. O <scope>test</scope> diz que a dependência existe só para os testes: o JUnit não entra no jar do mercadinho, porque o cliente do sistema não roda testes. E o bloco do surefire fixa a versão do executor de testes do Maven. A trava protege contra máquinas e projetos presos a Maven antigo, cujo executor padrão ignora testes de JUnit 5 em silêncio, exibindo BUILD SUCCESS sem rodar teste nenhum; o Maven do capítulo 14 já traz executor moderno, e fixar a versão mantém a construção igual em toda máquina.

O primeiro teste

Teste é código, e mora na pasta que a convenção de diretórios reservou: src/test/java. A classe de teste espelha a classe testada, com Test no nome:

import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class ProdutoTest {

    @Test
    void calculaPrecoParaTresUnidades() {
        Produto cafe = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"));
        BigDecimal total = cafe.precoPara(3);
        assertEquals(new BigDecimal("59.70"), total);
    }
}
$ mvn test
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

A anotação @Test, no espírito do @Override, marca o método para o executor encontrar; o nome do método é a descrição do caso, e vale uma frase inteira, porque é ele que aparece no placar quando falha. O corpo segue um ritmo em três tempos que praticamente todo teste do mundo repete: preparar os objetos, executar a operação, conferir o resultado.

A conferência é a asserção: uma afirmação verificável sobre o resultado, que derruba o teste quando é falsa. assertEquals(esperado, obtido) é a asserção central, e a ordem dos argumentos importa: o valor esperado vem primeiro, e trocá-los não quebra nada hoje, mas inverte a mensagem de erro de todas as falhas futuras, apontando o certo como errado. O import static da abertura é a novidade de sintaxe: importa o método, em vez do tipo, para assertEquals dispensar prefixo. Existem asserções irmãs para os demais casos, assertTrue, assertFalse e assertNotNull entre elas, todas na ficha.

Quando a asserção falha, o teste conta exatamente o quê e onde:

$ mvn test
[ERROR] CaixaTest.aplicaDescontoDeCincoPorCento:18
org.opentest4j.AssertionFailedError: expected: <57.00> but was: <57.0000>
[INFO] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE

A falha acima nasce de um teste sobre o método que aplica o desconto de 5% do dinheiro:

@Test
void aplicaDescontoDeCincoPorCento() {
    BigDecimal resultado = caixa.comDesconto(new BigDecimal("60.00"));
    assertEquals(new BigDecimal("57.00"), resultado);
}

Sessenta reais menos 5% são exatamente cinquenta e sete, e o método multiplica por 0.95 sem errar a conta. Por que o teste falha mesmo assim?

Porque assertEquals usa o equals, e o equals de BigDecimal é o estrito por escala: 57.00 e 57.0000 são o mesmo número, o compareTo daria zero, e ainda assim não são iguais para o equals, porque a multiplicação somou as casas dos dois lados. O teste está protegendo o sistema de um jeito que a etiqueta não mostra: ou o método fixa a escala com setScale(2), e todo valor de dinheiro do sistema circula com duas casas, ou cada desconto acumula casas fantasmas que vão aparecer em alguma comparação futura. Teste bom falha por motivo verdadeiro, e este é o primeiro serviço do placar: a conversa sobre escala aconteceu no capítulo do teste, e não numa diferença de caixa.

Fixture e o ciclo de cada teste

Testes da mesma classe repetem preparação, e a repetição tem lugar próprio. A fixture é o conjunto de objetos que os testes de uma classe usam como ponto de partida, e o método anotado com @BeforeEach a monta de novo antes de cada teste:

Os imports já vistos ficam omitidos dos próximos trechos.

import org.junit.jupiter.api.BeforeEach;

class CaixaTest {

    private Caixa caixa;
    private Produto cafe;

    @BeforeEach
    void prepara() {
        caixa = new Caixa();
        cafe = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"));
    }

    @Test
    void somaUmaVendaAoTotal() {
        caixa.registrar(cafe, 2);
        assertEquals(0, new BigDecimal("39.80").compareTo(caixa.totalDoDia()));
    }

    @Test
    void comecaComTotalZerado() {
        assertEquals(0, BigDecimal.ZERO.compareTo(caixa.totalDoDia()));
    }
}

O “de novo antes de cada” é a parte que importa: cada teste recebe uma fixture recém-criada, e a ordem em que os testes rodam deixa de ter importância, porque nenhum herda o estado sujo do anterior. Um teste que só passa depois de outro é um teste que mente, e a fixture por teste é o que evita a mentira. A comparação via compareTo, nas duas asserções, é a regra de dinheiro aplicada a testes.

Falta testar o que deve dar errado: o construtor de Produto precisa recusar estoque negativo, e isso também é comportamento com contrato. Sem as peças do capítulo 18, a forma disponível usa try e catch:

@Test
void recusaEstoqueNegativo() {
    try {
        new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"), -8);
        fail("Deveria ter recusado estoque negativo.");
    } catch (IllegalArgumentException erro) {
        assertTrue(erro.getMessage().contains("-8"));
    }
}

fail derruba o teste ao ser alcançado: se o construtor aceitar o valor inválido, a linha roda e o placar acusa. Se recusar, o catch confirma que a mensagem carrega o valor, e o teste passa. O capítulo 18 apresenta a forma moderna e mais curta de escrever exatamente isso.

Um teste novo entra em CaixaTest, e o placar da classe segue limpo:

void recusaQuantidadeZero() {
    try {
        new ItemDeVenda(cafe, 0);
        fail("Deveria ter recusado quantidade zero.");
    } catch (IllegalArgumentException erro) {
    }
}
$ mvn test
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 -- in CaixaTest
[INFO] BUILD SUCCESS

A validação de quantidade zero nunca foi escrita em ItemDeVenda. Por que nada falhou?

Porque o teste nunca rodou: faltou o @Test, o método é invisível para o executor, e o placar conta só os dois testes antigos. Nenhum erro, nenhum aviso, e uma proteção que todo mundo acredita existir não existe. A defesa é dupla: conferir o número do placar quando se acrescenta teste, esperando vê-lo subir, e desconfiar de teste que nasce passando. Um teste novo deve falhar pelo menos uma vez, na frente de quem o escreveu, antes de valer alguma coisa; teste que nunca falhou não provou que sabe falhar.

Regressão: o bug vira teste

O uso mais valioso do JUnit num sistema vivo tem nome. Um teste de regressão é o teste escrito a partir de um defeito encontrado: antes de corrigir, escreve-se o teste que reproduz o erro e falha; corrige-se; o teste passa e fica. O defeito do dado viciado do capítulo 6 nunca mais volta sem ser notado, porque a semente fixa daquele capítulo torna o sorteio testável:

@Test
void sorteioComSementeFixaFicaNaFaixaDoDado() {
    Random dado = new Random(42);
    for (int volta = 0; volta < 1000; volta++) {
        int face = dado.nextInt(1, 7);
        assertTrue(face >= 1 && face <= 6);
    }
}

É a promessa de reprodutibilidade paga: com a semente fixa, o teste confere as mesmas mil jogadas em qualquer máquina, para sempre. E o conjunto de testes acumulado muda a economia do sistema inteiro: refatorar o Caixa, trocar o cálculo do desconto ou acelerar um método deixa de ser um salto no escuro, porque o placar diz na hora o que quebrou. O contrato do capítulo 9 dizia o que cada tipo promete; o teste é a versão executável da promessa, conferida a cada mvn test.

De onde o JUnit conhece os testes. Nenhuma linha do mercadinho chama ProdutoTest. O executor abre as classes de teste compiladas e pergunta a cada uma quais métodos carregam @Test, usando um mecanismo da linguagem para examinar tipos em execução; o capítulo 23 constrói um executor desses do zero, em vinte linhas.

Prática

  1. Adote o pom.xml deste capítulo no mercadinho e escreva ProdutoTest completo: preço para quantidade, recusa de nome vazio, recusa de estoque negativo, com o padrão try-fail-catch para as recusas.

  2. Escreva CaixaTest com fixture: total zerado no início, uma venda, três vendas somadas, e o desconto do dinheiro com a escala decidida por setScale. Faça cada teste falhar uma vez de propósito, alterando o código testado, e desfaça.

  3. Reproduza a armadilha do teste sem @Test e descreva em uma frase qual número do placar teria denunciado o problema.

  4. Escreva um teste de regressão para a armadilha do equals sobrecarregado do capítulo 10: dois produtos de mesmo código comparados através de Object devem ser iguais. Rode-o contra a versão errada e contra a certa.

  5. Escreva um teste que documente o comportamento do LimiteDeFiadoException do capítulo 13: vender dentro do limite passa, estourar o limite lança, e a mensagem carrega o limite. Decida na fixture qual limite usar.

Ficha do capítulo

Anotação / chamadaO que faz
@Testmarca o método como teste, para o executor encontrar
@BeforeEachroda antes de cada teste; monta a fixture
assertEquals(esperado, obtido)falha se diferentes; esperado vem primeiro
assertTrue / assertFalse / assertNotNullasserções sobre condição e presença
fail(mensagem)derruba o teste ao ser alcançado
mvn testcompila e roda todos os testes de src/test/java
TermoDefinição
JUnitferramenta que descobre, executa e reporta testes
teste unitáriométodo que executa uma unidade do sistema e confere o resultado
asserçãoafirmação verificável que derruba o teste quando falsa
assertEqualsasserção de igualdade, via equals; dinheiro pede compareTo
fixtureobjetos de partida dos testes, recriados antes de cada um
teste de regressãoteste nascido de um defeito, para ele não voltar despercebido

Generics e type erasure

O capítulo 8 deixou anotado que uma variável Object aceita qualquer objeto, e que a biblioteca padrão usava isso para escrever código que serve a todos os tipos. O mercadinho tenta o mesmo truque numa prateleira de capacidade fixa:

class Prateleira {
    private final Object[] itens;
    private int quantidade = 0;

    Prateleira(int capacidade) {
        itens = new Object[capacidade];
    }

    public void guardar(Object item) {
        itens[quantidade] = item;
        quantidade++;
    }

    public Object pegar(int posicao) {
        return itens[posicao];
    }
}

void main() {
    Prateleira docas = new Prateleira(10);
    docas.guardar(new Produto("7891000100103", "Café 500g", new BigDecimal("19.90")));
    docas.guardar("anotação do repositor: conferir validade");
    Produto produto = (Produto) docas.pegar(1);
    IO.println(produto.nome());
}
$ java Recebimento.java
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class Produto ...
	at Recebimento.main(Recebimento.java:23)

A prateleira aceita produto, aceita bilhete, aceita qualquer coisa, porque Object aceita qualquer coisa; e tudo que sai dela sai como Object, obrigando um downcast em cada leitura. O programa compila inteiro e cai na leitura da posição errada, em execução, longe do guardar que plantou o problema. O truque do Object tem exatamente esse custo: o compilador, que passou o livro inteiro pegando engano de tipo, não confere nada dentro dela. Generics são tipos e métodos parametrizados por tipo, conferidos em compilação, e são o mecanismo que devolve essa conferência ao compilador. Os experimentos deste capítulo são rascunhos de laboratório, arquivos-fonte compactos fora do projeto Maven, para provocar erros sem tocar o estoque real; as conclusões voltam ao projeto na prática.

O parâmetro de tipo

class Prateleira<T> {
    private final Object[] itens;
    private int quantidade = 0;

    Prateleira(int capacidade) {
        itens = new Object[capacidade];
    }

    public void guardar(T item) {
        itens[quantidade] = item;
        quantidade++;
    }

    public T pegar(int posicao) {
        @SuppressWarnings("unchecked")
        T item = (T) itens[posicao];
        return item;
    }
}

O <T> depois do nome declara um parâmetro de tipo: um nome que representa um tipo ainda não escolhido, usável dentro da classe onde qualquer tipo seria usável. É o mesmo movimento do parâmetro de método, subido um andar: o método generaliza sobre valores, o parâmetro de tipo generaliza sobre tipos. Quem escolhe o tipo é quem usa, entre os sinais de menor e maior, e a escolha se chama argumento de tipo:

Prateleira<Produto> docas = new Prateleira<>(10);
docas.guardar(new Produto("7891000100103", "Café 500g", new BigDecimal("19.90")));
docas.guardar("anotação do repositor");
$ javac Recebimento.java
Recebimento.java:3: error: incompatible types: String cannot be converted to Produto
docas.guardar("anotação do repositor");
              ^

Com T valendo Produto, o guardar só aceita Produto, o bilhete do repositor morreu em compilação com arquivo e linha, e o pegar devolve Produto sem cast nenhum do lado de quem chama. O engano que era queda em execução virou recusa do compilador, que é a direção de sempre deste livro. O <> vazio do new pede inferência: o compilador copia o argumento de tipo da declaração.

Dentro da classe sobrou um resto honesto: o array interno continua de Object, e o pegar faz um cast para T que o compilador não consegue provar, avisando com um alerta de compilação. A anotação @SuppressWarnings("unchecked"), da família do @Override, silencia o alerta naquele ponto, e só é decente quando a classe garante por construção o que o compilador não vê: aqui, só T entra pelo guardar, então só T sai. A razão de o compilador não conseguir provar é a seção do apagamento, logo adiante.

Métodos também generalizam sozinhos, sem a classe inteira ser genérica. Um método genérico declara o próprio parâmetro de tipo antes do retorno:

static <T> T ultimoDe(T[] itens) {
    return itens[itens.length - 1];
}

Chamado com um array de Produto, devolve Produto; com um array de String, devolve String; e o compilador infere o T pelo argumento, sem ninguém escrever o <>.

Type erasure: o tipo que a execução não vê

Duas prateleiras de tipos diferentes, e a pergunta sobre o tipo real delas. O método getClass, herdado de Object, devolve o tipo do objeto em execução:

Prateleira<Produto> estoque = new Prateleira<>(10);
Prateleira<String> avisos = new Prateleira<>(10);
IO.println(estoque.getClass() == avisos.getClass());

true ou false?

true

Para a execução, as duas prateleiras têm exatamente o mesmo tipo. Esse é o type erasure, o apagamento de tipos: os parâmetros e argumentos de tipo existem apenas para o compilador, que os usa para conferir tudo e depois os apaga; o bytecode carrega uma Prateleira só, com Object onde havia T. A consequência prática vem em três formas que aparecem cedo: não existe new T(...) nem new T[...], porque na execução T não existe; instanceof Prateleira<Produto> é recusado pelo compilador, porque a pergunta não teria como ser respondida; e o cast para T dentro da classe gera o alerta da seção anterior, porque vira um cast para Object que não confere nada. A segurança dos generics é integralmente de compilação, o que não a diminui: é onde este livro sempre quis os erros.

Por que apagar. Generics chegaram na versão 5 da linguagem, quase uma década depois do Java 1.0, e o apagamento foi o preço da compatibilidade: o bytecode genérico roda em bibliotecas e JVMs que nunca souberam de T, e todo o código anterior seguiu válido. A alternativa, tipos genéricos vivos em execução, existe em outras linguagens e cobrou delas a migração que o Java decidiu não cobrar.

Invariância

Com tipos parametrizados vem uma regra que surpreende quem chega da herança: Prateleira<ProdutoPorPeso> não é um subtipo de Prateleira<Produto>, ainda que ProdutoPorPeso seja um Produto. Essa propriedade se chama invariância: entre tipos genéricos, a relação de subtipo dos argumentos não se propaga. O motivo é defesa, e o raciocínio cabe em três linhas: se a atribuição fosse aceita, uma referência Prateleira<Produto> para a prateleira dos queijos deixaria guardar um Produto comum ali dentro, e a prateleira dos queijos passaria a conter um não-queijo, com a falha adiada para o primeiro pegar tipado do outro lado. O compilador recusa a atribuição para não ter que aceitar a consequência.

Arrays fizeram a escolha oposta quase uma década antes, e ela continua na linguagem:

void main() {
    ProdutoPorPeso[] queijos = new ProdutoPorPeso[3];
    Produto[] produtos = queijos;
    produtos[0] = new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"));
    IO.println("guardou");
}

O programa compila sem um aviso. O que acontece ao rodar?

$ java Deposito.java
Exception in thread "main" java.lang.ArrayStoreException: Produto
	at Deposito.main(Deposito.java:4)

A atribuição de queijos para produtos compila, porque arrays propagam o subtipo, e a JVM é obrigada a vigiar cada escrita em execução para impedir o café na prateleira dos queijos: a vigilância falha o mais tarde possível, com ArrayStoreException no ponto da escrita, que pode estar a um sistema de distância da atribuição que criou o disfarce. É o mesmo problema que a invariância dos generics mata em compilação, e a comparação das duas escolhas é a melhor defesa da regra que surpreende.

Wildcard: aceitar a família inteira

A invariância protege e atrapalha: um método de relatório que recebe Prateleira<Produto> recusa a prateleira dos queijos, e escrever uma versão por subtipo seria a duplicação que o capítulo 8 condenou. O curinga resolve:

static BigDecimal valorTotal(Prateleira<? extends Produto> prateleira, int quantidade) {
    BigDecimal total = BigDecimal.ZERO;
    for (int i = 0; i < quantidade; i++) {
        total = total.add(prateleira.pegar(i).preco());
    }
    return total;
}

? extends Produto lê-se “prateleira de algum tipo que é Produto”, e o wildcard, o ?, é exatamente isso: um argumento de tipo desconhecido, com um limite declarado. O método aceita Prateleira<Produto> e Prateleira<ProdutoPorPeso>, e paga com uma restrição coerente: dá para ler como Produto, e não dá para guardar coisa nenhuma, porque o tipo exato guardável é desconhecido. O espelho para escrita, ? super, fica registrado na ficha; no dia a dia, o ? extends de leitura responde pela maioria esmagadora dos usos, e o capítulo 17 o traz embutido nas assinaturas que o mercadinho vai consumir.

Prática

  1. Implemente a Prateleira<T> completa, com validação de capacidade e de posição, e reproduza as duas versões da abertura: a queda com Object e a recusa de compilação com generics. Anote as duas mensagens.

  2. Escreva um método genérico trocar(T[] itens, int i, int j) que inverta duas posições de qualquer array, e use-o com produtos e com textos.

  3. Reproduza a armadilha do ArrayStoreException e depois tente escrever a linha equivalente com Prateleira. Anote a mensagem do compilador e explique por escrito qual dos dois momentos de erro custa mais caro num sistema.

  4. Prove o apagamento de dois jeitos: a previsão do getClass e uma tentativa de instanceof Prateleira<Produto>. Registre a mensagem do compilador da segunda.

  5. Escreva contarBaratos(Prateleira<? extends Produto> p, int quantidade, BigDecimal teto) devolvendo quantos produtos custam menos que o teto, usando compareTo. Confirme que aceita prateleiras de Produto e de ProdutoPorPeso.

Ficha do capítulo

TermoDefinição
genericstipos e métodos parametrizados por tipo, conferidos em compilação
parâmetro de tipoo nome declarado em <T>, valendo por um tipo a escolher
argumento de tipoo tipo escolhido no uso: Prateleira<Produto>
método genéricométodo com parâmetro de tipo próprio: static <T> T ultimoDe(T[])
type erasureo apagamento: argumentos de tipo existem só para o compilador
invariânciaPrateleira<Sub> não é Prateleira<Super>; protege a escrita
wildcard?: argumento de tipo desconhecido com limite; ? extends lê, ? super escreve
@SuppressWarnings("unchecked")silencia o alerta de cast que o compilador não consegue provar; só com garantia por construção

Coleções: array vs. List, Set, Map

A Prateleira do capítulo anterior herda do array a limitação de nascença: a capacidade é fixada no new, e um carrinho de compras não sabe de antemão quantos itens vai ter. Crescer um array é criar outro maior e copiar tudo, e esse serviço, junto com dezenas de outros, a biblioteca padrão já presta, num conjunto de tipos que todo programa Java usa todos os dias:

List<ItemDeVenda> carrinho = new ArrayList<>();
carrinho.add(new ItemDeVenda(cafe, 2));
carrinho.add(new ItemDeVenda(queijo, 1));
IO.println(carrinho.size());
IO.println(carrinho.get(0).subtotal());

Este capítulo apresenta as três estruturas que respondem pela quase totalidade do uso real, List, Set e Map, com o critério de escolha entre elas, e a árvore de interfaces que as organiza, completada pela fila, Queue. Como cada estrutura funciona por dentro é assunto de estrutura de dados, uma disciplina inteira que fica fora deste livro; o que entra aqui é o que se usa e o que se pergunta em entrevista.

A árvore: de Iterable a Queue

Os tipos deste capítulo não são avulsos: formam uma árvore de interfaces ligadas por herança, e quem conhece o desenho dela lê dezenas de assinaturas da biblioteca sem decorar nenhuma. A definição que ancora tudo: uma coleção é um objeto que reúne múltiplos elementos numa única unidade. No topo da árvore está Iterable, o contrato de quem pode ser percorrido, com um único método essencial, iterator(), que entrega o objeto de percurso; é exatamente o que o for-each exige, e qualquer Iterable serve nele (o array, que fica fora da árvore, recebe do compilador um tratamento à parte). De Iterable herda Collection, o contrato geral das coleções de fato: o que vale para qualquer grupo de elementos mora nela, add, remove, contains, size, isEmpty. E de Collection descem as três especializações, cada uma acrescentando uma promessa ao contrato de Collection: List promete posição, Set promete ausência de duplicata, e Queue promete uma disciplina de saída, tipicamente a ordem de chegada.

Map fica fora da árvore de propósito: um dicionário de chave para valor não é um grupo de elementos soltos, e forçá-lo no contrato de Collection estragaria os dois. Ele encabeça uma hierarquia própria, paralela e menor.

flowchart TD
    I["Iterable"] --> C["Collection"]
    C --> L["List"]
    C --> S["Set"]
    C --> Q["Queue"]
    Q --> D["Deque"]
    M["Map"]

O padrão de leitura da biblioteca sai do diagrama: a interface diz o contrato, e a implementação diz a mecânica no prefixo do nome, ArrayList para List, HashSet para Set, HashMap para Map, ArrayDeque para a fila. O resto do capítulo desce a árvore ramo por ramo.

List e ArrayList

List é uma interface, no sentido pleno da palavra: o contrato de uma sequência de tamanho variável, com posição, cujos métodos aposentam o improviso de crescer array à mão: add acrescenta no fim, get(i) lê pela posição, size() conta, remove tira, contains procura usando equals. ArrayList é a implementação padrão do contrato: guarda os elementos num array interno e o troca por um maior quando enche, sozinha. A declaração segue a lição de programar contra o contrato:

List<ItemDeVenda> carrinho = new ArrayList<>();

A variável é do tipo da interface e o new escolhe a implementação, no mesmo desenho do MeioDePagamento: o resto do código depende só de List, e trocar a implementação um dia não toca linha nenhuma. O parâmetro de tipo é quem faz get devolver ItemDeVenda sem cast e o compilador recusar qualquer intruso no add; a assinatura do construtor de cópia, new ArrayList<>(colecao), traz o wildcard ? extends trabalhando, aceitando qualquer Collection.

O repertório de List vai além do essencial da abertura, e o cartaz de ofertas da feira serve de bancada:

List<String> cartaz = new ArrayList<>();
cartaz.add("banana");
cartaz.add("alface");
cartaz.add("tomate");
cartaz.add(1, "couve");
cartaz.set(0, "banana prata");
IO.println(cartaz);
IO.println(cartaz.indexOf("tomate"));
[banana prata, couve, alface, tomate]
3

O add de dois argumentos insere na posição dada e empurra os seguintes uma casa adiante, por isso a couve entrou na posição 1 e a alface passou para a 2. O set troca o elemento da posição pelo novo, sem empurrar ninguém, e devolve o que saiu. O indexOf procura com equals e devolve a posição da primeira ocorrência, ou -1 quando não encontra, a convenção de “não achei” das buscas por posição. O repertório do dia a dia, na mesma bancada:

ChamadaEfeito
cartaz.add("tomate")acrescenta no fim
cartaz.add(1, "couve")insere na posição 1 e empurra os seguintes
cartaz.get(0)lê pela posição
cartaz.set(0, "banana prata")troca na posição, sem empurrar, e devolve o que saiu
cartaz.remove(2)tira pela posição
cartaz.remove("alface")tira a primeira ocorrência igual, pelo equals
cartaz.contains("tomate")pergunta pela presença, com equals
cartaz.indexOf("tomate")posição da primeira ocorrência; -1 se não achar
cartaz.size() / cartaz.isEmpty()quantos elementos / se está vazia
cartaz.addAll(outroCartaz)despeja outra coleção inteira no fim
cartaz.clear()esvazia

As duas formas de remove, pela posição e pelo elemento, carregam uma armadilha guardada para a seção dos wrappers. Sobre o custo, o suficiente para decidir: get e set são saltos diretos no array interno, e inserir ou remover no meio empurra os elementos seguintes, um preço que só aparece em listas grandes.

A pergunta de entrevista mora na segunda implementação da ficha: LinkedList guarda os elementos em nós encadeados, cada um apontando para o seguinte, em vez de num array contíguo. Na teoria, inserir no meio dela é mais barato; na prática, percorrer nós espalhados pela memória perde de longe para varrer um array contíguo, e o get(i) dela precisa caminhar até a posição. A resposta honesta, que serve para a entrevista e para o código: ArrayList é a escolha padrão, praticamente sempre, e LinkedList é a resposta de uma pergunta clássica, não uma ferramenta do dia a dia.

Wrappers e autoboxing

List<int> não compila. Parâmetro de tipo aceita só tipo de objeto, e para cada primitivo a biblioteca tem uma classe wrapper que o embrulha como objeto: Integer para int, Long, Double, Boolean e Character para os demais. O Integer.parseInt do capítulo 5 morava exatamente nessa classe, e a promessa de apresentar a família vence aqui. A conversão é automática nas duas direções: autoboxing na ida do primitivo para o wrapper, unboxing na volta.

List<Integer> quantidades = new ArrayList<>();
quantidades.add(3);
int primeira = quantidades.get(0);

O 3 entra como Integer e sai como int sem cerimônia visível, e é fácil esquecer que wrapper é objeto, com tudo que vale para objetos:

Quatro caixas de leite e um limite de estoque:

void main() {
    Integer contagem = 127;
    Integer limite = 127;
    IO.println(contagem == limite);

    Integer contagemMaior = 128;
    Integer limiteMaior = 128;
    IO.println(contagemMaior == limiteMaior);
}
true
false

O mesmo código, com 127 responde true e com 128 responde false.

== entre wrappers compara referências, como entre quaisquer objetos, e o resultado depende de os dois lados serem o mesmo objeto. Para valores pequenos eles costumam ser, pelo mecanismo do aprofundamento abaixo; de 128 em diante deixam de ser, e a comparação que passou em todos os testes pequenos erra em produção com os números grandes. A regra de sempre não ganha exceção para números: wrapper se compara com equals, e contagemMaior.equals(limiteMaior) responde true sempre. Uma variante dessa armadilha: um Integer nulo atribuído a um int explode com NullPointerException na conversão automática, e a linha nem parece tocar em referência.

O cache dos wrappers. A conversão automática de int para Integer reaproveita objetos prontos para os valores de −128 a 127, e por isso dois 127 são o mesmo objeto e dois 128 não. O intervalo pequeno é justamente o mais usado em teste, o que faz o == de wrapper parecer correto até o primeiro valor grande de verdade.

A armadilha prometida na seção de List também nasce dos wrappers.

O painel de senhas do açougue guarda as senhas em espera, e o cliente da senha 2 foi embora:

void main() {
    List<Integer> senhasEmEspera = new ArrayList<>();
    senhasEmEspera.add(5);
    senhasEmEspera.add(2);
    senhasEmEspera.add(8);
    senhasEmEspera.remove(2);
    IO.println(senhasEmEspera);
}
[5, 2]

A senha 2 continua na lista, e a senha 8 sumiu.

List tem dois remove: um recebe int e tira pela posição, outro recebe objeto e tira o elemento igual. O literal 2 casa exato com a versão de posição, e o compilador nem considera o autoboxing, porque entre uma conversão exata e uma que embrulha, a exata vence sempre. A linha removeu a posição 2, que era a senha 8, sem erro e sem aviso, e o painel seguiu chamando um cliente que já foi atendido. A forma de dizer “o valor 2” é embrulhar de propósito: senhasEmEspera.remove(Integer.valueOf(2)), e o teste da fila de senhas pega a diferença.

Set e HashSet: sem duplicatas

Set é o contrato do conjunto: coleção que recusa duplicata, onde duplicata é definida pelo equals. HashSet é a implementação padrão, e o nome entrega a mecânica: é a estrutura que usa o código de hash para achar a vizinhança de um elemento num salto, em vez de comparar com todos. O estoque do mercadinho não deve ter o mesmo produto duas vezes, e o Set transforma essa regra de disciplina em propriedade da estrutura: o add de um produto igual a um presente devolve false e não insere.

ChamadaEfeito
estoque.add(produto)insere; devolve false e não insere se um igual já está lá
estoque.contains(produto)pergunta pela presença, pelo hash e depois pelo equals
estoque.remove(produto)tira o elemento igual, se houver
estoque.size() / estoque.isEmpty()herdados de Collection, como em List

O capítulo 10 prometeu mostrar ao vivo o preço de sobrescrever equals sem hashCode, e o palco é este:

Um Produto com equals correto pelo código de barras e hashCode esquecido, herdado de Object:

void main() {
    Set<Produto> estoque = new HashSet<>();
    estoque.add(new Produto("7891000100103", "Café 500g", new BigDecimal("19.90")));
    IO.println(estoque.contains(new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"))));
}
false

Os dois objetos são iguais pelo equals, o produto está no conjunto, e o contains diz que não.

O HashSet procura pelo hash primeiro: calcula o do produto perguntado, salta para a vizinhança correspondente e só ali usa equals. Com o hashCode herdado, os dois cafés iguais têm hashes de identidade diferentes, a busca olha a vizinhança errada e o produto some sem sair do lugar. Pior: por coincidência de vizinhança, uma execução aqui e ali pode encontrar, e defeito intermitente escapa do teste e volta em produção. É a cláusula do contrato de equals cobrada pela estrutura que confia nela, e a correção é a de sempre: sobrescreveu equals, sobrescreve hashCode, sobre os mesmos campos.

Queue e Deque: a disciplina da fila

O mercadinho anota encomendas por telefone e as entrega na ordem em que chegaram. Queue é o contrato dessa disciplina: a fila, em que os elementos saem na ordem em que entraram, o regime FIFO (first in, first out, primeiro a entrar, primeiro a sair):

Queue<String> encomendas = new ArrayDeque<>();
encomendas.offer("Dona Marta: 2 café, 1 açúcar");
encomendas.offer("Seu Jorge: 1 queijo minas");
IO.println(encomendas.peek());
IO.println(encomendas.poll());
IO.println(encomendas.poll());
IO.println(encomendas.poll());
Dona Marta: 2 café, 1 açúcar
Dona Marta: 2 café, 1 açúcar
Seu Jorge: 1 queijo minas
null

offer entra no fim, poll sai pela frente, e peek espia a frente sem tirar; com a fila vazia, poll e peek devolvem null em vez de lançar, e o percurso de entregas termina quando o poll devolve o primeiro null. A implementação, ArrayDeque, cumpre na verdade um contrato mais rico: Deque (double-ended queue, fila de duas pontas) estende Queue com operações nas duas extremidades. Com as duas pontas, o mesmo objeto serve de pilha, a estrutura em que o último a entrar é o primeiro a sair, o regime LIFO (last in, first out), o mesmo desenho da pilha de chamadas: push empilha e pop desempilha, ambos na mesma ponta. A classe Stack, dos primeiros anos da linguagem, fazia esse papel e sobrevive em código antigo; código novo empilha com ArrayDeque.

ChamadaEfeito
encomendas.offer(pedido)entra no fim da fila
encomendas.poll()sai pela frente; null se a fila está vazia
encomendas.peek()espia a frente sem tirar; null se vazia
pilha.push(pedido)empilha, com o Deque servindo de pilha
pilha.pop()desempilha da mesma ponta

No uso real a fila aparece menos que List e Map; na entrevista, FIFO contra LIFO é pergunta de aquecimento, e a resposta agora tem código.

Map e HashMap: de chave para valor

Map é o contrato do dicionário: pares de chave e valor, com busca pela chave. É a estrutura do estoque de verdade, do código de barras para o produto, e HashMap é a implementação padrão, com a mesma mecânica de hash do HashSet aplicada às chaves:

Map<String, Produto> estoque = new HashMap<>();
estoque.put("7891000100103", cafe);
estoque.put("7891000200100", queijo);

Produto achado = estoque.get("7891000100103");
Produto ausente = estoque.get("0000000000000");
IO.println(achado.nome());
IO.println(ausente);
Café 500g
null

put grava o par, sobrescrevendo o valor se a chave já existia, e get devolve o valor ou null quando a chave não está lá: é o reencontro marcado no capítulo 5, a busca que pode legitimamente não ter resposta. Consultar antes com containsKey, ou usar getOrDefault(chave, valorPadrao), evita o NullPointerException de quem esquece o caso ausente. O idioma de contagem, presente em todo sistema e em toda entrevista, junta as duas pontas:

var vendasPorCategoria = new HashMap<Categoria, Integer>();
for (ItemDeVenda item : itensDoDia) {
    Categoria categoria = item.produto().categoria();
    int atual = vendasPorCategoria.getOrDefault(categoria, 0);
    vendasPorCategoria.put(categoria, atual + item.quantidade());
}

O var do capítulo 3, prometido para quando os nomes de tipo crescessem, entra em cena aqui, com uma ressalva: ele deduz o tipo concreto, HashMap e não Map, então serve às variáveis locais curtas e cede a vez quando a disciplina da interface importa. E o percurso idiomático dos pares usa entrySet():

for (Map.Entry<Categoria, Integer> entrada : vendasPorCategoria.entrySet()) {
    IO.println(entrada.getKey() + ": " + entrada.getValue());
}

Cada entrada carrega chave e valor juntos, sem a segunda busca que o par keySet e get faria; Entry é um tipo declarado dentro de Map, e daí vem o nome composto Map.Entry. O repertório de Map, sobre o estoque:

ChamadaEfeito
estoque.put(codigo, produto)grava o par; sobrescreve o valor se a chave já existia
estoque.get(codigo)o valor, ou null quando a chave não está lá
estoque.getOrDefault(codigo, padrao)o valor, ou o padrão, sem null no caminho
estoque.containsKey(codigo)pergunta se a chave existe
estoque.remove(codigo)tira o par da chave
estoque.keySet() / estoque.entrySet()as chaves / os pares, para percorrer
estoque.size() / estoque.isEmpty()quantos pares / se está vazio

Ordem de iteração

As vendas do dia entram no mapa na ordem em que aconteceram:

Map<String, Integer> vendas = new HashMap<>();
vendas.put("Café 500g", 3);
vendas.put("Arroz 5kg", 1);
vendas.put("Sabão em pó", 2);
vendas.put("Queijo minas", 4);
for (String nome : vendas.keySet()) {
    IO.println(nome + ": " + vendas.get(nome));
}

Em que ordem os quatro produtos saem?

Arroz 5kg: 1
Queijo minas: 4
Café 500g: 3
Sabão em pó: 2

Em ordem nenhuma que se reconheça: nem a de inserção, nem a alfabética. A ordem de iteração de um HashMap é consequência dos hashes das chaves, um detalhe interno sem promessa nenhuma, que muda entre versões e implementações. Relatório que depende da ordem de um HashMap é bug agendado, e a regra é declarar a ordem quando ela for requisito. TreeMap é a implementação que mantém as chaves ordenadas, e a ordem vem de Comparable: a interface de quem sabe se comparar com os da própria espécie, com um único método, compareTo, devolvendo negativo, zero ou positivo, exatamente o protocolo do BigDecimal. String e os wrappers já a implementam, e trocar new HashMap<>() por new TreeMap<>() na previsão faria os produtos saírem em ordem alfabética. Existe ainda o meio-termo, e ele cai em entrevista: LinkedHashMap guarda os pares com a mecânica do HashMap e lembra a ordem de inserção; na previsão acima, devolveria os quatro produtos na ordem em que entraram. A resposta de entrevista vira um trio, nos mesmos moldes da de List: HashMap por padrão, pela busca em um salto e sem promessa de ordem; LinkedHashMap quando a ordem de chegada importa; TreeMap quando a ordem das chaves é requisito do problema, pagando a busca mais lenta.

Um tipo nosso entra no TreeMap implementando a interface:

public class Produto implements Comparable<Produto> {
    @Override
    public int compareTo(Produto outro) {
        return nome.compareTo(outro.nome);
    }
}

E quando a ordem desejada não é a natural do tipo, ou o tipo não tem nenhuma, o capítulo 18 traz a peça que falta.

Coleções imutáveis

As três interfaces têm fábricas de exemplares que nascem prontos e não mudam:

List<String> diasDeFeira = List.of("terça", "sexta");
Set<String> meiosAceitos = Set.of("dinheiro", "pix", "cartão");
Map<Categoria, BigDecimal> descontosDaFeira = Map.of(Categoria.HORTIFRUTI, new BigDecimal("0.10"));

Uma coleção imutável recusa add, remove e put com um erro de execução imediato e barulhento, UnsupportedOperationException, e o erro imediato é o serviço prestado: ele transforma “ninguém deveria mexer nisto” em “ninguém consegue”. Os usos que pagam a passagem: constantes de domínio, como os dias de feira, e retorno defensivo, devolvendo List.copyOf(itens) para quem pede a lista interna, em vez da referência viva que outros alteram por fora. E a pendência do capítulo 11 fecha aqui: sequência dentro de record é List imutável, garantida no construtor compacto:

public record PesagensDoDia(List<Integer> gramas) {
    public PesagensDoDia {
        gramas = List.copyOf(gramas);
    }
}

O copyOf blinda contra a lista viva de quem chamou, o equals gerado passa a comparar conteúdo, porque List o faz, e a armadilha do componente array morre de vez.

Resta pagar o topo da árvore: o iterador, que o iterator() de Iterable entrega, é o objeto que percorre uma coleção, um elemento por vez, e é ele que o for-each usa por baixo em tudo que este capítulo mostrou. O encontro direto com ele costuma acontecer de um jeito específico: alterar a coleção no meio de um for-each costuma derrubar o programa com ConcurrentModificationException, um erro barulhento de propósito. A detecção, porém, é melhor esforço, não garantia: há casos, como remover o penúltimo elemento de um ArrayList, em que o percurso termina em silêncio, corrompido. A regra vale sem exceção de sorte: não alterar dentro do for-each, nunca; a remoção por critério tem forma própria e curta no capítulo 18.

Prática

  1. Refaça o Carrinho do capítulo 8 sobre List<ItemDeVenda>, com total() em BigDecimal, e aposente os arrays paralelos de vez.

  2. Reproduza a armadilha dos wrappers com os valores 127 e 128, conserte com equals e escreva a regra em uma frase. Depois provoque o NullPointerException do unboxing com um Integer nulo, e refaça o painel de senhas removendo a senha certa com Integer.valueOf.

  3. Modele a fila de encomendas da entrega em domicílio com Queue<String>: chegada com offer, atendimento com poll, espiada com peek, e o encerramento correto quando a fila esvazia. Depois use um ArrayDeque como pilha e escreva em uma frase a diferença entre os dois regimes.

  4. Reproduza o desaparecimento no HashSet com um Produto sem hashCode, conserte sobrescrevendo, e rode dez vezes cada versão anotando os resultados. Explique por escrito por que a versão errada poderia passar num teste.

  5. Monte o estoque como Map<String, Produto> com cinco produtos, escreva a consulta que responde “existe? qual o preço?” sem risco de NullPointerException, e o relatório de contagem por categoria com getOrDefault.

  6. Monte um TreeMap<Produto, Integer> de vendas por produto e observe a ordem seguir o compareTo por nome. Depois troque a ordem natural de Produto para preço, decidindo com compareTo de BigDecimal, e anote o que muda na saída.

  7. Escreva um método que receba List<ItemDeVenda> e devolva uma versão imutável dela, e prove com uma tentativa de add que a devolução é segura.

Ficha do capítulo

ChamadaO que faz
add / get(i) / size() / remove / containso essencial de List
add(i, e) / set(i, e) / indexOf / isEmpty / clear / addAllo resto do dia a dia de List
offer / poll / peekfila: entra no fim, sai da frente, espia; vazia devolve null
push / poppilha sobre Deque: empilha e desempilha na mesma ponta
put / get / getOrDefault / containsKeyo essencial de Map; get ausente devolve null
keySet() / entrySet()as chaves / os pares, para percorrer
List.of, Set.of, Map.of, List.copyOfcoleções imutáveis prontas
TermoDefinição
coleçãoobjeto que reúne múltiplos elementos numa única unidade
Iterableo contrato de quem pode ser percorrido; iterator(); exigência do for-each
Collectiono contrato de que List, Set e Queue herdam; Map fica à parte
List / ArrayListsequência com posição / implementação padrão, sobre array que cresce
LinkedListnós encadeados; resposta de entrevista, raramente a escolha certa
Set / HashSetconjunto sem duplicatas (por equals) / implementação por hash
Map / HashMappares chave-valor / implementação por hash, sem ordem prometida
LinkedHashMapmecânica de HashMap lembrando a ordem de inserção
TreeMapchaves mantidas em ordem, via Comparable
Queue / ArrayDequea fila FIFO: sai na ordem de chegada / implementação padrão
Dequefila de duas pontas; serve de fila e de pilha (LIFO)
Comparable / compareToa ordem natural de um tipo: negativo, zero, positivo
classe wrappero primitivo como objeto: Integer, Double e os demais
autoboxing / unboxingconversão automática primitivo→wrapper e a volta; == continua proibido
iteradoro objeto que percorre a coleção; motor do for-each
ordem de iteraçãoHashMap/HashSet não prometem nenhuma; TreeMap promete a das chaves
coleção imutávelnasce pronta e recusa alteração com erro imediato

Lambdas e interfaces funcionais

O Produto saiu do capítulo 17 sabendo se comparar por nome, e o relatório de reposição quer outra coisa: os produtos do estoque em ordem de preço. A ordem natural do Comparable é uma só por tipo, e ordem de relatório muda a cada relatório. O método sort de List aceita a ordem como argumento, e a linha que a entrega apresenta a sintaxe deste capítulo:

List<Produto> estoque = new ArrayList<>(estoqueCompleto);
estoque.sort((a, b) -> a.preco().compareTo(b.preco()));

O new ArrayList<>(colecao) copia a coleção recebida, deixando a original em paz para o relatório ordenar à vontade. O trecho (a, b) -> a.preco().compareTo(b.preco()) é uma lambda: uma função sem nome, escrita como expressão, com os parâmetros antes da seta e o resultado depois. Até aqui, todo comportamento do livro morava em métodos com nome, dentro de tipos; a lambda deixa escrever um comportamento pequeno no exato lugar onde ele é usado, e entregá-lo como argumento, como se entrega um valor. É a peça que faltava para as coleções renderem o que prometem, e o capítulo 19 é inteiro construído sobre ela.

A sintaxe e o alvo

A lambda tem três formas, da mais curta para a mais completa:

p -> p.estoque() == 0
(a, b) -> a.preco().compareTo(b.preco())
(Produto p) -> {
    IO.println(p.nome());
    return p.preco();
}

Um parâmetro dispensa parênteses; o corpo de uma expressão só dispensa chaves e return; o corpo em bloco escreve os dois. Os tipos dos parâmetros quase nunca aparecem, porque o compilador os infere, e a pergunta certa é de onde: toda lambda nasce num contexto que espera um tipo específico, e esse tipo é sempre uma interface funcional, a interface com um único método abstrato. A lambda é um atalho para implementar exatamente esse método: os parâmetros dela são os do método, o corpo dela é o corpo dele, e o compilador confere tudo contra a assinatura. O sort espera um Comparator<Produto>, cujo único método recebe dois produtos e devolve int; a lambda da abertura é uma implementação dele, sem classe, sem nome e sem cerimônia.

O MeioDePagamento tem um único método abstrato, portanto é uma interface funcional sem saber, e um teste pode fabricar um meio de pagamento de mentira numa linha:

MeioDePagamento semTaxa = compra -> compra;

A anotação @FunctionalInterface, opcional e recomendada, marca a intenção na interface e faz o compilador recusar um segundo método abstrato, no espírito do @Override.

As quatro famílias prontas

A biblioteca padrão traz interfaces funcionais de prateleira para os quatro papéis que cobrem quase todo uso, todas no pacote java.util.function:

Predicate<T> testa e responde boolean. É o tipo do critério, e o removeIf que o capítulo 17 deixou prometido o recebe:

estoque.removeIf(p -> p.estoque() == 0);

Function<T, R> transforma um valor em outro: recebe T, devolve R.

Function<Produto, String> etiqueta = p -> p.nome() + ", R$ " + p.preco();

Consumer<T> recebe um valor e não devolve nada, só age; o forEach das coleções o aceita:

estoque.forEach(p -> IO.println(p.nome()));

Supplier<T> é o espelho: não recebe nada e fornece um valor a cada chamada, pelo método get.

Um fornecedor de produto padrão, declarado e depois usado:

void main() {
    Supplier<Produto> padrao = () -> {
        IO.println("criando o produto padrão");
        return new Produto("0000000000000", "Sem cadastro", BigDecimal.ZERO);
    };
    IO.println("fornecedor pronto");
    Produto p = padrao.get();
    IO.println(p.nome());
}

Em que ordem saem as três impressões?

fornecedor pronto
criando o produto padrão
Sem cadastro

Declarar a lambda não executa o corpo: a linha do Supplier só guarda o comportamento, e “criando” aparece apenas quando alguém chama get. Lambda é um valor que carrega comportamento ainda não executado, e quem decide o momento da execução é quem a recebe; essa inversão, modesta aqui, é o motor central do capítulo 19.

Comparator e a referência de método

Comparator merece seção própria porque é a interface funcional mais usada fora das quatro famílias, e porque ela apresenta o segundo atalho da sintaxe. A fábrica Comparator.comparing monta o comparador a partir da função que extrai a chave de ordenação:

estoque.sort(Comparator.comparing(Produto::preco));
estoque.sort(Comparator.comparing(Produto::preco).reversed());
estoque.sort(Comparator.comparing(Produto::categoria)
                       .thenComparing(Produto::nome));

Produto::preco é uma referência de método: quando a lambda inteira seria só “chame este método”, os dois-pontos duplos a escrevem sem inventar parâmetros. p -> p.preco() e Produto::preco são o mesmo comportamento; a referência existe para a leitura, e vale usá-la sempre que a lambda não faz nada além da chamada. Há três formas: a referência a método de instância pelo tipo, como Produto::preco; a referência a método estático, família de Integer::parseInt e também de IO::println, e é por isso que estoque.forEach(IO::println) compila; e a referência a método de um objeto já existente, como relatorio::append com um StringBuilder em mãos. E quando a ordem desejada é a natural do Comparable, Comparator.naturalOrder() a entrega como comparador.

Parente visual dessa sintaxe, e coisa completamente diferente, é o literal de classe: Produto.class, o objeto que representa o próprio tipo em execução. Ele estreia de verdade na seção seguinte e reina no capítulo 23.

O clube de fidelidade do mercadinho guarda o saldo de pontos como int, e estorno deixa saldo negativo. O extrato ordena os saldos subtraindo:

List<Integer> saldos = new ArrayList<>(List.of(10, 2_000_000_000, -300_000_000));
saldos.sort((a, b) -> a - b);
IO.println(saldos);
[10, 2000000000, -300000000]

A lista saiu em ordem nenhuma, sem erro nenhum.

A subtração devolve o sinal certo enquanto não estoura, e o estouro exige exatamente o que o exemplo tem: sinais opostos com magnitudes grandes. 2_000_000_000 - (-300_000_000) passa do teto do int, sofre o overflow do capítulo 3, dá a volta e sai negativo, dizendo ao sort que dois bilhões vêm antes de trezentos milhões negativos. As magnitudes aqui são de laboratório; o mecanismo não é. Com estoques, não negativos por invariante desde o capítulo 7, a subtração nunca estoura; com qualquer chave que possa ser negativa, saldo, variação, diferença, ela é defeito latente, e como ninguém audita faixas a cada uso, a regra é uma só: comparador não subtrai. Integer.compare(a, b) compara sem estourar, e Comparator.comparingInt faz o mesmo por dentro.

Captura, e o efetivamente final

Uma lambda enxerga as variáveis do escopo onde nasceu, e usá-las tem nome: captura de variável.

BigDecimal teto = new BigDecimal("10.00");
estoque.removeIf(p -> p.preco().compareTo(teto) > 0);

O teto não é parâmetro da lambda: veio capturado do método em volta. A regra que governa a captura: uma variável local só pode ser capturada se for efetivamente final, isto é, se nunca for reatribuída depois de receber valor, tenha ou não a palavra final escrita. O compilador recusa a captura de variável que muda:

int contador = 0;
estoque.forEach(p -> {
    contador = contador + 1;
});
$ javac Relatorio.java
Relatorio.java:9: error: local variables referenced from a lambda expression must be final or effectively final
        contador = contador + 1;
        ^
1 error

O motivo é o tempo: a lambda pode rodar muito depois, quando o método que a criou já retornou e as variáveis locais dele já morreram com a pilha de chamadas. O que a lambda captura é o valor, congelado; permitir reatribuição criaria duas verdades sobre a mesma variável. Quando a vontade é acumular algo dentro de uma lambda, o desenho certo quase sempre é outro: ou o laço comum de sempre, ou as ferramentas de agregação do capítulo 19, que existem exatamente para isso.

O capítulo 15 fica pago aqui: o teste de recusa, escrito lá com try e fail, encolhe para a forma moderna, com a lambda segurando o código que deve explodir e o literal de classe dizendo o tipo esperado:

@Test
void recusaEstoqueNegativo() {
    assertThrows(IllegalArgumentException.class,
                 () -> new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"), -8));
}

A forma longa. Antes das lambdas, versão 8, o mesmo comparador se escrevia criando no ato uma classe sem nome que implementa a interface, a classe anônima: new Comparator<Produto>() { public int compare(...) {...} }, cinco linhas de moldura para uma de conteúdo. Código anterior a 2014 está cheio delas, e o apêndice de legado ensina a lê-las de relance; a lambda não mudou o mecanismo, mudou a grafia.

Prática

  1. Ordene o estoque de quatro jeitos: por nome com a ordem natural, por preço com comparing, por preço decrescente e por categoria com desempate de nome. Imprima cada versão com forEach e referência de método.

  2. Escreva um método filtrar(List<Produto> produtos, Predicate<Produto> criterio) que devolva a lista dos aprovados, e use-o três vezes com critérios diferentes, incluindo um capturando um teto de preço.

  3. Reproduza a armadilha da subtração com os saldos de sinais opostos, mostre a ordem errada, e conserte das duas formas. Escreva o teste de regressão que teria pegado o defeito.

  4. Converta os testes de recusa do capítulo 15 para assertThrows, e escreva um novo: ItemDeVenda com quantidade zero, conferindo também a mensagem, com o objeto exceção que assertThrows devolve.

  5. Fabrique com lambda um MeioDePagamento de teste que cobre o dobro, use-o no Caixa sem criar classe nenhuma, e explique por escrito por que isso funciona à luz da definição de interface funcional.

Ficha do capítulo

InterfaceMétodoPapel
Predicate<T>test: T → booleancritério; removeIf, filtros
Function<T, R>apply: T → Rtransformação
Consumer<T>accept: T → nadaação sobre o valor; forEach
Supplier<T>get: nada → Tfornecimento sob demanda
Comparator<T>compare: T, T → intordem; comparing, thenComparing, reversed
TermoDefinição
lambdafunção sem nome escrita como expressão: parâmetros -> corpo
interface funcionalinterface com um único método abstrato; o alvo de toda lambda
referência de métodoTipo::metodo: a lambda que só chamaria um método, sem a cerimônia
literal de classeTipo.class: o objeto que representa o tipo; reina no capítulo 23
captura de variávela lambda usando variáveis do escopo onde nasceu
efetivamente finalvariável nunca reatribuída; condição para ser capturada

Streams e Optional

O relatório de reposição do mercadinho quer os nomes dos produtos de mercearia com estoque abaixo de dez, em ordem alfabética. Com as coleções e as lambdas dos dois capítulos anteriores, o laço sai assim:

List<String> reposicao = new ArrayList<>();
for (Produto p : estoque) {
    if (p.categoria() == Categoria.MERCEARIA && p.estoque() < 10) {
        reposicao.add(p.nome());
    }
}
reposicao.sort(Comparator.naturalOrder());

Funciona, com Comparator.naturalOrder() devolvendo o comparador da ordem natural do tipo, e mistura três assuntos numa estrutura só: o critério, a transformação de produto em nome e a coleta do resultado, todos dentro do mesmo laço, com uma lista intermediária mutável de apoio. A biblioteca padrão oferece uma segunda forma de escrever exatamente isso, em que cada assunto vira uma etapa nomeada:

List<String> reposicao = estoque.stream()
        .filter(p -> p.categoria() == Categoria.MERCEARIA)
        .filter(p -> p.estoque() < 10)
        .map(Produto::nome)
        .sorted()
        .toList();

O que é um stream

Um stream é um fluxo de valores processado por uma cadeia de operações declaradas, criado a partir de uma fonte, tipicamente uma coleção, pelo método stream(). Ele não é uma coleção nova: não guarda elemento nenhum, não substitui a lista de origem e serve para uma única passagem, do começo ao fim. A cadeia inteira se chama pipeline, e tem anatomia fixa: uma fonte, zero ou mais operações intermediárias e exatamente uma operação terminal.

Uma operação intermediária transforma o fluxo e devolve outro stream, o que permite emendar a próxima: filter deixa passar quem cumpre o critério, um Predicate; map transforma cada elemento com uma Function; sorted ordena, com a ordem natural ou com um Comparator; limit(n) corta o fluxo nos primeiros n. Uma operação terminal encerra o pipeline produzindo o resultado de fato: toList() coleta numa lista imutável, count() conta, forEach consome um a um. A leitura do pipeline é a razão de ele existir: cada linha diz uma coisa, na ordem em que acontecem, e o critério, a transformação e a coleta pararam de dividir o mesmo bloco de chaves.

Avaliação preguiçosa

Um pipeline declarado com uma impressão espiã dentro do critério:

void main() {
    List<Integer> estoques = List.of(3, 40, 7, 25);
    estoques.stream()
            .filter(e -> {
                IO.println("avaliando " + e);
                return e < 10;
            });
    IO.println("relatório pronto");
}

Quantas linhas “avaliando” aparecem, e onde entra o “relatório pronto”?

relatório pronto

Nenhuma. O pipeline foi declarado e nunca executado, porque não tem operação terminal: as intermediárias são preguiçosas, e essa é a avaliação preguiçosa, o mesmo comportamento guardado sem executar do Supplier, agora em cadeia: declarar as etapas não processa nada, e é a operação terminal que liga o motor e puxa os elementos através das etapas. Acrescentar .count() ao pipeline acima faria as quatro linhas “avaliando” aparecerem antes do “relatório pronto”. As consequências práticas: efeito colateral dentro de intermediária é promessa de confusão, porque roda quando e se o terminal mandar; e um pipeline sem terminal é um engano silencioso que o compilador aceita de bom grado. Stream sem terminal não faz nada.

reduce, e o dinheiro nos streams

Somar é o exemplo canônico de agregação, e agregação em stream tem nome: reduce combina todos os elementos num único resultado, a partir de um valor inicial e de uma operação de dois em um:

BigDecimal totalDoDia = vendas.stream()
        .map(ItemDeVenda::subtotal)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

Lê-se: comece do zero e vá somando. O acumulador que o capítulo 18 proibiu de capturar aparece aqui do jeito certo: o pipeline carrega o acumulado por dentro, sem variável externa mutável.

O mesmo total, escrito por quem viu que existe uma soma pronta para números:

double totalDoDia = vendas.stream()
        .mapToDouble(item -> item.subtotal().doubleValue())
        .sum();
IO.println(totalDoDia);

Com três vendas de dez centavos, o fechamento imprime:

0.30000000000000004

O atalho converteu cada BigDecimal para double no meio do caminho, e o problema do capítulo 3 voltou inteiro: um décimo não tem escrita binária exata, a soma acumula as sobras, e o caixa fecha com diferença de fração de centavo que ninguém rastreia. O mapToDouble e o sum existem e servem para medidas; dinheiro atravessa o pipeline como BigDecimal do começo ao fim, com o reduce somando pelo método add. É a mesma regra de sempre, cobrada num lugar novo, e ela cai em teste porque a conversão escondida no meio do pipeline é fácil de não ver em revisão.

Collectors: agrupar e resumir

toList() é a coleta simples; as coletas com forma vivem na classe Collectors, uma fábrica de coletores para os resumos que relatórios pedem. O agrupamento é o mais usado deles:

Map<Categoria, List<Produto>> porCategoria = estoque.stream()
        .collect(Collectors.groupingBy(Produto::categoria));

Map<Categoria, Long> contagemPorCategoria = estoque.stream()
        .collect(Collectors.groupingBy(Produto::categoria, Collectors.counting()));

groupingBy monta um Map cujas chaves saem da função dada e cujos valores são os elementos de cada grupo; a segunda forma troca a lista de cada grupo por outro resumo, aqui a contagem. O relatório de vendas por categoria, que o capítulo 17 montou com getOrDefault num laço, encolhe para uma expressão, e as duas versões seguem corretas: o pipeline não aposenta o laço, dá a ele um concorrente mais legível quando o assunto é transformar e resumir coleções. Stream e Collectors moram no pacote java.util.stream; Optional, da seção seguinte, mora em java.util.

Optional: a ausência com tipo

Buscar o produto mais barato da mercearia tem um caso na espreita: a mercearia pode estar vazia. O stream não devolve null nem lança erro; devolve o tipo feito para isso:

Optional<Produto> maisBarato = estoque.stream()
        .filter(p -> p.categoria() == Categoria.MERCEARIA)
        .min(Comparator.comparing(Produto::preco));

Optional<T> é um recipiente de zero ou um valor: a ausência deixa de ser um null à espreita e vira parte do tipo, que o compilador obriga a encarar. O que se faz com ele é sempre uma das poucas formas:

Produto escolhido = maisBarato.orElse(produtoPadrao);
Produto exigido = maisBarato.orElseThrow(
        () -> new IllegalStateException("Mercearia sem produtos."));
maisBarato.ifPresent(p -> IO.println("Oferta: " + p.nome()));

orElse entrega o valor ou o substituto; orElseThrow entrega ou lança a exceção do Supplier, para quando a ausência é violação; ifPresent age só na presença, com um Consumer; e map transforma o conteúdo sem abri-lo, devolvendo outro Optional. IllegalStateException, prima da IllegalArgumentException, sinaliza estado inválido em vez de argumento inválido. Existe também o método get, que lança quando vazio, e a regra sobre ele é curta: quem usa get sem conferir presença reinventou o NullPointerException com mais letras.

As convenções de uso valem tanto quanto a classe. Optional nasceu para tipo de retorno, dizendo “esta busca pode não encontrar”, como o findFirst, terminal que devolve o primeiro elemento do fluxo, se houver, e o min dos streams dizem; campo Optional e parâmetro Optional são desenho torto, porque a pergunta certa nesses lugares é outra, e a resposta de entrevista resume: Optional documenta ausência possível no retorno, e o resto continua sendo modelagem.

Streams paralelos. Trocar stream() por parallelStream() divide o trabalho entre os processadores da máquina, e é tentador como toda linha única. O custo escondido é o assunto inteiro do capítulo 22: dividir trabalho cria os problemas de coordenação de lá, e pipeline paralelo com efeito colateral é receita de resultado errado intermitente. Até lá, e na maior parte do código real, o stream comum basta.

Prática

  1. Reescreva com pipeline os dois relatórios do capítulo 17: contagem de vendas por categoria e a versão agrupada com os produtos de cada uma. Compare linha a linha com as versões de laço.

  2. Reproduza a previsão da preguiça, depois acrescente .count() e anote a ordem exata das impressões. Escreva em uma frase o papel da operação terminal.

  3. Reproduza a armadilha do mapToDouble com três vendas de R$ 0,10, mostre a diferença, e escreva o teste de regressão com reduce e compareTo que a impede de voltar.

  4. Escreva maisCaroDaCategoria(List<Produto> estoque, Categoria c) devolvendo Optional<Produto>, e trate a ausência das três formas: substituto, exceção e ação condicional. Decida por escrito qual das três o relatório do mercadinho deveria usar.

  5. Monte o “top 3 mais vendidos”: some as quantidades por produto num Map, e produza a lista dos três maiores com um pipeline sobre entrySet(), usando sorted com Comparator e limit.

Ficha do capítulo

OperaçãoTipoO que faz
filter(Predicate)intermediáriadeixa passar quem cumpre o critério
map(Function)intermediáriatransforma cada elemento
sorted() / sorted(Comparator)intermediáriaordena o fluxo
limit(n)intermediáriacorta nos primeiros n
toList() / count() / forEachterminalcoleta, conta, consome
reduce(inicial, operação)terminalagrega tudo num resultado
collect(Collectors.groupingBy(...))terminalagrupa num Map, com resumo opcional
min / max / findFirstterminaldevolvem Optional
TermoDefinição
streamfluxo de valores processado por uma cadeia declarada; uma passagem só
operação intermediáriatransforma o fluxo e devolve stream; preguiçosa
operação terminalencerra o pipeline e produz o resultado; liga o motor
avaliação preguiçosanada roda até a terminal puxar os elementos
Collectorsfábrica de coletas com forma: agrupar, contar, resumir
reduceagregação: valor inicial mais operação de dois em um
Optionalrecipiente de zero ou um valor; a ausência como parte do tipo

java.time: datas sem armadilha

O queijo do mercadinho tem validade, e a pergunta do balcão é diária: quantos dias faltam? Datas são o território clássico do bug bobo, ano bissexto, mês de trinta dias, virada de ano, e a biblioteca java.time existe para que nenhuma dessas contas seja feita à mão:

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

LocalDate hoje = LocalDate.of(2026, 8, 23);
LocalDate validade = LocalDate.of(2026, 9, 30);
long diasRestantes = ChronoUnit.DAYS.between(hoje, validade);
IO.println(diasRestantes);
38

LocalDate: a data civil

LocalDate representa uma data de calendário, dia, mês e ano, sem hora e sem lugar: a data da etiqueta de validade, do feriado, do vencimento. Nasce de LocalDate.of(ano, mes, dia), que valida na entrada e recusa 31 de fevereiro com DateTimeException, uma exceção unchecked, ou de LocalDate.now(), a data corrente do relógio da máquina. A aritmética vem por métodos, plusDays, plusMonths, minusWeeks, e as comparações por isBefore, isAfter e o compareTo de Comparable, que o tipo implementa; produto vencido é validade.isBefore(hoje), sem conta manual nenhuma.

Todo tipo deste capítulo é imutável, na tradição do String e do BigDecimal: cada plus devolve um objeto novo e o original permanece. A consequência já tem cicatriz conhecida no livro, e aqui ela reaparece com fantasia de data:

LocalDate vencimento = LocalDate.of(2026, 8, 23);
vencimento.plusDays(30);
IO.println(vencimento);
2026-08-23

A segunda linha calculou um vencimento novo e o jogou fora, como o toUpperCase ignorado do capítulo 5: o fiado continua vencendo hoje, sem erro nenhum. A forma certa guarda o retorno, vencimento = vencimento.plusDays(30), e a imutabilidade paga o preço da distração com o mesmo troco de sempre: nenhum outro trecho do sistema vê a data mudar por baixo dele.

A promoção de aniversário do mercadinho, marcada para “um mês depois” do dia 31 de janeiro:

LocalDate inicio = LocalDate.of(2026, 1, 31);
IO.println(inicio.plusMonths(1));

Fevereiro de 2026 tem 28 dias. O que imprime?

2026-02-28

plusMonths avança o mês e, quando o dia não existe no destino, recua para o último dia válido, sem lançar erro e sem inventar 3 de março. É uma decisão documentada da biblioteca, boa para vencimentos e mensalidades, e o que a linha nem tenta fazer é adivinhar intenção: quem precisa de outra política, como “primeiro dia do mês seguinte”, escreve a política. A lição transferível: aritmética de calendário tem casos de borda por natureza, e a biblioteca os resolve com regras declaradas, não com aproximações.

Period, Duration e as duas perguntas de distância

Entre duas datas existem duas perguntas diferentes. “Quanto tempo civil?” responde-se com Period, o intervalo em anos, meses e dias:

Period prazo = Period.between(hoje, validade);
IO.println(prazo);
P1M7D

Um mês e sete dias, na notação compacta que o toString de Period usa: o P abre o período, e cada número carrega sua unidade. “Quantos dias corridos?” responde-se com ChronoUnit.DAYS.between, como na abertura, que devolve o total na unidade pedida, 38, sem decompor. Os dois discordam de propósito, porque medem coisas diferentes, e relatório que mistura os dois mistura unidades. Para distâncias com relógio, horas, minutos, segundos, o tipo é Duration, com a mesma notação: Duration.ofMinutes(90) imprime PT1H30M, uma hora e meia depois do T que separa a parte de tempo. LocalDateTime, data e hora juntas, é o carimbo dos registros do dia a dia, a venda das 2026-08-23T14:32, com a mesma aritmética imutável.

Instant, ZoneId e o fuso horário

Nenhum tipo acima diz onde. LocalDateTime é anotação de calendário sem localização: “23 de agosto, 14:32” acontece em momentos físicos diferentes em São Paulo e em Tóquio, e para dinheiro, auditoria e sincronização isso importa. O tipo do momento físico é o Instant: o ponto na linha do tempo global, único para o planeta inteiro, sem calendário e sem opinião sobre como humanos o chamam. Instant.now() é o agora, o mesmo agora em qualquer máquina do mundo.

A tradução entre o momento físico e o calendário humano depende do fuso horário: a regra regional que diz que horas são em cada lugar, representada por ZoneId:

import java.time.Instant;
import java.time.ZoneId;

Instant momentoDaVenda = Instant.now();
ZoneId saoPaulo = ZoneId.of("America/Sao_Paulo");
IO.println(momentoDaVenda.atZone(saoPaulo).toLocalDateTime());

O atZone aplica o fuso e revela o calendário local daquele instante. A regra de arquitetura que essa distinção sustenta é curta e cai em entrevista: registra-se o momento como Instant, e converte-se para data e hora locais só na hora de mostrar a alguém, com o fuso de quem olha. Um sistema que grava LocalDateTime sem anotar o fuso grava um registro ambíguo, e a ambiguidade cobra quando o servidor muda de país, quando dois sistemas se integram, ou na próxima mudança de regra de horário, que governos fazem quando querem.

DateTimeFormatter: mostrar e ler

O toString dos tipos usa o formato internacional, ano-mês-dia, ótimo para registro e ordenável como texto. O balcão fala outra língua, e o DateTimeFormatter traduz nas duas direções:

import java.time.format.DateTimeFormatter;

DateTimeFormatter brasileiro = DateTimeFormatter.ofPattern("dd/MM/yyyy");
IO.println(validade.format(brasileiro));
LocalDate lida = LocalDate.parse("30/09/2026", brasileiro);

O padrão usa letras com papel fixo: dd dia, MM mês, yyyy ano. A caixa das letras importa, e a dupla MM maiúsculo mês contra mm minúsculo minuto é a confusão frequente; a do YYYY, logo adiante, é pior, porque passa meses sem sintoma:

Um cupom com a data formatada por quem lembrou do padrão “quase certo”:

DateTimeFormatter doCupom = DateTimeFormatter.ofPattern("dd/MM/YYYY");
IO.println(LocalDate.of(2025, 12, 31).format(doCupom));
31/12/2026

A data é de 2025, e o cupom imprime 2026.

YYYY maiúsculo não é o ano do calendário: é o ano da semana, uma convenção em que os últimos dias de dezembro podem pertencer à primeira semana do ano seguinte, e 31 de dezembro de 2025 pertence. O formato funciona idêntico ao yyyy o ano inteiro e erra exatamente na virada do ano. A regra: ano de calendário é yyyy minúsculo, e YYYY só existe para quem trabalha deliberadamente com semanas. É o tipo de defeito que o teste de regressão do capítulo 15 fixa para sempre com uma data de dezembro.

O relógio injetável. LocalDate.now() dentro de uma regra de negócio é o Random sem semente do capítulo 6: cada execução, um valor, e o teste do “produto vence em três dias” passa hoje e falha na semana que vem. A biblioteca prevê a saída: as versões de now aceitam um Clock, um relógio substituível, e o teste entrega um relógio congelado num dia escolhido. O capítulo 24 dá o nome geral dessa manobra.

Prática

  1. Acrescente validade ao produto perecível do mercadinho: LocalDate no construtor, um método vencido(LocalDate hoje) e um diasParaVencer(LocalDate hoje). Recuse validade nula com a exceção de sempre.

  2. Escreva o relatório “vence em até sete dias” com um pipeline do capítulo 19: filtrar por proximidade da validade, ordenar pela data, mapear para nome e dias restantes.

  3. Reproduza a previsão do 31 de janeiro para os doze meses de 2026 num laço, imprimindo cada resultado, e marque quais meses recuaram o dia.

  4. Reproduza a armadilha do YYYY com cinco datas de dezembro e janeiro, identifique exatamente quais erram, e escreva o teste de regressão que trava o formato certo.

  5. Registre uma venda com Instant.now(), mostre-a no fuso de São Paulo e no de Tóquio com o mesmo formatador de data e hora, e explique por escrito por que o Instant gravado é um só.

  6. Calcule o vencimento do fiado: trinta dias corridos após a compra, e também “mesmo dia do mês seguinte”, comparando as duas políticas para uma compra feita em 31 de janeiro.

Ficha do capítulo

TipoRepresentaNasce de
LocalDatedata civil, sem hora e sem lugarof(ano, mes, dia), now(), parse
LocalDateTimedata e hora, sem lugarof(...), now()
Instanto momento físico globalInstant.now()
Perioddistância em anos, meses e diasPeriod.between(a, b)
Durationdistância com relógioDuration.ofMinutes(90)
ZoneIdo fuso horárioZoneId.of("America/Sao_Paulo")
TermoDefinição
fuso horárioa regra regional que traduz momento físico em hora local
ChronoUnitdistância total numa unidade só: DAYS.between(a, b)
DateTimeFormatterformata e interpreta datas: ofPattern("dd/MM/yyyy")
imutabilidadetodo cálculo devolve objeto novo; guardar o retorno é obrigatório
Regra prática
registrar momentoInstant; converter para local só na exibição
ano no formatoyyyy minúsculo; YYYY é ano de semana e erra na virada
now() em regra de negóciopede relógio injetável para o teste mandar no tempo

I/O, NIO.2, argumentos e jar executável

O mercadinho fecha, o processo termina, e todo o estoque montado em memória morre junto: na manhã seguinte, o sistema nasce vazio de novo. Falta a última peça de um sistema de verdade, guardar e recuperar dados em arquivo:

void main() throws IOException {
    Path arquivo = Path.of("estoque.txt");
    List<String> linhas = List.of(
        "7891000100103;Café 500g;19.90;25",
        "7891000200100;Queijo minas;39.80;8"
    );
    Files.write(arquivo, linhas);

    List<String> lidas = Files.readAllLines(arquivo);
    IO.println(lidas.size() + " produtos no arquivo");
}
$ java Persistencia.java
2 produtos no arquivo

No arquivo-fonte compacto, nada disso pede import; na moldura completa, Path e Files vêm de java.nio.file e IOException vem de java.io. Duas classes fazem quase todo o trabalho de arquivo do dia a dia. Path é o endereço: representa um caminho no disco, sem tocar nele, criado por Path.of. Files é o operário: a classe de métodos estáticos que age no endereço, escrevendo, lendo, conferindo exists, criando pastas com createDirectories, apagando. Ao lado de readAllLines, Files.readString devolve o conteúdo inteiro numa única String, o par para arquivo pequeno lido de uma vez. As duas classes pertencem ao pacote java.nio.file, apelidado NIO.2, a geração atual da biblioteca de arquivos; a geração anterior aparece no apêndice de legado.

E a assinatura do main carrega a novidade prometida no capítulo 13: quase tudo em Files lança IOException, a exceção checked do mundo externo, porque disco cheio, permissão negada e arquivo sumido não são bugs do programa, são respostas possíveis do ambiente. O compilador cobra a escolha de sempre: tratar com catch, para o mercadinho reagir, ou declarar com throws, para a decisão subir. O main de laboratório declara; o sistema de verdade trata, e a prática deste capítulo faz as duas versões.

O caminho relativo e o diretório de trabalho

O programa acima rodou dentro da pasta do projeto e funcionou. No dia seguinte, o operador o executa da pasta de cima:

$ java mercadinho/Persistencia.java

O estoque.txt continua onde sempre esteve, dentro de mercadinho/. O que acontece?

$ java mercadinho/Persistencia.java
2 produtos no arquivo

A saída é idêntica à de ontem, e a pasta de cima amanhece com um estoque.txt novo. Path.of("estoque.txt") é um caminho relativo: ele se resolve contra o diretório de trabalho, a pasta onde o comando foi digitado, não a pasta onde o programa mora. Como o programa escreve antes de ler, o Files.write criou o arquivo no lugar errado sem reclamar, e o readAllLines leu esse recém-nascido; o estoque verdadeiro, dentro de mercadinho/, nunca foi tocado, e a partir daqui existem dois arquivos divergindo em silêncio. Um programa que só lesse cairia com NoSuchFileException, uma descendente de IOException com nome honesto, e a queda seria o desfecho bom: erro visível em vez de dado duplicado. É o irmão gêmeo do classpath: “não encontrou”, e também “criou onde não devia”, quase nunca significa que o arquivo não existe, significa que o programa partiu de outro lugar. As defesas do dia a dia: imprimir arquivo.toAbsolutePath() na dúvida, e deixar o caminho vir de fora, que é a deixa dos argumentos adiante.

Charset: onde bytes viram texto

Arquivo não guarda texto: guarda bytes, e a tabela que traduz caracteres em bytes chama-se charset. O charset dominante é o UTF-8, capaz de codificar toda a tabela Unicode, e os métodos de Files o usam por padrão nas duas direções, escrever e ler. O defeito clássico dessa camada é a discordância: texto gravado com um charset e lido com outro transforma acento em sujeira, o “Café” vindo de fora que aparece como “Café” no relatório. Com Files dos dois lados não há discordância; ao receber arquivo de sistemas alheios, planilhas e afins, o charset da origem é a primeira pergunta a fazer, e as versões dos métodos que recebem um Charset explícito, como StandardCharsets.UTF_8, deixam a escolha escrita no código.

try-with-resources

Ler o arquivo inteiro com readAllLines serve até o arquivo crescer; a leitura linha a linha usa um leitor aberto, e tudo que se abre no sistema operacional precisa ser fechado, com exceção ou sem. O capítulo 13 prometeu a escrita dedicada para isso:

try (var leitor = Files.newBufferedReader(arquivo)) {
    String linha;
    while ((linha = leitor.readLine()) != null) {
        processar(linha);
    }
}

O try-with-resources declara o recurso entre parênteses e garante o fechamento ao sair do bloco, por qualquer porta: fim normal, return ou exceção subindo. O contrato por trás é a interface AutoCloseable, de um método só, close; tudo que a implementa pode morar nesses parênteses, e tudo que representa recurso do sistema a implementa. A regra é curta: todo recurso nasce dentro de um try-with-resources, e o finally de fechamento vira peça de museu, lida em código antigo e não escrita em código novo. O readLine devolvendo null no fim do arquivo é a convenção dessa família de leitores, um dos reencontros legítimos com o null.

O estoque em arquivo usa ponto e vírgula como separador, e o fornecedor novo cadastra o produto com capricho:

Produto novo = new Produto("7891000300107", "Açúcar; cristal 5kg", new BigDecimal("21.50"), 30);
gravarNoEstoque(novo);

A gravação corre sem erro. O que acontece quando o sistema carregar o arquivo na manhã seguinte?

A carga cai com NumberFormatException: o ponto e vírgula dentro do nome virou separador, a linha ganhou uma coluna a mais, e a conversão de preço recebeu “ cristal 5kg“. A queda acontece longe da causa, no dia seguinte, com o arquivo já corrompido no disco; e ela é o desfecho bom, porque bastaria o deslocamento cair numa coluna de texto para a linha carregar em silêncio, com os dados trocados e ninguém avisado. Dado corrompido em silêncio é o pior resultado de uma gravação. As saídas honestas, em ordem de esforço: proibir o separador no dado, com a invariante do construtor recusando ; no nome; escapar o separador na escrita e desfazer na leitura; ou adotar um formato com biblioteca pronta, o caminho de sistemas maiores, fora do escopo daqui. O mercadinho adota a primeira, uma linha no construtor, e um teste de regressão a vigia.

Argumentos de linha de comando, de novo

O String[] args da moldura completa do main fecha o circuito da previsão do caminho relativo: o arquivo do estoque não precisa estar cravado no fonte.

public static void main(String[] args) throws IOException {
    Path arquivo = args.length > 0 ? Path.of(args[0]) : Path.of("estoque.txt");
    // carrega e segue
}
$ java -jar mercadinho.jar /dados/mercadinho/estoque.txt

Argumento presente escolhe o arquivo, ausente cai no padrão, e o operador decide na execução, sem recompilar. É o suficiente para o mercadinho; sistemas com muitas opções adotam bibliotecas de linha de comando, mais uma fronteira anotada e não cruzada.

O jar executável

A linha acima usou java -jar, e falta pagá-la. O jar do capítulo 14 era uma caixa de bytecode para o classpath dos outros; o jar executável é a mesma caixa sabendo por onde começar. Quem guarda essa informação é o manifest: o arquivo de metadados que vive dentro de todo jar, em META-INF/MANIFEST.MF, pares de chave e valor sobre o pacote; a chave Main-Class aponta a classe cujo main o java -jar deve chamar. No Maven, isso é configuração do plugin que empacota:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifest>
                <mainClass>Loja</mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>
$ mvn package
$ java -jar target/mercadinho-1.0.jar

O nome em mainClass é o da classe com o main; quando o projeto ganhar pacotes, no capítulo 25, ele passa a ser o nome qualificado. Dois comandos separam o fonte de um arquivo único que roda em qualquer máquina com JDK: é a promessa de distribuição da JVM embrulhada para entrega. O projeto integrador fecha nesse formato.

Arquivo como stream. Files.lines(arquivo) devolve as linhas como um stream preguiçoso: as linhas são lidas do disco conforme a operação terminal puxa. O detalhe que pega: esse stream segura o arquivo aberto, é um recurso, e mora num try-with-resources como qualquer leitor; é o único stream comum do livro com essa exigência.

Prática

  1. Escreva RepositorioDeEstoqueEmArquivo com dois métodos: salvar(List<Produto>) e carregar(), no formato de quatro colunas, com try-with-resources na leitura linha a linha e IOException tratada com mensagem digna para o operador.

  2. Reproduza a previsão do caminho relativo executando de duas pastas diferentes, conserte com argumento de linha de comando, e imprima o caminho absoluto no início do programa como diagnóstico permanente.

  3. Reproduza a armadilha do separador, mostre o produto corrompido, e aplique a defesa da invariante: Produto recusa ; no nome. Escreva o teste de regressão com assertThrows.

  4. Acrescente ao repositório a criação da pasta de dados quando não existir, e o comportamento de primeiro uso: arquivo ausente devolve estoque vazio em vez de cair. Decida se carregar devolve lista ou Optional e justifique.

  5. Configure o jar executável, empacote, e rode o mercadinho de fora da pasta do projeto, passando o caminho do estoque por argumento. Confira o manifest gerado com unzip -p target/mercadinho-1.0.jar META-INF/MANIFEST.MF.

Ficha do capítulo

ChamadaO que faz
Path.of("...")o endereço; não toca o disco
Files.write / readAllLines / readStringescrita e leitura inteiras, UTF-8
Files.exists / createDirectoriesconferência e criação de pastas
Files.newBufferedReaderleitor linha a linha; recurso a fechar
Files.linesas linhas como stream preguiçoso; recurso a fechar
java -jar arquivo.jarexecuta o jar pela Main-Class do manifest
TermoDefinição
Pathum caminho no disco, absoluto ou relativo ao diretório de trabalho
Filesas operações de arquivo, em métodos estáticos
IOExceptiona checked do mundo externo: disco, permissão, ausência
charseta tabela que traduz caracteres em bytes; UTF-8 é o padrão
try-with-resourcesrecurso declarado no try, fechado por qualquer saída
AutoCloseableo contrato de um método close; habilita o try-with-resources
manifestos metadados dentro do jar; Main-Class define a entrada
jar executáveljar com Main-Class, executado por java -jar

Concorrência: executors e virtual threads

O fechamento do mês do mercadinho lê trinta arquivos de vendas do capítulo 21, um por dia, e soma tudo. A leitura é sequencial e demora; a máquina tem oito processadores e usa um. A tentação é óbvia, dividir o trabalho, e este capítulo existe porque a tentação, executada sem as regras, produz fechamento errado sem erro nenhum. Antes do desastre, a peça nova.

Thread: a segunda linha de execução

Todo programa deste livro até aqui executou numa linha só: uma instrução por vez, uma pilha de chamadas, do main ao fim. Uma thread é uma linha de execução independente dentro do mesmo processo: tem a própria pilha de chamadas e o próprio “onde estou”, e compartilha com as outras o heap, os objetos do capítulo 5. Criar uma segunda é pedir à JVM que execute um trecho, um Runnable, a interface funcional de um método run sem argumentos e sem retorno, em paralelo com quem pediu:

O fechamento divide o mês em duas quinzenas, uma thread para cada, e o main espera as duas com join:

void main() throws InterruptedException {
    Thread primeira = new Thread(() -> {
        for (int i = 1; i <= 3; i++) {
            IO.println("quinzena 1, arquivo " + i);
        }
    });
    Thread segunda = new Thread(() -> {
        for (int i = 1; i <= 3; i++) {
            IO.println("quinzena 2, arquivo " + i);
        }
    });
    primeira.start();
    segunda.start();
    primeira.join();
    segunda.join();
    IO.println("fechado");
}

Em que ordem saem as seis linhas?

Em ordem nenhuma garantida, e essa é a resposta certa: numa execução as seis podem sair intercaladas, noutra a quinzena 1 inteira antes da quinzena 2, e duas execuções seguidas podem diferir. Quem decide qual thread avança a cada momento é o escalonador do sistema, e o programa correto sob concorrência é o que está certo em todas as intercalações possíveis, não o que deu certo na intercalação de hoje. Só o “fechado” tem posição prometida, porque join faz o main esperar a thread terminar; join declara InterruptedException, a exceção checked da espera interrompida, que os main de laboratório repassam com throws. start dispara; run chamado direto seria um método comum na mesma linha de execução, sem paralelismo nenhum, um clássico de primeira semana.

A condição de corrida

O fechamento conta os itens vendidos no mês num contador compartilhado: duas tarefas, cada uma varrendo metade dos arquivos e registrando cem mil itens:

class Contador {
    int total = 0;

    void incrementar() {
        total++;
    }
}

void main() throws InterruptedException {
    Contador itens = new Contador();
    Runnable tarefa = () -> {
        for (int i = 0; i < 100_000; i++) {
            itens.incrementar();
        }
    };
    Thread primeiraMetade = new Thread(tarefa);
    Thread segundaMetade = new Thread(tarefa);
    primeiraMetade.start();
    segundaMetade.start();
    primeiraMetade.join();
    segundaMetade.join();
    IO.println(itens.total);
}

Duzentos mil itens registrados. Três execuções reais imprimiram:

127572
114925
120697

Nem duzentas mil, nem duas vezes o mesmo número. Isso é uma condição de corrida: duas ou mais threads acessando o mesmo dado, com pelo menos uma escrevendo, e o resultado dependendo da intercalação. A raiz é a atomicidade, ou a falta dela: total++ parece uma operação e são três, ler o valor, somar um, gravar de volta. Quando as duas threads leem o mesmo total antes de qualquer uma gravar, as duas gravam o mesmo resultado e um incremento evapora; em cem mil voltas isso acontece dezenas de milhares de vezes, ao acaso. Nenhuma exceção, nenhum aviso, um total diferente por execução: é o bug silencioso levado à perfeição, e a versão dele com dinheiro se chama fechamento que não bate.

synchronized

A correção clássica torna a operação indivisível:

class Contador {
    private int total = 0;

    synchronized void incrementar() {
        total++;
    }

    synchronized int total() {
        return total;
    }
}

O modificador synchronized pendura no método uma tranca: cada objeto tem uma, só uma thread por vez a segura, e as demais esperam na porta. Com a tranca, o ler-somar-gravar acontece inteiro antes de a próxima thread entrar, e o programa imprime 200000 em toda execução. O preço está no próprio desenho: dentro do trecho trancado, o paralelismo deixa de existir, e trancar demais devolve o programa lento que motivou a divisão. A tranca protege o dado; o desenho decide onde ela é inevitável.

Visibilidade e volatile

A corrida não é o único fantasma. Cada processador guarda cópias locais do que lê, e uma thread pode não enxergar o que outra escreveu:

class Fechamento {
    boolean aberto = true;

    void rodar() {
        while (aberto) {
            // registra vendas
        }
        IO.println("caixa encerrado");
    }
}

Outra thread escreve aberto = false e o laço pode nunca parar: a thread do laço, otimizada pela JVM, segue lendo a cópia velha do campo, e o “encerrado” não sai nunca. Esse é o problema da visibilidade entre threads: sem sinalização explícita, não há garantia de quando uma escrita feita por uma thread aparece para as outras. O modificador volatile no campo, volatile boolean aberto, é a sinalização mínima: toda leitura vê a última escrita, e o laço para. O que o volatile não faz é tanto quanto o que faz: ele garante visibilidade, não atomicidade, e um volatile int total com total++ continua perdendo incrementos na corrida da seção anterior. A tabela mental: synchronized para operações compostas sobre dado compartilhado; volatile para bandeiras simples que uma thread escreve e outras leem.

ExecutorService, Future e o desenho que evita tudo isso

Criar threads à mão espalha new Thread pelo sistema; o executor centraliza. Um ExecutorService é um serviço que recebe tarefas e as executa num conjunto de threads que ele administra, e um Future é o recibo de uma tarefa entregue: a promessa de um resultado que ainda não existe, resgatada com get, que espera se preciso. O fechamento do mês, na forma correta:

try (ExecutorService executor = Executors.newFixedThreadPool(8)) {
    List<Future<BigDecimal>> recibos = new ArrayList<>();
    for (Path arquivo : arquivosDoMes) {
        recibos.add(executor.submit(() -> totalDoArquivo(arquivo)));
    }

    BigDecimal totalDoMes = BigDecimal.ZERO;
    for (Future<BigDecimal> recibo : recibos) {
        totalDoMes = totalDoMes.add(recibo.get());
    }
    IO.println("Fechamento: R$ " + totalDoMes);
}

O desenho importa mais que a ferramenta: cada tarefa lê o próprio arquivo e devolve o próprio subtotal, sem tocar em nada compartilhado, e o main combina os resultados sozinho, em sequência. Não há corrida porque não há dado disputado: as threads trabalham em confinamento e se comunicam por valores imutáveis de retorno, BigDecimal do capítulo 7, e essa é a primeira regra do código concorrente que envelhece bem, não compartilhe; combine. synchronized e volatile ficam para quando o compartilhamento é inevitável. O executor entra num try-with-resources porque encerrar o serviço é fechamento de recurso como os do capítulo 21, e submit aceita tarefas que devolvem valor e lançam exceção. O get do Future declara duas exceções checked: a InterruptedException da espera e a ExecutionException, que embrulha a falha da tarefa e a entrega a quem resgatou o recibo.

Virtual threads

A thread da seção anterior, dita de plataforma, é um recurso caro do sistema operacional: milhares delas esgotam a máquina. A virtual thread é a thread barata da JVM: milhões podem existir, porque quando uma bloqueia esperando o mundo externo, a JVM a tira do processador e o empresta a outra. O executor muda numa linha:

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // uma tarefa por trabalho, sem contar quantas
}

A regra de escolha fecha o capítulo. Trabalho que espera, arquivos, rede, banco de dados, é o habitat da virtual thread, uma por tarefa, sem pool e sem contagem; trabalho que calcula sem esperar ganha pouco além do número de processadores, e o pool fixo continua certo. É esta segunda peça que explica onde a concorrência vive de verdade: um programa de terminal como o do livro tem um usuário e paraleliza por dentro quando quer velocidade, mas um servidor atende milhares de pedidos simultâneos, cada um numa thread, e todo o vocabulário deste capítulo, corrida, visibilidade, confinamento, é o idioma nativo de lá. O leitor que seguir para esse mundo vai reconhecer cada palavra.

A caixa de ferramentas pronta. O pacote java.util.concurrent tem peças prontas para os padrões comuns: AtomicInteger incrementa sem tranca e sem corrida, coleções concorrentes substituem HashMap compartilhado, e filas conectam threads produtoras a consumidoras. A regra de quem sabe o básico deste capítulo: antes de escrever synchronized, procurar a peça pronta que já resolve o padrão.

Prática

  1. Reproduza a previsão da intercalação cinco vezes e cole duas saídas diferentes lado a lado. Depois remova os join e explique o que muda no “fechado”.

  2. Reproduza a corrida do contador dez vezes, anotando os totais. Conserte com synchronized, rode dez vezes de novo, e meça com System.nanoTime o preço da tranca nas duas versões.

  3. Reproduza o laço que não enxerga a bandeira, conserte com volatile, e depois prove no contador que volatile não conserta corrida: troque synchronized por volatile e mostre os totais errados.

  4. Implemente o fechamento do mês com ExecutorService sobre os arquivos do capítulo 21: gere trinta arquivos de vendas de teste, some em paralelo com confinamento e combine com BigDecimal. Confira contra a soma sequencial.

  5. Troque o pool fixo por virtual threads no fechamento e descreva o que mudou e o que não mudou. Escreva em um parágrafo qual dos dois executores você usaria para: somar arquivos locais; consultar trezentos fornecedores pela rede.

Ficha do capítulo

PeçaO que faz
new Thread(runnable) / start / joincria, dispara e espera uma thread
synchronizedtranca por objeto: um método trancado por vez
volatilevisibilidade da escrita; não dá atomicidade
ExecutorService / submitserviço de execução; recebe tarefas
Future / geto recibo da tarefa; espera e devolve o resultado
Executors.newFixedThreadPool(n)pool de tamanho fixo, para trabalho de cálculo
Executors.newVirtualThreadPerTaskExecutor()uma virtual thread por tarefa, para trabalho que espera
TermoDefinição
threadlinha de execução com pilha própria, partilhando o heap
condição de corridaresultado dependente da intercalação de acessos, com escrita
atomicidadeoperação indivisível; total++ não é
visibilidade entre threadsgarantia de que uma escrita apareça para as outras
virtual threadthread barata da JVM; milhões, para tarefas que esperam
Regra prática
primeiro desenhonão compartilhe; confine e combine resultados imutáveis
compartilhousynchronized para operação composta; volatile para bandeira
antes de trancar à mãoprocurar a peça pronta em java.util.concurrent

Annotations, reflection e proxies

O capítulo 15 deixou uma pergunta armada: nenhuma linha do mercadinho chama CaixaTest, e os testes rodam. Alguém encontra as classes de teste, descobre quais métodos carregam @Test e os executa, sem conhecer nenhum deles de antemão. Este capítulo constrói esse alguém, com as vinte linhas prometidas, e no caminho apresenta o mecanismo por trás das ferramentas que examinam e executam código alheio: executores de teste, mapeadores, containers.

Anotação própria e retenção

Anotações existem no livro desde o @Override, sempre de prateleira. Declarar uma é curto:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface MeuTeste { }

O @interface declara a anotação, e o corpo vazio diz que ela é pura marca, sem dados. Anotações também carregam dados, declarados como métodos no corpo: um String nome(); ali dentro permitiria @MeuTeste(nome = "desconto do dinheiro"), e o leitor da anotação recupera o valor em execução. O @SuppressWarnings("unchecked") usa essa forma com um atalho: quando o único elemento se chama value, o nome pode ser omitido no uso; com qualquer outro nome, como nome, ele é obrigatório, e @MeuTeste("x") nem compila. A linha de cima é a decisão que importa aqui: a retenção define até onde a anotação sobrevive. SOURCE vive só no fonte, para o compilador e ferramentas de fonte, o destino do @Override; CLASS chega ao bytecode e para lá; RUNTIME chega à execução, visível para o mecanismo da próxima seção. Uma anotação que pretende ser lida por um executor, como @Test e como a nossa, precisa de RUNTIME declarado.

Reflexão

Reflexão é a capacidade de um programa examinar e manipular os próprios tipos em execução: perguntar a uma classe quais métodos ela tem, que anotações carrega, e invocá-los, tudo por objetos que representam essas coisas. A porta de entrada é o Class, o objeto que representa um tipo, obtido pelo literal de classe, CaixaTest.class, ou pelo getClass de qualquer objeto. Dele saem os representantes dos membros: Method para métodos, Field para campos e Constructor para construtores, cada um sabendo o próprio nome, os tipos envolvidos e as anotações que carrega; do Class também saem a superclasse e as interfaces implementadas, a hierarquia inteira disponível como dado. É o programa lendo a própria estrutura, e um encontro com isso o livro já teve sem dizer o nome: o Produto@6f2b958e é o getClass por trás do toString herdado, imprimindo o nome do tipo real.

O laboratório deste capítulo usa a moldura completa, cada classe em seu próprio arquivo: em arquivo-fonte compacto as classes viram aninhadas, e o executor adiante não as encontraria pelo caminho mostrado. O alvo é uma CaixaTest sem JUnit nenhum, com a asserção escrita à mão para o laboratório rodar com java puro, sem classpath de biblioteca:

import java.math.BigDecimal;

public class CaixaTest {
    @MeuTeste
    public void somaDuasVendas() {
        BigDecimal total = new BigDecimal("19.90").add(new BigDecimal("19.90"));
        if (!total.equals(new BigDecimal("39.80"))) {
            throw new AssertionError("esperado: <39.80> mas foi: <" + total + ">");
        }
    }

    @MeuTeste
    public void descontoDoDinheiro() {
        BigDecimal recebido = new BigDecimal("100.00").multiply(new BigDecimal("0.95"));
        if (!recebido.equals(new BigDecimal("95.00"))) {
            throw new AssertionError("esperado: <95.00> mas foi: <" + recebido + ">");
        }
    }
}

O if com throw new AssertionError(...), o erro que representa uma asserção violada, faz à mão o papel do assertEquals, e o segundo teste reencontra de propósito a escala do BigDecimal: 100.00 vezes 0.9595.0000, e equals distingue as escalas. O executor prometido cabe inteiro numa tela:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Executor {
    public static void main(String[] args) throws Exception {
        int passaram = 0;
        int falharam = 0;
        Class<?> tipo = CaixaTest.class;
        for (Method metodo : tipo.getDeclaredMethods()) {
            if (metodo.isAnnotationPresent(MeuTeste.class)) {
                Object instancia = tipo.getDeclaredConstructor().newInstance();
                try {
                    metodo.invoke(instancia);
                    IO.println("PASSOU  " + metodo.getName());
                    passaram++;
                } catch (InvocationTargetException erro) {
                    IO.println("FALHOU  " + metodo.getName() + ": " + erro.getCause().getMessage());
                    falharam++;
                }
            }
        }
        IO.println(passaram + " passaram, " + falharam + " falharam");
    }
}
$ javac *.java
$ java Executor
PASSOU  somaDuasVendas
FALHOU  descontoDoDinheiro: esperado: <95.00> mas foi: <95.0000>
1 passaram, 1 falharam

A leitura, linha a linha do que é novo. getDeclaredMethods devolve os métodos declarados na classe, em ordem não prometida, e isAnnotationPresent filtra os marcados, recebendo o literal de classe da anotação. getDeclaredConstructor().newInstance() cria a instância pelo construtor sem argumentos, uma por teste, que é exatamente a fixture nova do JUnit, agora explicada. O invoke chama o método sobre a instância; quando o método lança, a exceção chega embrulhada numa InvocationTargetException, e o getCause desembrulha a original, a AssertionError do if da CaixaTest. Vinte linhas, e o @Test, o placar e a fixture do JUnit deixaram de ser mágica: a ferramenta real tem dez anos de recursos em volta, e este núcleo no centro.

O Class<?>, de passagem, usa o wildcard no lugar honesto: um tipo desconhecido, porque o executor serve para qualquer classe.

O executor pronto, a anotação declarada, e uma distração de uma linha:

public @interface MeuTeste { }
$ javac *.java
$ java Executor
0 passaram, 0 falharam

Os dois testes continuam no arquivo, compilando, marcados. Nenhum roda.

Sem @Retention declarada, a retenção padrão é CLASS: a anotação chega ao bytecode e é invisível em execução, e isAnnotationPresent responde false para todos, sem erro, sem aviso. O placar zerado é a única pista, e é por isso que a armadilha do teste que não roda tem uma prima aqui: ferramenta baseada em anotação falha em silêncio quando a retenção está errada, e conferir o placar continua sendo a defesa.

A reflexão cobra dois preços que definem onde usá-la. O primeiro é o contrato por texto: getDeclaredConstructor() e companhia procuram por nomes e formas em execução, e o compilador não confere nada, de modo que renomear um construtor ou mudar uma assinatura quebra o código reflexivo só quando ele rodar, a categoria de erro que o livro inteiro empurra para a compilação. O segundo é que ela atravessa muros: com setAccessible, código reflexivo lê e escreve até membros private, e o encapsulamento vale contra código comum, não contra ferramentas decididas. A regra de uso sai dos dois preços: reflexão é técnica de ferramenta e de infraestrutura, executores, mapeadores, injetores, e não de regra de negócio; o estoque do mercadinho nunca precisa dela.

Proxy dinâmico

A segunda metade do arsenal fabrica objetos. Um proxy dinâmico é um objeto criado em execução que implementa interfaces escolhidas e entrega toda chamada recebida a um único ponto, o InvocationHandler: uma interface funcional cujo método recebe o proxy, o Method chamado e os argumentos, e decide o que fazer. O mercadinho quer auditoria de tudo que passa pelo pagamento, sem tocar nas classes de pagamento:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

MeioDePagamento dinheiro = compra -> compra.multiply(new BigDecimal("0.95"));

InvocationHandler auditoria = (proxy, metodo, argumentos) -> {
    IO.println("[auditoria] " + metodo.getName() + " com " + argumentos[0]);
    return metodo.invoke(dinheiro, argumentos);
};

MeioDePagamento vigiado = (MeioDePagamento) Proxy.newProxyInstance(
        MeioDePagamento.class.getClassLoader(),
        new Class<?>[] { MeioDePagamento.class },
        auditoria);

IO.println(vigiado.valorFinal(new BigDecimal("100.00")));
IO.println(vigiado.getClass().getName());
[auditoria] valorFinal com 100.00
95.0000
$Proxy0

O vigiado é um MeioDePagamento legítimo: o caixa o aceita sem saber de nada, o polimorfismo funciona, e cada chamada passa pela auditoria antes de ser delegada ao objeto real pelo invoke. A última linha mostra a certidão de nascimento: $Proxy0, uma classe que não existe em arquivo nenhum, fabricada pela JVM na hora. O nome sai sem pacote porque a interface do laboratório vive sem pacote; num projeto com pacotes, como o da prática, a mesma linha imprime o nome qualificado, jdk.proxy1.$Proxy0. O padrão, interceptar chamadas de uma interface sem alterar as implementações, é o mecanismo com que frameworks, as bibliotecas que invertem o controle e chamam o código de quem as usa, adicionam auditoria, medição de tempo e controle de acesso em volta de código alheio; o capítulo 24 usa a ideia por um ângulo mais simples, e quem encontrar um $Proxy num stack trace de framework agora sabe o que está olhando. A dupla deste capítulo fecha um circuito que vale enxergar de uma vez: anotação marca a intenção no código, reflexão encontra a marca e age, e o proxy embrulha o resultado quando o agir é interceptar. É a receita do executor deste capítulo, e a das ferramentas dessa família que o leitor vai encontrar depois deste livro, e ela cabe numa tela de cada vez.

O custo. Chamada reflexiva custa múltiplas vezes uma chamada direta, entre validações e embrulhos, e proxy soma uma indireção a cada método. Para executor de testes e montagem de sistema, irrelevante; para o método quente de um laço, proibitivo. Ferramentas sérias geram bytecode ou usam variantes otimizadas depois da primeira chamada, e a regra do usuário fica a mesma: reflexão na borda do sistema, nunca no miolo do cálculo.

Prática

  1. Monte o executor completo num projeto Maven: a anotação, duas classes de teste com métodos que passam e falham, e o placar. Depois remova o @Retention e reproduza a armadilha.

  2. Estenda o executor com @AntesDeCada: um método assim anotado roda antes de cada teste da classe, na mesma instância. Compare com o @BeforeEach do capítulo 15.

  3. Escreva um método descrever(Class<?> tipo) que imprima os métodos públicos de qualquer classe com seus tipos de retorno, e aponte-o para Produto e para String. Anote o que a saída revela que você não sabia.

  4. Crie um proxy de medição para MeioDePagamento que imprima a duração de cada chamada com System.nanoTime, empilhado por cima do proxy de auditoria. Descreva a ordem em que os dois interceptam.

  5. Prove os dois preços da reflexão: renomeie um método de teste e mostre que nada quebra até a execução; use setAccessible para ler um campo private de Produto e escreva em um parágrafo por que isso não invalida o encapsulamento como prática.

Ficha do capítulo

PeçaO que faz
@interfacedeclara uma anotação própria
@Retention(RetentionPolicy.RUNTIME)anotação visível em execução; exigida por executores
Tipo.class / getClass()o objeto Class que representa o tipo
getDeclaredMethods / isAnnotationPresentlista métodos; pergunta pela marca
getDeclaredConstructor().newInstance()cria instância pelo construtor
Method.invoke(instancia, args)chama o método representado
Proxy.newProxyInstance(loader, interfaces, handler)fabrica o proxy dinâmico
TermoDefinição
anotação própriaanotação declarada com @interface
retençãoaté onde a anotação sobrevive: fonte, bytecode ou execução
reflexãoexaminar e manipular tipos e membros em execução
proxy dinâmicoobjeto fabricado em execução que implementa interfaces e delega tudo
InvocationHandlero ponto único que recebe cada chamada do proxy

Injeção de dependência na unha

O Caixa do mercadinho precisa do estoque para vender, e a versão ingênua resolve isso por conta própria:

public class Caixa {
    private final RepositorioEmArquivo repositorio = new RepositorioEmArquivo(Path.of("estoque.txt"));

    public Resultado vender(String codigo, int quantidade) {
        Produto produto = repositorio.buscar(codigo);
        // baixa o estoque, grava, devolve o resultado
    }
}

Compila, funciona, e cobra em dois lugares que este livro já preparou. O primeiro é o teste: o CaixaTest não tem como testar essa classe sem tocar o disco de verdade, porque o new do repositório está cravado dentro dela, fora de alcance. O segundo é a troca: no dia em que o estoque for para outro lugar, o apêndice de banco de dados é esse dia, cada classe que cravou new RepositorioEmArquivo precisa ser editada. O Caixa sabe demais: além do próprio trabalho, vender, ele decidiu de onde vêm os dados e como o acesso é construído. Este capítulo tira dele essa segunda responsabilidade.

Injeção pelo construtor

O desenho tem três passos, todos já ensinados. Primeiro, a dependência vira contrato, uma interface:

public interface RepositorioDeProdutos {
    Produto buscar(String codigo);
    void salvar(Produto produto);
}

Segundo, as implementações cumprem o contrato: a RepositorioEmArquivo, que é a RepositorioDeEstoqueEmArquivo da prática do capítulo 21 rebatizada com o nome no tamanho do contrato, e uma irmã de laboratório, a RepositorioEmMemoria, um HashMap por dentro, sem disco nenhum. Terceiro, o passo que dá nome ao capítulo: o Caixa para de criar e passa a receber.

public class Caixa {
    private final RepositorioDeProdutos repositorio;

    public Caixa(RepositorioDeProdutos repositorio) {
        this.repositorio = repositorio;
    }
    // vender continua igual
}

Isso é injeção de dependência: as dependências de um objeto entram prontas por fora, pelo construtor, em vez de serem criadas por dentro. Nenhum mecanismo novo participa, é um construtor comum recebendo uma interface, e o efeito no teste é imediato:

@Test
void vendaBaixaOEstoque() {
    RepositorioDeProdutos repositorio = new RepositorioEmMemoria();
    repositorio.salvar(new Produto("7891000100103", "Café 500g", new BigDecimal("19.90"), 25));
    Caixa caixa = new Caixa(repositorio);

    caixa.vender("7891000100103", 2);

    assertEquals(23, repositorio.buscar("7891000100103").estoque());
}

O teste roda em memória, em milissegundos, sem arquivo e sem limpeza, porque a fixture escolhe a implementação. A RepositorioEmMemoria é um dublê (test double): uma implementação que cumpre o contrato só para servir ao teste; o sistema em produção recebe a de arquivo, e o Caixa é o mesmo bytecode nos dois mundos, sem saber em qual está. Testabilidade e trocabilidade são a mesma propriedade olhada de dois lados, e as duas nascem do mesmo gesto: quem usa não constrói.

Meses depois, alguém acrescenta ao Caixa um relatório de fim de dia, e na pressa resolve a dependência ali mesmo:

public void fecharDia() {
    RepositorioEmArquivo historico = new RepositorioEmArquivo(Path.of("estoque.txt"));
    // grava o resumo do dia por cima do estoque
}

O CaixaTest continua inteiro verde. O que ele deixou de proteger?

Tudo que passa por fecharDia: o new escondido ignora o repositório injetado, e o método fala com o arquivo de verdade mesmo quando o teste acredita estar em memória. Na melhor hipótese o teste do fechamento falha em máquina alheia por falta do arquivo; na pior, a suíte roda na máquina do mercadinho e escreve por cima do estoque real, um teste verde com efeito colateral em produção. Dependência escondida é exatamente isso, uma dependência que não aparece no construtor, e a disciplina que a armadilha compra é curta: se a classe precisa, o construtor declara; new de dependência dentro de método é sinal de revisão reprovada.

Inversão de controle e o grafo de objetos

O gesto tem um nome maior. Inversão de controle é o padrão em que a classe deixa de controlar a criação e a escolha das próprias dependências, e esse controle sobe para quem a monta. Cada classe fica sabendo menos, o acoplamento cai, e sobra a pergunta prática: se ninguém cria as próprias dependências, quem cria todas?

A resposta é o ponto de montagem. Todo sistema com injeção tem um lugar, tipicamente o main, onde os objetos são criados e conectados de uma vez:

public static void main(String[] args) throws IOException {
    Path arquivo = args.length > 0 ? Path.of(args[0]) : Path.of("estoque.txt");

    RepositorioDeProdutos repositorio = new RepositorioEmArquivo(arquivo);
    Caixa caixa = new Caixa(repositorio);
    CadernetaDeFiado caderneta = new CadernetaDeFiado(repositorio, new BigDecimal("200.00"));
    TelaDoTerminal tela = new TelaDoTerminal(caixa, caderneta);

    tela.rodar();
}

O que esse trecho constrói chama-se grafo de objetos: o conjunto dos objetos do sistema e das referências entre eles, montado uma vez na partida. O repositório entra em dois lugares e existe uma vez; a tela conhece o caixa, o caixa conhece o repositório, e ninguém conhece mais do que o próprio construtor pede. O main vira o único lugar do sistema onde as decisões de composição moram, e trocar o arquivo por memória, ou por qualquer implementação futura, é editar essas linhas e mais nenhuma.

Um container na unha

Num sistema grande, o main de montagem cresce, e a ordem das criações vira quebra-cabeça. O capítulo 23 deu as peças para automatizar: um container de injeção é o objeto que sabe construir o grafo sozinho, criando cada tipo pedido e resolvendo os parâmetros de construtor recursivamente. Um funcional cabe numa tela:

public class Container {
    private final Map<Class<?>, Class<?>> vinculos = new HashMap<>();
    private final Map<Class<?>, Object> prontos = new HashMap<>();

    public void vincular(Class<?> contrato, Class<?> implementacao) {
        vinculos.put(contrato, implementacao);
    }

    public void registrar(Class<?> tipo, Object valor) {
        prontos.put(tipo, valor);
    }

    public <T> T resolver(Class<T> tipo) {
        if (prontos.containsKey(tipo)) {
            @SuppressWarnings("unchecked")
            T pronto = (T) prontos.get(tipo);
            return pronto;
        }
        try {
            Class<?> alvo = vinculos.getOrDefault(tipo, tipo);
            Constructor<?>[] construtores = alvo.getDeclaredConstructors();
            if (construtores.length == 0) {
                throw new IllegalStateException("Não sei construir " + tipo.getName());
            }
            Constructor<?> construtor = construtores[0];
            Object[] argumentos = new Object[construtor.getParameterCount()];
            Class<?>[] tiposDosParametros = construtor.getParameterTypes();
            for (int i = 0; i < argumentos.length; i++) {
                argumentos[i] = resolver(tiposDosParametros[i]);
            }
            Object instancia = construtor.newInstance(argumentos);
            prontos.put(tipo, instancia);
            return tipo.cast(instancia);
        } catch (ReflectiveOperationException erro) {
            throw new IllegalStateException("Não sei construir " + tipo.getName(), erro);
        }
    }
}

A leitura: vincular registra qual implementação atende cada contrato; registrar guarda um valor já pronto, porque valor de configuração, um caminho, um limite, não se constrói por construtor; resolver olha o construtor da implementação, resolve cada parâmetro chamando a si mesmo, recursivamente sobre o grafo, cria com o newInstance e guarda. A conferência do array vazio pega o esquecimento clássico, o contrato sem vincular, cujo alvo é uma interface sem construtor nenhum; o catch de ReflectiveOperationException, a superclasse das exceções da reflexão, embrulha o resto. O main encolhe para as decisões e um pedido:

Container container = new Container();
container.vincular(RepositorioDeProdutos.class, RepositorioEmArquivo.class);
container.registrar(Path.class, Path.of("estoque.txt"));
container.registrar(BigDecimal.class, new BigDecimal("200.00"));
TelaDoTerminal tela = container.resolver(TelaDoTerminal.class);
tela.rodar();

O caixa e a caderneta dependem os dois de RepositorioDeProdutos, e cada um ganhou um acessor repositorio() de laboratório, só para a comparação:

Caixa caixa = container.resolver(Caixa.class);
CadernetaDeFiado caderneta = container.resolver(CadernetaDeFiado.class);
IO.println(caixa.repositorio() == caderneta.repositorio());

true ou false, e por quê?

true

O mapa prontos guarda cada instância criada e a reaproveita: dois pedidos do mesmo tipo recebem o mesmo objeto, a mesma referência. Esse regime tem nome, singleton: uma única instância de um tipo, compartilhada por todo o sistema, e para um repositório é o regime certo, porque dois repositórios do mesmo arquivo seriam dois caches brigando. O nome também batiza um anti-padrão famoso, o singleton por campo estático global, que recria o problema da armadilha, dependência escondida e intestável, com o agravante do estado compartilhado; a diferença entre os dois usos é quem controla, o container, visível e trocável, ou a própria classe, escondida.

O que os frameworks acrescentam. O container de uma tela resolve construtores; os de mercado somam o escaneio por anotações para dispensar o vincular manual, escopos além do singleton, configuração externa e diagnósticos de grafo. O mecanismo central, resolver construtores recursivamente e cachear, é o desta página, e quem o escreveu uma vez lê qualquer stack trace de container sem susto.

Prática

  1. Extraia a interface RepositorioDeProdutos do mercadinho, implemente a versão em memória, e converta Caixa e CadernetaDeFiado para injeção pelo construtor. Meça o tempo da suíte de testes antes e depois de trocar o repositório dos testes para a memória.

  2. Reproduza a armadilha do new escondido: faça o teste do fechamento passar enquanto grava num arquivo real, prove o efeito colateral, e conserte injetando. Escreva a regra de revisão em uma frase.

  3. Implemente o Container completo e monte o mercadinho com ele. Depois vincule RepositorioDeProdutos à memória e rode a suíte inteira pelo container, sem editar nenhuma classe de domínio.

  4. Acrescente ao container a detecção de ciclo: A que depende de B que depende de A hoje estoura a pilha de chamadas. Transforme o estouro numa IllegalStateException com a cadeia de tipos na mensagem.

  5. Escreva em um parágrafo, com o vocabulário deste capítulo, por que o relógio injetável do capítulo 20 é o mesmo padrão, e injete um Clock fixo na CadernetaDeFiado para testar o vencimento do fiado sem esperar trinta dias.

Ficha do capítulo

TermoDefinição
injeção de dependênciadependências entram prontas pelo construtor, criadas por fora
inversão de controlea criação e a escolha das dependências sobem para quem monta
grafo de objetosos objetos do sistema e as referências entre eles, montados na partida
containero objeto que constrói o grafo: resolve construtores recursivamente e cacheia
singletonuma instância única compartilhada; regime do container, anti-padrão quando estático e escondido
Regra prática
dependênciase a classe precisa, o construtor declara; new interno é dependência escondida
ponto de montagemum só: o main ou o container; decisões de composição moram juntas
testedublê em memória via injeção; disco e relógio de verdade só onde são o assunto

Projeto integrador: arquitetura e MVC no terminal

Vinte e quatro capítulos produziram todas as peças do mercadinho: produto e código de barras, caixa e pagamentos, estoque em arquivo, testes, injeção. Falta a decisão que transforma peças em sistema, e ela não é uma classe nova: é responder, para cada classe existente, onde ela mora e quem pode conhecer quem. A resposta é a estrutura final do projeto:

src/main/java/br/com/mercadinho/
├── dominio/
│   ├── Produto.java            CodigoDeBarras.java
│   ├── ItemDeVenda.java        Categoria.java
│   ├── Caixa.java              CadernetaDeFiado.java
│   ├── MeioDePagamento.java    Resultado.java
│   └── RepositorioDeProdutos.java
├── infraestrutura/
│   └── RepositorioEmArquivo.java
├── apresentacao/
│   ├── TelaDoTerminal.java
│   └── Impressora.java
└── Principal.java

As três camadas

A estrutura de pastas é a fotografia de três camadas, e cada uma tem definição de uma frase. A camada de domínio contém as regras do negócio, os tipos e as operações que existiriam em qualquer versão do mercadinho, sem saber que terminal, arquivo ou banco existem. A camada de infraestrutura contém as bordas técnicas, as classes que tocam o mundo, implementando contratos que o domínio declara: o RepositorioEmArquivo cumprindo o RepositorioDeProdutos do capítulo 24. A camada de apresentação contém a conversa com quem usa: ler comandos, mostrar resultados, e nada além disso.

O que faz das três uma arquitetura é a regra de dependência, uma só e sem exceção: as setas apontam para o domínio, e o domínio não aponta para ninguém.

flowchart LR
    A["apresentação"] --> D["domínio"]
    I["infraestrutura"] --> D

A apresentação chama o domínio; a infraestrutura implementa interfaces do domínio; e o domínio compila sem que as outras duas existam, porque nenhum import dele as menciona. Todo o investimento do livro converge nessa regra: as interfaces são o que permite a seta da infraestrutura apontar para dentro, a injeção é o que poupa o domínio de criar as bordas, e o pacote é o muro que torna a regra visível numa listagem de pastas.

A regra de dependência é verificável com o compilador, sem ferramenta nenhuma:

$ javac -d saida src/main/java/br/com/mercadinho/dominio/*.java
$ javac -d saida src/main/java/br/com/mercadinho/apresentacao/*.java

Um dos dois comandos funciona sozinho e o outro não. Qual, e por quê?

O primeiro compila: o domínio inteiro se resolve sem as outras camadas, porque não importa nada delas. O segundo falha já na primeira mensagem, error: package br.com.mercadinho.dominio does not exist, seguida da fileira de cannot find symbol, porque a apresentação importa o domínio e ele não está no classpath do comando. A assimetria é a arquitetura em forma executável, e vale como teste de saúde a qualquer momento: no dia em que o domínio parar de compilar sozinho, alguma seta inverteu, e a inversão tem nome na armadilha adiante.

MVC

Dentro dessa organização, a camada de apresentação segue um padrão com nome próprio. MVC, de Model-View-Controller, modelo, visão e controlador, é a divisão da interação em três papéis: o modelo guarda o estado e as regras; a visão apresenta o estado a quem usa; o controlador recebe o que o usuário fez e traduz em chamadas ao modelo. No mercadinho de terminal, o modelo é a camada de domínio inteira; a visão é a Impressora, que sabe transformar resultados em texto; e o controlador é a TelaDoTerminal, o laço que lê comandos e aciona o domínio:

public class TelaDoTerminal {
    private final Caixa caixa;
    private final Impressora impressora;

    public TelaDoTerminal(Caixa caixa, Impressora impressora) {
        this.caixa = caixa;
        this.impressora = impressora;
    }

    public void rodar() {
        boolean aberto = true;
        while (aberto) {
            String comando = IO.readln("mercadinho> ");
            switch (comando) {
                case "vender" -> vender();
                case "estoque" -> impressora.listar(caixa.estoqueAtual());
                case "sair" -> aberto = false;
                default -> IO.println("Comandos: vender, estoque, sair");
            }
        }
    }

    private void vender() {
        String codigo = IO.readln("Código de barras: ");
        int quantidade = Integer.parseInt(IO.readln("Quantidade: "));
        Resultado resultado = caixa.vender(codigo, quantidade);
        impressora.mostrar(resultado);
    }
}

Cada linha vem de um capítulo, com uma liberdade nova de passagem: o switch também vale como instrução, executando um braço sem produzir valor, e aceita String no seletor. Nenhuma linha decide regra de negócio: o controlador pergunta, converte e repassa; quem sabe se a venda pode acontecer é o Caixa, e quem sabe escrever “Pago: R$ 39.80” é a Impressora, com o switch de padrões sobre o Resultado. Uma honestidade de terminal: nesta interface, visão e controlador são vizinhos de porta, e a fronteira entre eles é mais fina do que o padrão sugere; o MVC aparece em forma plena quando a interface tem eventos e estado próprios, e os apêndices de interface gráfica mostram exatamente isso. O que não muda de interface para interface é o lado do modelo: regra de negócio nunca mora na tela.

O mercadinho decide dar 5% de desconto para compras acima de R$ 100,00, e a mudança é feita onde parecia mais fácil:

private void vender() {
    String codigo = IO.readln("Código de barras: ");
    int quantidade = Integer.parseInt(IO.readln("Quantidade: "));
    Resultado resultado = caixa.vender(codigo, quantidade);
    if (resultado instanceof Aprovado(BigDecimal valor)
            && valor.compareTo(new BigDecimal("100.00")) > 0) {
        IO.println("Total com desconto: R$ " + valor.multiply(new BigDecimal("0.95")));
    } else {
        impressora.mostrar(resultado);
    }
}

A tela mostra o desconto, o cliente paga o valor da tela, e o sistema segue sem nenhum erro. O que o fechamento do dia vai dizer?

Que faltou dinheiro na gaveta. O desconto existe só na impressão: o Caixa registrou a venda pelo valor cheio, o relatório do fechamento soma o valor cheio, e a caderneta, os testes e o arquivo nunca souberam do desconto, porque a regra nasceu na camada errada; o padrão de registro, de passagem, também casa dentro do instanceof, fora do case. Regra de negócio na apresentação é uma regra que as outras vias do sistema não enxergam, e o sintoma é sempre esse: divergência entre o que a tela disse e o que o sistema registrou. A correção é de endereço, não de código: a política de desconto entra no Caixa, testada no CaixaTest, e a tela volta a só mostrar o Resultado. O que pega essa família em revisão é o próprio sintoma, tela e relatório dizendo valores diferentes, e o controlador ganhando lógica e import onde só devia haver tradução; o teste de compilação da previsão fica reservado para o defeito irmão, a seta invertida, quando o domínio passa a importar as bordas.

Testes por camada

A arquitetura paga o segundo dividendo na suíte. O domínio, onde mora tudo que pode dar errado de verdade, testa-se com teste unitário puro: dublês em memória via injeção, milissegundos por teste, dezenas de casos, todo o arsenal de testes do livro. A infraestrutura testa-se pouco e de outro jeito: alguns testes que escrevem e leem um arquivo temporário de verdade, porque o assunto dela é exatamente o disco. E a apresentação quase não se testa, de propósito: um controlador que só pergunta, converte e repassa não tem regra para errar, e mantê-lo fino é o que mantém essa conta verdadeira. A distribuição é o inverso da pirâmide de dificuldade: quanto mais perto do mundo externo, menos testes e mais lentos; quanto mais perto do domínio, mais testes e mais rápidos.

Montagem, empacotamento e o sistema de pé

Principal.java é o ponto de montagem do capítulo 24, e o jar executável é o do capítulo 21, com um ajuste: agora que as classes têm pacote, o mainClass do pom.xml passa a ser o nome qualificado, br.com.mercadinho.Principal. A sessão final é a razão do livro:

$ mvn package
[INFO] Tests run: 34, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
$ java -jar target/mercadinho-1.0.jar dados/estoque.txt
mercadinho> estoque
7891000100103  Café 500g      R$ 19.90  25 un
7891000200100  Queijo minas   R$ 39.80   8 un
mercadinho> vender
Código de barras: 7891000100103
Quantidade: 2
Pago: R$ 39.80
mercadinho> sair
$

Um arquivo, uma máquina com JDK, e o mercadinho atende. O sistema que essa sessão mostra é pequeno; a estrutura dele é a mesma de sistemas grandes, e essa é a aposta do capítulo: quem sabe onde cada coisa mora num sistema de quinze classes sabe procurar num de mil e quinhentas. Os três apêndices esticam exatamente este projeto, cada um por uma borda: o primeiro troca a infraestrutura de arquivo por um banco de dados, e a regra de dependência garante que o domínio não saberá; os outros dois trocam a apresentação de terminal por uma interface gráfica, onde o MVC mostra a forma plena. O domínio, coração do sistema e do livro, atravessa os três sem uma linha editada.

Nomes de mercado. A organização deste capítulo aparece na literatura com vários nomes, arquitetura em camadas, portas e adaptadores, arquitetura hexagonal, cada um com refinamentos próprios sobre o mesmo núcleo: domínio no centro, dependências apontando para dentro, bordas trocáveis. Quem dominou a versão de três pastas lê qualquer um dos diagramas famosos como variação, não como novidade.

Prática

O projeto integrador é a prática, e os itens são incrementos completos, cada um atravessando as camadas pelo caminho certo.

  1. Monte o projeto na estrutura deste capítulo, migre todas as classes dos capítulos anteriores para as camadas, e faça o teste de saúde da previsão passar: o domínio compila sozinho.

  2. Acrescente o comando repor, de ponta a ponta: leitura na tela, regra e validação no domínio, persistência na infraestrutura, e os testes no lugar certo de cada parte.

  3. Implemente a política de desconto da armadilha no lugar certo, com o Resultado ganhando a informação do desconto aplicado, o switch da Impressora exibindo, e o teste provando que relatório e tela dizem o mesmo valor.

  4. Acrescente o comando relatorio: total do dia, vendas por categoria e os três mais vendidos, tudo com pipelines de stream sobre os dados do domínio.

  5. Acrescente o fiado como meio de pagamento no fluxo de venda, com a caderneta, o limite e a exceção de limite estourado atravessando as camadas até virarem uma mensagem digna na tela.

  6. Rode o sistema inteiro pelo jar em outra pasta e em outra máquina se puder, com o estoque por argumento, e escreva um parágrafo sobre o que precisou de ajuste e o que rodou intocado.

Ficha do capítulo

TermoDefinição
camada de domínioas regras do negócio; não conhece terminal, disco nem banco
camada de infraestruturaas bordas técnicas; implementa contratos do domínio
camada de apresentaçãoa conversa com quem usa; pergunta, converte, repassa, mostra
MVCmodelo guarda estado e regras; visão mostra; controlador traduz entrada em chamadas
Regra prática
dependênciasetas para o domínio; o domínio não aponta para ninguém
teste de saúdeo domínio compila sozinho; quando parar, uma seta inverteu
regra de negócionunca na tela; divergência tela-relatório é o sintoma
testesmuitos e rápidos no domínio; poucos e reais na infraestrutura; apresentação fina

JDBC: o mercadinho num banco de dados

Este apêndice ainda não foi escrito, e entra numa próxima versão do livro. O corpo do livro, os capítulos 1 a 25, está completo.

Este apêndice troca a persistência em arquivo do projeto integrador por um banco de dados relacional, com JDBC: conexão, PreparedStatement, ResultSet, transações e o mapeamento entre tabelas e o domínio. A regra de dependência do capítulo 25 é o que está em teste aqui, e o domínio atravessa a troca sem uma linha editada.

Dois pré-requisitos ficam declarados desde já, porque o apêndice não os ensina: SQL e um banco de dados em funcionamento, configurado com docker-compose ou sem ele.

JavaFX: janela, cena e o thread de UI

Este apêndice ainda não foi escrito, e entra numa próxima versão do livro. O corpo do livro, os capítulos 1 a 25, está completo.

Este apêndice abre a primeira janela do mercadinho: a estrutura de Application, Stage e Scene, os controles básicos, o arranjo dos elementos na tela e a regra que governa toda interface gráfica, a de que existe um thread próprio para a interface e o que pode ser feito nele.

JavaFX: eventos, properties e binding

Este apêndice ainda não foi escrito, e entra numa próxima versão do livro. O corpo do livro, os capítulos 1 a 25, está completo.

Este apêndice trata do que faz uma janela responder: eventos de teclado e de mouse, as properties observáveis do JavaFX e o binding, a ligação que mantém a tela em dia com o valor que ela exibe, sem código de atualização espalhado.

JavaFX: ligando a tela ao domínio

Este apêndice ainda não foi escrito, e entra numa próxima versão do livro. O corpo do livro, os capítulos 1 a 25, está completo.

Este apêndice fecha a interface gráfica ligando a janela ao domínio do projeto integrador, com o MVC do capítulo 25 na forma plena que o terminal não permitia: o modelo intocado, a visão declarada, e o controlador traduzindo interação em chamadas de negócio.

Java legado: lendo o código que existe

Este apêndice ainda não foi escrito, e entra numa próxima versão do livro. O corpo do livro, os capítulos 1 a 25, está completo.

Este apêndice ensina a ler o que o livro deliberadamente não ensinou a escrever, e que existe aos milhões em código de produção: System.out e Scanner, Date e Calendar, StringBuffer, classes anônimas, tipos genéricos crus e o finally de fechamento. Nada disso é para escrever; tudo isso é para reconhecer.