Introduzione ai Moduli in Java
JDK 9 ha introdotto una nuova e importante funzionalità: i moduli.
I moduli forniscono un modo per descrivere le relazioni e le dipendenze del codice che compone un'applicazione. I moduli permettono anche di controllare quali parti di un modulo sono accessibili ad altri moduli e quali non lo sono. Attraverso l'uso dei moduli è possibile creare programmi più affidabili e scalabili.
Come regola generale, i moduli sono più utili per applicazioni di grandi dimensioni perché aiutano a ridurre la complessità di gestione spesso associata con un sistema software di grandi dimensioni. Tuttavia, anche i programmi piccoli beneficiano dei moduli perché la libreria API di Java è ora stata organizzata in moduli.
Pertanto, è ora possibile specificare quali parti dell'API sono richieste dal programma e quali non lo sono. Questo rende possibile distribuire programmi con un'impronta di esecuzione più piccola (execution footprint), il che è particolarmente importante quando si crea codice per dispositivi embedded, come quelli destinati al settore IoT (Internet of Things).
Il supporto per i moduli è fornito sia da elementi del linguaggio, incluse diverse parole chiave, che da miglioramenti alle utility javac
(il compilatore), java
(l'esecutore), e altri strumenti JDK.
Inoltre, sono stati introdotti nuovi strumenti e formati di file. Di conseguenza, il JDK e il sistema di esecuzione sono stati sostanzialmente aggiornati per supportare i moduli.
In breve, i moduli costituiscono un'importante aggiunta e un'evoluzione del linguaggio Java.
Fondamenti dei Moduli
Nel suo senso più fondamentale, un modulo è un raggruppamento di package e risorse che possono essere riferiti collettivamente dal nome del modulo.
Una dichiarazione di modulo specifica il nome di un modulo e definisce la relazione che un modulo e i suoi package hanno con altri moduli.
Le dichiarazioni di modulo sono istruzioni di programma in un file sorgente Java e sono supportate da diverse parole chiave correlate ai moduli. Sono mostrate qui:
exports
module
open
opens
provides
requires
to
transitive
uses
with
È importante comprendere che queste parole chiave sono riconosciute come parole chiave solo nel contesto di una dichiarazione di modulo.
Altrimenti, sono interpretate come identificatori in altre situazioni. Così, la parola chiave module
potrebbe, ad esempio, essere usata anche come nome di parametro, sebbene tale uso non sia certamente raccomandato. Tuttavia, rendere le parole chiave correlate ai moduli sensibili al contesto previene problemi con codice preesistente che potrebbe usare una o più di esse come identificatori.
Una dichiarazione di modulo è contenuta in un file chiamato module-info.java
. Così, un modulo è definito in un file sorgente Java. Questo file è poi compilato da javac
in un file di classe ed è conosciuto come il suo descrittore di modulo. Il file module-info.java
deve contenere solo una definizione di modulo. Non può contenere altri tipi di dichiarazioni.
Una dichiarazione di modulo inizia con la parola chiave module
. Ecco la sua forma generale:
// Definizione di un modulo.
module nomeModulo {
/* ... */
}
Il nome del modulo è specificato da nomeModulo
, che deve essere un identificatore Java valido o una sequenza di identificatori separati da punti. La definizione del modulo è specificata all'interno delle parentesi graffe. Sebbene una definizione di modulo possa essere vuota (il che risulta in una dichiarazione che semplicemente nomina il modulo), tipicamente specifica una o più clausole che definiscono le caratteristiche del modulo.
Un Esempio di Modulo Semplice
Alla base delle capacità di un modulo ci sono due caratteristiche chiave.
La prima è la capacità di un modulo di specificare che richiede un altro modulo. In altre parole, un modulo può specificare che dipende da un altro. Una relazione di dipendenza è specificata tramite l'uso di un'istruzione requires
.
Di default, la presenza del modulo richiesto viene verificata sia al momento della compilazione che a runtime.
La seconda caratteristica chiave è la capacità di un modulo di controllare quali dei suoi package sono accessibili da un altro modulo, se presenti. Questo è realizzato tramite l'uso della parola chiave exports
.
I tipi public
e protected
all'interno di un package sono accessibili ad altri moduli solo se sono esplicitamente esportati.
Qui svilupperemo un esempio che introduce entrambe queste caratteristiche.
L'esempio seguente crea un'applicazione modulare che dimostra alcune semplici funzioni matematiche.
Anche se questa applicazione è deliberatamente molto piccola, illustra i concetti e le procedure fondamentali richieste per creare, compilare ed eseguire codice basato su moduli.
Inoltre, l'approccio generale mostrato qui si applica anche ad applicazioni più grandi del mondo reale. È fortemente raccomandato di lavorare attraverso l'esempio sul proprio computer, seguendo attentamente ogni passaggio.
Uso degli Strumenti da Riga di Comando
Questa lezione, e le prossime lezioni sui moduli, mostrano il processo di creazione, compilazione ed esecuzione di codice basato su moduli tramite l'uso degli strumenti da riga di comando.
Questo approccio ha due vantaggi:
- Primo, funziona per tutti i programmatori Java perché non è richiesto alcun IDE.
- Secondo, mostra molto chiaramente i fondamenti del sistema di moduli, incluso come utilizza le directory.
Per seguire le lezioni, bisogna creare manualmente un numero di directory e assicurarsi che ogni file sia posizionato nella sua directory appropriata.
Come ci si può aspettare, quando si creano applicazioni basate su moduli del mondo reale probabilmente si troverà più facile usare un IDE consapevole dei moduli perché, tipicamente, automatizzerà gran parte del processo.
Tuttavia, imparare i fondamenti dei moduli usando gli strumenti da riga di comando assicura che si abbia una solida comprensione dell'argomento.
L'applicazione definisce due moduli:
-
Il primo modulo è chiamato
appstart
.Contiene un package chiamato
appstart.mymodappdemo
che definisce il punto di ingresso dell'applicazione in una classe chiamataMyModAppDemo
. Quindi,MyModAppDemo
contiene il metodomain()
dell'applicazione. -
Il secondo modulo è chiamato
appfuncs
.Contiene un package chiamato
appfuncs.simplefuncs
che include la classeSimpleMathFuncs
. Questa classe definisce tre metodi static che implementano alcune semplici funzioni matematiche. L'intera applicazione sarà contenuta in un albero di directory che inizia conmymodapp
.
Prima di continuare, sono appropriate alcune parole sui nomi dei moduli.
Primo, negli esempi che seguono, il nome di un modulo (come appfuncs
) è il prefisso del nome di un package (come appfuncs.simplefuncs
) che contiene. Questo non è richiesto, ma è usato qui come modo per indicare chiaramente a quale modulo appartiene un package.
In generale, quando si impara e si sperimenta con i moduli, nomi brevi e semplici, come quelli usati in questo capitolo, sono utili, e si può usare qualsiasi tipo di nomi a piacere.
Tuttavia, quando si creano moduli adatti per la distribuzione, bisogna essere attenti con i nomi che scegliete perché si vuole che quei nomi siano unici.
Una convenzione molto adoperata è quella di usare il metodo del nome di dominio inverso. In questo metodo, il nome di dominio inverso del dominio che "possiede" il progetto è usato come prefisso per il modulo. Per esempio, un progetto associato con distortionbyte.com
userebbe com.distortionbyte
come prefisso del modulo. Lo stesso vale per i nomi dei package.
Iniziamo creando le necessarie directory del codice sorgente seguendo questi passaggi:
-
- Creiamo una directory chiamata
mymodapp
. Questa è la directory di livello superiore per l'intera applicazione.
- Creiamo una directory chiamata
-
- Sotto
mymodapp
, creiamo una sottodirectory chiamataappsrc
. Questa è la directory di livello superiore per il codice sorgente dell'applicazione.
- Sotto
-
- Sotto
appsrc
, creiamo la sottodirectoryappstart
. Sotto questa directory, creiamo una sottodirectory chiamata ancheappstart
. Sotto questa directory, creiamo la directorymymodappdemo
. Quindi, iniziando conappsrc
, avremo creato questo albero:appsrc\appstart\appstart\mymodappdemo
- Sotto
-
- Anche sotto
appsrc
, creiamo la sottodirectoryappfuncs
. Sotto questa directory, creiamo una sottodirectory chiamata ancheappfuncs
. Sotto questa directory, creiamo la directory chiamatasimplefuncs
. Quindi, iniziando conappsrc
, avremo creato questo albero:appsrc\appfuncs\appfuncs\simplefuncs
- Anche sotto
L'alberatura di directory appena creata dovrebbe apparire come quella mostrato qui:
mymodapp
│
└───appsrc
│
├───appfuncs
│ └───appfuncs
│ └───simplefuncs
│
└───appstart
└───appstart
└───mymodappdemo
Dopo aver impostato queste directory, possiamo creare i file sorgente dell'applicazione.
Questo esempio userà quattro file sorgente. Due sono i file sorgente che definiscono l'applicazione. Il primo è SimpleMathFuncs.java
, mostrato qui. Si noti che SimpleMathFuncs
appartiene al package appfuncs.simplefuncs
.
// Alcune semplici funzioni matematiche.
package appfuncs.simplefuncs;
public class SimpleMathFuncs {
// Determina se a è un fattore di b.
public static boolean isFactor(int a, int b) {
if ((b % a) == 0)
return true;
return false;
}
// Restituisce il più piccolo fattore positivo che a e b hanno in comune.
public static int lcf(int a, int b) {
// Fattorizza usando valori positivi.
a = Math.abs(a);
b = Math.abs(b);
int min = a < b ? a : b;
for (int i = 2; i <= min / 2; i++) {
if (isFactor(i, a) && isFactor(i, b))
return i;
}
return 1;
}
// Restituisce il più grande fattore positivo che a e b hanno in comune.
public static int gcf(int a, int b) {
// Fattorizza usando valori positivi.
a = Math.abs(a);
b = Math.abs(b);
int min = a < b ? a : b;
for (int i = min / 2; i >= 2; i--) {
if (isFactor(i, a) && isFactor(i, b))
return i;
}
return 1;
}
}
SimpleMathFuncs
definisce tre semplici funzioni matematiche static
. La prima, isFactor()
, restituisce true se a
è un fattore di b
. Il metodo lcf()
restituisce il più piccolo fattore comune sia ad a
che a b
. In altre parole, restituisce il minimo fattore comune di a
e b
. Il metodo gcf()
restituisce il massimo fattore comune di a
e b
. In entrambi i casi, 1 è restituito se non vengono trovati fattori comuni. Questo file deve essere messo nella seguente directory:
appsrc/appfuncs/appfuncs/simplefuncs
Questa è la directory del package appfuncs.simplefuncs
.
Il secondo file sorgente è MyModAppDemo.java
, mostrato di seguito. Usa i metodi in SimpleMathFuncs
. Si noti che appartiene al package appstart.mymodappdemo
. Si noti anche che importa la classe SimpleMathFuncs
perché dipende da SimpleMathFuncs
per la sua operazione.
// Dimostra una semplice applicazione basata su moduli.
package appstart.mymodappdemo;
// Importa la classe SimpleMathFuncs dal modulo appfuncs.
import appfuncs.simplefuncs.SimpleMathFuncs;
public class MyModAppDemo {
public static void main(String[] args) {
if (SimpleMathFuncs.isFactor(2, 10))
System.out.println("2 è un fattore di 10");
System.out.println("Il più piccolo fattore comune sia a 35 che a 105 è " +
SimpleMathFuncs.lcf(35, 105));
System.out.println("Il più grande fattore comune sia a 35 che a 105 è " +
SimpleMathFuncs.gcf(35, 105));
}
}
Questo file deve essere messo nella seguente directory:
appsrc/appstart/appstart/mymodappdemo
Questa è la directory per il package appstart.mymodappdemo
.
Successivamente, dovrete aggiungere file module-info.java
per ogni modulo. Questi file contengono le definizioni dei moduli.
Primo, aggiungiamo queste righe, che definiscono il modulo appfuncs
:
// Definizione del modulo per il modulo delle funzioni.
module appfuncs {
// Esporta il package appfuncs.simplefuncs.
exports appfuncs.simplefuncs;
}
Si noti che appfuncs
esporta il package appfuncs.simplefuncs
, che lo rende accessibile ad altri moduli. Questo file deve essere messo in questa directory:
appsrc/appfuncs
Quindi, va nella directory del modulo appfuncs
, che è sopra le directory dei package.
Infine, il file module-info.java
per il modulo appstart
è mostrato di seguito. Si noti che appstart
richiede il modulo appfuncs
.
// Definizione del modulo per il modulo dell'applicazione principale.
module appstart {
// Richiede il modulo appfuncs.
requires appfuncs;
}
Questo file deve essere messo nella sua directory del modulo:
appsrc/appstart
Prima di esaminare più da vicino le istruzioni requires
, exports
e module
, compiliamo ed eseguiamo prima questo esempio. Assicuriamoci di aver creato correttamente le directory e inserito ogni file nella sua directory appropriata, come appena spiegato.
Compilare ed Eseguire il Primo Esempio di Modulo
A partire da JDK 9, javac
è stato aggiornato per supportare i moduli.
Pertanto, come tutti gli altri programmi Java, i programmi basati sui moduli vengono compilati utilizzando javac
. Il processo è semplice, con la differenza principale che solitamente specificheremo esplicitamente un percorso del modulo. Un percorso del modulo dice al compilatore dove saranno situati i file compilati. Quando seguiamo questo esempio, assicuriamoci di eseguire i comandi javac
dalla directory mymodapp
affinché i percorsi siano corretti. Ricordiamo che mymodapp
è la directory di livello superiore per l'intera applicazione del modulo.
Per iniziare, compiliamo SimpleMathFuncs.java
utilizzando questo comando:
javac -d appmodules/appfuncs appsrc/appfuncs/appfuncs/simplefuncs/SimpleMathFuncs.java
Ricordiamo, questo comando deve essere eseguito dalla directory mymodapp
.
Notiamo l'uso dell'opzione -d
. Questa dice a javac
dove mettere il file di output .class
. Per gli esempi in questo capitolo, la cima dell'albero delle directory per il codice compilato è appmodules
. Questo comando creerà le directory dei package di output per appfuncs.simplefuncs
sotto appmodules/appfuncs
secondo necessità.
Successivamente, ecco il comando javac
che compila il file module-info.java
per il modulo appfuncs
:
javac -d appmodules/appfuncs appsrc/appfuncs/module-info.java
Questo mette il file module-info.class
nella directory appmodules/appfuncs
.
Sebbene il processo precedente in due passaggi funzioni, è stato mostrato principalmente per il bene della discussione. È solitamente più facile compilare il file module-info.java
di un modulo e i suoi file sorgente in una riga di comando. Qui, i due comandi javac
precedenti sono combinati in uno:
javac -d appmodules/appfuncs \
appsrc/appfuncs/module-info.java \
appsrc/appfuncs/appfuncs/funzionisemplici/FunzioniMatematiceSemplici.java
In questo caso, ogni file compilato è messo nella sua directory appropriata del modulo o del package.
Ora, compiliamo module-info.java
e MyModAppDemo.java
per il modulo appstart, utilizzando questo comando:
javac -d appmodules/appstart \
appsrc/appstart/module-info.java \
appsrc/appstart/appstart/mymodappdemo/MyModAppDemo.java
Notiamo l'opzione --module-path
. Essa specifica il percorso del modulo, che è il percorso su cui il compilatore cercherà i moduli definiti dall'utente richiesti dal file module-info.java
.
In questo caso, cercherà il modulo appfuncs
perché è necessario per il modulo appstart
. Inoltre, notiamo che specifica la directory di output come appmodules/appstart
. Questo significa che il file module-info.class
sarà nella directory del modulo appmodules/appstart
e MyModAppDemo.class
sarà nella directory del package appmodules/appstart/appstart/mymodappdemo
.
Una volta completata la compilazione, possiamo eseguire l'applicazione con questo comando java:
java --module-path appmodules -m appstart/appstart.demoappmodmia.MyModAppDemo
Qui, l'opzione --module-path
specifica il percorso ai moduli dell'applicazione. Come menzionato, appmodules
è la directory in cima all'albero dei moduli compilati. L'opzione -m
specifica la classe che contiene il punto di ingresso dell'applicazione e, in questo caso, il nome della classe che contiene il metodo main()
. Quando eseguiamo il programma, vedremo il seguente output:
2 è un fattore di 10
Il più piccolo fattore comune sia a 35 che a 105 è 5
Il più grande fattore comune sia a 35 che a 105 è 7
requires
ed exports
L'esempio basato sui moduli precedente si basa sulle due caratteristiche fondamentali del sistema di moduli: la capacità di specificare una dipendenza e la capacità di soddisfare quella dipendenza.
Queste capacità sono specificate attraverso l'uso delle istruzioni requires
ed exports
all'interno di una dichiarazione module
. Ciascuna merita un esame più approfondito in questo momento.
Ecco la forma dell'istruzione requires
utilizzata nell'esempio:
// Richiede un modulo.
requires nomeModulo;
Qui, nomeModulo
specifica il nome di un modulo che è richiesto dal modulo in cui si verifica l'istruzione requires
.
Questo significa che il modulo richiesto deve essere presente affinché il modulo corrente possa compilare. Nel linguaggio dei moduli, si dice che il modulo corrente legge il modulo specificato nell'istruzione requires
. Quando è richiesto più di un modulo, deve essere specificato nella propria istruzione requires
. Pertanto, una dichiarazione di modulo può includere diverse istruzioni requires
. In generale, l'istruzione requires
fornisce un modo per assicurarsi che il programma abbia accesso ai moduli di cui ha bisogno.
Ecco la forma generale dell'istruzione exports
utilizzata nell'esempio:
// Esporta un package.
exports nomePackage;
Qui, nomePackage
specifica il nome del package che viene esportato dal modulo in cui compare questa dichiarazione. Un modulo può esportare tutti i package necessari, con ognuno specificato in una dichiarazione exports
separata. Pertanto, un modulo può avere diverse dichiarazioni exports
.
Quando un modulo esporta un package, rende tutti i tipi public e protected nel package accessibili ad altri moduli. Inoltre, i membri public e protected di quei tipi sono anch'essi accessibili. Tuttavia, se un package all'interno di un modulo non viene esportato, allora è privato per quel modulo, inclusi tutti i suoi tipi public. Ad esempio, anche se una classe è dichiarata come public
all'interno di un package, se quel package non è esplicitamente esportato da una dichiarazione exports
, allora quella classe non è accessibile ad altri moduli. È importante comprendere che i tipi public e protected di un package, che siano esportati o meno, sono sempre accessibili all'interno del modulo di quel package. La dichiarazione exports
li rende semplicemente accessibili ai moduli esterni. Pertanto, qualsiasi package non esportato è solo per l'uso interno del suo modulo.
La chiave per comprendere requires
ed exports
è che lavorano insieme. Se un modulo dipende da un altro, allora deve specificare quella dipendenza con requires
. Il modulo da cui un altro dipende deve esplicitamente esportare (cioè, rendere accessibili) i package di cui il modulo dipendente ha bisogno. Se manca una delle due parti di questa relazione di dipendenza, il modulo dipendente non compilerà. Per quanto riguarda l'esempio precedente, MyModDemoApp
usa le funzioni in SimpleMathFuncs
.
Di conseguenza, la dichiarazione del modulo appstart
contiene una dichiarazione requires
che nomina il modulo appfuncs
. La dichiarazione del modulo appfuncs
esporta il package appfuncs.simplemath
, rendendo così disponibili i tipi public nella classe SimpleMathFuncs
. Poiché entrambi i lati della relazione di dipendenza sono stati soddisfatti, l'applicazione può compilare ed eseguire. Se manca uno dei due, la compilazione fallirà.
È importante sottolineare che le dichiarazioni requires
ed exports
devono comparire solo all'interno di una dichiarazione module
. Inoltre, una dichiarazione module
deve comparire da sola in un file chiamato module-info.java
.