Fondamenti di C++ - Cay Horstmann Appendice: Da C++ a Java Obiettivi didattici Gli argomenti affrontati in questa appendice sono: • “Hello, World” in Java; • commenti di documentazione; • tipi primitivi; • istruzioni di controllo del flusso; • riferimenti agli oggetti; • passaggio dei parametri; • pacchetti; • gestione di base delle eccezioni; • liste di array e array; • stringhe; • lettura dell’input; • campi e metodi statici; • stile di programmazione. L’obiettivo di questa appendice è quello di illustrare gli elementi di Java, o almeno di offrirne un breve compendio, partendo dal presupposto della conoscenza di un linguaggio di programmazione orientato agli oggetti. In particolare, è preferibile avere una certa familiarità con i concetti di classe e di oggetto. Se si conosce C++ e si capiscono le classi, le funzioni membro e i costruttori, sarà più facile fare il passaggio a Java. 2 Appendice A.1 “Hello, World!” in Java Le classi sono gli elementi fondanti dei programmi Java. Per iniziare, ecco un esempio classico ma comune di classe: Greeter.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Greeter { public Greeter(String aName) { name = aName; } public String sayHello() { return "Hello, " + name + "!"; } private String name; } Questa classe è costituita da tre elementi: • Un costruttore Greeter(String aName) che viene utilizzato per costruire gli oggetti nuovi della classe. • Un metodo sayHello() che può essere applicato agli oggetti della classe (Java utilizza il termine metodo per indicare una funzione definita in una classe). • Un campo name che è presente in ogni oggetto della classe. Ogni elemento è marcato come public o private. I dettagli di implementazione, come il campo (name) sono privati. Le funzioni concepite per l’utente della classe (come il costruttore e il metodo sayHello) sono private. La classe stessa viene dichiarata come public; la ragione verrà spiegata nel paragrafo relativo ai pacchetti. Per costruire un oggetto si utilizza l’operatore new seguito da una chiamata al costruttore. new Greeter("World") L’operatore new restituisce l’oggetto costruito; più precisamente, restituisce un riferimento a quell’oggetto, come verrà illustrato nel dettaglio nel paragrafo sui riferimenti agli oggetti. Su questo oggetto e possibile invocare un metodo. La chiamata new Greeter("World").sayHello() restituisce la stringa "Hello, World!", la concatenazione delle stringhe "Hello, ", name, e "!". Solitamente si memorizza il valore restituito dall’operatore new in una variabile di oggetto Greeter worldGreeter = new Greeter("World"); Da C++ a Java 3 Si invoca un metodo come String greeting = worldGreeter.sayHello(); Ora che si è visto come definire una classe, si costruirà un primo programma Java, ovvero il tradizionale programma Java che visualizza le parole “Hello, World!” sullo schermo. Si definirà quindi una seconda classe, GreeterTest, per produrre l’output. GreeterTest.java 1 2 3 4 5 6 7 8 9 public class GreeterTest { public static void main(String[] args)) { Greeter worldGreeter = new Greeter("World"); String greeting = worldGreeter.sayHello(); System.out.println(greeting); } } Questa classe ha un metodo main, necessario per poter avviare l’applicazione Java. Il metodo main è statico, ossia non opera su un oggetto. Dopotutto, quando l’applicazione viene avviata non ci sono ancora oggetti. È compito di questo metodo costruire gli oggetti necessari per eseguire il programma. Il parametro args del metodo main contiene gli argomenti della riga di comando (che verranno discussi nel paragrafo sugli array). Si sono così già viste le prime due istruzioni all’interno del metodo main. Queste istruzioni costruiscono un oggetto Greeter, lo memorizzano in una variabile di oggetto, invocano il metodo sayHello e catturano il risultato in una variabile di stringa. L’istruzione finale utilizza il metodo println dell’oggetto System.out per stampare il messaggio e aggiunge una nuova riga allo stream dell’output standard. Per costruire ed eseguire il programma, si inserisca la classe Greeter in un file Greeter.java e la classe GreeterTest in un file GreeterTest.java separato. Le direttive per la compilazione e l’esecuzione del programma dipendono dal proprio ambiente di sviluppo. Il Java Software Development Kit (SDK) di Sun Microsystems è un set di programmi a riga di comando per la compilazione, l’esecuzione e la documentazione dei programmi Java. Versioni di SDK per varie piattaforme sono disponibili all’indirizzo http:// java.sun.com/j2se. Se si decide di utilizzare SDK, si seguano le istruzioni riportate di seguito. 1. Creare una nuova directory a propria scelta che conterrà i file di programma. 2. Utilizzare un editor di testo a propria scelta per preparare i file Greeter.java e GreeterTest.java. Collocare i file nella directory appena creata. 3. Aprire una finestra della shell. 4. Utilizzare il comando cd per passare alla directory appena creata. 5. Eseguire il compilatore con il comando javac GreeterTest.java 4 Appendice Se il compilatore Java non è nel percorso di ricerca, sarà necessario utilizzare il percorso completo (per esempio, /usr/local/j2sdk1.4/bin/javac oppure c:\j2sdk1.4\bin\javac ), invece del solo javac . Si noti che anche il file Greeter.java viene compilato automaticamente, poiché la classe GreeterTest richiede la classe Greeter. Se vengono riportati eventuali errori di compilazione, si prenda nota del file e dei numeri di riga e si apportino le eventuali correzioni. 6. Si considerino ora i file nella directory corrente. Si verifichi che il compilatore abbia generato due file di classe, Greeter.class e GreeterTest.class. 7. Avviare l’interprete Java con il comando. java GreeterTest Verrà visualizzato un messaggio “Hello, World” nella finestra della shell (Figura A.1). La struttura di questo programma è tipica di un’applicazione Java. Il programma è costituito da una collezione di classi, delle quali una ha un metodo main. Per eseguire il programma è necessario lanciare l’interprete Java con il nome della classe il cui metodo main contiene le istruzioni per l’avvio delle attività del programma. L’ambiente di sviluppo BlueJ, sviluppato presso la Monash University, consente di testare le classi senza dover scrivere un nuovo programma per ogni nuovo test. BlueJ fornisce un ambiente interattivo per costruire oggetti e invocare metodi su di essi. Può essere prelevato all’indirizzo http://www.bluej.org. Con BlueJ non è necessaria una classe GreeterTest per testare la classe Greeter. Si segua invece la procedura qui riportata. 1. Selezionare Project|New dalla barra dei menu; nella finestra di dialogo dei file, selezionare una directory a propria scelta e digitare il nome della sottodirectory che dovrà contenere il nome delle proprie classi. BlueJ creerà una sottodirectory con il nome precedentemente digitato. 2. Fare clic sul pulsante New Class e digitare la classe Greeter. 3. Fare clic sul pulsante Compile per compilare la classe. Fare clic sul pulsante Close. Figura A.1 Esecuzione del programma “Hello, World!” in una finestra di shell. Da C++ a Java 5 4. La classe viene rappresentata come icona a forma di rettangolo. Fare clic con il pulsante destro del mouse sul rettangolo della classe e selezionare new Greeter(aName) per costruire un nuovo oggetto. Chiamare l’oggetto worldGreeter e fornire il parametro del costruttore "Hello" (comprese le virgolette). 5. L’oggetto apparirà nell’area relativa. Fare clic con il pulsante destro del mouse sul rettangolo dell’oggetto e selezionare String sayHello( ) per eseguire il metodo sayHello. 6. Apparirà una finestra di dialogo che visualizzerà il risultato (Figura A.2). Come si può vedere, BlueJ permette di pensare agli oggetti e alle classi senza doversi affannare con public static void main. Figura A.2 A.2 Test di una classe con BlueJ. Commenti di documentazione Java ha una forma standard per i commenti di documentazione che descrivono i metodi e le classi. L’SDK di Java contiene uno strumento chiamato javadoc che genera automaticamente un set di pagine HTML che documentano le classi. I commenti di documentazione sono delimitati da /** e */. Tanto i documenti di classe quanto quelli di metodo iniziano con un testo libero. L’utilità javadoc copia la prima frase di ogni commento in una tabella riepilogativa. È quindi preferibile scrivere la prima frase con attenzione, assicurandosi che inizi con una maiuscola e termini con un punto. Non deve essere una frase compiuta dal punto di vista grammaticale, ma deve avere un senso quando viene estratta dal commento per poter essere visualizzata in un riepilogo. 6 Appendice I commenti dei metodi contengono informazioni supplementari. Per ogni parametro di metodo è necessario fornire una riga che inizia con il tag @param, seguito dal nome di parametro e da una breve spiegazione. Per descrivere il valore restituito si fornisce una riga che inizia con @return. Il tag @param viene omesso quando i metodi non hanno parametri, mentre il tag @return viene omesso per i metodi il cui tipo restituito è void. Ecco la classe Greeter con i commenti di documentazione per la classe e la sua interfaccia pubblica. /** Una classe per produrre un saluto semplice. */ class Greeter { /** Costruisce un oggetto Greeter che può salutare una persona o un’entità. @param aName il nome della persona o entità a cui deve essere rivolto il saluto. */ public Greeter(String aName) { name = aName; } /** Saluta con un messaggio “Hello”. @return un messaggio che contiene "Hello" e il nome della persona o entità a cui si rivolge il saluto. */ public String sayHello() { return "Hello, " + name + "!"; } private String name; } La prima reazione di fronte a questa classe può essere di preoccupazione: è davvero necessario dover scrivere tutto questo codice? Questi commenti sembrano anche piuttosto ridondanti, ma è necessario prendersi un po’ di tempo per scriverli, anche se può sembrare sciocco. Ci sono tre motivi per farlo. Il primo è che l’utility javadoc formatta i commenti in un set definito di documenti HTML e utilizza spesso frasi apparentemente ripetitive. La prima frase di ogni commento di metodo viene utilizzata per una tabella riepilogativa di tutti i metodi della classe (Figura A.3). I commenti @param e @return sono formattati chiaramente nella descrizione dettagliata di ciascuno metodo (Figura A.4). Se si omette uno qualsiasi di essi, javadoc genera documenti che appaiono stranamente vuoti. In realtà diventa più facile perdere tempo a pensare che un commento è troppo banale per scriverlo che non a scriverlo direttamente. Nella programmazione pratica, i metodi molto semplici sono rari. Avere metodi semplici sovracommentati è meno pericoloso che non avere un metodo complesso senza commenti, cosa che può comportare grossi problemi ai programmatori nella manutenzione successiva. In base allo stile di documentazione standard di Java, ogni classe, metodo, parametro e valore restituito dovrebbe avere un commento. Infine, è sempre una buona idea scrivere prima il commento al codice e solo in seguito il codice del metodo. È un test eccellente per vedere se si hanno le idee chiare sullo scopo del programma. Se non si riesce a spiegare cosa fanno una classe o un metodo, non si è pronti per implementarli. Da C++ a Java Figura A.3 Una classe riepilogativa di javadoc. Figura A.4 La documentazione sui parametri e i valori restituiti in javadoc. Una volta scritti i commenti, si invochi l’utility javadoc. 1. Aprire una finestra della shell. 2. Utilizzare il comando cd per passare alla directory appena creata. 3. Eseguire l’utility javadoc. 7 8 Appendice javadoc *.java Se gli strumenti SDK non sono nel percorso di ricerca, sarà necessario utilizzare il percorso completo (per esempio /usr/local/j2sdk1.4/bin/javadoc oppure c:\j2sdk1.4\bin\javadoc) invece del solo javadoc. L’utility javadoc produrrà un file HTML per ogni classe (come Greeter.html e GreeterTest.html), oltre a un file index.html e ad alcuni altri file di riepilogo. Il file index.html contiene collegamenti a tutte le classi. Lo strumento javadoc è eccezionale proprio perché consente di assemblare la documentazione con il codice. In questo modo, quando si aggiorna il programma, si può vedere immediatamente quale documentazione deve essere aggiornata (sperando che non sia un’operazione da eseguire troppo spesso). In seguito si potrà eseguire nuovamente javadoc per ottenere un set di pagine HTML ben formattate con i commenti aggiornati. INTERNET Il programma DocCheck rileva gli eventuali commenti javadoc mancanti. Prelevatelo all’indirizzo http://java.sun.com/j2se/javadoc/doccheck/. L’SDK di Java contiene la documentazione per tutte le classi nella libreria di Java, chiamata anche interfaccia di programmazione dell’applicazione o API. La Figura A.5 mostra la documentazione relativa alla classe String. Questa documentazione viene estratta direttamente dal codice sorgente della libreria. I programmatori che scrivono la libreria di Java hanno documentato ogni classe e metodo e hanno quindi eseguito javadoc per estrarre la documentazione HTML. Si scarichi la documentazione all’indirizzo http://java.sun.com/j2se e la si installi nello stesso punto dell’SDK di Java. Si punti quindi il browser al file docs/api/ index.html nella propria directory dell’SDK di Java e si crei un segnalibro. È importante farlo appena possibile, perché sarà necessario accedervi di frequente. Figura A.5 La documentazione API di Java. Da C++ a Java A.3 9 Tipi primitivi In Java, i numeri, i caratteri e i valori booleani non sono oggetti, ma valori di tipo primitivo. La Tabella A.1 mostra otto tipi primitivi del linguaggio Java. Tabella A.1 I tipi primitivi del linguaggio Java. Tipo Dimensioni Intervallo int 32 byte –2147483648 . . . 2147483647 long 64 byte –9223372036854775808L . . . 9223372036854775807L 16 byte short byte 8 byte char 16 byte 1 byte boolean double 64 byte float 32 byte –32768 . . . 32767 –128 . . . 127 '\u0000' - '\uFFFF' false, true approssimativamente ± 1,79769313486231570E+308 approssimativamente ± 3,40282347E+38F Si noti che le costanti di tipo long hanno un suffisso L, quelle di tipo float hanno un suffisso F, come 10000000000L o 3.1415927F. I caratteri sono codificati in Unicode, uno schema di codifica uniforme per molte lingue. Le costanti di carattere sono racchiuse tra virgolette semplici, come 'a'. Diversi caratteri, come quello di nuova riga 'n', sono rappresentati come sequenza di due caratteri escape. La Tabella A.2 mostra tutte le sequenze escape ammesse. I caratteri Unicode arbitrari sono indicati da una \u seguita da quattro cifre esadecimali racchiuse tra virgolette. Per esempio, '\u2122' è il simbolo di marchio brevettato (TM). Tabella A.2 Sequenze di caratteri escape. Sequenza di escape Significato \b cancella il carattere a sinistra (\u0008) \f avanzamento di pagina (\u000C) \n nuova riga (\u000A) \r ritorno a capo (\u000D) \t tabulatore (\u0009) \\ barra retroversa \' virgolette semplici \" virgolette doppie \un n n n 1 2 3 4 codifica Unicode 10 Appendice INTERNET Per trovare le codifiche di decine di migliaia di lettere in numerosi alfabeti, si visiti il sito http://www.unicode.org. Le conversioni che non comportano una perdita di informazioni (come da short a int o da float a double) sono sempre valide. I valori di tipo char possono essere convertiti in int. Tutti i tipi interi possono essere convertiti in float o double, anche se questo produce una perdita di precisione. Tutte le altre conversioni richiedono un cast, come in double x = 10.0 / 3.0; // imposta x a 3.3333333333333335 int n = (int)x; // imposta n a 3 float f = (float)x; // imposta f a 3.3333333 Non è possibile eseguire una conversione tra tipi booleani e tipi numerici. La classe Math implementa funzioni matematiche utili, elencate nella Tabella A.3. I metodi di questa classe sono statici, ossia non operano su oggetti (si ricordi che in Java i numeri non sono oggetti). Per esempio, ecco come viene chiamato il metodo sqrt: double y = Math.sqrt(x); Poiché il metodo non opera su un oggetto, è necessario fornire il nome della classe per dire al compilatore che il metodo sqrt si trova nella classe Math. In Java, tutti i metodi devono appartenere a una classe. Tabella A.3 Metodi matematici. Metodo Descrizione Math.sqrt(x) radice quadrata di x, x Math.pow(x, y) xy(x > 0, o x = 0 e y > 0, o x > 0 e y è un intero) Math.sin(x) seno di x (x in radianti) Math.cos(x) coseno di x Math.tan(x) tangente di x Math.asin(x) (arco seno) sen–1 x ∈ [– π/2, π/2], x ∈[–1,1] Math.acos(x) (arco coseno) arco sen–1 x ∈[0, π], x ∈[–1,1] Math.atan(x) (arco tangente) tan–1(x/y) ∈ [– π/2, π/2] Math.atan2(y,x) (arco tangente) tan–1 (x/y) ∈[– π/2, π/2], x può essere 0 Math.toRadians(x) converte x gradi in radianti (per esempio, restituisce x ⋅ p/180) Math.toDegrees(x) converte x radianti in gradi (per esempio, restituisce x ⋅ 180/p) Math.exp(x) ex Math.log(x) (log naturale) ln(x), x > 0 Math.round(x) l’intero più vicino a x (come long) Math.ceil(x) l’intero più piccolo ≥ x Math.floor(x) l’intero più grande ≤ x Math.abs(x) valore assoluto x Da C++ a Java A.4 11 Istruzioni di controllo del flusso L’istruzione if viene utilizzata per l’esecuzione condizionale. La condizione else è facoltativa. if (x >= 0) y = Math.sqrt(x); else y = 0; Le istruzioni while e do vengono utilizzate per i cicli. Il corpo di un ciclo do viene eseguito almeno una volta. while (x { x = x n++; } do { x = x n++; } while (x < target) * a; * a; < target); L’istruzione for viene utilizzata per i cicli che sono controllati da un contatore di cicli. for (i = 1; i <= n; i++) { x = x * a; sum = sum + x; } È possibile definire una variabile in un ciclo for. Il suo ambito si estende fino alla fine del ciclo. for (int i = 1; = 1; i <= n; i++) { x = x * a; sum = sum + x; } // i non più definita qui A.5 Riferimenti agli oggetti In Java, un valore di oggetto è sempre un riferimento a un oggetto o, in altre parole, un valore che descrive la posizione dell’oggetto. Per esempio, si consideri l’istruzione Greeter worldGreeter = new Greeter("World"); Il valore dell’espressione new è la posizione dell’oggetto appena costruito. La variabile worldGreeter può contenere la posizione di qualsiasi oggetto Greeter e viene riempita con la posizione del nuovo oggetto (Figura A.6.). È possibile avere più riferimenti allo stesso oggetto. Per esempio, dopo l’istruzione Greeter anotherGreeter = worldGreeter; le due variabili di oggetto condividono entrambe un singolo oggetto (Figura A.7). 12 Appendice Figura A.6 Un riferimento a un oggetto. Figura A.7 Un oggetto condiviso. Se la classe Greeter ha un metodo che consente la modifica dell’oggetto (come un metodo setName), e se quel metodo viene invocato dal riferimento all’oggetto, allora anche tutti i riferimenti condivisi accederanno all’oggetto modificato. anotherGreeter.setName("Dave"); //anche now worldGreeter si riferisce all’oggetto modificato Il riferimento speciale null non fa riferimento ad alcun oggetto. È possibile impostare una variabile di oggetto su null: worldGreeter = null; È possibile verificare se un riferimento a un oggetto è correntemente null: if (worldGreeter == null) ... Se si invoca un metodo o un riferimento null, viene rilevata un’eccezione. A meno che non si fornisca un gestore per l’eccezione, il programma termina. Può accadere che un oggetto non abbia alcun riferimento che punta a esso, per esempio quando tutte le variabili di oggetto a cui si fa riferimento vengono riempite con altri valori o sono state riciclate. In questi casi la memoria dell’oggetto viene recuperata automaticamente dal garbage collector. In Java non è mai necessario riciclare manualmente la memoria. A.6 Passaggio dei parametri In Java, un metodo può modificare lo stato di un parametro di oggetto perché la variabile di parametro corrispondente è impostata a una copia del riferimento di oggetto passato. Si consideri questo metodo di esempio della classe Greeter: /** Imposta un nome diverso per il salutante. Da C++ a Java 13 @param other un riferimento all’altro Greeter */ public void setName(Greeter other) { other.name = name; } Si consideri ora questa chiamata: Greeter worldGreeter = new Greeter("World"); Greeter daveGreeter = new Greeter("Dave"); worldGreeter.setName(daveGreeter); La Figura A.8 mostra come l’altra variabile di parametro venga inizializzata con una copia del riferimento daveGreeter. Il metodo setName cambia other.name, e dopo la restituzione del metodo, daveGreeter.name è stato cambiato. Figura A.8 Accedere a un oggetto attraverso una variabile di parametro. Sebbene un metodo possa modificare lo stato di un oggetto che viene passato come parametro, non può mai aggiornare il contenuto di nessuna variabile. Se si chiama obj.f(var); dove var è una variabile, allora il contenuto di var sarà lo stesso numero, valore di tipo Boolean o posizione di oggetto prima o dopo la chiamata. In altre parole, è impossibile scrivere un metodo che imposta il valore corrente di var con un altro valore di tipo primitivo o posizione di oggetto. Si consideri un altro insieme di metodi di esempio: /** Prova a copiare la lunghezza del nome del salutante in una variabile di tipo integer. @param n la variabile in cui il metodo prova a copiare la lunghezza */ public void setLength(int n) { // questa assegnazione non ha alcun effetto all’esterno del metodo n = name.length(); } /** Prova a impostare un altro oggetto Greeter a una copia di questo oggetto. @param diverso dall’oggetto Greeter da inizializzare */ public void setGreeter(Greeter other) { // questa assegnazione non ha alcun effetto all’esterno del metodo other = new Greeter(name); } 14 Appendice Si chiamino questi due metodi: int length = 0; Greeter worldGreeter = new Greeter("World"); Greeter daveGreeter = new Greeter("Dave"); worldGreeter.setLength(length); // non ha effetti sul contenuto di length worldGreeter.setGreeter(daveGreeter); // non ha effetti sul contenuto di daveGreeter Non ci sono chiamate che hanno effetti. La modifica del valore della variabile di parametro non influisce sulla variabile fornita nella chiamata del metodo. Java non prevede quindi chiamate per riferimento; sia i tipi primitivi sia i riferimenti agli oggetti vengono passati per valore. A.7 Pacchetti Le classi di Java possono essere raggruppate in pacchetti. I nomi dei pacchetti sono sequenze di identificatori separati con un punto, come in java.util javax.swing com.sun.misc edu.sjsu.cs.cs151.alice Per assicurare univocità ai nomi dei pacchetti, Sun consiglia di iniziare un nome di pacchetto con un nome di dominio (per esempio, com.sun o edu.sjsu.cs), poiché i nomi di dominio sono sempre univoci. Si segua quindi un ulteriore criterio all’interno della propria organizzazione per assicurarsi che anche il riferimento al nome di pacchetto sia univoco. Per collocare una classe all’interno di un pacchetto si deve aggiungere un’istruzione di pacchetto all’inizio del file sorgente: package edu.sjsu.cs.cs151.alice; public class Greeter { ... } Qualsiasi classe senza un’istruzione di pacchetto si troverà senza nome nel pacchetto predefinito. Il nome completo di una classe è costituito dal nome del pacchetto seguito da quello della classe, come in edu.cs.cs151.alice.Greeter. Esempi tratti dalla libreria di Java sono java.util.ArrayList e javax.swing.JOptionPane. Poiché è abbastanza noioso utilizzare questi nomi completi nel proprio codice, si possono utilizzare le istruzioni di importazione per utilizzare i nomi di classe. Per esempio, dopo aver collocato un’istruzione import java.util.ArrayList; nel proprio file sorgente, è possibile fare riferimento alla classe semplicemente come ArrayList. Se si utilizzano contemporaneamente due classi che hanno lo stesso nome breve (come java.util.Date e java.sql.Date), allora si sarà costretti a utilizzare i nomi completi. Da C++ a Java 15 È anche possibile importare tutte le classi da un pacchetto: import java.util.*; Non è tuttavia necessario importare le classi nel pacchetto java.lang, come String o Math. I programmi più grandi sono costituiti da più classi in più pacchetti. I file della classe devono essere collocati in sottodirectory che coincidono con il nome del pacchetto. Per esempio, il file di classe Greeter.class per la classe edu.sjsu.cs.cs151.alice.Greeter deve trovarsi nella sottodirectory edu/sjsu/cs/cs151/alice della directory di base del progetto. Questa directory è quella che contiene tutte le directory di pacchetto e le classi che sono contenute nel pacchetto predefinito, ossia il pacchetto senza nome (Figura A.9). Directory di base Figura A.9 Il nome del pacchetto deve coincidere con il percorso della directory. La compilazione deve avvenire sempre dalla directory di base; per esempio javac edu/sjsu/cs/cs151/alice/Greeter.java oppure javac edu\sjsu\cs\cs151\alice\Greeter.java Il file della classe viene collocato automaticamente nella posizione corretta. Per eseguire un programma si deve avviare l’interprete Java nella directory di base e specificare il nome completo della classe che contiene il metodo main: java edu.sjsu.cs.cs151.alice.Greeter.java A.8 Gestione di base delle eccezioni Quando un programma esegue un’azione non valida, viene generata un’eccezione. Ecco un caso comune: si supponga di inizializzare una variabile con il riferimento null e che s’intenda assegnare un effettivo riferimento di oggetto successivamente. Per errore, si utilizza però la variabile quando è ancora null: 16 Appendice String name = null; int n = name.toUpperCase(); //ERRORE Questo è un errore. Non è possibile applicare una chiamata a un metodo su null. Il computer virtuale rileva un’eccezione NullPointerException. A meno che il proprio programma non gestisca questa eccezione, il programma verrà terminato dopo aver visualizzato una traccia di stack come questa: Exception in thread "main" java.lang.NullPointerException at Greeter.sayHello(Greeter.java:25) at GreeterTest.main(GreeterTest.java:6) Diversi errori di programmazione generano eccezioni differenti. Per esempio, provare ad aprire un file con un nome di file non valido comporta un’eccezione IOException. Il linguaggio di programmazione di Java fa una distinzione importante tra due tipi di eccezioni, chiamate rispetticamente non controllata e controllata. L’eccezione NullPointerException è di tipo controllato. In questi casi il compilatore non controlla se il codice gestisce l’eccezione. Se si verifica un’eccezione, questa viene individuata durante il runtime e può terminare il programma. L’eccezione IOException è invece un’eccezione non controllata. Se si chiama un metodo che può generare un’eccezione, è necessario specificare anche come si vuole che il programma si comporti nell’evenienza. In linea generale, un’eccezione controllata ha una causa esterna che va al di là del controllo del programmatore. Le eccezioni che si verificano durante l’input e l’output sono solitamente controllate, perché il file system o la rete possono provocare spontaneamente problemi che il programmatore non può controllare. Il compilatore insiste quindi affinché il programmatore fornisca il codice per gestire queste eventualità. Le eccezioni non controllate sono invece di responsabilità del programmatore. Non si dovrebbe mai verificare un’eccezione NullPointerException. In questi casi il compilatore non chiede di fornire un gestore per l’eccezione; è quindi indispensabile sforzarsi per evitare che l’errore si verifichi. Si consiglia di inizializzare le variabili nel modo appropriato o di verificare che non siano null prima di eseguire una chiamata a un metodo. Ogni volta che si scrive un codice che può provocare un’eccezione non controllata, si può procedere in due modi: 1. Dichiarare l’eccezione nell’intestazione del metodo (soluzione consigliata). 2. Catturare l’eccezione. Si consideri il prossimo esempio. Si vogliono leggere alcuni dati da un file. public void read(String filename) { FileReader r = new FileReader(filename); ... } Se non ci sono file con il nome dato, il costruttore FileReader rileva un’eccezione IOException. Poiché si tratta di un’eccezione controllata, il compilatore insiste perché venga gestita. Il rimedio ideale è quello di consentire la propagazione dell’eccezione fino al suo chiamante. Questo significa che il metodo read termina e che l’eccezione viene rilevata nel metodo che l’ha chiamata. Da C++ a Java 17 Quando un metodo propaga un’eccezione controllata, si deve dichiarare l’eccezione nell’intestazione del metodo, come in questo caso: public void read(String filename) throws IOException } FileReader r = new FileReader(filename) ... } Non c’è da vergognarsi nel riconoscere che il metodo creato genera un’eccezione: errare è umano. Se un metodo può rilevare più eccezioni, queste devono essere elencate separate da virgole: public void read(String filename) throws IOException, ClassNotFoundException Chiunque chiama questo metodo è messo sull’avviso che nella clausola throws può verificarsi un’eccezione controllata. Ovviamente questi metodi di chiamata devono anche saper gestire le eccezioni. In linea generale, i metodi di chiamata aggiungono anche dichiarazioni throws. Quando si esegue questo processo per l’intero programma, anche il metodo main finisce per essere indicato con tag: public static void main(String[] args) throws IOException, ClassNotFoundException { ... } Se un’eccezione si verifica davvero, il metodo main viene terminato, viene visualizzata una traccia di stack e il programma viene chiuso. Tuttavia, se si scrive un programma professionale, non si vuole certo che il programma termini ogni volta che un utente immette un nome di file non valido. In questo caso si vuole generare l’eccezione. Si utilizzi la seguente sintassi: try { ... codice che può rilevare un’eccezione IOException ... } catch (IOException exception) { esegue l’azione correttiva } Un’azione correttiva appropriata potrebbe essere quella di visualizzare un messaggio di errore e di informare l’utente che il tentativo di leggere il file è fallito. Nella maggior parte dei programmi, i metodi a livello più basso propagano semplicemente le eccezioni ai loro chiamanti. Qualche metodo di livello alto, come main o parte dell’interfaccia utente, cattura le eccezioni e informa l’utente. Può accadere che occorra catturare un’eccezione in un punto dove non è possibile intraprendere un’azione correttiva. 18 Appendice Per ora, si visualizzi la traccia dello stack e si esca dal programma. Per esempio, try { ... codice che può rilevare un’eccezione IOException ... } catch (IOException exception) { // non può eseguire l’azione correttiva exception.printStackTrace(); System.exit(0); } Per fare un lavoro migliore, è necessario sapere qualcosa di più sulla gestione delle eccezioni. Si consulti in merito il Capitolo 5. A.9 Liste di array e array La classe ArrayList del pacchetto java.util consente di raccogliere una sequenza di oggetti di qualsiasi tipo. Il metodo add aggiunge un oggetto alla fine della lista degli array. ArrayList countries = new ArrayList(); countries.add("Belgium"); countries.add("Italy"); countries.add("Thailand"); Il metodo size restituisce il numero di oggetti nella lista di array. Il metodo get restituisce l’oggetto in una posizione data; le posizioni valide vanno da 0 a size() – 1. Il tipo restituito del metodo get è però Object, la superclasse comune di tutte le classi di Java. È quindi necessario ricordare il tipo di oggetti che si inseriscono in una lista di array particolare ed eseguire il cast sul valore restituito su quel tipo. Per esempio, for (int i = 0; i < countries.size(); i++) { String country = (String)countries.get(i); ... } Il metodo set consente di sovrascrivere un elemento esistente con un altro. countries.set(1, "France"); Se si accede a una posizione non esistente (< 0 or >= size()), allora viene rilevata un’eccezione IndexOutOfBoundsException. Infine è possibile inserire e rimuove oggetti nel mezzo della lista di array. countries.insert(1, "Germany"); countries.remove(0); Da C++ a Java 19 Queste operazioni spostano gli elementi rimanenti in alto o in basso. Il termine lista di array indica che l’interfaccia pubblica consente sia operazioni di array (get/set) sia di lista (insert/remove). Le liste di array hanno uno svantaggio: possono contenere solamente oggetti e non tipi di valori primitivi. Gli array invece possono contenere sequenze di valori arbitrari. Un array viene costruito come new T[n] dove T è qualsiasi tipo e qualsiasi espressione n valutata come intero. L’array restituito è di tipo T[]. Per esempio, int[] numbers == new int[10]; Ora numbers è un riferimento a un array di 10 interi (Figura A.10). Quando un array viene costruito, i suoi elementi sono impostati come 0, false o null. Figura A.10 Un riferimento di array. La lunghezza di un array viene memorizzata nel campo length. int length = numbers.length Si noti che un array vuoto di lunghezza 0 new int[0] è diverso da null, ossia da un riferimento a nessun array. Per accedere a un elemento di array, si deve racchiudere l’indice tra parentesi quadre, come in int number = numbers[i] Se si accede a una posizione non esistente(< 0 o >= length), viene generata un’eccezione ArrayIndexOutOfBoundsException. Dopo che un array è stato costruito, non è possibile modificarne la lunghezza. Se si desidera un array più grande, è necessario costruirne uno nuovo e spostare gli elementi dal vecchio array al nuovo. È un compito molto noioso, e la maggior parte dei programmatori preferisce utilizzare un ArrayList per memorizzare gli oggetti. 20 Appendice È possibile ottenere un array a due dimensioni con una chiamata a un costruttore, come int[][] table = new int[10][20]; Si accede agli elementi come table[row][column]. Il parametro args del metodo main è un array di stringhe, in particolare di quelle specificate nella riga di comando. args[0] è la prima stringa dopo il nome di classe. Per esempio, se si invoca un programma come java GreeterTest Mars args.length sarà 1 e args[0] sarà "Mars" e non "java" o "GreetingTest". A.10 Stringhe Le stringhe di Java sono sequenze di caratteri Unicode. Il metodo charAt definisce i caratteri di una stringa. Le posizioni delle stringhe iniziano dallo 0. String greeting = "Hello"; char ch = greeting.charAt(1); // imposta ch su 'e' Le stringhe di Java sono immutabili e una volta create, non possono essere modificate. Non c’è quindi alcun metodo setCharAt. Può sembrare una restrizione severa, ma in realtà non lo è. Per esempio, si immagini di aver inizializzato il saluto come "Hello". È sempre possibile cambiare idea: greeting = "Goodbye"; L’oggetto stringa "Hello" non è stato modificato, ma ora il saluto fa riferimento a un oggetto stringa diverso. Il metodo length fornisce la lunghezza di una stringa. Per esempio, "Hello".length() è 5. Si noti che la stringa "" vuota di lunghezza 0 è diversa da un riferimento null, ossia un riferimento a nessuna stringa. Il metodo substring calcola le sottostringhe di una stringa. È necessario specificare le posizione del primo carattere che si vuole includere nella sottostringa e del primo carattere che non si vuole includere. Per esempio, "Hello".substring(1, 3) è la stringa "el" (Figura A.11). Si noti che la differenza tra le due posizioni è uguale alla lunghezza della sottostringa. Figura A.11 Estrazione di una sottostringa. Poiché le stringhe sono oggetti, è necessario utilizzare il metodo equals per confrontare due stringhe che hanno lo stesso contenuto. Da C++ a Java 21 if (greeting.equals("Hello")) ... // OK Se si utilizza l’operatore ==, il test viene seguito solo se due riferimenti di stringa hanno la stessa identica posizione. Per esempio, il test seguente fallisce: if ("Hello".substring(1, 3) == "el") ... // NO La sottostringa non è nella stessa posizione della stringa di costante "el", anche se ha lo stesso contenuto. Se le sottostringhe di una stringa sono separate da un delimitatore come una virgola o uno spazio bianco, è possibile utilizzare la classe StringTokenizer del pacchetto java.util per enumerare le sottostringhe. Per esempio, String countries = "Germany,France,Italy"; StringTokenizer tokenizer = new StringTokenizer(countries, ","); while (tokenizer.hasMoreTokens()) { String country = tokenizer.nextToken(); ... } Se non si fornisce un delimitatore nel costruttore, i token verranno delimitati con spazi bianchi. È già stato esaminato l’operatore di concatenamento delle stringhe: "Hello, " + name è il concatenamento della stringa "Hello" e l’oggetto di stringa a cui name fa riferimento. Se uno degli argomenti dell’operatore + è una stringa, allora l’altro viene convertito in una stringa. Per esempio, int n = 7; String greeting = "Hello, " + n; costruisce la stringa "Hello, 7". Se una stringa e un oggetto sono concatenati, allora l’oggetto viene convertito dal metodo toString. Ogni classe eredita un’implementazione predefinita del metodo toString dalla superclasse Object ed è libera di sovrascriverlo per restituire una stringa più appropriata. Per esempio, il metodo toString della classe Date nel pacchetto java.util restituisce una stringa che contiene la data e l’ora incapsulate nell’oggetto Date. Ecco cosa accade quando si concatena una stringa e un oggetto Date: // il costruttore predefinito Date imposta la data e l’ora correnti Date now = new Date(); String greeting = "Hello, " + now; // greeting è una stringa come "Hello, Wed Jan 17 16:57:18 PST 2001" Può accadere di avere una stringa che contiene un numero, per esempio la stringa "7". Per convertire la stringa nel suo valore di numero si utilizzano i metodi Integer.parseInt e Double.parseDouble. Per esempio, String input = "7"; n = Integer.parseInt(input); // imposta n su 7 Se la stringa non contiene un numero o se contiene altri caratteri oltre a un numero, viene rilevata un’eccezione NumberFormatException. 22 Appendice A.11 Lettura dell’input Il modo più semplice per leggere l’input in un programma Java è utilizzare il metodo statico showInputDialog nella classe JoptionPane, mediante il quale si fornisce una stringa di prompt come parametro. Il metodo visualizza una finestra di dialogo (Figura A.12) e restituisce la stringa fornita dall’utente, oppure null se l’utente ha chiuso la finestra. Per leggere un numero è necessario convertire la stringa in un numero con Integer.parseInt or Double.parseDouble. Per esempio, String input = JOptionPane.showInputDialog("How old are you?"); if (input != null) age = Integer.parseInt(input); Figura A.12 Una finestra di dialogo di input. In effetti è un po’ strano avere finestre di dialogo che appaiono a ogni input. Non è nemmeno possibile ridirigere l’input per fornire l’input da un file di testo. Per leggere dall’input standard occorre un lavoro supplementare. L’input da console legge dall’oggetto System.in . Tuttavia, a differenza di System.out, che era concepito appositamente per visualizzare numeri e stringhe, System.in può solo leggere byte. L’input da tastiera è costituito da caratteri. Per ottenere un lettore per i caratteri, occorre convertire System.in in un oggetto InputStreamReader, come questo: InputStreamReader reader = new InputStreamReader(System.in); Un lettore di stream di input può leggere i caratteri ma non può leggere un’intera stringa in una volta sola. Questo non lo rende molto pratico, visto che non si vuole certo mettere insieme ogni riga in input incollandone i singoli caratteri. Per superare questo limite si può convertire un lettore di stream in un oggetto BufferedReader. BufferedReader console = new BufferedReader( new InputStreamReader(System.in)); Ora si utilizzi il metodo readLine per leggere una riga in input, come questa: System.out.println("How old are you?"); String input = console.readLine(); int count = Integer.parseInt(input); Il metodo readLine può rilevare un’eccezione IOException se c’è un errore di input. Quando viene raggiunta la fine dell’input, il metodo readLine restituisce null. Da C++ a Java 23 A.12 Campi e metodi statici Può accadere che sia necessario condividere una variabile tra tutti gli oggetti di una classe. Ecco un tipico esempio. La classe Random nel pacchetto java.util implementa un generatore di numeri casuale. Ha metodi come nextInt, nextDouble e nextBoolean, che restituiscono interi casuali, numeri a virgola mobile e valori di tipo booleano. Per esempio, ecco come vengono visualizzati 10 interi casuali: Random generator = new Random(); for (int i = 1; i <= 10; i++) System.out.println(generator.nextInt()); Si utilizzi un generatore di numeri casuali nella classe Greeter: public String saySomething() { if (generator.nextBoolean()) return "Hello, " + name + "!"; else return "Goodbye, " + name + "!"; } Sarebbe uno spreco dotare ogni oggetto Greeter del proprio generatore di numeri casuali. Per condividere un generatore tra tutti gli oggetti Greeter, si dichiari il campo come statico: public class Greeter { ... private static Random generator = new Random(); } Le variabili condivise come questa sono relativamente rare. Un impiego più comune della parola chiave static è quello che riguarda la definizione delle costanti. Per esempio, la classe Math contiene le seguenti definizioni: public class Math { ... public static final double E = 2.7182818284590452354; public static final double PI = 3.14159265358979323846; } La parola chiave final denota un valore costante. Dopo che una variabile final è stata inizializzata, non è possibile modificarne il valore. Queste costanti sono pubbliche e vi si fa riferimento come Math.PI e Math.E. Un metodo statico è un metodo che non agisce sugli oggetti. Un esempio di metodo statico già incontrato è Math.sqrt. Un altro impiego dei metodi statici è nei metodi factory, ossia i metodi che restituiscono un oggetto, analogamente a un costruttore. Ecco un metodo factory per la classe Greeter che restituisce un oggetto greeter con un nome casuale: public class Greeter { 24 Appendice public static Greeter getRandomInstance() { if (generator.nextBoolean()) return new Greeter("World"); else return new Greeter("Mars"); } ... } Questo metodo viene invocato come Greeter.getRandomInstance(). Il metodo statico può accedere ai campi statici ma non a quelli di istanza, poiché non operano su un oggetto. I campi e i metodi statici hanno la loro posizione, ma sono abbastanza rari nei programmi orientati agli oggetti. Se un programma contiene molti campi e metodi statici, significa che si è persa l’occasione di scoprire un numero di classi sufficienti per implementare il programma in una modalità orientata agli oggetti. Ecco un cattivo esempio che mostra come si possa scrivere programmi poveri e non orientati agli oggetti con campi e metodi statici: public class BadGreeter { public static void main(String[] args) { name = "World"; printHello(); } public static void printHello() // cattivo stile { System.out.println("Hello, " + name + "!"); } private static String name; // cattivo stile } A.13 Stile di programmazione I nomi delle classi dovrebbero sempre iniziare con una lettera maiuscola, come String, e utilizzare una capitalizzazione mista, come in StringTokenizer. I nomi dei pacchetti dovrebbero essere sempre in minuscolo, come edu.sjsu.cs.cs151.alice. I campi e i metodi dovrebbero sempre iniziare come una minuscola e utilizzare una capitalizzazione mista, come in name e sayHello. I caratteri di sottolineatura (_) non vengono utilizzati molto spesso nei nomi di classi e metodi. Le costanti dovrebbero essere tutte in maiuscolo con un carattere di sottolineatura occasionale, come in PI or MAX_VALUE. Questo non è un requisito del linguaggio Java, ma una convenzione che viene seguita pressoché da tutti i programmatori Java. I programmi in cui le classi iniziano in minuscolo e i metodi in maiuscolo possono sembrare molto strani ai programmatori Java. Inoltre la maggior parte di essi non ritiene che sia una buona scelta di stile quella di utilizzare i prefissi per i campi (come in _name o m_Name). È molto comune utilizzare i prefissi get e set per i metodi che ottengono o impostano la proprietà di un oggetto, come public String getName() public void setName(String aName) Da C++ a Java 25 Una proprietà Boolean ha tuttavia i prefissi is e set, come in public boolean isPolite() public void setPolite(boolean b) Per quanto riguarda le parentesi graffe, esistono due stili principali: lo stile di Allmann, in cui le parentesi vengono allineate, e il più compatto ma meno chiaro stile di Kernighan e Ritchie. Ecco la classe Greeter formattata con lo stile di Kernighan e Ritchie. public class Greeter { public Greeter(String aName) { name = aName; } public String sayHello() { return "Hello, " + name + "!"; } private String name; } In questo libro si utilizza lo stile di Allmann. Alcuni programmatori elencano i campi prima dei metodi in una classe: public class Greeter { private String name; // elencare per prime le caratteristiche private non è una buona idea public Greeter(String aName) { ... } } Dal punto di vista della programmazione orientata agli oggetti, ha più senso elencare per prima l’interfaccia pubblica. Questo è l’approccio utilizzato in questo libro. A eccezione dei campi public static finali, tutti i campi dovrebbero essere dichiarati come private. Se si omette lo specificatore di accesso, il campo avrà una visibilità di pacchetto e tutti i metodi delle classi nello stesso pacchetto potranno accedervi. È una pratica poco sicura che è preferibile evitare. Ai programmatori di C++ è fatta una piccola concessione: è tecnicamente legale dichiarare le variabili di array come int numbers[] In Java è preferibile evitare questo stile e utilizzare int[] numbers Questo stile mostra chiaramente il tipo int[] della variabile. Tutte le classi, i metodi, i parametri e i valori restituiti dovrebbero avere commenti di documentazione. Alcuni programmatori amano formattare questi commenti con una colonna di asterischi, come in questo esempio: /** * Saluta con un messaggio "Hello". 26 Appendice * * */ @return un messaggio che contiene "Hello" e il nome della persona o entità salutata. È un codice bello da guardare, ma rende difficile modificare i commenti. Non è quindi uno stile consigliabile. Si raccomanda di collocare spazi attorno agli operatori binari e dopo le parole chiave, ma non dopo i nomi di metodi o cast. Stile buono Stile cattivo x > y x>y if (x > y) if(x > y) Math.sqrt(x) Math.sqrt (x) (int)x (int) x È preferibile non utilizzare numeri magici. Si utilizzino invece costanti denominate (variabili final). Per esempio, non si utilizzi h = 31 * h + val[off]; // Male--cos’è 31? Cos’è 31? Il numero di giorni di gennaio? La posizione del bit più alto in un intero? No, è il moltiplicatore hash. La soluzione corretta è quella di dichiarare una costante locale nel metodo final int HASH_MULTIPLIER = 31 o una costante statica nella classe (se è utilizzata da più di un metodo) private static final int HASH_MULTIPLIER = 31 e utilizzare quindi HASH_MULTIPLIER invece di 31. Esercizi Esercizio A.1. Aggiungere un metodo sayGoodbye alla classe Greeter e aggiungere una chiamata per verificare la classe GreeterTest (oppure eseguire il testo in BlueJ). Esercizio A.2. Cosa accade quando si esegue l’interprete Java sulla classe Greeter invece che sulla classe GreeterTest? Si provino i due casi e li si illustri. Esercizio A.3. Aggiungere commenti alla classe GreeterTest e al metodo main. Gli args di documento sono “non utilizzati”. Utilizzare javadoc per generare un file GreeterTest.html. Esaminare il file utilizzando il proprio browser. Esercizio A.4. Creare un segnalibro docs/api/index.html nel proprio browser. Trovare la documentazione della classe String. Quanti metodi ha questa classe? Esercizio A.5. Scrivere un programma che visualizzi “Hello, San José”. Utilizzare una sequenza di escape a \u per indicare la lettera é. Esercizio A.6. Qual è il carattere Unicode per la lettera greca “pi” (π)? E per il carattere cinese “bu” ( )? Da C++ a Java 27 Esercizio A.7. Eseguire l’utility javadoc sulla classe Greeter. Quale output si ottiene? Come cambia l’output quando si elimina parte dei commenti di documentazione? Esercizio A.8. Prelevare da Internet l’utility DocCheck e installarla. Quale output si ottiene quando si elimina parte dei commenti di documentazione della classe Greeter? Esercizio A.9. Scrivere un programma che calcoli e visualizzi la radice quadrata di 1000, arrotondata all’intero più vicino. Esercizio A.10. Scrivere un programma che calcoli e visualizzi la somma degli interi da 1 a 100 e da 100 a 1000. Creare una classe Summer appropriata che non abbia metodi main per questo scopo. Se non si utilizza BlueJ, creare una seconda classe con un metodo main per istanziare due oggetti della classe Summer. Esercizio A.11. Aggiungere un metodo setName alla classe Greeter. Scrivere un programma con due variabili Greeter che fanno riferimento allo stesso oggetto Greeter. Invocare setName su uno dei riferimenti e sayHello sull’altro. Visualizzare il valore restituito e illustrare il risultato. Esercizio A.12. Scrivere un programma che imposti una variabile Greeter su null e che in seguito chiami sayHello su quella variabile. Illustrare l’output risultante. Cosa indica il numero dietro al nome di file? Esercizio A.13. Scrivere un programma di test che verifichi i tre metodi set dell’esempio, visualizzando le variabili di parametro prima e dopo la chiamata del metodo. Esercizio A.14. Scrivere un metodo void swapNames(Greeter other) della classe Greeter che scambi i nomi di questo oggetto Greeter e di un altro. Esercizio A.15. Scrivere un programma in cui G r e e t e r è nel pacchetto edu.sjsu.cs.yourcourse.yourname e GreeterTest è nel pacchetto predefinito. In quali directory devono essere collocati i file sorgente e di classe? Esercizio A.16. Cosa c’è di errato in questo segmento di codice? ArrayList strings; strings.add("France"); Esercizio A.17. Scrivere un programma GreeterTest che costruisca gli oggetti Greeter per tutti gli argomenti della riga di comando e visualizzi i risultati del sayHello chiamante. Per esempio, se il programma viene invocato come java GreeterTest Mars Venus il programma dovrà visualizzare Hello, Mars! Hello, Venus! Esercizio A.18. Qual è il valore delle espressioni seguenti? a. 2 + 2 + "2" b. "" + countries, dove countries è un ArrayList riempito con numerose stringhe c. "Hello" + new Greeter("World") Scrivere un piccolo programma di esempio per trovare le risposte e illustrarle. Esercizio A.19. Scrivere un programma che visualizzi la somma dei suoi argomenti della riga di comando (presumendo che siano numeri). Per esempio java Adder 3 2.5 -4.1 28 Appendice dovrebbe visualizzare The sum is 1.4. Esercizio A.20. Scrivere un programma GreeterTest che chieda all’utente “What is your name?” e quindi visualizzi “Hello, nomeutente”. Utilizzare una finestra di dialogo. Esercizio A.21. Scrivere un programma GreeterTest che chieda all’utente “What is your name?” e quindi visualizzi “Hello, nomeutente”. Utilizzare un lettore bufferizzato. Esercizio A.22. Scrivere una classe che possa generare stringhe casuali con caratteri in un set dato. Per esempio, RandomStringGenerator generator = new RandomStringGenerator(); generator.addRange('a', 'z'); generator.addRange('A', 'Z'); String s = generator.nextString(10); // una stringa casuale costituita da dieci caratteri inglesi // minuscoli o maiuscoli La classe dovrebbe gestire un ArrayList di oggetti Range. Esercizio A.23. Scrivere un programma che giochi a Tris con un utente umano. Utilizzare una classe TicTacToeBoard che memorizzi un array 3 × 3 di valori char (riempiti con 'x', 'o' o caratteri di spazio). Il programma dovrebbe utilizzare una generatore di numeri casuali per decidere chi deve iniziare. Quando è il turno del computer, generare casualmente una mossa valida. Quando è il turno dell’utente umano, leggere la mossa e verificare che sia valida. Esercizio A.24. Scrivere un programma che legga i dati in input di un utente e visualizzi i valori minimo, massimo e medio dei dati in input. Utilizzare una classe DataAnalyzer e una classe DataAnalyzerTest separate. Utilizzare una sequenza di finestre di dialogo di input JoptionPane per leggere l’input. Esercizio A.25. Ripetere l’esercizio precedente, ma leggere i dati da un file. Per leggerli, creare un BufferedReader come questo: BufferedReader reader = new BufferedReader(new FileReader(filename)); Utilizzare quindi i metodi BufferedReader.readLine e Double.parseDouble. Esercizio A.26. Migliorare le prestazioni del metodo factory getRandomInstance restituendo uno dei due oggetti Greeter corretti (memorizzati in campi statici) invece di costruire un nuovo oggetto a ogni chiamata. Esercizio A.27. Utilizzare una qualsiasi utility di compressione dell’SDK di Java per decomprimere il file scr.jar che è parte dell’SDK. Osservare il codice della classe String in java/lang/String.java. Quante regole di stile sono state violate dai programmatori? Osservare il metodo hashCode. Come dovrebbe essere riscritto in modo meno confuso?