Polimorfismo, classi astratte, tipi a runtime

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 la connessione fra polimorfismo inclusivo e ereditarietà
  • Mostrare le interconnessioni con interfacce e classi astratte
  • Mostrare le varie ripercussioni nel linguaggio

Argomenti

  • Polimorfismo inclusivo con le classi
  • Layout in memoria degli oggetti
  • Il concetto (e il problema) dell’ereditarietà multipla
  • Autoboxing dei tipi primitivi
  • Tipi a run-time (cast, instanceof)
  • Classi astratte

Polimorfismo inclusivo con le classi

Ereditarietà e polimorfismo

Ricordando il principio di sostituibilità

SE B è un sottotipo di A

ALLORA ogni oggetto di B può/“deve poter” essere utilizzabile dove ci si attende un oggetto di A

void m(A a) { /* ... */ }
B b = /* ... */;

A a = b; // OK
m(b);    // OK

Con l’ereditarietà

  • Con la definizione: class B extends A { ... }
    • B eredita tutti i membri (campi, metodi) di A, e non può restringerne la visibilità
  • Gli oggetti della classe B rispondono a tutti i messaggi previsti dalla classe A (ed eventualmente a qualcuno in più)
  • $\to$ un oggetto di B può essere passato dove se ne aspetta uno di A, senza dare problemi (di “typing”)

Conseguenza:

Poiché è possibile, corretto, ed utile, allora in Java si considera B come un sottotipo di A a tutti gli effetti!

Polimorfismo con le classi

Polimorfismo con le classi

In una classe D che usi una classe C

  • ci saranno punti in D in cui ci si attende oggetti della classe C

    • come argomenti a metodi, o da depositare nei campi..
  • si potranno effettivamente passare oggetti della classe C, ma anche delle classi C1, C2,..,C5, o di ogni altra classe successivamente creata che estende, direttamente o indirettamente C

class C { public void foo() { /*...*/ } }

class D { // rappresenta un generico "contesto"
    C c;
    public void m(C c) { c.foo(); }
}
class C1 extends C { }
class C2 extends C { }

D d = new D();
d.c = new C1(); // OK
d.m(new C2());  // OK

Le sottoclassi di C

A tutti gli effetti, gli oggetti di C1, C2 … (sottoclassi di C) sono compatibili con gli oggetti della classe C

  • supportano lo stesso contratto (in generale, qualche operazione in più)
  • hanno lo stesso stato, ovvero gli stessi campi definiti in C (in generale, qualcuno in più)
  • hanno auspicabilmente un comportamento compatibile (cf. “Principle of Least Surprise”)

Layout oggetti in memoria

Alcuni aspetti del layout degli oggetti in memoria…

Diamo alcune informazioni generali e astratte. Ogni JVM potrebbe realizzare gli oggetti in modo un po’ diverso. Questi elementi sono tuttavia comuni.

Struttura di un oggetto in memoria

  • Inizia con una intestazione ereditata da Object (16 byte circa), che include

    • Indicazione di quale sia la classe dell’oggetto (runtime type information)
    • Tabella dei puntatori ai metodi, per supportare il late-binding
    • I campi della classe Object
  • Via via tutti i campi della classe, a partire da quelli delle superclassi

Conseguenze: se la classe C è sottoclasse di A

Allora un oggetto di C è simile ad un oggetto di A: ha solo informazioni aggiuntive in fondo, e questo semplifica la sostituibilità!

Esempio applicazione polimorfismo fra classi – UML

Person

public class Person {

    private final String name;
    private final int id;

    public Person(final String name, final int id) {
        super();
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public int getId() {
        return this.id;
    }

    public String toString() {
       return "P [name=" + this.name + ", id=" + this.id + "]";
    }
}

Student

public class Student extends Person {
	
	final private int matriculationYear;

	public Student(final String name, final int id, 
			           final int matriculationYear) {
		super(name, id);
		this.matriculationYear = matriculationYear;
	}

	public int getMatriculationYear() {
		return matriculationYear;
	}

	public String toString() {
		return "S [getName()=" + getName() + 
				", getId()=" + getId() +
				", matriculationYear=" + matriculationYear + "]";
	}

}

Teacher

import java.util.Arrays;

public class Teacher extends Person {
	
	final private String[] courses;

	public Teacher(final String name, final int id, 
			           final String[] courses) {
		super(name, id);
		this.courses = Arrays.copyOf(courses, courses.length);
	}

