Sei sulla pagina 1di 114

SINTASSI JAVA

NOTAZIONE E RAPPRESENTAZIONE NUMERI E CARATTERI

CLASSE JAVA
public class NomeClasse definisce una nuova classe
import.nomePacchetto.NomeClasse; importa una classe
import java.util.Scanner;

VARIABILI E COSTANTI

In Java tutti i tipi di dati fondamentali per numeri interi usano internamente la
rappresentazione in complemento a due.
• La JVM non segnala le condizioni di overflow nelle operazioni aritmetiche, si ottiene
semplicemente un risultato errato
• L’unica operazione aritmetica tra numeri interi che genera una eccezione è la divisione con
divisore zero (ArithmeticException)

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. Il compilatore Java segnala come errore l’utilizzo di variabili
a cui non sia mai stato assegnato un valore (mentre non è un errore la sola definizione...).
Questi errori non sono sintattici, bensì logici, ma vengono comunque individuati dal
compilatore, perché si tratta di errori semantici.
nomeVariabile = valore; assegnazione di un valore ad una variabile

nomeTipo nomeVariabile = espressione; definisce una variabile


int num = 000; definisce una variabile di tipo int e di nome num
float num = 13.9f; definisce una variabile di tipo float e di nome num
String greeting = “abcd”; definisce un oggetto di tipo string e di nome greeting

1.93E3 vale 1.93 * 10³ esprime un numero in forma esponenziale

final nomeTipo NOME_COSTANTE = espressione; definisce una costante


final double EURO_PER_DOLLAR = 0.72;

num += 33; incrementa la variabile num di 33


num -= 33; decrementa la variabile num di 33
counter++; incrementa la variabile counter di 1
counter--; decrementa la variabile counter di 1

OGGETTI
Un oggetto è un’entità che può essere manipolata in un programma mediante l’invocazione di
metodi.
NomeClasse nomeOggetto; definisce una variabile oggetto

new NomeClasse (parametri); crea un nuovo oggetto


Rectangle box = new Rectangle (5,10,20,20);
Rectangle box2 = new Rectangle (box.getX(), box.getY(), box.getWidth(), box.getHeight() );
box2.translate(15,25); trasla il rettangolo

Rectangle box = new Rectangle (5,10,20,20);


Rectangle box2 = box; fa riferimento all'oggetto box, usando box o box2 modifico lo stesso
oggetto

STRING E CHAR
int n = greeting.length(); population.trim() elimina gli spazi alla fine
String r = river.replace(“issipp”,”our”);
String big= river.toUpperCase();
String small= river.toLowerCase();

L’applicazione di questi metodi non altera il contenuto della stringa s, ma restituisce una
nuova stringa! In generale, nessun metodo della classe String modifical’oggetto con cui viene
invocato!! si dice perciò che gli oggetti della classe String sono oggetti immutabili.

La posizione dei caratteri nelle stringhe viene numerata a partire da 0 anziché da 1


String sub = greeting.substring(0,4); salva i caratteri dalla posizione 0 alla posizione 3
ovvero i primi 4. 0-4 individua la lunghezza della sottostringa. La posizione dell'ultimo
carattere corrisponde alla lunghezza della stringa meno 1.
String sub = greeting.substring(7); salva i caratteria dalla posizione 7 alla fine della
stringa
String s = euro + “” + euroName; concatena tre stringhe (di cui una è la stringa vuota)

int age=Integer.parseInt(ageString); converte una stringa contenente un numero intero


in int
double number = Double.parseDouble(numberString);
String s = “” + numInt; converte un numero in una stringa (brutto)
String s = Integer.toString(numInt); converte un numero intero in una stringa
String s = Double.toString(numDouble); converte un numero in virgola mobile in una
stringa

char ch = s.charAt(2); restituisce il singolo carattere nella posizione 2 della stringa s


char ch = 'x'; definisce una variabile di tipo char
char ch = '\u00E9'; la lettera “è” è definita mediante una SEQUENZA DI ESCAPE
int intc = ch1 + ch2; somma i codici ascii dei due caratteri. LA SOMMA DI DUE
CARATTERI NON DA UNA STRINGA. Se un char viene concatenato ad una stringa si ottiene
una stringa.

Oltre a \n, anche altre sequenze di escape possono essere usate per rappresentare caratteri
di controllo! Ovvero caratteri Unicode che non rappresentano simboli scritti! Ma che fanno
parte integrante di un flusso di testo, come tutti gli altri caratteri. I primi 32 caratteri nella
codifica Unicode sono tutti caratteri di controllo (Il cosiddetto insieme C0 di ASCII e Unicode)
Importanti caratteri di controllo
ETX (End-of-TeXt), immesso da tastiera con <CTRL> +C (usato per interrompere
l'esecuzione di un programma)
EOT (End-Of-Transmission, <CTRL>+D) e SUB (SUBstitute, <CTRL>+Z) (usati per segnalare
la fine dell'input, ad esempio di un file)

CONVERSIONI NUMERICHE E FORMATTAZIONE NUMERI


double doubleVar = intVar; converte un numero da int a double (non c'è perdita di
informazione)
int intVar = (int) doubleVar; converte un numero da double a int atrraverso un cast
(conversione forzata senza arrotondamento), senza il cast non viene convertito;

int intVar = (int) Math.round(doubleVar); Math.round arrotonda il numero, vuole


come parametro esplicito un double e restituisce un long, che in questo caso è convertito in
int con cast
Math.round è un metodo statico
Math.pow(x,y); Math.sqrt(x); Math.log(x);
Math.sin(x); Math.E; Math.PI;

BigInteger a = new BigInteger (“12345678”); salva grandi numeri interi


BigDecimal a = new BigInteger (“1234.5678”); salva grandi numeri in virgola mobile
BigInteger c = a.multiply(b); sugli oggetti devo usare i metodi non +,-,*,/

System.out.printf(“Totale:%5.2f” , total); stampa un numero in virgola mobile (%f)


formato da un totale di 5 caratteri compreso il punto, di cui 2 cifre dopo la virgola, questo
formato viene applicato alla variabile total, che è il secondo parametro esplicito
System.out.printf(“Totale:%5.2f%n” , total); stampa come la precedente ma poi va
a capo (%n vuol dire nuova riga)
System.out.printf(“%5.2f + %5.2f = %5.2f”,a,b,s); stampa la somma s di a e b
System.out.printf(Locale.ITALY, “%5.2f + %5.2f = %5.2f”,a,b,s);
Impostazioni locali nell'uso dell'input standard
A partire dalla JDK5.0 e nelle seguenti versioni, alcune funzioni di ingresso/uscita sono dipendenti da
convenzioni in uso localmente, dette impostazioni locali. Esempi di impostazioni locali sono il formato
dell'ora e il carattere di separazione nei numeri espressi in virgola mobile (come e` noto, nel mondo
anglosassone il carattere di separazione fra la parte intera e la parte frazionaria e` il carattere '.',
mentre in Italia e` il carattere ',').
Quando si usano oggetti di classe java.util.Scanner per acquisire dati in ingresso, il programmatore
puo` decidere esplicitamente l'impostazione locale. Se non sceglie, viene automaticamente usata
l'impostazione dell'elaboratore su cui viene eseguito il programma, detta impostazione di default.
La scelta dell'impostazione locale si fa invocando sull'oggetto di classe Scanner il
metodo useLocale(Locale locale), che accetta come parametro un oggetto di classe Locale del
pacchetto java.util.
La classe java.util.Locale contiene la definizione di comodi oggetti costanti da usare come
parametro; ad esempio:
•Locale.US oggetto costante per le impostazioni locali americane (USA)
•Locale.ITALY oggetto costante per le impostazioni locali italiane

Di seguito un esempio di uso esplicito di impostazioni locali:


import java.util.Scanner;
import java.util.Locale;
... Scanner in = new Scanner(System.in);
in.useLocale(Locale.US); // usa la convenzione USA ....
Approfondimento
Anche gli oggetti di classe java.io.PrintStream come System.out possono adoperare impostazioni
locali. In questo caso si usa il metodo format(Locale locale, String format, Object ...args) che
invia a standard output la stringa format. Si usano le formattazioni introdotte per il
metodo printf() della stessa classe. Ad esempio, per stampare a standard output un numero in
virgola mobile con una impostazione locale italiana si scrive:
import java.util.Locale;
...
double d = 12.3;
System.out.format(Locale.ITALY, "%f", d);
...

INPUT
import java.util.Scanner;
import java.util.Locale;

Scanner leggi = new Scanner (System.in); legge dati in ingresso dallo standart
input (la tastiera), ricevuti attraverso l'oggetto System.in, creando un oggetto di tipo scanner
in.useLocale (Locale.US); riconosce numeri formattati secondo diverse usanze locali
in.close(); chiude l'oggetto di classe Scanner

int num = in.nextInt(); acquisisce valori numerici interi da tastiera (l'acquisizione


termina con invio)
double num = in.nextDouble(); acquisisce valori numerici in virgola mobile da
tastiera

String s = in.next(); acquisisce da tastiera una parola (l'acquisizione termina con lo


spazio o con invio)
String s = in.nextLine(); acquisisce da tastiera una riga (l'acquisizione termina con
invio)

int age = Integer.parseInt(in.nextLine() );


String initials = nome.substring(0,1) + cognome.substring(0,1);
String pw = initials.toLowerCase() + age;
PROGETTARE UNA CLASSE

Progettare la classe
Stabilire quali sono le caratteristiche essenziali degli oggetti della classe, e fare un elenco delle
operazioni che sara` possibile compiere su di essi: processo di astrazione
Definire e scrivere l'interfaccia pubblica. Ovvero, scrivere l'intestazione della classe, definire i
costruttori ed i metodi pubblici da realizzare, e scrivere la firma (specificatore di accesso, tipo di
valore restituito, nome del metodo, eventuali parametri espliciti) di ciascuno di essi.
Consiglio: se un metodo non restituisce valori (ovvero il tipo del valore restituito e` void), scrivete
inizialmente un corpo vuoto, ovvero {}. Se un metodo restituisce valori non void, scrivete
inizialmente un corpo fittizio contenente solo un enunciato di return: ad esempio {return 0;} per
metodi che restituiscono valori numerici o char, {return false;} per metodi che restituiscono valori
booleani, {return null;} per metodi che restituiscono riferimenti ad oggetti. Con questi accorgimenti il
codice compilera` correttamente fin dall'inizio del vostro lavoro. Quando poi scriverete i metodi,
modificherete le istruzioni di return secondo quanto richiesto da ciascun metodo.
Definire le variabili di esemplare ed eventuali variabili statiche. E` necessario individuare tutte le
variabili necessarie. Per ciascuna di esse si deve, poi, definire tipo e nome.
Realizzare la classe
Verificate le impostazioni del vostro editor di testi, al fine rendere piu` efficiente il vostro lavoro di
programmazione. In particolare, verificate ed eventualmente modificate le impostazioni per i rientri di
tabulazione (valori tipici sono di 3 o 4 caratteri), e visualizzate i numeri di riga. A questo proposito si
vedano anche i Consigli per la produttivita' 5.1 sul libro di testo)
Scrivere il codice dei metodi.
Consiglio: non appena si e` realizzato un metodo, si deve compilare e correggere gli errori di
compilazione (se il corpo del metodo e` particolarmente lungo e complicato, compilare anche prima di
terminare il metodo). Non aspettate di aver codificato tutta la classe per compilare! Altrimenti vi
troverete, molto probabilmente, a dover affrontare un numero elevato di errori, con il rischio di non
riuscire a venirne a capo in un tempo ragionevole.
Collaudare la classe
Ovvero scrivere una classe di collaudo contenente un metodo main, all'interno del quale vengono
definiti e manipolati oggetti appartenenti alla classe da collaudare.
E` possibile scrivere ciascuna classe in un file diverso. In tal caso, ciascun file avra` il nome della
rispettiva classe ed avra` l'estensione .java. Tutti i file vanno tenuti nella stessa cartella, tutti i file
vanno compilati separatamente, solo la classe di collaudo (contenente il metodo main) va eseguita.
E` possibile scrivere tutte le classi in un unico file. In tal caso, il file .java deve contenere una sola
classe public. In particolare, la classe contenente il metodo main deve essere public mentre la classe
(o le classi) da collaudare non deve essere public (non serve scrivere private, semplicemente non si
indica l'attributo public). Il file .java deve avere il nome della classe public

METODI COSTRUTTORI E VARIABILI ESEMPLARI


oggetto.nomeMetodo(parametri); invoca un metodo
System.out.println(“abcd”);
System.out.print(“abcd”);

NomeClasse.nomeMetodo(parametri); invoca un metodo statico


Un metodo statico elabora o modifica solo i propri parametri espliciti

nomeIstanza.nomeMetodo(parametri); invoca un metodo non statico

specificatoreDiAccesso tipoDatiRestituiti nomeMetodo (TipoParametro


nomeParametro) {...}
definisce un metodo (firma), costituita da SPECIFICATORE DI ACCESSO, TIPO DI DATI
RESTITUITO, NOME DEL METODO, ELENCO DI PARAMETRI di cui si indica il tipo e il nome
(eventualmente vuoto), più parametri sono separati dalla virgola. La firma è seguita dal
corpo del metodo stesso, tra graffe. Ci sono metodi di accesso e metodi modificatori.
public void println (String output) {...}
public String replace (String target, String replace) {...}
public static long round (double a) {...}
public void deposit(double amount) {...}
public double getBalance {...}
tipoAccesso NomeClasse (TipoParametro nomeParametro,...) {... realizzazione del
costruttore...}
definisce un costruttore che serve per consentire ad altri utenti di costruire oggetti della
classe NomeClasse inizializzandone lo stato . Il nome del costruttore è uguale al nome della
classe. Se per una classe ci sono più costruttori essi si differenziano per i metodi espliciti
dichiarati.i costruttori non restituiscono alcun valore (non bisogna dichiarare void).

New NomeCostruttore;
invoca il costruttore. L'operatore new riserva la memoria per l'oggetto mentre il costruttore
definisce il suo stato iniziale

public class NomeClasse {...


tipoDiAccesso TipoVariabile nomeVariabile;
...}
definisce una VARIABILE ESEMPLARE nomeVariabile di tipo tipoVariabile, una cui copia sia
presente in ogni oggetto della classe NomeClasse. Le variabili di esemplare memorizzano lo
stato di un oggetto. Sono di solito “private” ovvero possono essere lette o modificate soltanto
dai metodi della classe a cui appartengono, o mediante l'invocazione di metodi pubblici della
classe. Ciascun oggetto della classe ha una propria copia della variabile di esemplare.

DEFINIRE UNA CLASSE


public class BankAccount
{
//COSTRUTTORI
public BankAccount()
{ balance=0;
}

public BankAccount(double initialBalance)


{ balance = initialBalance;
}

//METODI
public void deposit (double amount)
{ balance = balance + amount;
}

public void withdraw (double amount)


{ balance = balance - amount;
}
public double getBalance()
{return balance;
}

//CAMPI DI ESEMPLARE
private double balance;
}

Tutti i costruttori di una classe devono avere lo stesso nome (ma diversi parametri espliciti).
“return espressione;” definisce il valore restituito dal metodo che deve essere del tipo
dichiarato nella firma del metodo.
“return;” termina l'esecuzione di un metodo, ritornando all'esecuzione sospesa del metodo
invocante.
Al termine di un metodo con valore restituito di tipo void viene eseguito un return implicito. Il
compilatore segna errore se si ternima senza return o se il tipo di dato return non
corrisponde a quello dichiarato nella firma del metodo.

BankAccount account = null; significa che la variabile oggetto account non fa riferimento
ad alcun oggetto e non può essere quindi usata per invocare metodi
COLLAUDARE LA CLASSE
le classi di colaudo devono contenere un metodo main che deve essere public. Per scrivere
più classi nello stesso file chiamo solo una classe public: la classe col main (le altre sono
private ma non serve indicarlo, semplicemente non si scrive). Il file deve chiamarsi come la
classe col main.

//APRIRE UN NUOVO CONTO E DEPOSITARE DENARO


double initialDeposit = 1000;
BankAccount account = new BankAccount();
account.deposit(initialDesposit);
System.out.println(“saldo: “ + account.getBalance() );

//FARE UN BONIFICO
double amount = 500;
account1.withdraw(amount);

//ACCREDITARE INTERESSI
double rate = 0.05;
double amount = account.getBalance()*rate;
account.deposit(amount);

CONFRONTO TRA NUMERI IN VIRGOLA MOBILE

final double EPSILON = 1E-14;


if (Math.abs(x-y) <=EPSILON*Math.max(Math.abs(x), Math.abs(y)) )
...
verifica l'uguaglianza tra x e y di tipo double (10^-14 è un valore di differenza ragionevole
tra numeri double (il loro confronto deve prevedere una tolleranza. Math.max(|x|,|y|) lo uso
se x e y molto grandi o molto piccoli).

Posso anche definire un metodo statico che verifichi l'uguaglianza con tolleranza (in un solo
file):
import java.util.Scanner;
public class ConfrontoDouble {
static class Comparator {
public static boolean approxEqual (double x1, double x2, int digits)
{
double scale = Math.pow (10, digits);
return Math.round(scale*x1) == Math.round(scale*x2);
}
}
public static void main (String [] args)
{
Scanner in = new Scanner (System.in);
System.out.println("scrivi due numeri double");
double x1 = in.nextDouble();
double x2 = in.nextDouble();
final int digits = 2;
String negation = " non";
if (Comparator.approxEqual(x1,x2,digits)) negation = "";

System.out.println(x1 + " e " + x2 + negation + " sono approssimativamente


uguali");
}
}

le parti in blu possono essere omesse (in questo caso approxEqual fa riferimento alla sua
classe).
CONFRONTO STRINGHE (e OGGETTI)
Per comparare due stringhe non si usa == ma un metodo (equals)
if (s1.equals(s2) ) restituisce true o false
if (s1.equalsIgnoreCase(s2) ) confronta s1 e s2 ignorando maiuscole e minuscole

se uso == (non è un errore di sintassi) il risultato del confronto sembra essere casuale.
Per verificare se un oggetto si riferisce a null uso ==
if (s1==null) restituisce true o false

if (s1.compareTo(s2) <0) fa un confronto lessicografico tra s1 e s2


compareTo restituisce un valore NEGATIVO se s1 precede s2, un valore POSITIVO se s1
segue s2, 0 se s1 e s2 sono identiche.
I numeri precedono le lettere. Le maiuscole precedono le minuscole. Lo spazio precede tutto.

Il metodo equals può essere applicato a qualsiasi oggetto (perchè è definito nella classe
Object) ma è compito di ciascuna classe ridefinire il proprio metodo equals poiché esso
prevede il confronto delle caratteristiche (=variabili esemplare) degli oggetti specifici della
classe.
CompareTo non è definito in Object quindi può essere usato per molti oggetti ma non per
tutti.

ESPRESSIONI BOOLEANE
ogni espressione relazionale (<<= ecc) ha un valore booleano (true o false). Essi
appartengono ad un tipo di dati fondamentale detto “booleano”. Le variabili booleane possono
assumere solo i valori TRUE o FALSE per questo a volte vengono chiamate FLAG.
Int x;
...
a = x>0;
i metodi che restituiscono valori di tipo booleano vengono chiamati METODI PREDICATIVI e
solitamente verificano una condizione sullo stato di un oggetto (iniziano con “is” oppure
“has”). I metodi predicativi possono essere ustati come condizioni di enunciati if.

Character.isDigit(ch), Character.isLetter(ch), Character.isUpperCase(ch),


Character.isLowerCase(ch)
.hasNextInt, .hasNextDouble

Gli operatori booleani sono ! (not) && (and) || (or) in ordine di precedenza.
La valutazione di un'espressione con operatori booleani viene effettuata con la strategia del
cortocircuito (o valutazione pigra) ovvero la valutazione di un'espressione termina appena
è possibile decidere il risultato. TRUE or qualcosa è sempre TRUE.

CONDIZIONI E CICLI/ITERAZIONI
IF
if (condizione)
{ enunciati; }
else
{ enunciati; }

Una clausola else appartiene sempre all'if più vicino.


Una variabile, se dichiarata dentro l'if, è visibile solo all'interno dell'if.
LA VISIBILITA' DI VARIABILI LOCALI E' INTERNA AL BLOCCO IN CUI SONO DEFINITE

SWITCH
confronta un'unica variabile con diverse alternative costanti
int x;
int y; ...
switch (x)
{ case 1: y=1; break;
case 2: y=9; break; ...
case default: y=4; break; }
WHILE
while (condizione)
{ enunciati;}

esegue gli enunciati finché la condizione è vera, realizzando un ciclo. L'enunciato solitamente
contiene l'incremento di una variabile (es: year++, i++,...).
Nel caso in cui vengano generati cicli infiniti (a causa di errori logici) bisogna arrestare il
programma con CTRL+C o riavviando il pc.

DO-WHILE
do { enunciati; }
while{condizione}

Capita di dover eseguire il corpo di un ciclo almeno una volta, per poi ripeterne l’esecuzione
se è verificata una particolare condizione
• Esempio: leggere un valore in ingresso, eventualmente rileggerlo finché non viene
introdotto un valore “valido”.

// si usa un’inizializzazione "ingiustificata" double rate;


double rate = 0; do
while (rate <= 0) { System.out.println("Inserire il tasso:");
{ System.out.println("Inserire il tasso:"); rate = console.readDouble();
rate = console.readDouble(); } while (rate <= 0);
}

FOR
for(inizializzazione; condizione; aggiornamento) { enunciati;}

l'inizializzazione può contenere la definizione di una variabile che sarà visibile solo all'interno
del corpo del ciclo.

Esempio: stampare una tabella (ciascuna cella di larghezza 5) con i valori della potenze di xy
per ogni valore di x tra 1 e 4 e per ogni valore di y tra 1 e 5.
final int COLUMN_WIDTH = 5;

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
//converte in stringa il valore
String p = “” + (int)Math.round(Math.pow(x,y) );
while (p.lenght()<COLUMN_WIDTH)
{ //aggiunge gli spazi necessari
p= “ ” + p;
}

System.out.print(p);
}

System.out.println();
}

FOR-EACH
Serve nel caso in cui si voglia eseguire un determinato blocco di codice per ogni elemento di
una data collezione (o array).
for( Type item : itemCollection ) { ... }

prendi uno ad uno gli elementi della collezione itemCollection, assegna ciascuno di essi alla variabile item ed
esegui per ciascun elemento il blocco (che potrà quindi usare item al suo interno)
IL PROBLEMA DEL CICLO E MEZZO

boolean done = false;


while (!done) //ciclo e mezzo
{
System.out.println("Inserire numero (Q per terminare sequenza):");
String newInput = in.next();
if (newInput.equalsIgnoreCase("Q"))
done = true;
else // aggiorniamo la somma, la somma dei quadrati, il contatore
{
double value = Double.parseDouble(newInput);
sum += value;
sumSquares += value * value;
count++;
}
}

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.

PARAMETRI DEI METODI


public void deposit (double amount)
{ balance = balance + amount;
}

In questo caso amount è il parametro esplicito del metodo.


Balance si riferisce alla variabile di esemplare balance della classe BankAccount di cui esiste
una copia per ogni oggetto della classe (ma alla variabile balance di quale oggetto si riferisce
il metodo?). Si riferisce alla variabile che appartiene all'oggetto con cui viene invocato il
metodo.

account.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 “this”. Ogni metodo ha solo un parametro
implicito (i metodi statici non hanno parametro implicito). Il parametro implicito non deve
essere dichiarato e si chiama sempre this.

Public void deposit (double amount)


{ this.balance = this.balance + amount;
}
la parte blu posso non metterla

this è di tipo BankAccount. Se un metodo si riferisce ad un campo di esemplare il compilatore


costruisce automaticamente un riferimento al campo di esemplare dell'oggetto raprresentato
dal parametro implicito this.

