mattone
dopo mattoneLA COLLANA
IT/EN
← Guida Java

Java 25 · 39/39

Conclusioni. Continuare a costruire, mattone dopo mattone

Java 25 · Guida completa · Bozza in revisione

Questa guida conserva lo stato di revisione del libro. La verifica editoriale e le prove di comprensione con lettori indipendenti sono ancora da completare.

Cerca in tutta la guida →

Per concludere

Se siete arrivati fino a qui, Java Mattone dopo Mattone è servito almeno a tenervi compagnia per un bel tratto di strada. Mi auguro anche che vi abbia lasciato qualcosa di più duraturo della sintassi di un costrutto o del nome di una classe del JDK. Le API cambiano; il modo di affrontare un problema, di spiegare una scelta e di lasciare il lavoro comprensibile a chi viene dopo resta.

Vorrei che di tutte le cose incontrate nel libro rimanessero l'amore per il buon codice e il desiderio di migliorare. Scrivere bene non è soltanto una soddisfazione personale. È un modo di rispettare i colleghi, l'azienda e i clienti: rende più facile correggere un difetto, adattare un requisito, capire se una modifica ha fatto danni. Un programma che funziona oggi e che domani nessuno osa toccare ha risolto soltanto una parte del problema.

In queste pagine abbiamo incontrato oggetti, tipi, collezioni, funzioni, thread, file, rete, strumenti di build e diagnosi. Non vorrei che ne usciste con una lista di nomi da ricordare. Ogni mattone ha avuto un motivo: riconoscere una difficoltà, vedere perché la soluzione precedente non bastava, costruire la successiva e verificarla. Se domani vi troverete davanti a una biblioteca, a un servizio HTTP o a un programma che si blocca, la domanda utile non sarà «quale capitolo devo copiare?». Sarà «qual è il confine del problema, quale proprietà devo conservare e quale prova mi dirà che ho scelto bene?».

Il metodo che portiamo fuori dal libro. Davanti a un compito nuovo, descriviamo prima il risultato osservabile e il caso che può fallire. Scegliamo poi la soluzione più piccola capace di rispettare quel contratto. La proviamo, ne scopriamo il limite e la adattiamo al contesto reale. Questo percorso non garantisce che non sbaglieremo; rende gli errori visibili e correggibili.

Dare un nome alla cosa giusta

Quando apriamo un file scritto da altri, le prime domande sono semplici: che cosa rappresenta questa classe? Che cosa promette il metodo? Da dove arriva questo dato? Se i nomi sono opachi, dobbiamo eseguire il programma nella testa prima ancora di capire quale problema affronta. var1 e var2 fanno risparmiare qualche battuta a chi scrive e ne chiedono molte di più a chi legge. Un nome come quantitaDisponibile non elimina la necessità di verificare la regola, ma ci dice quale domanda porre. Allo stesso modo, tang() può essere sufficiente dentro un contesto matematico strettamente dichiarato; calcolaAngoloTangente() racconta di più quando quel contesto non è evidente. Il nome migliore dipende dalla responsabilità e dal pubblico del codice, non dalla sua lunghezza.

Abbiamo visto questa scelta nel catalogo degli ultimi capitoli. MAX_RIGA e MAX_TITOLI dichiarano limiti diversi. Se li chiamassimo entrambi LIMITE, un lettore dovrebbe risalire ogni volta all'uso per capire quale risorsa stiamo controllando. Se invece un metodo si chiama leggi ma salva su database o invia richieste di rete, il nome mente. O cambiamo il nome per rappresentare l'effetto, o separiamo le responsabilità. Un buon nome è una piccola promessa verificabile.

Non trasformiamo però la regola in una gara al nome più lungo. variabileCheFaQuestoEQuello resta vago, anche se occupa mezza riga. Un nome utile indica il concetto del dominio o l'intenzione dell'operazione. Se non riusciamo a trovarlo, forse non abbiamo ancora capito abbastanza bene il compito. Fermarsi a discutere un nome può far emergere due significati che avevamo confuso nel codice.

È tutto un leggi e scrivi, ma bisogna sapere che cosa

