Ereditarietà

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

versione stampabile

Riconoscimenti

  • Questo materiale è ampiamente basato su quello realizzato dai Prof. Mirko Viroli e Roberto Casadei, che ringrazio.

  • Ogni errore riscontratovi è esclusiva responsabilità degli autori di questo documento.

Outline

Goal della lezione

  • Illustrare il riuso via ereditarietà
  • Introdurre i vari meccanismi collegati all’ereditarietà

Argomenti

  • Estensione di classi: extends
  • Livello d’accesso protected
  • Overriding dei metodi
  • Gestione dei costruttori e chiamate super
  • Il modificatore final su classi e metodi

Riuso via ereditarietà

Ereditarietà

È un meccanismo che consente di definire una nuova classe specializzandone una esistente, ossia

  • “ereditando” i suoi campi e metodi, quindi riusando codice già scritto e testato
    • è ortogonale al discorso di “visibilità”: membri privati, seppur non visibili, sono comunque ereditati
  • estendendo attraverso
    • (A) modifica di metodi (sovrascrittura/overriding) e/o
    • (B) aggiunta di campi/metodi

Scenari di riuso ed estensione

  • Data una classe, realizzarne un’altra con caratteristiche solo in parte diverse (o nuove)
    • ad esempio, data una lampadina (Lamp), realizzare un televisore (Tv)
    • SOTTOCASO: stessa cosa, ma senza disporre dei sorgenti della classe originaria (p.e., la classe di partenza è di libreria)
      • Non c’è alcuna differenza rispetto al primo caso: quello che conta in generale è il bytecode, non il sorgente
  • Data una classe, crearne una più specializzata
    • ad esempio, a partire da una lampadina (SimpleLamp), realizzare una lampadina con controllo livello di intensità luminosa (AdvancedLamp)
    • ad esempio, una classe più robusta e sicura, anche se più lenta
  • Creare gerarchie di classi (ossia di “comportamenti”)
    • Ad esempio, una famiglia di Device diversi e specializzati

L’ereditarietà è un concetto chiave dell’OO

  • È connesso al meccanismo delle interfacce
  • È uno degli elementi chiave insieme a incapsulamento e interfacce
  • Non riguarda solo il riuso di codice, ma influenza anche il polimorfismo conseguente

Solito approccio

  • Illustreremo i meccanismi base attraverso semplici classi
  • Successivamente recupereremo l’importanza nei casi reali
  • Utilizzeremo l’idea di contatore

Esempio base: Counter

public class Counter {

	private int value;

	public Counter(final int initialValue) {
		this.value = initialValue;
	}

	public void increment() {
		this.value++;
	}

	public int getValue() {
		return this.value;
	}
}

Uso della classe Counter

public class UseCounter {
	public static void main(String[] s) {
		final Counter c = new Counter(0);

		System.out.println(c.getValue()); // 0
		c.increment();
		c.increment();
		System.out.println(c.getValue()); // 2
	}
}

Una nuova classe: MultiCounter

  • potrebbe essere implementata a partire dal codice sorgente di Counter
  • rispetto a Counter, offre un metodo multiIncrement(int)
public class MultiCounter {

    private int value;

    public MultiCounter(final int initialValue) {
        this.value = initialValue;
    }

    public void increment() {
        this.value++;
    }

    public int getValue() {
        return this.value;
    }

    /* Nuovo metodo */
    public void multiIncrement(final int n) {
        for (int i = 0; i < n; i++) {
            this.increment();
        }
    }
}

Uso della classe MultiCounter

public class UseMultiCounter {
    public static void main(String[] s) {
        final MultiCounter mc = new MultiCounter(10);
        System.out.println(mc.getValue()); // 10
        mc.increment();
        mc.increment();
        System.out.println(mc.getValue()); // 12
        mc.multiIncrement(10);
        System.out.println(mc.getValue()); // 22
    }
}

Versione con riuso via composizione: MultiCounter2

public class MultiCounter2 {

    private Counter counter;

    public MultiCounter2(final int initialValue) {
        this.counter = new Counter(initialValue);
    }

    public void increment() {
        this.counter.increment();
    }

    public int getValue() {
        return this.counter.getValue();
    }

    /* Nuovo metodo */
    public void multiIncrement(final int n) {
        for (int i = 0; i < n; i++) {
            this.counter.increment();
        }
    }
}
  • abbiamo riusato le funzionalità di Counter (via delega) ma non abbiamo veramente “ridotto” la quantità di codice scritto

