Lezione 4 · Canale 2 · mercoledì 30 settembre 2026
Introduzione a Java: classi, JVM e memoria
Progettazione del Software
Riassunto
Il ciclo di vita del software mostra come esigenze, specifiche, realizzazione, documentazione e consegna possono differire dalle intenzioni iniziali. Java è nato nel 1995 come linguaggio orientato agli oggetti, con supporto ai paradigmi imperativo e funzionale. Il suo successo si deve alla portabilità tramite JVM, alle ricche librerie, al supporto della rete e alla sicurezza offerta. Un programma Java è organizzato in classi; il metodo main statico è il punto d'ingresso convenzionale e risolve il problema del bootstrap. Il compilatore javac produce bytecode indipendente dall'architettura; la JVM lo esegue sulla piattaforma ospite. Gli oggetti collaborano invocando metodi nella forma oggetto.metodo(parametri). Le variabili richiedono dichiarazioni di tipo esplicite; possono contenere valori primitivi o riferimenti a oggetti. Lo stack contiene i record di attivazione e le variabili locali; l'heap contiene gli oggetti. La garbage collection recupera automaticamente la memoria degli oggetti non più raggiungibili.
Concetti chiave
- Ciclo di vita del software — Insieme delle attività con cui il software viene ideato, progettato, realizzato, documentato, installato e gestito nel tempo.
- Firma o segnatura — Definizione di una funzione o di un metodo tramite tipo restituito, numero, tipi e nomi dei parametri formali.
- Classe — Struttura che contiene metodi e, nel modello a oggetti presentato, descrive gli oggetti che possono essere creati a runtime.
- Metodo — Funzione definita all'interno di una classe; costituisce un servizio che gli oggetti della classe possono offrire.
- Metodo
main— Punto d'ingresso convenzionale di un programma Java; la forma introdotta èpublic static void main(String[] args). - Bootstrap — Problema dell'avvio della computazione a oggetti: occorre un punto di partenza che possa essere invocato senza avere già un oggetto. La metafora lo identifica con il metodo
mainstatico. - Bytecode — Istruzioni prodotte dal compilatore Java per la macchina virtuale, indipendenti dall'architettura fisica del processore.
- JVM (Java Virtual Machine) — Ambiente che carica ed esegue il bytecode Java sulla piattaforma ospite.
- IDE (Integrated Development Environment) — Ambiente integrato che facilita la scrittura, la compilazione e l'esecuzione del codice, richiamando strumenti come il compilatore.
- CLI e GUI — La CLI è l'interfaccia a linea di comando del sistema operativo; la GUI è l'interfaccia grafica a finestre.
- Oggetto di invocazione — Oggetto sul quale viene chiamato un metodo, nella forma
oggetto.metodo(parametri). - Parametro formale e parametro attuale — Il parametro formale è dichiarato nella segnatura; il parametro attuale è il valore fornito quando il metodo viene invocato.
- Stringa (
String) — Oggetto che rappresenta una sequenza di caratteri; i valori letterali sono delimitati da doppi apici. - Variabile — Identificatore dichiarato con un tipo e usato per accedere a un valore o, nel caso dei tipi di classe, a un riferimento a un oggetto.
- L-value e R-value — Nel modello illustrato, l'L-value identifica la variabile come destinazione dell'assegnazione; l'R-value è il valore letto da un'espressione.
- Stack e heap — Aree descritte nel modello didattico: lo stack contiene i record di attivazione e le variabili locali, mentre gli oggetti sono collocati nell'heap.
- Garbage collection — Recupero automatico della memoria occupata da oggetti non più raggiungibili tramite riferimenti.
Sviluppo
Complessità del processo software
Una vignetta classica illustra diverse rappresentazioni dello stesso progetto: ciò che il cliente spiega, ciò che il project leader comprende, ciò che l'analista progetta, ciò che il programmatore realizza, ciò che viene venduto e documentato, ciò che operations installa e supporta e, infine, ciò che il cliente desiderava davvero. L'immagine sottolinea la distanza che può crearsi tra esigenze, promesse e prodotto consegnato.
La raccolta dei requisiti e l'analisi sono fasi particolarmente difficili: il cliente può avere un'idea ma descriverla in modo diverso. Anche esigenze di business, promesse commerciali, tempi e budget condizionano il risultato. Il software può poi essere gestito per molti anni, perciò serve un ciclo di vita sostenibile dal punto di vista industriale.
Java: origine e paradigmi
Java è nato nel 1995 come linguaggio orientato agli oggetti. Supporta anche il paradigma imperativo, in cui l'unità fondamentale di organizzazione è la funzione, e il paradigma funzionale. Nell'approccio a oggetti il ragionamento si concentra sugli oggetti e sulle informazioni.
Portabilità e librerie
La portabilità è una delle ragioni principali del successo di Java: lo stesso programma può essere eseguito su sistemi diversi quando è disponibile una JVM per la piattaforma. Le interfacce Java possono adattarsi all'aspetto grafico del sistema ospite, per esempio usando widget diversi su Windows e macOS.
Un'altra ragione della diffusione è la ricchezza delle librerie, che offre componenti riutilizzabili per realizzare molte funzionalità. Questo approccio è divenuto importante ed è stato adottato anche da altri linguaggi.
Rete, sicurezza e applet
Java è adatto allo sviluppo di applicazioni connesse in rete. La facilità nello sviluppo di rete si contrappone alla maggiore complessità della programmazione di rete in C, dove è necessario occuparsi più direttamente dei dettagli della macchina.
La JVM offre un livello di protezione rispetto al sistema ospite. Storicamente, ciò contribuì alla diffusione di Java sul web. Le applet erano applicazioni scaricate tramite una pagina web, che consentivano interazioni più avanzate delle pagine statiche dell'epoca. La stessa filosofia è apparsa successivamente nel codice JavaScript eseguito nel browser.
Struttura iniziale di un programma Java
Il codice Java viene organizzato in classi. Una classe viene denominata e contiene metodi che racchiudono le istruzioni. La parola chiave public indica che la classe è accessibile da altre parti del codice.
La forma del metodo di avvio è:
public static void main(String[] args)
La segnatura specifica il tipo restituito e i parametri. In questa firma, public rende il metodo accessibile, static permette di invocarlo senza creare prima un oggetto, void indica che non restituisce un valore e String[] args è un array di stringhe.
main e il problema del bootstrap
La necessità di main statico risponde al problema del bootstrap: se la computazione a oggetti procede tramite oggetti che invocano altri oggetti, serve un punto iniziale che possa partire senza un oggetto già disponibile. La metafora usata è quella di un punto di creazione iniziale. Il sistema operativo avvia il launcher java, che crea la JVM, carica la classe e invoca main.
Gli elementi di args sono i parametri passati al programma da riga di comando. Ogni argomento viene ricevuto come stringa.
Stampa sulla console
Il primo esempio usa System.out.println("Questo è il mio primo programma Java"). System.out rappresenta l'uscita verso la console e println stampa il testo terminando la riga. Il metodo print svolge una stampa senza andare a capo.
Corpo di classi e metodi, commenti
La dichiarazione iniziale, come public class Primo, è l'intestazione della classe; il codice racchiuso tra le parentesi graffe ne costituisce il corpo. Un metodo ha una firma, parentesi graffe e un corpo contenente le istruzioni. I metodi rappresentano i servizi descritti dalla classe.
I commenti possono apparire in varie parti del codice. // introduce un commento su una riga, mentre /* ... */ delimita un commento su più righe. Non modificano il comportamento del programma e il compilatore li ignora. Sono utili per documentare la funzionalità del codice.
Spazi bianchi e formattazione
In Java spazi, righe vuote e tabulazioni servono a rendere il codice leggibile, ma l'indentazione non determina il significato del programma. La struttura è stabilita soprattutto dalle parentesi e dalle dichiarazioni. Java si comporta in questo modo similmente a C, a differenza di Python dove l'indentazione è significativa.
File sorgente e nomi delle classi
Il codice sorgente viene salvato in un file .java. Se la classe pubblica si chiama Primo, il file deve chiamarsi Primo.java. Java distingue lettere maiuscole e minuscole: Primo e primo sono nomi diversi. La corrispondenza tra nome della classe pubblica e nome del file è fonte comune di errori.
Compilazione e bytecode
Il compilatore si chiama javac; per esempio, javac Primo.java compila il sorgente e produce un file di bytecode, di norma Primo.class. Il bytecode è un insieme di istruzioni per un processore virtuale, anziché codice macchina destinato direttamente a una CPU specifica.
JVM e portabilità a runtime
La JVM esegue il bytecode sulla macchina ospite, traducendolo o compilando parti del programma in istruzioni native secondo l'implementazione disponibile. In questo modo il programma può mantenere lo stesso bytecode su processori diversi, come ARM e x86, purché sia disponibile un runtime adatto. Il comando java Primo avvia il programma compilato; si indica il nome della classe, senza aggiungere .class.
Linguaggi come Kotlin possono essere compilati per l'ambiente Android e usano strumenti e runtime specifici della piattaforma. Le applicazioni Android contengono bytecode specifico dell'ambiente, non semplicemente file .class eseguiti da una JVM standard.
Editor, IDE e ambienti di sviluppo
Per scrivere Java si possono usare editor semplici o ambienti come Eclipse, Apache NetBeans e Visual Studio Code. Un IDE offre pulsanti e funzioni integrate; per compilare ed eseguire richiama gli strumenti sottostanti.
CLI, GUI e dispositivi senza finestre
La CLI (Command Line Interface) è l'interfaccia a linea di comando; la GUI (Graphical User Interface) è l'interfaccia grafica a finestre. La CLI è importante quando si lavora su server o dispositivi IoT che non hanno una GUI, per esempio accedendo da remoto tramite SSH. Saper usare gli strumenti da terminale permette di compilare anche in ambienti privi di finestre.
Errori di compilazione e di esecuzione
Gli errori di sintassi e di semantica statica sono rilevati dal compilatore. Gli errori di semantica dinamica emergono durante l'esecuzione; la divisione per zero e l'accesso fuori dai limiti di un array ne sono esempi.
In Java le eccezioni e gli errori sono rappresentati da oggetti. Le classi Error ed Exception sono distinte nella gerarchia di Throwable: Exception non è una sottoclasse di Error. Si possono anche definire classi di eccezioni proprie.
Oggetti e computazione
A runtime il programma è descritto come una società di oggetti che collaborano invocando metodi secondo un protocollo. La classe serve a definire il codice; durante l'esecuzione operano gli oggetti, che vivono nella memoria e usano la CPU della macchina. Una metafora utile è quella di un videogioco: il sistema operativo è il terreno di gioco e gli oggetti sono i personaggi che interagiscono.
Invocazione di un metodo
Per chiedere a un oggetto di svolgere un'operazione si scrive l'oggetto, un punto, il nome del metodo e gli eventuali parametri: oggetto.metodo(parametri). L'oggetto è il ricevente dell'invocazione; il metodo è il servizio richiesto e i valori passati sono i parametri attuali. La scelta di chi debba eseguire un'operazione parte quindi dalla domanda «chi lo fa?».
System.out come oggetto
Nell'espressione System.out.println("ciao"), System.out è l'oggetto usato per scrivere sulla console e println è il metodo invocato. print svolge una stampa senza andare a capo. Se si tenta di invocare un metodo inesistente, come System.out.pippo, il compilatore segnala l'errore: l'oggetto può offrire soltanto i metodi definiti per esso.
Stringhe e convenzioni sui nomi
String è una classe Java e una stringa letterale si scrive tra doppi apici. I nomi delle classi iniziano convenzionalmente con una lettera maiuscola; rispettare le convenzioni rende il codice più leggibile e riconoscibile durante la manutenzione e la peer review.
Una String può contenere spazi. Gli spazi separano normalmente gli argomenti non quotati della riga di comando, ma non impediscono a una stringa di contenere più parole.
Metodi di String e indici
La classe String fornisce diversi metodi:
concatriceve un'altra stringa e restituisce una nuova stringa concatenata a quella su cui viene chiamato.lengthrestituisce la lunghezza della stringa.substringrestituisce una sottostringa a partire da un indice oppure tra un indice iniziale e uno finale.
Gli indici iniziano da zero: il primo carattere ha indice 0. La convenzione è adottata da Java e da molti linguaggi.
Argomenti da riga di comando
Gli argomenti forniti dopo il nome della classe vengono raccolti nell'array args. Normalmente gli spazi separano gli argomenti; le virgolette permettono di passare più parole come un unico argomento. Per esempio, gli elementi ricevuti si possono consultare tramite args[0], args[1] e così via. Anche qui gli indici partono da zero.
Variabili e tipi
Una variabile è un identificatore associato a un tipo. L'analogia di una grande cassettiera aiuta: la variabile è l'etichetta del cassetto e il valore è ciò che viene collocato al suo interno. Questa astrazione evita di ragionare subito sui dettagli fisici della memoria; per operazioni che richiedono controllo fine sui bit il C è più adatto.
Java richiede che il tipo sia dichiarato esplicitamente. Si distinguono tipi primitivi e tipi di classe. I tipi primitivi si raggruppano nelle categorie degli interi, dei reali, dei caratteri e dei booleani: Java ha otto tipi primitivi, byte, short, int, long, float, double, char e boolean. I nomi dei tipi primitivi si scrivono con iniziale minuscola.
Una variabile di tipo classe contiene un riferimento all'oggetto, non l'oggetto stesso. Le classi possono provenire dalle librerie oppure essere definite dal programmatore.
Assegnazione, L-value e R-value
L'assegnazione usa =. Nell'esempio int A, B, C;, si dichiarano tre variabili intere; poi si possono assegnare valori ad A e B e calcolare C = A + B. Per calcolare il risultato si leggono i valori delle variabili sorgente e si memorizza il risultato nella variabile di destinazione.
L'R-value è il valore letto da un'espressione; l'L-value è la variabile usata come destinazione. L'analogia della cassettiera distingue ciò che si legge dal contenuto del cassetto da ciò che identifica il cassetto in cui scrivere.
Tipizzazione e inizializzazione
Java richiede dichiarazioni di tipo esplicite; questo si contrappone a linguaggi come Python, dove il tipo viene associato implicitamente al valore assegnato. Una variabile locale deve essere inizializzata prima di essere letta. Per esempio, dichiarare String s; e poi passarla a println senza averle assegnato un valore produce un errore di compilazione.
Per una variabile di tipo classe, il valore è un riferimento all'oggetto; prima dell'assegnazione la variabile locale non può essere usata.
Allocazione automatica e garbage collection
In Java l'allocazione degli oggetti è gestita dalla JVM e il programmatore non deve chiamare malloc come in C. Anche il recupero della memoria è automatico: la garbage collection può liberare un oggetto quando non è più raggiungibile da riferimenti attivi. Questo modello si contrappone alla gestione manuale della memoria in C.
Stack e heap nel modello didattico
Lo stack viene descritto come una struttura a pila che contiene i record di attivazione delle funzioni e le variabili locali. L'heap è l'area in cui si trovano gli oggetti. Nel modello illustrato, l'avvio di main crea il primo contesto; le variabili locali possono contenere riferimenti agli oggetti collocati nell'heap.
La metafora della pila a strati spiega perché i record di attivazione vengono gestiti secondo l'ordine delle chiamate. Lo schema è un modello introduttivo: la JVM gestisce internamente memoria e oggetti, senza richiedere al programmatore di manipolare direttamente gli indirizzi.
Recupero degli oggetti non raggiungibili
Quando una variabile locale scompare e nessun'altra parte del programma mantiene un riferimento a un oggetto, quell'oggetto diventa non raggiungibile e la JVM può recuperarne la memoria. Se invece esistono altri riferimenti, l'oggetto resta accessibile. Questo è il principio della garbage collection automatica.