	public String[] getCourses() {
		// copia difensiva necessaria a preservare incapsulamento
		return Arrays.copyOf(courses, courses.length);
	}

	public String toString() {
		return "T [getName()=" + getName() + 
				", getId()=" + getId() + 
				", courses=" + Arrays.toString(courses) + "]";
	}
}

UsePerson

public class UsePerson {

	public static void main(String[] args) {
	    
		final var people = new Person[]{
				new Student("Marco Rossi",334612,2013),
				new Student("Gino Bianchi",352001,2013),
				new Student("Carlo Verdi",354100,2012),
				new Teacher("Mirko Viroli",34121,new String[]{
						"OOP","PPS","PC" 
				})
		};
		
		for (final var p: people){
			System.out.println(p.getName()+": "+p);
		}
	}
}

La differenza col caso del polimorfismo con le interfacce

Polimorfismo con le interfacce

  • La classe D realizza una interfaccia I, ma non eredita da un’altra classe C
  • Si può assumere vi sia un certo contratto, ma non che vi sia uno specifico comportamento
    • Ad esempio, una classe che realizza l’interfaccia Counter può assumere vi sia un metodo increment(); mentre una classe che eredita da LimitCounter può assumere che increment() sia soggetto a un limite di incrementi unitari
  • E’ possibile realizzare più interfacce (cioè, supportare più contratti) ma non è possibile (in Java) estendere più classi

Le classi in Java non consentono “ereditarietà multipla” (in C++ si)

  • NON è possibile in Java dichiarare: class C extends D1, D2, ... { ... }
    • si creerebbero problemi se D1 e D2 avessero proprietà comuni (cf. Triangle Problem e Diamond Problem)
      • ad es., quale sarebbe l’implementazione ereditata?
    • diventerebbe complicato gestire la struttura in memoria dell’oggetto (cf. vtable)
  • Con le interfacce non ci sono questi problemi, risultato:
    • è molto più semplice prendere una classe esistente e renderla compatibile con una interfaccia I, piuttosto che renderla una specializzazione di una classe C

Riassunto polimorfismo inclusivo

Polimorfismo

  • Fornisce sopratipi che raccolgono classi uniformi tra loro
  • Usabili da funzionalità/contesti ad alta riusabilità
  • Utile per costruire collezioni omogenee di oggetti

Polimorfismo con le interfacce

  • Solo relativo ad un contratto
  • Facilità nel far aderire al contratto classi esistenti
  • Spesso vi è la tendenza a creare un alto numero di interfacce (cf. interface segregation principle)

Polimorfismo con le classi

  • Relativo a contratto e comportamento
  • In genere ci si aderisce per costruzione dall’inizio
  • Vincolato dall’ereditarietà singola

Come simulare ereditarietà multipla?

Come simulare ereditarietà multipla?

Definizioni

interface Counter { ... }
interface MultiCounter extends Counter { ... }
interface BiCounter extends Counter { ... }
interface BiAndMultiCounter extends MultiCounter, BiCounter { ... }
class CounterImpl implements Counter { ... }
class MultiCounterImpl extends CounterImpl implements MultiCounter{ ... }
class BiCounterImpl extends CounterImpl implements BiCounter { ... }
class BiAndMultiCounterImpl extends BiCounterImpl implements BiAndMultiCounter { ... }

Implementazione di BiAndMultiCounterImpl

  • si estende da BiCounterImpl, si delega via composizione ad un oggetto di MultiCounterImpl

Complessivamente

  • si ha completa e corretta sostituibilità tramite le interfacce
  • si ha ottimo riuso delle implementazioni
  • $\Rightarrow$ si esplori la possibilità di usare solo delegazione, non ereditarietà

Tipi a run-time

Everything is an Object

Perché avere una radice comune per tutte le classi?

  • Consente di fattorizzare lì il comportamento comune ad ogni oggetto
  • Consente la costruzione di funzionalità che lavorano su qualunque oggetto

Esempi di applicazione:

  • Container polimorfici, ad esempio via array di tipo Object[]
    • permette di costruire un elenco di oggetti di natura anche diversa
    • new Object[]{ new SimpleLamp(), new Integer(10) }
  • Definizione di metodi con numero variabile di argomenti
    • argomenti codificati come Object[]

Uso di Object[]

import java.util.Arrays;

/* Tutti gli oggetti possono formare un elenco Object[] */
public class AObject {
	public static void main(String[] s) {
		final Object[] os = new Object[5];
		os[0] = new Object();
		os[1] = "stringa";
		os[2] = Integer.valueOf(10);
		os[3] = new int[] { 10, 20, 30 };
		os[4] = new java.util.Date();
		printAll(os);
		System.out.println(Arrays.toString(os));
		System.out.println(Arrays.deepToString(os));
	}
	