La necessità di estendere e modificare

Una tipica situazione

  • È tipico nei progetti software, accorgersi di dover creare anche “versioni modificate/estese” delle classi esistenti
  • Appoggiarsi al “copia e incolla” per ottenere ripetizione di codice è sempre sconsigliabile (principio DRY)
    • poiché tende a spargere errori in tutto il codice, e complica la manutenzione (se ciò che vogliamo modificare è sparso in N punti, dovremo prima localizzare questi N punti e poi ivi applicare la modifica)
  • Ottenere riuso via composizione (ossia delegazione) è in generale una ottima soluzione.. ma è possibile in alcuni casi fare meglio..

Si usa il meccanismo di ereditarietà

  • Definizione: class C extends D { ... }
  • La nuova classe C eredita campi/metodi di D
    • Nota: eredita anche campi/metodi privati, ma non sono accessibili da C (la “visibilità” di un campo è ortogonale alla sua “presenza” in una classe)
    • Nota: i costruttori non vengono ereditati, ma riusati implicitamente o esplicitamente
  • Terminologia: D superclasse, o classe base, o classe padre
  • Terminologia: C sottoclasse, o classe figlia, o specializzazione
  • Nota: non serve disporre dei sorgenti di D (basta il codice binario)

Una nuova versione di MultiCounter

/* Si noti la clausola extends */
public class MultiCounter extends Counter {

    /*
     * I costruttori vanno ridefiniti. Devono tuttavia richiamare 
     * quelli ereditati dalla sopraclasse
     */
    public MultiCounter(int initialValue) {
    	super(initialValue);
    }

    // increment e getValue automaticamente ereditati

    // si aggiunge multiIncrement
    public void multiIncrement(final int n) {
        for (int i = 0; i < n; i++) {
            this.increment();
        }
    }
}

Razionale

Ridefiniamo la classe MultiCounter come estensione di Counter

  • Definiamo il nuovo metodo multiIncrement()
  • Definiamo il costruttore necessario
    • Il costruttore di una sottoclasse può cominciare con l’istruzione super, che chiama un costruttore (non privato) della classe padre
    • Se non lo fa, si chiama il costruttore senza argomenti del padre (se c’è, altrimenti ERRORE)
    • Senza costruttori, si ha al solito solo quello di default (che chiama il costruttore senza argomenti del padre)
  • UseMultiCounter continua a funzionare!

Il senso della definizione

  • Un oggetto di MultiCounter è simile ad un oggetto di Counter
    • Ha i metodi increment() e getValue()
    • Ha anche il campo value (che in effetti è incrementato), anche se essendo privato è inaccessibile dal codice della classe MultiCounter
  • Due modifiche necessarie rispetto a Counter: metodo multiIncrement() e ridefinizione del costruttore

Notazione UML per l’estensione

  • Arco a linea continua e punta a triangolo (vuoto/pieno) per la relazione “extends” (specializzazione)
    • Stessa notazione di “realizzazione interfaccia”, a parte che quest’ultima ha linea tratteggiata
  • Nel caso di più classi figlie: archi raggruppati per migliorare la resa grafica
Counter
-int value
+Counter(initialValue: int) : Counter
+void increment()
+int getValue()
MultiCounter
+MultiCounter(initialValue: int) : MultiCounter
+void multiIncrement(n: int)

Notazione UML – versione semplificata per il design

Counter
void increment()
int getValue()
MultiCounter
void multiIncrement(n: int)

Livello d’accesso protected

  • Motivazione: una classe figlia è un caso particolare di scope e quindi si potrebbe voler regolare la visibilità in tale scope
    • Si ricordi che per il principio dell’information hiding si vuole ridurre il più possibile la “superficie visibile” per evitare una proliferazione di dipendenze
    • Per motivi pratici (principalmente: riuso), può aver senso allargare la visibilità alle sottoclassi

Usabile per le proprietà d’una classe

  • È un livello intermedio fra public e private
  • Indica che la proprietà (campo, metodo, costruttore) è accessibile, oltre che dalla classe corrente, da :
    • una sottoclasse (in qualsiasi package), e dalle sottoclassi delle sottoclassi (ricorsivamente)
    • cavillo: anche da tutto il package (qualsiasi classe nello stess package)
  • In altre parole: il membro è “package-private” con visibilità estesa a tutte le classi discendenti

A cosa serve, protected?

  • Consente alle sottoclassi di accedere ad informazioni della sopraclasse che non si vogliono far vedere agli utilizzatori
  • Molto spesso usato a posteriori rimpiazzando un private
  • Molto meglio avere campi privati e getter/setter protetti

