A primeira aula da Fase 5: como organizar o código em pacotes, como a JVM encontra as classes, o que é um JAR e como usar código de outros projetos.
A Fase 4 terminou com uma pergunta: como reaproveitar código entre programas? Até aqui, todos os exemplos viveram em um ou dois arquivos, numa pasta, compilados e executados com java Arquivo.java. Isso funciona para aprender, e não para construir sistemas. Esta aula apresenta os mecanismos que o Java usa para organizar e distribuir código: os pacotes, que agrupam classes e dão nomes únicos a elas; o classpath, que diz à JVM onde procurar as classes; e o JAR, o arquivo que empacota um programa ou uma biblioteca. Você vai montar um pequeno projeto de lista de tarefas, que acompanha as próximas aulas da fase, e usar dentro dele uma biblioteca própria, empacotada à parte.
Pacotes: sobrenomes para as classes
Um pacote é um grupo de classes relacionadas, com um nome. Você usa pacotes desde a primeira aula: String e Math estão em java.lang, ArrayList em java.util, Path em java.nio.file. Pacotes resolvem dois problemas.
Nomes únicos. Existem milhares de bibliotecas Java, e várias têm uma classe chamada Usuario ou Config. O nome completo de uma classe inclui o pacote, o nome qualificado: java.util.List e java.awt.List são classes diferentes, que convivem sem conflito. Para garantir nomes únicos no mundo inteiro, a convenção é começar o pacote pelo domínio de internet invertido da organização: uma empresa com o site exemplo.com.br usa pacotes que começam com br.com.exemplo. Os nomes de pacote são sempre em minúsculas.
Organização e visibilidade. Um pacote agrupa classes que trabalham juntas. E ele é uma fronteira de acesso: o nível sem modificador da tabela de "O que Fica Dentro, o que Fica Fora", o acesso de pacote, permite que as classes do mesmo pacote se enxerguem, e esconde tudo das outras.
A declaração do pacote é a primeira linha do arquivo, e a estrutura de pastas precisa espelhar o nome do pacote. O projeto de tarefas fica assim:
tarefas/
└── src/
└── br/com/exemplo/tarefas/
├── modelo/
│ └── Tarefa.java
├── servico/
│ └── ListaDeTarefas.java
└── app/
└── Principal.java
O modelo, um record com uma regra de validação e duas operações:
package br.com.exemplo.tarefas.modelo;
import java.time.LocalDate;
import java.util.Objects;
public record Tarefa(int id, String titulo, LocalDate prazo, boolean concluida) {
public Tarefa {
Objects.requireNonNull(titulo, "titulo");
if (titulo.isBlank()) {
throw new IllegalArgumentException("O título não pode ser vazio");
}
}
public Tarefa concluir() {
return new Tarefa(id, titulo, prazo, true);
}
public boolean atrasada(LocalDate hoje) {
return !concluida && prazo != null && prazo.isBefore(hoje);
}
}
O serviço, que guarda as tarefas e oferece as operações:
package br.com.exemplo.tarefas.servico;
import br.com.exemplo.tarefas.modelo.Tarefa;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class ListaDeTarefas {
private final List<Tarefa> tarefas = new ArrayList<>();
private int proximoId = 1;
public Tarefa adicionar(String titulo, LocalDate prazo) {
Tarefa nova = new Tarefa(proximoId++, titulo, prazo, false);
tarefas.add(nova);
return nova;
}
public Optional<Tarefa> concluir(int id) {
for (int i = 0; i < tarefas.size(); i++) {
if (tarefas.get(i).id() == id) {
Tarefa concluida = tarefas.get(i).concluir();
tarefas.set(i, concluida);
return Optional.of(concluida);
}
}
return Optional.empty();
}
public List<Tarefa> pendentes() {
return tarefas.stream().filter(t -> !t.concluida()).toList();
}
public List<Tarefa> atrasadas(LocalDate hoje) {
return tarefas.stream().filter(t -> t.atrasada(hoje)).toList();
}
int total() {
return tarefas.size();
}
}
E o programa principal:
package br.com.exemplo.tarefas.app;
import br.com.exemplo.tarefas.modelo.Tarefa;
import br.com.exemplo.tarefas.servico.ListaDeTarefas;
import java.time.LocalDate;
import static java.time.temporal.ChronoUnit.DAYS;
public class Principal {
public static void main(String[] args) {
LocalDate hoje = LocalDate.of(2026, 9, 25);
ListaDeTarefas lista = new ListaDeTarefas();
lista.adicionar("Estudar pacotes", hoje.plusDays(2));
lista.adicionar("Revisar genéricos", hoje.minusDays(3));
lista.adicionar("Fazer os exercícios", null);
lista.concluir(1);
IO.println("Pendentes:");
for (Tarefa t : lista.pendentes()) {
IO.println(" #" + t.id() + " " + t.titulo());
}
for (Tarefa t : lista.atrasadas(hoje)) {
IO.println("Atrasada há " + DAYS.between(t.prazo(), hoje) + " dias: " + t.titulo());
}
}
}
import
Uma classe de outro pacote pode ser usada pelo nome qualificado, como java.time.LocalDate, o que fica cansativo rapidamente. O import permite usar o nome simples. As formas:
import java.time.LocalDate;importa uma classe.import java.util.*;importa todas as classes públicas de um pacote, mas não as de subpacotes.java.util.*não incluijava.util.function, porque, para o Java, subpacotes são pacotes independentes, sem relação especial.import static java.time.temporal.ChronoUnit.DAYS;importa um membro estático, que passa a ser usado sem o nome da classe:DAYS.between(...).
Importar não copia nem carrega nada. É só uma abreviação para o compilador. As classes de java.lang são importadas automaticamente, e as do mesmo pacote não precisam de importação. Os arquivos compactos das aulas anteriores importavam automaticamente toda a biblioteca básica, e por isso nunca precisaram de import.
Quando dois pacotes têm classes com o mesmo nome, só uma pode ser importada pelo nome simples, e a outra é usada pelo nome qualificado. O caso clássico é java.util.Date e java.sql.Date, o exercício 2.
A regra de visibilidade se aplica aqui. O método total() de ListaDeTarefas não tem modificador, e por isso só é visível no pacote servico. Uma classe de outro pacote que tente usá-lo não compila:
Espiao.java:7: error: total() is not public in ListaDeTarefas; cannot be accessed from outside package
Compilando um projeto com pacotes
Com várias pastas, o comando de compilação muda. Dentro da pasta tarefas:
javac -d out $(find src -name "*.java")
A opção -d out diz ao compilador onde gravar os arquivos .class, e ele recria a estrutura de pastas dos pacotes (o $(find ...) é um recurso do terminal Linux e macOS, que lista todos os arquivos-fonte. No Windows, liste os arquivos ou use a ferramenta de construção da próxima aula):
out/br/com/exemplo/tarefas/app/Principal.class
out/br/com/exemplo/tarefas/modelo/Tarefa.class
out/br/com/exemplo/tarefas/servico/ListaDeTarefas.class
Separar os fontes (src) dos compilados (out) é uma prática universal. Os .class são gerados e podem ser apagados e recriados a qualquer momento. Por isso, eles nunca vão para o controle de versão.
Para executar, passa-se o nome qualificado da classe e a pasta onde começam os compilados:
java -cp out br.com.exemplo.tarefas.app.Principal
Pendentes:
#2 Revisar genéricos
#3 Fazer os exercícios
Atrasada há 3 dias: Revisar genéricos
O classpath
A opção -cp (abreviação de -classpath) define o classpath: a lista de lugares onde a JVM procura as classes. Quando o programa usa a classe br.com.exemplo.tarefas.modelo.Tarefa, a JVM transforma o nome num caminho, br/com/exemplo/tarefas/modelo/Tarefa.class, e procura esse arquivo em cada lugar do classpath, na ordem. As classes da biblioteca padrão vêm da própria JVM e não precisam estar no classpath.
Os erros de classpath estão entre os mais comuns do Java, e os dois principais têm nomes parecidos e causas diferentes. Executar sem o classpath certo, ou com o nome errado da classe, produz:
Error: Could not find or load main class br.com.exemplo.tarefas.app.Principal
Caused by: java.lang.ClassNotFoundException: br.com.exemplo.tarefas.app.Principal
O mesmo acontece ao passar o nome simples, java -cp out Principal, porque a classe se chama, de verdade, br.com.exemplo.tarefas.app.Principal.
O outro erro aparece quando o programa compilou com uma classe disponível, mas ela falta durante a execução:
Exception in thread "main" java.lang.NoClassDefFoundError: br/com/exemplo/textos/Textos
at br.com.exemplo.relatorio.Gerador.main(Gerador.java:8)
Caused by: java.lang.ClassNotFoundException: br.com.exemplo.textos.Textos
A mensagem diz que a classe Gerador foi compilada contra uma classe Textos que, agora, não está em lugar nenhum do classpath. É o sintoma típico de uma dependência esquecida, como no exemplo a seguir.
JAR: um programa num arquivo
Distribuir um programa como uma pasta cheia de .class é incômodo. Um JAR (Java ARchive) é um único arquivo que contém as classes, com a estrutura de pastas, e mais alguns metadados. Por dentro, um JAR é simplesmente um arquivo ZIP, que qualquer programa de descompactação abre. A ferramenta jar do JDK o cria:
jar --create --file tarefas.jar --main-class br.com.exemplo.tarefas.app.Principal -C out .
-C out . diz "entre na pasta out e inclua tudo". O conteúdo, listado com jar tf tarefas.jar, inclui uma entrada para cada pasta:
META-INF/
META-INF/MANIFEST.MF
br/
br/com/
br/com/exemplo/
br/com/exemplo/tarefas/
br/com/exemplo/tarefas/app/
br/com/exemplo/tarefas/app/Principal.class
br/com/exemplo/tarefas/modelo/
br/com/exemplo/tarefas/modelo/Tarefa.class
br/com/exemplo/tarefas/servico/
br/com/exemplo/tarefas/servico/ListaDeTarefas.class
O arquivo META-INF/MANIFEST.MF é o manifesto, com metadados do JAR:
Manifest-Version: 1.0
Created-By: 25.0.4.1 (Ubuntu)
Main-Class: br.com.exemplo.tarefas.app.Principal
A linha Main-Class, criada pela opção --main-class, diz qual classe tem o main. Com ela, o JAR é executável:
java -jar tarefas.jar
Um JAR pode também ser uma biblioteca, sem main, feita para ser usada por outros programas. É a forma como todo código reutilizável em Java é distribuído.
Usando uma biblioteca
Para ver a reutilização acontecendo, vamos criar uma pequena biblioteca de utilitários de texto, num projeto separado, textos:
package br.com.exemplo.textos;
import java.text.Normalizer;
public final class Textos {
private Textos() { }
public static String semAcentos(String texto) {
return Normalizer.normalize(texto, Normalizer.Form.NFD).replaceAll("\\p{M}", "");
}
public static String slug(String texto) {
return semAcentos(texto).strip().toLowerCase()
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("(^-|-$)", "");
}
}
Normalizer separa cada letra acentuada em duas partes, a letra base e o acento, e a expressão regular \p{M} remove os acentos, uma técnica comum. slug transforma um texto em algo adequado para nomes de arquivos e endereços de páginas. Compilado e empacotado:
cd textos
javac -d out src/br/com/exemplo/textos/Textos.java
jar --create --file textos.jar -C out .
Outro projeto, relatorio, usa a biblioteca:
package br.com.exemplo.relatorio;
import br.com.exemplo.textos.Textos;
public class Gerador {
public static void main(String[] args) {
String titulo = "Relatório de Ações — Março/2026";
IO.println(Textos.slug(titulo) + ".pdf");
}
}
Compilar sem informar onde está a biblioteca não funciona:
Gerador.java:3: error: package br.com.exemplo.textos does not exist
O JAR da biblioteca precisa estar no classpath da compilação e da execução:
cd relatorio
javac -cp ../textos/textos.jar -d out src/br/com/exemplo/relatorio/Gerador.java
java -cp out:../textos/textos.jar br.com.exemplo.relatorio.Gerador
relatorio-de-acoes-marco-2026.pdf
O classpath da execução tem dois lugares, separados por : no Linux e no macOS, e por ; no Windows: a pasta com as classes do próprio programa e o JAR da biblioteca. Esquecer o JAR na execução produz o NoClassDefFoundError da seção anterior.
O inferno do classpath
Com uma biblioteca, montar o classpath à mão é tranquilo. Agora imagine um projeto real. Uma aplicação web típica usa algumas dezenas de bibliotecas diretamente, e cada uma usa outras: um framework web depende de bibliotecas de JSON, de log e de servidor HTTP, que por sua vez dependem de outras. O total costuma passar de cem JARs. Para cada um, alguém precisaria:
- descobrir que ele é necessário, inclusive os que só aparecem como dependências de dependências;
- encontrar e baixar a versão certa;
- resolver conflitos, quando duas bibliotecas exigem versões diferentes de uma terceira;
- montar o classpath, com a ordem certa, para compilar, testar e executar;
- repetir tudo em cada computador da equipe e no servidor.
Nos primeiros anos do Java era assim, e o problema ganhou o apelido de inferno do classpath (classpath hell): programas que funcionavam na máquina de um desenvolvedor e não na de outro, porque um JAR estava numa versão diferente, ou duas versões da mesma biblioteca estavam no classpath e a JVM usava a primeira que encontrasse.
Há dois problemas separados aqui. O primeiro é automatizar a obtenção das bibliotecas e a construção do projeto: é o trabalho das ferramentas de build, que também vão padronizar a estrutura de pastas que montamos à mão. O segundo é mais profundo: o classpath é uma lista plana, sem fronteiras, em que qualquer classe pública de qualquer JAR é visível para todas as outras, e em que nada impede que falte uma peça até o programa quebrar em execução. A resposta do próprio Java a esse segundo problema é o sistema de módulos.
Fontes e leituras recomendadas
- Oracle, The Java Tutorials, "Packages": https://docs.oracle.com/javase/tutorial/java/package/index.html. A criação e o uso de pacotes, com as convenções de nomes e a relação com as pastas.
- Documentação da ferramenta
jar(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/specs/man/jar.html. Todas as opções da ferramenta, incluindo a atualização e a extração de JARs. - Documentação do comando
java(Java SE 25): https://docs.oracle.com/en/java/javase/25/docs/specs/man/java.html. As opções de classpath, módulos e execução de JARs, e o formato do separador em cada sistema. - JAR File Specification: https://docs.oracle.com/en/java/javase/25/docs/specs/jar/jar.html. O formato do JAR e de todos os atributos do manifesto.
- The Java Language Specification, Java SE 25, capítulo 7, "Packages and Modules": https://docs.oracle.com/javase/specs/jls/se25/html/jls-7.html. As regras formais de pacotes e importações, incluindo a resolução de nomes.
Exercícios
Exercício 1
Uma classe Calculadora pertence ao pacote br.edu.escola.matematica. Diga em que pasta o arquivo-fonte deve ficar, qual é a primeira linha do arquivo, e quais comandos compilam e executam a classe, supondo que ela tenha um main.
Ver resposta
✓ Resposta: O arquivo deve ficar em src/br/edu/escola/matematica/Calculadora.java, com a pasta src como raiz dos fontes. A primeira linha é package br.edu.escola.matematica;. Para compilar, a partir da pasta do projeto: javac -d out src/br/edu/escola/matematica/Calculadora.java, que grava o compilado em out/br/edu/escola/matematica/Calculadora.class. Para executar: java -cp out br.edu.escola.matematica.Calculadora, com o nome qualificado. Três erros comuns: executar com o nome simples (Calculadora), passar o caminho do arquivo em vez do nome da classe (out/br/edu/.../Calculadora.class) e apontar o classpath para a pasta errada, como out/br. O classpath aponta para onde começam os pacotes.
Exercício 2
Um programa precisa usar ao mesmo tempo java.util.Date e java.sql.Date. Mostre como declarar as importações e as variáveis.
Ver resposta
✓ Resposta:
import java.util.Date;
public class Conversor {
Date agora = new Date(); // java.util.Date
java.sql.Date dataDoBanco = new java.sql.Date(agora.getTime()); // nome qualificado
}
Só uma das duas pode ser importada pelo nome simples. A outra é escrita com o nome qualificado sempre que usada. Importar as duas, com import java.util.Date; e import java.sql.Date;, é erro de compilação. Importar ambos os pacotes com * compila, mas qualquer uso de Date pelo nome simples fica ambíguo e dá erro. A escolha de qual importar é por frequência: a mais usada fica com o nome curto. Em código novo, as duas classes são substituídas pelas do java.time, como vimos em "Tempo, Dinheiro e Padrões", mas o problema de nomes iguais em pacotes diferentes é geral e aparece com frequência entre bibliotecas.
Exercício 3
Explique a diferença entre ClassNotFoundException, ao executar java -cp out Principal, e NoClassDefFoundError, ao executar o Gerador sem o JAR da biblioteca.
Ver resposta
✓ Resposta: No primeiro caso, a JVM recebeu o pedido de carregar uma classe pelo nome, e não a encontrou no classpath: Principal não existe, porque o nome real inclui o pacote. É uma ClassNotFoundException, a exceção lançada quando uma busca explícita por nome falha, que também acontece com Class.forName na reflexão da aula anterior. No segundo caso, a classe Gerador foi encontrada e começou a executar, e ela depende de Textos, contra a qual foi compilada. Quando a execução chega ao ponto que usa Textos, a JVM tenta carregá-la e não consegue. Isso é um NoClassDefFoundError, um Error, e não uma Exception, porque indica que o ambiente de execução está inconsistente com o da compilação, e a causa aparece em "Caused by". Na prática, ClassNotFoundException costuma ser um nome ou classpath errado na hora de iniciar, e NoClassDefFoundError, uma dependência que faltou ao montar o classpath.
Exercício 4
O método total() de ListaDeTarefas tem acesso de pacote. Explique por que uma classe em br.com.exemplo.tarefas.app não o enxerga, e dê um exemplo de situação em que esse nível de acesso é útil.
Ver resposta
✓ Resposta: Para o Java, br.com.exemplo.tarefas.app e br.com.exemplo.tarefas.servico são pacotes diferentes, sem relação especial, apesar do começo do nome em comum. O acesso de pacote só vale para classes exatamente do mesmo pacote. Isso é útil para métodos que várias classes de um mesmo pacote precisam compartilhar, mas que não devem fazer parte da interface pública. O uso mais comum é em testes: a convenção, que você vai ver em "Provando que Funciona", é colocar a classe de testes no mesmo pacote da classe testada, embora numa pasta separada, e assim os testes podem verificar detalhes internos, como total(), sem que eles sejam expostos ao resto do programa. Outro uso é esconder classes auxiliares de uma biblioteca: uma classe sem public só existe dentro do pacote.
Exercício 5
Monte, a partir das pastas da aula, um JAR executável do relatorio e execute-o. Por que java -jar relatorio.jar falha com NoClassDefFoundError, mesmo com o textos.jar na mesma pasta, e como resolver?
Ver resposta
✓ Resposta: Para criar: jar --create --file relatorio.jar --main-class br.com.exemplo.relatorio.Gerador -C out .. Ao executar com java -jar relatorio.jar, a JVM ignora a opção -cp e a variável de ambiente CLASSPATH, e usa como classpath só o próprio JAR, mais o que o manifesto declarar. Estar na mesma pasta não basta, porque a JVM não procura JARs vizinhos. Há três soluções. A primeira é executar sem -jar, montando o classpath: java -cp relatorio.jar:textos.jar br.com.exemplo.relatorio.Gerador. A segunda é declarar a dependência no manifesto com o atributo Class-Path: textos.jar, com caminhos relativos ao JAR, e aí java -jar funciona se os dois estiverem juntos. A terceira é criar um JAR único com as classes das duas bibliotecas dentro, um "fat JAR". Ferramentas de build fazem as duas últimas automaticamente, e a trilha de web usa a terceira.