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 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
.
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.
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.
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()
.
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.
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
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:
Per esempio su uppercast e downcast (cast espicito) vedi:
Esempio su overridding dei metodi
Considera cosa succede quando creiamo una nuova istanza di GraphicCircle. Prima, il costruttore di GraphicCircle
, , รจ 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
.
Vedi esempio .
Esempio:
Per un ripasso dei package, cosa sono, perchรฉ il codice รจ organizzato in package, vedi: