Conversioni di tipo fra sottoclasse e superclasse Conversioni di tipo fra sottoclasse e superclasse • Può esserci la necessità di memorizzare un riferimento di una sottoclasse in un riferimento a superclasse. • È possibile? • Abbiamo visto l’invocazione del metodo push di uno Stack di Object a cui era passata come parametro una stringa: s.push("Pippo"); • È possibile perché String è sottoclasse di Object. Conversioni di tipo fra sottoclasse e superclasse • Analogamente è possibile memorizzare un riferimento ad un oggetto di tipo ContoRisparmio in un riferimento di tipo ContoBancario, che è una superclasse. • In generale è possibile memorizzare un qualsiasi riferimento a oggetto in un riferimento di tipo Object che è la superclasse universale. • La conversione di tipo è automatica. (par. 10.5) Conversioni di tipo fra sottoclasse e superclasse Conversioni di tipo fra sottoclasse e superclasse • Consideriamo questi riferimenti: ContoRisparmio cr = new ContoRisparmio(2); ContoBancario cb = cr; Object ogg = cr; • Abbiamo tre riferimenti diversi che vedono la stessa area di memoria. Conversioni di tipo fra sottoclasse e superclasse • Attenzione. Ogni riferimento può usare solo i metodi della sua classe: saldo 0 cr tassoInteresse 2 cb ogg cb.deposito(500); cb può modificare il saldo, però non può usare aggiungiInteresse. • Con il riferimento ogg (di Object) si possono usare solo i metodi di Object (come toString, equals, e pochi altri). • Ciò nonostante può essere utile usare un riferimento della superclasse Object. 1 Conversioni di tipo fra sottoclasse e superclasse • Aggiungiamo a ContoBancario il seguente metodo che permette di trasferire denaro da un conto ad un altro: (par. 10.5) public class ContoBancario{ . . . public void trasferisciA (ContoBancario altro, double denaro) { this.prelievo(denaro); altro.deposito(denaro); } }//fine CB Conversioni di tipo fra sottoclasse e superclasse System.out.println (padre.rendiSaldo());//4700 System.out.println (figlio.rendiSaldo());//300 • Con l’invocazione del metodo padre.trasferisciA(figlio, 300); • abbiamo: this.prelievo(300); //this=padre altro.deposito(300); //altro=figlio Conversioni di tipo fra sottoclasse e superclasse • L’istruzione padre.trasferisciA(cliente,200); è corretta perché il metodo trasferisciA si aspetta di ricevere come parametro un riferimento a CB e invece riceve un riferimento (cliente) a CR che è una sottoclasse: viene eseguita una conversione automatica. Conversioni di tipo fra sottoclasse e superclasse • Esempio. ContoBancario padre = new ContoBancario(5000); ContoBancario figlio = new ContoBancario(); padre.trasferisciA(figlio, 300); • Controlliamo con una stampa il saldo di entrambi: Conversioni di tipo fra sottoclasse e superclasse • Supponiamo ora che padre debba trasferire denaro ad un conto di tipo ContoRisparmio: ContoRisparmio cliente = new ContoRisparmio(2); • Possiamo fare: padre.trasferisciA(cliente,200); Conversioni di tipo fra sottoclasse e superclasse • Il compilatore controlla solo che il riferimento di trasfersiciA, se non è di tipo CB, sia di tipo classe derivata da CB. • Avviene quindi la conversione di un tipo ad un tipo superiore: ContoBancario altro ← cliente 2 Conversioni di tipo fra sottoclasse e superclasse Conversioni di tipo fra sottoclasse e superclasse • Ciò è analogo a quanto avviene quando si assegna ad una variabile reale un valore intero: double a = 25; • Attenzione. • Il metodo trasferisciA invoca il metodo deposito: • In realtà nei tipi base avviene una cosa diversa perché il valore 25 (32 bit in complemento a 2) viene memorizzato in una sequenza di 64 bit con mantissa, esponente. public void trasferisciA (ContoBancario altro, double denaro) { this.prelievo(denaro); altro.deposito(denaro); } Conversioni di tipo fra sottoclasse e superclasse • Ma quale deposito? • Il parametro altro contiene un riferimento di tipo ContoRisparmio. Polimorfismo • Quindi sarà deposito di ContoRisparmio: il metodo con cui si paga una tassa ad ogni versamento. Polimorfismo Polimorfismo • Il termine polimorfismo (deriva dal greco πολιµορφίσµος) significa “molte forme”. • Nel trasferimento di denaro viene attivato: • Quando trasferisciA viene invocato, il riferimento passato ad altro “vede” un oggetto di tipo CR. • il metodo deposito di CB, quando il parametro è figlio • il metodo deposito di CR, quando il parametro è cliente. • La stessa operazione “versare denaro” viene eseguita in modi diversi che dipendono dal tipo dell’oggetto che viene effettivamente usato come parametro implicito. CB: altro CR: cliente 3 Polimorfismo Polimorfismo • L’interprete Java sa che in altro c’è un riferimento a CR e quindi invoca deposito di CR. • Il compilatore può solo verificare la possibilità che ciò possa avvenire. • La scelta di quale sia effettivamente il riferimento viene fatta durante l’esecuzione del programma, vale a dire, solo quando il metodo viene invocato. (par. 10.6) • La scelta pertanto non è fatta in base al tipo del riferimento, altro è definito di tipo CB, ma in base al tipo dell’oggetto che è realmente memorizzato in altro e che è di tipo CR. • Il metodo ha lo stesso nome ma ha “forme diverse”. • Il metodo deposito di CR, infatti, sovrascrive il metodo deposito di CB. Sovraccarico e polimorfismo Sovraccarico e polimorfismo • Sovraccarico. • Si parla di sovraccarico quando in una classe un metodo o un costruttore ha diverse scritture e possiede quindi parametri diversi. • Esempio: • in ContoBancario abbiamo due costruttori, uno senza parametro e uno con parametro • il metodo println possiede parametri diversi a seconda del tipo base. • È il compilatore che sceglie quale metodo invocare, prima che il programma venga eseguito. • Si parla di selezione anticipata (early binding o anche binding statico). Sovraccarico e polimorfismo Sovraccarico e polimorfismo • Polimorfismo. • Si parla di polimorfismo quando un metodo ha comportamenti diversi in relazione al tipo realmente memorizzato nel parametro implicito. • Esempio: • nel metodo trasferisciA l’utilizzo del metodo deposito di CB oppure di CR. • È l’interprete JVM che decide durante l’esecuzione del programma quale metodo deve essere scelto. • Si parla di selezione posticipata (late binding o anche binding dinamico). • Il compilatore controlla solo che riferimento sia di tipo classe o sottoclasse. il 4 Conversione inversa Conversione inversa Conversione inversa • Esempio. ContoRisparmio cr = new ContoRisparmio(2); ContoBancario cb = cr; ContoRisparmio cr2 = (ContoRisparmio)cb; //cast • Senza cast il compilatore segnala errore: i tipi sono incompatibili. • Si può fare la conversione inversa, vale a dire memorizzare un riferimento a superclasse in un riferimento a sottoclasse? • È possibile fare una forzatura, cast, analogamente a quanto avviene per i tipi base. • La conversione però ha senso solo se nel riferimento a superclasse è effettivamente memorizzato un riferimento a sottoclasse; in caso contrario si ha un errore. Conversione inversa • Se però cb (sul quale si fa il cast) non contiene un riferimento a cr, durante l’esecuzione la JVM lancia l’eccezione ClassCastException. • Come fare per essere sicuri che un riferimento contenga un riferimento valido per l’oggetto e non commettere un errore? Conversione inversa Conversione inversa • Per essere sicuri che un riferimento contenga un riferimento valido per quell’oggetto, si può usare l’operatore instanceof. • Esempio. Vogliamo essere sicuri che il riferimento cb contiene un riferimento a cr prima di eseguire l’assegnazione a cr2, possiamo scrivere: • Sintassi. variabileoggetto instanceof Nomeclasse • L’operatore instanceof è booleano: restituisce true, se la variabile è del tipo NomeClasse, false altrimenti. if(cb instanceof ContoRisparmio) cr2 = (ContoRisparmio)cb; 5 Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice • La classe OperasuNumeri contiene metodi per eseguire il calcolo della somma, del massimo e del numero di un elenco di dati inseriti (reale). (classe DataSet par. 6.4 e par. 9.1) public class OperasuNumeri{ private double s; private double max; private int cont; Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice /* costruttore: inizializza la somma a zero, il massimo con l'estremo inferiore, il contatore dei numeri a zero */ /* metodo aggiungi : aggiunge un valore alla volta aggiornando il valore della somma, del massimo e del contatore dei valori inseriti */ public OperasuNumeri(){ s = 0; max = - Double.MAX_VALUE; cont = 0; }//fine costruttore Interfacce e riutilizzo del codice /* metodo somma : restituisce la somma dei valori inseriti */ public double somma(){ return s; } /* metodo massimo : restituisce il massimo dei valori inseriti */ public double massimo(){ return max; } public void aggiungi(double x){ s = s + x; if(max < x) max = x; cont++; }//fine aggiungi Interfacce e riutilizzo del codice /** metodo quanti : restituisce il numero di valori inseriti */ public int quanti(){ return cont; } }//fine classe OperasuNumeri • Consideriamo la classe ContoBancario e supponiamo di avere voler gestire una banca rappresentata da un certo numero di conti bancari e di voler calcolare quanti soldi ci sono in totale nella banca (somma). 6 Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice • Supponiamo anche di voler sapere quanti sono i conti gestiti dalla banca (contatore) e quale è il conto bancario che ha il saldo maggiore (massimo). • Avendo a disposizione la classe OperasuNumeri, possiamo riscriverla adattandola agli oggetti di tipo ContoBancario. public class OperasuContoBancario{ private double s; private ContoBancario max; private int cont; /* non mettiamo alcun costruttore, quindi si attivera' quello di default */ • Dovremo però fare qualche modifica. /* Il massimo che cerchiamo e' il conto corrente che possiede il saldo maggiore */ Interfacce e riutilizzo del codice public void aggiungi (ContoBancario x){ s = s + x.rendiSaldo(); if(cont==0 || max.rendiSaldo() < x.rendiSaldo()) max = x; //fine if /* quando cont = 0 si inizializza max con il primo conto bancario: valutazione pigra dei predicati */ cont++; } Interfacce e riutilizzo del codice public ContoBancario massimo(){ return max; } . . . // altri metodi: quanti, somma . . . }//fine OperasuContoBancario Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice • In maniera analoga potremo dover risolvere un altro problema in cui si vuole trovare quale oggetto, in un elenco di oggetti, ha il valore massimo in uno dei suoi dati. • Esempio. Dato un borsellino contenente delle monete vogliamo sapere quale è la somma totale e quale è la moneta con il valore più elevato. • Dovremo scrivere una classe OperasuMonete? Vediamo di risolvere il problema in altro modo. • Dato un insieme di oggetti possiamo stabilire cosa si intende per “misura” di quell’oggetto; vogliamo poi risolvere il problema di trovare l’oggetto che ha la misura più grande. • Esempi. • Dato un insieme di Pile trovare la Pila che contiene più elementi (quantità di elementi). • Dati dei numeri complessi trovare quello il cui modulo è maggiore (valore del modulo). • Abbiamo bisogno di definire una proprietà astratta: “essere misurabile”. 7 Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice • Abbiamo visto che in Java per definire una proprietà astratta si usa una interfaccia con la quale si esprimono funzionalità comuni alle classi che la realizzano. • Nell’interfaccia si dichiarano le firme dei metodi che rappresentano le funzionalità. • Le classi che realizzano l’interfaccia dovranno costruire il codice per tutti i metodi dell’interfaccia. • Definiamo pertanto l’interfaccia Misurabile per definire la proprietà di: avere una misura Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice • Possiamo • • • • voler aggiungere un metodo altriDati per poter rappresentare altre informazioni sugli oggetti misurabili. Esempio: il nome del correntista il cui conto bancario è il massimo; una caratteristica (figura) della moneta che ha il valore più grande il numero della Pila con maggior elementi. Interfacce e riutilizzo del codice public class ContoBancario implements Misurabile{ . . . public double estraiMisura(){ return saldo; } public Object altriDati(){ return nome; } }//fine ContoBancario public interface Misurabile{ // metodo che rappresenta una misura double estraiMisura(); // Object altriDati(); } • Lo scopo è quello di non aver più bisogno di scrivere la classe OperasuContoBancario (e classi analoghe per risolvere problemi analoghi). • Per prima cosa dovremo dichiarare che ContoBancario realizza l’interfaccia Misurabile e stabilire cosa intendiamo per “misura” di un conto bancario. • Stabiliamo che la misura per CB è il valore del saldo. Interfacce e riutilizzo del codice • Però dobbiamo ancora fare delle modifiche sulla classe che esegue la somma e trova il massimo, in modo da poterla riutilizzare nella soluzione di problemi analoghi. • La classe OperasuContoBancario eseguiva le operazioni per il calcolo della somma, del numero di elementi inseriti e trovava l’elemento max: il conto bancario con il saldo maggiore. 8 Interfacce e riutilizzo del codice • Un’altra classe avrebbe trovato come max la moneta con il valore più grande, oppure la Pila con il maggior numero di elementi. • Dobbiamo allora di costruire una classe per gestire questi oggetti misurabili. • Costruiamo una classe OperasuOggetti che gestisca le operazioni su oggetti le cui classi realizzano l’interfaccia Misurabile. Interfacce e riutilizzo del codice max = x; cont++; }//fine aggiungi //fine if public Misurabile massimo(){ return max; } . . .//metodi quanti, somma }//fine OperasuOggetti Interfacce e riutilizzo del codice public class OperasuOggetti{ private double s; private Misurabile max; private int cont; . . .//costruttore public void aggiungi(Misurabile x){ s = s + x.estraiMisura(); if(cont==0 || max.estraiMisura()< x.estraiMisura()) Interfacce e riutilizzo del codice • Questa classe potrà essere utilizzata dagli oggetti la cui classe che realizza Misurabile. • Questo è un esempio di polimorfismo. • Il metodo estraiMisura è polimorfo. • Con l’invocazione del metodo x.estraiMisura() otteniamo diversi comportamenti. Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice • La classe ContoBancario realizza estraiMisura restituendo il saldo. • Un’altra classe realizerà estraiMisura restituendo un valore double con il significato di “misura” in quella classe. • Sarà l’interprete Java che, durante l’esecuzione, vede quale riferimento è memorizzato in x ed attiva il metodo della classe corrispondente. • In realtà x non ha un suo tipo di dato: x è di “tipo interfaccia”. 9 Interfacce e riutilizzo del codice Interfacce e riutilizzo del codice • Con l’interfaccia Misurabile abbiamo trasferito agli oggetti una caratteristica dei numeri: avere un valore; con i numeri possiamo eseguire somme, trovare un massimo, rappresentare una misura. • I numeri possono anche essere messi in ordine. • Prima di tutto ci dobbiamo chiedere se ha senso confrontare oggetti. Lo abbiamo appena fatto cercando il massimo, quindi i confronti possono avere significato. • Esempio. • Vogliamo mettere in fila dei conti bancari in modo che siano in ordine da quello con saldo minimo a quello con saldo massimo. • Vogliamo mettere in fila dei numeri complessi in modo che siano secondo l’ordine crescente del loro modulo (reale). • Possiamo anche ordinare oggetti? Interfacce e riutilizzo del codice • Cosa significa per oggetti il dire: l’oggetto1 è minore (maggiore o uguale) dell’oggetto2? • Abbiamo già visto con le stringhe che non possiamo confrontare i riferimenti. • Dobbiamo pensare ad una proprietà: “essere confrontabile”. • Esempio. Per la classe Complex può essere il modulo: è un valore reale, esiste un ordine. Interfacce e riutilizzo del codice • Nella classe ContoBancario potrebbe essere il saldo, un numero reale, e pensare un ordine dal più povero al più ricco; oppure potrebbe essere il numero di conto, numero intero: dal cliente che per primo ha aperto un conto all’ultimo che lo ha attivato. • Per la classe Studente potrebbe essere la matricola, numero intero, oppure il nome, una stringa e sappiamo che le stringhe hanno un ordine. Interfacce e riutilizzo del codice Interfaccia Comparable • Nel pacchetto java.lang c’è un’interfaccia che rappresenta la proprietà astratta “essere confrontabile”: l’interfaccia Comparable: • Siano a e b due oggetti di una classe che realizza Comparable, si ha: (par. 13.8) public interface Comparable{ int compareTo(Object other); } • Il metodo compareTo restituisce un valore intero. La classe String realizza Comparable: s1.compareTo(s2) con s1, s2 di tipo String. a.compareTo(b) <0 =0 a ”precede” b a “è uguale a” b >0 a “segue” b • Che cosa significano i termini precede, segue, uguale, dipende dalle scelte che facciamo nelle classi. 10 Interfaccia Comparable Interfaccia Comparable • Esempio. • Consideriamo la classe Studente con i campi nome e matricola; stabiliamo un confronto in base alla matricola. • Nella classe Studente, che realizza Comparable, dobbiamo costruire il metodo compareTo con il codice per esprimere il significato di “confronto tra studenti”. public int compareTo(Object altrostud) {Studente altro = (Studente)altrostud; int valore; if(this.matricola < altro.matricola) valore = -1; else if(matricola > altro.matricola) valore = 1; else valore = 0; return valore; } //fine CompareTo Interfaccia Comparable Interfaccia Comparable • Abbiamo dovuto fare il cast: Studente altro = (Studente)altrostud; perché altrostud non possiede il campo matricola, dato che è Object. Possiamo fare altro.matricola perché siamo nella classe Studente. • Vediamo la classe ContoBancario implementa due interfacce. che Interfaccia Comparable • Possiamo anche confrontare stringhe, ad esempio ordinare gli studenti per nome (purché diversi): public int compareTo(Object altro){ Studente altrostud = (Studente)altro; return nome.compareTo(altrostud.nome); } dove nome è this.nome e quello delle stringhe. compareTo è public class ContoBancario implements Misurabile, Comparable{ . . . // metodi: estraiMisura, altriDati public int compareTo(Object altro){ ContoBancario cb =(ContoBancario)altro; if(this.saldo < cb. saldo) return -1; if(this. saldo > cb. saldo) return 1; return 0; } //fine compareTo }//fine CB /* non e’ strutturato: tre return, ma e’ chiaro */ Interfaccia Comparable • Ora che sappiamo cosa significa “confrontare oggetti”, possiamo metterli in ordine: public static void ordlineare ( ContoBancario a[]){ ContoBancario sc; //ciclo su i //ciclo su k if(a[i].compareTo(a[k]) > 0) //scambiare a[i] con a[k] } 11 Interfaccia Comparable • Se dobbiamo ordinare elementi di tipo Complex o Studente dobbiamo riscrivere l’ordinamento. Possiamo fare meglio? • All’ordinamento basta avere degli oggetti che siano confrontabili, vale a dire oggetti la cui classe realizzi Comparable. • In maniera analoga a quanto fatto con Misurabile, andiamo a definire una variabile di “tipo” Comparable e scriviamo un ordinamento per oggetti qualunque. Interfaccia Comparable public static void ordlineare( Comparable a[]){ //ordinamento per oggetti "confrontabili" Comparable sc; for(int i=0; i<a.length-1; i++) for(int k=i+1; k<a.length; k++) if(a[i].compareTo(a[k])> 0){ sc = a[i]; a[i] = a[k]; a[k] = sc; }//fine if } Sovrascrivere il metodo equals Sovrascrivere il metodo equals • Consideriamo un array di numeri, ci interessa sapere se ci sono oppure no degli elementi ripetuti nella sequenza; esempio a = (1, 3, 2, 7, 3, 4) il 3 è ripetuto a = (1, 2, 4, 7, -5, 0) sono tutti diversi avremo un algoritmo del tipo: algoritmo boolean distinti (array a) def. variabili diversi logico i, k intero Sovrascrivere il metodo equals Sovrascrivere il metodo equals diversi ← vero i←0 mentre i < a.length -1 e diversi per k = i+1 fino a a.length-1 se a[i] == a[k] allora diversi ← falso //fineif //fineper i ← i+1 //finementre restituire diversi // fine algoritmo • Possiamo fare una cosa simile con gli oggetti? La superclasse Object possiede un metodo per verificare se due oggetti sono uguali: eseguire eseguire public boolean equals(Object ob){ return (this == ob); } Il confronto this==ob restituisce vero o falso. • Il confronto precedente è fatto tra riferimenti, quindi per gestire “oggetti uguali” andiamo a sovrascrivere il metodo equals (come nella classe String). (par. 10.8.2) 12 Sovrascrivere il metodo equals Sovrascrivere il metodo equals Confrontiamo in base al numero di conto bancario: public class ContoBancario{ . . . public boolean equals(Object ob){ ContoBancario ac =(ContoBancario)ob; if(this.numeroConto == ac.numeroConto) return true; else return false; }//fine equals }//fine CB • In CB possiamo anche scegliere di usare compareTo invece di equals: a.compareTo(b) == 0 corrisponde a a.equals(b) vero Non per tutti gli oggetti può aver senso cercare l’ordine, mentre l’uguaglianza è una caratteristica degli oggetti. • Esercizio. Costruire un metodo per verificare se in un array di oggetti ci sono elementi ripetuti. Lista concatenata Struttura di dati lista concatenata • La struttura di dati array memorizza i dati in maniera consecutiva in memoria e l’accesso ai dati viene fatto tramite un indice. • L’attribuzione dello spazio viene fatta a livello di compilazione e quindi la dimensione dei dati è fissa (anche se poi si usa il raddoppio). • Per poter costruire una sequenza di informazioni con una dimensione non fissata a priori è necessario agire in maniera diversa: durante l’esecuzione del programma e con locazioni non necessariamente consecutive. Lista concatenata Lista concatenata • Per poter costruire una sequenza di informazioni non consecutive è necessario che ogni dato della struttura memorizzi un’informazione per collegarsi all’elemento successivo • Consideriamo una nuova modalità di memorizzare i dati in cui l’accesso non avviene più tramite un indice, ma tramite un indirizzo di memoria. • Si costruisce una struttura di dati “collegata”, chiamata lista concatenata o catena. • Ogni nodo (anello) di una lista concatenata oltre all’informazione memorizza il riferimento dell’elemento successivo: avremo bisogno di un nodo con le caratteristiche del record per poter rappresentare due (o più) campi di tipo diverso: dato: info riferimento: next 13 Lista concatenata Lista concatenata • Il nodo sarà composto da : • l’informazione: un elemento di tipo base o un oggetto (l’informazione che possiamo memorizzare anche nell’array); • il riferimento sarà un riferimento ad un nodo (un riferimento ad un elemento dello stesso tipo del nodo). • Si intuisce che le definizione del nodo sarà un po’ particolare: dobbiamo definire il tipo del nodo, ma al suo interno dobbiamo usare quel tipo per definire il campo riferimento. • Poiché vi si accede tramite un riferimento le informazioni memorizzate non sono più necessariamente contigue in memoria, come nell’array. Lista concatenata Lista concatenata • Supponiamo di voler costruire una struttura di dati lista concatenata per rappresentare una sequenza di interi. • Sintassi. • Definizione del nodo: class ElemIntero{ int info; //dato ElemIntero next; //riferimento } • Come possiamo gestire l’accesso per la classe ElemIntero? • 1) classe public, elementi private: si devono costruire i metodi per accesso e modifica dei campi; • 2) controllo di pacchetto (nessuna specifica) sia per la classe che per i campi: l’accesso ai campi è tramite il nome; • 3) classe interna privata e nessuna specifica sui campi. Adottiamo questa scelta. (par. 14.2) Lista concatenata Lista concatenata • Rappresentiamo un TDA su lista concatenata public class TDAsuListaConc{ . . . //metodi per gestire il TDA . . . //classe interna private class ElemIntero{ int info; ElemIntero next; }//fine classe ElemIntero } • Quale vantaggio e quale sicurezza comporta questa scelta? • Essendo privata la classe non è visibile dall’esterno: quindi si rispettano per i suoi campi le direttive dell’incapsulamento. • Essendo interna e non specificando l’accesso i suoi campi sono visibili internamente alla classe che realizza il TDA e si possono utilizzare direttamente, senza dover scrivere in ElemIntero i metodi di accesso e di modifica per i campi. 14 Confronto tra array e lista concatenata Confronto tra array e lista concatenata • Mettiamo a confronto le due strutture di dati per ciò che riguarda: • accesso • elemento successivo • inserimento e cancellazione • dimensione • spazio di memoria Accesso: lista concatenata Accesso: lista concatenata • L’accesso è sequenziale: per accedere ad un dato si deve scorrere la lista facendo una scansione lineare. • Di fatto si esegue una “ricerca” nella struttura esaminando i nodi fino a trovare il valore cercato: sia a di tipo ElemIntero, cerchiamo se a.info è uguale ad un valore x, iniziando “dal primo” elemento della struttura “fino all’ultimo” elemento della struttura. • Non si può ritornare indietro: si può solo vedere in avanti. • L’accesso al campo informazione si ottiene con: a.info x inizio • La struttura è accessibile da un riferimento inizio che “vede” il primo nodo. Si può anche gestire la struttura con un primo elemento privo di informazione: “nodo vuoto”. Accesso: array Successivo: lista concatenata • L’accesso è diretto: per accedere ad un dato si utilizza l’indice dell’array • Ogni nodo, tranne l’ultimo, contiene nel campo next la referenza (indirizzo di memoria) al nodo successivo. Se a è di tipo ElemIntero e a “vede” un nodo della lista, per passare al nodo successivo si memorizza in a il riferimento al nodo successivo: a = a.next; //riferimento a.next x • Se v è il nome dell’array, v[i] rappresenta l’accesso all’i-esimo elemento: nel nostro esempio x coincide con v[3]. • Con l’accesso diretto non c’è un ordine da rispettare: v[3], v[0], v[5], …: si può tornare indietro. a 15 Successivo: array • Dato un elemento nella posizione i , v[i], il successivo (se i ≠ v.length-1) si trova nella posizione i+1; per passare al successivo si fa: v[++i] oppure i = i+1; uso di v[i] Inserimento e cancellazione: lista concatenata • Per inserire un nuovo nodo si deve: 1) costruire il nuovo nodo 2) agganciarlo nella posizione voluta con assegnazioni sui riferimenti 1) costruzione del nuovo nodo: ElemIntero nuovo = new ElemIntero(); nuovo.info = x; 2) per poterlo agganciare bisogna sapere dove. v[i] v[i+1] Inserimento e cancellazione: lista concatenata Inserimento e cancellazione: lista concatenata • Bisogna trovare una “posizione” nella lista, dopo la quale effettuare l’inserimento del nuovo nodo: questa posizione si ottiene facendo la scansione lineare alla ricerca di un valore z (campo info) che dovrà essere presente nella lista. • Per cancellare un nodo (successivo) bisogna assegnare ad un riferimento next il valore del riferimento successivo: a.next = a.next.next; z a x nuovo.next=a.next a.next=nuovo a Inserimento e cancellazione: array Inserimento e cancellazione: array • Per inserire un nuovo dato nella i-esima posizione si deve: • 1) aumentare la lunghezza dell’array, se l’array è pieno: costruire un nuovo array w • 2) copiare i valori fino alla posizione i nel nuovo array, inserire il nuovo elemento, copiare i rimanenti valori (copiare dall’ultimo fino all’i-esimo sul successivo e poi inserire). • 2 bis) se non serve la posizione intermedia, si può aggiungere il nuovo dato alla fine. • Per cancellare un dato dalla i-esima posizione si deve: • 1) ricopiare gli elementi a partire dalla posizione i+1 sul precedente • 1 bis) se l’elemento da togliere è unico e non interessa l’ordine, si può copiare l’ultimo sull’i-esimo posto. 16 Dimensione: lista concatenata Dimensione: array • La dimensione non è fissata: non c’è un limite sulla dimensione massima, l’unico limite è lo spazio di memoria: non bisogna mai ridimensionare una lista concatenata. • L’array è a dimensione fissa. • Possiamo risolvere il problema con la tecnica del raddoppio, riassegnando il riferimento della nuova area di memoria al vecchio riferimento. • Si può gestire male la memoria e occuparla tutta: se viene esaurita la memoria disponibile la JVM interrompe l’esecuzione segnalando: OutOfMemoryError • Anche in questo caso il limite è lo spazio di memoria complessivo (OutOfMemoryError). Spazio di memoria: lista concatenata Spazio di memoria: array • La lista concatenata occupa più spazio: ogni nodo è composto da due campi: • l’informazione • il riferimento • Se anche l’informazione è un oggetto, il nodo è composto da due riferimenti, uno dei quali vede l’elemento successivo e l’altro l’oggetto. • L’array occupa meno spazio: • c’è solo l’informazione che deve essere memorizzata • Conclusione. • L’array richiede “spostamento” di dati (O(n)) nel caso di inserimento e cancellazione, che per la lista sono O(1); possiede invece accesso diretto, che è O(1), mentre la lista accesso sequenziale, che è O(n). Pertanto il tipo di problema suggerirà quale struttura di dati sia più idonea: molti accessi e poche modifiche oppure pochi accessi e molte modifiche. Lista concatenata Pila su lista concatenata • Se la lista concatenata non ha una dimensione massima dobbiamo però individuarne la fine: un valore di riferimento dal quale non si acceda ad alcun nodo. Tale valore è null. • Vogliamo ora realizzare il TDA Pila di interi su una lista concatenata: dovremo realizzare gli assiomi: • verifica se la Pila è vuota • guarda la testa • inserisci in testa • estrai la testa • inizio 17 Pila su lista concatenata • Dobbiamo avere un riferimento al primo nodo per poter accedere alla struttura di dati: ElemIntero primo; Pila su lista concatenata • Dobbiamo inserire un elemento in testa: • 1) costruiamo un elemento: ElemIntero nuovo = new ElemIntero(); nuovo.info = valore; • Dobbiamo rappresentare la situazione: Pila vuota. Se la Pila è vuota, non c’è alcun elemento nella Pila, quindi primo non contiene “nulla”: primo = null; • 2) agganciamo nuovo in testa: nuovo deve diventare la nuova testa: Pila su lista concatenata Pila su lista concatenata • Queste assegnazioni valgono anche quando si inserisce il primo elemento, partendo da lista vuota: nuovo.next = primo; //aggancio alla Pila primo = nuovo; //nuovo diventa la testa • Quando la lista è vuota, primo = null; quindi la prima assegnazione memorizza null nel campo nuovo.next: il primo elemento è anche l’ultimo. • La seconda assegnazione lo fa diventare il primo elemento della lista. Pila su lista concatenata • Dobbiamo guardare la testa: se la Pila non è vuota: nuovo.next = primo; //aggancio alla Pila primo = nuovo; //nuovo diventa la testa • Dobbiamo estrarre la testa: se la Pila non è vuota: primo = primo.next; se la Pila è vuota: lanciamo l’eccezione EmptyStackException • Se nella Pila c’è un solo elemento, con l’istruzione precedente si vuota la Pila: infatti, primo.next=null (l’unico è anche l’ultimo), pertanto primo diventa null e quindi la Pila è vuota. Pila su lista concatenata public class PilaListaConc { // Pila di interi realizzata con // lista concatenata primo.info; se la Pila è vuota: lanciamo l’eccezione EmptyStackException • Vediamo pertanto la classe che realizza una Pila di interi su una lista concatenata. private ElemIntero primo = null; public boolean vuota() {// isEmpty() if(primo == null) return true; else return false; } 18 Pila su lista concatenata public void inserisci (int elem) { // push ElemIntero nuovo = new ElemIntero(); nuovo.next= primo; nuovo.info=elem; primo = nuovo; } Pila su lista concatenata public void estrai() {// pop if ( !vuota() ) primo = primo.next; else throw new EmptyStackException(); } public int testa () {// top if ( !vuota() ) return primo.info; else throw new EmptyStackException(); } Pila su lista concatenata Complessità delle operazioni della Pila /** Classe interna: la classe e' privata ma le sue variabili d'istanza sono visibili ai metodi della classe PilaListaConc */ private class ElemIntero { int info; ElemIntero next; } }//fine PilaListaConc • Vogliamo calcolare la complessità delle operazioni che riguardano la realizzazione degli assiomi della Pila. • Le prestazioni dipendono dalla struttura di dati e non dal TDA. • Caso1. Il tempo di esecuzione di ogni operazione su una Pila realizzata con array di dimensioni fisse è costante: abbiamo solo un numero costante di assegnazioni, confronti e ritorno di valore. Il tempo non dipende dalla dimensione n della struttura dati: quindi O(1). Complessità delle operazioni della Pila Complessità delle operazioni della Pila • Caso2. Nella realizzazione con array ridimensionabile, l’unica cosa che cambia è l’operazione inserisci (push). • La realizzazione con array ridimensionabile alcune volte richiede un tempo O(n): • Cerchiamo di valutare il costo medio: questo metodo di stima si chiama analisi ammortizzata delle prestazioni asintotiche. • Per ogni elemento inserito il costo è O(1) • Quando l’array è pieno il ridimensionamento comporta un ciclo con costo O(n): n inserimenti a costo O(1) 1 inserimento a costo O(n) • tale tempo è necessario per copiare tutti gli elementi nel nuovo array, all’interno del metodo raddoppio. • il ridimensionamento viene fatto ogni n operazioni 19 Complessità delle operazioni della Pila Complessità delle operazioni della Pila • Con la notazione O-grande valgono le seguenti relazioni: • Pertanto: costomedio = (n · O(1) + 1 · O(n)) / (n+1) = = (O(n) + O(n)) / (n+1) = = O(1) • Distribuendo il tempo, speso per il ridimensionamento, in parti uguali su tutte le operazioni push si ottiene ancora O(1). • Le operazioni sono tutte O(1), tranne push che è O(1) in media. O(1) = c O(n) = c · n ⇒ n·O(1) = c · n = O(n) O(n) + O(n) = 2 · c · n = O(n) O(n)/O(n) = c · n/c · n = 1 = O(1) Complessità degli assiomi della Pila sulle due strutture di dati lista concatenata 1) isEmpty: if(…) O(1) 2) pop: primo=primo.next O(1) 3) top: restituisce un valore O(1) 4) push: nuovo + 2 assegnazioni O(1) array O(1) sp — O(1) Strutture di dati per numeri e oggetti O(1) raddoppio O(1) in media Strutture di dati per numeri e oggetti • Come abbiamo portato agli oggetti le proprietà dei numeri (Misurabile, Comparable) così utilizziamo le strutture di dati per memorizzare sia numeri che oggetti: array di numeri e array di oggetti, lista concatenata di numeri e lista concatenata di oggetti. • In realtà dovremmo dire: array e lista concatenata di riferimenti a oggetti. • Infatti la struttura di dati gestisce i riferimenti per le operazioni di inserimento, cancellazione, ecc. Numeri e oggetti • Array numeri campi oggetti 20 Strutture di dati per numeri e oggetti Numeri e oggetti • Lista concatenata numeri • • • Quando vogliamo capire come funziona una struttura di dati o cosa fa un TDA, ci basta trattare numeri. • Quando vogliamo gestire un problema più generale ed utilizzare gli oggetti, aggiungiamo al TDA e alla struttura di dati le caratteristiche dell’oggetto. oggetti Strutture di dati per numeri e oggetti Strutture di dati per numeri e oggetti • Quando abbiamo introdotto la struttura dati array, l’abbiamo vista per gestire interi (reali). • Siamo poi passati a considerare array su oggetti. • Per memorizzare informazioni che riguardavano degli studenti abbiamo introdotto la classe Stud, per accedere al campo matricola dell’i-esimo studente dobbiamo individuare la posizione e poi il campo, tramite un metodo di accesso: corso23[i].matricola() • Se vogliamo costruire una lista concatenata per informazioni di tipo Stud dovremo definire il nodo: class ElemStud{ Stud info; ElemStud next; } e per accedere al campo matricola da un riferimento a che vede il nodo: a.info.matricola() 21