CLASSI DI UTILITA'
sono classi che non servono a creare oggetti ma che contengono metodi statici (es: classe
Math)
Spesso servono variabili condivise da tutti gli oggetti di una classe (le variabili di esemplare
sono copiate in ogni oggetto, quindi ogni oggetto può averne un valore diverso). Queste si
chiamano VARIABILI STATICHE o VARIABILI DI CLASSE (non devono essere inizializzate nei
costruttori ma fuori.
VARIABILI STATICHE
private static int lastAssignedNumber;

crea una variabile statica. Ogni metodo o costruttore di una classe può accedere alla variabile
statica e modificarla (di variabile statica ce n'è una sola, di variabile esemplare ce ne sono più
copie).

public class BankAccount


{ ...
private int accountNumber; //modifica BankAccount in modo che contenga un numero di conto
private static int lastAssignmentNumber = 0; //inizializzata fuori dal costruttore
...
public BankAccount()
{ lastAssignmentNumber++; //ogni oggetto ha il numero di conto incrementato di uno
accountNumber = lastAssignmentNumber;
}
}

Se lastAccountNumber non fosse dichiarata static, ogni istanza di BankAccount avrebbe il


proprio valore di lastAssignmentNumber.

public static final double PI=3.14... COSTANTE STATICA nella classe Math

CICLO DI VITA DELLE VARIABILI


-VARIABILE LOCALE
● creata quando viene eseguito l'enunciato in cui è definita
● eliminata quando l'esecuzione del programma esce dal blocco di enunciati in cui la
variabile è definita
● se non è definita in un blocco di enunciati viene eliminata quando l'esecuzione del
programma esce dal metodo in cui è definita
-VARIABILE PARAMETRO (quelle che passiamo come parametro dei metodi)
● creata quando viene invocato il metodo
● eliminata quando l'esecuzione del metodo termina
-VARIABILE DI ESEMPLARE
● creata quando la JVM carica la classe per la prima volta
● eliminata quando la JVM scarica la classe (=rimane sempre..)
-VARIABILE STATICA
● creata quando viene creato l'oggetto
● eliminata quando viene eliminato l'oggetto, ovvero quando non esiste più nessun
riferimento ad esso nel programma (memoria periodicamente riciclata = garbage
collection)

MEMORIA
al momento dell'esecuzione a ciascun programma java viene assegnata un'area di memoria
-AREA STATICA
memorizzare il codice
-JAVA STACK (=pila)
area dinamica (cambia dimensione durante l'esecuzione) in cui vengono memorizzati i
parametri e le variabili locali che vengono create man mano
-JAVA HEAP (=cumulo)
area dinamica in cui vengono creati oggetti durante l'esecuzione dei metodi (con new)
VISIBILITA' DI MEMBRI DI CLASSE

-MEMBRI PRIVATE
● hanno visibilità di classe, qualsiasi metodo di una classe può accedere a variabili e
motodi della stessa classe
-MEMBRI PUBLIC
● hanno visibilità al di fuori della classe a patto di renderne qualificato il nome, ovvero:
.specificare il nome nella classe per membri STATIC (Math.round)
.specificare l'oggetto per membri NON STATIC (s1.lenght)
.non è necessario qualificare i membri appartenenti a una stessa classe perchè
ci si riferisce automaticamente al parametro implicito this

public class BankAccount


{
public void transfer (double amount, BankAccount other)
{ withdraw (amount); //cioè this.withdraw
other.deposit(amount);
}

public void withdraw (double amount)


{ if (balance > amount) //cioè this.balance
balance = balance – OVERDAFT_FEE; //cioè BankAccount.OVERDAFT_FEE
else ...
}
...
private static double OVERDAFT_FEE = 5; //multa
...
}

gli ambiti di una variabile di esemplare e di una variabile locale possono sovrapporsi. Quella
di esmplare viene messa in ombra, prevale il nome della variabile locale. La variabile
esemplare può essere sempre qualificata usando this. (idem per la variabile statica invece di
quella di esemplare).

CHIAMATE PER VALORE E RIFERIMENTO


passaggio in java è sempre per valore (passo il valore della variabile oggetto nel caso del
riferimento). 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 è la possibilità di modificare lo stato dell'oggetto stesso.

ERRORI CON I COSTRUTTORI

● INVOCARE METODI SENZA OGGETTO

public class Program


{ public static void main (String [] args)
{ BankAccount account = new BankAccount();
deposit (500); //ERRORE

il compilatore non sa a quale oggetto applicare deposit che non è statico e quindi richiede
sempre un riferimento.

Mentre è possibile invocare un metodo non statico senza riferimento ad un oggetto, quando
lo si invoca da un altro metodo non statico della stessa classe.

public void withdraw (double amount)


{ balance = this.getBalance() -amount; //this non serve
COESIONE ED ACCOPPIAMENTO
Metodi pubblici e costanti elencati nell'interfaccia pubblica devono avere una buona coesione
ovvero devono essere strettamente correlate al singolo concetto rappresentato dalla classe.
Le classi accoppiate (quando una classe nel proprio codice usa un'altra classe) sono
dipendenti, bisogna eliminare tutti gli accoppiamenti non necessari perchè se una classe
viene modificata anche tutte quelle da cui dipende potrebbero richiedere modifiche.

EFFETTI COLLATERALI
E' l'effetto di un metodo, osservabile fuori dal metodo. Qualsiasi metodo modificatore ha
effetti collaterali (modificano il proprio parametro implicito), altri metodi possono modificare il
parametro esplicito.
Un altro effetto collaterale è la visualizzazione di dati in uscita.

PACCHETTI
import nomePacchetto.NomeClasse;
import nomePacchetto.*; per importare tutte le classi di un pacchetto

se una classe appartiene a più di due pacchetti importati il compilatore da errore (riferimento
ambiguo) quando uso un metodo della classe o un costruttore devo indicare il nome completo
della classe (es: java.math.BigInteger a = new java.math.BigInteger(“123456”); )

PRECONDIZIONI
come deve reagire un metodo se riceve un parametro che non rispetta i requisiti richiesti
(=PRECONDIZIONI)?

1) Potrei eseguire solo se le precondizioni sono esatte, ma questo va bene solo per metodi
con valori di ritorno void, se restituisce un valore casuale senza segnalare errore è un
problema

2)potrei terminare il programma con System.exit(1), System.exit(0) termina l'esecuzione


segnalando che tutto è andato bene. Si usa soltanto in programmi non professionali

3)potrei usare un asserzione, ma si usa soltanto per programmi in fase di sviluppo e collaudo

4)potrei LANCIARE UN'ECCEZIONE

ECCEZIONI

GESTIONE ECCEZIONI
Le eccezioni in java sono OGGETTI che non è necessario memorizzare. Ci sono molte classi di
eccezioni. Il meccanismo di lancio e cattura delle eccezioni permette di gestire un errore di
esecuzione in un punto diverso rispetto a dove questo si è generato.

“lanciare presto, catturare tardi”


Quando un metodo incontra un problema che non è in grado di gestire meglio conviene
lanciare un'eccezione. Quando un metodo riceve un'eccezione lanciata da un altro metodo
conviene catturarla solo se si è in grado di risolvere bene il problema altrimenti è meglio
propagarla fino al main.

throw oggettoEccezione;

di solito l'oggetto eccezione viene creato con new ClasseEccezione()

public void deposit (double amount)


{ if (amount <=0)
throw new IllegalArgumentException();
balance = balance + amount;
}
In questo caso il programma non è detto che termini. Viene interrotta l'esecuzione del
metodo, l'eccezione viene propagata al metodo chiamante (potrebbe essere anche il main) la
cui esecuzione viene a sua volta interrotta. L'eccezione viene via via propagata fino al main e
ciò provoca l'arresto anormale del programma con segnalazione dell'eccezione. Bisogna
gestire le eccezioni, per non farle arrivare al main.

I metodi che servono a gestire le eccezioni (che potebbero sollevarle) vanno racchiusi in un
blocco TRY {} seguito da catch (che le cattura).

try
{ enunciati che forse generano una o più eccezioni }
catch (ClasseEccezione1 nomeOggetto1)
{ enunciati eseguiti in caso di eccezione1 }
catch (ClasseEccezione2 nomeOggetto2)
{ enunciati eseguiti in caso di eccezione2 }

Se per esempio voglio risolvere il problema di convertire in fomato numerico una stringa, se
la stringa non contiene un numero valido viene generata un'eccezione
NumberFormatException

try{
...
n = Integer.parseInt(line;
...
}

il blocco try è seguito da una o più clausole CATCH che catturano l'eccezione. Definita in
modo simile ad un metodo riceve un solo parametro del tipo dell'eccezione che si vuole
gestire

catch(NumberFormatException e) // la “e” è il nome del parametro


{ System.out.println(“messaggio”);
}

l'eccezione non si propaga più al main ma esegue il catch. Nel catch ci sono le istruzioni che
devono essere eseguite 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.
E se il metodo con più precondizioni genera più eccezioni? Devo scrivere più catch perchè
eccezioni diverse vanno gestite il modo diverso

public static void main


{ System.out.println(“scrivi un int”);
...
int n=0;
boolean done = false;
while (!done)
{ try
{ String line = in.nextLine();
n= Integer.parseInt(line);
//l'asseganzione viene eseguita solo se non viene lanciata l'eccezione
done=true;
}
catch (NumberFormatException e)
{ System.out.println(“no devi inserire un int”);
//all'uscita del blocco catch done è false, quindi l'utente lo reinserisce
}
}
System.out.println(“si è un numero int”);
}
TIPI DI ECCEZIONI
-tipo error
-tipo exception

Quelle di tipo error e RuntimeException sono eccezioni non controllate (UNCHECKED) e la


loro gestione è facoltativa, se non vengono gestite provocano la terminazione del
programma. Descrivono problemi dovuti ad errori del programmatore e che non dovrebbero
verificarsi. Non è obbligatorio catturarle con try catch.

Le altre sono eccezioni controllate (CHECKED) e la loro gestione è obbligatoria. Esse


descrivono problemi che possono verificarsi, per questo motivo le eccezioni del tipo
IOException (input-output) sono controllate.
Devono essere gestite con try e catch sennò danno errore in compilazione.

ECCEZIONI CONTROLLATE

GESTIRE LE ECCEZIONI DI IO
Se Scanner non trova il file, il costruttore FileReader() lancia l'eccezione controllata
java.io.FileNotFoundException

try
{ String name = buffer.readLine();
}
catch (IOException e)
{ System.out.println(e);
System.exit(1);
}

Poi il programma termina l'esecuzione segnalando l'eccezione. Però il metodo che invoca il
metodo che riceve l'eccezione può sperare che il metodo superiore lo possa gestire meglio.
Scanner non appartiene al pacchetto di IO, quindi non lancia eccezioni IO.
PROPAGARE ECCEZIONI DI IO
un metodo può non gestire un'eccezione controllata e propagarla: il metodo termina la
propria esecuzione e lancia la gestione al metodo chiamante.
Per dichiarare che un metodo propaga un'eccezione controllata, si contrassegna il metodo con
il marcatore throws

throws nome1, nome2

public class InputReader


{
public void read (String filaname) throws IOException
{ String filaname = “filename.txt”;
FileReader reader = new FileReader (filename);
Scanner in = new Scanner (reader);
}
}

Se avessi scritto
public void read (String filaname) throws FileNotFoundException
il metodo read non avrebbe gestito le eccezioni FileNotFoundException (lo deve dichiarare con
throws).
Se non si vuole gestire un'eccezione controllata si può dichiarare che il metodo main la
propaga il programma si blocca ma compila

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Scanner;
...
public static void main (String [] args) throws IOException
{ String filaname = “filename.txt”;
FileReader reader=new FileReader (filename);
Scanner in = new Scanner (reader);
}

ECCEZIONI DI SCANNER
Scanner è una classe di utilità. NON lancia eccezioni di IO.
NoSuchElementException (lanciata da next e nextLine se l'input non è disponibile)
InputMismatchException (lanciata da nextInt e nextDouble)
IllegalStateException (lanciata dai metodi se invocati quando lo scanner è chiuso)
MEGLIO USA NEXT() E NEXTLINE() E POI CONVERTIRE CON DOUBLE.PARSEDOUBLE

ECCEZIONI IN INPUT
Cosa succede se l'esecuzione viene interrotta da un'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.

FileReader reader = new FileReader (filename);


try
{
Scanner in = new Scanner (reader);
readData(in);
}
finally
{ reader.close();}
L'istruzione contenuta nel blocco finally viene eseguita comunque prima di passare l'eccezione
al suo gestore ( se non avessi messo finally potrebbe essere che non si arrivi mai al close se
viene generata un'eccezione).
LEGGERE E SCRIVERE SU FILE

LEGGERE DA UN FILE DI TESTO


La gestione dei file avviene interagendo con il sistema operativo mediante classi del
pacchetto java.io.
Per farlo:
1)creo un oggetto lettore di file (FileReader)
2)creo un oggetto Scanner
3)collego l'oggetto Scanner al lettore di file

FileReader reader = new FileReader(“input.txt”); //apre il file in lettura


Scanner in = new Scanner (reader);
...
reader.close();

Possiamo usare next e nextLine per leggere da file. Volendo ci sono anche i metodi di
FileReader.

while(in.hasNextLine() ) //fino a che il file non è finito (metodo predicativo, ritorna boolean)
{ String line = in.nextLine(); //sono sicuro di non sollevare eccezione perche viene eseguito
solo se la riga effettivamente c'è
//elaborazione stringa
}

Il costruttore FileReader lancia IOException (da gestire!!).


Il metodo close() lancia IOException (da gestire!!).

SCRIVERE SU UN FILE DI TESTO


1)creare un oggetto scrittore PrintWriter
2)collegare l'oggetto PrintWriter ad un file

PrintWriter out = new PrintWriter(“output.txt”);


...
out.close();

se output.txt non esiste viene creato, se esiste già viene svuotato prima di essere scritto.
Per aggiungere scritte in coda ad un file già scritto:

FileWriter writer = new FileWriter(“file.txt”), true);


PrintWriter out = new PrintWriter (writer);

BISOGNA SEMPRE CHIUDERE UN OGGETTO PRINTWRITER DOPO AVER FINITO DI USARLO


Per RILEVARE LA FINE DELL'INPUT posso usare q (ciclo e mezzo), hasNext() o
hasNextLine() (restituiscono false quando il file è terminato).

Import java.io.FileReader;
Import java.io.IOException;
Import java.io.PrintWriter;
Import java.util.Scanner;

public class LineNumberer


{
public static void main (String [] args)
{
Scanner console = new Scanner (System.in);
System.out.print(“input file: “);
String inputFileName = console.nextLine();
System.out.print(“output file: “);
String outputFileName = console.nextLine();
try
{
FileReader reader = new FileReader (inputFileName);
Scanner in = new Scanner (reader);
PrintWriter out = new PrintWriter (outputFileName);
int lineNumber=1;

while (in.hasNextLine() )
{
String line = in.nextLine();
out.println(“/*” + lineNumber + “*/” + line);
lineNumber++;
}
out.close();
}
catch (IOException e)
{
System.out.println(“error processing file: “ + e);
}
}
}

Per comunicare la fine dell'input da tastieral'utenze può inserire CTRL-Z (windows) o CTRL-D
(unix). hasNextLine restituirà false.

Per calcolare la somma di numeri:


