Sei sulla pagina 1di 17

PROGRAMMAZIONE 2

Appunti del Corso


(L. Lesmo - febbraio 2009)

PARTE TERZA Lereditariet

INDICE Indice dei Capitoli 3.


3.1 3.2 3.3 3.4 3.5

Lereditariet
Categorie

. .. Ereditariet delle variabili di istanza . Ereditariet di base dei metodi Compatibilit di tipo e casting Classi astratte e interfacce

2 2 3 6 10 12

Indice delle Procedure e dei Programmi


Proc.1 Alcuni metodi della classe Student 4-5 Proc.2 Il costruttore della classe Student ... 8 Proc.3 Definizione della classe Person .. 8 Proc.4 Il metodo getName della classe Student ... 9 Proc.5 La classe StudenteCollaboratore . 10 Proc.6 La classe di eccezione OreInEccesso .. 10 Proc.7 Aggiunta ore ad uno StudenteCollaboratore 10 Proc.8 Una definizione (molto parziale) della classe Insegnante 13 Proc.9 Una definizione (molto parziale) della classe Professore 13 Proc.10 Una definizione (molto parziale) della classe Ricercatore . 13 Proc.11 Come si definisce uninterfaccia 15 Proc.12 Come si usa uninterfaccia .... 15

Indice delle figure


Fig.1 Unorganizzazione della conoscenza in categorie e sotto-categorie . Fig.2 Unistanza della classe Student .. Fig.3 La funzione di super (vale non solo per i costruttori) Fig.4 Una gerarchia estesa di sottoclassi di Person ... Fig.5 Una gerarchia ad ereditariet multipla ... Fig.6 La classe Insegnante implementa linterfaccia PagaTasse .. 2 3 8 12 14 15

3.

Lereditariet

3.1 Categorie
La visione del mondo che hanno gli esseri umani, e quindi anche il loro comportamento, organizzato in termini di categorie. ANIMALE e PERSONA sono categorie, AUTOMOBILE e PIZZERIA sono categorie, CORRERE e STUDIARE sono categorie. Le categorie includono individui: il mio cane un ANIMALE, io sono una PERSONA, la mia Delta unAUTOMOBILE, la pizzeria dove ho cenato ieri un PIZZERIA, il movimento che ho fatto per prendere il tram laltro giorno un (anche se non si dice cos in italiano) CORRERE, lattivit che svolgerete per preparare lesame di Programmazione uno STUDIARE. E chiaro che lorganizzazione in categorie fondamentale per permetterci di muoverci nel mondo, in quanto si suppone che tutti gli individui di una stessa categoria abbiano alcune caratteristiche comuni. Cos, se debbo ottenere uninformazione da una persona, posso chiedergliela, assumendo che, come tutte le persone, comprenda il linguaggio (a parte se si tratta di uno straniero). Nello stesso modo, se decido di andare in pizzeria, so come ordinare, so cos un menu e la sua funzione, so che alla fine debbo pagare, anche se in quella particolare pizzeria non cero mai stato prima. Ovviamente, lidea di categoria in Java non nuova, in quanto corrisponde molto strettamente al costrutto class, cos come gli individui sono, in Java, delle istanze. Daltra parte, nella nostra organizzazione mentale le categorie non sono sempre separate le une dalle altre, ma ci sono anche categorie pi generali e categorie pi specifiche. Ad esempio PIZZERIA una sottocategoria (e cio una categoria pi specifica) di RISTORANTE. Le pizzerie sono cio come tutti i ristoranti, ma con alcune caratteristiche particolari, e cio che si pu mangiare la pizza e che spesso non ci sono molte altre alternative. La stessa cosa si pu dire per i RISTORANTI_CINESI, che sono come tutti i RISTORANTI, ma puoi trovarci del maiale in agro-dolce e difficilmente un risotto al barolo. E chiaro per che, a parte le differenze, non c bisogno che io impari come si ordina in un ristorante cinese o che si deve pagare il conto: questo si fa come in tutti i RISTORANTI, e quindi come nelle PIZZERIE. Questa organizzazione mostrata in fig.1, in cui ho aggiunto qualche nuova categoria.

ENTITA

RISTORANTE

AZIONE

PIZZERIA

RIST_CINESE

MUOVERSI

STUDIARE

CORRERE

CAMMINARE

Fig.1 Unorganizzazione della conoscenza in categorie e sotto-categorie Uno schema come quello di fig.1 viene spesso detto tassonomia e, come vedete, non altro che un albero come quelli che abbiamo studiato nel capitolo precedente. Lutilit di questa organizzazione dovrebbe essere chiara dallintroduzione: se debbo specificare come ci si comporta nei ristoranti in genere, nelle pizzerie e nei ristoranti cinesi, non debbo ripetere queste informazioni tre volte, ma sufficiente che le indichi in un posto solo, e cio in relazione alla categoria RISTORANTE: tanto, sapendo che le pizzerie sono dei ristoranti, ad esse si applicano tutte le informazioni utili che abbiamo sui ristoranti. Questo meccanismo, tramite cui alle PIZZERIE vengono applicate tutte le informazioni date per i RISTORANTI, detto ereditariet: se voi ricordate i termini padre e figlio, che si usano per indicare le relazioni tra nodi negli alberi, allora PIZZERIA figlio di RISTORANTE ed 2

