Questa raccolta accompagna le stesure della fase 3. Le attività ancora assenti entreranno insieme ai rispettivi capitoli e sorgenti verificati. La versione di riferimento è Java 25 senza preview.
C07-A01 – Classe, oggetto e riferimento
Consegna. Senza eseguire il programma Libro.java, indica quante dichiarazioni di classe, quante variabili di tipo Libro e quanti oggetti Libro vengono creati nel suo main. Spiega perché primo == secondo è vero.
Criterio. La risposta distingue la dichiarazione della classe dalle due variabili e dall'unica istanza creata con new. Il confronto è spiegato come identità dei riferimenti, non come confronto dei titoli.
Suggerimenti. Cerca le righe che contengono new Libro. Poi leggi l'assegnazione di secondo da destra a sinistra: quale valore riceve? La figura 16 offre il modello logico.
Soluzione commentata. Il file dichiara una classe Libro. Nel metodo main compaiono due variabili di tipo Libro, primo e secondo. Solo la prima riga contiene new Libro, quindi nasce un oggetto. La seconda assegna a secondo lo stesso riferimento conservato in primo; per questo il confronto con == produce true.
C07-G01 – Aggiungere un metodo al libro
Consegna. In una copia di Libro.java, aggiungi public void registraPrestiti(int quanti) che chiama registraPrestito() tante volte quante indica il parametro. Nel main sostituisci la singola chiamata con primo.registraPrestiti(2). Prevedi l'output prima di compilare.
Criterio. Il metodo usa il parametro come limite di un ciclo; il titolo stampato resta lo stesso e il numero dei prestiti diventa 2. primo == secondo resta true, perché non è stato creato un secondo libro.
Suggerimenti. Parti da for (int i = 0; i < quanti; i++). Il metodo nuovo può riusare quello già esistente, senza modificare direttamente il campo nel main.
Soluzione commentata. registraPrestiti esegue for (int i = 0; i < quanti; i++) registraPrestito();. Con quanti uguale a due, lo stesso oggetto aumenta il contatore due volte; secondo.descrizione() stampa Java Mattone dopo Mattone - prestiti: 2. Il confronto dei riferimenti resta vero. Nel capitolo 8 discuteremo come rifiutare un parametro negativo se l'operazione diventa parte di un contratto pubblico.
C07-P01 – Due libri con lo stesso titolo
Consegna. Modifica una copia di Libro.java in modo che secondo riceva il risultato di una nuova espressione new Libro("Java Mattone dopo Mattone"). Prima di compilare, prevedi le due righe stampate. Poi compila con javac --release 25 -Xlint:all -d build Libro.java dalla cartella della copia e avvia con java -cp build Libro. Spiega perché la stessa stringa del titolo non basta a rendere veri i riferimenti confrontati con ==.
Criterio. Il programma stampa Java Mattone dopo Mattone - prestiti: 0 e false: il prestito registrato su primo non cambia il contatore dell'altro oggetto. La spiegazione distingue identità, stato e contenuto del titolo. Il comando di compilazione termina senza warning.
Suggerimenti. Conta le espressioni new. Disegna due riquadri, uno per ciascun oggetto, e scrivi il contatore iniziale in entrambi. La chiamata primo.registraPrestito() ha un destinatario preciso: segna su quale riquadro agisce.
Soluzione commentata. Dopo la modifica, primo e secondo puntano a due istanze diverse. Entrambe hanno lo stesso titolo, ma solo il contatore della prima passa a uno. secondo.descrizione() legge il contatore della seconda e stampa zero; primo == secondo è falso perché confronta l'identità. Nel capitolo C10 si vedrà come definire un criterio di uguaglianza del contenuto tramite equals, ma non va usato per rispondere alla domanda sull'identità.
C07-D01 – Un riferimento assente
Consegna. In una copia del main, inserisci Libro assente = null; seguito da System.out.println(assente.descrizione());. Prevedi se l'errore sarà segnalato dal compilatore o durante l'esecuzione, poi prova. Ripristina la copia originale alla fine.
Criterio. La compilazione riesce; la chiamata sul riferimento null provoca NullPointerException durante l'esecuzione. Il lettore sa indicare la riga che tenta la chiamata e spiega che null non identifica un oggetto Libro.
Suggerimenti. Il tipo di assente è valido e l'assegnazione di null è permessa. Chiediti quale oggetto dovrebbe eseguire descrizione(). Il capitolo C12 mostrerà come leggere lo stack trace senza nascondere il problema.
Soluzione commentata. Il compilatore può verificare che Libro abbia il metodo descrizione, ma non può garantire che una variabile di riferimento non sia null in ogni esecuzione. Quando la JVM arriva alla chiamata, manca l'istanza destinataria e lancia NullPointerException. Un controllo esplicito o un contratto che escluda null può prevenire l'errore; la scelta dipende dal significato del dato nel programma.
C08-A01 – Individuare l'invariante
Consegna. Leggi RegistroVoti.java. Scrivi le tre condizioni che devono essere vere dopo la costruzione di un registro valido. Indica quale riga impedisce al chiamante di cambiare i voti attraverso l'array passato al costruttore e quale impedisce di cambiarli attraverso il risultato di voti().
Criterio. Il nome dello studente non è assente né vuoto, esiste almeno un voto e ogni voto è fra zero e trenta. Le due direzioni della copia sono riconosciute separatamente.
Suggerimenti. Cerca le istruzioni che lanciano IllegalArgumentException. Poi segui l'array dal parametro al campo e dal campo al valore restituito. La figura 17 rappresenta questi passaggi.
Soluzione commentata. Il costruttore rifiuta uno studente nullo o composto soltanto da spazi, un array nullo o vuoto e qualunque voto fuori intervallo. this.voti = voti.clone() protegge l'ingresso; return voti.clone() protegge l'uscita. I campi private e final aiutano, ma non sostituiscono queste due copie.
C08-P01 – Dimostrare la copia difensiva
Consegna. Compila ed esegui RegistroVoti.java dalla cartella del sorgente con javac --release 25 -Xlint:all -d build RegistroVoti.java e java -cp build RegistroVoti. In una copia del file, elimina solo la copia nel costruttore e prevedi la media; ripristinala, elimina solo la copia nel metodo voti() e prevedi di nuovo la media. Prova entrambe le varianti separatamente. Infine passa un voto 31 al costruttore a un voto e descrivi quando e perché il programma si interrompe.
Criterio. Il programma originale stampa Ada: 27.0; senza copia all'ingresso stampa Ada: 19.0; senza copia all'uscita stampa Ada: 18.0. Il voto 31 è rifiutato prima che venga creato un registro valido. Le varianti sono isolate e il sorgente originale viene ripristinato.
Suggerimenti. Calcola le tre somme: 24+27+30, 0+27+30, 24+0+30. Chiediti quale assegnazione nel main agisce sull'array condiviso in ogni variante. Se due cambiamenti sono presenti insieme, non puoi attribuire con sicurezza la differenza a una sola copia mancante.
Soluzione commentata. Le copie rendono indipendente l'array del registro dagli array che il chiamante può modificare. Senza la prima copia, originali[0] = 0 cambia il dato interno e la media diventa 57/3, cioè 19.0. Senza la seconda, ricevuti[1] = 0 cambia il dato interno e la media diventa 54/3, cioè 18.0. Il costruttore alternativo controlla 31 e lancia IllegalArgumentException prima di delegare; nel capitolo C12 vedremo come leggere e gestire correttamente l'eccezione.
C08-R01 – Togliere una validazione duplicata
Consegna. In una copia di RegistroVoti.java, estrai il controllo dell'intervallo 0-30 in un metodo private static void validaVoto(int voto). Usalo sia nel ciclo del costruttore con array sia nel costruttore con un solo voto, prima di this(...). Mantieni invariati l'output Ada: 27.0 e il rifiuto del voto 31.
Criterio. Il programma compila su Java 25 senza preview; i due costruttori applicano la stessa regola. La revisione non elimina le copie difensive né cambia la media dei dati validi.
Suggerimenti. Il metodo di supporto non deve leggere campi dell'istanza; per questo può essere static e può essere chiamato nel prologo del costruttore flessibile.
Soluzione commentata. Il metodo validaVoto lancia IllegalArgumentException("voto fuori intervallo") quando voto < 0 || voto > 30. Nel ciclo si chiama validaVoto(voto); nel costruttore singolo la chiamata precede this(studente, new int[] {voto}). L'array viene comunque ricontrollato nel costruttore delegato: la prima verifica serve a rifiutare l'argomento prima della delega, la seconda conserva il contratto del costruttore con array.
C09-A01 – Leggere due relazioni
Consegna. Nel programma dei veicoli, indica quali classi hanno una relazione «è un» con Veicolo e quale oggetto Auto conserva come parte. Spiega perché Motore non estende Veicolo.
Criterio. La risposta identifica Auto e Bicicletta come sottoclassi e Motore come componente dell'auto; collega la scelta al significato del dominio.
Suggerimento. Leggi extends e poi il tipo del campo motore. Confronta le due frecce nella figura 19.
Soluzione commentata. Auto e Bicicletta specializzano Veicolo. Auto conserva un Motore perché un motore è una sua parte; non è un tipo di veicolo.
C09-P01 – Dispatch e casi chiusi
Consegna. Prevedi le tre righe stampate da DemoVeicoli.java. Poi sostituisci la costruzione dell'auto con new Bicicletta("B"), esegui di nuovo e descrivi perché il ramo instanceof Auto non stampa una riga. In una copia separata elimina il caso Bicicletta dallo switch e compila.
Criterio. La prima esecuzione stampa Auto A con motore elettrico, elettrico, auto. La variante con bicicletta stampa la sua descrizione e bicicletta. La variante senza caso non compila per mancata esaustività.
Suggerimento. Il tipo dichiarato di mezzo resta Veicolo; segui la classe dell'oggetto creato. Leggi separatamente il controllo instanceof e lo switch.
Soluzione commentata. Il metodo ridefinito viene scelto in base all'oggetto concreto. Con Bicicletta, il pattern per Auto fallisce e il ramo viene saltato; lo switch sceglie Bicicletta. Togliere quel caso lascia scoperto un sottotipo ammesso dalla gerarchia sealed, quindi il compilatore rifiuta l'espressione.
C09-D01 – Costruire base e figlio
Consegna. Prevedi le due righe del programma sull'ordine dei costruttori. Cambia poi 7 in 12 e verifica quali righe cambiano.
Criterio. L'ordine resta base, poi figlio: 12: la parte base viene costruita prima del completamento del figlio.
Suggerimento. Parti da new FiglioOrdine(12) e segui super() prima dell'assegnazione al campo valore.
Soluzione commentata. Il costruttore della base stampa base; quello del figlio assegna il campo e stampa figlio: 12. Il valore cambia soltanto la seconda riga.
C09-R01 – Proteggere una gerarchia valida
Consegna. Rivedi una copia di DemoVeicoli.java affinché Veicolo rifiuti un nome nullo o vuoto e Auto rifiuti un Motore nullo. Mantieni le tre righe dell'esecuzione valida e verifica separatamente i due nuovi casi errati. Spiega in quale costruttore appartiene ciascun controllo.
Criterio. new Bicicletta(" ") e new Auto("A", null) non creano oggetti validi; l'esecuzione con Auto("A", new Motore("elettrico")) resta invariata. La sottoclasse non tenta di assegnare direttamente il campo privato nome della base.
Suggerimenti. La classe base è responsabile del proprio nome. Il controllo del motore è specifico di Auto. Ricorda che super(nome) precede l'assegnazione del campo della sottoclasse.
Soluzione commentata. Il costruttore Veicolo può lanciare IllegalArgumentException quando nome == null || nome.isBlank(), poi assegnare il campo. Nel costruttore di Auto, dopo super(nome), si controlla motore == null prima di assegnarlo. Il percorso positivo usa gli stessi dati e stampa le stesse tre righe; i nuovi controlli rendono esplicito il contratto della gerarchia.
C10-A01 – Identità e valore
Consegna. Prima di avviare DemoValore.java, prevedi le prime quattro righe. Spiega perché la terza riga non basta, da sola, a dire che due etichette sono uguali.
Criterio. Le righe sono false, true, true, Etichetta[Java]. La risposta distingue identità, uguaglianza di dominio e hash, e sa che hash uguali possono avere oggetti diversi.
Suggerimento. Conta le espressioni new Etichetta; poi leggi da quali campi sono calcolati equals e hashCode.
Soluzione commentata. Due costruzioni danno due oggetti, perciò == è falso. I testi sono uguali e equals è vero. Gli hash devono coincidere per oggetti uguali, ma una collisione può far coincidere hash di oggetti non uguali. toString produce la descrizione leggibile.
C10-D01 – Un contratto rotto
Consegna. In una copia del sorgente sostituisci il corpo di hashCode() con return System.identityHashCode(this);. Spiega quale regola del contratto viene violata, indipendentemente dai numeri osservati in una singola esecuzione. Ripristina il sorgente.
Criterio. Il lettore riconosce che due istanze uguali secondo equals devono avere lo stesso hash, mentre un hash legato all'identità non garantisce questa proprietà.
Suggerimento. Non basare la conclusione su una coppia di numeri vista una sola volta: confronta le definizioni dei due metodi.
Soluzione commentata. equals confronta testo, ma identityHashCode usa l'identità degli oggetti. Il codice non garantisce più che oggetti uguali producano lo stesso hash; un'eventuale coincidenza numerica non ripara il contratto.
C10-P01 – Un'enumerazione che conosce una regola
Consegna. Esegui DemoValore.java e osserva l'ultima riga. Aggiungi nel main una stampa di Stato.IN_PRESTITO.consentePrestito(). Prevedi il risultato, poi modifica il metodo per accettare il prestito anche da un eventuale nuovo stato RICONSEGNATO e spiega perché la modifica resta dentro l'enumerazione.
Criterio. Prima della modifica il metodo restituisce true per NUOVO e false per IN_PRESTITO. Dopo aver aggiunto RICONSEGNATO, restituisce true anche per quel valore, senza confrontare stringhe nel chiamante.
Suggerimenti. Il metodo può restituire il risultato di una condizione booleana; non serve costruire un oggetto nuovo per ogni chiamata.
Soluzione commentata. Con due stati, return this == NUOVO; distingue il libro nuovo da quello già prestato. Dopo l'aggiunta del terzo stato, return this == NUOVO || this == RICONSEGNATO; esprime la nuova regola nel tipo che rappresenta gli stati. Il codice esterno chiede consentePrestito() senza conoscere i nomi interni delle costanti.
C11-A01 – Due misure di una stringa
Consegna. Spiega le righe 2 e 1 stampate da DemoStringhe.java per il simbolo U+1F600. Indica che cosa conta ogni metodo e perché nessuno dei due è, in generale, un contatore dei simboli percepiti dall'utente.
Criterio. length() conta due unità UTF-16; codePointCount conta un code point. La risposta riconosce la possibile presenza di sequenze di più code point per un unico simbolo visibile.
Suggerimento. Usa la figura 21 per seguire la coppia di unità. Pensa poi a una lettera e a un accento combinante.
Soluzione commentata. Il code point supplementare dell'esempio è codificato con due unità UTF-16. La differenza fra unità, code point e simboli percepiti emerge con testo che include combinazioni o emoji composte.
C11-P01 – Immutabilità osservabile
Consegna. Sostituisci la stampa di testo.strip() con la sola chiamata testo.strip();, poi ristampa testo. In una seconda prova scrivi testo = testo.strip(); e ristampa. Motiva la differenza.
Criterio. La prima prova conserva gli spazi; la seconda stampa il testo pulito perché la variabile riceve il nuovo valore. Non si descrive strip() come modifica dell'oggetto originale.
Suggerimento. Segui quale riferimento viene assegnato a testo dopo la chiamata.
Soluzione commentata. Una String è immutabile. Chiamare strip() senza usare il risultato lascia testo com'era; assegnare il risultato fa riferire la variabile alla stringa pulita.
C11-D01 – Righe di un text block
Consegna. Compila DemoTextBlock.java e osserva le due righe stampate. Sposta il delimitatore di chiusura immediatamente dopo Java! e confronta l'output come sequenza di caratteri, includendo la possibile interruzione di riga finale.
Criterio. La risposta distingue testo visibile e carattere di fine riga; riconosce che la posizione del delimitatore influisce sul valore della stringa.
Suggerimento. Una stampa normale può nascondere la differenza finale: aggiungi System.out.print("[fine]") dopo il messaggio per renderla visibile.
Soluzione commentata. Nel sorgente originale la chiusura è su una nuova riga e il valore termina con un'interruzione di riga; [fine] compare sulla riga successiva. Con la chiusura subito dopo Java!, il valore non contiene quell'ultima interruzione e [fine] segue Java! sulla stessa riga.
C12-A01 – Ordine degli eventi
Consegna. Prevedi l'output del programma sulle risorse prima di avviarlo. Identifica l'eccezione principale e quella soppressa.
Criterio. Le quattro righe sono lavoro, chiusura, principale: errore durante il lavoro, secondaria: errore durante la chiusura. Il lettore spiega perché close() viene chiamato anche dopo il primo errore.
Suggerimento. Segui prima il corpo del try, poi la chiusura automatica, infine il catch.
Soluzione commentata. Il corpo chiama usa() e poi lancia l'errore di lavoro. Prima del catch, Java chiude la risorsa; anche close() lancia un errore, che viene conservato come soppresso. Il catch riceve l'errore del corpo e stampa entrambi.
C12-P01 – Se fallisce solo la chiusura
Consegna. In una copia del sorgente elimina soltanto il throw new IOException("errore durante il lavoro") dal corpo del try. Prevedi l'output e il numero di eccezioni soppresse; poi verifica.
Criterio. L'output è lavoro, chiusura, principale: errore durante la chiusura; non ci sono eccezioni soppresse.
Suggerimento. Chiediti quale eccezione esiste quando il corpo termina normalmente, prima di chiamare close().
Soluzione commentata. Il corpo non lancia più errori. La chiamata automatica a close() fallisce e quell'errore diventa il principale; getSuppressed() restituisce un array vuoto.
C12-D01 – Eccezione del dominio e causa
Consegna. Esegui DemoVoto.java. Per ciascuno dei tre input indica se il metodo restituisce un voto o lancia VotoNonValidoException. Spiega perché solo uno degli errori mostra anche una causa.
Criterio. 27 produce voto: 27; 31 produce errore: fuori intervallo: 31; xx produce errore: non è un numero: xx e causa: NumberFormatException.
Suggerimento. Segui prima Integer.parseInt, poi il controllo dell'intervallo. Distingui l'eccezione creata direttamente dalla conversione di un errore ricevuto dalla libreria.
Soluzione commentata. 27 supera entrambi i controlli. 31 è un intero ma non un voto ammesso, quindi il metodo crea direttamente l'eccezione del dominio. xx non è convertibile; il catch riceve NumberFormatException e la conserva come causa nella nuova eccezione.
C12-D02 – Leggere una traccia
Consegna. Riprendi la variante con Libro assente = null dell'attività C07-D01 ed eseguila senza un catch. Nello stack trace individua tipo dell'eccezione, messaggio e prima riga del tuo sorgente. Descrivi quale riferimento manca.
Criterio. La diagnosi cita NullPointerException e la chiamata a assente.descrizione(), e distingue il punto di manifestazione dall'origine del valore null.
Suggerimento. Parti dalla prima riga della traccia, poi cerca il nome della tua classe nelle righe successive.
Soluzione commentata. Il tipo è NullPointerException: la variabile assente contiene null, quindi nessun oggetto può ricevere descrizione(). La riga della chiamata identifica dove l'errore emerge; l'assegnazione precedente spiega perché quel riferimento è assente.
C12-D03 – Seguire l'errore della pila
Consegna. Compila insieme Pila.java e DemoPropagazione.java. Senza eseguire, prevedi il messaggio e i quattro nomi di metodo stampati. Poi inserisci pila.inserisci(9); prima del try: quale percorso verrà seguito e perché il catch non stamperà nulla?
Criterio. Con pila vuota l'output è pila vuota, estrai, leggi, mostra, main. Con un elemento viene stampato 9; l'estrazione riesce e nessuna eccezione raggiunge il gestore.
Suggerimenti. La figura 23 mostra il percorso dell'eccezione, non quello del valore restituito. Parti dal metodo che lancia e risali ai chiamanti.
Soluzione commentata. estrai rifiuta la pila vuota; leggi e mostra non hanno un gestore, quindi l'eccezione arriva a main. La traccia elenca prima il punto di lancio e poi i chiamanti. Dopo l'inserimento, estrai restituisce 9 e la catena termina normalmente: il ramo catch non viene eseguito.