Blog

Pacotes, módulos e JARs em Java sem Maven

Monte um projeto Java de dois módulos só com javac, jar e java. Veja como pacotes, imports, o classpath, arquivos JAR e o module-info.java se encaixam, e o que o Maven e o Gradle acrescentam por cima.

Até aqui, todo programa desta série coube num único arquivo chamado Main.java. Código Java de verdade é dividido em pacotes, compilado numa pasta de arquivos .class, empacotado em arquivos JAR e iniciado a partir de um classpath ou de um module path. As ferramentas de build fazem tudo isso por você, e é por isso que a maioria dos desenvolvedores nunca viu isso ser feito à mão.

Este post faz tudo à mão. Ele cobre pacotes e imports, o classpath e seus dois erros clássicos, arquivos JAR e o sistema de módulos com module-info.java. Todo programa abaixo rodou no Java 25, e a saída foi colada da execução. Para rodar um dos programas de arquivo único você mesmo, salve como Main.java e rode java Main.java. As sessões de terminal vêm de um pequeno projeto de dois módulos, conferido por um script que usa só o JDK.

Um pacote é um nome e uma pasta

Um pacote dá a uma classe um nome mais longo e único, então a List de java.util nunca se confunde com uma classe List que outra pessoa escreveu. O nome completo, pacote mais classe, é o nome totalmente qualificado (fully qualified name), e você sempre pode escrevê-lo por inteiro. Este programa não usa nenhum import. É uma public class Main clássica, porque um arquivo-fonte compacto importa o java.base para você e esconderia a questão:

public class Main {
    public static void main(String[] args) {
        java.util.List<String> words = java.util.List.of("cat", "sat", "mat");
        IO.println(words.getFirst() + " is one of " + words.size() + " words");
        IO.println(java.util.List.class.getName());
        IO.println(java.util.List.class.getPackageName());
        IO.println("[" + Main.class.getPackageName() + "]");
    }
}

Ele imprime:

cat is one of 3 words
java.util.List
java.util
[]

java.util.List é o nome real da classe. List sozinho é uma abreviação. A última linha mostra que Main tem um nome de pacote vazio. Ela fica no pacote padrão (default package), também chamado de pacote sem nome, porque o arquivo não tem linha package.

Num projeto, a primeira linha de um arquivo-fonte diz o pacote, e as pastas no disco seguem os mesmos pontos. Uma classe em package com.example.text; vai em com/example/text/. Nomes de pacote costumam começar com um domínio que você controla, escrito ao contrário, para que duas empresas não escolham o mesmo.

Um nome de pacote parece uma árvore, mas o Java não o trata assim. com.example.text e com.example.text.internal são dois pacotes sem relação que por acaso compartilham um prefixo. Importar um não importa o outro, e nenhum dos dois ganha acesso especial ao código do outro.

import e import static

Um import deixa você escrever o nome curto de uma classe em vez do nome totalmente qualificado. Ele não carrega nada nem copia código. O compilador só reescreve List como java.util.List para você. O import static faz o mesmo para membros estáticos, como métodos e constantes:

import java.util.ArrayList;
import java.util.List;

import static java.lang.Math.max;
import static java.util.Comparator.reverseOrder;

public class Main {
    public static void main(String[] args) {
        List<Integer> scores = new ArrayList<>(List.of(4, 9, 2));
        scores.sort(reverseOrder());
        IO.println(scores);
        IO.println(max(scores.getFirst(), 10));
    }
}

Ele imprime:

[9, 4, 2]
10

Sem os imports estáticos, você escreveria Math.max e Comparator.reverseOrder(). Use com moderação, senão quem lê fica caçando de onde vem cada nome solto.

Nada em java.lang precisa de import. É por isso que String, Math e IO sempre funcionam.

Um import com wildcard, import java.util.*;, traz todas as classes de um pacote. Ele causa problema quando dois pacotes têm uma classe com o mesmo nome:

import java.util.*;
import java.sql.*;

public class Main {
    public static void main(String[] args) {
        Date today = new Date(0);
        IO.println(today);
    }
}

O build falha com:

Main.java:6: error: reference to Date is ambiguous
        Date today = new Date(0);
        ^
  both class java.sql.Date in java.sql and class java.util.Date in java.util match

A correção é um import de classe única, import java.util.Date;. Um import de classe única sempre vence um wildcard. Ou escreva o nome totalmente qualificado onde você usa a classe.

O pacote padrão, e por que arquivos-fonte compactos não têm pacote

O pacote padrão serve para um programa de um arquivo e é errado para qualquer coisa maior. Uma classe num pacote com nome não consegue importar de jeito nenhum uma classe do pacote padrão, porque não há nome para escrever depois do import.

Um arquivo-fonte compacto, a forma void main() que esta série usa, sempre fica no pacote padrão. O compilador o embrulha numa classe que você nunca nomeou, então não há nada a que outro código possa se referir, e uma linha package é recusada:

package com.example;

void main() {
    IO.println("hello");
}

O build falha com:

Main.java:1: error: compact source file should not have package declaration

Então arquivos-fonte compactos servem para scripts e para aprender. Código que outras pessoas usam vai num pacote com nome.

import module: todos os pacotes exportados de uma vez

Um import de módulo, import module java.base;, importa todas as classes de todos os pacotes que o módulo exporta. Ele virou recurso final no Java 25, e o javac --release 24 o rejeita com module imports are not supported in -source 24.

import module java.base;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("b", "a");
        IO.println(words);
    }
}

Ele imprime:

[b, a]

Essa única linha cobre java.util, java.io, java.time e o resto do java.base. É exatamente o que um arquivo-fonte compacto recebe sem pedir. Conflitos de nome funcionam do mesmo jeito que com wildcards: um import de classe única resolve.

O projeto: um contador de palavras em dois módulos

O resto deste post usa um projeto pequeno: uma biblioteca que conta palavras e um comando que imprime as três mais frequentes. Aqui está o projeto inteiro, fora o .gitignore:

14-wordcount/
├── run-checks.sh
├── sample.txt
├── expected-output.txt
├── checks/
│   └── Peek.java
└── src/
    ├── com.example.text/
    │   ├── module-info.java
    │   └── com/example/text/
    │       ├── WordCount.java
    │       ├── WordCounter.java
    │       └── internal/
    │           └── Tokenizer.java
    └── com.example.app/
        ├── module-info.java
        └── com/example/app/
            └── Main.java

As pastas logo abaixo de src têm nomes de módulos, que aparecem mais adiante no post. Dentro de cada uma, as pastas seguem os nomes dos pacotes.

A biblioteca guarda a separação de palavras num pacote chamado internal:

package com.example.text.internal;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/** Splits text into lower-case words. Public, but its package is not exported. */
public final class Tokenizer {
    private Tokenizer() {
    }

    public static List<String> words(String text) {
        var words = new ArrayList<String>();
        for (String part : text.split("[^\\p{L}\\p{N}']+")) {
            String word = part.replaceAll("^'+|'+$", "");
            if (!word.isEmpty()) {
                words.add(word.toLowerCase(Locale.ROOT));
            }
        }
        return words;
    }
}

Tokenizer precisa ser public, porque WordCounter fica em outro pacote e precisa chamá-la. Antes dos módulos, isso significava que qualquer um podia chamá-la. Guarde essa ideia.

A API de verdade da biblioteca é um record e uma classe:

package com.example.text;

/** One word and how many times it appeared. */
public record WordCount(String word, int count) {
}
package com.example.text;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import com.example.text.internal.Tokenizer;

/** Counts how often each word appears in a piece of text. */
public final class WordCounter {
    private WordCounter() {
    }

    /** Returns each word and its count, sorted by word. */
    public static Map<String, Integer> count(String text) {
        var counts = new TreeMap<String, Integer>();
        for (String word : Tokenizer.words(text)) {
            counts.merge(word, 1, Integer::sum);
        }
        return counts;
    }

    /** Returns the n most frequent words. Ties are broken alphabetically. */
    public static List<WordCount> top(Map<String, Integer> counts, int n) {
        return counts.entrySet().stream()
                .map(e -> new WordCount(e.getKey(), e.getValue()))
                .sorted(Comparator.comparingInt(WordCount::count).reversed()
                        .thenComparing(WordCount::word))
                .limit(n)
                .toList();
    }
}

E o comando lê a entrada padrão, imprime as três palavras mais frequentes e depois diz em qual módulo está rodando:

package com.example.app;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import com.example.text.WordCount;
import com.example.text.WordCounter;