Esempio classe BiCounter – contatore bidirezionale

  • Un contatore con anche il metodo decrement
  • Irrealizzabile senza rendere accessibile il campo counter

Un contatore estendibile: ExtendibleCounter

/* Il nome ExtendibleCounter è di comodo, più propriamente
 andrebbe chiamata semplicemente Counter */

public class ExtendibleCounter {

    /* campo value protetto */
    protected int value;

    public ExtendibleCounter(final int initialValue) {
        this.value = initialValue;
    }

    public void increment() {
        this.value++;
    }

    public int getValue() {
        return this.value;
    }
}

Classe MultiCounter

public class MultiCounter extends ExtendibleCounter {

    public MultiCounter(final int initialValue) {
        super(initialValue);
    }

    public void multiIncrement(final int n) {
        // Ora realizzabile più efficientemente
        if (n > 0) {
            this.value = this.value + n;
        }
    }
}

Classe BiCounter

  • NB: irrealizzabile (a meno di trick come overflow) mantenendo l’interfaccia di Counter (i.e. increment + getValue) senza un rendere accessibile in scrittura alle sottoclassi il campo value (direttamente o via setter)
public class BiCounter extends ExtendibleCounter {

	public BiCounter(final int initialValue) {
		super(initialValue);
	}

	public void decrement() {
		/* Ora this.counter è accessibile */
		this.value--;
	}
}

Overriding di metodi

Estensione e modifica

  • Quando si crea una nuova classe per estensione, spesso non è sufficiente aggiungere nuove funzionalità
  • A volte serve anche modificare alcune di quelle disponibili, eventualmente anche stravolgendone il funzionamento originario
  • Questo è realizzabile riscrivendo nella sottoclasse uno (o più) dei metodi della superclasse (ossia, facendone l’overriding)
    • Se necessario, il metodo riscritto può invocare la versione del padre usando il receiver speciale super
  • NB: non si confondano i termini overloading e overriding (“to preveil”, “to extend over”, “to supersede”, “to overrule”)
    • Terminologicamente: override vs. overwrite (“Override: the original is not being destroyed afterwards. Overwrite: the original is being destoryed afterwards”)

Esempio

  • Creare un contatore che, giunto ad un certo limite, non prosegue più
    • non basta aggiungere qualcosa
  • È necessario fare overriding del metodo increment()
  • Un ulteriore metodo getter ispeziona il raggiungimento del limite

Classe LimitCounter

public class LimitCounter extends ExtendibleCounter {

    /* Aggiungo un campo, che tiene il limite */
    protected final int limit;

    public LimitCounter(final int limit) {
        super(0);
        this.limit = limit;
    }

    public boolean isOver() {
        return this.getValue() == this.limit;
    }

    /* Overriding del metodo increment() */
    public void increment() {
        if (!this.isOver()) {
            super.increment();
        }
    }
}

Uso della classe LimitCounter

public class UseLimitCounter {
	public static void main(String[] s) {
		final LimitCounter c = new LimitCounter(5);
		System.out.println(c.getValue()); // 0
		System.out.println(c.isOver()); // false
		c.increment();
		c.increment();
		System.out.println(c.getValue()); // 2
		System.out.println(c.isOver()); // false
		c.increment();
		c.increment();
		c.increment();
		c.increment();
		c.increment();
		c.increment();
		c.increment();
		System.out.println(c.getValue()); // 5
		System.out.println(c.isOver()); // true
	}
}

Notazione UML

  • I campi/metodi protetti si annotano con un “#”
  • I metodi overridden si riportano anche nella sottoclasse
ExtendibleCounter
#int value
+ExtendibleCounter(initialValue: int) : ExtendibleCounter
+void increment()
+int getValue()
LimitCounter
#int limit
+LimitCounter(initialValue: int, limit: int) : LimitCounter
+isOver() : boolean
+void increment()

Uno scenario completo

Una applicazione allo scenario domotica

Elementi

  • Usiamo LimitCounter
  • Definiamo una LimitedLamp (via estensione) che contiene un contatore, e che ha un tempo di vita basato sul numero di accensioni ammesse
  • Un EcoDomusController si compone di $n$ LimitedLamp, e ha la possibilità di verificare se tutte le lampadine sono esaurite, e di accendere la lampadina alla quale è rimasto più tempo di vita