Ricordo Gaetano Barbagallo, il mio primo capo progetto. Quando un compito mi sembrava enorme, mi riportava a una domanda molto concreta: che cosa leggiamo, che cosa trasformiamo e che cosa scriviamo? Negli anni quella semplicità mi è rimasta accanto. Non significa che ogni sistema sia banale. Significa che possiamo cercare i passaggi osservabili invece di lasciare tutto in un metodo di mille righe sperando che il debugger ci salvi.

Prendiamo ancora l'importazione del catalogo. Leggiamo caratteri da una sorgente; li separiamo in righe; normalizziamo i titoli; verifichiamo due limiti; infine restituiamo un risultato. Quando la sorgente diventa una risposta HTTP, aggiungiamo timeout e dimensione massima. Quando la destinazione diventa un database, decidiamo se il nuovo archivio debba apparire tutto insieme. Il percorso «leggi e scrivi» non è uno slogan che cancella i rischi: è una traccia per scoprire dove nascono e a chi spetta gestirli.

Una classe dovrebbe rappresentare una responsabilità che sappiamo raccontare. Un metodo dovrebbe offrire un'operazione con un contratto comprensibile. Se una parte del problema è troppo grande, dividerla può renderla verificabile. Ma dividere non vuol dire creare decine di classi minuscole senza una ragione. La composizione, le interfacce e talvolta l'ereditarietà sono strumenti, non premi per un diagramma complicato. Le usiamo quando rendono chiaro un confine o un comportamento da sostituire; altrimenti scegliamo la forma più diretta.

Dal problema alla struttura. Se una modifica a una regola dei titoli costringe a cambiare il client HTTP e il codice che disegna una schermata, abbiamo accoppiato responsabilità che cambiano per motivi diversi. Se invece dividiamo un algoritmo in dieci metodi che si chiamano l'un l'altro senza offrire un concetto riconoscibile, abbiamo soltanto spostato la complessità. Il criterio è chiedere quale decisione appartiene a ogni parte e quale prova la difende.

Lasciare nel sorgente ciò che serve

Una pratica che ho incontrato spesso è conservare la vecchia versione di un metodo come blocco commentato accanto a quella nuova. All'inizio sembra prudenza. Dopo qualche modifica, nessuno sa più se quel blocco rappresenti un'alternativa valida, un esperimento fallito o un pezzo dimenticato. Il lettore deve attraversare due programmi sovrapposti per capire quale dei due esegua la JVM. Il versionamento conserva la storia in modo più affidabile: possiamo ritrovare una versione, leggere perché fu cambiata e, se serve, recuperarla con una decisione esplicita. Nel sorgente corrente lasciamo il codice che serve al comportamento corrente.

Questo non vuol dire eliminare ogni commento. Un commento utile spiega ciò che il codice da solo non può mostrare bene: perché abbiamo imposto un limite di cento titoli, quale formato esterno dobbiamo rispettare, quale anomalia di una libreria abbiamo aggirato e quando potremo rimuovere quell'aggiramento. Un commento come // ciclo su tutto l'array accanto a for (int i = 0; i < array.length; i++) ripete l'ovvio. Peggio ancora, può diventare falso se il codice cambia e il commento resta. Se un'operazione è difficile da spiegare, proviamo prima a dare nomi migliori e a separare i passaggi; poi documentiamo il motivo che non si vede dalla sintassi.

Anche la Javadoc ha una responsabilità precisa. Nel capitolo sulla build abbiamo documentato ingressi, risultati ed errori del catalogo. Una pagina HTML generata con successo non prova che il contratto sia corretto: se il metodo ora chiude il Reader passato e la documentazione tace, abbiamo nascosto un effetto importante. La documentazione utile si legge insieme ai test e cambia insieme al comportamento.

Convenzioni e leggibilità sono un patto con gli altri

Le convenzioni di un progetto aiutano a prevedere dove trovare una classe, come si chiamerà un metodo e dove sono i test. Java ha forme riconoscibili per package, classi, metodi e costanti. Possiamo avere ragioni per discostarci da una convenzione, ma una deviazione casuale costringe ogni collega a ricominciare da capo. Restare coerenti nel progetto riduce il lavoro di interpretazione. È una forma di collaborazione, non una questione di gusto personale imposta dall'alto.