	public static void printAll(final Object[] array) {
		for (final Object o : array) {
			System.out.println("Oggetto:" + o.toString());
		}
	}
}

Tipo statico e tipo a run-time

Una dualità introdotta dal subtyping (polimorfismo inclusivo)

  • Tipo statico: il tipo di dato di una variabile dichiarata
  • Tipo run-time: il tipo di dato del valore(/oggetto) effettivamente presente (potrebbe essere un sottotipo di quello statico)
    • in questo caso le chiamate di metodo sono fatte per late-binding

Esempio nel codice di printAll(), dentro al for

	public static void printAll(final Object[] array) {
		for (final Object o : array) {
			System.out.println("Oggetto:" + o.toString());
		}
	}
  • Tipo statico di o è Object
  • Tipo run-time di o varia di volta in volta: Object, String, Integer, …

Ispezione tipo a run-time

  • In alcuni casi è necessario ispezionare il tipo a run-time
  • Lo si fa con l’operatore instanceof

instanceof e conversione di tipo

/* Everything is an Object, ma quale?? */
public class AObject2 {
	public static void main(String[] s) {
		final Object[] os = new Object[5];
		os[0] = new Object();
		os[1] = Integer.valueOf(10);
		os[2] = Integer.valueOf(20);
		os[3] = new int[] { 10, 20, 30 };
		os[4] = Integer.valueOf(30);
		printAllAndSum(os);
	}

	/* Voglio anche stampare la somma degli Integer presenti */
	public static void printAllAndSum(final Object[] array) {
		int sum = 0;
		for (final Object o : array) {
			System.out.println("Oggetto:" + o.toString());
			if (o instanceof Integer) { // test a runtime
				final Integer i = (Integer) o; // (down)cast
				sum = sum + i.intValue();
			}
		}
		System.out.println("Somme degli Integer: " + sum);
	}
}

instanceof e conversione di tipo

Ispezione ed uso della sottoclasse effettiva

Data una variabile (o espressione) del tipo statico C può essere necessario capire se sia della sottoclasse D, in tal caso, su tale oggetto si vuole richiamare un metodo specifico della classe D.

Coppia instanceof + conversione

  • con l’operatore instanceof si verifica se effettivamente sia di tipo D
  • con la conversione si deposita il riferimento in una espressione con tipo statico D
  • a questo punto si può invocare il metodo
C x = new java.util.Random().nextInt() > 0 ? new D() : null;
if(x instanceof D) { D d = (D) x; /* ... */ }

Solo due tipi di conversione fra classi consentite

  • Upcast: da sottoclasse a superclasse / classe antenata (spesso automatica)
  • Downcast: da superclasse a sottoclasse / classe discendente (potrebbe fallire)

Errori possibili connessi alle conversioni

Errori semantici (a tempo di compilazione, quindi innocui)

  • Tentativo di conversione che non sia né upcast né downcast
  • Chiamata ad un metodo non definito dalla classe (statica) del receiver

Errori d’esecuzione (molto pericolosi, evitabili con l’instanceof)

  • Downcast verso una classe incompatibile col tipo dinamico, riportato come ClassCastException
/* Showing ClassCastException */
public class ShowCCE {
	public static void main(String[] as) {
		Object o = Integer.valueOf(10);
		Integer i = (Integer) o; // OK
		String s = (String) o; // ClassCastException
		// int i = o.intValue(); // No, intValue() non def.
		// String s = (String)i; // No, tipi inconvertibili
	}
}

instanceof, conversioni e Person

public class UsePerson2 {

	public static void main(String[] args) {

		final Person[] people = new Person[] {
				new Student("Marco Rossi", 334612, 2013),
				new Student("Gino Bianchi", 352001, 2013),
				new Student("Carlo Verdi", 354100, 2012),
				new Teacher("Mirko Viroli", 34121, 
						    new String[] { "PO", "FINF-A", "ISAC" }) };

		for (final Person p : people) {
			if (p instanceof Student) {
				final Student s = (Student) p; // Qui non fallisce
				System.out.println(s.getName() + " " + 
				                   s.getMatriculationYear());
			}
		}
	}
}

Classi astratte

Motivazioni

Fra interfacce e classi

