Sintassi Java
Sintassi Java
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)
OGGETTI
Un oggetto è un’entità che può essere manipolata in un programma mediante l’invocazione di
metodi.
NomeClasse nomeOggetto; definisce una variabile 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.
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)
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
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
New NomeCostruttore;
invoca il costruttore. L'operatore new riserva la memoria per l'oggetto mentre il costruttore
definisce il suo stato iniziale
//METODI
public void deposit (double amount)
{ balance = balance + amount;
}
//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.
//FARE UN BONIFICO
double amount = 500;
account1.withdraw(amount);
//ACCREDITARE INTERESSI
double rate = 0.05;
double amount = account.getBalance()*rate;
account.deposit(amount);
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 = "";
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
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.
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; }
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”.
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;
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
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.
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 static final double PI=3.14... COSTANTE STATICA nella classe Math
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
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).
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.
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
3)potrei usare un asserzione, ma si usa soltanto per programmi in fase di sviluppo e collaudo
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.
throw oggettoEccezione;
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
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
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
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.
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
}
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:
Import java.io.FileReader;
Import java.io.IOException;
Import java.io.PrintWriter;
Import java.util.Scanner;
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.
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 ++;
}
}
}
}
// \\s* significa 0 o più ripetizioni delo spazio bianco, fish è il pattern da trovare
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.
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 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 oneValue = values [3] //salva in una variabile il QUARTO valore dell'array
int a = nomeVariabileArray.lenght;
riferimentoArray[indice];
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
oppure
...
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
Il metodo Math.random() restituisce numeri tra 0 e 1, quindi per ottenere numeri interi devo
scrivere
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;
}
-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)
(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,...).
(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.
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).
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");
}
}
}
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.
n=0 è un CASO BASE. Deve esserci sempre almeno un caso base di cui conosco la soluzione.
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
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
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]);
}
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);
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
Dobbiamo trovare
• un caso base
• Un passo di ricorsione che semplifica il problema
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).
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
T(n) ~ 1/2*n2
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
• 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
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]
Si ottiene quindi una equazione di secondo grado in n, che giustifica l’andamento parabolico
dei tempi rilevati sperimentalmente.
ORDINAMENTO PER FUSIONE (MargeSort)
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
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
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.
Dal termine T(n/2k) si vede che il caso base è raggiunto dopo k = log2n, sostituzioni, ovvero
quando n/2k= 1
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!
• 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
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
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
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
Dal termine T(n/2k) si vede che il caso base T(1)=1 è raggiunto per k = log2n, ovvero per
2k = n
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.
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
}
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
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
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
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.
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
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
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
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)
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
Sappiamo che un oggetto di una sottoclasse può essere usato come se fosse un oggetto della
superclasse
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
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à
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.
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)
• In generale la descrizione testuale standard non è particolarmente utile, è piu utile ottenere
una stringa di testo contenente informazioni sullo stato dell’oggetto in esame.
• Il metodo toString di una classe viene invocato implicitamente anche per concatenare un
oggetto con una stringa:
• La seconda riga viene interpretata dal compilatore come se fosse stata scritta così:
System.out.println(anAccount); BankAccount[balance=1500]
• 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
• toString visualizza il nome corretto della classe anche quando viene invocato su un oggetto
di una sottoclasse
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
• Problema: nella firma di equals il parametro esplicito è di tipo Object, quindi è necessario
eseguire un cast per accedere ai campi di esemplare di otherObject.
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
• È 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
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.
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
• 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
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
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.
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.
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
Allora a che serve avere una variabile oggetto il cui tipo è quello di una interfaccia?
• 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
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 x;
...
if (...) x = new BankAccount(1000);
else x = new String(“”);
...
if (x.compareTo(y) > 0) ...
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) ...
INTERFACCIA COMPARABLE
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
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!
Quindi una classe che implementa Comparable dovrebbe sempre sovrascrivere equals…
ma non sempre noi lo faremo
• 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 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);
}
}
• 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
• In compilazione:
Se non si usa l'interfaccia Comparable parametrica, questo errore non viene rilevato in
compilazione e si genera invece una ClassCastException in esecuzione.
• 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
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.
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
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.
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)
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
Senza sapere come siano realizzate StackX e StackY, possiamo usare esemplari di queste
classi sfruttando il comportamento astratto definito in Stack.
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
• 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.
// 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
• Il metodo push sovrascritto fa accesso alle variabili di esemplare v e vSize definite nella
superclasse
• Questo è consentito dalla definizione protected
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)
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
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.
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)
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.
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
try
{ Character ch = (Character)st.pop(); }
catch (ClassCastException e)
{ // 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");}
}
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).
LA CLASSE SlowFixedArrayQueue
Il numero di elementi è (back – front), in particolare quando front == back l'array è vuoto.
LA CLASSE FixedArrayQueue
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).
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
LA CLASSE FixedCircularArrayQueue
// non serve sovrascrivere getFront perche` non modifica le variabili back e front
}
RIDIMENSIONARE UN ARRAY CIRCOLARE
In generale però la zona utile della coda è attorno alla sua fine (ovvero back < front): c’è un
problema in più
LA CLASSE GrowingCircularArrayQueue
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).
• Tutti i metodi hanno prestazioni O(1) (in senso ammortizzato per i metodi di inserimento
con ridimensionamento dell'array)
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
L’interfaccia Dictionary
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.
PRESTAZIONI 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
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).
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
La classe interna può essere resa inaccessibile al codice scritto in altre classi
•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]
//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
//campi di esemplare
protected Pair[] v;
protected int vSize;
protected final static int INITSIZE = 10;
COLLAUDO DI UN DIZIONARIO
• 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].
• Abbiamo scritto enunciati return per metodi che non restituiscono void, in questo modo la
classe si compila da subito.
LA CLASSE ArraySet
• unione
• Prestazioni:
se contains è O(n) (e, quindi, lo è anche add), questa operazione è O(n2)
• intersezione
• Prestazioni:
se contains è O(n) l’operazione di intersezione è O(n2).
• sottrazione
• Prestazioni:
se contains è O(n) l’operazione di intersezione è O(n2).
INSIEMI ORDINATI
• Gli algoritmi di unione, intersezione, sottrazione per insiemi generici possono essere
utilizzati anche per insiemi ordinati [infatti, un SortedSet è anche un Set].
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;
• 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.
• 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).
Complessivamente i metodi statici intersection e subctract hanno prestazioni O(n log n).
COLLAUDO DI SET E SORTEDSET
Per realizzare tali ADT, abbiamo finora sempre usato la stessa struttura dati:
• array
Per agire sulla catena è sufficiente memorizzare il riferimento al suo primo nodo
[è comodo avere anche un riferimento all’ultimo nodo]
CATENA VUOTA
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
//metodi pubblici
public Object getElement()
{ return element;
}
//campi di esemplare
private Object element;
private ListNode next;
}
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).
//metodi pubblici
public void makeEmpty()
{ head = tail = new ListNode(); }
//campi di esemplare
private ListNode head, tail;
}
class EmptyLinkedListException extends RuntimeException { }
I METODI addFirst, removeFirst, addLast e removeLast
• metodo addFirst
Verifichiamo che tutto sia corretto anche inserendo in una catena vuota
[Fare sempre attenzione ai casi limite]
METODO addFirst ALTERNATIVO
• metodo removeFirst
• L’operazione è O(1)
Verifichiamo che tutto sia corretto anche rimanendo con una catena vuota
[Fare sempre attenzione ai casi limite]
• metodo addLast
Verifichiamo che il tutto sia corretto anche inserendo in una catena vuota
[Fare sempre attenzione ai casi limite]
• metodo removeLast
Verifichiamo che il tutto sia corretto anche inserendo in una catena vuota
[Fare sempre attenzione ai casi limite]
PRESTAZIONI DEI METODI DELLA CLASSE LinkedList
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
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.
Questo limita molto l’utilizzo della catena come struttura dati definita una volta per tutte…
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:
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:
import java.util.NoSuchElementException;
A questo punto, è sufficiente che la catena fornisca un metodo per creare un iteratore
L’interfaccia Iterator è implementata da una classe che abbiamo utilizzato molto spesso:
Scanner:
• Ha il metodo next
• Ha il metodo hasnext
IMPLEMENTARE LISTITERATOR
Tutto questo ci porta a definire LinkedListIterator come una classe interna privata di
LinkedList.
//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;
//campi di esemplare
private ListNode current;//nodo che precede pos. attuale
private ListNode previous;//nodo che precede current
}
}