La leggibilità non coincide con il minor numero di righe. Comprimere dieci passaggi in una sola espressione può essere divertente; la domanda è se il prossimo lettore saprà individuare l'errore quando il risultato sarà sbagliato. D'altra parte, spezzare una regola semplice in un labirinto di helper può peggiorarla. Nel capitolo sulle Stream abbiamo visto che una pipeline funziona bene quando ogni passaggio ha un significato e l'ordine resta visibile. Se un ciclo esplicito rende più chiara la chiusura di una risorsa o il punto in cui si ferma la lettura, quel ciclo è una scelta migliore per quel problema.

Pensiamo anche a chi userà il codice senza aver letto questo libro. Un test dal nome preciso, un messaggio d'errore che distingue input malformato da file inaccessibile, un log che non espone titoli riservati e un JAR che parte con le istruzioni dichiarate sono tutti atti di comunicazione. La qualità del sorgente non finisce alla parentesi graffa: comprende il modo in cui il programma si presenta a chi deve eseguirlo, diagnosticarlo e modificarlo.

Scegliere lo strumento dopo aver riconosciuto il problema

Capita di sentirsi dire: «Usiamo questo framework, così facciamo prima». A volte sarà davvero la scelta giusta. Ma prima dobbiamo sapere che cosa deve fare il programma, quali vincoli ha e che cosa introduciamo adottando quello strumento. Se il requisito è leggere un file di due righe, aggiungere una piattaforma intera può complicare distribuzione e manutenzione. Se invece il programma deve gestire molti utenti, persistenza, sicurezza e osservabilità, fingere che basti la stessa classe didattica sarebbe altrettanto ingenuo.

Lo abbiamo visto con gli strumenti della concorrenza. Un AtomicInteger aiuta quando più thread aggiornano in modo atomico una variabile. Non trasforma automaticamente una prenotazione che modifica disponibilità, ordine e pagamento in una transazione indivisibile. Un thread virtuale permette di mantenere uno stile diretto per molte attività che attendono I/O, senza riservare un thread di piattaforma per tutta l'attesa; non accelera per magia un calcolo che satura la CPU. In entrambi i casi il nome dell'API arriva dopo aver descritto l'invariante o il collo di bottiglia. La soluzione giusta è quella che risponde alla domanda concreta e supera una prova pertinente.

Lo stesso vale per la Vector API, ancora incubator nel percorso Java 25 di questo volume, e per l'accesso alla memoria nativa: sono argomenti affascinanti, ma chiedono condizioni specifiche, costi e rischi. La scelta di studiarli è utile; la scelta di usarli in produzione richiede un problema che li giustifichi. Un benchmark senza carico rappresentativo o una build verde senza test sui confini non sono prove sufficienti.

Imparare dagli altri, poi rifattorizzare con una prova

Per crescere conviene leggere codice scritto da persone più esperte, ma anche codice diverso dal nostro. Guardiamo come un progetto gestisce gli errori, dove colloca i test, quali decisioni spiega nei commenti e quali lascia alla struttura. Chiediamo perché una soluzione è stata scelta, invece di copiarne soltanto la forma. Il codice aperto può insegnare molto; anche una revisione fra colleghi, fatta con rispetto, rende visibili ipotesi che da soli non avremmo notato. Chi riceve una domanda spesso è felice di spiegare una scelta: è una delle parti migliori del nostro mestiere.

Rifattorizzare significa migliorare la struttura mantenendo il comportamento osservabile concordato. Prima ci serve una prova che quel comportamento sia riconoscibile. Poi possiamo cambiare un passo alla volta: rinominare una variabile, estrarre una regola, sostituire una duplicazione reale, ricompilare e rieseguire le verifiche. Se introduciamo un limite nuovo, cambiamo invece il contratto: lo chiamiamo per quello che è e aggiungiamo test sui casi ammessi e rifiutati. Questa distinzione evita che una modifica funzionale si nasconda dietro l'etichetta rassicurante di «pulizia».

Il suggerimento che avevo scritto anni fa, prima farlo funzionare, poi ottimizzarlo, infine renderlo più bello, va letto con una precisazione: funzionare significa rispettare il requisito e avere una prova adeguata, non mostrare una schermata verde una volta sola. Misuriamo prima di ottimizzare; rileggiamo prima di rendere elegante; ripetiamo i test dopo. Quando un collega apre il cambiamento, deve poter capire quale problema abbiamo risolto e perché la nuova forma è più semplice da mantenere.

