Introduzione su oggetti
Ripresa del capitolo "Introduction to Objects" di Thinking in Java.
Last updated
Ripresa del capitolo "Introduction to Objects" di Thinking in Java.
Last updated
Ci sono due modi per riutilizzare le classi esistenti, la composition (o anche detta aggregazione) e con l'ereditarietà.
Con la composition, definiamo una nuova classe, che è composta di classi già esistenti. Con l'ereditarietà, si deriva una nuova classe basandosi su classi già esistenti, con modifiche o esensioni.
L' aggregazione permette di costruire oggetto più complessi ad esempio un oggetto di tipo (classe) Car dalla composizione di oggetti di altri tipi ad esempio Engine o Weel. La relazione aggregazione è detta anche "has-a", es: 'La macchina ha un motore'.
Sopra grafico UML relazione superclasse/sottoclasse.
Sotto esempio di relazione tra la superclasse shape e le sottoclassi Circle, Square e Triangle, sottoclassi di Shape.
Le sottoclassi Circle, Square e Triangle ereditano i metodi draw(), erase(), move(), getColor() e setColor() dalla superclasse Shape.
Nell'esempio sotto vediamo che la sottoclasse Triangle, estende la classe Shape in quanto aggiunge due metodi a quelli ereditati da Shape, i metodi FlipVertical()
e FlipHorizontal()
.
Sebbene ereditarietà implica talvolta (specialmente in Java, dove la keyword per l'ereditarietà è extends) che to stia andando ad aggiungere nuovi metodi nella sottoclasse, questo non è necessariamente vero. Il secondo e più importante metodo per differenziare la tua nuova classe è cambiare il comportamento del metodo della classe base. Questo è comunemente dettto fare l'override (sovrascrivere) il metodo.
Nel grafico sotto UML vediamo che le sottoclassi Circle, Square e Triangle, sovrascrivono (override) i metodi draw() ed erase() di Shape. Nella terminologia dei linguaggi ad oggetti, si dice che nelle classi Circle, Square e Triangle viene eseguita l'override dei metodi draw() e erase(). Ereditarietà e override dei metodi nelle sottoclassi sono gli ingredienti per il Polimorfismo (vedi paragrafo successivo).
Da notare che oggetti del tipo della sottoclasse possono essere assegnati a variabili della superclasse, in quanto ad esempio un oggetto di tipo Circle è anche una Shape (is-a in terminologia dei linguaggi ad oggetti).
Viceversa non è detto: potrebbe essere una Shape o ad esempio una forma differente a quella a cui sto facendo l'assegnamento. E' necessario un cast esplicito (downcast) per fare l'assegnazione e se l'assegnamento non è valido, genera un'eccezione java.lang.ClassCastException.
Il downcasting richiede il cast espicito nella forma dell'operatore prefisso (
new-type
)
. Come già detto l'operazione di downcast non è sempre sicura, e lancia una eccezione a runtime ClassCastException
se l'istanza che su cui è eseguito il downcast non appartiene alla sottoclasse. Un oggetto di una sottoclasse può esssere sostituito a uno della superclasse, ma il contrario non è vero.
Per esempio su uppercast e downcast (cast espicito) vedi: client/TestSubstitution.java
Per spiegare il polimorfismo partiamo da un esempio: il metodo doSomething accetta qualsiasi oggetto di tipo Shape, quindi è indipendentemente dal tipo di oggetto passato per eseguire l'operazione di erase e draw.
Se in qualche altra parte del programma viene chiamato il metodo doSomething():
La chiamata a doSomething() funziona correttamente indipendentemente dal tipo di oggetto passato.
Considera la linea:
Quello che succede quì è the un oggetto di tipo Circle è passato a un metodo che si aspetta un oggetto di tipo Shape. Siccome il tipo Circle è un tipo di Shape, il metdodo doSomthing lo può trattare come Shape.
Il termine Upcasting per come l'albero della gerarchia di classi è disegnato, con la classe base in alto.
Altro esempio, riguardo gli strumenti musicali sempre sul polimorfismo: scrivo il metodo tune(Instrument i)
che prende come parametro un oggetto di tipo Instrument
(superclasse della gerarchia degli strumenti musicali).
Esempio per far vedere che se invece di sfruttare il polimorfismo nel metodo tune(Instrument i)
nella classe Music
dell'esempio sopra, avessi usato un metodo tune
per ogni tipo di strumento, facendo quindi l'overloading del metodo tune()
:
Facendo l'overloading del metodo tune()
, per ogni nuovo tipo di strumento (classe), dovrei aggiungere un metodo tune
per quel particolare tipo di strumento: ad esempio tune(Guitar i)
nella classe Music2
, se aggiungo la classe Guitar
, mentre con il polimorfismo tune(Instrument i)
funzionerebbe correttamente senza bisogno di modifiche della classe Music
, basta che il nuovo strumento Guitar
sia sottoclasse di Instrument
.
Si vede da questo esempio come il polimorfismo permetta di esendere il codice (ad esempio aggiungere un nuovo tipo di strumento) senza modificare il codice già scritto: il metodo tune(Instrument i)
funziona correttamente anche con la nuova classe Guitar
.
Esempio su overridding dei metodi https://github.com/checksound/Polimorfismo
Il costruttore serve per instanziare, creare, un' oggetto data una classe ad esempio
Modifichiamo la classe Circle
definendo un costruttore:
Mentre con il vecchio, di default, costruttore, dovevamo scrivere il codice in questo modo:
Ora con il nuovo costruttore:
Ci sono due cose importanti da notare, circa il nome e la dichiarazione del costruttore:
il costruttore ha lo stesso nome della classe;
il tipo di ritorno è implicitamente un istanza della classe. Nessun return type è specificato nella dichiarazione del costruttore, neppure la parola chiave void è utilizzata. L'oggetto this è implicitamente ritornato; il contruttore non deve utilizzare return per restituire un valore;
Una classe può avere più costruttori:
Nell'esempio un oggetto di tipo Circle
può essere istanziato, utilizzando uno di questi costruttori e viene inizializzato in base al costruttore invocato, esempio:
Quando scriviamo più costruttori per una classe, ci sono volte che verrebbe comodo chiamare un costruttore da un altro costruttore per evitare di duplicare del codice. Si può fare una simile chiamata usando la keyword this.
Normalmente, quando diciamo this, intendiamo nel senso di "questo oggetto" o "l'oggetto corrente" e in se stesso è la reference all'oggetto corrente. In un costruttore, la keyword this prende un diverso significato quando è seguita da una lista di argomento (anche vuota). Fa una chiamata esplicita al costruttore che corrisponde alla lista degli argomenti. Quindi abbiamo un modo semplice per chiamare altri costruttori.
Il costruttore Flower(String s, int petals)
mostra che si può usare this per chiamare un altro costruttore, non potete usere this due volte. In aggiunta la chiamata al costruttore deve essere la prima istruzione altrimente si riceve un errore di compilazione.
In questo esempio si vede un altro modo in cui this può essere usato. Siccome il nome dell'argomento s e il nome del campo della classe s sono gli stessi per evitare l'ambiguità, si può utilizzare this.s, per dire che ci stiamo riferendo al dato membro.
Nel metodo printPetalCount() si può vedere che il compilatore non permette di chiamare il costruttore da nessun altro metodo se non il costruttore.
Riprendendo l'esempio di Circle
, riscriviamo il codice, usando this ed evitando così duplicazioni di codice:
C'è una restrizione nell'utilizzo di this( ): può esssere solo il primo statement del costruttore.
Questa restrizione è legata all'invocazione automatica del costruttore della superclasse, meccanismo che verrà spiegato in seguito.
Oltre a static
c'è la parola chiave final
che significa che il valore della variabile non può più cambiare. Così non è possibile fare qualcosa come:
Math.sqrt
è un metodo di classe della classe Math
del JDK.
I metodi di classe sono definiti con l'identificativo static
I metodi statici di classe differiscono da quelli di istanza per una cosa importante: non è passato il this
al metodo automaticamente come avviene per i metodi di istanza.
Invocazione di un metodo d'instanza così:
Invocazione di un metodo di classe:
Riprendiamo il concetto di sottoclassi ed ereditarietà.
Un esempio:
Con la parola chiave extends
significa che GraphicCircle
è una sottoclasse di Circle
e che eredita di campi e metodi della superclasse.
Un' altra proprietà importante è che ogno oggetto di tipo GraphicCircle
, è anche un oggetto di tipo Circle
.
Questo construttore si basa sul fatto che la classe GraphicCircle
eredita tutte le variabili da Circle
e semplicemente inizializza lui stesso quelle variabili. Ma questo duplica il codice del costruttore di Circle
, e se Circle
facesse, nel costruttore, una inizializzazione più elaborata, la duplicazione, in GraphicCircle
sarebbe complicata. Inoltre, se la classe Circle
avesse attributi private
(sono spiegati dopo) non potrebbe neppure inizializzarli direttamente, come abbiamo fatto sopra. Ciò di cui abbiamo bisogno è um modo per chiamare un costruttore di Circle, all'interno del nostro costruttore di GraphicCircle
.
Nel codice sotto vediamo come si fa.
Invocazione del costruttore della superclasse:
Utilizzo di super(...)
.
super
è una parola riservata di Java. Uno dei suoi utilizzi è mostrata in questo esempio - per invocare il costruttore della superclasse. Il suo utilizzo è analogo all'uso di this
per invocare il costruttore di una classe dall'interno di un altro costruttore della stessa classe.
L'utilizzo di super
per invocare un costruttore è soggetto alle stesse restrizioni dell'utilizzo di this
per l'invocazione di un costruttore:
super può essere solo usato in questo modo all'interno di un metodo costruttore;
La chiamata al costruttore della superclasse deve essere la prima istruzione all'interno del costruttore;
Quando si definisce una classe, Java garantisce che il metodo costruttore della classe è invocato quando un'istanza della classe è creata. Inoltre garantisce che il costruttore è invocato anche quando un' istanza della sottoclasse è creata. Per garantire questo secondo punto, Java deve assicurare che ogni costruttore chiama un costruttore della superclasse. Se la prima istruzione nel costruttore non è la chiamata al costruttore della superclasse con la super
, allora Java implicitamente aggiunge l'istruzione super()
- cioè, chiama il costruttore della superclasse senza parametri. Se la superclasse non ha un costruttore senza parametri, viene generato un errore di compilazione.
C'è una eccezione alla regola che Java invoca super()
implicitamente se non lo fai esplicitamente. Se la prima riga del costruttore, C1, usa la sintassi this() per invocare un altro costruttore, C2, della classe, Java si basa su C2 per invocare il costruttore della superclasse., e non inserisce in C1 la chiamata super()
. Se, per esempio, anche C2 utilizzasse this() per invocare un terzo costruttore, allora anche C2 non chiamerebbe super(), ma un qualche costruttore della classe, esplicitamente o implicitamente dovrà invocare il costruttore della superclasse.
Considera cosa succede quando creiamo una nuova istanza di GraphicCircle. Prima, il costruttore di GraphicCircle
, codice, è invocato. Questo costruttore esplicitamente invoca il costruttore di Circle
e il costruttore di Circle
implicitamente chiama super()
per invocare il costruttore della superclasse, Object
. In questo modo, il codice del costruttore di Object
viene eseguito prima, seguito dal codice del costruttore di Circle
e finalmente dal codice del costruttore di GraphicCircle
.
Tutto questo vuol dire che le chiamate dei costruttori sono in catena, tutte le volte che un oggetto è creato, tutta la sequenza dei costruttori è invocata, dalla sottoclasse alla superclasse, fino ad Object
, la radice della gerarchia delle classi. Siccome il costruttore della superclasse è sempre invocato come prima istruzione del costruttore, il codice del costruttore di Object
sempre viene eseguito per primo, seguito del codice del costruttore di ogni sottoclasse, e giù per la gerarchia delle classi, fino alla classe che sta per essere istanziata.
Da tener anche presente che se nella classe non è specificato un costruttore, Java genera implicitamente un costruttore di default, che invoca super()
. Esempio, se in GraphicCircle
non fossero definiti dei costruttori, Java genera implicitamente questo costruttore:
Nota che se nella superclasse Circle
, non c'è un costruttore senza argomenti (o quello di default o perchè dichiarati altri costruttori) allore dà errore di compilazione.
Vedi esempio Circle e GraphicCircle - costruttori ed ereditarietà.
L' esempio seguente serve a mostrare l'utilizzo di super per invocare metodi delle superclassi e von la versione sovrascritta (overridden) della sottoclasse.
Data una classe Circle
deriviamo una sottoclasse Cylinder
La sottoclasse Cylinder
:
La classe di test:
Una sottoclasse eredita tutte le variabili membro e metodi dalla sua superclasse (quella padre e i suoi predecessori). Essa può utilizzare i metodi ereditati e le variabili così come sono. Può anche sovrascrivere un metodo ereditato provvedendo la propria versione (overridding), o nascondere la variabile ereditata definendo una variabile con lo stesso nome.
Per esempio, il metodo ereditato getArea()
(da Circle
) in un oggetto di tipo Cylinder
, calcola l'area di base del cilindro. Supponiamo che vogliamo sovrascrivere il metodo getArea()
per calcolare l'area di superfice del cilindro. Sotto ci sono le modifiche alla classe Cylinder
:
Se getArea()
è chiamato da un oggetto di tipo Circle
, esso calcola l'area del cerchio. Se getArea()
è invocato da un oggetto di tipo Cylinder
, esso calcola l'area di superfice del cilindro, usando l'implementazione sovrascritta (overridden). Nota che bisogna utilizzare i metodti pubblici di accesso getRadius()
per recuperare il valore di radius
del Circle
, perchè radius è dichiarato private e quindi non accessibile dalle altre classi, neppure dalle sottoclassi (in questo caso Cylinder
).
Ma se sovrascriviamo getArea()
in Cylinder
, il metodo getVolume()
(=getArea()*height
) non è più corretto. Questo perchè il metodo nuovo, sovrascritto, getArea()
sarà quello utilizzato nel metodo getVolume()
di Cilinder, ma la nuova implementazione del metodo non calcola più l'area di base del cerchio (che a me servirebbe per calcolare il volume). Puoi correggere l'errore nel metodo getVolume()
scrivendo super.getArea()
, per utilizzare la versione di Circle
del metodo getArea()
.
Esempio: https://github.com/checksound/MethodOverridding-CircleAndCilinder
Uno dei principi fondamentali della programmazione ad oggetti è che all'esterno la classe esponga solo ciò che è utile perchè sia utilizzata, senza particolari inutili, ad esempio variabili di istanza della classe che non è appropriato che chi utilizza l'oggetto possa accedere e modificare, magari creando poi un comportamento non voluto riguardo l'utilizzo del metodo. Il metdodo principale per nascondere variabili e metodi di istanza di una classe è tramite il modificatore d'accesso private messo davanti alla variabile o la dichiarazione del metodo. Questo fa si che anche le eventuali sottoclassi non possano accedere alla variabile e al metodo. Con il modificatore private abbiamo quindi la chiusora più totale: solo dall'interno della classe posso accedere a variabili e metodi dichiarati come private. Al contrario con la dichiarazione con il modificatore d'accesso public ho l'apertura più totale. Chiunque può invocare il metodo o accedere alla variabile (anche da classi in altri package, basta che importi la classe).
Situazioni intermedie si han il modificatore protected: variabili o metodi dichiarati come protected sono accedibili da classi dello stesso pacchetto ma anche all'interno di sottoclassi anche se in package diversi.
Se non si mette nessun modificatore d'accesso alla variabile o al metodo, si ha l'accessibilità di tipo package, cioè sono accedibili solo da classi all'interno dello stesso package.
Per un ripasso dei package, cosa sono, perché il codice è organizzato in package, vedi: http://www3.ntu.edu.sg/home/ehchua/programming/java/J9c_PackageClasspath.html
Le interface e le classi astratte provvedono a un meccanismo più strutturato per separare le interfacce (inteso come comportamento esposto da una classe) e l'implementazione.
Questi meccanismi non sono supportati in altri linguaggi, come il C++, fornendo un supporto solo indiretto a questi concetti. Il fatto che in Java esistano parole chiave (abstract e interface) indica che questi concetti sono così importanti da fornirgli supporto diretto.
Per prima cosa vedremo le classi astratte, che sono in un certo senso una via di mezzo tra le classi ordinarie e le interface. Sebbene il primo impulso sarebbe quello di creare le interface, le classi astratte sono uno strumento importante e necessario per costruire classi che hanno qualche metodo non implementato.
Qui sotto l'esempio dell'orchestra modificato con l'uso di abstract class e metodi.
Esempio invece della classe Shape
resa abstract:
Esempio di utilizzo:
Con la parola chiave interface porta il concetto di abstract class ancora più in là. Con abstract permetteva di definire uno o più metodi non implementati, definendo l'interfaccia ma non la corrispettiva implementazione. L'implementazione è definita nelle sottoclassi. Con la parola chiave interface si crea una classe completamente astratta, una classe che non provvede alcuna implementazione. Permette al creatore di determinare il nome dei metodi, la lista degli argomenti, i tipi di ritorno, ma non l'implementazione del metodo.
Una interface dice: "Tutte le classi che implementano questa particolare interfaccia saranno con questa immagine". Quindi, ogni codice che utilizza una interface, sa quali metodi possono esser chiamati per quella interface, e basta. Così l'interface è utilizzata per stabilire il protocollo tra le classi.
Per fare una classe che conforma a una particolare interface (o insieme di interfacce), si usa la parola chiave implements, che dice: "L'interfaccia indica come appare, ma ora io sto facendo vedere come funziona". Il diagramma delle classi per l'esempio degli strumenti:
Si può vedere dalla classi Woodwind e Brass che una volta che è implementata un'interfaccia, la implementazione diventa una classe ordinaria che può essere estesa nel modo normale.
Da notare che non è importante se si fa l'upcasting a una classe "regolare" chiamata Instrument
, una classe abstact chiamata Instrument
o un interface chiamata Instrument
. Il comportamento è identico. Infatti si può vedere che il metodo tune()
non ha nessun indizio se Instrument
sia una classe normale, astratta o una interface.
Altro esempio:
Implementazione di un'interface:
Accessibile a:
public
protected
package
private
Stessa classe
si
si
si
si
Classi stesso pacchetto
si
si
si
no
Sottoclassi in pacchetto differente
si
si
no
no
Non sottoclassi, pacchetti differenti
si
no
no
no