abbastanza naturale dire che il figlio eredita le caratteristiche del padre. Naturalmente, se la tassonomia un albero, deve avere una radice; questo il motivo per cui, in fig.1, ho introdotto il nodo ENTITA, che pu sembrare strano, ma si deve intendere come la categoria pi generale possibile, che include tutte le altre categorie. Deve inoltre essere osservato che la relazione categoria-categoria_pi_generale transitiva: se MUOVERSI una sopracategoria di CORRERE e AZIONE una sopracategoria di MUOVERSI, allora AZIONE una sopracategoria di CORRERE. E lereditariet si applica anche a diversi livelli: se tutte le AZIONI hanno un Agente, allora anche tutte le istanze di MUOVERSI hanno un Agente (colui che si muove), e anche tutte le istanza di CORRERE hanno un agente (colui, o colei, che corre).

3.2 Ereditariet delle variabili di istanza


Lorganizzazione tassonomica che ho descritto sopra utilizzata anche in Java; poich in Java si parla di classi e non di categorie, si dir che RISTORANTE una sopraclasse di PIZZERIA e che PIZZERIA una sottoclasse di RISTORANTE. E in Java c anche una classe che corrisponde a ENTITA: come naturale, essa si chiama Object; abbiamo detto pi volte che tutte le istanze in Java sono oggetti: adesso sappiamo che ci significa che esse appartengono a qualche classe che necessariamente sottoclasse di Object (perch Object la radice dellalbero). Per esprimere il fatto che una classe sottoclasse di unaltra, sufficiente inserire, nellintestazione della classe, la specifica extends, che in realt noi abbiamo gi visto nel caso delle eccezioni. Supponiamo di aver introdotto la classe Person, nel modo seguente: public class Person { private String nome; private String cognome; private String indirizzo; public void setAddress (String address) {indirizzo = address;} public String getAddress () {return indirizzo;} } // variabile di istanza // variabile di istanza // variabile di istanza // due metodi

La definizione molto semplice e non mi sembra richieda particolari commenti. Vogliamo ora definire la classe Student. Gli studenti sono persone, e quindi hanno un nome, un cognome e un indirizzo. Ma, oltre a questo, hanno anche un numero di matricola un carico didattico e un elenco di esami sostenuti. Poich esiste gi la classe Person, inutile ridefinire le informazioni gi presenti in tale classe; invece sufficiente specificare che Student una sottoclasse di Person1: public class Student extends Person { private String matricola; private String [] caricoDidattico; private String [] esamiSostenuti; } // variabile di istanza // variabile di istanza // variabile di istanza

Se noi definiamo la classe Student in questo modo, otteniamo leffetto mostrato in figura 2:
cognome caricoDidattico esamiSostenuti

nome

indirizzo

matricola

Fig.2 Unistanza della classe Student


1

Nel caso delle classi, come Person e quasi tutte quelle che abbiamo visto finora, che non hanno una clausola extends, Java assume implicitamente extends Object, e cio che la nuova classe sia una sottoclasse diretta di Object, che, come abbiamo detto, la classe pi generale (lanalogo di ENTITA in fig.1). 3

Come si vede dalla figura, le istanze di Student hanno 6 variabili e non 3; infatti, tre di esse sono definite localmente (matricola, caricoDidattico, esamiSostenuti), mentre le altre tre sono ereditate dalla sopraclasse Person (nome, cognome, indirizzo). Tutto molto semplice; bisogna solo fare attenzione ai nomi delle variabili di istanza! Supponiamo, infatti che nella definizione di Student sia inserita la variabile indirizzo, intendendo con questo lindirizzo di studi. In tal caso vi sarebbe una collisione di nomi: quando si preleva indirizzo da una istanza di Person, si ottiene effettivamente lindirizzo dellabitazione, ma se si lavora su una istanza di Student, indirizzo si riferisce allindirizzo di studi e non c pi modo di ottenere per uno studente lindirizzo di abitazione!2 Questo il modo con cui sono ereditate le variabili di istanza. Ma tali variabili non sono le uniche informazioni utili associate alle classi; ci sono anche i metodi. Cosa possiamo dire per lereditariet dei metodi? Innanzitutto che essa opera in modo simile a quella delle variabili, anche se, ovviamente, i metodi si comportano in modo completamente differente: infatti non ci sono metodi nelle istanze (v.fig.2), ma, inviando messaggi alle istanze si ottiene accesso ai metodi della classe. E quali sono i metodi della classe Student? Come ho detto, lereditariet opera come nel caso delle istanze, per cui, anche se non ci sono definizioni di metodi nella classe Student, le istanze di Student sono in grado di rispondere ai messaggi setAddress e getAddress, che sono ereditati dalla sopraclasse Person. Naturalmente, come nel caso delle variabili, possibile aggiungere nuovi metodi, quali, ad esempio, quelli per cambiare il carico o per aggiungere un nuovo esame a quelli dati. Per spiegare come funzionano tali metodi, anche se questo non centra molto con lereditariet, due parole sulle variabili caricoDidattico e esamiSostenuti. Suppongo che il numero massimo di esami sia 50, e che ambedue le variabili (che sono delle array di stringhe) abbiano i dati compattati allinizio. In altre parole, se uno studente ha dato 15 esami, gli elementi da esamiSostenuti[0] a esamiSostenuti[14] hanno un valore diverso dalla stringa vuota, mentre tutti gli altri sono null. La stessa cosa per il caricoDidattico. Il metodo esameInCarico include due variabili booleane: la prima (trovato) dice se si trovato lesame che si sta cercando; la seconda (fail) dice se si trovata la lista vuota. Si procede nel while fin quando ambedue le variabili sono false e non si sono esaminati tutti gli esami del carico. Alluscita del while, trovato sar diventata true, se lesame c, altrimenti avr mantenuto il suo valore originale false. E quindi non c altro da fare che restituire trovato. Si noti che la variabile fail non strettamente necessaria: serve solo ad evitare di esaminare tutte le stringhe vuote fino al 50esimo esame: quando si trova la prima, inutile andare avanti. cambiaCarico, quindi, opera nel modo seguente: cerca vecchioEsame (quello da cambiare), usando esameInCarico. Se lesame c sostituisce lesame vecchio con quello nuovo (con un while del tutto simile a quello dellaltro metodo). Altrimenti, invia uneccezione EsameNonTrovato, per segnalare che si vuole cambiare un esame che non c nel carico didattico attuale. A questo punto, le istanze di classe Student possono rispondere ai messaggi esameInCarico, cambiaCarico, aggiungiEsameDato, che sono definiti localmente, ma anche ai messaggi setAddress e getAddress, che sono ereditati dalla classe Person.