public class Main {
    public static void main(String[] args) throws IOException {
        String text = new String(System.in.readAllBytes(), StandardCharsets.UTF_8);
        var counts = WordCounter.count(text);
        for (WordCount wc : WordCounter.top(counts, 3)) {
            IO.println(wc.word() + " " + wc.count());
        }

        Module module = Main.class.getModule();
        IO.println("module: " + (module.isNamed() ? module.getName() : "unnamed"));
    }
}

Essa última linha muda conforme o jeito que você inicia o programa, e é justamente para isso que ela existe.

O classpath: onde o java procura classes

O classpath é uma lista de pastas e arquivos JAR onde o javac e o java procuram classes compiladas. Você compila a biblioteca primeiro, depois compila o app com a biblioteca no classpath e então roda o app com os dois no classpath:

$ javac -d classes/lib src/com.example.text/com/example/text/*.java src/com.example.text/com/example/text/internal/*.java
$ javac -cp classes/lib -d classes/app src/com.example.app/com/example/app/Main.java
$ find classes -name "*.class" | sort
classes/app/com/example/app/Main.class
classes/lib/com/example/text/WordCount.class
classes/lib/com/example/text/WordCounter.class
classes/lib/com/example/text/internal/Tokenizer.class
$ java -cp classes/app:classes/lib com.example.app.Main < sample.txt
the 3
cat 2
and 1
module: unnamed

O sample.txt guarda a linha the cat sat on the mat and the cat slept. O -d define a pasta de saída, e o javac cria as pastas dos pacotes dentro dela para você. O -cp recebe uma lista separada por : no Linux e no macOS, e por ; no Windows. O comando java indica uma classe pelo nome totalmente qualificado, não um arquivo.

Esses comandos javac pularam o module-info.java, então não há módulos aqui. Tudo no classpath cai num grande módulo sem nome (unnamed module), e é isso que a última linha diz.

O classpath dá errado de dois jeitos, e eles se parecem. Se o java não encontra a classe que você mandou iniciar, você recebe isto:

$ java -cp classes/lib com.example.app.Main < sample.txt
Error: Could not find or load main class com.example.app.Main
Caused by: java.lang.ClassNotFoundException: com.example.app.Main

Se ele encontra Main, mas não uma classe de que Main precisa, o programa começa e depois falha na primeira linha que usa a classe que falta. O stack trace foi cortado aqui:

$ java -cp classes/app com.example.app.Main < sample.txt
Exception in thread "main" java.lang.NoClassDefFoundError: com/example/text/WordCounter
	at com.example.app.Main.main(Main.java:12)
Caused by: java.lang.ClassNotFoundException: com.example.text.WordCounter

ClassNotFoundException significa que uma classe foi procurada pelo nome e não estava lá. NoClassDefFoundError significa que uma classe que existia quando o código foi compilado agora está faltando. Os dois têm a mesma correção: pôr a pasta ou o JAR que falta no classpath.

Repare quando o segundo falhou. Nada conferiu o classpath na inicialização. O main rodou, e a linha 12 foi onde a JVM precisou de WordCounter pela primeira vez.

Arquivos JAR: um zip com um manifesto

Um arquivo JAR é um arquivo zip de arquivos .class com um pequeno arquivo de texto, o manifesto, em META-INF/. A ferramenta jar monta um, e o --main-class grava no manifesto a classe a iniciar:

$ jar --create --file wordcount.jar --main-class com.example.app.Main -C classes/app . -C classes/lib .
$ jar --list --file wordcount.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/app/
com/example/app/Main.class
com/example/text/
com/example/text/WordCount.class
com/example/text/WordCounter.class
com/example/text/internal/
com/example/text/internal/Tokenizer.class
$ unzip -p wordcount.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 25.0.4 (Ubuntu)
Main-Class: com.example.app.Main

$ java -jar wordcount.jar < sample.txt
the 3
cat 2
and 1
module: unnamed

-C classes/app . quer dizer “entre em classes/app e adicione tudo o que tem lá”. Os caminhos dentro do JAR são as pastas dos pacotes, o mesmo layout do disco. O java -jar lê o Main-Class do manifesto, então você não indica a classe. Ele também ignora qualquer -cp que você passe: o JAR, mais o que a linha Class-Path do manifesto listar, é o classpath inteiro.

Este JAR guarda o app e a biblioteca, então roda sozinho. Aplicações de verdade dependem de dezenas de JARs de bibliotecas, e é aí que entram as ferramentas de build.

Módulos: module-info.java

Um módulo é um conjunto de pacotes com um nome e um descritor, o module-info.java, que diz de quais módulos ele precisa e quais dos seus pacotes outros módulos podem usar. Os módulos chegaram no Java 9. O descritor da biblioteca exporta um pacote e não diz nada sobre internal:

/** Counts words in text. */
module com.example.text {
    exports com.example.text;
}