  • Le interfacce descrivono solo un contratto
  • Le classi definiscono un comportamento completo
  • …c’è margine per costrutti intermedi?

Classi astratte

  • Le classi astratte sono usate per descrivere classi dal comportamento parziale (ossia, in cui alcuni metodi sono dicharati ma non implementati)
  • Tali classi non sono istanziabili (l’operatore new non può essere usato)
  • Possono essere estese e ivi completate, da cui la generazione di oggetti

Tipica applicazione: pattern Template Method

Serve a dichiare uno schema di strategia con un metodo “template” (spesso final) che definisce un comportamento comune, basato su metodi astratti da concretizzare in sottoclassi

Classi astratte

Una classe astratta:

  • è dichiarata tale: abstract class C ... { ... }
  • non è istanziabile (in quanto astratta, ovvero non pienamente specificata)
  • può opzionalmente dichiarare metodi astratti:
    • hanno forma ad esempio: abstract int m(int a, String s);
    • ossia senza body, come nelle dichiarazioni delle interfacce

Altri aspetti

  • può definire campi, costruttori, metodi, concreti e non
    • …deve definire con cura il loro livello d’accesso
  • può estendere da una classe astratta o non astratta
  • può implementare interfacce, senza essere tenuta ad ottemperarne il contratto
    • i metodi dell’interfaccia implementata, se non implementati, sono astratti
  • chi estende una classe astratta può essere non-astratto solo se concretizza/implementa tutti i metodi astratti

Esempio: LimitedLamp come classe astratta

Obiettivo

  • Vogliamo progettare una estensione di SimpleLamp col concetto di esaurimento
  • La strategia con la quale gestire tale esaurimento può essere varia
  • Ma bisogna far sì che qualunque strategia si specifichi, sia garantito che:
    • la lampadina si accenda solo se non esaurita
    • in caso di effettiva accensione sia possibile tenerne traccia ai fini della strategia

Soluzione

  • Un uso accurato di abstract, final, e protected
  • Daremo tre possibili specializzazioni per una LimitedLamp
    1. che non si esaurisce mai
    2. che si esaurisce all’n-esima accensione
    3. che si esaurisce dopo un certo tempo dalla prima accensione

UML complessivo

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 abstract class LimitedLamp extends SimpleLamp {

	public LimitedLamp() {
		super();
	}

	/* Questo metodo è finale: regola la coerenza con okSwitch() e isOver() */
	public final void switchOn() { // TEMPLATE METHOD
		if (!this.isSwitchedOn() && !this.isOver()) {
			super.switchOn();
			this.okSwitch();
		}
	}

	// Cosa facciamo se abbiamo effettivamente acceso? Dipende dalla strategia
	protected abstract void okSwitch();

	/* Strategia per riconoscere se la lamp è esaurita */
	public abstract boolean isOver();

	public String toString() {
		return "Over: " + this.isOver() + ", switchedOn: "	+ this.isSwitchedOn();
	}
}

UnlimitedLamp

/* Non si esaurisce mai */
public class UnlimitedLamp extends LimitedLamp {

	/* Nessuna informazione extra da tenere */
	public UnlimitedLamp() {
		super();
	}

	/* Allo switchOn.. non faccio nulla */
	protected void okSwitch() {
	}

	/* Non è mai esaurita */
	public boolean isOver() {
		return false;
	}
}

CountdownLamp

/* Si esaurisce all'n-esima accensione */
public class CountdownLamp extends LimitedLamp {

	/* Quanti switch mancano */
	private int countdown;

	public CountdownLamp(final int countdown) {
		super();
		this.countdown = countdown;
	}

	/* Allo switchOn.. decremento il count */
	protected void okSwitch() {
		this.countdown--;
	}

	/* Finito il count.. lamp esaurita */
	public boolean isOver() {
		return this.countdown == 0;
	}
}

ExpirationTimeLamp

import java.util.Date;

/* Si esaurisce dopo un certo tempo (reale) dopo la prima accensione */
public class ExpirationTimeLamp extends LimitedLamp {
	/* Tengo il momento dell'accensione e la durata */
	private Date firstSwitchDate;
	private long duration;

	public ExpirationTimeLamp(final long duration) {
		super();
		this.duration = duration;
		this.firstSwitchDate = null;
	}