public class Student extends Person { private String matricola; private String [] caricoDidattico = new String [50]; private String [] esamiSostenuti = new String [50]; public boolean esameInCarico (String esame) { int i = 0; boolean trovato = false; boolean fail = false; int numEsami = caricoDidattico.length;
2

// variabile di istanza // variabile di istanza // variabile di istanza

In realt, la variabile indirizzo ereditata presente nella istanza di classe, ma non vi alcun modo per accedervi. 4

while ( i < numEsami && !trovato && !fail) { if (caricoDidattico[i] == null) fail = true; else { if (caricoDidattico[i].equals(esame)) trovato = true; else i++; } // fine del primo else } // fine body del while return trovato; } // fine metodo esameInCarico public void cambiaCarico (String vecchioEsame, String nuovoEsame) throws EsameNonPresente { if (! esameInCarico (vecchioEsame)) { throw new EsameNonPresente ();} else { int i = 0; boolean trovato = false; while ( !trovato ) { if (caricoDidattico[i].equals(vecchioEsame)) { caricoDidattico[i] = nuovoEsame; trovato = true; } else { i++; } } // fine body del while } // fine primo else } // fine metodo cambiaCarico public void aggiungiEsameDato (String nuovoEsame) throws EsameNonPresente { if (!esameInCarico (nuovoEsame)) { throw new EsameNonPresente ();} else { int i = 0; boolean fatto = false; while ( !fatto ) { if (esamiSostenuti[i] == null) { esamiSostenuti [i] = nuovoEsame; fatto = true; } else { i++; } } // fine body del while } // fine primo else } // fine metodo aggiungiEsameDato } Proc.1 Alcuni metodi della classe Student

Sebbene i metodi che abbiamo visto operino correttamente, non abbiamo introdotto nessun costruttore, n per Student, n per Person. Questo corretto, perch un costruttore comunque ereditato dalla classe pi generale (Object). Ovviamente, tale costruttore cos generale che non pu fare operazioni specifiche delle singole classi, e quindi non fa altro che creare istanze vuote, e cio istanze che per le variabili di istanza di tipo stringa assumono il valore null, per quelle intere 0, etc. Poich invece non sembra avere molto senso creare unistanza di Person o Student senza avere almeno il nome e cognome, possiamo introdurre dei costruttori. Potremmo innanzitutto preoccuparci di Student, definendo un costruttore del tipo: public Student (String name, String surname, String matr) { nome = name; cognome = surname; matricola = matr; } Purtroppo questo non va, in quanto produce un errore. E importante capire bene questo tipo di errore per comprendere come funziona lereditariet delle variabili: la variabile nome (e anche cognome) non pu essere usata in Student perch non definita nella classe Student!!! A livello di istanza, la variabile c, ma a livello di definizione, essa non compare nella classe (perch ereditata): essendo definita come private in Person, essa non pu essere usata al di fuori di tale classe, neanche nelle sue sottoclassi. Ovviamente, potremmo definire nome e cognome come public; ma questo proprio ci che non dobbiamo fare! nome e cognome sarebbero accessibili da tutte le classi, non solo da Student, e ci saremmo persi lincapsulamento di Java. La soluzione quella che conosciamo gi, e cio quella di definire (in Person) dei metodi di accesso e modifica per le variabili di istanza (come abbiamo gi fatto per indirizzo): public void setName (String name) {nome = name;} public String getName () {return nome;} public void setSurname (String surname) {cognome = surname;} public String getSurname () {return cognome;} Ora, il costruttore per Student potr essere: public Student (String name, String surname, String matr) { setName (name); setSurname(surname); matricola = matr; } Adesso le cose funzionano, ma notate che qui sta succedendo qualcosa di strano! Quando si esegue setName(name), non c nessun metodo setName nella classe Student! Eppure la definizione del costruttore corretta, come vedremo nel prossimo capitolo.

3.3 Ereditariet di base dei metodi


I metodi vengono ereditati in modo molto simile alle variabili di istanza. Ricordiamo, per, che nella istanza vera e propria (spazio di memoria allocato nello heap) sono presenti le variabili di istanza, ma non i metodi, che vengono sempre prelevati (quando necessario) dalla definizione della classe. Questa differenza molto importante: quando si crea unistanza di Student le informazioni associate (variabili di istanza) sono raccolte nellistanza, sia che esse provengano da Student, sia che provengano da Person. Di conseguenza, come si osservato a pag. 4, se si definisce una variabile indirizzo per Person e una variabile matricola in Student, nellistanza reale di Student ci sar lo spazio sia per lindirizzo che per la matricola.

Per i metodi, come si detto, lereditariet ha effetti simili a quelli sulle variabili di istanza. Quando viene inviato un messaggio ad unistanza, se esiste un metodo appropriato definito localmente, viene eseguito tale metodo, altrimenti si verifica se il metodo esiste nella sopraclasse, altrimenti nella sopraclasse della sopraclasse, e cos via, fino ad arrivare, se necessario, fino a Object. Nel nostro caso, il metodo setName non presente nella classe Student, e quindi Java verifica se si trova nella sopraclasse (Person). Poich il metodo c, viene eseguito setName di Person (che proprio quanto si voleva). Torniamo ora ai costruttori. Abbiamo definito il costruttore per Student. Possiamo ora preoccuparci di Person (di solito si fa il contrario: prima si definisce la classe pi generale, poi quella pi specifica). Il costruttore per Person pu essere simile a quello per Student, ma senza linizializzazione della matricola, che in Person non c: public Person (String name, String surname) { nome = name; cognome = surname; } Sebbene questa definizione sia giusta, purtroppo produce un nuovo errore nella definizione del costruttore di Student. Quello che successo un po complicato. Cerchiamo di spiegarlo per punti: 1. Quando viene richiesta una new di Student, viene sempre richiesta (implicitamente) una new di Person (proprio per inserire nella nuova istanza di Student anche le variabili ereditate da Person). 2. A meno si specifichi il contrario (vedremo come), la new richiesta su Person senza parametri (new Person();) 3. Prima di introdurre il costruttore di Person (quello visto sopra), la new senza parametri, che non era definita in Person, veniva ereditata dalla classe Object, e quindi non cerano problemi. 4. Quando definisco il nuovo costruttore localmente in Person, leredit da Object viene bloccata (secondo lo schema dellereditariet di metodi, in cui il metodo locale prevale su quello ereditato). E quindi il metodo new di Object non pu pi essere usato. 5. Si verifica quindi un errore, perch, per Person, non pi disponibile un metodo new senza parametri. Una prima soluzione semplice: introdurre il metodo new senza parametri in Person: public Person (String name, String surname) { nome = name; cognome = surname; } public Person () {} C per unaltra strategia, pi elegante. Possiamo osservare che, a questo punto, i costruttori di Person e Student sono molto ripetitivi. In ambedue i casi vengono inizializzate le variabili nome e cognome, con alcune difficolt in Student (abbiamo dovuto usare i metodi setName e setSurname), e abbiamo dovuto definire un (inutile?) costruttore vuoto per Person. La seconda strategia basata sulla seguente idea: ma se c gi un metodo in Person che inizializza nome e cognome, non possiamo usare quello anche per Student, usando ereditariet? Si e no! No, se si cerca di usarlo direttamente: il metodo new non pu essere ereditato, perch c quello definito localmente. Si, se si usa un artificio, consentito in Java: se cio si richiede esplicitamente di usare il metodo della sopraclasse. Questo si pu fare con la parola chiave super, ed possibile proprio grazie alle considerazion che facevamo allinizio di questo capitolo: contrariamente alla variabili di istanza, il metodo pi generale c, nella definizione della sopraclasse, anche se se ne definito uno locale: bisogna solo sapere come andare a prenderlo. Nel caso dei costruttori, sufficiente scrivere super (argomenti). Vedremo che super si pu usare, in generale, anche per metodi standard, indicando il nome del metodo. Nel nostro caso, dovremo quindi inserire, nel costruttore di Student, lespressione: super(name,surname); per ottenere leffetto voluto. Riportiamo ora, sotto, il costruttore della classe Student e, poi, una versione completa della classe Person.

public class Student { . // variabili di istanza public Student (String name, String surname, String matr) { super ( name, surname); matricola = matr; } .. // altri metodi di Student } Proc.2 Il costruttore della classe Student public class Person { private String nome; private String cognome; private String indirizzo; public Person (String name, String surname) { nome = name; cognome = surname); } public void setAddress (String address) {indirizzo = address;} public String getAddress () {return indirizzo;} public void setName (String name) {nome = name;} public String getName () {return nome;} public void setSurname (String surname) {cognome = surname;} public String getSurname () {return cognome;} }

// variabile di istanza // variabile di istanza // variabile di istanza // costruttore

Proc.3 Definizione della classe Person

Person

Costruttore: name, surname super (= comincia la ricerca dalla sopraclasse)

Student

Costruttore:

+ matricola

Fig.3 La funzione di super (vale non solo per i costruttori) Ora che abbiamo risolto il problema del costruttore, possiamo passare ai metodi standard. Come si detto sopra, lereditariet dei metodi avviene cercando un metodo locale che consenta di rispondere al messaggio; in assenza di esso, si cerca un metodo opportuno nella sopraclasse, e cos via. Questo significa che, nel nostro esempio, le istanze della classe Student possono rispondere a (a parte new, di cui abbiamo parlato a lungo sopra): Metodi definiti localmente esameInCarico (String esame) cambiaCarico (String vecchioEsame, String nuovoEsame) aggiungiEsameDato (String nuovoEsame)

Metodi ereditati setAddress (String address) getAddress () setName (String name) getName () setSurname (String surname) getSurname () Supponiamo ora che, per gli studenti, ma non per tutte le persone, noi vogliamo imporre che il metodo getName restituisca non solo il nome, ma la concatenazione di nome e cognome. Questo si pu fare molto facilmente, definendo di nuovo il metodo getName nella classe Student. In questo caso, si dice che il metodo originale getName di Person stato soprascritto dal metodo locale avente lo stesso nome. Notate per che questo caso ben diverso da quello delle variabili: infatti anche il metodo originale disponibile, come si pu vedere nellimplementazione della nuova getName. Nella nuova getName, noi dobbiamo comunque prelevare il nome dello studente (oltre al cognome), ma questo non banale: non possiamo usare la variabile nome ( privata della sopraclasse), e neppure possiamo usare getName (altrimenti ci riferiremmo a quello locale di Student, che stiamo definendo!). Dobbiamo avere un modo per dire: per fare la seguente operazione, usa il metodo getName della sopraclasse. Ma noi abbiamo gi visto qualcosa di questo tipo: e cio super, che abbiamo usato nel costruttore. Lo stesso super possiamo usare in questo caso, anche se le modalit di utilizzo sono un po diverse: anzich super (parametri) scriveremo: super.nomeMetodo (parametri) In questo modo, la definizione del metodo getName di Student sar: public class Student { . public String getName () { return super.getName() + + getSurname () } .. // altri metodi di Student } Proc.4 Il metodo getName della classe Student Si noti che mentre per getName siamo obbligati ad usare super, per getSurname questo non necessario (anche se si potrebbe fare). Quindi, riassumendo, in una sottoclasse: 1. 2. 3. Si possono definire metodi nuovi, che non sono ereditabili dalla sopraclasse (es. getMatricola) Si possono usare metodi ereditati dalla sopraclasse (es. getSurname) Si possono ridefinire metodi gi presenti nella sopraclasse (es. getName). Nel caso in cui serva la definizione della sopraclasse, si pu usare la parola chiave super.

Al contrario, per le variabili di istanza, vi sono solo due alternative: 1. Si possono definire nuove variabili, locali della sottoclasse (es. matricola) 2. Si possono usare variabili ereditate dalla sopraclasse (es. nome), anche se le variabili private della sopraclasse non sono accessibili direttamente. La terza opzione (definizione di nuove variabili con lo stesso nome di altre che gi ci sono nella sopraclasse (es. indirizzo) possibile, ma maschera, cio rende inaccessibili le corrispondenti variabili della sopraclasse. Prima di concludere questo capitolo, osserviamo che tutto quanto detto si pu applicare a gerarchie di pi livelli, per cui una sottoclasse pu a sua volta essere sopraclasse di unaltra classe. Ad esempio, possiamo introdurre la classe StudenteCollaboratore nel modo seguente:

public class StudenteCollaboratore extends Student { private int oreTotali; private int oreFatte; . public void nuoveOre (int numeroOre) { if (oreFatte+numeroOre <= oreTotali) { oreFatte = oreFatte + numeroOre; } else { throw new OreInEccesso ( oreFatte, oreTotali, getName() ) } } Proc.5 La classe StudenteCollaboratore In cui abbiamo introdotto, pi che altro per ricordare come funzionano, leccezione OreInEccesso, che potrebbe essere definita come nel riquadro seguente. Il costruttore delleccezione memorizza nelle variabili istanza nomeStud, oreF e oreT le informazioni sul nome, sulle ore fatte e sulle ore totali previste per lo studente per cui stata sollevata leccezione. Mette inoltre a disposizione i metodi getName, getOreF e getOreT per recuperare tali valori. Notate che ho di proposito definito il metodo getName, per sottolineare che questo metodo non ha nulla a che vedere con i metodi di Person e Student che abbiamo visto in precedenza, visto che la classe OreInEccesso non sottoclasse n sopraclasse di Person o Student. public class OreInEccesso extends Exception { private String nomeStud; private int oreF; private int oreT; public OreInEccesso (int ore1, int ore2, String nS) { oreF = ore1; oreT = ore2; nomeStud = nS; } public String getName () { return nomeStud; } public int getOreF () { return oreF; } public int getOreT () { return oreT; } }

// costruttore

Proc.6 La classe di eccezione OreInEccesso Il main pu richiedere laggiunta di nuove ore ad uno studente collaboratore come mostrato nel riquadro Proc.7. public class StudentMain { public void main (String [] args) StudenteCollaboratore nextSC; try { nextSC.nuoveOre (4); } catch (OreInEccesso exceptionVar) { System.out.println ( Lo studente + exceptionVar.getName () + ha fatto pi ore del previsto ); System.out.println ( Ore Fatte: + exceptionVar.getOreF () ); System.out.println ( Ore Totali: + exceptionVar.getOreT () ); System.out.println ( Nuove ore: 4); } } Proc.7 Aggiunta ore ad uno StudenteCollaboratore

10

Ovviamente, in questo caso non sarebbe necessario estrarre le informazioni necessarie per la stampa dalla istanza di eccezione il cui riferimento si trova in exceptionVar, in quanto le stesse informazioni si trovano nella istanza nextSC. Ma, in generale, possibile che il main usi un metodo che aggiorna i dati di una tabella di studenti (non uno studente solo), per cui in tal caso non sarebbe possibile sapere per quale studente si verificato lerrore, se non lo specificasse leccezione.

3.4 Compatibilit di tipo e casting


Nel riquadro Proc.7 abbiamo usato la variabile nextSC, di tipo StudenteCollaboratore. Supponiamo ora di voler realizzare un elenco di studenti iscritti ad un esame. Possiamo definire unarray: Student [] elencoStud = new Student [20]; Ora, permessa la seguente assegnazione? elencoStud [8] = nextSC; La risposta s: la relazione extends che lega una classe con una sottoclasse dice proprio che tutte le istanza della sottoclasse sono anche istanze della sopraclasse. E quindi, tutti gli StudenteCollaboratore sono anche Student, e quindi i relativi riferimenti possono essere assegnati a variabili di tipo Student. Cosa succede, adesso, se proviamo a fare loperazione seguente? elencoStud [8].aggiungiOre(2); In questo caso, il compilatore Java solleva uneccezione di tipo: elencoStud[8] di tipo Student, e le istanze di Student non possono rispondere al messaggio aggiungiOre! Per il riferimento che abbiamo messo in elencoStud[8] un riferimento ad una istanza della classe StudenteCollaboratore, e quindi su di essa dovremmo poter fare le operazioni relative. Il problema che noi lo sappiamo, ma Java no: Java conosce solo il tipo che stato dichiarato. In realt, questo vero parzialmente: vero durante la compilazione (il compilatore non pu tenere traccia delle assegnazioni che saranno effettuate quando il metodo sar eseguito), ma non vero durante lesecuzione, quando la Java Virtual Machine conosce nel dettaglio le classi delle varie istanze. Gi, ma se il programma non pu essere compilato, non pu neanche essere eseguito! Per risolvere questo problema, si pu utilizzare un meccanismo che si era gi visto per le operazioni sulle variabili numeriche, cio quello del casting, e cio della forzatura di tipo. La seguente operazione, infatti, accettata dal compilatore Java3: ( (StudenteCollaboratore) elencoStud[8] ).aggiungiOre(2); E chiaro che potrebbero per poi esserci dei problemi in fase di esecuzione, se in realt in elencoStud[8] avessimo messo un riferimento ad uno studente che non uno studente collaboratore. Per evitare che ci si verifichi, possibile usare loperatore instanceof nel modo seguente: if (elencoStud [8] instanceof StudenteCollaboratore) { ( (StudenteCollaboratore) elencoStud[8] ) . aggiungiOre(2); } else { System.out.println ( Lo studente + elencoStud[8].getName() +
3

Si osservi, per, che questo casting funziona in modo un po diverso da quello che si era visto per i numeri. In quel caso, infatti, bisognava davvero cambiare il formato della rappresentazione (ad es. da int a double). Nel caso attuale, invece il dato su cui si lavora (listanza di Student) non viene cambiata. In altre parole, il primo tipo di casting richiede del lavoro in fase di esecuzione, mentre questo ha effetto solo sul compilatore. Si noti la doppia parentesi in ( (StudenteCollaboratore)elencoStud[8] ).aggiungiOre(2); essa necessaria perch, altrimenti, il casting lultima operazione che viene effettuata: in altre parole, Java prima invierebbe il messaggio (aggiungiOre) a elencoStud[8], e poi farebbe il casting sul risultato; la parentesi impone la priorit delloperazione di casting. 11

non uno studente collaboratore ); } Supponiamo ora di estendere la nostra gerarchia, come mostrato nella Fig.3. Definiamo ora unarray di Person: Person [] anagrafica = new Person [50]; Possiamo ora supporre di aver inserito un certo numero di istanze di Person nellarray (che per potrebbero essere di una qualsiasi delle sottoclassi di Person, come abbiamo visto). Scriviamo ora il seguente ciclo: public void stampaNomi (Person [] listaP) { System.out.println ( Elenco dei nomi : ); i = 0; lung = listaP.length; while (listaP [i] != null && i <= lung) { System.out.println ( anagrafica [i].getName () ); } }

Person

Student

Insegnante

Studente Collaboratore

Professore

Ricercatore

Fig.4 Una gerarchia estesa di sottoclassi di Person che pu essere richiamato con il messaggio: stampaNomi (anagrafica); Qual il risultato dellesecuzione di questo metodo, che stampa ci che si ottiene con getName? In particolare, verr stampato solo il nome, come previsto dal metodo getName di Person? La risposta no. O meglio, dipende dallistanza effettiva che presente in listaP[i] (e cio in anagrafica [i]). Se c uno Student (e quindi anche uno StudenteCollaboratore) verr usato il metodo getName di Student (e quindi verr stampato sia il nome che il cognome), se invece si tratta di una Person generica (o anche di un Insegnante, un Professore, o un Ricercatore) verr usato il metodo getNamedi Person (e quindi verr visualizzato solo il nome). Questo non deve stupire: il procedimento di Java per determinare il metodo quello che avevamo gi descritto prima: considera listanza, poi vedi di che classe , poi prendi il metodo desiderato nella classe e, se non c, cercalo nella sopraclasse. Osservate che qui il problema diverso da quello visto in precedenza (casting). Il compilatore non produce un errore, perch il metodo getName nella classe Person (quella dellarray) definito. Al contrario, nel caso precedente aggiungiOre non definito per la classe Student! Nellesempio attuale, il compilatore non sa che per alcuni degli studenti non verr usato il metodo getName che crede lui, ma questo in realt non provoca difficolt. Questo meccanismo, per cui istanze di tipo diverso possono rispondere allo stesso messaggio (getName nel nostro esempio) in modo diverso viene detto polimorfismo.

3.5 Classi astratte e interfacce


Riportiamo ora nei seguenti riquadri una possibile definizione (parziale) delle classi Insegnante, Professore e Ricercatore. Supponiamo che a un qualsiasi Insegnante, quando viene assunto, venga assegnato uno stipendio ed un ufficio. Per i Professore, ma non per i Ricercatore, esistono inoltre le variabili titolariet e secondoCorso: quando un professore viene assunto gli viene assegnato un corso stabile, il corso di cui titolare, e poi, tutti gli anni, gli viene assegnato un secondoCorso, che pu cambiare. 12

public class Insegnante { private int stipendio; private String ufficio; public Insegnante (String nome, String cognome, int stip, String uff) { super (nome, cognome); stipendio = stip; ufficio = uff; } public void modificaStipendio (int newStip) { stipendio = newStip; } . } Proc.8 Una definizione (molto parziale) della classe Insegnante public class Professore { private String titolariet; private String secondoCorso; public Professore (String tit, int stip, String uff) { super (stip, uff); titolariet = tit; } public void assegnaCorso (String corso) { secondoCorso = corso; } } Proc.9 Una definizione (molto parziale) della classe Professore public class Ricercatore { private String incarico; public void assegnaCorso (String corso) { incarico = corso; } } Proc.10 Una definizione (molto parziale) della classe Ricercatore Per la classe Ricercatore, invece esiste solo la variabile incarico, che un corso unico che pu cambiare (come il secondoCorso dei professori). Si noti che nella classe Ricercatore non definito un costruttore; come sappiamo, in tal caso viene ereditato quello della sopraclasse (Insegnante). Se nellUniversit gli unici insegnanti sono i professori e i ricercatori, si potrebbe voler impedire la creazione di istanze della classe Insegnante: tutti gli insegnanti debbono per forza essere o professori, o ricercatori. Per, nel main, un messaggio del tipo: Insegnante docXX = new Insegnante (Corrado, Caloni, 1500, U37); consentita. E come ben sapete, non servirebbe togliere il costruttore di Insegnante: questo farebbe s che il costruttore venisse ereditato da Person (anche se solo con gli argomenti nome e cognome). C per un modo per far s che un messaggio new venga bloccato, ed di dichiarare una classe come classe astratta. Questo si ottiene sostituendo lintestazione della classe Insegnante (v. Proc. 8), con: public abstract class Insegnante

13

In questo modo si dichiara che la classe Insegnante non pu avere istanze dirette. In altre parole, non sar possibile inviare il messaggio new alla classe Insegnante. Ovviamente, la classe pu avere delle istanze, ma solo indirettamente, in quanto il messaggio new pu sempre essere inviato alle classi Professore e Ricercatore, che non sono astratte (si dice che sono classi concrete). Ma allora, a cosa serve la classe Insegnante? Non si poteva rendere Professore e Ricercatore sottoclassi di Person, senza aggiungere una classe intermedia senza istanze? Ovviamente possibile; per chiaro che in questo caso tutte le informazioni comuni (stipendio, ufficio, il costruttore, ecc.; si veda la classe Insegnante) andrebbero ripetute due volte, separatamente, per le classi Professore e Ricercatore. Oltre al fatto che, con la nostra soluzione, possiamo anche introdurre unarray di Insegnante, che pu essere utile per vari scopi. Prima di passare alla interfacce, opportuno citare lesistenza di metodi astratti. Consideriamo di nuovo la definizione di Professore e Ricercatore. Ambedue includono un metodo assegnaCorso, anche se la definizione del metodo diversa (perch diverse sono le variabili coinvolte). Supponiamo ora che sia ammessa la definizione di nuove sottoclassi di Insegnante, ma poich tutti gli insegnanti (anche quelli delle nuove classi) devono avere almeno un corso, ci piacerebbe obbligare chi definisce una nuova sottoclasse a includere il metodo assegnaCorso. E chiaro che nella situazione attuale questo non possibile: nella definizione di una classe uno mette i metodi che ritiene opportuni. E per possibile inserire nella classe Insegnante il metodo astratto assegnaCorso, semplicemente inserendo in tale classe la seguente definizione: public abstract void assegnaCorso (String corso); Come si pu vedere, questo metodo non ha il body (non ci sono le parentesi graffe). Questo fa s che le cose da fare in risposta al messaggio assegnaCorso debbano essere specificate nelle sottoclassi, e quindi non permessa la definizione di una sottoclasse senza il metodo assegnaCorso. Ovviamente, le cose non funzionerebbero se si potesse avere unistanza diretta di Insegnante: come potrebbe rispondere tale istanza al messaggio assegnaCorso? E infatti i metodi astratti possono comparire solo nelle classi astratte: una classe concreta non pu avere metodi astratti (anche se una classe astratta pu anche non avere nessun metodo astratto). Per concludere, presentiamo una breve introduzione alle interfacce. Per comprendere lutilit delle interfacce, supponiamo sia definita la nuova classe PagaTasse. Questa pu essere una sottoclasse di Object (non solo le Person pagano le tasse). In questa classe sono definite norme e modalit per il pagamento delle tasse, con tutti i metodi relativi. Ora, noi sappiamo che gli Insegnanti, poich ricevono uno stipendio, devono pagare le tasse. Ma allora possiamo rendere Insegnante sottoclasse di PagaTasse, ottenendo una struttura come quella di Fig.4, detta ad ereditariet multipla.

Person

PagaTasse

Insegnante
Fig.5 Una gerarchia ad ereditariet multipla Purtroppo, questo in Java non permesso. In altre parole, non si pu scrivere: public class Insegnante extends Person, PagaTasse SBAGLIATO !!!

Vi sono vari motivi per questo ; il pi evidente che vi sarebbero dei grossi problemi se sia in Person che in PagaTasse fosse definito il metodo metodoXXX, senza che tale metodo appaia in Insegnante. Se si invia il messaggio metodoXXX ad una istanza di Insegnante (o, se essa astratta, ad unistanza di una delle sue sottoclassi), come si pu rispondere? Quale criterio sensato ci pu essere per scegliere se ereditare la definizione che compare in Person o quella che compare in PagaTasse? Una (molto parziale) soluzione offerta da Java tramite il meccanismo delle interfacce. Uninterfaccia va nella direzione delle classi astratte, ma molto oltre. Una classe astratta non pu avere istanze dirette, e neanche 14

uninterfaccia pu averne, ma almeno una classe astratta pu avere variabili di istanza e metodi concreti, mentre uninterfaccia non pu avere neanche quelli! Ma allora, a cosa serve? In effetti, collegare le interfacce al problema dellereditariet multipla un po azzardato: le interfacce non sono classi! Esse assolvono, possiamo dire, una funzione di controllo. Supponiamo che PagaTasse sia uninterfaccia, e che definisca il metodo (astratto) calcolaImportoAnnuale; allora tutte le classi ad essa collegate (ad esempio Insegnante, EsercizioCommerciale, SocietPerAzioni, ecc.) dovranno necessariamente definire il metodo concreto con lo stesso nome, altrimenti si verificher un errore! E utile riflettere un attimo sul termine interfaccia. Perch questo nome? Lidea che questi oggetti mettono a disposizione una serie di nomi di metodi (con i relativi parametri) che specificano a cosa possono rispondere le classi che sono connesse (si dice che implementano) linterfaccia. Sono cio un vero e proprio filtro verso lesterno (uninterfaccia, appunto, nel senso standard). Vediamo ora come si definisce e si usa uninterfaccia. Lo schema che vogliamo realizzare dunque quello di Fig.5. La definizione dellinterfaccia riportata in Proc.11. E sufficiente sostituire il termine interface a quello di class, e poi definire le intestazioni (le cosiddette firme) dei metodi. Non si inserisce la specifica public nei metodi, perch tutti i metodi di uninterfaccia sono per forza public. Si noti che il metodo calcolaImportoAnnuale richiede in input come argomento una variabile di tipo DatiIntroiti (una classe che in questo semplice esempio non definisco).

Person

PagaTasse

Insegnante
Fig.6 La classe Insegnante implementa linterfaccia PagaTasse public interface PagaTasse { int calcolaImportoAnnuale (DatiIntroiti tabella); } Proc.11 Come si definisce uninterfaccia public class Insegnante extends Person implements PagaTasse { . public int calcolaImportoAnnuale (DatiIntroiti tabella) { definizione del metodo che calcola le tasse che un insegnante deve pagare in termini dei suoi introiti annuali (tabella) } } Proc.12 Come si usa uninterfaccia Infine, nel riquadro Proc.12 si mostra come linterfaccia pu essere usata. Anche qui, non ho definito in realt il metodo che calcola quanto pagare (sarebbe stato piuttosto complicato), ma lo scopo dellesempio solo quello di illustrare il modo con cui le interfacce possono essere usate. Lunica cosa da osservare che, mentre una classe ha esattamente un sopraclasse (eventualmente Object)4, essa pu implementare nessuna, una, o pi di una
4

Con lovvia eccezione di Object, che, essendo la radice dellalbero, non ha sopraclassi. 15

interfaccia. Per concludere, osserviamo che, anche se uninterfaccia non pu includere variabili, essa pu per definire delle costanti. Le costanti sono sempre public static final, sono cio costanti di classe (static), pubbliche e, chiaramente, non modificabili (final). Ad esempio, linterfaccia PagaTasse pu includere la scadenzaPagamento, nel modo seguente: public interface PagaTasse { String scadenzaPagamentoTasse = 31 maggio; } Poich la classe Insegnante implementa linterfaccia PagaTasse, sar possibile prelevare da essa (non dalle sue istanze, perch le costanti di interfaccia sono static, anche se non lo si dice esplicitamente), il valore della costante: String scad = Insegnante.scadenzaPagamentoTasse;

16