O app não exporta nada e requer a biblioteca:

/** A command that prints the most frequent words from standard input. */
module com.example.app {
    requires com.example.text;
}

requires trata de módulos, e exports trata de pacotes. Todo módulo também lê o java.base sem pedir. O javac consegue compilar os dois módulos num só comando, usando o layout de pastas sob src:

$ javac -Xlint:all -Werror --release 25 -d out --module-source-path src -m com.example.text,com.example.app
$ find out -type f | sort
out/com.example.app/com/example/app/Main.class
out/com.example.app/module-info.class
out/com.example.text/com/example/text/WordCount.class
out/com.example.text/com/example/text/WordCounter.class
out/com.example.text/com/example/text/internal/Tokenizer.class
out/com.example.text/module-info.class
$ java --module-path out -m com.example.app/com.example.app.Main < sample.txt
the 3
cat 2
and 1
module: com.example.app

--module-source-path src diz ao javac que cada pasta sob src é um módulo com o nome da pasta. O -m escolhe os módulos a compilar. O javac descobriu a ordem pela linha requires.

--module-path out, ou -p out, é o module path. O java trata cada pasta sob out como um módulo com nome. -m module/class diz qual módulo iniciar e qual classe tem o main. O mesmo Main.class agora informa module: com.example.app.

module com.example.app com.example.app class Main não exporta nada module com.example.text com.example.text WordCounter, WordCount exports com.example.text com.example.text.internal public class Tokenizer não exportado requires pode usar não visível module java.base, lido por todo módulo

com.example.app requer com.example.text, então Main pode usar o pacote exportado. O pacote internal fica dentro do mesmo módulo, e sua classe é pública, mas ele não é exportado, então nada fora do módulo pode usá-lo.

Encapsulamento forte: public já não basta

Uma classe public num pacote que o seu módulo não exporta não pode ser usada de fora do módulo. Isso se chama encapsulamento forte (strong encapsulation). O checks/Peek.java tenta mesmo assim:

import com.example.text.internal.Tokenizer;

public class Peek {
    public static void main(String[] args) {
        IO.println(Tokenizer.words("reaching inside"));
    }
}

Compile contra o module path. O --add-modules adiciona a biblioteca ao build, como faria uma linha requires:

$ javac -p out --add-modules com.example.text -d peek checks/Peek.java
checks/Peek.java:1: error: package com.example.text.internal is not visible
import com.example.text.internal.Tokenizer;
                       ^
  (package com.example.text.internal is declared in module com.example.text, which does not export it)
1 error

O erro diz a regra e o motivo. Tokenizer é pública, e isso não importa. O módulo dela não exportou o pacote.

Agora o mesmo arquivo, compilado contra o build de classpath de antes:

$ javac -cp classes/lib -d peek checks/Peek.java
$ java -cp peek:classes/lib Peek
[reaching, inside]

Compilou e rodou. No classpath não há nenhum module-info.class em jogo, então não há nada para impor. O encapsulamento forte só existe quando a biblioteca está no module path. Isso nos surpreendeu da primeira vez que rodamos, e vale lembrar sempre que alguém disser que os internos de uma biblioteca estão protegidos por módulos.

Explicado como se você tivesse dez anos

Pacotes são pastas num arquivo de escritório. Cada pasta tem uma etiqueta, como com.example.text, e papéis com a mesma etiqueta vão na mesma pasta. A etiqueta é como você pede um papel: “a folha WordCounter da pasta com.example.text“.

Um módulo é um arquivo de escritório com tranca. As gavetas guardam as pastas. O dono cola um adesivo escrito “exportado” em algumas gavetas. Qualquer pessoa de outro arquivo pode abrir essas. As gavetas sem adesivo ficam trancadas por fora, mesmo que o papel dentro diga “público” no topo. Quem trabalha naquele arquivo ainda consegue abrir todas as gavetas.