	/* Alla prima accensione, registro la data */
	protected void okSwitch() {
		if (this.firstSwitchDate == null) {
			this.firstSwitchDate = new java.util.Date();
		}
	}

	/* Esaurita se è passato troppo tempo */
	public boolean isOver() {
		return this.firstSwitchDate != null &&
		   (new Date().getTime() - this.firstSwitchDate.getTime() 
				   >= this.duration);
	}
}

UseLamps

public class UseLamps {
    // clausola throws Exception qui sotto necessaria!!
    public static void main(String[] s) throws Exception {
        LimitedLamp lamp = new UnlimitedLamp();
        lamp.switchOn();
        System.out.println("ul| " + lamp);
        for (int i = 0; i < 1000; i++) {
            lamp.switchOff();
            lamp.switchOn();
        }
        System.out.println("ul| " + lamp); // non si è esaurita

        lamp = new CountdownLamp(5);
        for (int i = 0; i < 4; i++) {
            lamp.switchOn();
            lamp.switchOff();
        }
        System.out.println("cl| " + lamp);
        lamp.switchOn();
        System.out.println("cl| " + lamp); // al quinto switch si esaurisce

        lamp = new ExpirationTimeLamp(1000); // 1 sec
        lamp.switchOn();
        System.out.println("el| " + lamp);
        Thread.sleep(3000); // attendo 1.1 secs
        System.out.println("el| " + lamp); // dopo 1.1 secs si è esaurita
        lamp.switchOff();
        lamp.switchOn();
        System.out.println("el| " + lamp);
    }
}

Classi astratte vs interfacce

  • Due versioni quasi equivalenti
  • Unica differenza: ereditarietà singola per classi, ereditarietà multipla per le interfacce
/* Versione interfaccia */
public interface Counter {
    void increment();
    int getValue();
} 

/* Versione classe astratta */
public abstract class Counter {
    public abstract void increment();
    public abstract int getValue();
} 

Approfondimento: classi astratte vs. interfacce con metodi di default

Interfacce con metodi di default …

public interface I4 extends I1, I2, I3 {
    void doSomething(String s);
    // da Java 8
    double E = 2.718282; // implicitamente public, static, final
    default void doSomethingTwice(String s) { doSomething(s); doSomething(s); }
    static double PI() { return Math.PI; }
}

… sembrano piuttosto simili alle classi astratte, in quanto possono fornire, in aggiunta a un contratto, alcune implementazioni di default

Tuttavia, ci sono differenze cruciali:

  • le classi astratte possono definire variabili d’istanza (stato)
  • le classi astratte possono definire costruttori
  • le classi astratte possono definire membri con visibilità diverse
  • le classi astratte possono fare overriding di metodi da Object
  • i default method non possono essere final

Wrap-up su ereditarietà

Il caso più generale:

class C extends D implements I, J, K, L { ... }

Cosa deve/può fare la classe C

  • deve implementare tutti i metodi dichiarati in I,J,K,L e super-interfacce
  • può fare overriding dei metodi (non finali) definiti in D e superclassi

Classe astratta:

abstract class CA extends D implements I, J, K, L { ... }

Cosa deve/può fare la classe CA

  • non è tenuta a implementare alcun metodo
  • può implementare qualche metodo per definire un comportamento parziale

Autoboxing dei tipi primitivi, e argomenti variabili

Autoboxing dei tipi primitivi

Già conosciamo i Wrapper dei tipi primitivi

Integer i = new Integer(10); // Deprecated in Java 17 (for removal)
i = Integer.valueOf(10);  // recommended
Double  d = new Double(10.5); // Deprecated in Java 17 (for removal)
d = Double.valueOf(10.5); // recommended

..ossia, ogni valore primitivo può essere “impacchettato” (“boxed”) in un oggetto

Autoboxing

  • Un meccanismo di Java per supportare l’equivalenza fra tipi primitivi e loro Wrapper
  • Due meccanismi:
    • Se si trova un primitivo dove ci si attende un oggetto, se ne fa il boxing
    • Se si trova un wrapper dove ci si attende un primitivo, si fa il de-boxing

Risultato

