Ereditarietà e ovverriding
A volte si incontrano classi con funzionalità simili, in quanto utilizzano concetti semanticamente “vicini”. È possibile creare classi disgiunte replicando le porzioni di stato/comportamento condivise, l’approccio “Copia & Incolla”, però, non è una strategia vincente visto che c’è una maggiore difficoltà di manutenzione correttiva e perfettiva. Infatti è meglio “specializzare” codice funzionante sostituendo il minimo necessario.
Meccanismo per definire una nuova classe (classe derivata) come specializzazione di un’altra (classe base). La classe base modella un concetto generico, la classe derivata modella un concetto più specifico. La classe derivata: dispone di tutte le funzionalità (attributi e metodi) di quella base, può aggiungere funzionalità proprie e può ridefinirne il funzionamento di metodi esistenti (polimorfismo).
L’overriding è il meccanismo che permette la sovrapposizione dei metodi di una superclasse da parte di una sottoclasse. Per poter sovrapporre un metodo proveniente da una superclasse bisogna che il nuovo metodo abbia:
- lo stesso nome, lo stesso tipo di ritorno e la stessa lista di parametri;
- visibilità non inferiore (abbiamo visto nelle lezioni precedente ad esempio che private è molto più restrittivo rispetto a public).
L'editarietà in Kotlin è abbastanza diversa da quella a cui siamo abituati in Java e in molti altri linguaggi derivati dal C. Cominciando dalla classe base:
open class Person constructor(var name: String, var age: Int) {
open fun isAdult(): Boolean {
return this.age >= 18
}
}
Come vedete abbiamo usato la keyword open sia a livello di classe che di funzione. Omettendo l'open di classe, non potrebbe essere estesa; omettendo l'open di funzione, non la si potrebbe sovrascrivere nella classe derivata. La sintassi della classe derivata sarà la seguente:
class Student(name: String, age: Int, var typeOfSchool: String) :
Person(name, age) {
override fun isAdult(): Boolean {
return age >= 18 && typeOfSchool.equals("University")
}
}
Per sovrascrivere la funzione base, usiamo la parola standard override. Ricordo che quando si richiama il costruttore della classe derivata in maniera implicita viene chiamato quella della classe base. Per il resto non c'è niente di nuovo. Questo un esempio di uso di entrambe:
fun main(args: Array<String>) {
var person = Person("Matteo", 28)
println(person.isAdult())
var student = Student("Benedetta", 18, "Liceo")
println(student.isAdult())
}
La parola chiave super è utilizzata in Kotlin per riferirsi agli elementi (metodi, proprietà e variabili) della classe base. Come per l’operatore this, anche super, viene utilizzato sia per le variabili (super.variabile), per invocare i metodi (super.metodo()), per richiamare un costruttore da un costruttore secondario (super()). È possibile riferirsi solo agli elementi contenuti nella classe base, non è consentito pertanto utilizzare super.super.variabile. Se da una superclasse si ereditano degli elementi visibili, questi diventano degli elementi a tutti gli effetti della classe, di conseguenza saranno accessibili anche tramite super.
class Veicolo {
var ruote: Int = 0
}
class Auto : Veicolo() {
var stereo = false
}
class Fiat : Auto() {
fun print() {
println(super.ruote)
}
// Qui è possibile utilizzare super.ruote
// utilizzare stereo, equivale a scrivere implicitamente super.stereo
}
La keyword super, come il riferimento this è molto utile in presenza di variabili o metodi con lo stesso nome:
open class Superclass {
open fun printMethod() {
println("Printed in Superclass.")
}
}
class Subclass : Superclass() {
// overrides printMethod in Superclass
override fun printMethod() {
super.printMethod()
println("Printed in Subclass")
}
}
fun main(args: Array<String>) {
val s = Subclass()
s.printMethod()
}
Oppure in presenza di costruttori:
open class Dipendente(nome: String, cognome: String, oreLavorativeMensili: Int, retribuzioneOraria: Int) {
constructor(nome: String, cognome: String) : this(nome, cognome, 0, 0)
}
class ResponsabileDiProgetto : Dipendente {
var bonus = 0
constructor (nome: String, cognome: String, oreLavorativeMensili: Int, retribuzioneOraria: Int, bonus: Int) : super(nome, cognome, oreLavorativeMensili, retribuzioneOraria) {
this.bonus = bonus
}
constructor(nome : String, cognome: String, bonus: Int) : super(nome, cognome, 0,0) {
this.bonus = bonus
}
}
In questo caso però nella classe derivata dobbiamo dire addio al costruttore primario. Se non vogliamo dire addio al costruttore primario dobbiamo evitare di usare la keyword super:
class ResponsabileDiProgetto (nome: String, cognome: String, oreLavorativeMensili: Int, retribuzioneOraria: Int, var bonus: Int) : Dipendente (nome, cognome, oreLavorativeMensili, retribuzioneOraria) {
constructor(nome : String, cognome: String, bonus: Int) : this(nome, cognome, 0,0, bonus) {
this.bonus = bonus
}
}
Visibilità degli elementi
L’oggetto derivato contiene tutti i componenti (attributi, proprietà e metodi) dell’oggetto da cui deriva. Ma i suoi metodi non possono operare direttamente su quelli definiti private. La restrizione può essere allentata: La super-classe può definire attributi e metodi con visibilità protected, questi sono visibili alle sottoclassi.
Come avevamo già visto nella lezione 2 Kotlin offre dei modificatori di visibilit: analizziamoli e vediamo a cosa servono:
open class Outer {
private val a = 1
protected open val b = 2
internal val c = 3
val d = 4 // public by default
protected class Nested {
public val e: Int = 5
}
}
class Subclass : Outer() {
// a is not visible
// b, c and d are visible
// Nested and e are visible
override val b = 5 // 'b' is protected
}
class Unrelated(o: Outer) {
// o.a, o.b are not visible
// o.c and o.d are visible (same module)
// Outer.Nested is not visible, and Nested::e is not visible either
}
fun main(args: Array<String>)
{
var o: Outer
// o.a, o.b are not visible
// o.c and o.d are visible (same module)
// Outer.Nested is not visible, and Nested::e is not visible either
}
Differenza tra overriding e overriding
L’overloading consente di definire in una stessa classe più metodi aventi lo stesso nome, ma che differiscano nella firma, cioè nella sequenza dei tipi dei parametri formali. È il compilatore che determina quale dei metodi verrà invocato, in base al numero e al tipo dei parametri attuali. L’overriding, invece, consente di ridefinire un metodo in una sottoclasse: il metodo originale e quello che lo ridefinisce hanno necessariamente la stessa firma e il tipo di ritorno, e solo a tempo di esecuzione si determina quale dei due deve essere eseguito. Per capire meglio la differenza osservare la seguente figura:
Immagine 7.1 - Differenza illustrata tramite arco e freccie tra overloading e overriding
Compatibilità formale
Un’istanza di una classe derivata è formalmente compatibile con il tipo della super-classe:
val b : Base = Derivata()
Il tipo della variabile b (Base) limita le operazioni che possono essere eseguite sull’oggetto contenuto. Anche se questa è un’istanza di una classe più specifica (Derivata) che è in grado di offrire un maggior numero di operazioni. Le operazioni eseguite sono solo quelle della classe derivata e se sono presenti richiama le versioni “overringate”, perché la JVM mantiene sempre la traccia della classe effettiva di un dato oggetto. Questa feature prende il nome di “binding dinamico”.
val b : Derivata = Base() | NO! |
---|---|
Non è possibile assegnare ad una variabile figlia una variabile padre.