Esercizi
- Home
- Esercizi Capitolo 6
Capitolo 6
Ereditarietà ed interfacce
Per questo capitolo eviteremo di far scrivere troppo codice al lettore. È molto importante invece
concentrarsi piuttosto sulle definizioni. Se non si conoscono bene tutte i concetti della teoria, si
finirà con lo scrivere codice incoerente dal punto di vista della filosofia ad oggetti.
Di seguito trovate gli esercizi del capitolo.
Per ogni esercizio, cliccando sulla traccia potete vedere la relativa soluzione.
Gli esercizi caratterizzati dall'icona sono
considerati i più complessi relativamente agli argomenti trattati.
Se preferite lavorare offline, è possibile scaricare tutti gli esercizi e le relative
soluzioni in formato PDF nella sezione download.
-
Esercizio 6.a) Object Orientation in Java (teoria), Vero o Falso:
1. L'implementazione dell'ereditarietà implica scrivere sempre qualche riga in meno.
2. La seguente dichiarazione di classe è scorretta:
public final class Classe extends AltraClasse {...}
3. L'ereditarietà è utile solo se si utilizza la specializzazione. Infatti, specializzando ereditiamo nella sottoclasse (o sottoclassi) membri della superclasse che non bisogna riscrivere. Con la generalizzazione invece creiamo una classe in più, e quindi scriviamo più codice.
4. La parola chiavesuper
permette di chiamare metodi e costruttori di superclassi. La parola chiavethis
consente di chiamare metodi e costruttori della stessa classe in cui ci si trova.
5. L'ereditarietà multipla non esiste in Java perché non esiste nella realtà.
6. Un'interfaccia funzionale è un'interfaccia che dichiara un unico metodo di default.
7. Una sottoclasse è più "grande" di una superclasse (nel senso che solitamente aggiunge caratteristiche e funzionalità nuove rispetto alla superclasse).
8. Supponiamo di sviluppare un'applicazione per gestire un torneo di calcio. Esiste ereditarietà derivata da specializzazione tra le classiSquadra
eGiocatore
.
9. Supponiamo di sviluppare un'applicazione per gestire un torneo di calcio. Esiste ereditarietà derivata da generalizzazione tra le classiSquadra
eGiocatore
.
10. In generale, se avessimo due classiPadre
eFiglio
, non esisterebbe ereditarietà tra queste due classi.
Soluzione
Falso, il processo di generalizzazione implica scrivere una classe in più e ciò non sempre implica scrivere qualche riga in meno.
Falso.
Falso, anche se dal punto di vista della programmazione la generalizzazione può non farci sempre risparmiare codice, essa ha comunque il pregio di farci gestire le classi in maniera più naturale, favorendo l'astrazione dei dati. Inoltre apre la strada all'implementazione del polimorfismo.
Vero.
Falso, l'ereditarietà multipla esiste nella realtà, e in Java esiste in una versione soft perché si eredita solo la parte funzionale.
Falso, è un'interfaccia che dichiara un unico metodo astratto.
Vero.
Falso, una squadra non "è un" giocatore, né un giocatore "è una" squadra. Semmai una squadra "ha un" giocatore ma questa non è la relazione di ereditarietà. Si tratta infatti della relazione di associazione.
Vero, infatti entrambe le classi potrebbero estendere una classePartecipante
.
Falso, unPadre
è sempre unFiglio
, o entrambe potrebbero estendere la classePersona
. -
Esercizio 6.b)
Data la seguente classe:
public class Persona { private String nome; public void setNome(String nome) { this.nome = nome; } public String getNome() { return nome; } }
Commentare la seguente classeImpiegato
evidenziando dove sono utilizzati i paradigmi object oriented: incapsulamento, ereditarietà e riuso.
public class Impiegato extends Persona { private int matricola; public void setDati(String nome, int matricola) { setNome(nome); setMatricola(matricola); } public void setMatricola(int matricola) { this.matricola = matricola; } public int getMatricola() { return matricola; } public String dammiDettagli() { return getNome() + ", matricola: " + getMatricola(); } }
Soluzione
public class Impiegato extends Persona { //Ereditarietà private int matricola; public void setDati(String nome, int matricola) { setNome(nome); //Riuso ed ereditarietà setMatricola(matricola); //Riuso } public void setMatricola(int matricola) { this.matricola = matricola; //incapsulamento } public int getMatricola() { return matricola; //incapsulamento } public String dammiDettagli() { //Riuso, incapsulamento ed ereditarietà return getNome() + ", matricola: " + getMatricola(); } }
-
Esercizio 6.c) Classi astratte e interfacce, Vero o Falso:
1. La seguente dichiarazione di classe è scorretta:
2. La seguente dichiarazione di classe è scorretta:public abstract final class Classe {...}
3. La seguente dichiarazione di interfaccia è scorretta:public abstract class Classe;
4. Una classe astratta contiene per forza metodi astratti.public final interface Classe {...}
5. Un'interfaccia può essere estesa da un'altra interfaccia.
6. Una classe può estendere una sola classe ma implementare più interfacce.
7. Il pregio delle classi astratte e delle interfacce è che obbligano le sottoclassi ad implementare i metodi astratti ereditati. Quindi rappresentano un ottimo strumento per la progettazione object oriented.
8. Un'interfaccia può dichiarare più costruttori.
9. Un'interfaccia non può dichiarare variabili ma costanti statiche e pubbliche.
10. Una classe astratta può implementare un'interfaccia.
Soluzione
Vero, i modificatori
abstract
efinal
sono in contraddizione.
Vero, manca il blocco di codice che definisce la classe.
Vero, un'interfaccia final non ha senso.
Falso.
Vero.
Vero.
Vero.
Falso, un'interfaccia non può dichiarare costruttori perché non si può istanziare.
Vero.
Vero. -
Esercizio 6.d)
Descrivere tutte le relazioni di ereditarietà tra le seguenti classi:
• Professore
• Studente
• Persona
• Cattedra
• Corso
• Aula
• Lezione
Soluzione
Persona potrebbe essere superclasse di
Professore
eStudente
, per tutto il resto non c'è ereditarietà.L'ereditarietà si testa sempre e comunque con la relazione "is a". Quindi risulta molto semplice verificare che per tutte le altre coppie di classi non è confermata questa relazione. -
Esercizio 6.e)
Se volessimo creare la gerarchia definita nell'esercizio precedente, tra
Studente
,Persona
eProfessore
quale potrebbe essere una classe astratta?
Soluzione
Indubbiamente la classe
Persona
potrebbe essere una classe astratta, ma ancheProfessore
eStudente
potrebbero essere dichiarate astratte nel caso si volessero a loro volta estendere con classi comeStudenteIngegneria
oProfessoreMatematica
. -
Esercizio 6.f)
Creare l'interfaccia (con commenti)
Musicale
che dichiara un metodo suona. Come dichiarereste questo metodo: statico, di default o astratto?
Soluzione
Premettendo che tutte le scelte object oriented sono soggettive, probabilmente l'implementazione astratta è la scelta più corretta per un concetto così astratto:
/** * Astrae il concetto di oggetto musicale. * * @author Claudio De Sio Cesari */ public interface Musicale { /** * Esegue la musica dell'oggetto musicale corrente. */ void suona(); }
-
Esercizio 6.g)
Creare due sottointerfacce (con commenti) di
Musicale
:StrumentoMusicale
eSuoneria
. Come dichiarereste il metodosuona
nelle due sottointerfacce: statico, di default o astratto?
Soluzione
Il listato dell'interfaccia
StrumentoMusicale
potrebbe essere il seguente:
/** * Astrae il concetto di strumento musicale. * * @author Claudio De Sio Cesari */ public interface StrumentoMusicale extends Musicale { }
Il listato dell'interfacciaSuoneria
potrebbe essere il seguente:
/** * Astrae il concetto di suoneria musicale. * * @author Claudio De Sio Cesari */ public interface Suoneria extends Musicale { }
Il metodosuona
per noi continua ad essere astratto. -
Esercizio 6.h)
Supponiamo di creare una classe
Smartphone
che implementa entrambe le interfacce definite nell'esercizio precedente. Cosa c'è di sbagliato?
Soluzione
Mentre potrebbe essere plausibile considerare uno strumento musicale uno smartphone che usufruisce di una determinata app, è semplicemente scorretto che possa essere considerato una suoneria, infatti il test "is a" fallisce:
Domanda: "uno smartphone è uno strumento musicale?"
Risposta: Sì (a patto che sia installata una app che lo renda tale)
Domanda: "uno smartphone è una suoneria?"
Risposta: No (semmai contiene suonerie) -
Esercizio 6.i) Interfacce dopo Java 8, Vero o Falso:
1. I metodi statici non si ereditano.
2. La seguente dichiarazione di interfaccia è scorretta:
3. La seguente dichiarazione di interfaccia è scorretta:public static interface Interface;
public interface class {...}
4. I metodi astratti di un'interfaccia non vengono ereditati da un'altra interfaccia.
5. I metodi astratti di un'interfaccia non possono essere implementati da un'altra interfaccia.
6. I metodi statici di un'interfaccia non possono essere implementati da un'altra interfaccia.
7. Un'interfacciaA
, definisce un metodo di defaultm()
. L'interfacciaB
estende l'interfacciaA
ridefinendo il metodom()
con un'implementazione di default. Una classe astrattaC
implementa l'interfacciaA
ma ridefinendo il metodom()
. Una classe concreta (non astratta)D
estende la classeC
e implementa l'interfacciaB
, senza ridefinire il metodom()
. La classeD
eredita il metodom()
definito nella classeC
.
8. Un'interfacciaE
, definisce un metodo astrattom()
. L'interfacciaF
estende l'interfacciaE
ridefinendo il metodom()
con un'implementazione di default. Una classe astrattaG
implementa l'interfacciaE
ma non ridefinisce il metodom()
. Una classe concreta (non astratta)H
estende la classe astrattaG
e implementa l'interfacciaF
, senza ridefinire il metodom()
. La classeH
non può essere compilata correttamente.
9. Un'interfaccia può estendere più interfacce.
10. Un'interfaccia può estendere una classe astratta e un'interfaccia.
Vero.
Vero, il modificatorestatic
non si può applicare a classi ed interfacce.
Vero, class è una parola chiave e non può essere utilizzata come identificatore.
Falso.
Falso.
Vero, in particolare è possibile riscrivere un metodo con la stessa firma in una sottointerfaccia (ma lo stesso concetto si applica alle classi), ma tecnicamente non si tratta di un override perché i metodi statici semplicemente non vengono ereditati. Infatti un eventuale uso nella sottointerfaccia dell'annotazione@Override
per marcare il nuovo metodo statico provocherà un errore in compilazione (cfr. listati SuperInterfaccia.java e SottoInterfaccia.java).
Vero, la regola 5 caratterizzata dalla massima "class always win", continua a valere anche se la classe interessata è astratta (cfr. listati A.java, B.java, C.java, D.java, TestABCD.java).
Falso, potrebbe trarre in inganno il fatto che la classe astrattaG
anche ereditando il metodo astratto dall'interfacciaE
faccia valere i motivi della regola "class always win". Invece in questo caso, vale la regola 4, ovvero vince l'implementazione più specifica. Infatti la classeG
, eredita un metodo dell'interfacciaE
, che è meno specifico di quello ridefinito nell'interfacciaF
(cfr. listati E.java, F.java, G.java, H.java, TestEFGH.java).
Vero, anche se potrebbe sorgere qualche dubbio, un'interfaccia può estendere più interfacce (cfr. listato MiaInterfaccia.java).
Falso, un'interfaccia non può estendere una classe in qualsiasi caso. -
Esercizio 6.j)
Riprendendo la soluzione dell'esercizio 5.ff, supponiamo di voler far sì che la nostra libreria venda anche album musicali. Astrarre quindi la classe
Album
, tenendo conto che anche per gli album musicali esiste un numero identificativo chiamato ISMN (anche se esistono altri modi di identificare un album come in lo standard europeo EAN), così come per i libri esiste il numero identificativo ISBN. Dopo verificare se sussiste una relazione di ereditarietà tra la classeLibro
e la classeAlbum
. Se esiste, implementare una soluzione. Per ora, non implementare i controlli che il metodosetGenere
della classeLibro
eseguiva affinché il genere specificato come parametro appartenesse ad un insieme predefinito di generi letterari (vedi esercizio 5.ff). Questo vale sia per la classeAlbum
, che per la classeLibro
. Risolveremo la questione nel prossimo esercizio.
Soluzione
L'astrazione della classe
Album
che abbiamo realizzato, ricorda molto l'astrazione della classeLibro
:
public class Album { private String ismn; private String titolo; private String artista; private String genere; private String prezzo; public Album(String ismn, String titolo, String autore, String genere, String prezzo) { setIsmn(ismn); setTitolo(titolo); setArtista(autore); setGenere(genere); setPrezzo(prezzo); } public String getIsmn() { return ismn; } public void setIsmn(String ismn) { this.ismn = ismn; } public String getTitolo() { return titolo; } public void setTitolo(String titolo) { this.titolo = titolo; } public String getArtista() { return artista; } public void setArtista(String autore) { this.artista = autore; } public String getGenere() { return genere; } public void setGenere(String genere) { this.genere = genere; } public String getPrezzo() { return prezzo; } public void setPrezzo(String prezzo) { this.prezzo = prezzo; } }
Notiamo che ci sono chiaramente tre campi in comune con la classeLibro
. Per le variabiliprezzo
etitolo
non ci sono dubbi, mentre per la variabile genere qualcuno potrebbe averne perché i generi musicali non corrispondono a quelli letterari. In realtà, avendo gestito i possibili valori dei generi letterali all'interno della classe di utilitàGenereUtils
, la variabilegenere
non dipende direttamente dai valori che può assumere. Inoltre notiamo che le variabiliisbn
eismn
, rappresentano sostanzialmente lo stesso concetto: un numero che identifica univocamente un oggetto. Ma anche le variabiliautore
inLibro
eartista
inAlbum
, si assomigliano molto se ci caliamo nel contesto della vendita di libri e di album musicali. Benché ci siano delle differenze di significato delle due parole, un acquirente di un libro, solitamente lo identifica anche mediante il suo autore. Per esempio il libro "Shining" è di Stephen King. Allo stesso modo un acquirente di un album musicale, potrebbe identificare un album anche specificando il relativo l'artista, per esempio potremmo dire che l'album "The Wall" è dei Pink Floyd. Questo è vero perché ci troviamo nell'ambito della vendita di album e di libri, che per noi sono semplicemente "articoli". Quindi abbiamo deciso di creare la superclasse astrattaArticolo
delle classiLibro
eAlbum
nel seguente modo:
public abstract class Articolo { private String id; private String titolo; private String nome; private int prezzo; private String genere; public Articolo(String id, String titolo, String nome, int prezzo, String genere) { super(); setId(id); setTitolo(titolo); setNome(nome); setPrezzo(prezzo); setGenere(genere); } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitolo() { return titolo; } public void setTitolo(String titolo) { this.titolo = titolo; } public String getNome() { return nome; } public void setNome(String nome) { this.nome = nome; } public int getPrezzo() { return prezzo; } public void setPrezzo(int prezzo) { this.prezzo = prezzo; } public String getGenere() { return genere; } public void setGenere(String genere) { this.genere = genere; } }
Le classiLibro
eAlbum
, possono quindi essere semplificati nella seguente maniera:
public class Libro extends Articolo { public Libro(String isbn, String titolo, String autore, int prezzo, String genere) { super(isbn, titolo, autore, prezzo, genere); } } public class Album extends Articolo { public Album(String ismn, String titolo, String artista, int prezzo, String genere) { super(ismn, titolo, artista, prezzo, genere); } }
in pratica abbiamo "normalizzato" i campiisbn
della classeLibro
eismn
della classeAlbum
, in un campoid
(abbreviativo di "numero identificativo") nella superclasseArticolo
. Stesso discorso per i campi autore della classeLibro
eartista
della classeAlbum
, "normalizzati" in un campo nome nella superclasseArticolo
. -
Esercizio 6.k)
Ripartendo dalle soluzioni dell'esercizio precedente e dell'esercizio 5.ff, rinominare la classe
GenereUtils
inGenereLetterarioUtils
e creare la classe equivalenteGenereMusicaleUtils
. Verificare se esiste una relazione di ereditarietà tra le due classi, e se esiste, implementarla. Inoltre, anche la classeAlbum
deve avere un genere appartenente ad un insieme predefinito di generi musicali (vedi metodosetGenere
della classeLibro
). Infine creare una classeTestArticoli
, equivalente alla classeTestLibro
dell'esercizio 5.ff, che testa il corretto funzionamento degli oggetti istanziarti dalle classiLibro
eAlbum
.
Soluzione
Con una soluzione non semplice, abbiamo implementato l'ereditarietà tra le classi di utilità richieste. In particolare abbiamo fatto estendere alle classi
GenereMusicaleUtils
eGenereLetterarioUtils
una nuova classe generica per la quale abbiamo riutilizzato il nomeGenereUtils
. In quest'ultima abbiamo implementato i metodi staticiisGenereValido
estampaErrore
aggiungendo come secondo parametro l'array di generi su cui basare il controllo richiesto. Questo è necessario, perché non sappiamo a priori se in questa classe generica useremo generi musicali o letterari, ma contemporaneamente non vogliamo duplicare codice nelle sottoclassi:
public class GenereUtils { public static boolean isGenereValido(String genere, String[] generiValidi) { boolean genereValido = false; for (String generePredefinito : generiValidi) { if (generePredefinito.equals(genere)) { genereValido = true; } } return genereValido; } public static void stampaErrore(String genere, String[] generiValidi) { System.out.println("Genere " + genere + " non valido! Usare uno dei seguenti generi:"); for (String generePredefinito : generiValidi) { System.out.println(generePredefinito); } } }
Quindi abbiamo definito la classeGenereLetterarioUtils
nella seguente maniera:
public class GenereLetterarioUtils extends GenereUtils { public static final String ROMANZO = "Romanzo"; public static final String SAGGIO = "Saggio"; public static final String THRILLER = "Thriller"; public static final String MANUALE = "Manuale"; public static final String FANTASCIENZA = "Fantascienza"; public static final String[] generi = { ROMANZO, SAGGIO, THRILLER, MANUALE, FANTASCIENZA }; public static boolean isGenereValido(String genere) { return isGenereValido(genere, generi); } public static void stampaErrore(String genere) { stampaErrore(genere, generi); } }
E la classeGenereMusicaleUtils
in questo modo:
public class GenereMusicaleUtils extends GenereUtils { public static final String ROCK = "Rock"; public static final String JAZZ = "Jazz"; public static final String BLUES = "Blues"; public static final String POP = "Pop"; public static final String RAP = "Rap"; public static final String[] generi = { ROCK, JAZZ, BLUES, POP, RAP }; public static boolean isGenereValido(String genere) { return isGenereValido(genere, generi); } public static void stampaErrore(String genere) { stampaErrore(genere, generi); } }
In queste due classi abbiamo fatto in modo che i metodiisGenereValido
estampaErrore
invochino i metodi definiti nella superclasse. Inoltre abbiamo definito le informazioni sui generi.Le difficoltà tecniche nell'implementare questa ereditarietà, dipendono soprattutto dal fatto che abbiamo a che fare con classi che definiscono metodi astratti, e che quindi non si adattano bene al discorso dell'estensione.Nella classeLibro
, abbiamo quindi ridefinito il metodosetGenere
come si può vedere di seguito:
public class Libro extends Articolo { public Libro(String isbn, String titolo, String autore, int prezzo, String genere) { super(isbn, titolo, autore, prezzo, genere); } public void setGenere(String genere) { if (GenereLetterarioUtils.isGenereValido(genere)) { super.setGenere(genere); } else { GenereLetterarioUtils.stampaErrore(genere); } } }
Notare che siamo stati costretti ad invocare il metodosetGenere
della superclasseArticolo
utilizzando il referencesuper
, visto che avevamo la necessità di chiamare un metodo che aveva lo stesso nome del metodo che stavamo ridefinendo. Lo stesso discorso per la classeAlbum
:
public class Album extends Articolo { public Album(String ismn, String titolo, String artista, int prezzo, String genere) { super(ismn, titolo, artista, prezzo, genere); } public void setGenere(String genere) { if (GenereMusicaleUtils.isGenereValido(genere)) { super.setGenere(genere); } else { GenereMusicaleUtils.stampaErrore(genere); } } }
infine abbiamo implementato la classe di testTestArticoli
nella seguente maniera:
public class TestArticoli { public static void main(String[] args) { Libro jfaVol1 = new Libro("979-12-200-4915-3", "Java for Aliens Vol. 1", "Claudio De Sio Cesari", 25, GenereLetterarioUtils.MANUALE); Libro jfaVol2 = new Libro("979-12-200-4916-0", "Java for Aliens Vol. 2", "Claudio De Sio Cesari", 25, "NonEsistente"); System.out.println("Genere JFA Vol 1 = " + jfaVol1.getGenere()); System.out.println("Genere JFA Vol 2 = " + jfaVol2.getGenere()); Album lad = new Album("979-0-236-44-3", "Live after Death", "Iron Maiden", 25, GenereMusicaleUtils.ROCK); Album mop = new Album("978-0-789-01-2", "Master of Puppets", "Metallica", 25, "NonEsistente"); System.out.println("Genere Live after Death = " + lad.getGenere()); System.out.println("Genere Master of Puppets = " + mop.getGenere()); } }
L'output finale è il seguente:
Genere NonEsistente non valido! Usare uno dei seguenti generi: Romanzo Saggio Thriller Manuale Fantascienza Genere JFA Vol 1 = Manuale Genere JFA Vol 2 = null Genere NonEsistente non valido! Usare uno dei seguenti generi: Rock Jazz Blues Pop Rap Genere Live after Death = Rock Genere Master of Puppets = null
-
Esercizio 6.l)
Tenendo presente tutti gli inserimenti di codice che il compilatore esegue implicitamente, riscrivere la seguente classe, aggiungendo tutte le istruzioni che il compilatore aggiungerebbe:
public class CompilatorePensaciTu { private int var; public void setVar(int v) { var = v; } public int getVar() { return var; } }
Soluzione
Il compilatore in realtà trasformerà la classe in qualcosa di molto simile alla seguente :
import java.lang.*; public class CompilatorePensaciTu extends Object { private int var; public CompilatorePensaciTu() { } public void setVar(int v) { this.var = v; } public int getVar() { return this.var; } }
Teniamo anche conto che, estendendoObject
, questa classe eredita anche tutti i suoi metodi. -
Esercizio 6.m)
Considerando le seguenti classi:
public class Persona { private String nome; public void setNome(String nome) { this.nome = nome; } public String getNome() { return this.nome; } } public class Impiegato extends Persona { private int matricola; public void setMatricola(int matricola) { this.matricola = matricola; } public int getMatricola () { return this.matricola; } public String getDati() { return getNome() + "\nnumero" + getMatricola(); } }
Quale sarà l'output del processo di compilazione (scegliere una sola opzione)?1. Nessun output (compilazione corretta).
2. Errore nel metodogetDati
diImpiegato
.
3. Errore nel metodogetNome
diPersona
.
4. Errore nel metodogetMatricola
diImpiegato
.
Soluzione
La risposta esatta è la prima. Nessun errore da segnalare.
-
Esercizio 6.n)
Se aggiungiamo alla classe
Impiegato
descritta nell'esercizio 6.m il seguente metodo:
public void setDati(String nome, int matricola) { setNome(nome); setMatricola(matricola); }
Quale sarà l'output del processo di compilazione (scegliere una sola opzione)?1. Nessun output (compilazione corretta).
2. Errore nel metodogetDati
diImpiegato
.
3. Errore nel metodosetNome
diPersona
.
4. Errore nel metodosetMatricola
diImpiegato
.
Soluzione
La risposta esatta è la prima. Nessun errore da segnalare.
-
Esercizio 6.o)
Quali di queste affermazioni sono vere?
1. L'ereditarietà permette di mettere in relazione di aggregazione più classi.
2. L'ereditarietà permette di mettere in relazione di aggregazione più interfacce.
3. L'ereditarietà permette di mettere in relazione di estensione più classi ed interfacce.
4. L'ereditarietà permette di mettere in relazione di aggregazione più classi ed interfacce.
Soluzione
Il concetto di aggregazione, utilizzato più volte negli esercizi precedenti, è una relazione che indica contenimento, che semmai può essere considerata un'alternativa all'estensione. Quindi l'unica risposta corretta è la terza.
-
Esercizio 6.p)
Considerato il seguente codice:
class Animale {} interface Felino {} class Leone {}
Se volessimo mettere in relazione di ereditarietà i tipi precedenti, quali dei seguenti snippet sono validi dal punto di vista del compilatore?1.class Animale extends Felino {}
2.interface Felino extends Animale {}
3.class Leone extends Felino {}
4.class Leone extends Animale implements Felino {}
5.class Animale extends Leone implements Felino {}
Soluzione
La quarta e la quinta opzione sono entrambe corrette per il compilatore. La quinta però, ha meno senso dal punto di vista della logica (un animale è un leone? Non per forza).
-
Esercizio 6.q)
Dato che un'interfaccia può dichiarare oltre a metodi astratti, anche metodi statici pubblici e privati, e implementati (di default) pubblici e privati, e visto che con le interfacce possiamo implementare l'ereditarietà multipla, perché mai dovremmo preferire una classe astratta ad un'interfaccia?
Soluzione
Anche se un'interfaccia può potenzialmente definire metodi di diversi tipi, non può dichiarare variabili d'istanza, ma solo costanti statiche pubbliche.
-
Esercizio 6.r)
Dati i seguenti tipi:
interface Volante {} class Aereo implements Volante {}
quali tra i seguenti snippet sono corretti?
1.Aereo a = new Aereo();
2.Volante v = new Volante();
3.aereo1.equals(aereo2);
(doveaereo1
eaereo2
sono oggetti di tipoAereo
)
4.Volante.aereo = new Aereo();
Soluzione
Gli statement corretti sono i numeri 1 e 3.
Il metodoequals
è ereditato direttamente dalla classeObject
. -
Esercizio 6.s)
Quali modificatori sono implicitamente aggiunti a tutti i metodi di un'interfaccia (è possibile scegliere più di una risposta)?
1.public
2.protected
3.private
4.static
5.default
6.abstract
7.final
Soluzione
Solo la prima risposta è corretta. Essendo la prima corretta, ovviamente la seconda e la terza non possono essere corrette. È possibile dichiarare metodi statici e di default, ma i relativi modificatori non sono mai aggiunti automaticamente. Il dubbio può venire per la risposta 6, perché prima dell'avvento di Java 8 questa risposta sarebbe stata giusta, ora non più visto che possiamo dichiarare nelle interfacce anche metodi di default e statici. Infine, il modificatore
final
è implicitamente aggiunto agli attributi delle interfacce (che sono anche implicitamente dichiarati statici e pubblici). -
Esercizio 6.t)
Data la seguente gerarchia:
interface A { void metodo(); } interface B extends A {} abstract class C implements B {} public final class D extends C { public void metodo() {}
Quali delle seguenti affermazioni sono false (è possibile scegliere più di un'affermazione):
1. La classeC
non può essere dichiarata astratta perché implementa un'interfaccia, quindi questo codice non compila.
2. L'interfacciaB
non può estendere un'altra interfaccia.
3. La classeC
implementandoB
, eredita anche il metodometodo()
astratto diA
.
4. La classeD
non compila perché non può essere dichiaratafinal
.
5. La classeD
non compila perché è dichiaratapublic
.
6. La classeD
non compila perché il suo metodo è dichiaratopublic
.
Soluzione
Le risposte sono tutte false tranne la 3.
-
Esercizio 6.u)
Quali delle seguenti affermazioni sono vere:
1. I metodi statici dichiarati in un'interfaccia non vengono ereditati nelle sotto-interfacce.
2. Non è possibile dichiarare una classeabstract
efinal
perché i due modificatori non sono compatibili tra loro.
3. Una classe astratta deve dichiarare per forza metodi astratti.
4. Le interfacce possono anche dichiarare costruttori.
5. Le classi astratte possono dichiarare metodi statici.
Soluzione
Le risposte corrette sono la 1, la 2 e la 5.
-
Esercizio 6.v)
Supponiamo di voler definire un tipo
Atleta
. Supponiamo che ogni atleta definisca i metodi corri e allenati. Supponiamo anche di voler definire delle sottoclassi più specifiche comeCalciatore
,Corridore
eTennista
. Come definiresteAtleta
, come interfaccia o classe astratta? Aggiungereste altri tipi?
Soluzione
Potremmo seguire il seguente ragionamento. Un tennista si allena e corre diversamente da un calciatore o da un corridore. Insomma, sia la classe
Tennista
, sia la classeCorridore
che la classeCalciatore
, ridefiniranno i metodiallenati
ecorri
. InAtleta
, questi due metodi invece li vorremmo definire astratti, perché, a priori, sarebbe difficile definire come si allena o come corre un atleta... dipende dal tipo di atleta. A questo punto si potrebbe pensare di definireAtleta
come interfaccia visto che utilizza solo due metodi astratti, e lo si può fare. Ma con l'evoluzione di questo programma, è altamente probabile che si vogliano definire campi di anagrafica degli atleti come potrebbero esserenome
ecognome
. Un'interfaccia però non può dichiarare variabili, e quindi si potrebbe preferire dichiarareAtleta
come classe astratta. Oppure potremmo creare una soluzione doveTennista
,Calciatore
eCorridore
implementino l'interfacciaAtleta
(che definisce i due metodi astrattiallenati
ecorri
) ed estendono la classe astrattaPersona
(che definisce gli attributinome
,cognome
etc.). Insomma sarà il contesto del programma in cui ci caleremo che ci porterà a definire la soluzione più corretta. -
Esercizio 6.w)
Quali delle seguenti affermazioni riguardanti il modificatore
protected
sono corrette:
1. Una classe dichiarataprotected
può essere istanziata solo all'interno dello stesso package in cui è definita, e in tutte le sue sottoclassi anche se definite in package differenti.
2. Se una classe ha un costruttore dichiaratoprotected
, questa potrà essere istanziata solo dalle classi che appartengono allo stesso package.
3. Un metodo dichiaratoprotected
è ereditato da una sottoclasse indipendentemente dal package in cui quest'ultima è definita.
4. Un variabile dichiarataprotected
può essere utilizzata da tutte le classi che appartengono allo stesso package della classe che definisce tale variabile.
5. Una variabile dichiarataprotected
è ereditata da una sottoclasse indipendentemente dal package in cui quest'ultima è definita.
6. Un costruttore dichiaratoprotected
è ereditato da una sottoclasse indipendentemente dal package in cui quest'ultima è definita.
7. Una sottoclasse definita in un package diverso da quello a cui appartiene la sua superclasse, può istanziare quest'ultima per poi utilizzare i suoi membriprotected
.
Soluzione
Le affermazioni corrette sono la 2, 3, 4, 5. La numero 1 è errata perché il modificatore
protected
non può essere utilizzato con le classi (non esistono classiprotected
). La 6 è falsa perché i costruttori non si ereditano (neanche se sono pubblici). La numero 7 è errata per le ragioni esposte nel paragrafo 6.2.4.1. -
Esercizio 6.x)
Partendo dalla soluzione dell'esercizio 4.z:
• Incapsulare le classiContatto
eRubrica
.
• Implementare per la classeRubrica
il pattern Singleton.
• Modificare la classeRicercaContatti
per fare in modo che funzioni come prima.
• Creare una classeSpeciale
che estendeContatto
. Supponiamo che un contatto speciale, debba avere anche una suoneria personalizzata (creando la relativa variabile d'istanza di tipo stringa).
• Nella classeRubrica
creare anche un array di oggettiSpeciale
, simile all'arraycontatti
che già esiste. Creare anche un metodocercaContattiSpecialiPerNome
, equivalente al metodocercaContattiPerNome
già presente nella classe.
• Creare una classeRicercaContattiSpeciali
equivalente alla classeRicercaContatti
dell'esercizio 4.z, per testare il corretto funzionamento del codice scritto.
Soluzione
La classe
Contatto
incapsulata potrebbe essere la seguente:
public class Contatto { protected static final String SCONOSCIUTO ="sconosciuto"; private String nome; private String numeroDiTelefono; private String indirizzo; public Contatto(String nome, String numeroDiTelefono) { this.setNome(nome); this.setNumeroDiTelefono(numeroDiTelefono); this.setIndirizzo(SCONOSCIUTO); } public Contatto(String nome, String numeroDiTelefono, String indirizzo) { this.setNome(nome); this.setNumeroDiTelefono(numeroDiTelefono); this.setIndirizzo(indirizzo); } public void setIndirizzo(String indirizzo) { this.indirizzo = indirizzo; } public String getIndirizzo() { return indirizzo; } public void setNumeroDiTelefono(String numeroDiTelefono) { this.numeroDiTelefono = numeroDiTelefono; } public String getNumeroDiTelefono() { return numeroDiTelefono; } public void setNome(String nome) { this.nome = nome; } public String getNome() { return nome; } public void stampaDettagli() { System.out.println(nome); System.out.println(indirizzo); System.out.println(numeroDiTelefono); System.out.println(); } }
Notare che abbiamo introdotto una costante statica privataSCONOSCIUTO
per rappresentare un indirizzo sconosciuto quando si utilizza il primo costruttore dichiarato (nelle soluzioni dell'esercizio 4.z, il valore dell'indirizzo non veniva proprio settato, e di conseguenza valevanull
).
La classeRubrica
incapsulata e resa Singleton potrebbe essere implementata nel modo seguente:
public class Rubrica { private static Rubrica instance; public Contatto[] contatti; private Rubrica () { contatti = new Contatto[] { new Contatto("Claudio De Sio Cesari"," Via Java 13","131313131313"), new Contatto("Stevie Wonder", "Via Musica 10", "1010101010"), new Contatto("Gennaro Capuozzo", "Piazza Quattro Giornate 1","1111111111") } ; } public static Rubrica getInstance() { if (instance == null) { instance = new Rubrica(); } return instance; } public Contatto[] cercaContattiPerNome(String nome) { Contatto []contattiTrovati = new Contatto[contatti.length]; for (int i = 0, j = 0; i < contattiTrovati.length; i++) { if (contatti[i].getNome().toUpperCase(). contains(nome.toUpperCase())) { contattiTrovati[j] = contatti[i]; j++; } } return contattiTrovati; } public Contatto[] getContatti() { return contatti; } }
Notare che abbiamo evitato di creare il metodo setter per la variabilecontatti
.
Abbiamo poi sostituito all'interno della classeRicercaContatti
, solo la riga:
var rubrica = new Rubrica();
con:
var rubrica = Rubrica.getInstance();
E tutto ha continuato a funzionare come funzionava prima.
La classeSpeciale
potrebbe essere implementata così:
public class Speciale extends Contatto { private String suoneria; public Speciale(String nome, String numeroDiTelefono, String indirizzo, String suoneria) { super(nome, numeroDiTelefono, indirizzo); setSuoneria(suoneria); } public String getSuoneria() { return suoneria; } public void setSuoneria(String suoneria) { this.suoneria = suoneria; } public void stampaDettagli() { System.out.println(getNome()); System.out.println(getIndirizzo()); System.out.println(getNumeroDiTelefono()); System.out.println(getSuoneria()); System.out.println(); } }
Notare che per istanziare un oggetto di tipoSpeciale
, potremo utilizzare solo un costruttore che prende in input quattro parametrinome
,numeroDiTelefono
,indirizzo
esuoneria
(perché per la nostra astrazione, è la suoneria che dovrebbe distinguere un contatto speciale da uno contatto normale). Tale costruttore richiama il costruttore della superclasseContatto
tramite la parola chiavesuper
. Notare anche che il metodostampaDettagli
è stato ridefinito aggiungendo la stampa anche della variabilesuoneria
.
Aggiorniamo quindi la classeRubrica
come richiesto:
public class Rubrica { private static Rubrica instance; public Contatto[] contatti; public Speciale[] contattiSpeciali; private Rubrica() { contatti = new Contatto[] { new Contatto("Claudio De Sio Cesari", "Via Java 13","131313131313"), new Contatto("Stevie Wonder", "Via Musica 10","1010101010"), new Contatto("Gennaro Capuozzo", "Piazza Quattro Giornate 1","1111111111") } ; contattiSpeciali = new Speciale[] { new Speciale("Mario Ruoppolo","Via Neruda 3","333333","Il Postino"), new Speciale("Vincenzo Malinconico", "Via dei Tribunali 8","888888","Tuca Tuca"), new Speciale("Logan Howlett","Piazza Canada 6","66666","Hurt") } ; } public static Rubrica getInstance() { if (instance == null) { instance = new Rubrica(); } return instance; } public Contatto[] cercaContattiPerNome(String nome) { Contatto []contattiTrovati = new Contatto[contatti.length]; for (int i = 0, j = 0; i < contattiTrovati.length; i++) { if (contatti[i].getNome().toUpperCase(). contains(nome.toUpperCase())) { contattiTrovati[j] = contatti[i]; j++; } } return contattiTrovati; } public Speciale[] cercaContattiSpecialiPerNome(String nome) { Speciale []contattiSpecialiTrovati = new Speciale[contattiSpeciali.length]; for (int i = 0, j = 0; i < contattiSpecialiTrovati.length; i++) { if (contattiSpeciali[i].getNome().toUpperCase(). contains(nome.toUpperCase())) { contattiSpecialiTrovati[j] = contattiSpeciali[i]; j++; } } return contattiSpecialiTrovati; } public Contatto[] getContatti() { return contatti; } public Speciale[] getContattiSpeciali() { return contattiSpeciali; } }
Ed infine possiamo creare la seguente classeRicercaContattiSpeciali
:
import java.util.Scanner; public class RicercaContattiSpeciali { public static void main(String args[]) { System.out.println("Ricerca Contatti Speciali"); System.out.println(); var rubrica = Rubrica.getInstance(); System.out.println("Inserisci nome o parte del nome da ricercare"); Scanner scanner = new Scanner(System.in); String input = scanner.nextLine(); Speciale[] contattiSpecialiTrovati = rubrica.cercaContattiSpecialiPerNome(input); System.out.println("Contatti Speciali trovati con nome contenente \"" + input +"\"" ); for (Speciale speciale : contattiSpecialiTrovati) { if (speciale != null) { speciale.stampaDettagli(); } } } }
-
Esercizio 6.y)
Si sconsiglia di eseguire il seguente esercizio utilizzando Blocco Note e riga di comando. Con un IDE (o almeno con EJE) si risparmieranno molti minuti per terminare l'esercizio.Partendo dall'esercizio 6.x, eseguire le seguenti tecniche di refactoring per usare i paradigmi di riuso, ereditarietà ed astrazione:
• Fare in modo che le classi create non contengano codice duplicato (riutilizzare tutto il codice che si riesce a riutilizzare).
• Creare un'interfacciaDato
da far implementare a tutte le classi che rappresentano dati nell'applicazione, e l'interfacciaIdentificabile
che dichiara un metodo astrattogetID
che ritorna unint
.
• Astrarre il concetto di entità implementando la classe astrattaEntita
(senza l'accento perché non è possibile utilizzare lettere accentate in Java), che implementa le interfacceDato
eIdentificabile
. Un oggettoEntita
deve essere caratterizzato da un numero identificativo incrementale generato automaticamente. Tale caratteristica ci servirà per far evolvere le nostre astrazioni nei futuri esercizi.
• Mettere in relazione di ereditarietà questa nuova classe (ed eventualmente le altre interfacce) con le altre classi del progetto dove applicabile.
• Creare i packagerubrica.dati
,rubrica.interfaccia
erubrica.business
, dove intendiamo conrubrica.dati
, un package che deve contenere classi che rappresentano i dati dell'applicazione. Il packagerubrica.business
invece, deve contenere le classi che contengono la logica di business dell'applicazione. Infinerubrica.interfaccia
deve contenere le classi che rappresentano l'interfaccia con cui può interagire l'utente.
• Spostare di conseguenza le classi create sotto i package che sembrano più adeguati. Se lo ritenete necessario, è possibile creare altre classi e package e modificare quelle esistenti (è possibile anche creare superclassi, sottoclassi e classi di utilità).
• Testare che le classiRicercaContattiSpeciali
eRicercaContatti
continuino a funzionare.
Soluzione
Nel package
rubrica.dati
, decidiamo di mettere le nuove astrazioniDato
:
package rubrica.dati; public interface Dato { }
Identificabile
:
package rubrica.dati; public interface Identificabile { int getId(); }
edEntita
:
package rubrica.dati; import rubrica.util.Contatore; public abstract class Entita implements Dato, Identificabile { private int id; public Entita () { setId(Contatore.dammiNumeroSeriale()); } private void setId(int id) { this.id = id; } public int getId() { return id; } }
Notare come quest'ultima classe utilizzi una classe di utilitàContatore
(inserita in un packagerubrica.util
, creato ad hoc) che contiene un metodo statico che aggiorna un numero seriale ogni volta che viene istanziata un'entità (ovvero una sua sottoclasse visto cheEntita
è una classe astratta). Segue la classeContatore
:
package rubrica.util; public class Contatore { private static int contatoreOggetti; public static int dammiNumeroSeriale() { return contatoreOggetti+=1; } }
Come già detto, il numero seriale ci servirà negli esercizi futuri.Abbiamo deciso di inserire nel packagerubrica.dati
anche le classiContatto
eRubrica
. Per quanto riguarda quest'ultima abbiamo ritenuto necessario modificarla nel seguente modo:
package rubrica.dati; public class Rubrica implements Dato { private static Rubrica instance; public Contatto[] contatti; public Speciale[] contattiSpeciali; private Rubrica () { contatti = new Contatto[]{ new Contatto("Claudio De Sio Cesari", "Via Java 13", "131313131313"), new Contatto("Stevie Wonder", "Via Musica 10", "1010101010"), new Contatto("Gennaro Capuozzo", "Piazza Quattro Giornate 1", "1111111111") }; contattiSpeciali = new Speciale[]{ new Speciale("Mario Ruoppolo", "Via Neruda 3", "333333", "Il Postino"), new Speciale("Vincenzo Malinconico", "Via dei Tribunali 8", "888888", "Tuca Tuca"), new Speciale("Logan Howlett", "Piazza Canada 6", "66666", "Hurt") }; } public static Rubrica getInstance() { if (instance == null) { instance = new Rubrica(); } return instance; } public Contatto[] getContatti() { return contatti; } public Speciale[] getContattiSpeciali() { return contattiSpeciali; } }
Notare che abbiamo rimosso i metodicercaContattiPerNome
ecercaContattiSpecialiPerNome
perché da considerarsi metodi di business, e non metodi di una classe che si chiamaRubrica
. Nella nostra astrazione di rubrica infatti, ci rifacciamo al concetto di rubrica contenente dei contatti, non di una rubrica che può eseguire ricerche. Come per le antiche e ormai non più utilizzate rubriche cartacee, intendiamo un oggetto simile ad un quaderno in cui sono scritti i contatti. Una rubrica rappresenta quindi un contenitore di dati e non un oggetto che esegue azioni di business, infatti gli abbiamo fatto implementare l'interfacciaDato
. Per cercare un certo contatto in una rubrica cartacea, un utente deve sfogliare le pagine della rubrica. Quindi abbiamo deciso per ora, di spostare i metodicercaContattiPerNome
ecercaContattiSpecialiPerNome
in una nuova classe chiamata proprioUtente
, che si occuperà di eseguire le ricerche nella rubrica:
package rubrica.business; import rubrica.dati.*; public class Utente { public Contatto[] cercaContattiPerNome(String nome) { Contatto []contatti = Rubrica.getInstance().getContatti(); Contatto []contattiTrovati = new Contatto[contatti.length]; for (int i = 0, j = 0; i < contattiTrovati.length; i++) { if (contatti[i].getNome().toUpperCase(). contains(nome.toUpperCase())) { contattiTrovati[j] = contatti[i]; j++; } } return contattiTrovati; } public Speciale[] cercaContattiSpecialiPerNome(String nome) { Speciale []contattiSpeciali = Rubrica.getInstance().getContattiSpeciali(); Speciale []contattiSpecialiTrovati = new Speciale[contattiSpeciali.length]; for (int i = 0, j = 0; i < contattiSpecialiTrovati.length; i++) { if (contattiSpeciali[i].getNome().toUpperCase(). contains(nome.toUpperCase())) { contattiSpecialiTrovati[j] = contattiSpeciali[i]; j++; } } return contattiSpecialiTrovati; } }
Notare che tale classe, appartiene al packagerubrica.business
. Riassumendo, una rubrica la astraiamo come dato, le facciamo implementare l'interfacciaDato
e la posizioniamo nel packagerubrica.dati
, mentre la classeUtente
rappresenta un oggetto di business e si trova nel packagerubrica.business
.
Infine abbiamo modificato le classiRicercaContatti
eRicercaContattiSpeciali
nel seguente modo:
package rubrica.interfaccia; import rubrica.dati.*; import rubrica.business.Utente; import java.util.Scanner; public class RicercaContatti { public static void main(String args[]) { System.out.println("Ricerca Contatti"); System.out.println(); var utente = new Utente(); System.out.println("Inserisci nome o parte del nome da ricercare"); Scanner scanner = new Scanner(System.in); String input = scanner.nextLine(); Contatto[] contattiTrovati = utente.cercaContattiPerNome(input); System.out.println("Contatti trovati con nome contenente \"" + input +"\"" ); for (Contatto contatto : contattiTrovati) { if (contatto != null) { contatto.stampaDettagli(); } } } } package rubrica.interfaccia; import rubrica.dati.*; import rubrica.business.Utente; import java.util.Scanner; public class RicercaContattiSpeciali { public static void main(String args[]) { System.out.println("Ricerca Contatti Speciali"); System.out.println(); var utente = new Utente(); System.out.println("Inserisci nome o parte del nome da ricercare"); Scanner scanner = new Scanner(System.in); String input = scanner.nextLine(); Speciale[] contattiSpecialiTrovati = utente.cercaContattiSpecialiPerNome(input); System.out.println("Contatti Speciali trovati con nome contenente \"" + input +"\"" ); for (Speciale speciale : contattiSpecialiTrovati) { if (speciale != null) { speciale.stampaDettagli(); } } } }
-
Esercizio 6.z)
Riprendiamo il caso di studio definito nell'appendice 5.7. Avevamo fatto una serie di passi, che rappresentavano un possibile processo da seguire per poter creare infine del codice di qualità. In particolare avevamo definito i seguenti passi:
• Analisi degli use case.Completare l'intero programma Logos è troppo impegnativo (ci vorrebbero settimane se non mesi di lavoro), ma possiamo concentrarci su particolari use case, e portare avanti il nostro processo solo su questi use case.
• Definizione degli scenari per ogni use case.
• Definire un high level deployment diagram di architettura.
• Individuare le key abstraction.
• Verificare la validità delle key abstraction utilizzando i diagrammi di iterazione pe convalidare i flussi degli scenari, con gli oggetti istanziati dalla key abstraction.
• Abbiamo visto negli esercizi finali relativi al capitolo 5, che altri passi da eseguire sono:
• Definire le key abstraction su un class diagram.
• Rivalutare il class diagram aggiungendo i dettagli essenziali e soprattutto ragionando sulle responsabilità.Il lettore potrebbe poi anche iterare i ragionamenti fatti su ogni use case per completare pezzo dopo pezzo il programma.Anche se nell'analisi iniziale di Logos non è stato individuato il processo di autenticazione, in realtà ci deve essere. Infatti, dall'analisi degli use case erano stati definiti due attori: l'amministratore (che aveva compiti di configurazione del sistema) e il commesso (che aveva compiti operativi). Sembra scontato che per potersi far riconoscere dal sistema, un meccanismo di login sia indispensabile. Quindi possiamo dire di aver scoperto un nuovo caso d'uso, che andiamo a definire come "autenticazione". Facciamo evolvere allora il diagramma dei casi d'uso della figura 5.7.2 dell'approfondimento 5.7 nel diagramma di figura 6.z.1, dove introduciamo il nuovo caso d'uso trovato.
Soluzione
Una soluzione semplice e quasi automatica, consisterebbe nell'estendere direttamente la classe
Utente
, con le due sottoclassiAmministratore
eCommesso
:
package com.claudiodesio.autenticazione; public class Amministratore extends Utente { public Amministratore(String nome, String username, String password) { super(nome, username, password); } }
e
package com.claudiodesio.autenticazione; public class Commesso extends Utente { public Commesso(String nome, String username, String password) { super(nome, username, password); } }
Solo che non abbiamo ancora informazioni sufficienti per inserire campi e metodi specifici per queste due classi (infatti sono vuote). Quindi per ora decidiamo di non implementarle.