import java.util.Scanner;
public class SumTester
{ public static void main(String[] args)
{ Scanner in = new Scanner(System.in);
double sum = 0;
boolean done = false;
while (!done)
{
String line;
/* attenzione a questa condizione: stiamo usando la valutazione pigra
if (!in.hasNextLine() || (line = in.nextLine()).equalsIgnoreCase("Q"))
done = true;
else
sum = sum + Double.parseDouble(line);
}
System.out.println("Somma: " + sum);
}
}
SCOMPOSIZIONE DI STRINGHE

Una sottostringa delimitata da caratteri speciali (delimitatori) si chiama TOKEN o LESSEMA.


Scanner considera come delimitatori i caratteri spazio, tab, a capo.
Per scomporre una stringa in token usando scanner:
1)creare un oggetto dellla classe fornendo la stringa come parametro al costruttore

Scanner in = new Scanner (System.in);


String line =in.nextLine();
Scanner t = new Scanner (line);

2)successive invocazioni del metodo next restituiscono successive sottostringhe, fin quando
hasNext restituisce true

while(t.hanNext() )
{ String token = t.next(); //elabora token

import java.util.Scanner;
public class TokenCounter
{ public static void main (String [] args)
{ Scanner in = new Scanner (System.in);
int count = 0;
while (in.hasNextLine() )
{
String line = in.nextLine();
Scanner t = new Scanner (line);
while(t.hanNext() )
{
t.next();
count ++;
}
}
}
}

Student[] studenti = new Student[3];


while (count <3)
{ Scanner in = new Scanner(System.in); //CLOSE THE SCANNER
String line = in.nextLine();
if (line.equals(“”))
break;
Scanner s = new Scanner (line); //CLOSE THE SCANNER
studenti[count++] = new Student (s.next(), s.next(), s.nextDouble(), s.nextDouble() );
}
for (int i =0; i<count; i++)
System.out.println(“\n” + studenti[i].nameInput + studenti[i].surnameInput + ... );

Nei Token per cambiare il delimitatore posso usare .useDelimiter(...)

String input = “1 fish 2 fish 3 fish”

// \\s* significa 0 o più ripetizioni delo spazio bianco, fish è il pattern da trovare

Scanner s = new Scanner (input).useDelimiter(“\\s*fish\\s*”);

System.out.println(s.next() ); // stampa 1
System.out.println(s.next() ); // stampa 2
System.out.println(s.next() ); // stampa 3
REINDIRIZZAMENTO DI INPUT E OUTPUT
System.in non è più tastiera ma, per esempio un file. Per far ciò quando eseguo devo
scrivere < per l'input e > per l'output.

java NomeClasse < testo.txt > output.txt

Il file testo.txt viene collegato all'input standard. Il programma è scritto per esempio cosi:

...
Scanner in = new Scanner (System.in);
while (in.hasNext() )
System.out.println(in.next() );
....

CANALIZZAZIONI (“PIPES”)

sort<temp.txt<testoOrdinato.txt
ma invece di usare un file temporaneo posso usare una canalizzazione:

java NomeFile < testo.txt | sort > testoOrdinato.txt

java NomeFile < testo.txt | sort | wc

FLUSSO DI ERRORE STANDARD

java ha due flussi standard (System.in e System.out). Ma esiste anche il flusso di errore
standard (System.err) che è di tipo PrintStream. La differenza tra out e err è che:
-out si usa per comunicare i risultati dell'eleborazione o dei messaggi
-err si usa per comunicare condizioni di errore

System.err.println(“ERRORE!!”);

lo standard error può essere indirizzato in un altro file rispetto alla standard output.
C:\> java HelloTester > out.txt 2> err.txt
ARRAY
Gli array sono oggetti.

double [] values = new double[10] //sulle seconde quadre c'è il 10 perchè il


computer deve assegnare lo spazio di memoria, sulle prime no perchè si riferisce ad un
oggetto generico
//i valori dell'array vengono inizializzati a 0, le variabili oggetto a null, i boolean a false

double oneValue = values [3] //salva in una variabile il QUARTO valore dell'array

values [3]= a+2; //modifico il quarto elemento

se cerco di leggere elemnti fuori dall'array viene sollevata un'eccezione a


runtime:ArrayIndexOutOfBoundsException

NON CI SONO METODI PUBBLICI APPLICABILI AGLI OGGETTI ARRAY


lenght non è un metodo ma una variabile esemplare pubblica! Che non è modificabile.

int a = nomeVariabileArray.lenght;

values.lenght=15; //ERRORE non è modificabile

...Scanner in = new Scanner(System.in);


double[] values = new double[10];
for (int i = 0; i <values.lenght; i++)
values[i] = in.nextDouble();
System.out.println(“inserisci un numero”);
int index = in.nextInt();
if (index<0 || index>= values.lenght)
System.out.println(“errore”);
else
System.out.ptintln(values[index]); ...

● Creare un oggetto array new NomeTipo[lunghezza];

● Creare un riferimento ad un array

NomeTipo[] nomeRiferimento; o NomeTipo nomeRiferimento[];

● Accedere ad un elemento i un array

riferimentoArray[indice];

NomeTipo[] nomeVariabile = new NomeTipo[lunghezza];

int [] nomeVariabile = {2, 3, 4}; //assegna i valori 2, 3, 4 ai posti 0, 1, 2.

PASSARE ARRAY COME PARAMETRI


public static double sum(double[] values)
{ if (values == null)
throw new IllegalArgumentException();
if (values.lenght == 0)
return 0;
double sum = 0;
for (int i = 0; i < values.lenght; i++)
sum = sum + values[i];
return sum;
}
USARE UN ARRAY COME VALORE DI RITORNO
public static int[] resize (int [] oldArray, int newLenght)
{ if (newLenght <0 || oldArray == null)
throw new IllegalArgumentException();
int[] newArray = new int[newLenght];
int n= Math.min (oldArray.lenght, newLenght);
for (int i = 0; i<n; i++)
newArray[i] = oldArray[i];
return newArray;
}

COPIARE ARRAY
copiando il conenuto della variabile oggetto che fa riferimento ad un oggetto array , NON si
copia l'array ma il riferimento allo STESSO oggetto.

Per copiare un array devo copiare ogni elemnto del primo array in un altro array dello stesso
tipo e dimensione

for (int i=0, i<values.lenght; i++)


otherArray[i] = myArray[i];

oppure

System.arraycopy (myArray, 0, otherArray, 0, myArray.lenght);

System.arraycopy (arrayFrom, indiceFromStart, arrayTo, indiceToStart, count);

oppure per copiare tutto un array c'è clone

double[] otherValues = (double[]) values.clone();


clone restituisce oggetti generici quindi serve il cast
ARRAY RIEMPITI SOLO IN PARTE
dobbiamo far inserire all'utente un tot di numeri di cui non siamo a conoscenza (non so
quanti elementi dovrò trattare). Devo creare un array ragionevolmente più grande del
necessario, ma quando lo riempirò non sarà tutto pieno ma solo i parte. Devo quindi salvare
l'ultimo indice dell'array che contiene un dato valido, memorizzandolo in una nuova variabile
intera.

...
final int ARRAY_LENGHT = 1000;
int[] values = new int [ARRAY_LENGHT];
Scanner in = new Scanner (System.in);
int valuesSize = 0;
boolean done = false;
while (!done)
{ String s = in.next();
if (s.equalsIgnoreCase(“Q”))
done=true;
else
{ values[valuesSize] = Integer.parseInt(s);
valuesSize++;
//valuesSize è l'indice del primo dato non valido
}
}
System.out.println(“inserisci un indice”);
int index = Integer.parseInt(in.next() );
if (index<0 || index >= valuesSize)
System.out.println(“valore errato”);
else
System.out.println(values[index]);
...

Se sbaglio la dimensione dell'array (lo definisco troppo piccolo) posso impedire di inserire
troppi dati

if (s.equalsIgnoreCase(“Q”))
done=true;
else if (valuesSize == values.lenght)
done=true;
else
{ values[valuesSize] = Integer.parseInt(s);
valuesSize++;
//valuesSize è l'indice del primo dato non valido
}

oppure cambiare la dimensione dell'array (ARRAY DINAMICO). Per farlo si utilizza il metodo
statico RESIZE che restituisce un array di lunghezza newLenght e contenenti dati dell'array
oldArray.

if (s.equalsIgnoreCase(“Q”))
done=true;
else
{ if (valuesSize == values.lenght)
values = resize (values, valuesSize *2);
values[valuesSize] = Integer.parseInt(s);
valuesSize++;
}
COSTRUIRE LA CLASSE ArrayAlgs

● Cambiare la dimensione di un array

public static int[] resize (int[] oldArray, int newLength)


{
if (newLength < 0 || oldArray == null)
throw new IllegalArgumentException();
int[] newArray = new int[newLength];
int n = oldArray.length;
if (newLength < n)
n = newLength;
for (int i = 0; i < n; i++)
newArray[i] = oldArray[i];
return newArray;
}

● Generare array di numeri casuali

Il metodo Math.random() restituisce numeri tra 0 e 1, quindi per ottenere numeri interi devo
scrivere

int n = (int) (a + (1+b-a)*Math.random() );

public static int[] randomIntArray (int lenght, int n)


{
int[] a = new int[lenght];
for (int i=0,1<a.lenght,i++)
a[i]= (int) (n*Math.random() );
return a;
}

● Convertire array in stringhe e stamparli

public static String PrintArray(int[] v, int vSize)


{ String s = “[“;
for (int i = 0; i<vSize; i++)
s=s+v[i]+” “;
s=s+”/b]”;
return s;
}
Poi nel main:
System.out.println(ArrayAlgs.printArray(a,aSize) );

● Eliminare un elemento di un array

-SE NON È IMPORTANTE L'ORDINE


1)copio l'ultimo elemento del vettore nella posizione dell'elemento da eliminare
2)ridimensionare l'array (oppure usare la tecnica degli array riempiti solo in parte)

public static void remove (int[] v, int vSize, int index)


{ v[index] = v[vSize-1];
}

Poi nel main:


int[] a = {1,2,3,4,5};
int aSize = a.lenght;
ArrayAlgs.remove(a,aSize,1); //diventa [1,5,3,4]
aSize--;
-SE L'ORDINE DEVE ESSERE MANTENUTO
1)spostare tutti gli elementi dell'array successivi all'elemento da eliminare nella posizione
con indice immediatamente inferiore
2)ridimensionare l'array

public static void removeSorted (int[] v, int vSize, int index)


{ for (int i=index; i<vSize-1; i++)
v[i] = v[i+1]
}

Poi nel main


int[] a = {1,2,3,4,5};
int aSize = a.lenght;
ArrayAlgs.removeSorted(a,aSize,1); //diventa [1,3,4,5]
aSize--;

● Inserire un elemento in un array


1)ridimensionare l'array per fargli spazio
2)spostare tutti gli elementi successivi alla posizione di inserimento nel posto di indice
immediatamente successivo

public static int[] insert(int[] v, int vSize, int index, int val)
{ if(vSize == v.lenght) v= resize (v, 2*v.lenght);
for (int i = vSize; i>index; i--)
v[i] = v[i-1];
v[index] = val;
return v;
}

Poi nel main


int[] a = {1,2,3,4,5};
int aSize = a.lenght;
ArrayAlgs.insert(a,aSize,2,7); //diventa [1,2,7,3,4,5]
aSize++;
● Ricerca di un valore

-RICERCA LINEARE
1)scorrere tutti gli elementi finchè l'elemento cercato viene trovato oppure si raggiunge la
fine del'array (se ci sono due valori uguali trova solo il primo)

public static int linearSearch(int[] v, int vSize, int value)


{
for (int i = 0; i < vSize; i++)
if (v[i] == value)
return i; // trovato valore
return -1; // valore non trovato
}

Poi nel main


int[] a = {1,2,3,4,5};
int aSize = a.length;
int i = ArrayAlgs.linearSearch(a,aSize,4);

● Trovare il valore massimo/minimo


1)inizializzare il valore candidato come primo elemento
2)confrontare il candidato con gli elementi rimanenti
3)aggiornare il valore candidato se viene trovato un valore maggiore/minore

public static int findMax(int[] v, int vSize)


{ int max = v[0];
for (int i = 1; i < vSize; i++)
if (v[i] > max)
max = v[i];
return max;
}

public static int findMin(int[] v, int vSize)


{ int min = v[0];
for (int i = 1; i < vSize; i++)
if (v[i] < min)
min = v[i];
return min;
}

Poi nel main


int[] a = {1,2,3,4,5};
int aSize = a.length;
int max = ArrayAlgs.findMax(a, aSize);

VEDI POI METODI


-selectionSort
-
ARRAY BIDIMENSIONALI
Una struttura con dati organizzati in righe e colonne, si dice matrice o array bidimensionale.
Un elemento all’interno di una matrice è identificato da una coppia (ordinata) di indici:
• un indice di riga
• un indice di colonna

int [n°righe] [n°colonne] nomeArray; dichiarazione di un array bidimensionale di tipo int

powers = new int [4] [5]; assegnazione di riferimento ad array bidimensionale

powers[2][3] = 2; accesso ad un elemento di un array bidimensionale

powers.length; per conoscere il numero di righe

powers[0].length; per conoscere il numero di colonne

(un array bidimensionale è in realtà un array di array e ogni array rappresenta una riga…)

Programma che visualizza una tabella con i valori delle potenze "x alla y", con x e y che
variano indipendentemente tra 1 ed un valore massimo assegnato dall’utente.
I dati relativi a ciascun valore di x compaiono su una riga, con y crescente da sinistra a
destra e x crescente dall’alto in basso.

import java.util.Scanner;
public class TableOfPowers
{ public static void main(String[] args)
{ Scanner in = new Scanner(System.in);
System.out.println("Calcolo dei valori di x alla y");
System.out.println("Valore massimo di x:");
int maxX = in.nextInt();
System.out.println("Valore massimo di y:");
int maxY = in.nextInt();
int maxValue = (int) Math.round(Math.pow(maxX, maxY)); //per stamparlo
int columnWidth =1 + Integer.toString(maxValue).length();
int[][] powers = generatePowers(maxX, maxY);
printPowers(powers, columnWidth);
}
/*Genera un array bidimensionale con i valori delle potenze di x alla y.*/
private static int[][] generatePowers(int x,int y)
{ int[][] powers = new int[x][y];
for (int i = 0; i < x; i++)
for (int j = 0; j < y; j++)
powers[i][j] =(int)Math.round(Math.pow(i + 1, j + 1));
return powers;
}

/*Visualizza un array bidimensionale di numeri interi con colonne di larghezza fissa e valori
allineati a destra.*/
private static void printPowers(int[][] v, int width)
{ for (int i = 0; i < v.length; i++)
{ for (int j = 0; j < v[i].length; j++)
{ String s = Integer.toString(v[i][j]);
while (s.length() < width)
s = " " + s;
System.out.print(s);
}
System.out.println();
}
}
}
Notare l’utilizzo di metodi private per la scomposizione di un problema in sottoproblemi più
semplici. In genere non serve preoccuparsi di pre-condizioni perché il metodo viene invocato
da chi l’ha scritto (es: indice minore di 0,...).

ARGOMENTI SULLA RIGA COMANDI

(String [] args) è un array di stringhe che si chiama args e che viene passato come
parametro al metodo main. Ma chi lo passa al main? L'utente!

Quando si esegue un programma Java, è possibile fornire dei parametri dopo il nome della
classe che contiene il metodo main.
Tali parametri vengono letti dall’interprete Java e trasformati in un array di stringhe che
costituisce il parametro del metodo main.

java CommLineTester 2 33 Hello

java LineNumberer –c HelloWorld.java HelloWorld.txt

Uso tipico degli argomenti sulla riga di comandi:


• Specificare opzioni e nomi di file da leggere/scrivere
• Per convenzione le stringhe che iniziano con un trattino sono considerate opzioni

esempi:
for (int i = 0; i< args.length; i++)
{ String a = args[i];
if (a.startsWith(“-”)) // è un’opzione
{
if (a.equals(“-c”)) useCommentDelimiters = true;
}
else
if (inputFileName == null)
inputFileName = a;
else
if (outputFileName == null)
outputFileName = a;
}
public class CommLineTester
{ public static void main(String[] args)
{ System.out.println("Passati " +args.length+ " parametri");
for (int i=0; i<args.length; i++)
System.out.println(args[i]);
}
}

ARRAY PARALLELI
Si usano diversi array per contenere i dati del problema, ma questi sono
tra loro fortemente correlati. In particolare, elementi aventi lo stesso
indice nei diversi array sono tra loro correlati. Rappresentano diverse
proprietà dello stesso studente.
I tre array devono sempre contenere lo stesso numero di elementi.
Molte operazioni hanno bisogno di usare tutti gli array, che devono quindi
essere passati come parametri (come nel caso di printAverage).

Scriviamo un programma che riceve in ingresso un elenco di dati che rappresentano


• i cognomi di un insieme di studenti
• il voto della prova scritta
• il voto della prova orale
I dati di uno studente vengono inseriti in una riga separati da uno spazio (prima il cognome,
poi il voto scritto, poi il voto orale). I dati sono terminati da una riga vuota.
Il programma chiede all’utente di inserire un comando per identificare l’elaborazione da
svolgere
• Q significa “termina il programma”
• S significa “visualizza la media dei voti di uno studente”
Nel caso S il programma chiede all’utente di inserire il cognome di uno studente.
Stampa il cognome dello studente seguito dalla media dei suoi voti.

public class StudentManager


{ public static void main(String[] args)
{ Scanner in = new Scanner(System.in);
String[] names = new String[10];
double[] wMarks = new double[10];
double[] oMarks = new double[10];
int count = 0; // array riempiti solo in parte
boolean done = false;

while (!done) //inserimento dati studenti


{ String input = in.nextLine();

if (input.length() == 0)
done=true;
else
{ Scanner t = new Scanner(input);
if (count == names.length) //ridimensionamento array
{ names = resizeString(names, count * 2);
wMarks = resizeDouble(wMarks, count * 2);
oMarks = resizeDouble(oMarks, count * 2);
}
names[count] = t.next();
wMarks[count] = Double.parseDouble(t.next());
oMarks[count] = Double.parseDouble(t.next());
count++;
}
}
done = false;
while (!done) //visualizzazione dati inseriti
{ System.out.println("Comando? (Q per uscire, S per vedere)");
String command = in.nextLine();
if (command.equalsIgnoreCase("Q"))
done = true;
else if (command.equalsIgnoreCase("S"))
{ System.out.println("Cognome?");
String name = in.nextLine();
printAverage(names, wMarks, oMarks,name, count); //NOTA:
//non abbiamo gestito l'eccezione lanciata da printAverage
}
else
{ System.out.println("Comando errato");
}
}
}

private static void printAverage(String[] names, double[] wMarks, double[] oMarks,


String name, int count)
{ int i = findName(names, name, count);
if (i == -1)
throw new IllegalArgumentException();
else
{ double avg = (wMarks[i] + oMarks[i]) / 2;
System.out.println(name + " " + avg);
}
}

private static int findName(String[] names, String name, int count)


{ for (int i = 0; i < count; i++)
if (names[i].equals(name))
return i;
return -1;
}

private static String[] resizeString(String[] oldv, int newLength)


{ if (newLength < 0 || oldv == null)
throw new IllegalArgumentException();
String[] newv = new String[newLength];
int count = oldv.length;
if (newLength < count)
count = newLength;
for (int i = 0; i < count; i++)
newv[i] = oldv[i];
return newv;
}

private static double[] resizeDouble(double[] oldv, int newLength)


{ if (newLength < 0 || oldv == null)
throw new IllegalArgumentException();
double[] newv = new double[newLength];
int count = oldv.length;
if (newLength < count) count = newLength;
for (int i = 0; i < count; i++)
newv[i] = oldv[i];
return newv;
}
}

Gli array paralleli sono molto usati in linguaggi di programmazione non OO, ma presentano
numerosi svantaggi che possono essere superati in Java. Le modifiche alle dimensioni di un
array devono essere fatte contemporaneamente a tutti gli altri. I metodi che devono
elaborare gli array devono avere una lunga lista di parametri espliciti. Non è semplice scrivere
metodi che devono ritornare informazioni che comprendono tutti gli array. Ad esempio, nel
caso presentato non è semplice scrivere un metodo che realizzi la fase di input dei dati,
perché tale metodo dovrebbe avere come valore di ritorno i tre array!

Tutte le volte in cui il problema presenta una struttura dati del tipo “array paralleli”, si
consiglia di trasformarla in un array di oggetti.
RICORSIONE
Ricorsione: tecnica di programmazione che sfrutta l'idea di suddividere un problema da
risolvere in sottoproblemi simili a quello originale, ma più semplici.
Un algoritmo ricorsivo per la risoluzione di un dato problema deve essere definito nel modo
seguente:
• prima si definisce come risolvere direttamente dei problemi analoghi a quello di partenza,
ma di dimensione “sufficientemente piccola” (detti casi base);
• poi (passo ricorsivo) si definisce come ottenere la soluzione del problema di partenza
combinando la soluzione di uno o più sottoproblemi di “dimensione inferiore”.
Ma la ricorsione aggiunge complessità computazionale.

Prima regola: Un algoritmo ricorsivo deve fornire la soluzione del problema in almeno un
caso particolare, senza ricorrere ad una chiamata ricorsiva, tale caso si chiama caso base
della ricorsione.

Seconda regola: Un algoritmo ricorsivo deve effettuare la chiamata ricorsiva dopo aver
semplificato il problema. Il concetto di “problema più semplice” varia di volta in volta: in
generale, bisogna avvicinarsi a un caso base.

Queste due regole sono fondamentali per dimostrare che la soluzione ricorsiva di un
problema sia un algoritmo che termini in un numero finito di passi si potrebbe pensare che le
chiamate ricorsive si susseguano una dopo l’altra, all’infinito. Invece se ad ogni invocazione il
problema diventa sempre più semplice e si avvicina al caso base e la soluzione del caso base
non richiede ricorsione.
Se non seguo le due regole, dato che la lista dei metodi “in attesa” si allunga
indefinitamente, l’ambiente runtime esaurisce la memoria disponibile per tenere traccia di
questa lista e il programma termina con un errore.

primo esempio: calcolo del fattoriale

n=0 è un CASO BASE. Deve esserci sempre almeno un caso base di cui conosco la soluzione.

public static int factorial(int n) Realizzando direttamente la definizione,


{ if (n < 0) sarebbe stato più naturale scrivere
throw new IllegalArgumentException();
if (n == 0) public static int factorial(int n)
return 1; { if (n < 0)
int p = 1; throw new IllegalArgumentException();
for (int i = 2; i <= n; i++) if (n == 0)
p = p * i; return 1;
return p; return n * factorial(n - 1);
} }

Invocare un metodo mentre si esegue lo stesso metodo è un paradigma di programmazione


che si chiama ricorsione
• Un metodo che ne faccia uso è un metodo ricorsivo
• La ricorsione è uno strumento potente per realizzare alcuni algoritmi, ma è anche fonte di
molti errori di difficile diagnosi

COME FUNZIONA L'INVOCAZIONE RICORSIVA DI UN METODO?


In una invocazione ricorsiva, la JVM esegue le
stesse azioni eseguite nell'invocazione di un
metodo qualsiasi:
• sospende l’esecuzione del metodo invocante
• esegue il metodo invocato fino alla sua
terminazione
• riprende l’esecuzione del metodo invocante
dal punto in cui era stata sospesa
RICORSIONE INFINITA

public class InfiniteRecursion


{ public static void main(String[] args)
{ main(args);
}
}

Il programma terminerà con la segnalazione dell’eccezione StackOverflowError


Ricordiamo che il runtime stack (“pila”) è la struttura di memoria all’interno dell’interprete
Java che gestisce le invocazioni di metodi in attesa.

RICORSIONE IN CODA
Esistono diversi tipi di ricorsione. Il modo visto fino ad ora si chiama ricorsione in coda (tail
recursion)
Il metodo ricorsivo esegue una sola invocazione ricorsiva e tale invocazione è l’ultima azione
del metodo

public void tail(...)


{ ...
tail(...);
}

ELIMINARE LA RICORSIONE IN CODA


La ricorsione in coda può sempre essere agevolmente eliminata, trasformando il metodo
ricorsivo in un metodo che usa un ciclo

public int factorial(int n) public int factorial(int n)


{ if (n == 0) { int f = 1;
return 1; while (n > 0)
return n * factorial(n - 1); { f = f * n;
} n--;
}
return f;
}

La ricorsione in coda rende il codice più leggibile. In ogni caso, la ricorsione in coda è meno
efficiente del ciclo equivalente, perché il sistema deve gestire le invocazioni sospese.
Se la ricorsione non è in coda, non è facile eliminarla (cioè scrivere codice non ricorsivo
equivalente), però si può dimostrare che ciò è sempre possibile perché il processore esegue
istruzioni in sequenza e non può tenere istruzioni in attesa. In Java l’interprete si fa carico di
eliminare la ricorsione (usando il runtime stack). In un linguaggio compilato il compilatore
trasforma il codice ricorsivo in codice macchina non ricorsivo.
RICORSIONE MULTIPLA
Si parla di ricorsione multipla quando un metodo invoca se stesso più volte durante la propria
esecuzione la ricorsione multipla è più difficile da eliminare, ma è sempre possibile

secondo esempio: calcolo dei numeri di Fibonacci

public class FibTester


{ private static int invcount = 0; // variabile statica
public static void main(String[] args)
{ int n = 0;
if (args.length != 1)
{ System.out.println("Uso: $java FibTester <numero>");
System.exit(1); }
try
{ n = Integer.parseInt(args[0]); }
catch(NumberFormatException e)
{ System.out.println("Inserire un intero!");
System.exit(1); }

System.out.println("*** Collaudo iterativeFib ***");


for (int i = 1; i <= n; i++)
{ long f = iterativeFib(i);
System.out.println("iterativeFib(" +i+ ") = " + f);}

System.out.println("\n*** Collaudo recursiveFib ***");


for (int i = 1; i <= n; i++)
{ invcount = 0;
long f = recursiveFib(i);
System.out.println("recursiveFib(" +i+ ") = " + f);
System.out.println(invcount+" invocazioni"); }
}

public static long recursiveFib(int n) //continua


{ if (n < 1) throw new IllegalArgumentException();
System.out.println("Inizio recursiveFib(" + n +")");
invcount++;
long f;
if (n < 3) f = 1;
else f = recursiveFib(n-1) + recursiveFib(n-2);
System.out.println("Uscita recursiveFib(" + n + ")");
return f;
}

public static long iterativeFib(int n) //meno complesso ma meno leggibile


{ if (n < 1) throw new IllegalArgumentException();
long f = 1;
if (n >= 3)
{ long fib1 = 1;
long fib2 = 1;
for (int i = 3; i<=n; i++)
{ f = fib1 + fib2;
fib2 = fib1;
fib1 = f; }
}
return f;
}
}
La ricorsione multipla va usata con molta attenzione, perché può portare a programmi molto inefficienti.
Eseguendo il calcolo dei numeri di Fibonacci di ordine crescente si nota che il tempo di elaborazione cresce molto
rapidamente.
PERMUTAZIONI
Esaminiamo un problema per il quale è facile trovare una soluzione ricorsiva mentre è molto
difficile trovarne una iterativa: determinare le permutazioni di una stringa.
(ovvero tutte le stringhe ottenibili utilizzando i caratteri della stringa di partenza in tutte le
possibili disposizioni). Se non ci sono caratteri ripetuti dalla matematica combinatoria
sappiamo che il numero di permutazioni di N simboli è N!

Esiste un algoritmo ricorsivo per trovare le permutazioni di una stringa di lunghezza N:


• Se N=1, l’unica permutazione è la stringa stessa. Questo è il caso base
• Altrimenti generiamo tutte le permutazioni che iniziano con il primo carattere, generiamo
tutte le permutazioni che iniziano con il secondo carattere … e così via. Otteniamo tutte le
possibili permutazioni.

Ma come si ottengono le permutazioni che iniziano con il generico i-esimo carattere?


• Concatenando l’i-esimo carattere con le permutazioni della stringa composta dai rimanenti
N-1 caratteri. Cioè risolvendo lo stesso problema per un dato di dimensione più piccola (N-1
invece che N). Abbiamo ottenuto il passo di ricorsione.
• Avevamo già il caso base (se N = 1 c’è 1 permutazione)
• Il passo di ricorsione semplifica il problema e si
avvicina al caso base. Questo algoritmo ci dice che il numero di permutazioni P(N) di una
stringa lunga N è P(N)=N*P(N-1). Cioè, come avevamo anticipato, P(N)=N!

import java.util.Scanner;
public class PermutationTester
{ public static void main(String[] args)
{ Scanner in = new Scanner(System.in);
System.out.println("Inserire stringa");
String aString = in.nextLine();
String[] permutations = getPermutations(aString);
for (int i = 0; i < permutations.length; i++)
System.out.println(permutations[i]);
}

public static String[] getPermutations(String word)


{ if (word == null || word.length() == 0) //precondizioni (se le inverto nonfunziona
perche non posso calcolare la lunghezza di un stringa null (valutazione pigra)
return new String[0]; // oppure return null
if (word.length() == 1) // caso base
return new String[] {word};
int nperm = 1; // il n. di permutazioni da generare...
for (int i = 2; i <= word.length(); i++)
nperm *= i; // ... e` il fattoriale di word.length()
String[] perm = new String[nperm];
for (int i = 0; i < word.length(); i++)
{ String subword = word.substring(0,i)+word.substring(i+1);
// calcolo una sottostringa di n-1 caratteri
// passo ricorsivo: permutazioni della sottosequenza
String[] subperm = getPermutations(subword);
for (int j = 0; j < subperm.length; j++)
perm[i*subperm.length+j] = word.substring(i,i+1) + subperm[j];
//concateno le sottostringhe con l'i-esimo carattere che avevo tolto
}
return perm;
}
}

Per completare le permutazioni:


• concateniamo l’i-esimo carattere con tutte le posizioni permutazioni della corrispondente
sottosequenza
• Aggiungiamo nell’array le permutazioni ottenute
word.substring(0,i) + word.substring(i+1);
Per ottenere subword stiamo sfruttando le proprietà del metodo substring
• Quando i vale 0, substring restituisce una stringa vuota, che è proprio ciò che vogliamo

word.substring(0,i) + word.substring(i+1);

• Quando i vale word.length( )-1 (suo valore massimo), allora i+1 vale word.length( )
In questo caso particolare, substring non lancia eccezione, ma restituisce una stringa vuota,
che è proprio ciò che vogliamo.

word.substring(0,i) + word.substring(i+1);

[vedi API di substring]

perm[i*subperm.length+j] = ...;
Analizziamo meglio questa espressione dell’indice.
Globalmente, tale indice deve andare da 0 a (word.length())! (escluso).
Verifichiamo innanzitutto i limiti
• Se i = 0 e j = 0, l’indice vale 0
• Per un generico i e j = subperm.length-1 l’indice vale
i*subperm.length +subperm.length-1 = (i+1)*subperm.length -1
• Se i=word.length()-1, j =subperm.length-1, l’indice vale
word.length()*subperm.length - 1

Alla prima iterazione di i, l’indice varia tra 0 e subperm.length-1 (perché i vale 0).
Alla seconda iterazione di i, l’indice varia tra 1*subperm.length+0 = subperm.length e
1*subperm.length+subperm.length-1 =2*subperm.length-1

Si osserva quindi che gli indici vengono generati consecutivamente, senza nessun valore
mancante e senza nessun valore ripetuto.

TORRI DI HANOI

Si può spostare un disco per volta, un disco non può


poggiare su uno più piccolo.

Dobbiamo trovare
• un caso base
• Un passo di ricorsione che semplifica il problema

Il CASO BASE è semplice:


Se la torre è composta da un solo disco la soluzione è composta da una sola mossa, che
sposta il disco stesso dal piolo di origine al piolo di destinazione.

Cerchiamo il PASSO RICORSIVO:


Ad un certo punto del gioco il disco più grande verrà spostato dall’origine alla destinazione.
In quel momento tutti gli altri dischi devono essere impilati sul piolo rimanente.
Quindi il problema con n-1 dischi è stato risolto.
Dopo avere spostato il disco più grande dall’origine alla destinazione, dobbiamo spostare tutti
gli altri dischi dal piolo rimanente alla destinazione.
Bisogna nuovamente risolvere il problema con n-1 dischi.
public class HanoiSolver
{ public static void main(String[] args)
{
if (args.length != 3)
{ System.out.println("uso: $java HanoiSolver from target disks");
System.out.println("con from e target tra 1 e 3, e disks > 0");
return;
}
try
{ solveHanoi(Integer.parseInt(args[0]), Integer.parseInt(args[1]),
Integer.parseInt(args[2]));
}
catch(NumberFormatException e)
{ System.out.println("Non hai inserito parametri di tipo int");
}
catch(IllegalArgumentException e)
{ System.out.println("Valore errato per from o target o disks");
}

public static void solveHanoi(int from, int target, int disks)


{
if (from==target || from<1 || from>3 || target<1 || target>3 || disks<0)
throw new IllegalArgumentException();
if (disks > 0)
{ int other = 6 - from – target; //3° piolo "di appoggio"
solveHanoi(from, other, disks-1);
System.out.println("Sposta un disco dal piolo " + from +" al piolo " + target);
solveHanoi(other, target, disks-1);
}
}
}
ANALISI DELLE PRESTAZIONI

RILEVAZIONE DELLE PRESTAZIONI


Le prestazioni di un algoritmo vengono valutate in funzione della dimensione dei dati trattati
Per valutare l’efficienza di un algoritmo si misura il tempo in cui viene eseguito su insiemi di
dati di dimensioni via via maggiori.
Il tempo non va misurato con un cronometro, perché parte del tempo reale di esecuzione non
dipende dall’algoritmo:
• caricamento della JVM • caricamento delle classi del programma
• lettura dei dati dallo standard input • visualizzazione dei risultati

Il tempo di esecuzione di un algoritmo va misurato all’interno del programma. Si usa il


metodo statico System.currentTimeMillis()

che, ad ogni invocazione, restituisce un numero di tipo long che rappresenta il numero di
millisecondi trascorsi da un evento di riferimento.
Ciò che interessa è la differenza tra due valori! Si invoca System.currentTimeMillis( )
immediatamente prima e dopo l’esecuzione dell’algoritmo (escludendo le operazioni di input/
output dei dati).

public static void main(String[] args)


{ Scanner in = new Scanner(System.in);
System.out.print("Dimensione dell'array? ");
int n = in.nextInt();
int[] a = ArrayAlgs.randomIntArray(n, 100); // costruisce un array casuale
int aSize = a.length;
long time = System.currentTimeMillis();
ArrayAlgs.selectionSort(a, aSize);
time = System.currentTimeMillis() -time;
System.out.println("Tempo trascorso: " + time + " ms");
}

Se raddoppio la lunghezza del vettore non raddoppia il tempo,


ma quadruplica!!
L’andamento del tempo di esecuzione non è lineare. Quindi il
tempo di esecuzione ha andamento quadratico (parabola tipo
T(n) = a n2).

Misuro la performance del metodo che implementa l'algoritmo,


non l'algoritmo.

ANALISI TEORICA DELLE PRESTAZIONI


Modello di costo: il tempo d’esecuzione di un algoritmo dipende in generale dai seguenti
fattori:
1.Il numero di OPERAZIONI PRIMITIVE (o passi base) eseguite dall'algoritmo (ad es.,
istruzioni macchina del processore)
2.Che a loro volta dipendono dalla DIMENSIONE DEI DATI da elaborare (ad esempio, la
lunghezza dell’array da ordinare)
3.Il VALORE DEI DATI da elaborare (ad esempio, un array già ordinato, ordinato al contrario,
con valori casuali, ...)

Dato un algoritmo, vogliamo stimare una funzione T(n) che ne descrive il tempo di
esecuzione T unicamente in funzione della dimensione n dei suoi dati tramite analisi teorica,
senza esperimenti numerici. Anzi, senza realizzare e compilare un algoritmo!
Cosa si intende per DIMENSIONE DEI DATI di un algoritmo?
A seconda del problema, la dimensione dell'input assume significati diversi
• La grandezza di un numero (ad esempio in problemi di calcolo)
• Il numero di elementi su cui lavorare (ad esempio in problemi di ordinamento)
• Il numero di bit che compongono un numero

Indipendentemente dal tipo di dati, indichiamo sempre con n la dimensione dell'input.

Cosa si intende per OPERAZIONE PRIMITIVA (passo base)?


È una operazione che ha tempo di esecuzione (circa) costante, indipendente da valori e tipi
dei dati. Ad es.:
• Una assegnazione di valore ad una variabile
• Una operazione aritmetica o logica tra variabili e/o costanti numeriche e booleane
• Un accesso in lettura/scrittura ad un elemento di un array

Invece un enunciato contenente una invocazione di un metodo non è un'operazione primitiva


Nel caso di selectionSort, il tempo di esecuzione si stima contando il numero di accessi in
lettura/scrittura ad un elemento dell’array, in funzione della lunghezza n dell'array
(dimensione dei dati del problema).

Come si tiene conto del VALORE DEI DATI?


Se l'algoritmo contiene cicli e decisioni, il numero di operazioni primitive dipende anche dal
valore dei dati. Ma noi vogliamo T(n) come funzione solo di n.
Di solito si stima per eccesso il tempo di esecuzione T(n), ovvero di ottenere una stima “di
caso peggiore”.
A seconda del problema, la definizione di “CASO PEGGIORE” assume significati diversi
• per un algoritmo di ordinamento il caso peggiore è quello in cui l’array in input è ordinato
alla rovescia.
Si possono anche fare stime di
• caso migliore (ad es. array in input già ordinato)
• caso medio (con ipotesi statistiche, ad es. array in input contenente numeri casuali)

ANDAMENTO ASINTOTICO DELLE PRESTAZIONI

T(n) = 1/2*n2 + 9/2*n – 5


Facciamo un’ulteriore semplificazione, tenendo presente che ci interessano le prestazioni per
valori elevati di n (andamento asintotico)
• Ad esempio, se n vale 1000, 1/2*n2 vale 500000, 9/2*n-5 vale 4495 (circa 1% del totale)
quindi diciamo che l'andamento asintotico dell’algoritmo in funzione di n è

T(n) ~ 1/2*n2

Facciamo un’ulteriore semplificazione


• ci interessa soltanto valutare cosa succede al tempo d’esecuzione T(n) se n, ad esempio,
raddoppia.
• Nel caso in esame il tempo di esecuzione quadruplica
T(n) = 1/2*n2
T(2n) = 1/2*(2n)2 = 1/2*4*n2 = 4*T(n)

• Si osserva che T(2n) = 4*T(n)


Questo è vero in generale nel caso in cui T(n) = cn2 indipendentemente dal fatto che sia
presente un fattore moltiplicativo 1/2, o qualsiasi altro fattore moltiplicativo c

Si dice quindi che per ordinare un array con l’algoritmo di selezione si effettua un numero di
accessi che è dell’ordine di n2
• Per esprimere sinteticamente questo concetto si usa la notazione O-grande e si dice che il
numero degli accessi è O(n2)
• Dopo aver ottenuto una formula che esprime l’andamento temporale dell’algoritmo, si
ottiene la notazione “O-grande” considerando soltanto il termine che si incrementa più
rapidamente all’aumentare di n, ignorando coefficienti costanti.
ORDINI DI COMPLESSITÀ
Un algoritmo può essere
classificato in funzione delle
proprie prestazioni.
• Un algoritmo è considerato
efficiente se il suo tempo di
esecuzione (caso peggiore) è
al più polinomiale.
• Un algoritmo è considerato
inefficiente se il suo tempo di
esecuzione (caso peggiore) è
almeno esponenziale.

• Un problema algoritmico
può essere classificato in
funzione della propria
complessità (ovvero le
prestazioni del più veloce algoritmo che lo risolve).
• Un problema è considerato trattabile se la sua complessità è al più polinomiale
• Un problema è considerato non trattabile se la sua complessità è almeno esponenziale

CICLI ANNIDATI: ANALISI DELLE PRESTAZIONI


Riesaminiamo la struttura di SelectionSort
È realizzato tramite due cicli annidati di questo tipo

for (int i = 0; i < n; i++)


//... operazioni primitive
for (int j = i; j < n; j++)
//... operazioni primitive

Per stimarne il tempo di esecuzione dobbiamo stimare il numero di operazioni primitive


eseguite nel ciclo interno.
Per i = 0 vengono eseguite n volte. Per i = 1 vengono eseguite n-1 volte. ...
• Il numero totale è quindi

• Due cicli annidati del tipo appena esaminato hanno sempre prestazioni O(n2)

NOTAZIONE “O-GRANDE”
Si dice quindi che per ordinare un array con l’algoritmo di selezione si effettua un numero di
accessi che è dell’ordine di n2
Per esprimere sinteticamente questo concetto si usa la notazione O-grande e si dice che il
numero degli accessi è O(n2)
Dopo aver ottenuto una formula che esprime l’andamento temporale dell’algoritmo, si ottiene
la notazione “O-grande” considerando soltanto il termine che si incrementa più rapidamente
all’aumentare di n, ignorando coefficienti costanti.
● f(n)=O(g(n)) significa: f non cresce più velocemente di g
● f(n)=Ω(g(n)) significa: f non cresce più lentamente di g
● f(n)=Θ(g(n)) significa: f cresce con la stessa velocità di g

RICORSIONE E STIMA DELLE PRESTAZIONI


Quando un algoritmo contiene almeno una invocazione ricorsiva, il suo tempo di esecuzione
T(n) può sempre essere espresso da una equazione di ricorrenza, ovvero un' equazione che
descrive una funzione in termini del valore che essa assume su valori di input più piccoli
Uno dei metodi (non l'unico) per risolvere queste equazioni è il metodo per sostituzione
(di cui vedremo un esempio analizzando mergeSort)
• Il Master Theorem fornisce una stima dell'ordine di T(n) in funzione di a, b, f(n)
ALGORITMI DI ORDINAMENTO

ORDINAMENTO PER SELEZIONE


Analizzeremo prima gli algoritmi per ordinare un insieme di numeri (interi) memorizzato in un
array. Vedremo poi come si estendono semplicemente al caso in cui si debbano elaborare
oggetti anziché numeri.
Prendiamo in esame un array a da ordinare in senso crescente:

Per prima cosa, bisogna trovare l’elemento dell’array contenente il valore minimo, come
sappiamo già fare.
In questo caso è il numero 5 in posizione a[3]
• Essendo l’elemento minore, la sua posizione corretta nell’array ordinato è a[0]
• in a[0] è memorizzato il numero 11, da spostare
• non sappiamo quale sia la posizione corretta di 11
• lo spostiamo temporaneamente in a[3]
• quindi, scambiamo a[3] con a[0]

La parte sinistra dell’array è già ordinata e non sarà più considerata

Dobbiamo ancora ordinare la parte destra.


• Ordiniamo la parte destra con lo stesso algoritmo
• cerchiamo l’elemento contenente il valore minimo, che è il numero 9 in posizione a[1]
• dato che è già nella prima posizione della parte da ordinare (la posizione a[1]), non c’è
bisogno di fare scambi

MA QUANTI ACCESSI IN LETTURA E QUANTI IN SCRITTURA COMPIE QUESTO ALGORITMO?


IL METODO SELECTIONSORT

public class ArrayAlgs{...

public static void selectionSort(int[] v, int vSize)


{ for (int i = 0; i < vSize - 1; i++) //arriva fino a vSize-1, quando arrivo all'ultimo
. . elemento l'array è gia ordinato (vSize indica il primo indice VUOTO)
{ int minPos = findMinPos(v, i, vSize-1);
if (minPos != i) swap(v, minPos, i);
}
} //abbiamo usato due metodi ausiliari, swap e findMinPos

private static void swap(int[] v, int i, int j) //scambio i due valori


{ int temp = v[i];
v[i] = v[j];
v[j] = temp;
}

private static int findMinPos(int[] v, int from, int to)


{ int pos = from;
int min = v[from];
for (int i = from + 1; i <= to; i++)
if (v[i] < min) //se ho due numeri uguali funziona perchè dopo avere trovato il primo
numero dei due uguali il secondo numero sarà il minore della parte restante dell'array
{ pos = i;
min = v[i]; }
return pos; //contiene l'indice dell'elemento minimo da ordinare
}
...
}

ESAMINIAMO LE PRESTAZIONI DI SELECTIONSORT


Potremmo anche esaminare lo pseudo-codice
1) Conteggio degli accessi all’array nella prima iterazione del ciclo esterno (ovvero per i=0)
• Per trovare l’elemento minore si fanno n accessi
• Per scambiare due elementi si fanno quattro accessi
• Caso peggiore: ipotizziamo che serva sempre lo scambio
• In totale si fanno quindi (n+4) accessi
2) Ora l'algoritmo deve ordinare la parte rimanente, cioè un array di (n-1) elementi
• serviranno quindi ((n-1) + 4) accessi
3) E così fino al passo con (n-(n-2))=2 elementi, incluso