Continuare a seguire Java senza inseguire ogni novità

Amber, Loom, Panama e Valhalla rappresentano percorsi diversi di evoluzione della piattaforma. Questo volume usa Java 25 come riferimento. Alcune idee sono ormai parte stabile della piattaforma: per esempio i thread virtuali, descritti nel capitolo 20, sono stati finalizzati in Java 21; l'API Foreign Function and Memory, approfondita nel capitolo 28, è stata finalizzata in Java 22. Amber ha accompagnato funzionalità che oggi usiamo normalmente, come l'inferenza locale con var, i text block, i record e le gerarchie sealed. Valhalla continua a esplorare i value object; in Java 25 non trattiamo le value class come una funzionalità stabile da usare nei programmi del libro.

Come leggere una novità. Una pagina di progetto racconta una direzione; un JEP descrive obiettivi, compromessi e stato di una proposta; la specifica e la documentazione della release dicono che cosa è disponibile nel JDK che stiamo usando. Prima di adottare una novità controlliamo release, stato stabile/preview/incubator, opzioni necessarie, compatibilità e casi d'uso. Poi scriviamo un piccolo programma e lo proviamo nel nostro ambiente. «Annunciato» e «disponibile in Java 25» non sono sinonimi.

Non serve prevedere oggi quale sarà la prossima API indispensabile. Serve conservare la capacità di imparare. Quando una nuova release arriverà, partite dal problema che avete, leggete le note ufficiali e chiedetevi quale parte del vostro modello cambia davvero. Una sintassi più breve può rendere espressivo un caso; non rende automaticamente più chiara una decisione di dominio. Un nuovo strumento di diagnosi può mostrare più dati; non decide da solo quale comportamento è corretto. Il lavoro del programmatore rimane collegare quei dati alle domande giuste.

Spero che questo libro vi abbia dato il coraggio di fare quelle domande, di provare una soluzione, di scoprire dove fallisce e di costruire il mattone successivo. Non smettete di cercare di capire il codice che scrivete e quello che vi viene affidato. Il buon codice non è un punto d'arrivo: è una pratica condivisa, fatta di problemi riconosciuti bene, scelte spiegabili e verifiche che consentono agli altri di continuare.

Per verificare il percorso

Le attività conclusive chiedono di partire da un requisito, riconoscere i confini che può attraversare e giustificare una soluzione con prove. Scegli un programma del libro che hai modificato: racconta il problema iniziale, l'invariante che hai protetto, una prova positiva e un caso che potrebbe smentire la soluzione. Poi immagina che un collega debba estenderlo a una nuova fonte di dati. Quale parte del modello può riusare e quale contratto deve verificare di nuovo? Se riesci a rispondere senza limitarti al nome di una API, hai acquisito il metodo che tiene insieme i capitoli.

Riferimenti per seguire il linguaggio

Lo stato tecnico richiamato in queste Conclusioni è riferito a Java 25, verificato il 2 ottobre 2026. Il JEP 444 sui thread virtuali li registra come funzionalità consegnata in Java 21; la documentazione Oracle della Foreign Function and Memory API documenta la finalizzazione in Java 22 e rimanda al JEP 454. La proposta istitutiva di Project Amber ne chiarisce l'obiettivo; le specifiche Java 25 dei capitoli precedenti documentano le singole funzionalità. Il sito ufficiale di Project Valhalla distingue gli esperimenti successivi da ciò che è incluso in Java 25. Per gli aggiornamenti futuri occorre ricontrollare queste fonti e le note della release usata, non riutilizzare questa fotografia come se fosse senza data.

Prova gli esempi

Per eseguire i programmi serve JDK 25. Puoi scaricare i singoli file Java collegati nel capitolo oppure il progetto completo, che contiene istruzioni e uno script di avvio. Le spiegazioni confrontano anche l’output atteso: prevedilo prima di eseguire il programma.

Massimiliano Tarquini · CC BY-NC 4.0

Torna all’inizio ↑