Slide quarta lezione al linguaggio Java 8 in preparazione alla certificazione OCA 1Z0-808.
Argomenti:
Design Pattern: Singleton
Classe Astratta
Interfaccia e interfaccia funzionale
Ereditarietà e costruttori
Super e this
Incapsulamento
Polimorfismo
Varargs
Overload e Override
Invocazione virtuale dei metodi
Lezione del 28-11-2017 tenuta da Valerio Radice presso Nextre Engeneering
https://www.nextre.it/corso/corso-java-oca/
3. Di cosa si parlerà ?
Design Pattern: Singleton
Classe Astratta
Interfaccia e interfaccia funzionale
Ereditarietà e costruttori
Super e this
Incapsulamento
Polimorfismo
Varargs
Overload e Override
Invocazione virtuale dei metodi
4. Design Pattern
Un design pattern descrive una soluzione generale a un problema di
progettazione ricorrente, gli attribuisce un nome, astrae e identifica gli aspetti
principali della struttura utilizzata per la soluzione del problema, identifica le
classi e le istanze partecipanti e la distribuzione delle responsabilità, descrive
quando e come può essere applicato. In breve definisce un problema, i contesti
tipici in cui si trova e la soluzione ottimale allo stato dell'arte.
Nel libro gli autori, detti Gang of four, cioè "banda dei quattro", identificarono
23 tipi di Design Pattern, suddivisi in 3 categorie: strutturali, creazionali e
comportamentali.
5. singleton
Garantisce un'unica istanza di una classe. Si realizza mediante:
rendere il costruttore privato
Inserire nella classe una variabile statica dello stesso tipo di essa per assegnarne
un'istanza
Un metodo pubblico, tipicamente getInstance(), per ottenere un suo riferimento.
Esso ritornerà l'unica istanza dell'oggetto memorizzato nella variabile d'istanza se
già esiste, altrimenti lo instanzia, lo assegna alla variabile di classe e lo
restituisce.
6. singleton
Esempio
public class MioSingolo {
private static MioSingolo istanza = null;
//Il costruttore private impedisce l'istanza di oggetti da parte di
classi esterne
private MioSingolo() {}
// Metodo della classe impiegato per accedere al singleton
public static synchronized MioSingolo getMioSingolo() {
if (istanza == null) {
istanza = new MioSingolo();
}
return istanza;
}
}
7. Quello che fa la differenza tra un bravo "smanettone" e un bravo sviluppatore è la
qualità del suo lavoro (codice)!
( … non significa scrivere con meno parentesi )
8. Ereditarietà
Altro pilastro cardine della OOP è l'ereditarietà,
anch'essa ispirata alla realtà.
Pensate alla relazione tra Animali, Cane e Gatto.
La nostra mente classifica tutto con classi e sottoclassi
senza volerlo, cerchiamo raggruppamenti logici sulla
base di fattori comuni per poi creare delle
specializzazioni.
In Java l'ereditarietà è la caratteristica che mette in
relazione (di estendibilità) più classi che hanno
caratteristiche comuni.
9. extends
Quando la rappresentazione della realtà è tale da trovare correlazioni tra le classi
in una relazione di tipo padre-figlio si dice che il figlio è sottoclasse di padre.
Padre è superclasse di figlio.
La classe figlio è derivata, può appartenere allo stesso package o ad altri package.
Per creare una classe figlia bisogna estendere ( extends ) la classe padre.
class Animale{
//Proprietà di
//tutti gli
// animali
}
class Mammifero extends Animale{}
class Cane extends Mammifero{}
class Gatto extends Mammifero{}
class Cavallo extends Mammifero{}
class CavalloDiRazza extends Cavallo{}
class CavalloDaCorsa extends Cavallo{}
class Pesce extends Animale{}
class PesceRosso extends Pesce{}
10. Object
Siccome la realtà è composta da oggetti e nella OOP tutto può essere composto
da oggetti allora anche le nostre classi saranno degli "oggetti".
Infatti la classe Object è la superclasse di tutte le classi!
Tutte le nostre classi fanno capo ad Object.
Se non specifichiamo quale classe estendere sarà il compilatore che inserirà,
prima di compilare il bytecode, il riferimento all'estensione di Object.
class Animale{}
e
class Animale extends Object{}
sono equivalenti quando si genera il bytecode.
11. Ereditarietà e incapsulamento
Cosa succede se estendiamo una classe che contiene membri incapsulati?
Estendere una classe significa ereditare tutto ciò che non è privato.
Se definiamo delle variabili private nella classe padre ma utilizziamo metodi di
accessor (getter) e mutator (setter) in realtà abbiamo comunque la possibilità di
usare tali variabili grazie all'incapsulamento.
Dichiarare protected una variabile di classe equivale a renderla pubblica a tutte
le classi dello stesso package ( slide modificatori )
12. is-a relation
Per sviluppare una corretta gerarchia di classi il programmatore deve sempre
porsi una semplice domanda: la sottoclasse è un oggetto particolare della
superclasse?
Se la risposta è affermativa si va a creare una sottoclasse che estende la
superclasse, in caso contrario si crea semplicemente una nuova classe ( e quindi
non si utilizzerà l'ereditarietà ).
In particolare se tutti gli attributi della superclasse sono anche quelli della
sottoclasse allora la relazione è corretta.
13. Ereditarietà: costruttori
I costruttori non vengono mai ereditati, anche se pubblici, per un semplice
motivo: i costruttori devono chiamarsi come la classe!
Un costruttore che ha un nome diverso da quello della classe non verrebbe mai
richiamato automaticamente ma si comporterebbe come metodo, creando solo
confusione.
Un qualunque costruttore, anche quello di default, invoca sempre (anche se non
espressamente dichiarato) come prima istruzione il costruttore alla superclasse
( super(); ).
14. Ereditarietà: costruttori
class Animale{
Animale(){
System.out.println("Costruttore di Animale");
}
}
class Mammifero extends Animale {
Mammifero(){
System.out.println("Costruttore di Mammifero");
//in realtà implicitamente ha richiamato anche super.Animale();
}
Mammifero(String s){
System.out.println("Costruttore di Mammifero");
//in realtà implicitamente ha richiamato anche super.Animale();
//anche se non esiste un Animale(String s)
}
}
Animale a1 = new Animale();
Mammifero m1 = new Mammifero();
Mammifero m2 = new Mammifero("cane");
15. super
La parola chiave super serve per indicare un riferimento alla risorsa della
superclasse.
È utilizzata per fare riferimento al costruttore della superclasse o classe padre.
Ogni classe derivata implicitamente, prima di essere creata, richiamerà come
prima istruzione, il costruttore di default ( super(); ) della classe padre o il
nostro se definito.
ATTENZIONE: la chiamata al costruttore ( super() o this() ) DEVE ESSERE nella
PRIMA linea e può essere INSERITA SOLO in un COSTRUTTORE!
Posso, in caso di più costruttori, scegliere quale richiamare.
16. super, this e inizializzatori
I comandi super e this possono, oltre che richiamare i costruttori, anche
richiamare i metodi.
Anche gli inizializzatori vengono "ereditati" anche se in realtà è solo un richiamo
nell'ordine più classico, prima inizializzatori statici della superclasse, poi della
classe estesa, successivamente inizializzatori d'istanza della superclasse,
costruttore della superclasse, inizializzatori della classe derivata e costruttore
della classe derivata
17. Generalizzazione e specializzazione
Sono due termini che contribuiscono a definire l'ereditarietà. Grazie ad essi si
definiscono le strutture delle classi e delle sottoclassi.
Si parla di generalizzazione quando a partire da un pool di classi comuni si
contribuisce a raccogliere le caratteristiche comuni in una superclasse.
Viceversa si parla di specializzazione quando a partire da una classe comune se
ne derivano altre atte a specificare il comportamento dell'oggetto.
18. abstract
Il modificatore abstract è utilizzato per la creazione di classi e metodi o variabili
funzione ( abstract int x(); ).
Oggetti molto generali che di per sé non rappresentano nulla ma sono necessari per
definirne altri si dicono abstract. Un oggetto abstract NON può essere istanziato!
Per istanziare un oggetto derivante da un modificatore abstract è necessario che tale
oggetto implementi tutti i metodi di tipo abstract , altrimenti dovrà essere anche'esso
di tipo abstract.
Questo permette di dichiarare comportamenti e attributi comuni a partire da oggetti
ancora troppo poco definiti ma che già hanno un evidente comportamento similare.
Un costruttore non può essere abstract, se si ridefinisce una classe il costruttore
cambia di nome, implicando che non potrà mai essere implementato.
19. Metodi astratti
Un metodo astratto non implementa un vero comportamento, dice soltanto che
chi estenderà l'oggetto dovrà preoccuparsi di definire come questo dovrà
funzionare.
Nella pratica un metodo astratto è definito dalla keyword abstract senza il
blocco delle graffe. Es:
public abstract void stampa();
Questo metodo non può essere invocato finchè non sarà definito. Se una classe
contiene anche solo una voce abstract anch'essa sarà abstract e quindi non
istanziabile.
20. Classi astratte
Una classe astratta NON può essere istanziata.
Solitamente si usa per descrivere un comportamento o degli attributi che per
tutte le sue derivazioni possono avere senso ma per le quali non c'è un
comportamento comune. Per esempio non posso sapere come i dispositivi
stamperanno un'informazione; una stampante e un monitor visualizzeranno una
stringa di caratteri in modo diverso, ma entrambi DOVRANNO farlo. Ecco quindi
perché è necessario definire il metodo stampa() come astratto.
NON è possibile usare final e abstract insieme e nemmeno abstract e static.
21. Ereditarietà multipla
Java NON supporta l'ereditarietà multipla!
Questo implica che una classe può avere solo un padre(una sola superclasse).
Ma con java 8 è possibile una sorta di ereditarietà multipla "soft" di tipo e non di
stato.
Grazie all'uso delle interfacce una classe può avere un solo padre, e quindi avere
una discendenza di tipo ben definita, ma può avere una serie di comportamenti e
caratteristiche multiple, derivanti dall'implementazione di più interfacce.
22. Interfacce
Un'interfaccia è un "contratto" al quale la classe, che dichiara di implementare, è
obbligata ad adempiere.
È un'evoluzione delle classe astratte, anche per essa valgono le stesse convenzioni
delle classi.
Nelle interfacce le variabili sono di tipo public static final, se non specificato il
compilatore lo metterà per noi.
Nelle interfacce i metodi sono di default abstract, quindi senza il blocco delle graffe.
Un'interfaccia, come una classe, deve essere salvata in un file .java con lo stesso nome
di essa.
23. Interfacce
Prima di Java8 le interfacce non erano istanziabili e non potevano avere un
comportamento.
Con Java8 è possibile avere anche dei comportamenti statici e pubblici per i
metodi, tali interfacce però non si implementano, essendo statiche è sufficiente
il richiamo da nomeInterfcaccia.nomeMetodo();
In un'interfaccia non sono ammessi gli inizializzatori.
Le interfacce si implementano con il comando implements. Es.
class Giocatore extends Persona implements Salta, Spara, Corri{}
24. Interfacce
In Java8 è possibile anche avere delle implementazioni di default per i metodi,
dichiarati con la keyword default.
Si possono avere interfacce con tanti metodi di default. Un'interfaccia che ha un
unico metodo astratto sono dette interfacce funzionali, questo unico metodo è
chiamato SAM (Single Abstract Method); servono per dichiarare un'unica
funzionalità da implementare (molto comode nell'uso delle lambda).
25. Ereditarietà multipla
Questo meccanismo simulato è permesso grazie all'implementazione di più
interfacce. Nel caso di funzionalità differenti non ci sono problemi, ma cosa
succede se una classe dovesse implementare più interfacce che contengono lo
stesso metodo e con la stessa firma?
Il compilatore non saprebbe quale usare, l'unica soluzione possibile è quella di
ridefinire il comportamento di default, nella classe che implementa le interfacce
(vedi Diamond Problem).
In caso di conflitti generati da avere lo stesso metodo astratto nella classe che
implementa lo stesso metodo come default il risultato è che la classe sovrascrive
l'implementazione dell'interfaccia con la sua astratta: class always win.
27. Interfaccia VS classe astratta
Né classi né interfacce possono essere istanziate!
Differenza pratica la si trova sui concetti che esse possono definire,
un'interfaccia può definire costanti statiche, metodi statici, metodi di default e
metodi astratti. Una classe astratta è una classe normale ma che non può essere
istanziata che potrebbe contenere metodi astratti.
Una classe astratta è solo un'astrazione troppo generica per esistere nel contesto
in cui è dichiarata.
Un'interfaccia è più un'astrazione comportamentale che se non associata ad un
oggetto non ha senso di esistere nel contesto.
28. Polimorfismo
Il polimorfismo è un concetto della realtà, che ben si addice alla OOP, importato nel
mondo della programmazione.
Può essere definito come la proprietà di un oggetto di adattarsi a forme differenti.
Esistono diverse forme di polimorfismo:
per metodi
Override
Overload
per dati
Collezioni polimorfe
Parametri polimorfi
Metodi virtuali
29. Reference
Molti definiscono una reference come un puntatore di memoria, il chè può essere
vero in alcuni linguaggi, in java è più corretto definire una reference con 2
dimensioni: indirizzo di memoria e intervallo di puntamento.
30. Polimorfismo per i metodi
In un metodo la coppia data dal nome e dai parametri (ordine e tipo) è detta firma o
signature. In Java un metodo è univocamente determinato non solo dal nome ma
ANCHE dai suoi parametri -> firma
Overload = si effettua utilizzando lo stesso nome per richiamare il metodo ma lo si
"carica" con diversi attributi distinti da tre criteri:
Tipale (si guarda il tipo di dato)
Numerico (si contano quanti sono i parametri)
Posizionale (si confronta l'ordine dei parametri)
I nomi che diamo agli identificatori dei parametri non rientrano tra i criteri di
controllo, in pratica i due metodi seguenti sono uguali al compilatore.
somma(int a, int b) somma(int k, int z)
31. Esercizio
Trovare, tra le classi di Java, esempi di overload
Lo stesso metodo println() usato fino ad ora accetta differenti tipi in
ingresso
32. varargs
L'uso dei varargs (java > 5) permette di passare un numero imprecisato e quindi rende
molto più veloce fare un overload dei metodi.
Usare un varargs richiede l'ausilio della keyword … (non importa se vicino al tipo,
all'identificatore o spaziato da entrambi) e va sempre inserito, per ovvi motivi, come
ultimo parametro. Non posso avere più varargs in una dichiarazione. I varargs sono
considerati come array.
public double somma(double base, double ... numeri){
double ris = 0D + base;
for(double n : numeri){
ris+=n;
}
return ris;
}
33. override
Override è il termine usato nella OOP per identificare la caratteristica che ha
una sottoclasse di ridefinire un comportamento (metodo) che viene ereditato da
una superclasse. Senza ereditarietà non esiste override. È la capacità di una
classe più specifica di riadattare il suo funzionamento al fine di funzionare
correttamente col nuovo contesto che rappresenta.
La signature (firma) resta identica (stesso nome e stessi parametri) a quella
della superclasse ma cambia l'implementazione.
Il ritorno deve essere identico al tipo della superclasse o una sua estensione
(una sua sottoclasse)
Il metodo ridefinito non deve essere meno accessibile di quello della
superclasse.
34. override
Giocatore
public String saluta(String s){
return new String("Ciao giocatore " + s);
}
Persona -> Giocatore -> GiocatoreArcade
Persona
protected Object saluta(String s){
return new String("Ciao" + s);
}
35. Annotazione @Override
Per maggiore sicurezza quando si esegue un override di un metodo è opportuno
marcarlo con l'annotazione @Override, questo permette al compilatore (e all'IDE) di
controllare che effettivamente sia un vero override.
Spesso si sbaglia il tipo di parametro, o l'ordine e quindi non si esegue un override ma
un overload. Marcando chiaramente si manifesta la volontà di fare un override ma se
ciò non corrisponde il compilatore ci segnala l'errore.
36. Polimorfismo per dati
Polimorfismo per dati permette di assegnare una reference di una superclasse
all'istanza di una sottoclasse. Es:
Persona p = new Giocatore();
Nella realtà punterà a uno spazio
inferiore di memoria, pertanto non
vedrà tutti i metodi / attributi della
sottoclasse con cui è stato istanziato.
Ciò rende possibile trattare classi
differenti come oggetti uguali a
fronte di operazioni comuni.
37. Parametri polimorfi
In java il passaggio di parametri avviene sempre per valore, in pratica si passa
l'indirizzo di memoria. È quindi possibile passare a un parametro un tipo che sia
sottoclasse di quello richiesto.
In realtà tutti i parametri di tipo reference sono polimorfi, derivando tutti da Object,
se come parametro accetto un Object posso avere qualsiasi parametro a scelta.
Possono anche essere interfacce o classi astratte.
public void getInfo(Persona p){
//TODO...
}
System.out.println(persona.getInfo(new Giocatore("Scacchi")));
38. Collezioni eterogenee
La grande flessibilità dei parametri polimorfi da' la sua massima espressione
nell'uso di oggetti eterogenei. È infatti possibile creare collezioni di oggetti misti,
la massima espressione la si ottiene se si crea una collezione di Object.
Attenzione che la collezione sarà valida al compilatore, ma alcuni metodi non
saranno disponibili per tutti gli elementi della collezione.
È possibile valutare il contenuto di una collezione con l'operatore "instanceof". In
caso di mancata corrispondenza verrà sollevata un'eccezione di NullPointer
39. Collezioni eterogenee
GiocatoreArcade ga1 = new GiocatoreArcade("PacMan",0);
GiocatoreArcade ga2 = new GiocatoreArcade("Arkanoid");
ArbitroArcade a1 = new ArbitroArcade();
Persona lista[] = {ga1,ga2,a1};
for(Persona tmp : lista){
if (tmp instanceof Giocatore){
System.out.println(
((Giocatore) tmp).saluta(
((Giocatore) tmp).getNominativo()
)
);
}
}
40. Casting di oggetti
Tramite il casting ad oggetti diamo la possibilità ad un reference di tipo superclasse di
accedere ai metodi dichiarati dal tipo della sottoclasse.
Castando il tipo di oggetto si ridà la possibilità di accedere ad altri puntatori della
memoria che prima erano "nascosti".
Infatti se grazie al polimorfismo per dati possiamo usare un oggetto di una sottoclasse
come un oggetto della superclasse con il casting possiamo andare, previo controllo
(instanceof), a riabilitare queste funzionalità proprie della sottoclasse.
Gli oggetti hanno lo stesso reference, ma differenti puntatori di memoria! Questa
operazione di cast va sempre esplicitata, altrimenti si ottiene un errore di
compilazione.
42. Invocazione virtuale dei metodi
Un'invocazione a un metodo m può definirsi virtuale quando m è definito in una classe
A, ridefinito nella sottoclasse B(override) e invocato su un'instanza di B tramite una
reference di A(polimorfismo per dati). Il compilatore pensa di avviare il metodo m di
A(virtualmente) ma in realtà viene invocato il metodo m della classe B!
Quello che avviene è che il metodo m di B ha sovrascritto lo stesso intervallo di
puntamento che usa A
Giocatore g4 = new GiocatoreArcade("Pong");
System.out.println(g4.toString());
Giocatore Creato
GiocatoreArcade{punteggio=0, nominativo='Bipede da sala giochi', id=12012, gioco='Pong'}
43. Polimorfismo per interfacce
Il polimorfismo funziona anche usando interfacce (non solo classi o classi astratte).
È possibile creare parametri polimorfi usando delle interfacce.
public interface Rotante(){
void gira();
void ferma();
}
Posso creare un metodo con parametro di tipo Rotante e usare i suoi metodi.
public void accendi(Rotante motore){
motore.gira();
}