Il conteggio totale degli accessi in lettura\scrittura è quindi:

Si ottiene quindi una equazione di secondo grado in n, che giustifica l’andamento parabolico
dei tempi rilevati sperimentalmente.
ORDINAMENTO PER FUSIONE (MargeSort)

Ha prestazioni migliori rispetto all'ordinamento per selezione.

Supponiamo che le due parti siano già ordinate. Allora è facile costruire il vettore ordinato,
prendendo sempre il primo elemento da uno dei due vettori, scegliendo il più piccolo.

Ovviamente, nel caso generale le due parti del vettore non saranno ordinate
Possiamo però ordinare ciascuna parte ripetendo il processo
• dividiamo il vettore in due parti (circa) uguali
• ordiniamo ciascuna delle due parti, separatamente
• uniamo le due parti ora ordinate, nel modo visto (questa ultima fase si chiama fusione)
Le due parti sono sicuramente ordinate quando contengono un solo elemento

Si delinea quindi un algoritmo ricorsivo per ordinare un array, chiamato MergeSort


• Caso base: se l’array contiene meno di due elementi, è già ordinato

Altrimenti
• si divide l’array in due parti (circa) uguali
• si ordina la prima parte usando MergeSort • si ordina la seconda parte usando MergeSort
• si fondono le due parti ordinate usando l’algoritmo di fusione (merge)

IL METODO MERGESORT

public static void mergeSort(int[] v, int vSize)


{ if (vSize < 2) return; // caso base
int mid = vSize / 2; //dividiamo circa a meta’
int[] left = new int[mid];
int[] right = new int[vSize - mid];
System.arraycopy(v, 0, left, 0, mid); //copio da v partendo da 0 a left partendo da 0 i
primi mid elementi (da 0 a mid-1)
System.arraycopy(v, mid, right, 0, vSize-mid);
mergeSort(left, mid); // passi ricorsivi: ricorsione multipla (doppia)
mergeSort(right, vSize-mid);
merge(v, left, right); // fusione (metodo ausiliario)
}
private static void merge(int[] v, int[] v1, int[] v2) //v1 sarebbe left, v2 right
{ int i = 0, i1 = 0, i2 = 0;
while (i1 < v1.length && i2 < v2.length)
if (v1[i1] < v2[i2]) //il primo elemento dell'array finale è v1[i1]
v[i++] = v1[i1++]; // prima si usa i, poi lo incremento (sennò sarebbe v[++i])
else //poi devrò capire se i1 o i2 è incrementato fino alla fine dell'array
v[i++] = v2[i2++];
while (i1 < v1.length) // terminato il while almeno uno dei due array è terminato
v[i++] = v1[i1++]; // quindi tutti gli elementi dell'altro array già ordinati vanno
while (i2 < v2.length) // inseriti nell'array finale
v[i++] = v2[i2++]; }
PRESTAZIONI DI MERGESORT

Rilevazioni sperimentali delle prestazioni mostrano


che l’ordinamento con MergeSort è molto più
efficiente di quello con selectionSort

Cerchiamo di fare una valutazione teorica delle prestazioni.


Chiamiamo T(n) il numero di accessi richiesti per ordinare un array di n elementi.
Analizziamo i tempi di esecuzioni delle tre fasi
• Creazione di due sottoarray
• Ordinamento dei due sottoarray (invocazioni ricorsive)
• Fusione dei due sottoarray

La creazione dei due sottoarray richiede 2n accessi perchè tutti gli n elementi devono essere
letti e scritti.
Le invocazioni ricorsive richiedono T(n/2) ciascuna (per definizione T(n) è il tempo di
esecuzione di mergeSort su un array di dimensione n).
La fusione richiede 2n accessi ai sottoarray ordinati (ogni elemento da scrivere nell'array
finale richiede la lettura di due elementi, uno da ciascuno dei due array da fondere) più n
accessi in scrittura nell’array finale.

Sommando tutti i contributi:

(l'algoritmo è su due metà)

Questa è (definizione) una equazione di ricorrenza


Soluzione: per trovare un'espressione esplicita di T(n) in funzione di n si procede per
sostituzioni successive, fino ad arrivare al caso base

T(n/2)=2T(n/4)+5(n/2), T(n/4)=2T(n/8)+5(n/4), ecc.

Inoltre T(1)=1 (è il caso base, ordinamento con n=1), quindi:


T(n) = 2( 2T(n/4) + 5(n/2) ) + 5n = 4T(n/4) + 2*5n = …(dopo k sostituzioni) ...=
= 2kT(n/2k) + k*5n

Dal termine T(n/2k) si vede che il caso base è raggiunto dopo k = log2n, sostituzioni, ovvero
quando n/2k= 1

Per trovare la notazione “O-grande” osserviamo che


• il termine 5n*log2n cresce più rapidamente di n
• il fattore 5 è ininfluente, come ogni costante moltiplicativa
• Nelle notazioni “O-grande” non si indica la base dei logaritmi, perché loga si può trasformare
in logb con un fattore moltiplicativo, che va ignorato
• In conclusione, l'algoritmo MergeSort ha tempi di esecuzione
ha quindi tempi di esecuzione migliori rispetto ad O(n2)
ORDINAMENTO PER INSERIMENTO
L’algoritmo di ordinamento per inserimento inizia osservando che il sottoarray di lunghezza
unitaria costituito dalla prima cella dell’array è ordinato
(essendo di lunghezza unitaria)
Estende verso destra la parte ordinata, includendo nel sottoarray ordinato il primo elemento
alla sua destra. Per farlo, il nuovo elemento viene spostato verso sinistra
finché non si trova nella sua posizione corretta, spostando
verso destra gli elementi intermedi.

IL METODO INSERTIONSORT

public static void insertionSort(int[] v,int vSize) //vsize perchè array riempiti in parte
{ // il ciclo inizia da 1 perché il primo elemento non richiede attenzione
for (int i = 1; i < vSize; i++)
{
int temp = v[i]; //nuovo elemento da inserire
// j va definita fuori dal ciclo perche`il suo valore finale viene usato in seguito
int j;
//sposta di uno verso destra tutti gli el. a sx di temp e > di temp partendo da dx
for (j = i; j > 0 && temp < v[j-1]; j--) //j diminuisce dall'ultimo elemnto ordinato
v[j] = v[j-1];
v[j] = temp; // inserisci temp su v[j] vuoto perchè ho spostato i valori a dx
}
}

PRESTAZIONI DI INSERTIONSORT
Ordiniamo con inserimento un array di n elementi
Il ciclo esterno esegue n-1 iterazioni, ad ogni iterazione vengono eseguiti
• 2 accessi (uno prima del ciclo interno ed uno dopo)
• il ciclo interno
Il ciclo interno esegue 3 accessi per ogni sua iterazione
• ma quante iterazioni esegue? dipende da come sono ordinati i dati!

•Caso peggiore: i dati sono ordinati a rovescio


Ciascun nuovo elemento inserito richiede lo spostamento di tutti gli elementi alla sua sinistra,
perché deve essere inserito in posizione 0

T(n) = (2 + 3*1) + (2 + 3*2) + (2 + 3*3) + ... + (2 + 3*(n-1))


= 2(n-1) + 3[1+2+...+(n-1)]
= 2(n-1) + 3n(n-1)/2
= O(n2)

Osservazione: la struttura di insertionSort è quella dei due cicli annidati esaminati in


precedenza (analoga a quella di selectionSort).
• Caso migliore: i dati sono già ordinati
Il ciclo più interno non esegue mai iterazioni, richiede un solo accesso per la prima verifica
Il ciclo esterno esegue n-1 iterazioni, ad ogni iterazione vengono eseguiti
• 2 accessi (uno prima del ciclo interno ed uno dopo)
• 1 accesso (per verificare la condizione di terminazione del ciclo interno)

T(n) = 3*(n-1) = O(n)

• Caso medio: i dati sono in ordine casuale


Ciascun nuovo elemento inserito richiede in media lo spostamento di metà degli elementi alla
sua sinistra

T(n) = (2 + 3*1/2) + (2 + 3*2/2) + (2 + 3*3/2) + ... + (2 + 3*(n-1)/2)


= 2(n-1) + 3[1+2+...+(n-1)]/2
= 2(n-1) + 3[n(n-1)/2]/2
= O(n2)

CONFRONTO TRA ORDINAMENTI

Se si sa che l’array è “quasi” ordinato, è meglio usare l’ordinamento per inserimento

• Esempio notevole: un array che viene mantenuto ordinato per effettuare ricerche,
inserendo ogni tanto un nuovo elemento e poi riordinando
ALGORITMI DI RICERCA

RICERCA LINEARE
algoritmo da utilizzare per individuare la posizione di un elemento che abbia un particolare
valore all’interno di un array i cui elementi non siano ordinati.
Dato che bisogna esaminare tutti gli elementi, si parla di ricerca sequenziale o lineare

METODO LINEARSEARCH

public static int linearSearch(int[] v, int vSize, int value)


{
for (int i = 0; i < vSize; i++)
if (v[i] == value) return i; // trovato valore
return -1; // valore non trovato
}

PRESTAZIONI RICERCA LINEARE


• Stima di caso peggiore
Se il valore cercato non è presente nell’array, sono sempre necessari n accessi
Se il valore cercato è presente nell’array, il numero di accessi necessari per trovarlo dipende
dalla sua posizione (nel caso peggiore la ricerca inizia dal primo indice dell'array, e il valore
cercato si trova nell’ultima posizione: sono necessari n accessi)
In ogni caso, quindi, le prestazioni dell’algoritmo sono

• Stima di caso medio


Se il valore cercato è presente nell'array, in media sono necessari n/2 accessi, quindi ancora

RICERCA BINARIA
Il problema di individuare la posizione di un elemento all’interno di un array può essere
affrontato in modo più efficiente se l’array è ordinato
• Esempio: Ricerca dell’elemento 12 in questo array

Confrontiamo 12 con l’elemento che si trova (circa) al centro dell’array, a[2], che è 11.
L’elemento che cerchiamo è maggiore di 11.
Se è presente nell’array, allora sarà a destra di 11.

A questo punto dobbiamo cercare l’elemento 12 nel solo sotto-array che si trova a destra di
a[2]. Usiamo lo stesso algoritmo, confrontando 12 con l’elemento che si trova al centro, a[4],
che è 17. L’elemento che cerchiamo è minore di 17, se è presente nell’array, allora sarà a
sinistra di 17.

A questo punto dobbiamo cercare l’elemento 12 nel sotto-array composto dal solo elemento
a[3]. Usiamo lo stesso algoritmo, confrontando 12 con l’elemento che si trova al centro, a[3],
che è 12. L’elemento che cerchiamo è uguale a 12 quindi l’elemento che cerchiamo è
presente nell’array e si trova in posizione 3.

Se il confronto tra l’elemento da cercare e l’elemento a[3] avesse dato esito negativo
avremmo cercato nel sotto-array vuoto a sinistra o a destra concludendo che l’elemento
cercato non è presente nell’array

Questo algoritmo si chiama ricerca binaria perché ad ogni passo si divide l’array in due parti.
Può essere utilizzato soltanto se l’array è ordinato
METODO BINARYSEARCH

public static int binarySearch(int[] v, int vSize, int value)


{
return binSearch(v, 0, vSize-1, value);
}

private static int binSearch(int[] v, int from, int to, int value) // to è il primo indice da cui
voglio cercare, from l'ultimo
{
if (from > to) return -1;// caso base: el. non trovato (sottoarray vuoto)
int mid = (from + to) / 2; // mid e` circa in mezzo
int middle = v[mid]; //prendo il valore contenuto nella casella in mezzo all'array
if (middle == value)
return mid; // caso base: elemento trovato
else if (middle < value) //cerca a destra
return binSearch(v, mid + 1, to, value); //ricorsione
else // cerca a sinistra
return binSearch(v, from, mid - 1, value); //ricorsione
} //ATTENZIONE: e` un algoritmo con ricorsione SEMPLICE

PRESTAZIONI DI BINARYSEARCH

Valutiamo le prestazioni dell’algoritmo di ricerca binaria in un array ordinato


• osserviamo che l’algoritmo è ricorsivo
• Per cercare in un array di dimensione n bisogna effettuare un confronto (con l’elemento
centrale)
• effettuare una ricerca in un array di dimensione n/2

Quindi: T(n) = T(n/2) + 1

analogamente all’analisi delle prestazioni di mergeSort, l’equazione per T(n) che abbiamo
trovato è una equazione di ricorrenza.

Come nel caso di mergeSort, un'espressione esplicita per T(n) si trova per sostituzioni
successive, fino ad arrivare al caso base T(1)=1

T(n) = (T(n/4) + 1 ) + 1 = T(n/4) + 2 == … (dopo k sostituzioni) ... = T(n/2k) + k

Dal termine T(n/2k) si vede che il caso base T(1)=1 è raggiunto per k = log2n, ovvero per
2k = n

T(n) = T(1) + log2n = 1 + log2n

Quindi le prestazioni sono T(n) = O(log n)


E sono migliori di quelle della ricerca lineare T(n) = T(n/2) + 1

[APPROFONDIMENTO: ANALISI DELLE PRESTAZIONI DI RECURSIVEFIB E COMPLESSITA'


TORRE DI HANOI] hanno complessità esponenziale
ORDINAMENTO DI OGGETTI

Si sono visti algoritmi di ordinamento su array di numeri. Ma spesso bisogna ordinare dati più
complessi, ad esempio stringhe, ma anche oggetti di altro tipo.
Gli algoritmi di ordinamento che abbiamo esaminato effettuano confronti tra numeri.
Si possono usare gli stessi algoritmi per ordinare oggetti, a patto che questi siano tra loro
confrontabili.
C’è però una differenza: confrontare numeri ha un significato matematico ben definito,
confrontare oggetti ha un significato che dipende dal tipo di oggetto, e a volte può non avere
significato alcuno.
Quindi la classe che definisce l’oggetto deve anche definire il significato del confronto.
Consideriamo la classe String: essa definisce il metodo compareTo che attribuisce un
significato ben preciso all’ordinamento tra stringhe (l’ordinamento lessicografico)
Possiamo quindi riscrivere, ad esempio, il metodo selectionSort per ordinare stringhe invece
di ordinare numeri, senza cambiare l’algoritmo.

METODO SELECTIONSORT PER STRINGHE

public static void selectionSort(String[] v, int vSize)


{ for (int i = 0; i < vSize - 1; i++)
{ int minPos = findMinPos(v, i, vSize-1);
if (minPos != i) swap(v, minPos, i);
}
} //abbiamo usato due metodi ausiliari, swap e findMinPos

private static void swap(String[] v, int i, int j)


{ String temp = v[i];
v[i] = v[j];
v[j] = temp;
}

private static int findMinPos(String[] v, int from, int to)


{ int pos = from;
String min = v[from];
for (int i = from + 1; i <= to; i++)
if (v[i].compareTo(min) < 0) //v[i] viene prima di min
{ pos = i;
min = v[i];
}
return pos;
}

Si possono riscrivere tutti i metodi di ordinamento e ricerca visti per i numeri interi ed usarli
per le stringhe
• Ma come fare per altre classi?
Possiamo ordinare oggetti di tipo BankAccount in ordine, ad esempio, di saldo crescente?
Bisogna definire nella classe BankAccount un metodo analogo al metodo compareTo della
classe String.
Bisogna riscrivere i metodi perché accettino come parametro un array di BankAccount
Non possiamo certo usare questo approccio per qualsiasi classe, deve esserci un sistema
migliore!
In effetti c'è, ma dobbiamo prima studiare l'ereditarietà,il polimorfismo e l'uso di interfacce in
Java ...
EREDITARIETA'

L'ereditarietà è uno dei principi basilari della programmazione orientata agli oggetti.
L’ereditarietà è un paradigma che supporta l’obiettivo di riusabilità del codice.
Si sfrutta quando si deve realizzare una classe ed è già disponibile un'altra classe che
rappresenta un concetto più generale.
In questi casi, la nuova classe da scrivere è una classe più specializzata, che eredita i
comportamenti (metodi) della classe più generale e ne aggiunge di nuovi.

Sintassi:
class NomeSottoclasse extends NomeSuperclasse
{ costruttori
nuovi metodi
nuove variabili
}

Scopo: definire la classe NomeSottoclasse che deriva dalla classe NomeSuperclasse


definendo nuovi metodi e/o nuove variabili, oltre ai suoi costruttori
• Nota: una classe estende sempre una sola altra classe
• Nota: se la superclasse non è indicata esplicitamente, il compilatore usa implicitamente
java.lang.Object

Supponiamo di voler realizzare una classe SavingsAccount per rappresentare un conto


bancario di risparmio
• Dovrà avere un tasso di interesse annuo determinato al momento dell’apertura
• Dovrà avere un metodo addInterest per accreditare gli interessi sul conto
• Un conto bancario di risparmio ha tutte le stesse caratteristiche di un conto bancario, più
alcune altre caratteristiche che gli sono peculiari
• Allora possiamo riutilizzare il codice già scritto per la classe BankAccount

public class BankAccount public class SavingsAccount


{ public BankAccount() { public SavingsAccount(double rate)
{ balance = 0; { balance = 0;
} interestRate = rate;
public void deposit(double amount) }
{ balance = balance + amount; public void addInterest()
} { deposit(getBalance() * interestRate / 100);
public void withdraw(double amount) }
{ balance = balance - amount; private double interestRate;
} public void deposit(double amount)
public double getBalance() { balance = balance + amount;
{ return balance; }
} public void withdraw(double amount)
private double balance; { balance = balance - amount;
} }
public double getBalance()
{ return balance;
}
private double balance; }

Come previsto, buona parte del codice di BankAccount ha potuto essere copiato nella classe
SavingsAccount. Inoltre ci sono tre cambiamenti
• Una nuova variabile di esemplare: interestRate
• Un costruttore diverso (ovviamente il costruttore ha anche cambiato nome)
• Un nuovo metodo: addInterest
Copiare il codice non è una scelta soddisfacente. Cosa succede se BankAccount viene
modificata?
Questo metodo è molto scomodo, e fonte di molti errori…
Ri-scriviamo la classe SavingsAccount usando il meccanismo dell'ereditarietà
• Dichiariamo che SavingsAccount è una classe derivata da BankAccount (extends)
• eredita tutte le caratteristiche (campi di esemplare e metodi) di BankAccount
• specifichiamo soltanto le peculiarità di SavingsAccount

public class SavingsAccount extends BankAccount


{ public SavingsAccount(double rate)
{ interestRate = rate;
}
public void addInterest()
{ deposit(getBalance() * interestRate / 100);
}
private double interestRate;
}

Oggetti della classe derivata SavingsAccount si usano come se fossero oggetti di


BankAccount con qualche proprietà in più
• La classe derivata si chiama sottoclasse
• La classe da cui si deriva si chiama superclasse

SavingsAccount acct = new SavingsAccount(10);


acct.deposit(500);
acct.withdraw(200);
acct.addInterest();
System.out.println(acct.getBalance()); //restituisce 330

public class SavingsAccount extends BankAccount


{ ... public void addInterest()
{ deposit(getBalance() * interestRate / 100); //addInterest usa deposit e getBal
} ... //manca il parametro implicito davanti a deposit
} //le variabili private non vengono ereditate (uso getBalance non balance)

Il metodo addInterest usa i metodi getBalance e deposit di BankAccount


• Non specifica un parametro implicito (cioè il parametro implicito è this)
• Il metodo addInterest invoca getBalance e deposit invece che usare direttamente il campo
balance (questo perché il campo balance è definito come private in BankAccount mentre
addInterest è definito in SavingsAccount e non ha accesso a balance)

In Java, ogni classe che non deriva da nessun’altra deriva implicitamente dalla superclasse
universale del linguaggio, che si chiama Object. Tutte le classi estendono una sola classe.
Quindi, SavingsAccount deriva da BankAccount, che a sua volta deriva da Object
• Object ha alcuni metodi, che vedremo più avanti (tra cui toString), che quindi sono ereditati
da tutte le classi in Java
• L’ereditarietà avviene anche su più livelli, quindi SavingsAccount eredita anche le proprietà
di Object
TERMINOLOGIA E NOTAZIONE

CONFONDERE SOTTOCLASSE E SUPERCLASSE


Dato che oggetti di tipo SavingsAccount sono un’estensione di oggetti di tipo BankAccount
più “grandi” di oggetti di tipo BankAccount, nel senso che hanno una variabile di esemplare in
più, più “abili” di oggetti di tipo BankAccount, perché hanno un metodo in più...
...perché mai SavingsAccount si chiama sottoclasse e non superclasse?
I termini superclasse e sottoclasse derivano dalla teoria degli insiemi
I conti bancari di risparmio (gli oggetti di tipo SavingsAccount) costituiscono un sottoinsieme
dell’insieme di tutti i conti bancari (gli oggetti di tipo BankAccount)

DIAGRAMMI E GERARCHIE DI EREDITARIETA'


Abbiamo già visto diagrammi di classi per visualizzare accoppiamento tra
classi
• In un diagramma di classi l’ereditarietà viene rappresentata mediante una
freccia con la punta a “triangolo vuoto”, diretta verso la superclasse.
Per esemio:
• La gerarchia delle classi di eccezioni
• Le gerarchie delle classi del pacchetto io
• InputStream, OutputStream, Reader, Writer

METODI DI UNA SOTTOCLASSE

Quando definiamo una sottoclasse, possono verificarsi tre diverse situazioni per quanto
riguarda i suoi metodi
• Primo caso: nella sottoclasse viene definito un metodo che nella superclasse non esisteva
Ad esempio il metodo addInterest di SavingsAccount
• Secondo caso: un metodo della superclasse viene ereditato dalla sottoclasse
Ad esempio deposit, withdraw, getBalance di SavingsAccount, ereditati da BankAccount
• Terzo caso: un metodo della superclasse viene sovrascritto nella sottoclasse
Vediamo ora un esempio anche di questo caso

La possibilità di sovrascrivere (override) un metodo della superclasse, modificandone il


comportamento quando è usato per la sottoclasse, è una delle caratteristiche più potenti del
OOP
• Per sovrascrivere un metodo bisogna definire nella sottoclasse un metodo con la stessa
firma di quello definito nella superclasse
• tale metodo prevale su quello della superclasse quando viene invocato con un oggetto della
sottoclasse

Vogliamo modificare la classe SavingsAccount in modo che ogni operazione di versamento


abbia un costo (fisso) FEE, che viene automaticamente addebitato sul conto
• I versamenti nei conti di tipo SavingsAccount si fanno però invocando il metodo deposit di
BankAccount, sul quale non abbiamo controllo
• Possiamo però sovrascrivere deposit, ridefinendolo in SavingsAccount
• In più aggiungiamo una costante di classe FEE, che contenga l'importo del costo fisso da
addebitare
Sovrascriviamo il metodo deposit:

public class SavingsAccount extends BankAccount


{ ...
public void deposit(double amount)
{ ... // aggiungi amount a balance
withdraw(FEE);
}
...
private final static double FEE = 2.58; // euro
}

Quando viene invocato deposit con un oggetto di tipo SavingsAccount, viene invocato il
metodo deposit definito in SavingsAccount e non quello definito in BankAccount.
Nulla cambia per oggetti di tipo BankAccount.

Proviamo a completare il metodo:


• dobbiamo versare amount e sommarlo a balance, non possiamo modificare direttamente
balance, che è una variabile privata in BankAccount
• l’unico modo per aggiungere una somma di denaro a balance è l’invocazione del metodo
deposit

public class SavingsAccount extends BankAccount


{ ...
public void deposit(double amount) //ricorsione al metodo stesso
{ deposit(amount); // NON FUNZIONA
withdraw(FEE);
}
}

Così non funziona, perché il metodo diventa ricorsivo con ricorsione infinita!!! (manca il caso
base…)

IL RIFERIMENTO SUPER
Ciò che dobbiamo fare è invocare il metodo deposit di BankAccount
Questo si può fare usando il riferimento implicito super, gestito automaticamente dal
compilatore per accedere agli elementi ereditati dalla superclasse

public class SavingsAccount extends BankAccount


{ ...
public void deposit(double amount)
{ super.deposit(amount); //invoca deposit della superclasse
withdraw(FEE);
}

Super è simile a this. Fai finta che l'oggetto sia di tipo banckaccount quindi usa deposit di
banckaccount.

Sintassi: super.nomeMetodo(parametri)

• Scopo: invocare il metodo nomeMetodo della superclasse anziché il metodo con lo stesso
nome (sovrascritto) della classe corrente
• super è un riferimento all'oggetto parametro implicito del metodo, come il riferimento this
• super viene creato al momento dell'invocazione del metodo, come il riferimento this
• Però super tratta l'oggetto a cui si riferisce come se fosse un esemplare della superclasse:
questa è l'unica differenza tra super e this
Subito dopo l'invocazione di deposit (sovrascritto in
SavingsAccount), esistono tre riferimenti all'oggetto.
• I riferimenti sAcct e this sono di tipo
SavingsAccount
• Il riferimento super è di tipo BankAccount

CAMPI DI ESEMPLARE DI UNA SOTTOCLASSE

EREDITARE CAMPI ESEMPLARE


Quando definiamo una sottoclasse, possono verificarsi solo due diverse situazioni per quanto
riguarda i suoi campi di esemplare
• Primo caso: nella sottoclasse viene definito un campo di esemplare che nella superclasse
..esisteva
Ad esempio il campo interestRate di SavingsAccount
• Secondo caso: un campo della superclasse viene ereditato dalla sottoclasse
Ad esempio il campo balance di SavingsAccount, ereditatoda BankAccount

• Invece non è possibile sovrascrivere un campo della superclasse nella sottoclasse

Cosa succede se nella sottoclasse viene definito un campo omonimo di uno della superclasse?
È un’operazione lecita, ma molto sconsigliabile
Si creano due campi di esemplare omonimi ma distinti, e in particolare il nuovo campo di
esemplare mette in ombra il suo omonimo della superclasse

public class SavingsAccount extends BankAccount


{ ...
public void deposit(double amount) //se modifico balance della sottoclasse non
{ withdraw(FEE); //modifico balance della superclasse
balance = balance + amount;
}
...
private double balance; // Meglio non farlo!
}
Il metodo deposit aggiorna questa variabile balance, mentre i metodi ereditati da BankAccount
fanno riferimento alla variabile balance ereditata da BankAccount: errore logico!

Posso sovrascrivere anche metodi statici (con qualche accorgimento!)

METTERE IN OMBRA CAMPI EREDITATI


Perchè Java presenta questa apparente “debolezza”?
Chi scrive una sottoclasse non deve conoscere niente di quanto dichiarato private nella
superclasse, né scrivere codice che si basa su tali caratteristiche: incapsulamento!
È perfettamente lecito definire e usare una variabile balance nella sottoclasse, sarebbe strano
impedirlo: chi progetta la sottoclasse non sa che esiste una variabile balance nella
superclasse!
Ma non si può usare nella sottoclasse variabili omonime di quelle della superclasse e sperare
che siano la stessa cosa
COSTRUTTORI DI UNA SOTTOCLASSE
Sappiamo che i campi di esemplare privati della superclasse non sono accessibili dalla
sottoclasse
Come si fa a inizializzare questi campi di esemplare da un costruttore della sottoclasse?
• Bisogna invocare un costruttore della superclasse, usando la parola chiave super seguita da
parentesi tonde ed eventuali parametri espliciti del costruttore
• L'invocazione del costruttore della superclasse deve essere il primo enunciato del
costruttore della sottoclasse

public class SavingsAccount extends BankAccount


{ public SavingsAccount(double initialBalance, double rate)
{
super(initialBalance); //non posso scrivere balance=initialBalance
interestRate = rate; //chiamo il costruttore della superclasse che inizializza balance
} ...}

E se non invochiamo un costruttore della superclasse?


• Viene invocato il costruttore predefinito della superclasse (quello privo di parametri espliciti)
• Se questo non esiste (ovvero, se tutti i costruttori definiti nella superclasse richiedono
parametri espliciti) viene generato un errore in compilazione

public class SavingsAccount extends BankAccount


{ public SavingsAccount(double rate)
{
//super(); invocato automaticamente
interestRate = rate;
} ...
}

La classe CheckingAccount: fa pagare al correntista una tassa dopo un certo numero di


operazioni compiute

public class CheckingAccount extends BankAccount


{
public CheckingAccount(double initialBalance) //COSTRUTTORE
{ super(initialBalance); //costruttore della superclasse
transactionCount = 0; //azzera conteggio transaz. //nuova var esemplare
}
public void deposit(double amount) //METODO SOVRASCRITTO!!
{ super.deposit(amount); //aggiungi amount al saldo
transactionCount++;
}
public void withdraw(double amount) //METODO SOVRASCRITTO!!
{ super.withdraw(amount); //sottrai amount dal saldo
transactionCount++; //dopo aver prelevato aumento di uno il n° di transazioni
}
public void deductFees() //NUOVO METODO
{ if (transactionCount > FREE_TRANSACTIONS)
{ double fees = TRANSACTION_FEE *
(transactionCount – FREE_TRANSACTIONS);
super.withdraw(fees);
}
transactionCount = 0; //quando ho pagato le tasse ricomincio da zero
}
//nuovi campi di esemplare
private int transactionCount;
private static final int FREE_TRANSACTIONS = 3;
private static final double TRANSACTION_FEE = 2.0;
}
CONVERSIONI DI TIPO TRA SOTTOCLASSE E SUPERCLASSE
Conversione di tipo tra variabili oggetto

CONVERSIONE FRA RIFERIMENTI


Un oggetto di tipo SavingsAccount è un caso speciale di oggetti di tipo BankAccount
• Questa proprietà si riflette in una proprietà della sintassi di Java
• una variabile oggetto del tipo di una superclasse può riferirsi ad un oggetto di una classe
. derivata
• non c’è nessuna “conversione” effettiva, cioè non vengono modificati i dati

SavingsAccount collegeFund = new SavingsAccount(10); // tutto ok


...
BankAccount anAccount = collegeFund; // questo è lecito!!
Object anObject = collegeFund; // anche questo!!

Le tre variabili, di tipi diversi, puntano


ora allo stesso oggetto.

SavingsAccount collegeFund = new SavingsAccount(10);


BankAccount anAccount = collegeFund;
anAccount.deposit(10000); //OK: deposit è metodo di BankAccount

Tramite la variabile anAccount si può usare l’oggetto come se fosse di tipo BankAccount.
Ma non si può accedere alle proprietà specifiche di SavingsAccount.
Il tipo della variabile oggetto specifica cosa si può fare con un oggetto (cioè quali metodi si
possono utilizzare)

anAccount.addInterest(); // questo NON è lecito perché addInterest NON è metodo di


. .. BankAccount

Aggiungiamo il metodo transfer a BanckAccount:

public class BankAccount


{ ...
public void transfer(double amount, BankAccount other)
{ withdraw(amount); // ovvero, this.withdraw(...)
other.deposit(amount);
}
...
}

BankAccount acct1 = new BankAccount(500);


BankAccount acct2 = new BankAccount();
acct1.transfer(200, acct2);
System.out.println(acct1.getBalance());
System.out.println(acct2.getBalance());
Per quanto detto finora, il parametro other può anche riferirsi ad un oggetto di tipo
SavingsAccount

BankAccount other = new BankAccount(1000);


SavingsAccount sAcct = new SavingsAccount(10);
BankAccount bAcct = sAcct;
other.transfer(500, bAcct);

La conversione tra riferimento a sottoclasse e riferimento a superclasse può avvenire anche


implicitamente (come tra int e double)

BankAccount other = new BankAccount(1000);


SavingsAccount sAcct = new SavingsAccount(10);
other.transfer(500, sAcct); //conversione automatica, nonostance nel metodo transfer è
specificato che il secondo parametro esplicito deve essere di tipo BankAccount

• Il compilatore sa che il metodo transfer richiede un riferimento di tipo BankAccount, quindi


controlla che sAcct sia un riferimento di tipo BankAccount o di una sua sottoclasse effettua la
conversione automaticamente

Vediamo la conversione inversa di quella vista finora


Ovvero la conversione di un riferimento a superclasse in un riferimento a sottoclasse
• Questa non può avvenire automaticamente (la sottoclasse ha più metodi della superclasse,
sarebbe come convertire double in int: perdo informazioni).

BankAccount bAcct = new BankAccount(1000);


SavingsAccount sAcct = bAcct;

• Ma ha senso cercare di effettuarla?


In generale non ha senso, perché in generale un oggetto della superclasse non è un oggetto
della sottoclasse

Tale conversione ha senso soltanto se, per le specifiche dell’algoritmo, siamo sicuri che il
riferimento a superclasse punta in realtà ad un oggetto della sottoclasse
• Richiede un cast esplicito
• Richiede attenzione, perché se ci siamo sbagliati verrà lanciata un’eccezione

SavingsAccount sAcct = new SavingsAccount(1000);


BankAccount bAcct = sAcct;
...
SavingsAccount sAcct2 = (SavingsAccount) bAcct;

se in fase di esecuzione bAcct non punta effettivamente ad un oggetto SavingsAccount


l’interprete lancia ClassCastException
POLIMORFISMO

Sappiamo che un oggetto di una sottoclasse può essere usato come se fosse un oggetto della
superclasse

BankAccount acct = new SavingsAccount(10);


acct.deposit(500);
acct.withdraw(200);

Ma quale metodo deposit viene invocato, quello di BankAccount o quello ridefinito in


SavingsAccount?
acct è una variabile dichiarata di tipo BankAccount ma contiene un riferimento ad
un oggetto che, in realtà, è di tipo SavingsAccount!
Questa informazione è disponibile solo in esecuzione (all'interprete Java), non in
compilazione.
Secondo la semantica di Java, viene invocato il metodo deposit di SavingsAccount.

In Java il tipo di una variabile non determina in modo completo il tipo dell’oggetto a cui essa
si riferisce. Questa semantica si chiama polimorfismo ed è caratteristica dei linguaggi OO.
• La stessa operazione (ad es. deposit) può essere svolta in modi diversi, a seconda
dell'oggetto a cui ci si riferisce
• L'esecuzione di un metodo su un oggetto è sempre determinata dal tipo
dell’oggetto, e NON dal tipo della variabile oggetto
• Il tipo della variabile oggetto specifica cosa si può fare con un oggetto (cioè quali metodi si
possono utilizzare)
• Il tipo dell'oggetto specifica come farlo

Abbiamo già visto una forma di polimorfismo a proposito dei metodi sovraccarichi
• L’invocazione del metodo println è in realtà una invocazione di un metodo scelto fra alcuni
metodi con lo stesso nome ma con firme diverse
• Il compilatore è in grado di capire quale metodo viene invocato, sulla base della firma
• In questo caso la situazione è molto diversa, perché la decisione non può essere presa dal
compilatore, ma deve essere presa dall’ambiente runtime (l’interprete)
• Si parla di selezione posticipata (late binding)
• Mentre nel caso di metodi sovraccarichi si parla di selezione anticipata (early binding)

ESEMPIO 1

public class AccountTester1


{
public static void main(String[] args)
{ SavingsAccount momsSavings = new SavingsAccount(0.5);
CheckingAccount harrysChecking = new CheckingAccount(100);

// metodi polimorfici di BankAccount e sue sottoclassi


momsSavings.deposit(10000); //vengono invocati i metodi delle sottoclassi
momsSavings.transfer(2000, harrysChecking);
harrysChecking.withdraw(1500);
harrysChecking.withdraw(80);
momsSavings.transfer(1000, harrysChecking);
harrysChecking.withdraw(400);

// simulazione della fine del mese


momsSavings.addInterest(); //metodo solo di SavingsAccount
harrysChecking.deductFees();//metodo solo di CheckingAccount
System.out.println("Mom’s savings balance = $"+ momsSavings.getBalance());
System.out.println("Harry’s checking balance = $"+ harrysChecking.getBalance());
}
}
ESEMPIO 2

public class AccountTester2


{
public static void main(String[] args)
{ BankAccount momsSavings = new SavingsAccount(0.5);
BankAccount harrysChecking = new CheckingAccount(100);

// metodi polimorfici di BankAccount e sue sottoclassi


momsSavings.deposit(10000); //vengono invocati i metodi delle sottoclassi
momsSavings.transfer(2000, harrysChecking); //anche se i riferimenti sono
harrysChecking.withdraw(1500); //di tipo BanckAccount
harrysChecking.withdraw(80);
momsSavings.transfer(1000, harrysChecking);
harrysChecking.withdraw(400);

// simulazione della fine del mese


((SavingsAccount)momsSavings).addInterest(); //e` necessario fare i cast
((CheckingAccount)harrysChecking).deductFees();
System.out.println("Mom’s savings balance = $" + momsSavings.getBalance());
System.out.println("Harry’s checking balance = $" + harrysChecking.getBalance());
}
}

CONTROLLO DEL TIPO E INSTANCEOF


Se si vuole controllare il tipo dell'oggetto a cui una variabile oggetto si riferisce, si può usare
un nuovo operatore: instanceof

varOggetto instanceof NomeClasse

È un operatore relazionale (restituisce valori booleani)


• Restituisce true se la variabile oggetto (primo argormento) sta puntando a un oggetto del
tipo specificato dal secondo argomento se varOggetto contiene un riferimento ad un oggetto
della classe NomeClasse (o una sua sottoclasse).
In caso di valore restituito true, un eventuale cast di varOggetto a una variabile di tipo
NomeClasse NON lancia l’eccezione ClassCastException
• Nota: il risultato non dipende dal tipo di varOggetto, ma dal tipo dell’oggetto a cui essa si
riferisce al momento dell’esecuzione.

Ad esempio:
SavingsAccount collegeFund = new SavingsAccount(10);
BankAccount anAccount = collegeFund;
if (anAccount instanceof SavingsAccount) //Restituisce true
SavingsAccount a = (SavingsAccount) anAccount;
//Possiamo effettuare il cast in tranquillità

RIFLESSIONI SULLA PROGRAMMAZIONE OOP


Robustezza
programmi capaci di gestire situazioni inaspettate (ad es. input inattesi).
Adattabilità
programmi capaci di evolvere (ad es. girare su diverse architetture).
Riusabilità
codice utilizzabile come componente di diversi sistemi in varie applicazioni.
Astrazione
“Distillare” i concetti che meglio rappresentano un oggetto o un sistema.
Information hiding
Nascondere l’informazione a utenti esterni, lasciando vedere solo l’interfaccia.
In questo modo parti di un programma possono cambiare senza effetti sulle altre.
Modularità
Organizzare un sistema software in componenti funzionali separate.
LA CLASSE UNIVERSALE OBJECT

Sappiamo già che ogni classe di Java è (in maniera diretta o indiretta) sottoclasse di Object.
Quindi ogni classe di Java eredita tutti i metodi della classe Object.
Alcuni dei più utili metodi di Object sono i seguenti:

Ma perché siano davvero utili, nella maggior parte dei casi bisogna sovrascriverli.

SOVRASCRIVERE I METODI toString E equals


Il metodo println, è in grado di ricevere come parametro un oggetto di qualsiasi tipo,
qualsiasi riferimento può sempre essere convertito automaticamente in un riferimento di tipo
Object.

public void println(Object obj) //invocato per oggetti generici


{ println(obj.toString()); }

public void println(String s) // invocato per le stringhe


{ ... }

• Ma un esemplare di String è anche di tipo Object…come viene scelto, tra i metodi


sovraccarichi, quello giusto quando si invoca println con una stringa?
• Il compilatore cerca sempre di “fare il minor numero di conversioni”, viene usato, quindi, il
metodo “più specifico” (il minor numero di cast automatici)

IL METODO toString
Il metodo toString della classe Object ha la firma public String toString()
L’invocazione di questo metodo per qualsiasi oggetto ne restituisce la descrizione testuale
standard.
Il nome della classe seguito dal carattere @ e dall' hashcode dell’oggetto (che è un numero
univocamente determinato dall’indirizzo in memoria dell’oggetto stesso)

BankAccount a = new BankAccount(1000);


System.out.println(a); BankAccount@111f71

• In generale la descrizione testuale standard non è particolarmente utile, è piu utile ottenere
una stringa di testo contenente informazioni sullo stato dell’oggetto in esame.

Passando un oggetto qualsiasi a System.out.println si visualizza la sua descrizione testuale


standard.
• println invoca toString dell’oggetto, e l’invocazione è possibile perché tutte le classi hanno il
metodo toString, eventualmente ereditato da Object

BankAccount a = new BankAccount(1000);


System.out.println(a);

• Il metodo toString di una classe viene invocato implicitamente anche per concatenare un
oggetto con una stringa:

BankAccount acct = new BankAccount();


String s = "Conto " + acct;

• La seconda riga viene interpretata dal compilatore come se fosse stata scritta così:

String s = "Conto " + acct.toString();


●SOVRASCRIVERE IL METODO toString
Sovrascrivere il metodo toString nelle classi che si scrivono è considerato buono stile di
programmazione.
• toString dovrebbe sempre produrre una stringa contenente tutte le informazioni di stato
dell’oggetto:
-Il valore di tutte le sue variabili di esemplare
-Il valore di variabili statiche non costanti della classe
• Questo stile di programmazione è molto utile per il debugging ed è usato nella libreria
standard

Esempio: descrizione testuale di BankAccount

System.out.println(anAccount); BankAccount[balance=1500]

Bisogna sovrascrivere toString in BankAccount

Stile adottato nella libreria standard:


toString crea una stringa contenente il nome della classe seguito dai valori dei campi di
esemplare racchiusi fra parentesi quadre.

public class BankAccount


{ ...
public String toString()
{
return “BankAccount[balance=” + balance + “]”;
}
}

• Problema: devo sovrascrivere toString anche per le sottoclassi, per vedere stampato il
nome di classe corretto

È possibile evitare di scrivere esplicitamente il nome della classe all’interno del metodo
• Si può usare il metodo getClass, che restituisce un oggetto di tipo classe e poi invocare il
metodo getName sull’oggetto di tipo classe, per ottenere il nome della classe

public String toString()


{
return getClass().getName() + “[balance=” + balance + “]”;
}

• toString visualizza il nome corretto della classe anche quando viene invocato su un oggetto
di una sottoclasse

Ovviamente, se si vogliono visualizzare i valori di nuovi campi di esemplare di una sottoclasse


bisogna sovrascrivere toString
• Bisogna invocare super.toString per ottenere i valori dei campi della superclasse

public class SavingsAccount extends BankAccount


{ ...
public String toString()
{
return super.toString() + “[interestRate=” + interestRate + “]”;
}...
}
IL METODO equals
Il metodo equals di Object ha la firma public boolean equals(Object otherObject)

L’invocazione di questo metodo restituisce


• true se l'oggetto su cui viene invocato coincide con l'oggetto passato come parametro
esplicito (Ovvero se this == otherObject)
• false altrimenti

In particolare equals restituisce un valore true se e solo se gli hashcode dell’oggetto this e
dell'oggetto otherObject sono uguali. Fanno riferimento alla stessa posizione di memoria.
Ovvero due oggetti sono uguali se sono lo stesso oggetto (se dentro le due variabili oggetto
fanno riferimento allo stesso ogetto. Funziona come ==.

In generale questo comportamento non è molto utile. È piu utile ottenere un'informazione
booleana che dica se gli stati degli oggetti in esame coincidono.

Sovrascrivere il metodo equals nelle classi che si scrivono è considerato buono stile di
programmazione.
• equals dovrebbe sempre dare informazione sulla coincidenza degli stati dei due oggetti
• Ovvero restituire true se e solo se i valori di tutte le variabili di esemplare dei due oggetti
coincidono.
Questo stile di programmazione è molto utile per il confronto di oggetti ed è usato nella
libreria standard

• Esempio: confronto di uguaglianza tra BankAccount

BankAccount a1 = new BankAccount(1000);


BankAccount a2 = new BankAccount(a1.getBalance());
System.out.println(“Confronto uguaglianza: ” + a1.equals(a2));

Confronto uguaglianza: true

Bisogna sovrascrivere equals in BankAccount.

●SOVRASCRIVERE IL METODO equals

•Esempio: equals in BankAccount


Confronto tra i campi di esemplare dei due oggetti:
public class BankAccount
{ ...
public boolean equals(Object otherObject)
{
BankAccount otherAcct = (BankAccount) otherObject;
return balance == otherAcct.balance;
}
}

• Problema: nella firma di equals il parametro esplicito è di tipo Object, quindi è necessario
eseguire un cast per accedere ai campi di esemplare di otherObject.

• Se si scrive un metodo public boolean equals(BankAccount acct) non si sovrascrive il


metodo equals di Object!
CONTROLLO DI ACCESSO

Java fornisce quattro livelli per il controllo di accesso a metodi, campi, e classi
• public
• private
• protected
• package

Finora noi abbiamo sempre usato solo i primi due, ma ora siamo in grado di comprendere
anche gli altri due. In particolare lo specificatore di accesso protected.

ACCESSO PACKAGE
Un membro di classe (o una classe) senza specificatore di accesso ha di default una
impostazione di accesso package (accesso di pacchetto)
• I metodi di classi nello stesso pacchetto vi hanno accesso
• Può essere una buona impostazione per le classi, non lo è per le variabili, perché si viola
l’incapsulamento
• Errore comune: dimenticare lo specificatore di accesso per una variabile di esemplare

public class Window extends Container


{ String warningString;
...
}

• È un rischio per la sicurezza: un altro programmatore può realizzare una classe nello stesso
pacchetto e ottenere l’accesso al campo di esemplare

ACCESSO PROTECTED

public class BankAccount


{ ...
protected double balance;
}

Abbiamo visto che una sottoclasse non può accedere a campi private ereditati dalla propria
superclasse. Il progettista della superclasse può rendere accessibile in modo protected un
campo di esemplare.

Tutte le sottoclassi vi hanno accesso


• Attenzione: è una violazione dell’incapsulamento
• Il progettista della superclasse non ha controllo sui progettisti di eventuali sottoclassi
• Diventa difficile modificare modificare variabili protected perché potrebbe esserci una
sottoclasse che ne fa uso
• Anche i metodi possono essere definiti protected possono essere invocati solo nella classe in
cui sono definiti e nelle classi da essa derivate
CLASSI E METODI FINAL

Sappiamo che la parola chiave final viene usata nella definizione di una variabile per impedire
successive assegnazioni di valori
• Anche metodi possono essere dichiarati final, un metodo final non può essere sovrascritto
da sottoclassi

• Esempio: un metodo per verificare una password


public class SecureAccount extends BankAccount
{ ...
public final boolean checkPassword(String password)
{ ... }
}

• In questo modo la sicurezza è garantita, nessuno può sovrascriverlo con un metodo che
restituisca sempre true
Anche le classi possono essere dichiarate final: una classe dichiarata final non può essere
estesa
Esempio: la classe String è una classe immutabile
Gli oggetti di tipo String non possono essere modificati da nessuno dei loro metodi

La firma della classe String nella libreria standard è: public final class String { ...}
• Nessuno può creare sottoclassi di String, quindi siamo sicuri che riferimenti ad oggetti di
tipo String si possono sempre copiare senza rischi di cambiamenti
INTERFACCE

REALIZZARE UNA PROPRIETÀ ASTRATTA


Torniamo al problema già enunciato: ordinare oggetti
generici.
Affinché i suoi esemplari possano essere ordinati, una
classe deve definire un metodo adatto a confrontare
esemplari della classe, con lo stesso comportamento di
compareTo
• Gli oggetti della classe diventano confrontabili
gli algoritmi di ordinamento/ricerca non hanno bisogno
di conoscere alcun particolare del criterio di confronto
tra gli oggetti, basta che gli oggetti siano tra loro
confrontabili
POSSO SCRIVERE ALGORITMI GENERALI

SCRIVERE UN' INTERFACCIA


Sintassi:
public interface NomeInterfaccia
{ firme di metodi }
//tutte le classi di oggetti comparabili devono avere compareTo

La definizione di una interfaccia in Java serve per definire un comportamento astratto.


Una interfaccia deve poi essere realizzata (implementata) da una classe che dichiari di avere
tale comportamento.

Definire un’interfaccia è simile a definire una classe: si usa la parola chiave interface al posto
di class. Un’interfaccia può essere vista come una classe ridotta perchè:
• non può avere costruttori
• non può avere variabili di esemplare
• contiene le firme di uno o più metodi non statici (definiti implicitamente public), ma non
può definirne il codice

IMPLEMENTARE UN' INTERFACCIA


• Esempio: L'interfaccia Comparable

public interface Comparable


{ int compareTo(Object other); }

Se una classe intende realizzare concretamente un comportamento astratto definito in


un’interfaccia, deve
• Dichiarare di implementare l’interfaccia
• Definire i metodi dell’interfaccia, con la stessa firma

public class BankAccount implements Comparable


{ ...
public int compareTo(Object other)
{//other è un Object perché questa è la firma in Comparable.Ma facciamo un cast a BankAccount

BankAccount acct = (BankAccount) other;


if (balance < acct.balance) return -1;
if (balance > acct.balance) return 1;
return 0;
} ...
}

Fino a java 8 nell'interfaccia c'erano solo firme di metodi, dopo java posso scrivere metodi
nell'interfaccia.
NON IN PROGRAMMA : JAVA8

Sino a Java8 tutti i metodi delle interfacce dovevano essere astratti. Java 8 consente di
definire nelle interfacce metodi statici che funzionano esattamente come i metodi statici
definiti nelle classi.
Un metodo statico definito in un’interfaccia non opera su un oggetto e il suo scopo dovrebbe
essere correlato a quello dell’interfaccia in cui è contenuto.

public interface Measurable {


// un metodo astratto:
double getMeasure();
// un metodo static:
static double average(Measurable[] objects)
{
. . . // implementazione
}

Con java 8 dopo aver creato un'interfaccia posso implementarla con metodi statici che
vengono chiamati come se fossero metodi statici di una classe. Non posso scrivere metodi
non statici perchè per usarli servirebbe un oggetto ma l'interfaccia non può avere suoi
oggetti. Il metodo viene usato da tutte le classi che implementano l'interfaccia.

Per invocare tale metodo si scrive il nome dell’interfaccia che lo contiene:

double meanArea = Measurable.average(countries);

Un metodo predefinito o di default (default o defender method) in un’interfaccia è un


metodo non statico di cui viene definita anche l’implementazione.
• Una classe che implementa l’interfaccia erediterà il comportamento predefinito del metodo
oppure lo potrà sovrascrivere: il fatto che in un’interfaccia venga predefinita
un’implementazione per un metodo alleggerisce il lavoro necessario per realizzare una
classe che la implementi.

Ad esempio, l’interfaccia Measurable potrebbe dichiarare un metodo smallerThan per


verificare se un oggetto ha una misura inferiore a quella di un altro, azione utile per disporre
oggetti in ordine di misura crescente, come predefinito:

public interface Measurable


{ double getMeasure(); // un metodo astratto
default boolean smallerThan(Measurable other)
{ return getMeasure() < other.getMeasure();
}
}

Una classe che voglia implementare l’interfaccia Measurable deve soltanto definire il metodo
getMeasure, ereditando automaticamente il metodo smallerThan: un meccanismo che può
essere molto utile.
CONVERSIONI DI TIPO TRA CLASSE E INTERFACCIA

USARE UNA INTERFACCIA


In generale, valgono le regole di conversione viste nell'ambito dell'ereditarietà
• Una interfaccia ha lo stesso ruolo di una superclasse
• Una classe che implementa un'interfaccia ha lo stesso ruolo di una sottoclasse che estende
la superclasse
• È lecito definire una variabile il cui tipo è un'interfaccia: Comparable c; // corretto
ma non è possibile costruire oggetti da un’interfaccia new Comparable(); // ERRORE
• Le interfacce non hanno campi di esemplare nè costruttori

Allora a che serve avere una variabile oggetto il cui tipo è quello di una interfaccia?

CONVERSIONE DA CLASSE AD INTERFACCIA


Una variabile oggetto di tipo Comparable può puntare ad un oggetto di una classe che
realizza Comparable

Comparable c = new BankAccount(10); //corretto

• Usando la variabile c non sappiamo esattamente quale è il tipo dell'oggetto a cui essa si
riferisce
• Non possiamo utilizzare tutti i metodi di BankAccount

double saldo = c.getBalance(); //ERRORE in compilazione

• Però siamo sicuri che quell'oggetto ha un metodo compareTo

Comparable d = new BankAccount(20);


if ( c.compareTo(d) > 0 ) //corretto

CONVERSIONE DA INTERFACCIA A CLASSE


A volte può essere necessario convertire un riferimento il cui tipo è un interfaccia in un
riferimento il cui tipo è una classe che implementa l'interfaccia

Comparable c = new BankAccount(10);


double saldo = c.getBalance(); //ERRORE! Come faccio?

Se siamo certi che c punta ad un oggetto di tipo BankAccount possiamo creare una nuova
variabile acct di tipo BankAccount e fare un cast di c su acct

Comparable c = new BankAccount(10);


BankAccount acct = (BankAccount) c;
double saldo = acct.getBalance(); //corretto

E se ci sbagliamo? ClassCastException in esecuzione


POLIMORFISMO E INTERFACCE

Comparable x;
...
if (...) x = new BankAccount(1000);
else x = new String(“”);
...
if (x.compareTo(y) > 0) ...

Possiamo invocare il metodo compareTo perché x è di tipo Comparable


• Ma quale metodo compareTo? Quello scritto in BankAccount o quello di String?
Il compilatore non ha questa informazione.
• Il tipo di oggetto a cui x si riferisce dipende dal flusso di esecuzione.
• In fase di esecuzione la JVM determina il tipo di oggetto a cui x si riferisce e applica il
metodo corrispondente.

Il tipo di una variabile non determina in modo completo il tipo dell’oggetto a cui essa si
riferisce.
• Come per l'ereditarietà, questa semantica si chiama polimorfismo
• La stessa operazione (compareTo) può essere svolta in modi diversi, a seconda dell'oggetto
a cui ci si riferisce
• L’invocazione di un metodo è sempre determinata dal tipo dell’oggetto effettivamente usato
come parametro implicito, e NON dal tipo della variabile oggetto
• La variabile oggetto (interfaccia) ci dice cosa si può fare con quell'oggetto (cioè quali
metodi si possono utilizzare)
• L'oggetto (appartenente ad una classe) ci dice come farlo

Comparable x;
...
if (...) x = new BankAccount(1000);
else x = new String(“”);
...
if (x.compareTo(y) > 0) ...

Se la condizione dell'if è vera, x contiene un riferimento ad un oggetto che, in quel momento


dell'esecuzione è di tipo BankAccount! E l’interprete Java lo sa (il compilatore no).
Secondo la semantica di Java, viene invocato il metodo compareTo scritto in BankAccount
INTERFACCIA COMPARABLE E ORDINAMENTI DI OGGETTI

INTERFACCIA COMPARABLE

public interface Comparable


{ int compareTo(Object other);}

L’interfaccia Comparable è definita nel pacchetto java.lang, per cui non deve essere importata
né deve essere definita
• la classe String, ad esempio, implementa Comparable
• Come può Comparable risolvere il nostro problema?
• Ovvero definire un metodo di ordinamento valido per tutte le classi?
• Basta definire un metodo per ordinare un array di riferimenti ad oggetti che realizzano
Comparable, indipendentemente dal tipo

Tutti i metodi di ordinamento e ricerca che abbiamo visto per array di numeri interi possono
essere riscritti per array di oggetti Comparable

• Basta usare le seguenti “traduzioni”

SelectionSort per oggetti Comparable:


public class ArrayAlgs
{ ...
public static void selectionSort(Comparable[] v, int vSize)
{ for (int i = 0; i < vSize - 1; i++)
{ int minPos = findMinPos(v, i, vSize-1);
if (minPos != i) swap(v, minPos, i);
}
}

private static void swap(Comparable[] v, int i, int j)


{ Comparable temp = v[i];
v[i] = v[j];
v[j] = temp;
}

private static int findMinPos(Comparable[] v, int from, int to)


{ int pos = from;
Comparable min = v[from];
for (int i = from + 1; i <= to; i++)
if (v[i].compareTo(min) < 0)
{ pos = i;
min = v[i];
}
return pos;
}
...
}
ORDINAMENTO DI OGGETTI

Definito un algoritmo per ordinare un array di riferimenti Comparable, se vogliamo ordinare


un array di oggetti BankAccount basta fare

BankAccount[] v = new BankAccount[10];


...
// creazione dei singoli elementi dell’array ed eventuali modifiche allo stato
// degli oggetti dell’array
...
ArrayAlgs.selectionSort(v, vSize);

Se BankAccount realizza l'interfaccia Comparable, l’array di riferimenti viene convertito in


maniera automatica.

SCRIVERE METODI CompareTo

Il metodo compareTo deve definire una relazione di ordine totale, ovvero deve avere queste
proprietà
• Antisimmetrica: a.compareTo(b) ≤ 0 ⇔ b.compareTo(a) ≥ 0
• Riflessiva: a.compareTo(a) = 0
• Transitiva: a.compareTo(b) ≤ 0 e b.compareTo(c) ≤ 0
• Implica a.compareTo(c)<=0

a.compareTo(b) può restituire qualsiasi valore negativo per segnalare che a precede b
if (a.compareTo(b) == -1) //errore logico!
if (a.compareTo(b) < 0) // giusto!

Una classe che implementa Comparable ha i metodi


• compareTo (per implementare Comparable)
• equals (ereditato da Object)

Il metodo compareTo definito in una classe


• dovrebbe sempre essere consistente con il metodo equals: ( e1.compareTo(e2) == 0 )
• dovrebbe sempre avere lo stesso valore booleano di e1.equals(e2) per ogni e1 e e2 della
classe

Il metodo equals in Object è:

public boolean equals(Object obj)


{ return (this == obj); }

Quindi una classe che implementa Comparable dovrebbe sempre sovrascrivere equals…
ma non sempre noi lo faremo

Esempio: la classe ArrayAlgorithms con tutti i metodi statici da noi sviluppati.

• Tutti i metodi per l’ordinamento e ricerca agiscono su array di tipo Comparable[ ] ed usano
il metodo compareTo per effettuare confronti tra oggetti
• binarySearch potrebbe usare il metodo equals per verificare l’uguaglianza
• Ma per avere comportamenti consistenti le classi che implementano Comparable devono
sovrascrivere equals
• Per ordinare (o cercare valori in) array numerici possiamo usare classi involucro (Integer,
Double,…)
• Compilando questa classe vengono generati alcuni messaggi di warning, che ignoreremo
public class ArrayAlgs // riportiamo solo i metodi per array di oggetti Comparable
{
--------------------- selectionSort per oggetti Comparable -------------------------------
public static void selectionSort(Comparable[] v, int vSize)
{ for (int i = 0; i < vSize - 1; i++)
{ int minPos = findMinPos(v, i, vSize-1);
if (minPos != i) swap(v, minPos, i); }
}

private static void swap(Comparable[] v, int i, int j)


{ Comparable temp = v[i];
v[i] = v[j];
v[j] = temp;
}

private static int findMinPos(Comparable[] v, int from, int to)


{ int pos = from;
Comparable min = v[from];
for (int i = from + 1; i <= to; i++)
if (v[i].compareTo(min) < 0)
{ pos = i;
min = v[i];
}
return pos;
}

----------------------mergeSort per oggetti Comparable -------------------------------


public static void mergeSort(Comparable[] v, int vSize)
{ if (vSize < 2) return; // caso base
int mid = vSize / 2; //dividiamo circa a meta’
Comparable[] left = new Comparable[mid];
Comparable[] right = new Comparable[vSize - mid];
System.arraycopy(v, 0, left, 0, mid);
System.arraycopy(v, mid, right, 0, vSize-mid);
mergeSort(left, mid); // passi ricorsivi
mergeSort(right, vSize-mid);
merge(v, left, right);
}

private static void merge(Comparable[] v, Comparable[] v1, Comparable[] v2)


{ int i = 0, i1 = 0, i2 = 0;
while (i1 < v1.length && i2 < v2.length)
if (v1[i1].compareTo(v2[i2]) < 0) v[i++] = v1[i1++];
else v[i++] = v2[i2++];
while (i1 < v1.length)
v[i++] = v1[i1++];
while (i2 < v2.length)
v[i++] = v2[i2++];
}

------------------- insertionSort per oggetti Comparable ------------------------------


public static void insertionSort(Comparable[] v, int vSize)
{ for (int i = 1; i < vSize; i++)
{ Comparable temp = v[i]; //elemento da inserire
int j;
for (j = i; j > 0 && temp.compareTo(v[j-1]) < 0; j--)
v[j] = v[j-1];
v[j] = temp; // inserisci temp in posizione
}
}
------------------- ricerca (binaria) per oggetti Comparable--------------------------
public static int binarySearch(Comparable[] v, int vSize, Comparable value)
{ return binSearch(v, 0, vSize-1, value);
}

private static int binSearch(Comparable[] v, int from, int to, Comparable value)
{ if (from > to) return -1;// el. non trovato
int mid = (from + to) / 2; // circa in mezzo
Comparable middle = v[mid];
if (middle.compareTo(value) == 0)
return mid; //trovato
else if (middle.compareTo(value) < 0) //cerca a destra
return binSearch(v, mid + 1, to, value);
else // cerca a sinistra
return binSearch(v, from, mid - 1, value);
}
}

ORDINAMENTO E RICERCA “DI LIBRERIA”


La libreria standard fornisce, per l’ordinamento e la ricerca, alcuni metodi statici in
java.util.Arrays:
• un metodo sort che ordina array di tutti i tipi fondamentali e array di Comparable

String[] ar = new String[10];


...
Arrays.sort(ar);

• un metodo binarySearch che cerca un elemento in array di tutti i tipi fondamentali e in


array di Comparable (restituisce la posizione come numero intero ).

Comparable e Java >= 5.0 (IMPORTANTE per gli esercizi di programmazione)


In seguito all’introduzione in Java 5.0 dei tipi generici (che non vediamo), l’utilizzo di
Comparable induce il compilatore all’emissione di particolari “segnalazioni”

Note: MyClass.java uses unchecked or unsafe operations.


Note: Recompile with -Xlint:unchecked for details.

• il compilatore avvisa che nell'invocazione di compareTo non è stato verificato che gli oggetti
Comparable da confrontare siano davvero esemplari della stessa classe.
• Possiamo ignorare questi “warning”: non sono errori, il compilatore produce comunque i/il
file di bytecode
• Con l’opzione -nowarn si eliminano le segnalazioni: javac –nowarn MyClass.java

INTERFACCIA COMPARABLE PARAMETRICA


Java >=5.0 fornisce un utile strumento: l’interfaccia Comparable parametrica, che permette
di definire il tipo di oggetto nel parametro esplicito del metodo compareTo
(T rappresenta una classe generica)

public interface Comparable<T>


{ int compareTo(T o); }

public class BankAccount implements Comparable<BankAccount>


{ public int compareTo(BankAccount obj)
{ if (balance < obj.balance) return -1; //non serve il cast
if (balance > obj.balance) return 1;
return 0;
}
}
In questo modo, errori dovuti a confronti tra oggetti di classi diverse (e quindi non
confrontabili) vengono intercettati dal compilatore

BankAccount acct1 = new BankAccount(10);


String s = “ciao”;
...
Object acct2 = s; // errore del programmatore
if (acct1.compareTo(acct2) < 0)

• In compilazione:

compareTo(BankAccount) cannot be applied to <java.lang.Object>

Se non si usa l'interfaccia Comparable parametrica, questo errore non viene rilevato in
compilazione e si genera invece una ClassCastException in esecuzione.

CLASSI E METODI ASTRATTI

• Un metodo astratto è un metodo che non ha implementazione nella classe in cui è definito
• Si definiscono l'intestazione del metodo e della classe a cui appartiene con la parola chiave
abstract

public abstract class Poligono implements Comparable


{ ...
public abstract double area();
public int compareTo(Object obj)
{ Poligono p = (Poligono) obj;
if ( area() > p.area() ) return 1;
if ( area() < p.area() ) return -1;
return 0;
}
}

Il metodo deve essere realizzato nelle classi che estendono la classe astratta.
TIPI DI DATI ASTRATTI

Definizione - una struttura dati (data structure) è un modo sistematico di organizzare i dati
in un contenitore e di controllarne le modalità d’accesso. In Java si definisce una struttura
dati con una classe.

Definizione - un tipo di dati astratto (ADT, Abstract Data Type) è una rappresentazione
astratta di una struttura dati, un modello che specifica:
• il tipo di dati memorizzati
• le operazioni che si possono eseguire sui dati, insieme al tipo di informazioni necessarie per
eseguire tali operazioni.

TIPI DI DATI ASTRATTI E INTERFACCE


In Java si definisce un tipo di dati astratto con una interfaccia.
• Come sappiamo, un’interfaccia descrive un comportamento che sarà assunto da una classe
che realizza l’interfaccia, è proprio quello che serve per definire un ADT.
• Un ADT definisce cosa si può fare con una struttura dati che realizza l’interfaccia. La classe
che rappresenta concretamente la struttura dati definisce invece come vengono eseguite le
operazioni.

Un ADT mette in generale a disposizione metodi per svolgere le seguenti azioni (a volte solo
alcune):
• inserimento di un elemento
• rimozione di un elemento
• ispezione degli elementi contenuti nella struttura
• ricerca di un elemento all’interno della struttura

I diversi ADT che vedremo si differenziano per le modalità di funzionamento di queste tre
azioni.
Il package java.util della libreria standard contiene molte definizioni/realizzazioni di ADT
come interfacce e classi.
Noi svilupperemo le nostre definizioni/realizzazioni, introducendo i più comuni ADT a partire
dai più semplici.

UN CONTENITORE GENERICO

public interface Container


{ boolean isEmpty();
void makeEmpty(); }

Container specifica la firma di due soli metodi:


• isEmpty verifica che il contenitore sia vuoto
• makeEmpty svuota il contenitore

Scriveremo i nostri ADT come interfacce di Java che estendono Container.

public interface TipoDatoAstratto extends Container


{ firme metodi di inserimento/rimozione/ispezione (diversi a seconda del tipo di dato
astratto }

• Le interfacce possono essere estese da altre interfacce


• Un’interfaccia eredita i metodi della super-interfaccia
• Una classe che realizza un’interfaccia estesa deve realizzare anche i metodi della sua super-
interfaccia
PILE (STACK)

In una pila (stack) gli oggetti possono essere inseriti ed estratti secondo un
comportamento definito LIFO (Last In, First Out).
• L’ultimo oggetto inserito è il primo ad essere estratto
• il nome è stato scelto in analogia con una pila di piatti
L’unico oggetto che può essere ispezionato è quello che si trova in cima alla pila.
Esistono molti possibili utilizzi di una struttura dati che realizza questo
comportamento.

Le operazioni (metodi) che caratterizzano una pila sono:


• push: inserisce un oggetto in cima alla pila
• pop: elimina l’oggetto che si trova in cima alla pila
• top: ispeziona l’oggetto in cima alla pila senza estrarlo

public interface Stack extends Container


{ void push(Object obj);
Object pop();
Object top();
}

Inoltre una pila (come ogni Container) ha i metodi


• isEmpty per sapere se il contenitore è vuoto
• makeEmpty per svuotare il contenitore

Molto importante: Definiremo tutti gli ADT in modo che possano genericamente contenere
oggetti di tipo Object. Ciò consente di inserire nel contenitore oggetti di qualsiasi tipo (un
riferimento di tipo Object può a qualsiasi oggetto)

PILE NELLA JAVA VIRTUAL MACHINE


Ciascun programma java in esecuzione ha una propria pila chiamata Java Stack che viene
usata per mantenere traccia delle variabili locali, dei parametri formali dei metodi e di altre
importanti informazioni relative ai metodi, man mano che questi sono invocati.

La JVM mantiene quindi uno stack i cui elementi sono descrittori dello stato corrente
dell’invocazione dei metodi (che non sono terminati).

I descrittori sono denominati Frame. A un certo istante durante l’esecuzione, ciascun metodo
sospeso ha un frame nel Java Stack.
Il metodo fool è chiamato dal metodo cool che a sua volta è stato chiamato dal metodo
main.
• Ad ogni invocazione di metodo in run-time viene inserito (push) un frame nello stack
• Ciascun frame dello stack memorizza i valori del program counter, dei parametri e delle
variabili locali di una invocazione a un metodo.
• Quando il metodo chiamato è terminato il frame viene estratto (pop) ed eliminato
• Quando il metodo fool termina la propria esecuzione, il metodo cool continuerà la propria
esecuzione a partire dall’istruzione di indirizzo 217, ottenuto incrementando il valore del PC
contenuto nel proprio frame.

UTILIZZO DI PILE
I browser per internet memorizzano gli indirizzi dei siti visitati recentemente in una struttura
di tipo pila.

Quando l’utente visita un sito, l’indirizzo è inserito (push) nella pila. Il browser permette
all’utente di saltare indietro (pop) al sito precedente tramite il pulsante “indietro”.

Gli editor di testo forniscono generalmente un meccanismo di “undo” che cancella operazioni
di modifica recente e ripristina precedenti stati del testo.
Questa funzione di “undo” è realizzata memorizzando le modifiche in una struttura di tipo
pila.

La JVM usa una pila per memorizzare l’elenco dei metodi in attesa durante l’esecuzione in un
dato istante.

Per evidenziare la potenza della definizione di tipi di dati astratti come interfacce, supponiamo
che qualcuno abbia progettato le seguenti classi

public class StackX implements Stack { ... }

public class StackY implements Stack { ... }

Senza sapere come siano realizzate StackX e StackY, possiamo usare esemplari di queste
classi sfruttando il comportamento astratto definito in Stack.

Esempio utilizzo pile:


public class StackSwapper
{ public static void main(String[] args)
{ Stack s = new StackX();
s.push("Pippo");
s.push("Pluto");
s.push("Paperino");
printAndClear(s);
System.out.println();
s.push("Pippo");
s.push("Pluto");
s.push("Paperino");

printAndClear(swapAndClear(s));
}
private static Stack swapAndClear(Stack s)
{ Stack p = new StackY();
while (!s.isEmpty()) p.push(s.pop());
return p;
}
private static void printAndClear(Stack s)
{ while (!s.isEmpty()) System.out.println(s.pop());
}
}
REALIZZAZIONE DELLA PILA
Per realizzare una pila è facile ed efficiente usare una struttura di tipo array “riempito solo in
parte”.
In fase di realizzazione vanno affrontati due problemi:
1) Cosa fare quando viene invocato il metodo (di inserimento) push nella situazione di array
pieno.
• Una prima soluzione prevede il lancio di un’eccezione
• Una seconda soluzione usa il ridimensionamento dell’array
2) Cosa fare quando vengono invocati i metodi pop (di rimozione) o top (di ispezione) quando
la pila è vuota.
• Una possibile soluzione prevede il lancio di un’eccezione

Definiamo due nuove eccezioni (sono classi): EmptyStackException e FullStackException


Entrambe estendono RuntimeException, quindi chi usa la pila non è obbligato a gestirle.

public interface Stack extends Container


{ ... } //codice come prima
class EmptyStackException extends RuntimeException { }
class FullStackException extends RuntimeException { }

• A cosa serve definire classi vuote?? A definire un tipo di dato che ha le stesse
caratteristiche della propria superclasse, ma un nome diverso.
• In realtà la classe non è vuota, perché contiene tutto ciò che eredita dalla sua superclasse
(con le eccezioni si usa spesso questa tecnica).
• Il nome della classe eccezione specifica il tipo di errore.

PILA SENZA RIDIMENSIONAMENTO:


public class FixedArrayStack implements Stack
{ //costruttore
public FixedArrayStack()
{ v = new Object[INITSIZE];
// per rendere vuota la struttura invochiamo il metodo makeEmpty: è sempre meglio
// evitare di scrivere codice ripetuto
makeEmpty();
}

// dato che Stack estende Container, occorre realizzare anche i suoi metodi
public void makeEmpty()
{ vSize = 0; }
public boolean isEmpty()
{ return (vSize == 0); }
public void push(Object obj)
{ if (vSize == v.length) throw new FullStackException();
v[vSize++] = obj;
}
public Object top()
{ if (isEmpty()) throw new EmptyStackException();
return v[vSize - 1];
}
public Object pop()
{ Object obj = top();//top fa controllo di pila vuota
vSize--;
return obj;
}
//campi di esemplare e variabili statiche
protected Object[] v; //array riempito solo in parte
protected int vSize;//ci è comodo usare var. protected
public static final int INITSIZE = 100;
}
PILA CON RIDIMENSIONAMENTO
Non lancia fullStack ma quando l'array che definisce la pila è pieno viene ingrandito

public class GrowingArrayStack implements Stack


{
public void push(Object obj)
{ if (vSize == v.length)
v = resize(v, 2*vSize);
v[vSize++] = obj;
}
... // tutto il resto è identico al codice di FixedArrayStack!
}

Possiamo evitare di riscrivere tutto il codice di FixedArrayStack in GrowingArrayStack?


Per evitare di riscrivere codice usiamo l’ereditarietà:

public class GrowingArrayStack extends FixedArrayStack


{ public void push(Object obj)
{ if (vSize == v.length)
v = resize(2*vSize);
v[vSize++] = obj;
}

protected Object[] resize(int newLength) //solita tecnica


{ if (newLength < v.length)
throw new IllegalArgumentException();
Object[] newv = new Object[newLength];
System.arraycopy(v, 0, newv, 0, v.length);
return newv;
}
}

• Il metodo push sovrascritto fa accesso alle variabili di esemplare v e vSize definite nella
superclasse
• Questo è consentito dalla definizione protected

CAMPI DI ESEMPLARE PROTECTED


• Il progettista della superclasse decide se rendere accessibile in modo protected lo stato
della classe (o una sua parte…)
• È una parziale violazione dell’incapsulamento, ma avviene in modo consapevole ed esplicito.
• Anche i metodi possono essere definiti protected (possono essere invocati soltanto
all’interno della classe in cui sono definiti e delle sue sottoclassi)
• Non bisogna usare variabili di esemplare con accesso protected, se non in casi particolari
• In questo caso la scelta di usare variabili di esemplare protected è motivata da finalità
didattiche, e serve per rendere più semplice (ma meno sicuro) il codice.

GERARCHIA DI CLASSI E INTERFACCE


Abbiamo realizzato la seguente gerarchia di classi e interfacce:

• L’interfaccia Stack estende l’interfaccia Container

• La classe FixedArrayStack implementa l’interfaccia Stack

• La classe GrowingArrayStack estende la classe FixedArrayStack


PRESTAZIONI DEI METODI DI UNA PILA E “ANALISI AMMORTIZZATA”

PRESTAZIONI DEI METODI DI UNA PILA


Si noti che:
• le prestazioni dipendono dalla definizione della struttura dati e non dalla sua interfaccia…
• per valutare le prestazioni è necessario conoscere il codice che realizza le operazioni!
• Il tempo di esecuzione di ogni operazione su una pila senza ridimensionamento (classe
FixedArrayStack) è costante
• Cioè non dipende dalla dimensione n della struttura dati stessa (non ci sono cicli…)
• Quindi ogni operazione su FixedArrayStack è O(1)

Analizziamo i tempi di esecuzione nel caso di pila con ridimensionamento:

• L’unica differenza con FixedArrayStack è l’operazione push


• “Alcune volte” push richiede un tempo O(n) (Perché all’interno del metodo resize è
necessario copiare tutti gli n elementi nel nuovo array)

Osservazione: poiché un array di lunghezza n viene ridimensionato in un array di lunghezza


doppia, un eventuale nuovo ridimensionamento verrà effettuato dopo altre n operazioni di
push.

ANALISI AMMORTIZZATA
Cerchiamo di calcolare il tempo di esecuzione medio di n operazioni di push, delle quali
• n-1 richiedono un tempo O(1)
• una richiede un tempo O(n)

T(n)= [(n-1)*O(1) + O(n)]/n = O(n)/n = O(1)

• Distribuendo il tempo speso per un ridimensiomento su n operazioni di push, si ottiene


quindi ancora O(1)
• Questo metodo di stima del costo medio di una operazione si chiama analisi ammortizzata
delle prestazioni asintotiche
Quindi nonostante ci siano termini che impiegano O(n) nel totale il tempo di esecuzione
medio è O(1).

Push con ridimensionamento ha prestazioni O(1) per qualsiasi costante moltiplicativa usata
per calcolare la nuova dimensione, anche diversa da 2.
• Se invece si usa una costante additiva k, cioè si ridimensiona l'array da n a n+k, allora su
n operazioni di inserimento quelle “lente” sono n/k

Esempio: aumentando la dimensione dell’array di 20 elementi (ovvero k = 20), 5


inserimenti su 100 (o 50 su 1000) richiedono di invocare resize
• Con una costante additiva le prestazioni di push sono:

T(n) = [(n-n/k)*O(1)+(n/k)*O(n)]/n
= [O(n) + n*O(n)]/n
= O(n)/n + O(n)
= O(1) + O(n) = O(n) -> in questo caso il costo medio è O(n).

ANALISI AMMORTIZZATA:
Tecnica di analisi delle prestazioni di un algoritmo. Si usa per mostrare che, in una sequenza
di operazioni, il costo medio di una operazione è piccolo, anche se una singola operazione
della sequenza è “costosa”.
È diversa dall'analisi di caso medio vista in precedenza:
• Analisi di caso medio: si basa su stime statistiche dell'input (riguarda le distribuzioni dei
dati)
• Analisi ammortizzata: fornisce il costo medio di una singola operazione nel caso peggiore
(calcola il costo di n operazioni fratto n)
Ci sono varie tecniche di analisi ammortizzata:
• Analisi aggregata: si stima il tempo T(n) per una sequenza di n operazioni, nel caso
peggiore. Allora il tempo medio per una operazione è T(n)/n
• Noi applichiamo questa tecnica all'analisi dei tempi di esecuzione dei metodi di inserimento
in strutture dati.

