Do Arquivo Solto ao Projeto — Pacotes, Classpath e JARs

Do Arquivo Solto ao Projeto — Pacotes, Classpath e JARs

Como organizar código Java em pacotes, usar import, compilar com javac -d, montar o classpath e empacotar programas e bibliotecas em JARs executáveis. Explica a diferença entre ClassNotFoundException e NoClassDefFoundError e por que o classpath à mão vira um inferno.
Java

• • 16 min de leitura

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 inclui java.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

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.

Comentários

Mais em Java

Extensão — Do Laptop ao Servidor: Docker, nginx e Deploy
Extensão — Do Laptop ao Servidor: Docker, nginx e Deploy

As Trilhas da Ilha saem do localhost: imagem Docker em dois estágios com…

Uma Linguagem para Torradeiras que Conquistou o Mundo — a História do Java
Uma Linguagem para Torradeiras que Conquistou o Mundo — a História do Java

Como um projeto da Sun para eletrodomésticos fracassou, foi salvo pela web e…

Comportamento como Valor — Lambdas e Interfaces Funcionais
Comportamento como Valor — Lambdas e Interfaces Funcionais

Como passar comportamento como argumento com expressões lambda em Java…