O classpath é o que acontece quando você despeja todos os arquivos numa mesa grande. Todo papel fica ali, e qualquer um pode pegar qualquer um deles.

A versão precisa

Um módulo é um conjunto de pacotes com nome, descrito pelo module-info.class. Uma linha requires faz um módulo ler outro. Código no módulo A só pode usar um tipo do módulo B quando três coisas são verdade: A lê B, B exporta o pacote do tipo para A, e o próprio tipo é public. Qualquer coisa a menos é erro de compilação, e a JVM impõe a mesma regra em tempo de execução. Reflexão sobre membros privados também exige que o pacote esteja aberto.

Tudo no classpath vai para o módulo sem nome. O módulo sem nome lê todo módulo que a JVM ou o compilador resolveu, e é por isso que o Peek precisou do --add-modules, mas ainda assim só vê os pacotes exportados deles. Classes de JARs no classpath não têm fronteiras de módulo entre si, então vale a regra antiga: public quer dizer qualquer um.

Onde a analogia falha: uma gaveta trancada parece segurança, e não é. Quem inicia a JVM pode passar --add-exports ou --add-opens na linha de comando para destrancar um pacote, ou mover o JAR para o classpath, como o Peek acabou de fazer. O encapsulamento protege os autores de uma biblioteca contra dependências acidentais dos seus internos. Ele não protege segredos de alguém que controla a linha de comando.

opens: deixando a reflexão entrar

O exports controla o acesso em tempo de compilação e as chamadas normais, enquanto o opens controla a reflexão profunda, aquela que lê campos privados. Frameworks que preenchem objetos a partir de JSON ou injetam dependências precisam dele. Uma linha como opens com.example.text.model; deixa a reflexão em tempo de execução alcançar todos os membros daquele pacote, incluindo os privados, sem exportá-lo para a compilação.

Os módulos do próprio JDK não abrem seus internos, e dá para ver a recusa num programa de um arquivo:

void main() {
    try {
        var field = String.class.getDeclaredField("value");
        field.setAccessible(true);
        IO.println("opened");
    } catch (NoSuchFieldException | InaccessibleObjectException e) {
        IO.println(e.getClass().getSimpleName());
        String message = e.getMessage();
        // The message ends with " @" and a hash code that changes on every run.
        IO.println(message.substring(0, message.lastIndexOf(" @")));
    }
}

Ele imprime:

InaccessibleObjectException
Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module

O getDeclaredField funcionou, então o campo existe. O setAccessible(true) é o passo que falhou. Do Java 9 ao 15, a mesma chamada dava certo com um aviso, e muitas bibliotecas antigas dependiam disso. O Java 16 fez a chamada falhar por padrão, e o Java 17 removeu a opção que trazia o comportamento antigo de volta.

JARs modulares e o module path

Um JAR modular é um JAR normal com module-info.class na raiz. Você monta um por módulo, e o --main-class no JAR do app grava a classe a iniciar dentro do descritor do módulo:

$ mkdir mods
$ jar --create --file mods/com.example.text.jar -C out/com.example.text .
$ jar --create --file mods/com.example.app.jar --main-class com.example.app.Main -C out/com.example.app .
$ java -p mods -m com.example.app < sample.txt
the 3
cat 2
and 1
module: com.example.app

Desta vez o -m indica só o módulo, porque o JAR já sabe qual é a sua classe principal. O jar --describe-module imprime o descritor. Depois de uma primeira linha com o nome do módulo e o caminho completo do JAR, ele imprimiu isto para cada JAR:

$ jar --describe-module --file mods/com.example.text.jar
exports com.example.text
requires java.base mandated
contains com.example.text.internal
$ jar --describe-module --file mods/com.example.app.jar
requires com.example.text
requires java.base mandated
contains com.example.app
main-class com.example.app.Main

mandated marca o requires java.base que você nunca escreveu. contains lista os pacotes que estão no JAR, mas não são exportados.

O module path também confere o que o classpath não conferia. Apague o JAR da biblioteca e rode o app de novo:

$ rm mods/com.example.text.jar
$ java -p mods -m com.example.app < sample.txt
Error occurred during initialization of boot layer
java.lang.module.FindException: Module com.example.text not found, required by com.example.app