PILE DI OGGETTI, PILE DI NUMERI, CLASSI INVOLUCRO

PILE DI OGGETTI E PILE DI NUMERI


L'interfaccia Stack che abbiamo definito può gestire dati (cioè riferimenti ad oggetti) di
qualsiasi tipo
• Non è però in grado di gestire dati dei tipi fondamentali di Java (int, double, char…), che
non sono oggetti e non possono essere assegnati a riferimenti di tipo Object

public interface IntStack extends Container


{ void push(int obj);
int top();
int pop();
}

Per gestire una pila di int potremmo ridefinire tutto.


Svantaggi:
• occorre replicare codice, con poche modifiche
• non esiste più un unico tipo di dati astratto Stack

CLASSI INVOLUCRO
Alternativa: “trasformare” un numero intero (o un altro tipo di dato fondamentale di Java) in
un oggetto.
• Questo è possibile, usando le classi involucro (wrapper)

Esempio: per dati int esiste la classe involucro Integer


Il costruttore accetta un parametro di tipo int e crea un oggetto Integer contenente il valore
int passato come parametro, “avvolto” nella struttura di un oggetto.
• gli oggetti di tipo Integer sono immutabili
• per conoscere il “valore” di un oggetto di tipo Integer si usa il metodo intValue, che
restituisce valori di tipo int

Integer iObj = new Integer(2); //avvolge l'int 2 in un oggetto


Object obj = iObj; //lecito, ovviamente
int x = intObj.intValue(); //x vale 2

Esistono classi involucro per tutti i tipi di dati fondamentali, con i nomi uguali al nome del tipo
corrispondente ma iniziale maiuscola (eccezioni alla regola: Integer e Character)
• Boolean, Byte, Character, Short, Integer, Long, Float, Double
• Metodi: booleanValue( ), charValue( ), doubleValue( ),ecc.

• Tutte si trovano in java.lang e realizzano Comparable

ESEMPI DI USO DI CLASSI INVOLUCRO


double d1 = 3.5; //tipo fondamentale
Double dObj1 = new Double(3.5); //oggetto che incapsula double
Double dObj2 = 3.5; //auto-boxing: lecito a partire da java 5.0
double d2 = dObj2.doubleValue(); //restituisce valore double
double d3 = dObj1; //auto-unboxing: lecito ... da java 5.0
ESTRAGGO IL VALORE NUMERICO DALL'OGGETTO DI TIPO DOUBLE E LO INSERISCO NELLA
VARIABILE.
ESTRARRE OGGETTI DA STRUTTURE DATI

Le strutture dati generiche, definite in termini di Object sono molto comode perché possono
contenere oggetti di qualsiasi tipo.
• Quando si effettuano estrazioni o ispezioni di oggetti in esse contenuti viene restituito un
riferimento di tipo Object, qualunque sia il tipo effettivo dell'oggetto
• Bisogna usare un cast per ottenere un riferimento del tipo originario
• Operazione potenzialmente pericolosa perché se il cast non è permesso viene lanciata
ClassCastException

Stack st = new GrowingArrayStack();


...
Object obj = st.pop();
Character ch = (Character)obj; //possibile lancio di eccezione

Ricordiamo che le eccezioni la cui gestione non è obbligatoria, come ClassCastException,


possono comunque essere gestite!

