Dare una casa alle classi
Finora i nostri programmi sono stati piccoli: un file, una classe, una cartella di lavoro. In un'applicazione reale incontrerai più classi con nomi simili, biblioteche di codice e persone che lavorano su parti diverse. Serve un modo per dare a ciascuna classe un posto riconoscibile. I package organizzano i nomi; i moduli permettono di dichiarare quali package vengono esposti e da quali altri moduli dipende un gruppo di codice. Sono due livelli distinti, e partiremo da quello più semplice.
Definizione – Package. Un package è uno spazio di nomi per classi e interfacce. Il nome completo di una classe unisce il nome del package e quello della classe, per esempio
java.util.ArrayList. Il punto separa le parti del nome; nei progetti che usano file e cartelle, gli strumenti riflettono di solito questa gerarchia nel percorso dei sorgenti.
Dichiarare e nominare un package
Mettiamo la classe Catalogo nel package javamattone.esercizi.capitolo6. La prima riga significativa del sorgente è:
package javamattone.esercizi.capitolo6;
La dichiarazione identifica il package della classe. La cartella che useremo per il sorgente Catalogo.java segue la stessa struttura: javamattone/esercizi/capitolo6/Catalogo.java. Questo accordo fra nome e percorso rende semplici compilazione, ricerca e manutenzione. Il punto, da solo, non crea cartelle: il filesystem e gli strumenti di build devono trovare i file nella disposizione attesa.
Catalogo è collocata nella cartella finale.Un package con un nome generico come util rischia di entrare in conflitto con un package di un altro progetto. Per codice destinato a essere distribuito si usa spesso un prefisso legato a un dominio Internet controllato dall'organizzazione, scritto in ordine inverso, per esempio it.esempio.catalogo. Non è una verifica automatica della proprietà del dominio: è una convenzione che aiuta a scegliere nomi unici. Dentro un package, usa parole che descrivono una responsabilità; it.esempio.catalogo.prestiti dice più di it.esempio.varie.
Nota bene – Un punto non dà privilegi.
it.esempio.catalogoeit.esempio.catalogo.internosono package distinti. Il secondo non ottiene per questo un accesso speciale alle classi del primo. La specifica dei package e dei moduli descrive con precisione nomi e visibilità.
Il nome di package viene scelto una volta per una parte del codice e poi compare in più luoghi: all'inizio del sorgente, negli import di chi lo usa, nei comandi che nominano una classe principale e spesso nelle cartelle. Cambiarlo non equivale a rinominare soltanto una directory. Se Catalogo.java dichiara ancora package javamattone.esercizi.capitolo6; ma lo sposti sotto it/esempio/catalogo, il nome dichiarato della classe resta javamattone.esercizi.capitolo6.Catalogo. Il percorso scelto può far fallire o complicare la ricerca dei sorgenti, e il file compilato con -d seguirà comunque il package dichiarato.
Un albero di package può aiutare a vedere che il punto separa segmenti del nome. Pensalo come a un indirizzo logico, non come a una relazione di parentela fra package. java.util e java.util.concurrent condividono un prefisso ma sono due spazi di nomi distinti. Se una classe è accessibile soltanto nel proprio package, aggiungere .interno al nome del chiamante non le apre la porta. È lo stesso principio che abbiamo usato nel capitolo sull'incapsulamento per decidere chi può leggere un membro.
Trovare e distribuire le classi
Per compilare Catalogo.java dalla radice dei sorgenti possiamo usare:
javac --release 25 -d build javamattone/esercizi/capitolo6/Catalogo.java
java -cp build javamattone.esercizi.capitolo6.Catalogo
L'opzione -d build chiede al compilatore di mettere i file .class sotto build, ricreando le sottocartelle del package.
Nell'avvio usiamo il nome qualificato della classe, non il percorso con le barre. -cp build indica il class path: l'insieme delle posizioni da cui il comando java può cercare le classi dell'applicazione in questa forma di avvio. Se il comando non trova la classe, controlla insieme dichiarazione package, percorso del file prodotto, class path e nome passato a java.
Facciamo una diagnosi passo dopo passo, come faremmo davanti a un messaggio d'errore vero. Dopo la compilazione, nella directory build deve esserci javamattone/esercizi/capitolo6/Catalogo.class. Se invece passi a java il percorso javamattone/esercizi/capitolo6/Catalogo.class, stai fornendo un nome di file dove il comando si aspetta un nome di classe. Se scrivi soltanto Catalogo, il runtime cercherà quel nome senza il package e non troverà la classe appena compilata. Se scrivi il nome completo ma imposti -cp sulla cartella build/javamattone, la radice della ricerca è troppo in basso. La radice giusta è la directory sopra il primo segmento del package, cioè build nel nostro esempio. Quattro stringhe possono sembrare quasi uguali e indicare invece quattro cose diverse: percorso del sorgente, percorso del file compilato, radice del class path e nome qualificato.
La variabile d’ambiente CLASSPATH può fornire un valore predefinito, ma per imparare è più chiaro passare -cp nel comando che stiamo provando: la dipendenza dal percorso resta visibile accanto al nome della classe. Chi riproduce l'esempio non deve indovinare una configurazione fatta mesi prima sul nostro computer. Nei progetti più grandi uno strumento di build costruirà per noi il class path; anche allora, capire quale sia la sua radice aiuta a diagnosticare un ClassNotFoundException o un avvio che cerca la classe sbagliata.
Gli archivi JAR permettono di raccogliere classi e risorse in un file. Un JAR può avere un META-INF/MANIFEST.MF con informazioni come la classe principale, così da avviare l'archivio con java -jar. Il manifest non sostituisce la dichiarazione package, non trasforma da solo il programma in un modulo e non modifica la visibilità delle classi. Il manuale di jar descrive come creare e ispezionare questi archivi; nel capitolo sugli strumenti prepareremo una distribuzione completa.
Il JAR e il manifest, letti senza magia
Il manifest può indicare al runtime che cosa avviare e contenere altri metadati dell’archivio distribuito. Il punto utile per il nostro esempio è concreto: se l'archivio contiene javamattone/esercizi/capitolo6/Catalogo.class, la voce Main-Class del manifest deve nominare javamattone.esercizi.capitolo6.Catalogo, con i punti, senza .class. Se il nome è sbagliato, l'archivio può essere valido ma java -jar non trova il punto di ingresso richiesto.
Puoi ispezionare il contenuto di un JAR con jar --list --file catalogo.jar. Vedere il percorso della classe nell'archivio aiuta a diagnosticare un errore di avvio. Un JAR può includere anche file di dati, come configurazioni o immagini, che non sono classi Java. L'archivio non incorpora automaticamente tutte le librerie di cui il programma dipende; come procurarle e distribuirle è una scelta di progetto. Non bisogna confondere «sono riuscito a creare un file .jar» con «ho preparato una distribuzione completa e riproducibile».
Possiamo completare la prova del nostro Catalogo senza lasciare il manifest come promessa astratta. Dopo aver compilato la classe nella directory build, eseguiamo dalla stessa directory di lavoro:
jar --create --file catalogo.jar --main-class javamattone.esercizi.capitolo6.Catalogo -C build .
jar --list --file catalogo.jar
java -jar catalogo.jar
L'opzione -C build . fa entrare nell'archivio il contenuto di build prendendo quella cartella come radice. Perciò il percorso interno della classe comincia con javamattone/, non con build/. --main-class fa scrivere allo strumento jar la voce Main-Class nel manifest con il nome qualificato corretto; non occorre inventare a mano gli spazi o la terminazione delle righe del file. La seconda istruzione permette di controllare che META-INF/MANIFEST.MF e la classe siano presenti, la terza avvia l'archivio e stampa Java Mattone dopo Mattone. Se la directory build contenesse classi di altri esperimenti, entrerebbero anch'esse nell'archivio: per una distribuzione reale conviene usare una cartella di compilazione pulita e dichiarare tutte le dipendenze necessarie.
Il manifest può contenere anche informazioni collegate alla sicurezza. Un manifest può trasportare attributi e metadati di firma, ma la presenza di una voce di testo non rende affidabile un archivio. Per controllare provenienza e integrità servono strumenti e verifiche appropriate alla distribuzione scelta. Qui usiamo il manifest per l'avvio e rimandiamo firma e catena di fiducia alla parte sulla distribuzione sicura, dove potremo trattarle senza suggerire che basti scrivere una riga in un file.
import: un nome più corto nel sorgente
La classe ArrayList appartiene a java.util. Possiamo scrivere java.util.ArrayList ogni volta che la usiamo oppure aggiungere una dichiarazione import dopo package e prima della classe:
package javamattone.esercizi.capitolo6;
import java.util.ArrayList;
public class Catalogo {
private Catalogo() {
}
public static void main(String[] args) {
ArrayList<String> titoli = new ArrayList<>();
titoli.add("Java Mattone dopo Mattone");
System.out.println(titoli.get(0));
}
}
Questo programma crea una lista di titoli, ne aggiunge uno e lo stampa. Il piccolo costruttore private Catalogo() segnala che qui la classe serve soltanto come punto di avvio e non deve essere istanziata; il significato dei costruttori sarà sviluppato nel capitolo sulle classi. ArrayList<String> indica che la lista contiene stringhe; il capitolo sulle collezioni spiegherà il tipo parametrizzato e i suoi metodi. Qui ci interessa il nome: grazie a import java.util.ArrayList; possiamo scrivere ArrayList nel resto del file. La classe rimane nel package java.util; l'import non la copia, non la sposta e non la rende pubblica se non lo era già.
java.util.ArrayList.import java.util.*; rende disponibili per nome semplice i tipi accessibili direttamente in quel package, ma non quelli dei suoi sottopackage. Per gli esempi del libro preferiremo import espliciti: chi legge vede subito da dove arriva una classe e un conflitto di nomi è più facile da diagnosticare. I tipi di java.lang, come String, sono disponibili senza import esplicito.
Un import non equivale nemmeno a un'istruzione di caricamento a runtime. Il compilatore lo usa per risolvere un nome scritto nel sorgente. Se cancelli l'import di ArrayList e sostituisci ogni occorrenza con java.util.ArrayList, il significato della classe usata non cambia. Se invece mantieni il nome breve senza import e senza una classe omonima disponibile nello stesso package, il compilatore non riesce a determinarne il tipo. È una differenza piccola ma fondamentale quando si leggono messaggi come «cannot find symbol».
Consideriamo due classi, Autore e StampaNomeAutore: una rappresenta l’autore, l’altra ne stampa il nome. Ci interessa la possibilità di riferirsi alla stessa classe in tre modi. Se StampaNomeAutore si trova nello stesso package di Autore, può usare il nome semplice senza alcun import. Se si trova in un altro package e la classe Autore è accessibile, può scrivere il nome qualificato in ogni punto in cui serve. Può infine dichiarare un import e usare poi il nome semplice. Nessuna delle tre forme crea una seconda classe Autore: cambiano soltanto le regole con cui il compilatore riconosce il nome scritto nel sorgente.
La parola «accessibile» in questo confronto è essenziale. Un import non rende pubblica una classe che non lo è. Una classe dichiarata senza il modificatore public può essere usata soltanto secondo le regole di accesso del suo package; spostare StampaNomeAutore in un package vicino, anche con lo stesso prefisso, non supera quel confine. Nemmeno il carattere * crea un'eccezione: import it.esempio.catalogo.*; semplifica alcuni nomi, ma non cambia le dichiarazioni di visibilità e non comprende automaticamente it.esempio.catalogo.interno. Ecco perché, quando un import sembra corretto ma il compilatore protesta, controlliamo sia il nome sia il permesso di accesso.
Esiste anche l'import di membri static, che permette di usare direttamente il nome di una costante o di un metodo statico. Un esempio comune è importare Math.PI; senza quell'import scriviamo Math.PI, e il prefisso ricorda immediatamente da dove arriva il valore. Per i primi esempi preferiamo il nome qualificato quando rende la riga più chiara. L'uso di * negli import non significa «copia tutto il package» e non importa i sottopackage, sia nella forma normale sia in quella statica.
Buona pratica – Se un nome è ambiguo, usa quello completo. Due package possono contenere classi con lo stesso nome semplice. Un import non cambia la loro identità. In un punto dove serve distinguerle, scrivere il nome qualificato è più chiaro che inventare un trucco per evitare il conflitto.
Un passo più grande: i moduli
Un package organizza nomi; un modulo raggruppa package e dichiara le proprie dipendenze e le parti accessibili dall'esterno. Dal JDK 9 il sistema dei moduli fa parte della piattaforma. Non devi trasformare ogni esercizio in un modulo: un piccolo programma sul class path è un punto di partenza sensato. Devi però sapere che i due modi di avvio esistono, perché molti strumenti e librerie moderni li incontrano.
javamattone.app legge javamattone.catalogo, che esporta il package del capitolo 6. Il nome capitolo6 nella figura abbrevia javamattone.esercizi.capitolo6; il package interno rappresenta un'eventuale parte non esportata. Un semplice import non renderebbe accessibile quella parte dall'applicazione esterna.Il descrittore di un modulo si chiama module-info.java. Per un modulo di esempio che vuole rendere pubblico il package del catalogo, potrebbe contenere:
module javamattone.catalogo {
exports javamattone.esercizi.capitolo6;
}
exports rende accessibili a moduli che leggono javamattone.catalogo le classi pubbliche di quel package. Non rende pubbliche le classi o i membri privati, e non esporta automaticamente altri package. Se il modulo dipendesse da un modulo diverso da java.base, dichiarerebbe la dipendenza con requires. java.base è letto implicitamente da ogni modulo, quindi non occorre scrivere requires java.base; nel nostro esempio.
La compilazione e l'avvio modulari usano un module path, separato dal class path. Con i file nella struttura mostrata nei materiali del capitolo, la forma essenziale dell'avvio è java --module-path mods -m javamattone.catalogo/javamattone.esercizi.capitolo6.Catalogo: prima il nome del modulo, poi quello qualificato della classe principale. La sezione sui moduli non è un invito a memorizzare subito la riga di comando; serve a capire che il nome di un package e il nome di un modulo rispondono a domande diverse.
Concetto chiave – Accessibilità in due passaggi. Una classe pubblica di un package non è necessariamente accessibile da un altro modulo. Il package deve essere esportato e il modulo chiamante deve leggere quello che lo esporta. Il descrittore rende visibili queste scelte che, con il solo class path, restano più implicite.
Il sistema dei moduli prevede anche servizi: un modulo può dichiarare che usa un servizio e un altro può offrire un'implementazione. L'API ServiceLoader permette di trovare implementazioni disponibili senza fissarne il nome nel codice che le usa. È un argomento utile quando progetteremo componenti sostituibili; per ora basta sapere che requires ed exports non sono le uniche dichiarazioni possibili. Lo strumento jlink, inoltre, può costruire un'immagine di runtime con i moduli necessari a un'applicazione: lo incontreremo nella parte dedicata alla distribuzione.
Un import nuovo in Java 25
Java 25 rende stabile anche la dichiarazione import module. Per esempio, import module java.base; permette di usare tramite nome semplice i tipi dei package esportati da quel modulo, secondo le regole della documentazione ufficiale. È una comodità del sorgente per importare tipi; non sostituisce la dichiarazione requires nel descrittore di un modulo. Nei programmi piccoli e didattici continueremo spesso a importare una classe per volta, perché rende immediata la provenienza di un nome.
Versione Java –
import module. La sintassi fa parte del linguaggio stabile in Java 25. Se compili per una release precedente, il file che la usa potrebbe non essere accettato. Gli esempi del percorso principale dichiarano il requisito di versione quando introducono una forma nuova.
Per concludere
Un package dà un nome alle classi correlate, un import abbrevia il modo in cui le nominiamo dentro un file, un JAR può raccogliere file compilati e risorse, un modulo dichiara dipendenze e confini di accesso. Nessuno di questi strumenti ripara automaticamente un modello confuso; servono a rendere leggibili e verificabili le scelte che abbiamo fatto.
Per esercitarti, sposta una copia di Catalogo.java nel package it.esempio.catalogo, aggiorna cartelle e comandi e verifica che il programma stampi ancora il titolo. L'attività è riuscita quando sai indicare quale riga cambia il nome qualificato della classe, quale opzione indica dove cercare i file compilati e perché la sola dichiarazione import non modifica il package di ArrayList.