1. Problemi del vecchio approccio
“Classpath party”: quando è tutto nella stessa cucina
Prima dell’arrivo dei moduli in Java (nella versione 9) tutto il codice dell’applicazione, le librerie e le dipendenze finivano in un unico grande calderone — il classpath. Di fatto non c’erano confini tra le librerie: qualsiasi classe presente nel classpath poteva essere trovata e usata da chiunque.
Si verificavano conflitti di nomi: due librerie contengono una classe con lo stesso nome completamente qualificato, per esempio com.example.Util — ne veniva scelta “una qualsiasi”, e del problema ci si accorgeva solo da comportamenti strani a runtime.
Classico del genere — dependency hell: una libreria richiede un logger versione 1.2, un’altra la 1.3, e nel classpath finiscono entrambe. La JVM non sa quale prendere e ci si ritrova con un misterioso NoSuchMethodError.
E inoltre: anche se una classe è dichiarata come public ma destinata a usi interni della libreria, altro codice può comunque usarla — e rompersi qualcosa nel processo. I progetti grandi diventavano un campo minato, e la manutenzione — un’avventura.
2. Che cos’è un modulo in Java?
Modulo: come una scatola con i fori
In Java 9 è apparso il sistema modulare (JPMS). Un modulo è una scatola con classi e pacchetti, nella quale fai tu le “finestrine”: indichi esplicitamente cosa esponi all’esterno (exports) e cosa nascondi all’interno. E sì: anche un public-classe non è visibile ad altri moduli se il suo pacchetto non è esportato.
Definizione formale
Un modulo è un’unità logica di suddivisione del codice che raggruppa pacchetti e classi correlati. Ogni modulo dichiara esplicitamente:
- cosa esporta — ciò che rende disponibile ad altri moduli (exports);
- e cosa importa — da quali altri moduli dipende (requires).
Proprietà chiave del modulo:
- Confini espliciti. È definito chiaramente cosa è accessibile dall’esterno e cosa no.
- Dipendenze esplicite. Un modulo non “vede” un altro modulo senza un esplicito requires.
- Isolamento. I dettagli interni possono essere completamente nascosti al mondo esterno.
3. Vantaggi della modularità
Incapsulamento migliorato
È apparso un nuovo livello di visibilità — a livello di modulo. Anche se una classe è public, ma il suo pacchetto non è esportato, è visibile solo all’interno del modulo. Le parti interne della libreria non “tracimano” più all’esterno.
Dichiarazione esplicita delle dipendenze
Le dipendenze necessarie vengono elencate in module-info.java. Compilatore e IDE ti avviseranno in anticipo se ti sei dimenticato di indicare il modulo da cui dipendi.
Sicurezza e affidabilità migliorate
Limitare l’accesso alle API interne rende più difficile un intervento accidentale o intenzionale. È fondamentale per le grandi librerie e per i moduli della piattaforma.
Possibilità di creare JRE “minimali”
Con jlink si può assemblare un runtime minimo composto solo dai moduli necessari. Nel cloud e in scenari embedded questo fa risparmiare spazio su disco e memoria.
Bonus: avvio più rapido e dimensioni ridotte
Non vengono caricati tutti i moduli, ma solo quelli realmente utilizzati — le applicazioni si avviano più in fretta e consumano meno risorse.
4. Dove si usano i moduli
Nella libreria standard di Java
A partire da Java 9, la piattaforma stessa è modulare. Esempi:
- java.base — modulo di base (obbligatorio per qualsiasi programma).
- java.sql — lavoro con i database.
- java.xml — lavoro con XML.
Se non usi XML, il modulo java.xml non verrà nemmeno incluso nel tuo runtime.
Nelle grandi applicazioni e librerie
Indispensabile per i sistemi enterprise, dove decine di team sviluppano parti di un monorepo. La modularità riduce i conflitti e semplifica la manutenzione.
Nei progetti personali
Anche in un pet project i moduli aiutano ad allenare il pensiero architetturale e a tenere il codice in ordine, evitando “spaghetti”.
5. Panoramica rapida della sintassi: module-info.java
La cosa più importante — il file module-info.java
Il file module-info.java si trova alla radice dei sorgenti del modulo e ne dichiara confini e dipendenze.
module my.awesome.module {
exports com.example.api; // Pacchetto esportato
requires java.sql; // Dipendenza da un modulo standard
}
Parole chiave principali:
- module <nome> — dichiara un modulo.
- exports <pacchetto> — rende il pacchetto accessibile ad altri moduli.
- requires <modulo> — dichiara una dipendenza da un altro modulo.
Esempio minimo:
module com.myproject.core {
exports com.myproject.core.api;
}
Esempio con dipendenza:
module com.myproject.app {
requires com.myproject.core;
requires java.sql;
}
Funzionalità aggiuntive (in breve): per la riflessione c’è opens, per i servizi — uses e provides ... with ... (in dettaglio — nelle lezioni avanzate).
6. Sfumature utili
I moduli sono come “passaporti” per le classi. Prima qualsiasi classe con il visto public poteva “viaggiare” per l’applicazione. Ora serve anche il “passaporto” modulare — l’export del pacchetto (exports). Senza di esso la classe resta “a casa”.
Dependency hell è un termine reale. Se hai visto ClassNotFoundException: com.google.common.base.Strings, ci sei già passato. I moduli sono pensati per scacciare quei “diavoletti” con confini e dipendenze rigorose.
Tutta Java ora è modulare. Anche se non scrivi i tuoi moduli, la piattaforma è già suddivisa in decine di moduli. Prova il comando:
java --list-modules
Come influisce sullo sviluppo
Gestisci esplicitamente la visibilità del codice, e i pacchetti interni non diventano più accessibili a tutti “per caso”. IDE e compilatore intercettano gli errori prima: se si dimentica di dichiarare una dipendenza — il progetto non verrà compilato. La manutenzione dei sistemi grandi si semplifica: è più facile capire chi dipende da chi e cosa si romperà in caso di modifiche.
7. Errori tipici nel passaggio ai moduli
Errore n. 1: hai dimenticato di esportare il pacchetto, ma la classe è public. Hai dichiarato la classe come public, ma non hai esportato il suo pacchetto — gli altri moduli non potranno usarla. Il compilatore segnalerà: “Il pacchetto non è esportato dal modulo”.
Errore n. 2: non hai dichiarato la dipendenza tramite requires. Stai usando un tipo da un altro modulo, ma hai dimenticato di aggiungere requires in module-info.java. Risultato — errore di compilazione “il modulo non riesce a trovare la classe richiesta”.
Errore n. 3: nomi di moduli duplicati. In un progetto grande due moduli hanno ricevuto per errore lo stesso nome. La JVM questo non lo perdona — rinomina e mantieni un naming uniforme.
Errore n. 4: ti sei dimenticato dei moduli standard. Per esempio, usi JDBC, ma non hai aggiunto requires java.sql;. In Java 8 “si vedeva comunque”, dalla 9 in poi — no.
Errore n. 5: tentativo di usare una classe interna di un altro modulo. Se il pacchetto non è esportato, anche un public-classe resta invisibile dall’esterno. Esporta il pacchetto o sposta l’API in uno strato “pubblico”.
GO TO FULL VERSION