try
{ Character ch = (Character)st.pop(); }
catch (ClassCastException e)
{ // gestione dell'errore }

• In alternativa si può usare l’operatore instanceof

Object obj = st.pop();


if (obj instanceof Character)
Character ch = (Character)obj;
else
// gestione dell'errore

ESERCIZIO: CALCOLATRICE
Vogliamo risolvere il seguente problema:
• calcolare il risultato di una espressione aritmetica (ricevuta come String) contenente
somme, sottrazioni, moltiplicazioni e divisioni.
• Se l’espressione usa la classica notazione infissa (in cui i due operandi di un’operazione si
trovano ai due lati dell’operatore) l’ordine di esecuzione delle operazioni è determinato dalle
regole di precedenza tra gli operatori e da eventuali parentesi.
• Scrivere un programma per tale compito è piuttosto complesso, mentre è molto più facile
calcolare espressioni che usano una diversa notazione.

NOTAZIONE POSTFISSA:
Usiamo una notazione postfissa detta notazione polacca inversa (RPN, Reverse Polish
Notation).
• Non sono ammesse parentesi (e non sono necessarie)
• Nella stringa che rappresenta l'espressione aritmetica, gli operandi sono scritti alla sinistra
dell'operatore
• Ogni volta che nella stringa si incontra un operatore, si esegue la corrispondente operazione
sui due numeri che lo precedono (Possono essere numeri scritti nella stringa di partenza
oppure possono essere numeri risultanti da un'operazione eseguita precedentemente).
Esiste un semplice algoritmo che usa una pila per valutare un’espressione in notazione
postfissa:
• Finché l’espressione non è terminata
• leggi da sinistra il primo simbolo dell'espressione non letto
• se è un valore numerico, inseriscilo sulla pila
• altrimenti (è un operatore…)
• estrai dalla pila l’operando destro
• estrai dalla pila l’operando sinistro
• esegui l’operazione
• inserisci il risultato sulla pila
• Se (al termine) la pila contiene più di un valore, l’espressione contiene un errore
• L’unico valore presente sulla pila è il risultato.

import java.util.Scanner;
public class RPNTester
{ public static void main(String[] args)
{ System.out.println("Inserisci operazione. Una stringa su una riga,");
System.out.println("solo numeri e operatori +-*/ separati da spazi");
Scanner in = new Scanner(System.in);
String rpnString = in.nextLine();
try
{ System.out.println("Risultato: " + evaluateRPN(rpnString)); }

catch(NumberFormatException e)
{ System.out.println("Uso di simboli non permessi!"); }
catch(EmptyStackException e)
{ System.out.println("Troppi operatori nell'espressione");}
catch(IllegalStateException e)
{ System.out.println("Troppi numeri nell'espressione");}
}

private static double evaluateRPN(String rpnString)


throws EmptyStackException, NumberFormatException, IllegalStateException
{ Stack st = new GrowingArrayStack();
Scanner linescan = new Scanner(rpnString);
while (linescan.hasNext())
{ String s = linescan.next();
if (isOperator(s)) // il token s e` un operatore
{ Double r = evalOp(s, (Double)st.pop(),(Double)st.pop());
st.push(r);
}
else // il token s non e` un operatore
st.push(Double.parseDouble(s)); //dovrebbe essere un numero
}
double result = (Double)st.pop();
if (!st.isEmpty()) throw new IllegalStateException();
return result;
}

private static boolean isOperator(String s) // verifica se il il token s e` un operatore


{ return s.equals("+")|| s.equals("-")|| s.equals("*")|| s.equals("/"); }

// calcola il risultato dell'operazione "left op right"


private static double evalOp(String op, double right, double left)
{ if (op.equals("+")) return left + right;
if (op.equals("-")) return left - right;
if (op.equals("*")) return left * right;
return left / right;
}
}
CODE (QUEUE)

In una coda (queue) gli oggetti possono essere inseriti ed estratti


secondo un comportamento definito FIFO (First In, First Out)
• il primo oggetto inserito è il primo ad essere estratto
• il nome suggerisce l'analogia con una coda di persone
L’unico oggetto che può essere ispezionato è il primo oggetto della coda.

Esistono molti possibili utilizzi di una coda:


• Simulazione del funzionamento di uno sportello bancario
• File da stampare vengono inseriti in una coda di stampa

CODA CIRCOLARE
Spesso si utilizza una coda secondo una modalità circolare: gli elementi vengono estratti
dalla prima posizione, “serviti”, e reinseriti in ultima posizione
• Esempio: lo scheduler di un sistema operativo assegna le risorse della CPU a molti processi
attivi in parallelo (Politica round robin: la CPU esegue una porzione del processo che ha
atteso più a lungo di essere “servito”, che poi viene reinserito in ultima posizione).

Le operazioni (metodi) che caratterizzano una coda sono


• enqueue: inserisce un oggetto nella coda
• dequeue: elimina il dalla coda l’oggetto inserito per primo
• getFront: esamina il primo oggetto, senza estrarlo

public interface Queue extends Container


{ void enqueue(Object obj);
Object dequeue();
Object getFront();
}

Si notino le similitudini con i metodi di una pila


• enqueue corrisponde a push
• dequeue corrisponde a pop
• getFront corrisponde a top

Come ogni ADT di tipo “contenitore”, la coda ha i metodi


• isEmpty per sapere se il contenitore è vuoto
• makeEmpty per vuotare il contenitore

REALIZZARE UNA CODA


Per realizzare una pila è facile ed efficiente usare una struttura di tipo array “riempito solo in
parte”. In fase di realizzazione vanno affrontati due problemi:
• Come gestire le condizioni di coda piena e coda vuota
• Definiamo EmptyQueueException e FullQueueException

public interface Queue extends Container


{ ... } //codice come prima
class EmptyQueueException extends RuntimeException { }
class FullQueueException extends RuntimeException { }
• Come realizzare inserimento ed estrazione di elementi ai due diversi estremi dell’array
• Nella pila si inseriscono e si estraggono elementi allo stesso estremo dell’array (estr DX)
• Decidiamo di inserire a destra ed estrarre a sinistra

LA CLASSE SlowFixedArrayQueue

public class SlowFixedArrayQueue implements Queue


{ public SlowFixedArrayQueue()
{ v = new Object[INITSIZE];
makeEmpty();
}
public void makeEmpty()
{ vSize = 0;
}
public boolean isEmpty()
{ return (vSize == 0);
}
public void enqueue(Object obj)
{ if (vSize == v.length) throw new FullQueueException();
v[vSize++] = obj;
}
public Object getFront()
{ if (isEmpty()) throw new EmptyQueueException();
return v[0];
}
public Object dequeue()
{ Object obj = getFront();
vSize--;
for (int i = 0; i < vSize; i++) v[i] = v[i+1];
return obj;
}
//campi di esemplare e variabili statiche
private Object[] v;
private int vSize;
public static final int INITSIZE = 100;
}

MIGLIORARE IL METODO DEQUEUE


Questa realizzazione, molto efficiente per la pila, è al contrario assai inefficiente per la coda:
il metodo dequeue è O(n) perché bisogna spostare tutti gli oggetti della coda per
“ricompattare” l’array.
• Ciò avviene perché inserimenti e rimozioni nella coda avvengono alle due diverse estremità
dell’array, mentre nella pila avvengono alla stessa estremità
• Per migliorare l'efficienza servono due indici: un indice punta al primo oggetto della coda e
l’altro indice punta alla prima posizione libera nella coda
• Aggiornando opportunamente gli indici si realizza una coda con un “array riempito solo nella
parte centrale”, in cui tutte le operazioni sono O(1).

ARRAY RIEMPITO NELLA PARTE CENTRALE


Si usano due indici anziché uno soltanto:

• Indice front: punta al primo


elemento nella coda

• Indice back: punta al primo posto


libero dopo l’ultimo elemento nella coda

Il numero di elementi è (back – front), in particolare quando front == back l'array è vuoto.
LA CLASSE FixedArrayQueue

public class FixedArrayQueue implements Queue


{
public FixedArrayQueue()
{ v = new Object[INITSIZE];
makeEmpty();
}
public void makeEmpty()
{ front = back = 0;
}
public boolean isEmpty()
{ return (back == front);
}
public void enqueue(Object obj)
{ if (back == v.length) throw new FullQueueException();
v[back++] = obj;
}
public Object getFront()
{ if (isEmpty()) throw new EmptyQueueException();
return v[front];
}
public Object dequeue()
{ Object obj = getFront();
front++; //attenzione all’effetto di questo incremento
return obj;
}

//campi di esemplare e variabili statiche


protected Object[] v;
protected int front, back;
public static final int INITSIZE = 100;
}

CODA RIDIMENSIONABILE
Per rendere la coda ridimensionabile, usiamo la stessa strategia vista per la pila:
• Estendiamo la classe FixedArrayQueue e sovrascriviamo il solo metodo enqueue.
Tutte le operazioni continuano ad avere la massima efficienza: sono O(1).

public class GrowingArrayQueue extends FixedArrayQueue


{
public void enqueue(Object obj)
{ if (back == v.length)
v = resize(2*v.length);
v[back++] = obj;
}

protected Object[] resize(int newLength)


{ ... } // stesso codice gia` scritto in GrowingArrayStack
}
CODA CON ARRAY CIRCOLARE

La realizzazione vista ha ancora un punto debole:


• Se l’array è lungo n, effettuare n operazioni enqueue e n operazioni dequeue (anche non
consecutive) produce front=back=n, ovvero la coda è vuota. Ma al successivo enqueue
l’array sarà pieno (perché back=n): lo spazio di memoria non viene riutilizzato!

Questo problema può essere risolto usando una struttura detta “array circolare”
• I due indici possono, una volta giunti alla fine dell’array, ritornare all’inizio se si sono
liberate delle posizioni.
• L’array circolare è pieno quando la coda contiene n-1 oggetti (e non n).
Si “spreca” quindi un elemento dell'array: ciò è necessario per distinguere la condizione di
coda vuota (front==back) dalla condizione di coda piena.
• le prestazioni temporali rimangono identiche

non va più bene scrivere back++


ma devo fare in modo che back
dopo essere arrivato alla fine
torni all'inizio!!

LA CLASSE FixedCircularArrayQueue

public class FixedCircularArrayQueue extends FixedArrayQueue


{
// il metodo increment fa avanzare un indice di una posizione, tornando all’inizio se si supera la fine.
// Attenzione: non aggiorna direttamente i campi front,back.

protected int increment(int index) //invece di back++


{ return (index + 1) % v.length; //il rapporto è 1 quindi il resto è 0
} //restituisce sempre 0 o index+1

public void enqueue(Object obj) //SOVRASCRITTO!


{ if (increment(back) == front) //condizione di coda piena back=front-1
throw new FullQueueException();
v[back] = obj;
back = increment(back);
}

public Object dequeue() //SOVRASCRITTO!


{ Object obj = getFront();
front = increment(front);
return obj;
}

// non serve sovrascrivere getFront perche` non modifica le variabili back e front
}
RIDIMENSIONARE UN ARRAY CIRCOLARE

Vogliamo estendere FixedCircularArrayQueue in maniera tale che l’array contenente i dati


possa essere ridimensionato quando la coda è piena.

• Effettuiamo un resize come su un array ordinario.

In generale però la zona utile della coda è attorno alla sua fine (ovvero back < front): c’è un
problema in più

LA CLASSE GrowingCircularArrayQueue

public class GrowingCircularArrayQueue extends FixedCircularArrayQueue


{
public void enqueue(Object obj)
{ if (increment(back) == front) //back e front distano 1
{ v = resize(2*v.length); //dopo il resize ancora array pieno
//se si ridimensiona v e la zona utile della coda e`attorno alla sua fine (cioe` back < front)
la seconda meta` del nuovo array rimane vuota e provoca un malfunzionamento della coda,
che si risolve spostando la parte di coda che si trova all’inizio dell’array
if (back < front)
{ System.arraycopy(v, 0, v, v.length/2, back); //crea un array
back += v.length/2; //dal posto 0 del primo copio back elementi da
} //v.lenght/2 del secondo. Poi sposto back alla fine
}
v[back] = obj;
back = increment(back);
}

protected Object[] resize(int newLength)


{ ... } // solito codice
}
GERARCHIA DI CLASSI E INTERFACCE

L'ADT (“CODA DOPPIA”) -cenni

CODA DOPPIA
In una coda doppia (deque) gli oggetti possono essere inseriti
ed estratti ai due estremi di una disposizione lineare, cioè
all'inizio e alla fine.

Inoltre è consentita l'ispezione dei due oggetti presenti alle due estremità.
Si parla di double-ended queue, ovvero di “coda con due estremità terminali”.

Tradizionalmente la definizione viene abbreviata con la parola deque (dove le prime due
lettere sono le iniziali di “double-ended”), pronunciata come deck (per evitare confusione con
il metodo dequeue).

public interface Deque extends Container


{ void addFirst(Object obj); // inserimento ai due capi
void addLast(Object obj);
Object removeFirst(); // rimozione ai due capi
Object removeLast();
Object getFirst(); // ispezione ai due capi
Object getLast();
int size(); // dimensione della deque
}

// solite eccezioni per contenitore pieno/vuoto


class EmptyDequeException extends RuntimeException { }
class FullDequeException extends RuntimeException { }

Può essere realizzata con array, in particolare array circolare ridimensionabile

• Tutti i metodi hanno prestazioni O(1) (in senso ammortizzato per i metodi di inserimento
con ridimensionamento dell'array)

PILE E CODE TRAMITE DEQUE


Una coda doppia può essere utilizzata per realizzare pile e code:
MAPPE E DIZIONARI

MAPPE: definizione
Una mappa è un ADT con le seguenti proprietà
• Contiene dati (non in sequenza) che sono
coppie di tipo chiave/valore(oggetti)
• Non può contenere coppie con identica
chiave: ogni chiave deve essere unica
nell’insieme dei dati memorizzati
• Consente di inserire nuove coppie
chiave/valore
• Consente di effettuare ricerca e rimozione di
valori usando la chiave come identificatore

DIZIONARI: definizione
L'ADT dizionario ha molte similitudini con l'ADT mappa:
• Valgono tutte le proprietà dell'ADT mappa, tranne una -> Non si richiede che le chiavi siano
uniche nel dizionario

C'è analogia con un dizionario di uso comune, in cui


• le chiavi sono le singole parole
• I valori sono le definizioni delle parole nel dizionario
• le chiavi (parole) possono essere associate a più valori (definizioni) e quindi
comparire più volte nel dizionario. Non serve che le chiavi siano uniche.
• la ricerca di un valore avviene tramite la sua chiave

Si distinguono dizionari ordinati e dizionari non-ordinati:


• A seconda che sull'insieme delle chiavi sia o no definita una relazione totale di ordinamento,
cioè (in Java) che le chiavi appartengano ad una classe che implementa Comparable

La nostra trattazione è limitata ad un caso ben preciso


• Dizionari ordinati a chiave unica (cioè mappe)

L’interfaccia Dictionary

public interface Dictionary extends Container


{
// l'inserimento deve andare sempre a buon fine; se la chiave non
// esiste la coppia viene aggiunta al dizionario. Se esiste,
// il valore ad essa associato viene sovrascritto dal nuovo
// valore; se key e` null si lancia IllegalArgumentException

void insert(Comparable key, Object value);


// la rimozione della chiave rimuove anche il corrispondente
// valore dal dizionario. Se la chiave non esiste si lancia
// DictionaryItemNotFoundException

void remove(Comparable key);


// la ricerca per chiave restituisce solo il valore ad essa
// associato nel dizionario. Se la chiave non esiste si
// lancia DictionaryItemNotFoundException

Object find(Comparable key);


}

//Eccezione che segnala il mancato ritrovamento di una chiave


class DictionaryItemNotFoundException extends RuntimeException
{... }
IMPLEMENTARE DICTIONARY CON ARRAY

Un dizionario può essere realizzato usando la struttura dati array:


• Ogni cella dell’array contiene un riferimento ad una coppia chiave/valore
• La coppia chiave/valore sarà un oggetto di tipo Pair (da definire)

Generalmente si usa un array riempito solo in parte. A seconda degli ambiti applicativi ci sono
due strategie possibili:
• mantenere le chiavi ordinate nell’array
• mantenere le chiavi non ordinate nell’array

A seconda della strategia scelta, cambiano le prestazioni dei metodi del dizionario.

•dizionario con array ordinato

Se le n chiavi vengono conservate ordinate nell’array:


• La ricerca ha prestazioni O(log n). Perché si può usare la ricerca per bisezione.
• La rimozione ha prestazioni O(n). Perché bisogna effettuare una ricerca, e poi spostare
mediamente n/2 elementi per mantenere l’ordinamento.
• L’inserimento ha prestazioni O(n). Perché si può usare l’ordinamento per inserimento in
un array ordinato. Usando un diverso algoritmo occorre riordinare l’intero array, con
prestazioni almeno O(n log n).

•dizionario con array non ordinato

Se le n chiavi vengono conservate non ordinate


• La ricerca ha prestazioni O(n). Bisogna usare la ricerca lineare
• La rimozione ha prestazioni O(n). Bisogna effettuare una ricerca (lineare), e poi spostare
nella posizione trovata l'ultimo elemento dell'array (l’ordinamento non interessa)
• L’inserimento ha prestazioni O(n). Bisogna rimuovere (sovrascrivere) un elemento con la
stessa chiave, se c'è, e poi inserire il nuovo elemento nella ultima posizione dell’array
(l’ordinamento non interessa) [Se non si richiede che le chiavi siano uniche nel dizionario, la
rimozione non è necessaria e l'inserimento è O(1) ]

PRESTAZIONI DI UN DIZIONARIO

La scelta di una particolare realizzazione dipende dall’utilizzo tipico del dizionario


nell’applicazione
• Se si fanno frequenti inserimenti e sporadiche ricerche e rimozioni la scelta migliore è
l’array non ordinato
• Se il dizionario viene costruito una volta per tutte, poi viene usato soltanto per fare ricerche
la scelta migliore è l’array ordinato
REALIZZAZIONE DI UN DIZIONARIO

LA CLASSE PAIR
Un dizionario contiene elementi formati da coppie Chiave – Valore.
• Per realizzare un dizionario tramite array, dobbiamo allora realizzare una classe Pair, che
definisce i generici elementi di un dizionario

L'array contenente gli elementi del dizionario sarà un array di tipo Pair[ ]. Oggetti di tipo Pair
devono avere:
• Due campi di esemplare, key (di tipo Comparable perchè trattiamo dizionari ordinati) e
value (di qualsiasi tipo, ovvero di tipo Object)
• Metodi di accesso e modificatori per questi campi di esemplare

public class Pair


{
public Pair(Comparable key, Object value)
{ setKey(key);
setValue(value); }

public String toString() //metodi pubblici


{ return key + " " + value; }

public Comparable getKey()


{ return key; }

public Object getValue()


{ return value; }

public void setKey(Comparable key)


{ this.key = key; }

public void setValue(Object value)


{ this.value = value; }

private Comparable key; //campi di esemplare


private Object value;
}

CLASSI INTERNE

Osserviamo che la classe Pair, usata dal dizionario, non viene mai usata al di fuori del
dizionario stesso:
• I metodi dell'interfaccia Dictionary non restituiscono mai riferimenti a Pair e non ricevono
mai parametri espliciti di tipo Pair
• Per il principio dell’incapsulamento sarebbe preferibile che questa classe e i suoi dettagli
non fossero visibili all’esterno della catena. In questo modo una modifica della struttura
interna del dizionario e/o della classe Pair non avrebbe ripercussioni sul codice scritto da chi
usa il dizionario.

Il linguaggio Java consente di definire classi all’interno di un’altra classe, tali classi si
chiamano classi interne (inner classes).

A noi interessa solo esaminare la seguente sintassi (che è lecita in Java):

public class ClEsterna


{ ... //costruttori, metodi, campi di esemplare, variabili statiche della classe esterna
<tipoaccesso> class ClInterna //<tipoaccesso> puo`anche essere private!
{ ... //costruttori, metodi, campi di esemplare, variabili statiche della classe interna }
}
COMPILARE CLASSI INTERNE

Quando compiliamo classi contenenti classi interne:


• Il compilatore traduce una classe interna in un normale file di bytecode diverso dal file di
bytecode della classe esterna
• Il file di bytecode della classe interna ha un nome dal formato particolare

Ad esempio, se compiliamo ClEsterna con la classe interna ClInterna, troviamo nella nostra
cartella:
• Il file di bytecode ClEsterna.class (come al solito...)
• Il file di bytecode ClEsterna$ClInterna.class
Solitamente si definisce una classe come interna se essa descrive un tipo logicamente
correlato a quello della classe esterna

vantaggi e limitazioni sull'uso di classi interne


• Vantaggi: le due classi, interna ed esterna, condividono una “relazione di fiducia”. Ciascuna
delle due classi ha accesso a tutti i metodi, campi di esemplare e statici dell'altra, anche se
private
• Limitazioni: un oggetto di ClInterna è sempre associato ad un oggetto di ClEsterna. Ovvero
si possono creare oggetti di tipo ClInterna solo dentro metodi non statici di ClEsterna

La classe interna può essere resa inaccessibile al codice scritto in altre classi

USO DI CLASSI INTERNE

• Primo caso: dentro ClEsterna

public class ClEsterna


{ //metodi della classe esterna
public ClInterna metEsterno()
{ ClInterna obj = new ClInterna();
//LECITO
obj.campointerno = 2;
//LECITO: anche se il campo e`privato in ClInterna
return obj;
}

//campi di esemplare della classe esterna


private double campoEsterno;

//definizione di classe interna


public class ClInterna //puo` anche essere private!
{ //metodi della classe interna
public void metInterno()
{ ClEsterna obj = new ClEsterna();
//LECITO
double a = campoEsterno;
//LECITO: anche se il campo e` privato in ClEsterna
}
//campi di esemplare della classe interna
private int campointerno;
}
}
• Secondo caso: in una classe diversa da ClEsterna.

•Il nome della classe interna va sempre qualificato rispetto al nome della classe esterna
• Non si può usare la sintassi ClInterna, bisogna usare la sintassi ClEsterna.ClInterna
• Non è mai possibile creare oggetti di tipo ClInterna
• Se ClInterna è public, allora è possibile definire variabili oggetto di tipo ClInterna
• Se ClInterna è private, allora è “inaccessibile” da codice che non sia scritto in ClEsterna
[In questo modo si protegge il codice da ogni possibile violazione dell'incapsulamento]

public class ClInterneTester


{ public static void main(String[] args)
{
ClEsterna e = new ClEsterna(); //tutto ok
ClEsterna.ClInterna obj = new ClEsterna.ClInterna();
//MAI LECITO: non si possono creare ogg. di ClInterna qui
Clinterna i = e.metEsterno();
//MAI LECITO: il tipo deve essere "ClEsterna.ClInterna"
ClEsterna.ClInterna i = e.metEsterno();
//LECITO SOLO SE ClInterna e` public in ClEsterna
(e.metEsterno()).metInterno();
//LECITO SOLO SE sia ClInterna che il metodo sono public
(e.metEsterno()).campointerno = 1;
//LECITO SOLO SE sia ClInterna che il campo sono public
e.metEsterno();
//SEMPRE LECITO (ma inutile se ClInterna e` private)
}
}

LA CLASSE INTERNA PAIR

public class ArrayDictionary implements Dictionary


{ ...
protected class Pair //classe interna ad ArrayDictionary
{
public Pair(Comparable key, Object value)
{ setKey(key);
setValue(value); }

//metodi pubblici
public String toString()
{ return key + " " + value; }
public Comparable getKey()
{ return key; }
public Object getValue()
{ return value; }
public void setKey(Comparable key)
{ this.key = key; }
public void setValue(Object value)
{ this.value = value; }

//campi di esemplare
private Comparable key;
private Object value;
}
...
}
LA CLASSE ARRAYDICTIONARY

public class ArrayDictionary implements Dictionary


{
public ArrayDictionary()
{ v = new Pair[INITSIZE]; // ... sempre uguale
MakeEmpty(); }

public boolean isEmpty()


{ return vSize == 0; } // ... sempre uguale

public void makeEmpty()


{ vSize = 0; } // ... sempre uguale

public String toString()


{ String s = "";
for (int i = 0; i < vSize; i++)
s = s + v[i] + "\n";
return s; }

public void insert(Comparable key, Object value)


{ if (key == null) throw new IllegalArgumentException();
try
{ remove(key); } //elimina elemento se gia` presente
catch (DictionaryItemNotFoundException e)
{} //... ovvero sovrascrive elemento se gia` presente
if (vSize == v.length) v = resize(2*vSize);
v[vSize++] = new Pair(key, value);
}
protected Pair[] resize(int newLength) //metodo ausiliario
{ ... } //solito codice

public void remove(Comparable key)


{ v[linearSearch(key)] = v[--vSize]; }

public Object find(Comparable key)


{ return v[linearSearch(key)].getValue(); }

private int linearSearch(Comparable key) //metodo ausiliario


{ for (int i = 0; i < vSize; i++)
if (v[i].getKey().compareTo(key) == 0)
//o if (v[i].getKey().equals(key)), se il metodo equals e` stato realizzato correttamente
return i;
throw new DictionaryItemNotFoundException();
}

//campi di esemplare
protected Pair[] v;
protected int vSize;
protected final static int INITSIZE = 10;

protected class Pair


{ ... } // codice della classe Pair
DIZIONARIO CON ARRAY ORDINATO
Avendo usato un array non ordinato, i metodi remove e find effettuano una ricerca lineare
sulle chiavi

Esercizio: realizzare un dizionario con un array ordinato


• Il metodo insert deve mantenere ordinato l'array ad ogni inserimento (usando
insertionSort...)
• I metodi remove e find possono usare la ricerca binaria per trovare una chiave
• Il metodo remove deve ricompattare l'array dopo la rimozione, mantenendolo ordinato

public class SortedArrayDictionary extends ArrayDictionary


{ // realizzazione con array non ordinato. Eredita campi di esemplare e variabili statiche,
// la classe Pair, i metodi isEmpty, makeEmpty, resize.
// Deve sovrascrivere i metodi insert, remove, find }

COLLAUDO DI UN DIZIONARIO

import java.util.Scanner; import java.io.*;

public class SimpleDictionaryTester


{ public static void main(String[] args) throws IOException
{ //creazione dizionario: leggo dati da file e assumo che
//il file abbia righe nel formato <numero int> <stringa>
Scanner infile =new Scanner(new FileReader("file.txt"));
Dictionary dict = new ArrayDictionary(); //oppure = new SortedArrayDictionary();
while (infile.hasNextLine())
{ Scanner linescan = new Scanner(infile.nextLine());
int key = Integer.parseInt(linescan.next());
String value = linescan.next();
dict.insert(key,value); //inserisco chiave e valore
}
infile.close();

Scanner in = new Scanner(System.in); //ricerca/rimozione dati nel dizionario


boolean done = false;
while (!done)
{ System.out.println("**** Stampa dizionario ****");
System.out.println(dict +"\nF=find,R=remove,Q=quit");
String cmd = in.nextLine();
if (cmd.equalsIgnoreCase("Q"))
done = true;
else if (cmd.equalsIgnoreCase("F"))
{ System.out.println("Chiave da trovare?");
int key = Integer.parseInt(in.nextLine());
try //cerca key chiave e restituisce il valore
{ String value = (String)dict.find(key);
System.out.println("Valore: " + value); }
catch(DictionaryItemNotFoundException e)
{ System.out.println("Chiave non trovata");}
}
else if (cmd.equalsIgnoreCase("R"))
{ System.out.println("Chiave da rimuovere?");
int key = Integer.parseInt(in.nextLine());
try //rimuove la coppia identificata da key
{ dict.remove(key);
System.out.println("Chiave rimossa"); }
catch(DictionaryItemNotFoundException e)
{ System.out.println("Chiave non trovata");}
}
}}}
INSIEMI

IL TIPO DI DATO ASTRATTO INSIEME (SET)

È un contenitore (eventualmente vuoto) di oggetti distinti (cioè non contiene


duplicati), senza alcun particolare ordinamento o memoria dell’ordine in cui gli oggetti sono
inseriti/estratti.
• Corrisponde alla nozione matematica di insieme

Definiamo la nostra astrazione di insieme tramite le seguenti operazioni:


• inserimento di un oggetto
[fallisce silenziosamente se l’oggetto è già presente]
• verifica della presenza di un oggetto
• ispezione di tutti gli oggetti
[restituisce un array (in generale non ordinato) di riferimenti agli oggetti dell’insieme]

• Non definiamo un’operazione di rimozione


[useremo l'operazione di sottrazione tra insiemi (cfr. più avanti)]

OPERAZIONI SUGLI INSIEMI


Per due insiemi A e B, si definiscono le operazioni

• unione, A ∪ B
appartengono all’unione di due insiemi tutti e soli gli oggetti che appartengono ad almeno
uno dei due insiemi.
• intersezione, A ∩ B
appartengono all’intersezione di due insiemi tutti e soli gli oggetti che appartengono ad
entrambi gli insiemi.
• sottrazione, A - B (oppure anche A \ B)
appartengono all’insieme sottrazione tutti e soli gli oggetti che appartengono ad A e non
appartengono a B [non è necessario che B sia un sottoinsieme di A].

INSIEME CON ARRAY NON ORDINATO

Scriviamo innanzitutto l’interfaccia Set:

public interface Set extends Container


{
void add(Object obj);
boolean contains(Object obj);
Object[] toArray();
}

La classe ArraySet ha questa interfaccia pubblica

public class ArraySet implements Set


{
public void makeEmpty() { }
public boolean isEmpty() { return true; }
public void add(Object x) { }
public boolean contains(Object x) { return true; }
public Object[] toArray() { return null; }
}

• Abbiamo scritto enunciati return per metodi che non restituiscono void, in questo modo la
classe si compila da subito.
LA CLASSE ArraySet

public class ArraySet implements Set


{
public ArraySet()
{ v = new Object[INITSIZE];
vSize = 0;}

public void makeEmpty()


{ vSize = 0; }
public boolean isEmpty()
{ return (vSize == 0); }

public void add(Object x) //prestazioni O(n) (usa contains)


{ if (contains(x)) return;
if (vSize == v.length) v = resize(2*vSize);
v[vSize++] = x; }

public boolean contains(Object x) //metodo con prestaz. O(n)


{ for (int i = 0; i < vSize; i++)
if (v[i].equals(x)) return true; //non si puo` usare
return false; } //compareTo perche` x e` solo un Object

public Object[] toArray() //metodo con prestaz. O(n).


{ Object[] x = new Object[vSize]; //Creiamo un nuovo array
System.arraycopy(v, 0, x, 0, vSize); //altrimenti si viola l’incapsulamento
return x; }

private Object[] resize(int n) { ... } //solito codice

//campi di esemplare e var. statiche


private Object[] v;
private int vSize;
private static int INITSIZE = 100;
}

OPERAZIONI SUGLI INSIEMI

• unione

public static Set union(Set s1, Set s2)


{
Set x = new ArraySet();
Object[] v = s1.toArray(); //inseriamo gli elementi del primo insieme
for (int i = 0; i < v.length; i++)
x.add(v[i]);

v = s2.toArray(); //inseriamo tutti gli elementi del secondo insieme,


for (int i = 0; i < v.length; i++) //sfruttando le proprietà di add (niente duplicati)
x.add(v[i]);
return x;
}

• Prestazioni:
se contains è O(n) (e, quindi, lo è anche add), questa operazione è O(n2)
• intersezione

public static Set intersection(Set s1, Set s2)


{ Set x = new ArraySet();
Object[] v = s1.toArray();
for (int i = 0; i < v.length; i++)
if (s2.contains(v[i]))
x.add(v[i]);
// inseriamo solo gli elementi che appartengono anche al secondo insieme, sfruttando le
proprieta’ di add (niente duplicati...)
return x;
}

• Prestazioni:
se contains è O(n) l’operazione di intersezione è O(n2).

• sottrazione

public static Set subtract(Set s1, Set s2)


{
Set x = new ArraySet();
Object[] v = s1.toArray();
for (int i = 0; i < v.length; i++)
if (!s2.contains(v[i]))
x.add(v[i]);
// inseriamo solo gli elementi che *non* appartengono al secondo insieme, sfruttando le
proprieta’ di add (niente duplicati...)
return x;
}

• Prestazioni:
se contains è O(n) l’operazione di intersezione è O(n2).

Riassumendo, realizzando un insieme con un array non ordinato:


• le prestazioni di tutte le operazioni primitive dell’insieme sono O(n)
• le prestazioni di tutte le operazioni che agiscono su due insiemi sono O(n2)
• Si può facilmente verificare che si ottengono le stesse prestazioni realizzando
l’insieme con una catena (LinkedListSet)

INSIEMI ORDINATI

ESERCIZIO: insieme di dati ordinabili


Cerchiamo di capire se si possono avere prestazioni migliori quando l’insieme contiene dati
ordinabili: • Definiamo l’interfaccia “insieme ordinato”

public interface Set extends Container


{
void add(Object obj);
boolean contains(Object obj);
Object[] toArray();
}
public interface SortedSet extends Set
{
void add(Comparable obj);
Comparable[] toSortedArray();
}
Realizziamo SortedSet usando un array ordinato
• dovremo definire due metodi add, uno dei quali impedisce l’inserimento di dati non
ordinabili

public class ArraySortedSet implements SortedSet


{ public ArraySortedSet()
{ v = new Comparable[INITSIZE]; vSize = 0; }

public void makeEmpty()


{ vSize = 0; }
public boolean isEmpty()
{ return (vSize == 0); }

public void add(Object x) //metodo di Set


{ throw new IllegalArgumentException(); }

public void add(Comparable x) //prestazioni O(n)


{ ... } //Da completare: riordinamento per inserimento
//E` O(n), perche' inseriamo in un array ordinato
public boolean contains(Object x) //prestaz. O(log n)
{ ... } //da completare: usare ricerca binaria e compareTo
public Comparable[] toSortedArray() //prestaz. O(n)
{ ... } //da completare (v e’ gia` ordinato...)
public Object[] toArray() //come sopra: l'array non deve
{ return toSortedArray(); } //essere per forza disordinato
private Comparable[] resize(int newLength) //solito metodo
{ ... } // da completare

//campi di esemplare e variabili statiche


private Comparable[] v;
private int vSize;
private static int INITSIZE = 100;
}

OPERAZIONI SU INSIEMI ORDINATI

• Gli algoritmi di unione, intersezione, sottrazione per insiemi generici possono essere
utilizzati anche per insiemi ordinati [infatti, un SortedSet è anche un Set].

Quale è la complessità dell'algoritmo di unione?


• Rimane O(n2) perché il metodo add è rimasto O(n), a causa del ri-ordinamento (con
insertionSort) dell’array

Sfruttiamo ciò che sappiamo delle realizzazioni di add e toSortedArray nella classe
ArraySortedSet:
• l’array ottenuto con il metodo toSortedArray è ordinato
• l’inserimento nell’insieme tramite add usa l’algoritmo di ordinamento per inserzione in un
array ordinato

• SortedSet: unione

Per realizzare l’unione, osserviamo che il problema è molto simile alla fusione di due array
ordinati
• come abbiamo visto in mergeSort, questo algoritmo di fusione (che abbiamo realizzato nel
metodo ausiliario merge) è O(n)
• L’unica differenza consiste nella contemporanea eliminazione (cioè nel non inserimento…) di
eventuali oggetti duplicati
• un oggetto presente in entrambi gli insiemi dovrà essere presente una sola volta
nell’insieme unione
public static SortedSet union(SortedSet s1,SortedSet s2)
{ SortedSet x = new ArraySortedSet();
Comparable[] v1 = s1.toSortedArray();
Comparable[] v2 = s2.toSortedArray();
int i = 0, j = 0;

while (i < v1.length && j < v2.length) // merge


if (v1[i].compareTo(v2[j]) < 0)
x.add(v1[i++]);
else if (v1[i].compareTo(v2[j]) > 0)
x.add(v2[j++]);
else // sono uguali
{ x.add(v1[i++]);
j++; }

while (i < v1.length)


x.add(v1[i++]);

while (j < v2.length)


x.add(v2[j++]);
return x;
}

• Prestazioni:
Effettuando la fusione dei due array ordinati secondo l’algoritmo visto in MergeSort, gli
oggetti vengono via via inseriti nell’insieme unione che si va costruendo. Questi inserimenti
avvengono con oggetti in ordine crescente.

Quali sono le prestazioni di add in questo caso?


• L’invocazione di contains ha prestazioni O(log n) per ogni inserimento.
• L’ordinamento per inserzione in un array ordinato, usato da add, ha prestazioni O(1) per
ogni inserimento!
In questo caso add ha quindi prestazioni O(log n).

Quindi complessivamente il metodo statico union ha prestazioni O(n log n).

• intersezione / sottrazione

Quali sono le prestazioni dei metodi intersection e subtract se gli oggetti s1 ed s2 sono di
tipo ArraySortedSet?
• L’invocazione s2.contains(v[i]) ha prestazioni O(log n)
• L'invocazione x.add(v[i]) ha in questo caso prestazioni O(log n).

Vale infatti il ragionamento di prima:


• L’invocazione di contains in add ha prestazioni O(log n) per ogni inserimento
• L’ordinamento per inserzione in un array ordinato, usato da add, ha prestazioni O(1) per
ogni inserimento!

Complessivamente i metodi statici intersection e subctract hanno prestazioni O(n log n).
COLLAUDO DI SET E SORTEDSET

import java.util.Scanner; import java.io.*;

public class SimpleSetTester


{ public static void main(String[] args) throws IOException
{
CREAZIONE INSIEMI: leggo da file, assumo che il file contenga un numero intero per riga
Scanner file1 = new Scanner(new FileReader("ins1.txt"));
Set insieme1 = new ArraySet();
//SortedSet insieme1 = new ArraySortedSet();
while (file1.hasNextLine())
insieme1.add(Integer.parseInt(file1.nextLine()));
System.out.println("\n\n*** Insieme 1 ***");
printSet(insieme1);

Scanner file2 = new Scanner(new FileReader("ins2.txt"));


Set insieme2 = new ArraySet();
//SortedSet insieme2 = new ArraySortedSet();
while (file2.hasNextLine())
insieme2.add(Integer.parseInt(file2.nextLine()));
System.out.println("\n\n*** Insieme 2 ***");
printSet(insieme2);
file1.close(); file2.close();

COLLAUDO METODI DI UNIONE, INTERSEZIONE, DIFFERENZA


Set unione = union(insieme1, insieme2);
//SortedSet unione = union(insieme1, insieme2);
System.out.println("\n\n*** Insieme Unione ***");
printSet(unione);

Set intersezione = intersection(insieme1, insieme2);


System.out.println("\n\n*** Insieme Intersezione ***");
printSet(intersezione);

Set differenza1 = subtract(insieme1, insieme2);


System.out.println("\n\n*** Insieme diff (1 - 2) ***");
printSet(differenza1);

Set differenza2 = subtract(insieme2, insieme1);


System.out.println("\n\n*** Insieme diff (2 - 1) ***");
printSet(differenza2);
} //CHIUSO IL MAIN

public static void printSet(Set s)


{ Object[] array = s.toArray(); //COLLAUDO METODO TOARRAY
for (int i = 0; i < array.length; i++)
System.out.print(array[i] + " ");
System.out.println(); }

public static Set union(Set s1, Set s2)


{ ... } //codice scritto prima
public static SortedSet union(SortedSet s1,SortedSet s2)
{ ... } //codice scritto prima
public static Set intersection(Set s1, Set s2)
{ ... } //codice scritto prima
public static Set subtract(Set s1, Set s2)
{ ... } //codice scritto prima
}
Riassunto: dati in sequenza
Abbiamo visto diversi tipi di contenitori per dati in sequenza, rappresentati dagli
ADT:
• pila
• coda
• coda doppia
• dizionario (mappa)
• insieme

Per realizzare tali ADT, abbiamo finora sempre usato la stessa struttura dati:
• array

LA STRUTTURA DATI “CATENA” e L'ADT “LISTA”

LA STRUTTURA DATI CATENA (LINKED LIST)

La catena o lista concatenata (linked list) non è un nuovo ADT, ma è una


struttura dati alternativa all’array per la realizzazione di ADT

Una catena è un insieme ordinato di nodi, ogni nodo è un oggetto che


contiene:
• un riferimento ad un elemento (il dato)
• un riferimento al nodo successivo nella catena (next)

Per agire sulla catena è sufficiente memorizzare il riferimento al suo primo nodo
[è comodo avere anche un riferimento all’ultimo nodo]

Il campo next dell’ultimo nodo contiene null


[Vedremo che è comodo avere un primo nodo senza dati, chiamato header]

CATENA VUOTA

Per capire bene il funzionamento della catena, è necessario


avere ben chiara la rappresentazione della catena vuota:

• contiene il solo nodo header, che ha null in entrambi i suoi


campi
• head e tail puntano entrambi al nodo header
ACCESSO AI NODI DELLA CATENA

Per accedere in sequenza a tutti i nodi della catena si parte dal riferimento head e si seguono
i riferimenti contenuti nel campo next di ciascun nodo
• non è possibile scorrere la lista in senso inverso
• la scansione termina quando si trova il nodo con il valore null nel campo next

NODO DI UNA CATENA

public class ListNode


{ //costruttori
public ListNode()
{ element = null; next = null;
}
public ListNode(Object e, ListNode n)
{ element = e;
next = n;
}

//metodi pubblici
public Object getElement()
{ return element;
}

public ListNode getNext()


{ return next;
}
public void setElement(Object e)
{ element = e;
}
public void setNext(ListNode n)
{ next = n;
}

//campi di esemplare
private Object element;
private ListNode next;
}

Nodo: incapsulamento eccessivo?


A cosa serve l’incapsulamento in classi che hanno lo stato completamente accessibile tramite
metodi? apparentemente a niente…
Supponiamo di essere in fase di debugging: vogliamo visualizzare un messaggio ogni volta
che viene modificato il valore di una variabile di un nodo
• Senza incapsulamento occorre aggiungere enunciati in ogni punto del codice dove vengono
usati i nodi
• Con incapsulamento è sufficiente inserire l’enunciato di visualizzazione all’interno dei
metodi set che interessano
• I campi di esemplare possono essere modificati solo mediante l’invocazione del
corrispondente metodo set
• terminato il debugging, per eliminare le visualizzazioni è sufficiente modificare il solo
metodo set
REALIZZAZIONE DI UNA CATENA

Metodi da realizzare sono:


• addFirst per inserire un oggetto all’inizio della catena
• addLast per inserire un oggetto alla fine della catena
• removeFirst per eliminare il primo oggetto della catena
• removeLast per eliminare l’ultimo oggetto della catena
• getFirst per esaminare il primo oggetto
• getLast per esaminare l’ultimo oggetto
• IsEmpty e makeEmpty (la catena è un contenitore)

Non vengono mai restituiti o ricevuti riferimenti ai nodi, ma sempre ai dati contenuti nei nodi.
Non si definisce un’interfaccia perchè la catena non è un ADT (abbiamo esplicitamente
indicato come deve essere realizzata, e non solo il suo comportamento).

Si definisce l’eccezione EmptyLinkedListException per segnalare la condizione di catena


vuota.

LA CLASSE LinkedList (catena)

public class LinkedList implements Container


{
// costruttore
public LinkedList()
{ makeEmpty(); }

//metodi pubblici
public void makeEmpty()
{ head = tail = new ListNode(); }

public boolean isEmpty()


{ return (head == tail); }

public Object getFirst() // operazione O(1)


{ if (isEmpty())
throw new EmptyLinkedListException();
return head.getNext().getElement(); }

public Object getLast() // operazione O(1)


{ if (isEmpty())
throw new EmptyLinkedListException();
return tail.getElement(); }

... //vanno scritti i metodi addFirst, addLast, removeFirst, removeLast

//campi di esemplare
private ListNode head, tail;
}
class EmptyLinkedListException extends RuntimeException { }
I METODI addFirst, removeFirst, addLast e removeLast

• metodo addFirst

public class LinkedList ...


{ ...
public void addFirst(Object e)
{
//inserisco dato nello header attuale
head.setElement(e);
//creo un nuovo nodo con due riferimenti null
ListNode n = new ListNode();
//collego il nuovo nodo allo header attuale
n.setNext(head);
//il nuovo nodo diventa lo header della catena
head = n;
//tail non viene modificato
} ...}

• Non esiste il problema di “catena piena” • L’operazione è O(1)

Verifichiamo che tutto sia corretto anche inserendo in una catena vuota
[Fare sempre attenzione ai casi limite]
METODO addFirst ALTERNATIVO

public void addFirst(Object e)


{
head.setElement(e);
head = new ListNode(null, head);
}
funziona perché prima head viene USATO (a destra) e solo successivamente viene
MODIFICATO (a sinistra)

• metodo removeFirst

public class LinkedList ...


{ ...
public Object removeFirst()
{ Object e = getFirst(); // delega a getFirst il controllo di lista vuota
head = head.getNext(); // aggiorno l'header
head.setElement(null);
return e;
} ... }

• L’operazione è O(1)

Verifichiamo che tutto sia corretto anche rimanendo con una catena vuota
[Fare sempre attenzione ai casi limite]
• metodo addLast

public class LinkedList ...


{ ...
public void addLast(Object e)
{
tail.setNext(new ListNode(e, null));
tail = tail.getNext(); //aggiorno il tail
}
...
}

• Non esiste il problema di “catena piena” • Anche questa operazione è O(1)

Verifichiamo che il tutto sia corretto anche inserendo in una catena vuota
[Fare sempre attenzione ai casi limite]
• metodo removeLast

public class LinkedList ...


{ ...
public Object removeLast()
{
Object e = getLast();
// L’ultimo nodo non ha un riferimento al penultimo!
// Bisogna cercare il penultimo nodo partendo dal primo
// e andando avanti finche’ non si arriva alla fine
ListNode temp = head;
while (temp.getNext() != tail)
temp = temp.getNext();
// a questo punto temp si riferisce al penultimo nodo
tail = temp;
tail.setNext(null);
return e;
} ...}

• Purtroppo questa operazione è O(n)

Verifichiamo che il tutto sia corretto anche inserendo in una catena vuota
[Fare sempre attenzione ai casi limite]
PRESTAZIONI DEI METODI DELLA CLASSE LinkedList

• Header della catena


•La presenza del nodo header nella catena rende più semplici i metodi della catena stessa, in
questo modo, non è necessario gestire i casi limite in modo diverso dalle situazioni ordinarie.
• Senza usare il nodo header, le prestazioni asintotiche rimangono comunque le stesse
• Usando il nodo header si “spreca” un nodo, per valori elevati del numero di dati nella catena
questo spreco, in percentuale, è trascurabile.

• Prestazioni dei metodi di LinkedList


• Tutte le operazioni sulla catena sono O(1) tranne removeLast che è O(n). Si potrebbe
pensare di tenere un riferimento anche al penultimo nodo, ma per aggiornare tale riferimento
sarebbe comunque necessario un tempo O(n).
• Se si usa una catena con il solo riferimento head, anche addLast diventa O(n), per questo è
utile usare il riferimento tail, che migliora le prestazioni di addLast senza peggiorare le altre
e non richiede molto spazio di memoria.

• Ridimensionare una catena


• Non esiste il problema di “catena piena”, non è mai necessario “ridimensionare” la catena,
la JVM lancia l’eccezione OutOfMemoryError se viene esaurita la memoria disponibile (in
particolare lo heap)
• Non c’è spazio di memoria sprecato (come negli array “riempiti solo in parte”), un nodo
occupa però più spazio di una cella di array, almeno il doppio (contiene due riferimenti
anziché uno).

[IMPORTANTE: CLASSE LINKEDLIST E LISTNODE COMPLETE da studiare]

LA CLASSE INTERNA LISTNODE


Osserviamo che la classe ListNode, usata dalla catena, non viene mai usata al di fuori della
catena stessa:
• la catena non restituisce mai riferimenti a ListNode
• la catena non riceve mai riferimenti a ListNode

Per il principio dell’incapsulamento sarebbe preferibile che questa classe e i suoi dettagli non
fossero visibili all’esterno della catena, in questo modo una modifica della struttura interna
della catena e/o di ListNode non avrebbe ripercussioni sul codice scritto da chi usa la catena
• È quindi più corretto definire ListNode come una classe interna a LinkedList

public class LinkedList


{ ... //costruttori, metodi, campi di LinkedList come prima
//definiamo ListNode come una classe interna privata
private class ListNode //codice di ListNode come prima
{ public ListNode() //costruttori
{ element = null; next = null; }
public ListNode(Object e, ListNode n)
{ element = e;
next = n; }

public Object getElement() //metodi pubblici


{ return element; }
public ListNode getNext()
{ return next; }
public void setElement(Object e)
{ element = e; }
public void setNext(ListNode n)
{ next = n; }

private Object element; //campi di esemplare


private ListNode next;
}}
Esercizio: realizzare pile e code con catene

Pile e code possono essere realizzate usando una catena invece di un array:
• Pila: entrambe le estremità di una catena hanno, prese singolarmente, il comportamento di
una pila, si può quindi realizzare una pila usando una delle due estremità della catena. È più
efficiente usare l’inizio della catena, perché le operazioni su tale estremità sono O(1)
• Coda: è sufficiente inserire gli elementi ad un’estremità della catena e rimuoverli dall’altra
estremità per ottenere il comportamento di una coda. Affinché tutte le operazioni siano O(1)
bisogna inserire alla fine e rimuovere all’inizio.

PILA REALIZZATA CON UNA CATENA

public class LinkedListStack implements Stack


{
public LinkedListStack()
{ list = new LinkedList(); }

public void push(Object obj)


{ list.addFirst(obj); }

public Object pop()


{ return list.removeFirst(); }

public Object top()


{ return list.getFirst(); }

public void makeEmpty()


{ list.makeEmpty(); }

public boolean isEmpty()


{ return list.isEmpty(); }

private LinkedList list;


}

CODA REALIZZATA CON UNA CATENA

public class LinkedListQueue implements Queue


{
public LinkedListQueue()
{ list = new LinkedList(); }

public void enqueue(Object obj)


{ list.addLast(obj); }

public Object dequeue()


{ return list.removeFirst(); }

public Object getFront()


{ return list.getFirst(); }

public void makeEmpty()


{ list.makeEmpty(); }

public boolean isEmpty()


{ return list.isEmpty(); }

private LinkedList list;


}
ITERATORI E ITERATORE IN UNA CATENA

ACCEDERE A NODI DI UNA CATENA


Per eseguire operazioni sulla catena che necessitano di accedere ai nodi è necessario
aggiungere metodi all’interno della classe LinkedList, che è l’unica ad avere accesso ai nodi
della catena.
Potremmo volere, ad esempio:
• Contare i nodi nella catena
• Verificare la presenza di un particolare oggetto nella catena (algoritmo di ricerca)
• Aggiungere/togliere elementi alla catena, anche in posizioni intermedie

Questo limita molto l’utilizzo della catena come struttura dati definita una volta per tutte…

Esempio: vogliamo contare i nodi della catena


È necessario scorrere tutta la catena. Il codice va scritto dentro la classe LinkedList.

public class LinkedList


{ ...
public int getSize()
{ ListNode temp = head.getNext();
int size = 0;
while (temp != null)
{ size++;
temp = temp.getNext();
} //se la catena è vuota size = 0 (giusto!)
return size;
} ....
}

PROBLEMA: Vogliamo che la catena fornisca uno strumento per accedere ordinatamente a
tutti i suoi elementi.
Idea: scriviamo un metodo getHead che restituisce un riferimento al nodo header:

• Questo approccio però non funziona: Se la classe


ListNode è public in LinkedList si viola l'incapsulamento,
perché diventa possibile modificare lo stato della catena
dall'esterno. Se invece è private allora i nodi sono
inaccessibili e il metodo
getHead è totalmente inutile.

ITERATORE IN UNA CATENA

SOLUZIONE DEL PROBLEMA: fornire all’utilizzatore della catena uno strumento con cui
interagire con la catena per scandire i suoi nodi.
Tale oggetto si chiama iteratore e ne definiamo prima di tutto il comportamento astratto:

• Un iteratore rappresenta in astratto il


concetto di posizione all’interno di una
catena
• Un iteratore si trova sempre dopo un
nodo e prima del nodo successivo (che
può non esistere se l’iteratore si trova
dopo l’ultimo nodo)
• In particolare, quando viene creato
l’iteratore si trova dopo il nodo header
• Un iteratore rappresenta in astratto il concetto di posizione all’interno di una catena
• la posizione è rappresentata concretamente da un riferimento ad un nodo (il nodo
precedente alla posizione dell’iteratore)
L'ADT ListIterator

import java.util.NoSuchElementException;

public interface ListIterator


{ //Una classe che realizza ListIterator per una catena avra` un costruttore del tipo
NomeClasse(ListNode h), che crea un iteratore che si trova subito dopo il nodo h
//Lancia NoSuchElementException se l'iteratore e` alla fine, altrimenti restituisce
l'oggetto che si trova dopo la pos. attuale e sposta l’iteratore di una pos. in avanti

Object next() throws NoSuchElementException; //verifica se si puo` invocare next()


boolean hasNext();

//inserisce x in prima della posizione attuale, senza modificare la posizione dell'iteratore


void add(Object x);

//Lancia IllegalStateException se invocato 2 volte consecutive altrimenti elimina l'ultimo


oggetto esaminato da next() o inserito da add() senza modificare la pos. dell’iteratore
void remove() throws IllegalStateException;
}

Possiamo immaginare un iteratore come un cursore in un elaboratore di testi


• Un nodo della catena corrisponde ad un carattere
• L’iteratore si trova sempre “tra due nodi”, come un cursore

A questo punto, è sufficiente che la catena fornisca un metodo per creare un iteratore

public class LinkedList


{ ...
public ListIterator getIterator()
{ ... } // dopo vediamo come scrivere questo metodo
...
}

E si può scandire la catena senza accedere ai nodi

LinkedList list = new LinkedList();


...
ListIterator iter = list.getIterator();
while(iter.hasNext())
System.out.println(iter.next());

// notare similitudine con StringTokenizer e Scanner


L’INTERFACCIA ITERATOR IN JAVA

La libreria standard di Java definisce in java.util una interfaccia Iterator:


• Definisce in particolare i metodi next e hasNext
Sempre in java.util si trova l’interfaccia ListIterator che estende Iterator:
• Definisce altri metodi, tra cui add e remove

L’interfaccia Iterator è implementata da una classe che abbiamo utilizzato molto spesso:
Scanner:
• Ha il metodo next
• Ha il metodo hasnext

IMPLEMENTARE LISTITERATOR

getIterator restituisce un riferimento ad una interfaccia, quindi in realtà deve creare un


oggetto di una classe che realizzi tale interfaccia.

Implementiamo ListIterator con la classe LinkedListIterator:


• I suoi oggetti sono costruiti solo all’interno di LinkedList e restituiti all’esterno solo tramite
riferimenti a ListIterator.
• Per un corretto funzionamento dell’iteratore occorre concedere a tale oggetto il pieno
accesso alla catena, in particolare, alla sua variabile di esemplare head.
• Non vogliamo che l’accesso sia consentito ad altre classi

Tutto questo ci porta a definire LinkedListIterator come una classe interna privata di
LinkedList.

La classe interna LinkedListIterator:

public class LinkedList


{ ... //codice di LinkedList come prima
... //incluso codice della classe privata ListNode
public ListIterator getIterator() //metodo di LinkedList
{ //crea un iteratore posizionato al primo nodo della catena
return new LinkedListIterator(head); }

private class LinkedListIterator implements ListIterator


{ //costruttore
public LinkedListIterator(ListNode h)
{ current = h;
previous = null;
}

//metodi pubblici (dobbiamo ancora realizzarli)


public boolean hasNext() { ... }
public Object next() { ... }
public void add(Object x) { ... }
public void remove() { ... }

//campi di esemplare
private ListNode current;//nodo che precede pos. attuale
private ListNode previous;//nodo che precede current
}
}
I METODI DI LINKEDLISTITERATOR

import java.util.NoSuchElementException;

public class LinkedList


{ ...
private class LinkedListIterator implements ListIterator
{ ...
//metodi pubblici hasNext, next
public boolean hasNext()
{ return current.getNext() != null; }

public Object next()


{ if (!hasNext())
throw new NoSuchElementException();
previous = current;
current = current.getNext();
return current.getElement(); }

//metodi pubblici add,remove (dobbiamo ancora realizzarli)


public void add(Object x) { ... }
public void remove() { ... }

//campi di esemplare
private ListNode current;//nodo che precede pos. attuale
private ListNode previous;//nodo che precede current
}
}

• Il metodo add di LinkedListIterator

• Aggiunge il nuovo nodo e avanza di una posizione


• Se il nodo viene aggiunto alla fine della catena, allora bisogna anche aggiornare il
riferimento tail della catena

Nota sintattica: il riferimento LinkedList.this punta all'oggetto LinkedList all'interno di cui è


stato creato l'iterator

public void add(Object obj)


{ ListNode n = new ListNode(obj, current.getNext());
current.setNext(n);
previous = current; //aggiorno riferimenti iteratore a subito dopo nuovo nodo
current = current.getNext(); //se ho aggiunto all'ultimo nodo
if (!hasNext())
LinkedList.this.tail = current; //aggiorno tail
}
• Il metodo remove di LinkedListIterator

L'iteratore non ha un riferimento al nodo prima di previous.


•Quindi non posso aggiornare correttamente previous dopo la rimozione: gli assegno valore
null, con la conseguenza che l'iteratore si trova in uno stato illegale dopo la rimozione.
• Per questo motivo non si può invocare nuovamente remove. Invocando next o add previous
verrà riaggiornato.

//Va sempre invocato dopo add o next, anche la prima volta


public void remove() throws IllegalStateException
//Se dalla costruzione dell'iteratore o dall'ultima invocazione di remove non e` stato
invocato next o add, allora previous==null. Stato illegale!
{
if (previous == null)
throw new IllegalStateException();
previous.setNext(current.getNext());
current = previous; // aggiorno riferimenti iteratore a subito prima del nodo rimosso
previous = null; // se ho rimosso l'ultimo nodo
if (!hasNext())
LinkedList.this.tail = current; //aggiorno tail
}