  • Si simula meglio l’idea “Everything is an Object”
  • Anche i tipi primitivi sono usabili ad esempio con Object[]

Uso dell’autoboxing

/* Showcase dell'autoboxing */
public class Boxing {
	public static void main(String[] s) {
		final Object[] os = new Object[5];
		os[0] = new Object();
		os[1] = 5; // equivale a os[1]=new Integer(5);
		os[2] = 10; // equivale a os[2]=new Integer(10);
		os[3] = true; // equivale a os[3]=new Boolean(true);
		os[4] = 20.5; // equivale a os[4]=new Double(20.5);
		final Integer[] is = new Integer[] { 10, 20, 30, 40 };
		final int i = is[0] + is[1] + is[2] + is[3];
		// equivale a: is[0].intValue()+ is[1].intValue()+..
		// non funzionerebbe se 'is' avesse tipo Object[]..
		System.out.println("Somma: " + i); // 100
	}
}

Variable arguments

A volte è utile che i metodi abbiano un numero variabile di argomenti

int i = sum(10, 20, 30, 40, 50, 60, 70);
printAll(10, 20, 3.5, new Object());
  • prima di Java 5 si simulava passando un unico array

Variable arguments

  • L’ultimo (o unico) argomento di un metodo può essere del tipo “Type... argname
void m(int a, float b, Object... argname) { ... }
  • Nel body del metodo, argname è trattato come un Type[]
  • Chi chiama il metodo, invece che passare un array, passa una lista di argomenti di tipo Type
  • Funziona automaticamente con polimorfismo, autoboxing, instanceof, …

Uso dei variable arguments

public class VarArgs {
	// somma un numero variabile di Integer
	public static int sum(final Integer... args) {
		int sum = 0;
		for (int i : args) {
			sum = sum + i;
		}
		return sum;
	}

	// stampa il contenuto degli argomenti, se meno di 10
	public static void printAll(final String start, final Object... args) {
		System.out.println(start);
		if (args.length < 10) {
			for (final Object o : args) {
				System.out.println(o);
			}
		}
	}

	public static void main(String[] s) {
		System.out.println(sum(10, 20, 30, 40, 50, 60, 70, 80));
		printAll("inizio", 1, 2, 3.2, true, new int[] { 10 }, new Object());
		
		System.out.format("%d %d\n", 10, 20); // C-like printf
	}
}

Alcuni pattern basati sulle classe astratte

Pattern Template Method: comportamentale, su classi

Intento/motivazione

Definisce lo scheletro (template) di un algoritmo (o comportamento), lasciando l’indicazione di alcuni suoi aspetti alle sottoclassi.

Esempi

  • Definizione della logica di accensione (switchOn) di una lampadina (Lamp)
  • Un comparatore può fornire metodi template per capire se un oggetto è minore/maggiore/uguale di un altro, sulla base di un metodo astratto int compareTo(T a, T b)
  • In un input stream (InputStream), i vari metodi di lettura sono dei Template Method: dipendono dall’implementazione del solo concetto di lettura di un int

Soluzione

  • L’algoritmo è realizzato attraverso un metodo template che realizza un algoritmo chiamando metodi astratti/da specializzare quando servono gli aspetti non noti a priori
  • Una sottoclasse fornisce l’implementazione dei metodi astratti
AbstractClass
TemplateMethod()
PrimitiveOp1()
PrimitiveOp2()
ConcreteClass
PrimitiveOp1()
PrimitiveOp2()
abstract class AbstractClass {
    public void TemplateMethod() {
        // ...
        PrimitiveOp1();
        // ...
        PrimitiveOp2();
        // ...
    }
    public abstract void PrimitiveOp1();
    public abstract void PrimitiveOp2();
}

class ConcreteClass extends AbstractClass {
    public void PrimitiveOp1() { /* ... */ }
    public void PrimitiveOp2() { /* ... */ }
}

Template Method: esempio BankAccount

public abstract class BankAccount {
	private int amount;
	
	public BankAccount(int amount){
		this.amount = amount;
	}
	
	public abstract int operationFee(); // costo bancario operazione
	
	public int getAmount(){
		return this.amount;
	}
	
	public void withdraw(int n){	// template method
		this.amount = this.amount - n - this.operationFee();
	}

	public static void main(String[] args){
		final BankAccount b = new BankAccountWithConstantFee(100);
		b.withdraw(20);
		System.out.println(b.getAmount()); // 79
	}
}

class BankAccountWithConstantFee extends BankAccount {
	public BankAccountWithConstantFee(int amount) {
		super(amount);
	}

	public int operationFee(){ return 1; }	
}

Preview del prossimo laboratorio

Obiettivi

Familiarizzare con:

  • Estensione delle classi e corrispondente polimorfismo
  • Classi astratte
  • Tipi a run-time e boxing