Note sulla soluzione

  • Una alternativa era far sì che EcoDomusController componesse $n$ SimpleLamp e $n$ LimitCounter
  • LimitedLamp realizza alcuni metodi per delegazione al suo contatore
  • Per ora, non prevediamo l’aspetto di interazione con l’utente

Diagramma UML complessivo

LimitCounter

public class LimitCounter extends ExtendibleCounter {

    private final int limit;

    public LimitCounter(final int initialValue, final int limit) {
        super(initialValue);
        this.limit = limit;
    }

    public boolean isOver() {
        return this.getDistanceToLimit() == 0;
    }

    public int getDistanceToLimit() {
        return this.limit - this.value;
    }

    public void increment() {
        if (!this.isOver()) {
            super.increment();
        }
    }
}

SimpleLamp

public class SimpleLamp {

	private boolean switchedOn;

	public SimpleLamp() {
		this.switchedOn = false;
	}

	public void switchOn() {
		this.switchedOn = true;
	}

	public void switchOff() {
		this.switchedOn = false;
	}

	public boolean isSwitchedOn() {
		return this.switchedOn;
	}
}

LimitedLamp

public class LimitedLamp extends SimpleLamp {

	private LimitCounter counter;

	public LimitedLamp(final int limit) {
		super(); // Questa istruzione è opzionale
		this.counter = new LimitCounter(0, limit);
	}

	public void switchOn() {
		if (!this.isSwitchedOn()) {
			// incremento solo se è una vera accensione
			this.counter.increment();
		}
		if (!this.counter.isOver()) {
			super.switchOn();
		}
	}

	public int getRemainingLifeTime() { // delegazione a counter
		return this.counter.getDistanceToLimit();
	}

	public boolean isOver() { // delegazione a counter
		return this.counter.isOver();
	}
}

EcoDomusController

public class EcoDomusController {

	/* Compongo n LimitedLamp */
	final private LimitedLamp[] lamps;

	public EcoDomusController(final int size, final int lampsLimit) {
		this.lamps = new LimitedLamp[size];
		for (int i = 0; i < size; i++) {
			this.lamps[i] = new LimitedLamp(lampsLimit);
		}
	}

	public LimitedLamp getLamp(final int position) {
		return this.lamps[position];
	}

	private LimitedLamp toBeUsedNext() {
		LimitedLamp best = null;
		for (final LimitedLamp lamp : this.lamps) {
			if (!lamp.isSwitchedOn() && 
				( best == null ||
				  lamp.getRemainingLifeTime() > best.getRemainingLifeTime())) {
				best = lamp;
			}
		}
		return best;
	}
	/* Accendo una lampadina spenta, scegliendola in modo economico */
	public void switchOnOne() {
		final LimitedLamp lamp = this.toBeUsedNext();
		if (lamp != null) {
			lamp.switchOn();
		}
	}

	/* Verifico se sono tutti accesi */
	public boolean allOver() {
		for (final LimitedLamp lamp : this.lamps) {
			if (!lamp.isOver()) {
				return false;
			}
		}
		return true;
	}

	public String toString() {
		String s = "";
		for (final LimitedLamp lamp : this.lamps) {
			s += (lamp.isSwitchedOn() ? "on" : "off");
			s += "(" + lamp.getRemainingLifeTime() + ")" + " | ";
		}
		return s;
	}
}

UseEcoDomusController

public class UseEcoDomusController {
	public static void main(String[] s) {
		// Simulazione sessione di lavoro
		final EcoDomusController controller;
		controller = new EcoDomusController(5, 10);
		System.out.println(controller);
		// off(10) | off(10) | off(10) | off(10) | off(10) |
		final LimitedLamp l = controller.getLamp(0);
		l.switchOn();
		l.switchOff();
		l.switchOn();
		System.out.println(controller);
		// on(8) | off(10) | off(10) | off(10) | off(10) |
		controller.switchOnOne();
		controller.switchOnOne();
		controller.switchOnOne();
		controller.switchOnOne();
		System.out.println(controller);
		// on(8) | on(9) | on(9) | on(9) | on(9) |
	}
}

Ulteriori dettagli

Ereditarietà e costruttori

Costruttori di default e chiamate implicite

class A { }
class B extends A { } // OK
  • B ha un costruttore di default, che invoca il costruttore senza argomenti (quello di default in questo caso) di A
class A { A() { out.print("A"); } }
class B extends A { } // OK
  • B ha un costruttore di default, che invoca il costruttore senza argomenti di A