Nem uma linha do main rodou. A JVM leu todos os requires antes de começar, achou um que não conseguia satisfazer e parou. Compare com o NoClassDefFoundError da linha 12 de antes.

Mais uma surpresa. Com os dois JARs de volta em mods, o java -jar no JAR modular do app falha:

$ java -jar mods/com.example.app.jar < sample.txt
Exception in thread "main" java.lang.NoClassDefFoundError: com/example/text/WordCounter
	at com.example.app.Main.main(Main.java:12)
Caused by: java.lang.ClassNotFoundException: com.example.text.WordCounter

O java -jar sempre põe o JAR no classpath. O module-info.class lá dentro é ignorado, então a linha requires não significa nada, e a biblioteca não está no classpath. Para rodar um JAR modular como módulo, use -p e -m.

java --list-modules, e quando os módulos valem a pena

O próprio JDK é dividido em módulos, e o java --list-modules os imprime com suas versões. Nesta máquina ele listou 69, e eles começam assim:

$ java --list-modules | head -4
java.base@25.0.4
java.compiler@25.0.4
java.datatransfer@25.0.4
java.desktop@25.0.4

Adicione -p mods e os seus próprios módulos aparecem no fim da lista, cada um com o arquivo de onde veio.

Agora a parte honesta. A maioria das aplicações Java hoje ainda roda no classpath, e roda bem. Muitos frameworks e bibliotecas populares foram feitos antes dos módulos, e alguns dependem de reflexão de formas que deixam o module path desajeitado. Se você escreve uma aplicação, pode ignorar o module-info.java e perder muito pouco.

Os módulos se pagam em dois lugares:

  • Bibliotecas. Um pacote não exportado deixa você mudar seus internos sem quebrar quem usa a sua biblioteca, desde que essas pessoas estejam no module path. É a ideia da pasta internal, imposta pelo compilador.
  • Runtimes personalizados. O jlink monta um JDK enxuto que guarda só os módulos de que o seu programa precisa. Ele precisa conhecer esses módulos, então o seu código tem que ser modular. A parte sobre testar e distribuir sem ferramenta de build usa o jlink.

O que o Maven e o Gradle acrescentam

Tudo acima usou três ferramentas do JDK, e para dois módulos sem dependências isso bastou. O script que confere este projeto lista pastas de código e passos de build à mão. O Maven e o Gradle trocam isso por um projeto declarado. Você lista as dependências por nome e versão, e a ferramenta as baixa de um repositório como o Maven Central, junto com tudo de que essas dependências precisam, e resolve os conflitos de versão entre elas. Depois ela monta o classpath ou o module path para você, compila na ordem certa, roda os testes e empacota os JARs. As mesmas versões declaradas dão o mesmo build em toda máquina, e essa é a parte que um script escrito à mão erra primeiro. Nada disso é mágica: por baixo, as ferramentas ainda chamam o javac com um -cp ou -p, e o jar, bem como este post fez.

O que lembrar

  • Um pacote é um namespace e uma pasta. O import é só uma abreviação do nome totalmente qualificado, o import static faz o mesmo para membros estáticos, e um import de classe única vence um wildcard.
  • Arquivos-fonte compactos ficam no pacote padrão e não podem declarar um pacote. O import module java.base; é final no Java 25 e é o que esses arquivos recebem automaticamente.
  • O classpath é uma lista de pastas e JARs. Uma classe inicial que falta dá ClassNotFoundException, e uma classe que falta mais tarde dá NoClassDefFoundError só quando aquela linha roda.
  • Um JAR é um zip com um manifesto. O --main-class define o Main-Class, e o java -jar sempre roda o JAR no classpath, mesmo quando é um JAR modular.
  • O module-info.java usa requires para módulos, exports para acesso a pacotes em tempo de compilação e opens para reflexão profunda. Uma classe public num pacote não exportado é invisível fora do seu módulo.
  • Encapsulamento e conferências na inicialização só existem no module path. No classpath, tudo é um único módulo sem nome.
  • A maioria das aplicações vai bem no classpath. Os módulos importam mais para bibliotecas e para o jlink.

Pacotes dão nome ao seu código, JARs o distribuem, e módulos decidem quem de fora pode usá-lo.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.