SETTIMANA 4 Problema Riprendiamo un problema visto nella prima lezione, per il quale abbiamo individuato un algoritmo senza realizzarlo Problema: Avendo depositato ventimila euro in un conto bancario che produce il 5% di interessi all’anno, capitalizzati annualmente, quanti anni occorrono affinché il saldo del conto arrivi al doppio della cifra iniziale? Abbiamo bisogno di un programma che ripeta degli enunciati (capitalizzazione degli interessi annuali, incremento del conto degli anni), finchè non si realizza la condizione desiderata Iterazioni (Capitolo 6) 1 Algoritmo che risolve il problema 2 Il ciclo while Sintassi: while (condizione) enunciato Scopo: eseguire un enunciato finché la condizione è vera Il corpo del ciclo while può essere un enunciato qualsiasi, quindi anche un blocco di enunciati L’enunciato while realizza un ciclo per capire cosa significa questa espressione è utile osservare la rappresentazione del codice mediante diagrammi di flusso 2. All’anno 0 il saldo è 20000 3. Ripetere i passi 3 e 4 finché il saldo è minore del doppio di 20000, poi passare al punto 5 4. Aggiungere 1 al valore dell’anno corrente 5. Il nuovo saldo è il valore del saldo precedente moltiplicato per 1.05 (cioè aggiungiamo il 5%) 6. Il risultato è il valore dell’anno corrente L’enunciato while consente la realizzazione di programmi che devono eseguire ripetutamente una serie di azioni finché è verificata una condizione 3 Soluzione: classe Investment 4 Soluzione: classe InvestmentTester public class Investment { public Investment(double aBalance, double aRate) { balance = aBalance; rate = aRate; years = 0; } public void waitForBalance(double targetBalance) { while (balance < targetBalance) { years++; double interest = balance * rate / 100; balance = balance + interest; } } public double getBalance() { return balance; } public int getYears() { return years; } public class InvestmentTester { public static void main(String[] args) { final double INITIAL_BALANCE = 10000; final double RATE = 5; Investment invest = new Investment(INITIAL_BALANCE, RATE); invest.waitForBalance(2 * INITIAL_BALANCE); int years = invest.getYears(); System.out.println("The investment doubled after " + years + " years"); } } private double balance; private double rate; private int years; } 5 6 È tutto chiaro? … 1. Quante volte viene eseguito il seguente ciclo? Cicli infiniti Esistono errori logici che impediscono la terminazione di un ciclo, generando un ciclo infinito l’esecuzione del programma continua ininterrottamente Bisogna arrestare il programma con un comando del sistema operativo, o addirittura riavviare il computer int year = 0; while (year < 20) { double interest = balance * rate / 100; balance = balance + interest; // qui manca year++ } int year = 20; while (year > 0) { year++; // doveva essere year-double interest = balance * rate / 100; balance = balance + interest; } while (false) // enunciato 2. Cosa succede nel programma InvestmentTester se RATE nel metodo main vale 0? Ctrl + C 7 8 Ciclo for Molti cicli hanno questa forma Cicli for i = inizio; while (i < fine) { enunciati i++; } Per comodità esiste il ciclo for equivalente for (i = inizio; i < fine; i++) { enunciati } Non è necessario che l’incremento sia di una sola unità, né che sia positivo, né che sia intero for (double x = 2; x > 0; x = x - 0.3) { enunciati } 9 10 L’enunciato for Il metodo waitYears di Investment Sintassi: Vogliamo sapere quale sarà il saldo finale del nostro investimento dopo 20 anni, con tasso di interesse costante al 5% for (inizializzazione; condizione; aggiornamento) enunciato public class Investment { ... public void waitYears(int n) { for (int i = 1; i <= n; i++) { double interest = balance * rate / 100; balance = balance + interest; } years = years +n; } Scopo: eseguire un’inizializzazione, poi ripetere l’esecuzione di un enunciato ed effettuare un aggiornamento finché la condizione è vera Nota: l’inizializzazione può contenere la definizione di una variabile, che sarà visibile soltanto all’interno del corpo del ciclo for (int y = 1; y <= 10; y++) { … } // qui y non è più definita ... 11 } 12 Esempio: invertire una stringa Visibilità delle variabili Se il valore finale di una variabile di controllo del ciclo deve essere visibile al di fuori del corpo del ciclo, bisogna definirla prima del ciclo Poiché una variabile definita nell’inizializzazione di un ciclo non è più definita dopo il corpo del ciclo, è possibile (e comodo) usare di nuovo lo stesso nome in altri cicli public class ReverseTester { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("Inserire una stringa:"); String s = in.nextLine(); String r = ""; for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); // aggiungi all’inizio r = ch + r; } System.out.println(s + " invertita è " + r); } } double b = ...; for (int i = 1; i < 10 && b < c; i++) { ... modifica b } // qui b è visibile, mentre i non lo è for (int i = 3; i > 0; i--) System.out.println(i); 13 14 È tutto chiaro? … 1. Riscrivere sotto forma di ciclo while il ciclo for del metodo waitYears 2. Quante volte viene eseguito il seguente ciclo for? for (i = 0; i <= 10; i++) System.out.println(i * i); Cicli annidati 15 16 Problema Problema Vogliamo stampare una tabella con i valori delle potenze xy, per ogni valore di x tra 1 e 4 e per ogni valore di y tra 1 e 5 Per stampare la riga x­esima, bisogna calcolare e stampare i valori x1, x2, x3, x4 e x5, cosa che si può fare facilmente con un ciclo for 1 2 3 4 1 4 9 16 1 8 27 64 1 1 16 32 81 243 256 1024 // stampa la riga x-esima della tabella for (int y = 1; y <= 5; y++) { int p = (int)Math.round(Math.pow(x, y)); System.out.print(p + " "); } System.out.println(); // va a capo Innanzitutto, bisogna stampare 4 righe for (int x = 1; x <= 4; x++) { // stampa la riga x-esima della tabella ... } Ogni iterazione del ciclo stampa un valore, seguito da uno spazio bianco per separare valori successivi; al termine si va a capo 17 18 Soluzione Soluzione Mettendo insieme le due “soluzioni parziali” si risolve il problema, mediante due cicli “annidati” (nested) , cioè “uno dentro l’altro” Incolonnare dati di lunghezza variabile è un problema frequente Ora ci sono final int COLUMN_WIDTH = 5; tre cicli for (int x = 1; x <= 4; x++) annidati! for (int x = 1; x <= 4; x++) { // stampa la riga x-esima della tabella for (int y = 1; y <= 5; y++) { // stampa il valore y-esimo // della riga x-esima int p = (int)Math.round(Math.pow(x, y)); System.out.print(p + " "); } System.out.println(); // va a capo } 1 2 3 4 1 1 1 1 4 8 16 32 9 27 81 243 16 64 256 1024 Le colonne non sono allineate! { } for (int y = 1; y <= 5; y++) { // converte in stringa il valore String p = "" + (int)Math.round(Math.pow(x, y)); // aggiunge gli spazi necessari while (p.length() < COLUMN_WIDTH) p = " " + p; System.out.print(p); } System.out.println(); 1 1 1 1 1 2 4 8 16 32 3 9 27 81 243 4 16 64 256 1024 19 20 Il problema del “ciclo e mezzo” Un ciclo del tipo “fai qualcosa, verifica una condizione, fai qualcos’altro e ripeti il ciclo se la condizione era vera” non ha una struttura di controllo predefinita in Java e deve essere realizzata con un “trucco”, come quello di usare una variabile booleana, detta variabile di controllo del ciclo Il problema del “ciclo e mezzo” Una struttura di questo tipo si chiama anche “ciclo e mezzo” o ciclo ridondante (perché c’è qualcosa di “aggiunto”, di innaturale…) 21 22 Ciclo e mezzo: il programma QEater Il problema del “ciclo e mezzo” import java.util.Scanner; Situazione tipica: l’utente deve inserire un insieme di valori, la cui dimensione non è predefinita Si realizza un ciclo while, dal quale si esce soltanto quando si verifica la condizione all’interno del ciclo boolean done = false; while (!done) { System.out.println(“Valore?”); String input = in.next(); if (input.equalsIgnoreCase(“Q”)) done = true; La lettera “Q” (Quit) è else ... // elabora line un valore sentinella che } segnala che l'immissione dei dati è terminata 23 public class QEater { public static void main(String[] args) { Scanner in = new Scanner(System.in); boolean done = false; while(!done) { System.out.println("Voglio una Q"); String input = in.nextLine(); if (input.equals("Q")) { System.out.println("Grazie!"); done = true; } else { System.out.println("Allora non hai capito..."); } } } } 24 Break, continue, e codice spaghetti while (true) { String input = in.next(); if (input.equalsIgnoreCase(“Q”)) break; else ... // elabora line } Materiale di complemento (Capitolo 7) Soluzione alternativa alla struttura “ciclo e mezzo”: usare un ciclo infinito while(true) e l’enunciato break L’enunciato break provoca la terminazione del ciclo Esiste anche l'enunciato continue, che fa proseguire l'esecuzione dalla fine dell'iterazione attuale del ciclo L'uso di break e continue è non necessario e sconsigliabile perchè contribuisce a creare codice spaghetti ovvero rappresentato da diagrammi di flusso pieni di linee, difficili da leggere e comprendere 25 26 Variabili non inizializzate È buona regola fornire sempre un valore di inizializzazione nella definizione di variabili Variabili non inizializzate int lit; Cosa succede altrimenti? la definizione di una variabile “crea” la variabile, cioè le riserva uno spazio nella memoria primaria (la quantità di spazio dipende dal tipo della variabile) tale spazio di memoria non è “vuoto”, una condizione che non si può verificare in un circuito elettronico, ma contiene un valore “casuale” (in realtà contiene l’ultimo valore attribuito a quello spazio da un precedente programma… valore che a noi non è noto) 27 28 Variabili non inizializzate Variabili non inizializzate Se si usasse il valore di una variabile prima di averle assegnato un qualsiasi valore, il programma si troverebbe ad elaborare quel valore che “casualmente” si trova nello spazio di memoria riservato alla variabile ERRORE Questo problema provoca insidiosi errori di esecuzione in molti linguaggi di programmazione il compilatore Java, invece, segnala come errore l’utilizzo di variabili a cui non sia mai stato assegnato un valore (mentre non è un errore la sola definizione...) Coins6.java:5: variable lit might not have been initialized public class Coins6 // NON FUNZIONA! { public static void main(String[] args) { int lit; double euro = 2.35; double totalEuro = euro + lit / 1936.27; System.out.print("Valore totale in euro "); System.out.println(totalEuro); } } questi errori non sono sintattici, bensì logici, ma vengono comunque individuati dal compilatore, perché si tratta di errori semantici (cioè di comportamento del programma) individuabili in modo automatico 29 30 Ciclo do Capita spesso di dover eseguire il corpo di un ciclo almeno una volta, per poi ripeterne l’esecuzione se è verificata una particolare condizione Esempio tipico: leggere un valore in ingresso, ed eventualmente rileggerlo finché non viene introdotto un valore “valido” Cicli do 31 32 Ciclo do Si può usare un ciclo while “innaturale” // si usa un’inizializzazione "ingiustificata" double rate = 0; while (rate <= 0) { System.out.println("Inserire il tasso:"); rate = console.readDouble(); } Cicli: errori e consigli ma per comodità esiste il ciclo do double rate; do { System.out.println("Inserire il tasso:"); rate = console.readDouble(); } while (rate <= 0); 33 Errori per scarto di uno 34 Cicli for di “cattivo gusto” int years = 0; // o forse int years =1; ??? while (balance < 2 * initialBalance) { // o forse balance <= 2 * initialBalance ??? years++; double interest = balance * rate / 100; balance = balance + interest; } System.out.println("Investimento raddoppiato dopo" for (rate = 5; years-- > 0; System.out.println(balance)) { enunciati } È un ciclo for sintatticamente corretto ma contiene condizioni estranee, che producono risultati difficilmente comprensibili for (int i = 1; i <= years; i++) { if (balance >= targetBalance) i = i + 1; else ... } + years + " anni."); Come evitare questi errori per scarto di uno? Provare con alcuni semplici casi di prova Investimento iniziale 100 euro, tasso 50% L'intestazione è corretta ma il contatore viene modificato nel corpo del ciclo • Allora years deve essere inizializzato a 0 Tasso di interesse 100% • Allora la condizione deve essere < e non <= 35 Sono cicli for di cattivo gusto! 36 Punti e virgola mancanti o di troppo sum = 0; for (int i = 1; i <= 10; i++); C'è un “;” di troppo! sum = sum + i; il corpo del ciclo è vuoto System.out.println(sum); L'istruzione di aggiornamento di sum viene eseguita una sola volta, dopo l'uscita dal ciclo E se n fosse negativo? for (int i = 1; i La condizione i!=n sarebbe sempre vera Molto meglio i<=n != n; i++) for (i = a; i < b; i+=c) // oppure Imparare a contare le iterazioni for (i = a; i <= b; i+=c) Il primo ciclo (asimmetrico) viene eseguito (b­a)/c volte Il secondo (simmetrico) viene eseguito (b­a)/c +1 volte for (years = 1; (balance = balance + balance * rate / 100) < targetBalance; years++) // ; System.out.println(years); In questo caso tutte le operazioni necessarie sono nell'intestazione del ciclo Il corpo del ciclo deve essere vuoto Ma abbiamo dimenticato il “;” dopo l'intestazione Quindi l'istruzione di stampa viene considerata parte del corpo Definire bene i limiti for (i = 0; i <= s.length() - 1; i++) 37 Due intestazioni // è corretta, ma è meglio questa: Sono equivalenti for (i = 0; i < s.length(); i++) Nella prima i limiti sono simmetrici Nella seconda sono asimmetrici ma il codice è più leggibile 38 Progettazione di classi (capitolo 8) (capitolo 3 – sez. 3.7 e 3.8) 39 40 I parametri dei metodi public void deposit(double amount) { balance = balance + amount; } Parametri espliciti/impliciti Il parametro this Cosa succede quando invochiamo il metodo? account.deposit(500); L’esecuzione del metodo dipende da due valori il riferimento all’oggetto account il valore 500 Quando viene eseguito il metodo, il suo parametro esplicito amount assume il valore 500 esplicito perché compare nella firma del metodo 41 42 I parametri dei metodi Il parametro implicito dei metodi public void deposit(double amount) { balance = balance + amount; } momsavings.deposit(500); All’interno di ogni metodo il riferimento all’oggetto con cui viene eseguito il metodo si chiama parametro implicito e si indica con la parola chiave this in questo caso, this assume il valore di account all’interno del metodo deposit Ogni metodo ha sempre uno ed un solo parametro implicito, dello stesso tipo della classe a cui appartiene il metodo eccezione: i metodi statici non hanno parametro implicito Il parametro implicito non deve essere dichiarato e si chiama sempre this Abbiamo già detto che un metodo può avere parametri espliciti e/o impliciti amount è il parametro esplicito del metodo balance si riferisce alla variabile di esemplare balance della classe BankAccount, ma sappiamo che di tale variabile esiste una copia per ogni oggetto Alla variabile balance di quale oggetto si riferisce il metodo? si riferisce alla variabile che appartiene all’oggetto con cui viene invocato il metodo, ma come fa? 43 44 È tutto chiaro? … 1. Quanti e quali sono i parametri impliciti ed Uso del parametro implicito La vera sintassi del metodo dovrebbe essere espliciti del metodo withdraw di BankAccount? Quali sono i loro nomi e tipi? 2. Qual è il significato di this.amount nel metodo deposit? Oppure, se l’espressione è priva di significato, per quale motivo lo è? public void deposit(double amount) { this.balance = this.balance + amount; } // this è di tipo BankAccount Ma Java consente una comoda scorciatoia: se un metodo si riferisce ad un campo di esemplare, il compilatore costruisce automaticamente un riferimento al campo di esemplare dell’oggetto rappresentato dal parametro implicito this 45 46 Classi “di utilità” e metodi statici Esistono classi che non servono a creare oggetti ma contengono metodi statici e costanti. Queste si chiamano solitamente classi di utilità La classe Math è un esempio di questo tipo di classi La classe Numeric scritta da noi è un altro esempio Esempio: una classe Financial Membri di classe “statici” public class Financial { public static double percentOf(double p, double a) { return (p / 100) * a; } // qui si possono aggiungere altri metodi finanziari } Non è necessario creare oggetti di tipo Financial per usare i metodi della classe: double tax = Financial.percentOf(taxRate, total); 47 48 Variabili statiche Soluzione Vogliamo modificare BankAccount in modo che il suo stato contenga anche un numero di conto Prima idea (che non funziona…) usiamo una variabile per memorizzare l’ultimo numero di conto assegnato public class BankAccount { ... private int accountNumber; } public class BankAccount { ... private int accountNumber; private int lastAssignedNumber; ... public BankAccount() { lastAssignedNumber++; accountNumber = lastAssignedNumber; } } il numero di conto sia assegnato dal costruttore • ogni conto deve avere un numero diverso • i numeri assegnati devono essere progressivi, iniziando da 1 49 Soluzione 50 Variabili statiche public class BankAccount { ... private int accountNumber; private int lastAssignedNumber; ... public BankAccount() { lastAssignedNumber++; accountNumber = lastAssignedNumber; } } Ci serve una variabile condivisa da tutti gli oggetti della classe una variabile con questa semantica si ottiene con la dichiarazione static public class BankAccount { ... private static int lastAssignedNumber; } Questo costruttore non funziona perché la variabile lastAssignedNumber è una variabile di esemplare ne esiste una copia per ogni oggetto il risultato è che tutti i conti creati hanno il numero di conto uguale a 1 Una variabile static (variabile di classe) è condivisa da tutti gli oggetti della classe Ne esiste un’unica copia indipendentemente da quanti oggetti siano stati creati (anche zero) 51 Variabili statiche 52 Variabili statiche Ora il costruttore funziona Le variabili statiche non possono (da un punto di vista logico) essere inizializzate nei costruttori Il loro valore verrebbe inizializzato di nuovo ogni volta che si costruisce un oggetto, perdendo il vantaggio di avere una variabile condivisa! Bisogna inizializzarle quando si dichiarano public class BankAccount { ... private int accountNumber; private static int lastAssignedNumber = 0; ... public BankAccount() { lastAssignedNumber++; accountNumber = lastAssignedNumber; } } private static int lastAssignedNumber = 0; Questa sintassi si può usare anche per le variabili di esemplare, anziché usare un costruttore non è una buona pratica di programmazione Ogni metodo (o costruttore) di una classe può accedere alle variabili statiche della classe e modificarle 53 54 Variabili statiche Variabili statiche Nella programmazione ad oggetti, l’utilizzo di variabili statiche deve essere limitato metodi che leggono variabili statiche ed agiscono di conseguenza, hanno un comportamento che non dipende soltanto dai loro parametri (implicito ed espliciti) In ogni caso, le variabili statiche devono essere private, per evitare accessi indesiderati È invece pratica comune (senza controindicazioni) usare costanti statiche, come nella classe Math public class Math { ... public static final double PI = 3.14159265358979323846; } Tali costanti sono di norma public e per ottenere il loro valore si usa il nome della classe seguito dal punto e dal nome della costante, Math.PI 55 56 È tutto chiaro? … 1. Citare due variabili statiche della classe System Categorie di variabili (sez. 3.7) Ciclo di vita di una variabile 57 58 Ciclo di vita di una variabile Ciclo di vita di una variabile Una variabile locale viene creata quando viene eseguito l’enunciato in cui è definita viene eliminata quando l’esecuzione del programma esce dal blocco di enunciati in cui la variabile è definita Sappiamo che in Java esistono quattro diversi tipi di variabili variabili locali (all’interno di un metodo) variabili parametro (dette parametri formali) variabili di esemplare o di istanza variabili statiche o di classe Hanno in comune il fatto di contenere valori appartenenti ad un tipo ben preciso. Differiscono per quanto riguarda il loro ciclo di vita cioè l’intervallo di tempo in cui, dopo essere state create, continuano ad occupare lo spazio in memoria riservato loro • se non è definita all’interno di un blocco di enunciati, viene eliminata quando l’esecuzione del programma esce dal metodo in cui la variabile viene definita Una variabile parametro (formale) viene creata quando viene invocato il metodo viene eliminata quando l’esecuzione del metodo termina 59 60 Ciclo di vita di una variabile Eliminazione di oggetti Una variabile statica viene creata quando la macchina virtuale Java carica la classe per la prima volta viene eliminata quando la classe viene scaricata dalla macchina virtuale Java ai fini pratici, possiamo dire che esiste sempre... Una variabile di esemplare viene creata quando viene creato l’oggetto a cui appartiene viene eliminata quando l’oggetto viene eliminato Un oggetto è a tutti gli effetti “inutilizzabile” quando non esiste più nessun riferimento ad esso Se un programma abbandona molti oggetti in memoria, puo’ esaurire la memoria a disposizione La JVM effettua automaticamente la gestione della memoria durante l’esecuzione di un programma Usa un meccanismo di garbage collection • Viene periodicamente riciclata, cioè resa di nuovo libera, la memoria eventualmente occupata da oggetti che non abbiano piu’ un riferimento nel programma 61 62 Modello della memoria in java Allocazione della memoria in java Al momento dell’esecuzione, a ciascun programma java viene assegnata un’area di memoria Una parte della memoria serve per memorizzare il codice; quest’area è statica, ovvero non modifica le sue dimensioni durante l’esecuzione del programma La Java Stack è un’area dinamica (ovvero che cambia dimensione durante l’esecuzione) in cui vengono memorizzate i parametri e le variabili locali dei metodi • Stack significa pila L'istruzione BankAccount b = new BankAccount(); Produce questo effetto in memoria: La Java Heap è un’altra area dinamica in cui vengono creati oggetti durante l’esecuzione dei metodi di un programma, usando lo speciale operatore new • Heap significa cumulo, mucchio 63 64 Visibilità di variabili locali Per evitare conflitti, dobbiamo conoscere l’ambito di visibilità di ogni tipo di variabile Ovvero la porzione del programma all’interno della quale si può accedere ad essa Esempio: due variabili locali con lo stesso nome Funziona perché gli ambiti di visibilità sono disgiunti Ambiti di visibilità di variabili 65 public class RectangleTester { public static double area(Rectangle rect) { double r = rect.getWidth() * rect.getHeight(); return r; } public static void main(String[] args) { Rectangle r = new Rectangle(5, 10, 20, 30); double a = area(r); System.out.println(r); } } 66 Visibilità di variabili locali Visibilità di membri di classe Anche qui gli ambiti di visibilità sono disgiunti: Membri private hanno visibilità di classe Qualsiasi metodo di una classe può accedere a variabili e metodi della stessa classe Membri public hanno visibilità al di fuori della classe A patto di renderne qualificato il nome, ovvero: Specificare il nome della classe per membri static if (x >= 0) { double r = Math.sqrt(x); . . . } // la visibilitò di r termina qui else { Rectangle r = new Rectangle(5, 10, 20, 30); // OK, questa è un’altra variabile r . . . } Invece l’ambito di visibilità di una variabile non può contenere la definizione di un’altra variabile locale con lo stesso nome: • Math.PI, Math.sqrt(x) Specificare l’oggetto per membri non static • account.getBalance() Non è necessario qualificare i membri appartenenti ad una stessa classe Perché ci si riferisce automaticamente al parametro implicito this Rectangle r = new Rectangle(5, 10, 20, 30); if (x >= 0) { double r = Math.sqrt(x); // Errore: non si può dichiarare un’altra var. r qui . . . } 67 68 Visibilità di membri di classe Visibilità sovrapposte Esempio: qualifica sottintesa dal parametro implicito Purtroppo gli ambiti di visibilità di una variabile locale e di una variabile di esemplare possono sovrapporsi public class BankAccount { public void transfer(double amount, BankAccount other) { withdraw(amount); // cioè this.withdraw(amount); other.deposit(amount); } public void withdraw(double amount) { if (balance > amount) // cioè this.balance balance = balance – OVERDRAFT_FEE; //cioè BankAccount.OVERDRAFT_FEE else ... } ... private static double OVERDRAFT_FEE = 5; ... } public class Coin { ... public double getExchangeValue(double exchangeRate) { double value; // Variabile locale ... return value; } private String name; private double value; // Campo di esemplare omonimo } Viene segnalato un errore in compilazione? 69 70 Visibilità sovrapposte Visibilità sovrapposte Non viene segnalato alcun errore in compilazione Java specifica che in casi come questo prevale il nome della variabile locale La variabile di esemplare viene “messa in ombra” (shadowed) Questa scelta è giustificata dal fatto che la variabile di esemplare può sempre essere qualificata usando il parametro this Errore molto comune: utilizzare accidentalmente lo stesso nome per una variabile locale e un campo di esemplare Un analogo effetto di shadowing è prodotto da una variabile locale su una variabile statica omonima La variabile statica deve essere qualificata con il nome della classe 71 public class Coin { ... public Coin(double aValue, String aName) { value = aValue; String name = aName; //ahi: abbiamo dichiarato una nuova variabile name } ... private String name; private double value; } 72 È tutto chiaro? … 1. Qual è l’ambito di visibilità delle variabili amount e newBalance del metodo deposit di BankAccount? 2. Qual è l’ambito di visibilità del campo di esemplare balance di BankAccount? Chiamate per valore e riferimento 73 Chiamate per valore e riferimento 74 Chiamate per valore e riferimento Un nuovo metodo transfer per la classe BankAccount: void transfer(double amount, double otherBalance) { balance = balance - amount; otherBalance = otherBalance + amount; } Proviamo ad usarlo: double savingsBalance = 1000; harrysChecking.transfer(500, savingsBalance); System.out.println(savingsBalance); Non funziona! All’inizio dell’esecuzione del metodo la variabile parametro otherBalance ha il valore di savingsBalance La modifica del valore di otherBalance non ha effetto su savingsBalance perché sono variabili diverse Alla fine dell’esecuzione otherBalance non esiste più 75 Chiamate per valore e riferimento 1000 76 Chiamate per valore e riferimento A volte si dice impropriamente che in Java i numeri sono passati per valore e gli oggetti per riferimento In realtà in Java il passaggio è sempre “per valore” il valore del parametro effettivo è assegnato al parametro formale, ma passando per valore una variabile oggetto, si passa una copia del riferimento in essa contenuto l’effetto “pratico” di questo passaggio è la possibilità di modificare lo stato dell’oggetto stesso, come avviene con il passaggio “per riferimento” Altri linguaggi (come C++) consentono il passaggio dei parametri “per riferimento”, rendendo possibile la modifica dei parametri effettivi 77 78 Materiale di complemento (Capitoli 3/8) Errori tipici con i costruttori 79 Invocare metodi senza oggetto 80 Invocare metodi senza oggetto Ci sono, però, casi in cui è ammesso (ed è pratica molto comune!) invocare metodi non statici senza specificare un riferimento ad oggetto Questo accade quando si invoca un metodo non statico di una classe da un altro metodo non statico della stessa classe public class Program { public static void main(String[] args) { BankAccount account = new BankAccount(); deposit(500); // ERRORE } } viene usato il parametro implicito this In questo caso il compilatore non è in grado di compilare la classe Program, perché non sa a quale oggetto applicare il metodo deposit, che non è un metodo statico e quindi richiede sempre un riferimento ad un oggetto da usare come parametro implicito public void withdraw(double amount) { balance = getBalance() - amount; } this.getBalance() 81 82 Tentare di ricostruire un oggetto Tentare di ricostruire un oggetto A volte viene la tentazione di invocare un costruttore su un oggetto già costruito con l’obiettivo di riportarlo alle condizioni iniziali Un costruttore crea un nuovo oggetto con prefissate condizioni iniziali un costruttore può essere invocato soltanto con l’operatore new La soluzione è semplice assegnare un nuovo oggetto alla variabile oggetto che contiene l’oggetto che si vuole “ricostruire” BankAccount account = new BankAccount(); account.deposit(500); account.BankAccount(); // NON FUNZIONA! cannot resolve symbol symbol : method BankAccount ( ) location : class BankAccount BankAccount account = new BankAccount(); account.deposit(500); account = new BankAccount(); Il messaggio d’errore è un po’ strano… il compilatore cerca un metodo BankAccount, non un costruttore, e naturalmente non lo trova! 83 84 Coesione e accoppiamento Progettare classi: accoppiamento, coesione, effetti collaterali (sezioni 8.2, 8.4) Una classe dovrebbe rappresentare un singolo concetto Metodi pubblici e costanti elencati nell’interfaccia pubblica devono avere una buona coesione Ovvero devono essere strettamente correlate al singolo concetto rappresentato dalla classe Quando una classe usa un'altra classe all'interno del proprio codice si parla di accoppiamento tra le due classi Visualizziamo accoppiamento con diagrammi UML (Unified Modeling Language) La linea tratteggiata con freccia aperta punta alla classe nei cui confronti esiste dipendenza 85 Accoppiamento 86 Effetti collaterali Problema: se una classe viene modificata, tutte quelle che da essa dipendono possono richiedere modifiche Problema: per usare una classe in un programma, vanno usate anche tutte quelle da cui essa dipende Bisogna eliminare tutti gli accoppiamenti non necessari Effetto collaterale di un metodo: Qualsiasi tipo di comportamento osservabile al di fuori del metodo stesso Qualsiasi metodo modificatore ha effetti collaterali Modificano il proprio parametro esplicito Esistono altri tipi di effetto collaterale: Modifica di un parametro esplicito Esempio: un metodo transfer per BankAccount 87 public void transfer(double amount, BankAccount other) { balance = balance - amount; other.balance = other.balance + amount; // modifica il parametro esplicito } 88 È tutto chiaro? … 1. Se a è un riferimento ad un oggetto Effetti collaterali Un altro tipo di effetto collaterale: visualizzazione di dati in uscita Esempio: un metodo printPurchase per CashRegister BankAccount, allora a.deposit(100) modifica l’oggetto. È un effetto collaterale? 2. Immaginiamo di avere scritto il seguente metodo in una classe Dataset: produce effetti collaterali? public void printPurchase() // Non consigliato { System.out.println("The purchase is now $" + purchase); } Problema: abbiamo ipotizzato che qualsiasi utente parli la lingua inglese Problema: abbiamo realizzato accoppiamento con le classi System e PrintStream void read(Scanner in) { while (in.hasNextDouble()) add(in.nextDouble); } Evitiamo di inserire attività di input/output all’interno di metodi di una classe che ha altre finalità 89 90 I pacchetti di classi (package) Tutte le classi della libreria standard sono raccolte in pacchetti (package) e sono organizzate per argomento e/o per finalità Esempio: la classe Rectangle appartiene al pacchetto java.awt (Abstract Window Toolkit) Per usare una classe di una libreria, bisogna importarla nel programma, usando l’enunciato import nomePacchetto.NomeClasse Le classi System e String appartengono al pacchetto java.lang il pacchetto java.lang viene importato automaticamente Pacchetti 91 92 Stili per l’importazione di classi Importare classi da un pacchetto Un enunciato import per ogni classe importata Sintassi: import nomePacchetto.NomeClasse; Scopo: importare una classe da un pacchetto, per poterla utilizzare in un programma import java.math.BigInteger; import java.math.BigDecimal; Un enunciato import per tutte le classi di un pacchetto non è un errore importare classi che non si usano! Sintassi: import nomePacchetto.*; Scopo: importare tutte le classi di un pacchetto, per poterle utilizzare in un programma import java.math.*; Se si usano più enunciati di questo tipo non è chiaro il pacchetto di appartenenza delle classi Ci possono essere classi omonime in pacchetti diversi Nota: le classi del pacchetto java.lang non hanno bisogno di essere importate Attenzione: non si possono importare più pacchetti con un solo enunciato import java.util.*; import javax.swing.*; La classe Timer, appartiene ad entrambi i pacchetti • Se si definisce una variabile di tipo Timer il compilatore segnala un errore di “riferimento ambiguo” import java.*.*; // ERRORE 93 94 95 96 Stili per l’importazione di classi È possibile non usare per nulla gli enunciati import, ed indicare sempre il nome completo delle classi utilizzate nel codice java.math.BigInteger a = new java.math.BigInteger("123456789"); Questo stile è assai poco usato, perché è molto noioso, aumenta la probabilità di errori di battitura e aumenta la lunghezza delle linee del codice (diminuendo così la leggibilità del programma) Argomenti inattesi (precondizoni) Spesso un metodo richiede che i suoi argomenti siano di un tipo ben definito La gestione delle eccezioni (capitolo 11) • questo viene garantito dal compilatore abbiano un valore che rispetti certi vincoli, ad esempio sia un numero positivo • in questo caso il compilatore non aiuta... Come deve reagire il metodo se riceve un parametro che non rispetta i requisiti richiesti (chiamati precondizioni)? 97 Argomenti inattesi (precondizioni) 98 Argomenti inattesi (precondizioni) Ci sono quattro modi per reagire ad argomenti inattesi non fare niente Terminare il programma con System.exit(1) • questo è possibile soltanto in programmi non professionali • La terminazione di un programma attraverso il metodo statico System.exit() non fornisce un meccanismo standard per informare dell’avvenuto • il metodo termina la propria esecuzione senza segnalare errori. In questo caso la documentazione del metodo deve indicare chiaramente le precondizioni. La responsabilità di ottemperare alle precondizioni e’ del metodo chiamante. • Eseguire solo se le precondizioni sono soddisfatte: System.exit(0) invece termina l’esecuzione segnalando che tutto è andato bene if (precondizioni) {... corpo del metodo ...} Usare una asserzione – questo però si può fare solo per metodi con valore di ritorno void, altrimenti che cosa restituisce il metodo? – Se restituisce un valore casuale senza segnalare un errore, chi ha invocato il metodo probabilmente andrà incontro a un errore logico • Il meccanismo delle “asserzioni” si usa solo per i programmi in fase di sviluppo e collaudo. • Noi non lo esaminiamo Lanciare un’eccezione 99 Lanciare una eccezione Enunciato throw Il meccanismo generale di segnalazione di errori in Sintassi: Java consiste nel “lanciare” (throw) un’eccezione si parla anche di sollevare o generare un’eccezione Lanciare un’eccezione in risposta ad un parametro che non rispetta una precondizione è la soluzione più corretta in ambito professionale la libreria standard mette a disposizione tale eccezione: IllegalArgumentException public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException(); balance = balance + amount; } 100 throw oggettoEccezione; Scopo: lanciare un’eccezione Nota: di solito l’oggettoEccezione viene creato con new ClasseEccezione( ) Non è necessario memorizzare il riferimento in una variabile oggetto 101 102 Le eccezioni in Java Quando un metodo lancia un’eccezione l’esecuzione del metodo viene interrotta l’eccezione viene “propagata” al metodo chiamante, la cui esecuzione viene a sua volta interrotta l’eccezione viene via via propagata fino al metodo main, la cui interruzione provoca l’arresto anormale del programma con la segnalazione dell’eccezione che è stata la causa di tale terminazione prematura Il lancio di un’eccezione è quindi un modo per terminare un programma in caso di errore non sempre, però, gli errori sono così gravi... Gestire le eccezioni Esaminiamo un problema tipico: convertire stringhe in numeri Supponiamo che la stringa sia stata introdotta dall’utente se la stringa non contiene un numero valido, viene generata un’eccezione NumberFormatException sarebbe interessante poter gestire tale eccezione, segnalando l’errore all’utente e chiedendo di inserire nuovamente il dato numerico, anziché terminare prematuramente il programma Possiamo intercettare l’eccezione e gestirla con il costrutto sintattico try/catch 103 Gestire le eccezioni 104 Gestire le eccezioni L’enunciato che contiene l’invocazione del metodo che può generare l’eccezione deve essere racchiuso tra due parentesi graffe, precedute dalla parola chiave try Il blocco try è seguito da una clausola catch, definita in modo simile ad un metodo che riceve un solo parametro, del tipo dell’eccezione che si vuole gestire try { ... n = Integer.parseInt(line); ... } catch (NumberFormatException e) { System.out.println("messaggio"); } Nel blocco catch si trova il codice che deve essere eseguito nel caso in cui si verifichi l’eccezione l’esecuzione del blocco try viene interrotta nel punto in cui si verifica l’eccezione e non viene più ripresa Bisogna poi sapere di che tipo è l’eccezione generata dal metodo nel nostro caso NumberFormatException 105 Esempio: gestire NumberFormatException import java.util.Scanner; public class IntEater { public static void main(String[] args) { System.out.println("Passami un numero int!"); Scanner in = new Scanner(System.in); int n = 0; boolean done = false; while (!done) { try{ String line = in.nextLine(); n = Integer.parseInt(line); // l'assegnazione seguente e` eseguita // solo se NON viene lanciata l'eccezione done = true; } catch(NumberFormatException e) { System.out.println("No, voglio un int"); // all'uscita del blocco catch done e` false } } System.out.println("Grazie, " + n + " e` un numero int!"); } } 106 Enunciati try/catch Sintassi: try { enunciatiCheForseGeneranoUnaEccezione } catch (ClasseEccezione oggettoEccezione) { enunciatiEseguitiInCasoDiEccezione } Scopo: eseguire enunciati che possono generare una eccezione se si verifica l’eccezione di tipo ClasseEccezione, eseguire gli enunciati contenuti nella clausola catch altrimenti, ignorare la clausola catch 107 108 Perché usare eccezioni? Enunciati try/catch Idea generale: per gestire un errore di esecuzione ci sono due compiti da svolgere Individuazione Ripristino Problema: il punto in cui viene individuato l’errore di solito non coincide con il punto di ripristino Esempio: il metodo parseInt fallisce (ovvero la stringa in esame non può essere convertita in intero) Il metodo parseInt non sa come gestire questo fallimento: dipende dal contesto! Soluzione: il meccanismo di lancio/cattura di eccezioni Consente di gestire un errore di esecuzione in un punto diverso rispetto a dove questo si è generato Obbliga a gestire l'errore, altrimenti l'esecuzione termina Se gli enunciati nel blocco try possono generare più eccezioni di diverso tipo, è necessario prevedere un blocco catch per ogni tipo di eccezione try { enunciatiCheForseGeneranoPiu’Eccezioni } catch (ClasseEccezione1 oggettoEccezione1) { enunciatiEseguitiInCasoDiEccezione1 } catch (ClasseEccezione2 oggettoEccezione2) { enunciatiEseguitiInCasoDiEccezione2 } 109 110 Tipi di eccezioni Come gestire le eccezioni? Lanciare presto, catturare tardi Quando un metodo incontra un problema che non è in grado di gestire al meglio Conviene lanciare un’eccezione piuttosto che mettere in atto una soluzione imprecisa/incompleta “Lanciare presto” Quando un metodo riceve un’eccezione lanciata da un altro metodo Conviene catturarla (catch) solo se si è effettivamente in grado di risolvere il problema in maniera competente Altrimenti è meglio lasciare propagare l’eccezione (eventualmente fino al metodo main in esecuzione) “Catturare tardi” 111 112 Eccezioni controllate e non Diversi tipi di eccezioni In Java esistono diversi tipi di eccezioni (cioè diverse classi di cui le eccezioni sono esemplari) eccezioni di tipo Error eccezioni di tipo Exception • un sottoinsieme sono di tipo RuntimeException Eccezioni di tipo Error e di tipo RuntimeException sono eccezioni non controllate La loro gestione è facoltativa, ovvero lasciata alla scelta del programmatore se non vengono gestite e vengono lanciate, provocano la terminazione del programma Le altre sono eccezioni controllate la loro gestione è obbligatoria 113 Le eccezioni controllate Descrivono problemi che possono verificarsi prima o poi, indipendentemente dalla bravura dell programmatore • Per questo motivo le eccezioni di tipo IOException sono controllate • Se si invoca un metodo che può lanciare un’eccezione controllata, è obbligatorio gestirla con try/catch • Altrimenti viene segnalato un errore in compilazione Le eccezioni non controllate Descrivono problemi dovuti ad errori del programmatore e che quindi non dovrebbero verificarsi (in teoria…) • Per questo motivo le eccezioni di tipo RuntimeException sono non controllate • Non è obbligatorio catturarle tramite try/catch 114 Gestire le eccezioni di Input/Output Scanner può essere usata anche per leggere file (lo vedremo prossimamente); in questo caso il parametro esplicito e’ un oggetto di classe FileReader Se non trova il file, il costruttore FileReader() lancia l’eccezione controllata java.io.FileNotFoundException Eccezioni controllate import java.io.FileNotFoundException; // QUESTO CODICE import java.io.FileReader; // NON COMPILA! import java.util.Scanner; public class MyInputReader { public static void main(String[] args) { String filename = “filename.txt”; FileReader reader = new FileReader(filename); Scanner in = new Scanner(reader); } MyInputReader.java:9: } 115 Perché le eccezioni di IO sono controllate? Perchè sono associate a problemi che si possono verificare indipendentemente dalla bravura del programmatore Quando si leggono dati da un flusso di input se l’input proviene da un file su disco, il disco può essere difettoso se l’input proviene da una connessione di rete, la connessione può interrompersi durante la lettura se l’input proviene dalla tastiera, l'utente può inserire informazioni non corrette (ad esempio può non inserire input) unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown Gestire le eccezioni di IO Ci sono varie strategie per gestire eccezioni di IO Il modo più semplice intercettiamo (catch) l’eccezione quando si verifica, usando un blocco try con una clausola catch, e terminiamo l’esecuzione del programma con una segnalazione d’errore try { String name = buffer.readLine(); } catch (IOException e) { System.out.println(e); System.exit(1); } 117 Gestire eccezioni di IO System.out.println(e); 116 catch (IOException e) { System.out.println(e); System.exit(1); } Se viene generata l’eccezione, il programma stampa sull’output standard la descrizione testuale standard dell’eccezione System.exit(1); Poi il programma termina l’esecuzione segnalando che qualcosa non ha funzionato correttamente Invece, System.exit(0) termina l’esecuzione segnalando che tutto è andato bene Si possono, ovviamente, adottare strategie di gestione più elaborate Ad esempio consentire l’immissione di un nuovo nome 119 di file se il file non viene trovato 118 Propagare eccezioni di IO: throws Come per le eccezioni non controllate, un metodo può non gestire un’eccezione controllata e propagare l’eccezione Il metodo termina la propria esecuzione E lascia la gestione al metodo chiamante Attenzione: Per dichiarare che un metodo propaga un’eccezione controllata, si contrassegna il metodo con il marcatore throws //questo metodo read non gestisce le FileNotFoundException e lo //deve dichiarare tramite il marcatore throws public void read(String filename) throws FileNotFoundException { FileReader reader = new FileReader(filename); Scanner in = new Scanner(reader); ... } 120 Le eccezioni di Scanner Propagare eccezioni di IO da main Scanner è una classe di utilità. Non appartiene al pacchetto IO e non lancia eccezioni di IO! Le principali eccezioni di Scanner NoSuchElementException lanciata ad es. da next e nextLine se l'input non è disponibile InputMismatchException lanciata da nextInt nextDouble, ecc., se l'input non corrisponde al formato richiesto IllegalStateException lanciata da molti metodi se invocati quando lo Scanner e` “chiuso” (ovvero dopo un'invocazione del metodo close) Suggerimento: quando possibile usiamo solo i metodi next e nextLine di Scanner, se necessario convertiamo stringhe in numeri usando Double.parseDouble e Integer.parseInt Se non si vuole gestire un’eccezione controllata, si può dichiarare che il metodo main la propaga Questo è in assoluto il modo più semplice (anche se non il migliore) di scrivere un main in cui si possono generare eccezioni di IO import java.io.FileNotFoundException; // QUESTO CODICE import java.io.FileReader; // COMPILA! import java.util.Scanner; public class MyInputReader { public static void main(String[] args) throws IOException { String filename = “filename.txt”; FileReader reader = new FileReader(filename); Scanner in = new Scanner(reader); } } 121 122 Gestire le eccezioni di input Abbiamo detto che un flusso va sempre chiuso (usando il metodo close) per rilasciare le risorse E se l’esecuzione viene interrotta da una eccezione prima dell’invocazione del metodo close? Si verifica una situazione di potenziale instabilità Si può usare la clausola finally Il corpo di una clausola finally viene eseguito comunque, indipendentemente dal fatto che un’eccezione sia stata generata oppure no Materiale di complemento (capitolo 11 – eccezioni di IO) 123 124 Gestire eccezioni di input Progettare nuovi tipi di eccezioni Esempio: uso di try/finally Ora sappiamo programmare nei nostri metodi il lancio di eccezioni appartenenti alla libreria standard (throw new …) gestire eccezioni (try/catch) lasciare al chiamante la gestione di eccezioni sollevate nei nostri metodi (throws nell’intestazione del metodo) Non sappiamo ancora creare eccezioni solo nostre Ad esempio, se vogliamo lanciare l’eccezione LaNostraEccezioneException come facciamo? Per ora saltiamo questo argomento (sezione 11.6) FileReader reader = new FileReader(filename); Scanner in = new Scanner(reader); readData(in); reader.close(); // può darsi che non si arrivi mai qui FileReader reader = new FileReader(filename); try { Scanner in = new Scanner(reader); readData(in); }Se finally { reader.close(); } // questa istruzione viene eseguita // comunque prima di passare // l’eccezione al suo gestore • Lo riprenderemo quando avremo studiato il concetto di ereditarietà in Java 125 126 127