class A { A(int x) { out.print("A" + x); } }
class B extends A { } // ERROR
  • B ha un costruttore di default, che vorrebbe invocare il costruttore di A senza argomenti, ma non lo trova! Il compilatore restituisce un errore.
class A { }
class B extends A { B() { out.print("B"); } } // OK
// Stessa cosa di:
class B extends A { B() { super(); out.print("B"); } }
  • B ha un costruttore definito, che invoca implicitamente il costruttore senza argomenti (quello di default in questo caso) di A
class A { A() { out.print("A"); } }
class B extends A { B() { out.print("B"); } } // OK
// Stessa cosa di:
class B extends A { B() { super(); out.print("B"); } }
  • B ha un costruttore definito, che invoca implicitamente/esplicitamente il costruttore senza argomenti di A
class A {  A(int x) { out.print("A" + x); }  }
class B extends A { B() { out.print("B"); } } // ERROR
// Qua occorre fare:
class B extends A { B() { super(7); out.print("B"); } }
  • B ha un costruttore definito, che vorrebbe implicitamente invocare il costruttore di A senza argomenti, ma non lo trova! Il compilatore restituisce un errore.

Costruzione di oggetti in gerarchie di classi

  • Assumiamo si stia costruendo una catena di sottoclassi
  • Ogni classe introduce alcuni campi, che si aggiungono a quelli della superclasse a formare la struttura di un oggetto in memoria

Linee guida per la singola classe

  • Dovrà definire tutti i costruttori necessari, seguendo l’approccio visto
  • Ogni costruttore dovrà preoccuparsi di:
    • Chiamare l’opportuno costruttore padre come prima istruzione (super), altrimenti il costruttore di default verrà chiamato, se c’è
    • Inizializzare propriamente i campi localmente definiti

Ordine operazioni a seguito di una new

  • Prima si crea l’oggetto con tutti i campi non inizializzati
  • Il codice dei costruttori sarà eseguito, dalle superclassi in giù

Analisi: cosa succede?

class A {
	protected int i;

	public A(int i) {
		System.out.println("A().. prima " + this.i);
		this.i = i;
		System.out.println("A().. dopo " + this.i);
	}
}
class B extends A {
	protected String s;

	public B(String s, int i) {
		super(i);
		System.out.println("B().. prima " + this.s + " " + this.i);
		this.s = s;
		System.out.println("B().. dopo " + this.s + " " + this.i);
	}
	
	public static void main(String[] s) {
		B b = new B("prova", 5); // Cosa succede?
	}
}

Chiamate di metodo alla superclasse (super)

Chiamate super

  • Una sottoclasse C può includere una invocazione del tipo super.m(..args..)
  • Non solo in caso di overriding
  • Cosa ci aspettiamo succeda?

Semantica

  • Accade quello che accadrebbe se la classe corrente non avesse il metodo m, ossia viene eseguito il metodo m della superclasse
    • O, se anche lì assente, quello nella sopraclasse più specifica che lo definisce
  • Se tale metodo al suo interno chiama un altro metodo n (su this), allora si ritorna a considerare la versione più specifica a partire dalla classe di partenza C

Analisi: cosa succede?

class C {
	protected int i;

	void m() {
		System.out.println("C.m.. prima " + i);
		this.i++;
		System.out.println("C.m.. dopo " + i);
	}
}
class D extends C {
	D(int i) {
		this.i = i;
	}
	void m() {
		super.m();
		System.out.println("D.m.. dopo " + this.i);
	}
	public static void main(String[] s) {
		new D(5).m(); // Cosa succede?
	}
}

Altra analisi: cosa succede?

class E {
	protected int i;

	void m() {
		this.i++;
		this.n();
	}
	void n() {
		this.i = this.i + 10;
	}
}
class F extends E {
	void n() {
		this.i = this.i + 100;
	}
	public static void main(String[] s) {
		F f = new F();
		f.i = 10;
		f.m();
		System.out.println("" + f.i);
	}
}

Un esempio: riprendiamo LimitCounter

public class LimitCounter extends ExtendibleCounter {

    private final int limit;

    public LimitCounter(final int initialValue, final int limit) {
        super(initialValue);
        this.limit = limit;
    }

    public boolean isOver() {
        return this.getDistanceToLimit() == 0;
    }

    public int getDistanceToLimit() {
        return this.limit - this.value;
    }

    public void increment() {
        if (!this.isOver()) {
            super.increment();
        }
    }
}

Un esempio: nuova specializzazione

Cosa succede chiamando increment() su un UnlimitedCounter?

  • Non avendo fatto overriding, si chiama la versione di LimitCounter
  • In LimitCounter si chiama this.isOver() che chiama this.getDistanceToLimit()
  • La versione di this.getDistanceToLimit() eseguita è quella di UnlimitedCounter
public class UnlimitedCounter extends LimitCounter {

	public UnlimitedCounter() {
		super(0, Integer.MAX_VALUE);
	}

	public int getDistanceToLimit() {
		// Quindi il contatore non scade mai
		return Integer.MAX_VALUE;
	}
}

Uso di UnlimitedCounter

public class UseUnlimitedCounter {
	public static void main(String[] s) {
		final UnlimitedCounter uc = new UnlimitedCounter();
		System.out.println("isOver: " + uc.isOver()); // false
		System.out.println("LifeTime: " + uc.getDistanceToLimit());
		uc.increment();
		uc.increment();
		uc.increment();
		System.out.println("isOver: " + uc.isOver()); // false
		System.out.println("LifeTime: " + uc.getDistanceToLimit());
	}
}

La tabella dei metodi virtuali

Anche detta: vtable, call table, dispatch table

  • ogni classe C ne ha una, ed è accessibile ai suoi oggetti
  • ad ogni metodo definito (o ereditato) in C, associa il codice corrispondente da eseguire, ossia la classe che riporta il body
  • le chiamate da risolvere con tale tabella sono quelle con late binding
  • è una struttura che rende efficiente il polimorfismo fra classi (che vedremo)
  • è utile conoscerla anche se non è detto che la JVM usi esattamente tale struttura
  • fa comprendere il funzionamento di this. e super.

Esempio

Come sono fatte le tabelle relative alle classi LimitedCounter e UnlimitedCounter nell’esempio precedente?

Esempio gestione memoria: stack/heap/vtables

Il modificatore final

Problema

  • Tramite l’overriding e le chiamate super è possibile prendere classi esistenti e modificarle con grande flessibilità
  • Questo introduce problemi di sicurezza, specialmente connessi al polimorfismo che vedremo nella prossima lezione

Soluzione: final

  • Oltre che per i campi (e argomenti di funzione o variabili, come già visto), è possibile dichiare final anche metodi e intere classi
  • Un metodo final è un metodo che NON può essere ri-definito per overriding
  • Una classe final non può essere estesa

Nelle librerie Java

  • Moltissime classi sono final, ad esempio String

Overriding e controllo d’accesso

Regole per fare l’overriding di un metodo $M$

  • La nuova versione deve avere esattamente la stessa signature
  • È possibile estendere la visibilità di un metodo (da protected a public)
  • Non è possile limitare la visibilità di un metodo (p.e. da public a protected, o da public a private)
  • È possibile indicare il metodo final

$\Rightarrow$ sono tutte conseguenze del principio di sostituibilità

La classe Object

Estensione di default

  • Una classe deve per forza estendere da qualcosa
  • Se non lo fa, si assume che estenda java.lang.Object
  • Quindi ogni classe eredita (indirettamente) da Object
  • Object è la radice della gerarchia di ereditarietà di Java

Classe Object

Fornisce alcuni metodi di utilità generale

  • toString(), che stampa informazioni sulla classe e la posizione in memoria dell’oggetto
  • clone(), per clonare un oggetto
  • equals() e hashCode(), usati nelle collection
  • notify() e wait(), usati nella gestione dei thread

Equals

  • Il metodo equals è definito nella classe Object e può essere sovrascritto
  • Il metodo equals è usato per confrontare due oggetti
  • La sua implementazione di default confronta i riferimenti degli oggetti
    • La sua implementazione di default è equivalente a ==
  • La sua implementazione di default è spesso inadeguata
    • Ad esempio, due oggetti con lo stesso contenuto dovrebbero essere considerati uguali
    • == quindi non è un confronto semantico
  • In generale quindi si sovrascrive equals per definire un confronto semantico
class Person {
    private String name;
    private int age;
    // ...
    @Override
    public boolean equals(Object o) {
      return age == person.age && Objects.equals(name, person.name);
    }
}

Annotazione @Override

  • Metodi che fanno override di metodi di interfacce/classi base possono essere annotati con @Override
    • Non è necessario, ma è buona prassi
  • Implicazioni
    • Il compilatore genererà un errore se il metodo non fa effettivamente override
    • Migliora la leggibilità di codice, rendendo immediatamente evidenti i casi di override di metodi
class Person {
    // ...
    @Override
    public String toString() {
      // ...
    }
}