Sei sulla pagina 1di 33

Una classe può essere interpretata come un tipo definito }

dall'utente che specifica anche le operazioni che vi si ...


Cheatsheet di Ingegneria possono effettuare. Questo tipo può essere usato per private void giornoDopo {
giorno++;
dichiarare altre variabili, ad esempio: Date d;.
del Software A.A. if (giorno > numeroGiorni()) {
giorno = 1;
Gli attributi della classe Data sono: giorno, mese e
2022/2023 anno. I metodi invece sono: ottieniGiorno, }
mese++;

ottieniMese, ottieniAnno. if (mese > 12) {


mese = 1;
Indice anno++;
All'interno dell'implementazione di ciascun metodo (non }
statico) possiamo utilizzare la keyword this per fare }
Java riferimento all'oggetto su cui il metodo è stato invocato. ...
JML }
Principio di sostituzione
Programmazione concorrente In Java è possibile invocare i metodi tramite la dot
Programmazione funzionale notation, ad esempio:
Allora se invochiamo giornoDopo:
Design pattern
Testing Data d = new Data();
UML int x; d.giornoDopo();

x = d.ottieniGiorno(); System.out.println(d.ottieniGiorno() + "/" +


Java d.ottieniMese() + "/" d.ottieniAnno());

Invocare un metodo su un oggetto può cambiarne lo


1/12/2011
Le classi stato. Supponiamo che la classe Data si trovi in questo
stato iniziale:
Vediamo un esempio di classe che rappresenta una
data. System.out.println(d.ottieniGiorno() + "/" +
d.ottieniMese() + "/" d.ottieniAnno());
public class Data {
private int giorno; 30/11/2011
private int mese;
private int anno;
e che siano stati definiti due nuovi metodi:
// Restituisce il giorno
public int ottieniGiorno() {
return giorno; public Class Data {
} ...
private int numeroGiorni() {
// Restituisce il mese switch (mese) {
public int ottieniMese() { case 1:
return mese; return 31;
} case 2:
return 28;
// Restituisce l'anno ...
public int ottieniAnno() { case 12:
return anno; return 31;
} default:
} return -1; 1
}
Operatori booleani aggiuntivi condizione JML definita sull'oggetto t di tipo T. Sia b :
T -> N una funzione (in senso matematico)
JML Siano a e b due condizioni JML, allora le seguenti sono implementata attraverso Java. Ad esempio: T =
valide condizioni JML: String, N = int, a(t) = !t.isEmpty() &&
JML (Java Modeling Language) è un linguaggio di t.charAt(0) == 'e', b(t) = t.length(). Allora
specifica formale. a ==> b: a implica b possiamo applicare i seguenti operatori di aggragazione:
a <== b: b implica a
a <==> b: a se e solo se b (\sum T t; a(t); b(t)): restituisce la somma
Sintassi a <=!=> b: !(a <==> b) b(t_1) + b(t_2) + ... per tutti i t_i tali che
a(t_i)
Lo schema generale di una specifica in JML è il (\product T t; a(t); b(t)): restituisce il
\old
seguente: prodotto b(t_1)*b(t_2)*... per tutti i t_i tali che
a(t_i)
L'operatore \old(<espressione>) prende in input una
// OVERVIEW: descrizione della specifica (\max T t; a(t); b(t)): restituisce il massimo tra
espressione che può essere una condizione JML oppure
//@ assignable <parametri che possono essere b(t_1), b(t_2), ... per tutti i t_i tali che a(t_i)
una espressione Java che non ha "effetti collaterali" (non
//@ modificati dal metodo>; (\min T t; a(t); b(t)): restituisce il minimo tra
si può utilizzare ++, =, metodi non puri, etc.) e restituisce
//@ requires <precondizione> b(t_1), b(t_2), ... per tutti i t_i tali che a(t_i)
//@ ...; il risultato che si ottiene valutando tale espressione nel
//@ ensures <postcondizione> momento della chiamata del metodo che stiamo
//@ ...; specificando. Notare che, valutando una espressione \result
//@ signals (<tipo eccezione 1> e) Java (tra quelle ammissibili), \old in generale non
//@ <postcondizione nel caso di eccezione 1> restituisce valori booleani. Non ha senso utilizzare \old Se stiamo scrivendo la specifica di un metodo NON
//@ ...; nel blocco requires dato che lì stiamo specificando ciò void, possiamo fare riferimento al valore restituito con la
//@ signals (<tipo eccezione 2> e) che deve essere vero prima che il metodo venga
//@ <postcondizione nel caso di eccezione 2>
keyword \result. Ha senso utilizzare \result solo
eseguito. nell'ensures (in requires il metodo non è ancora stato
//@ ...;
eseguito, signals specifica la postcondizione qualora
Quantificatori venga restituita un'eccezione).
Ogni riga della specifica (dopo l'OVERVIEW) inizia con
//@ . Alla fine di ciascun "blocco" (assignable, Sia T un generico tipo di Java ed a(t), b(t) due assignable
requires, ...) è necessario aggiungere ;. Omettere il condizioni JML definite su un oggetto t di tipo T (ad
blocco requires equivale a scrivere //@ requires esempio se T = int, a(t) = t > 1729), allora le
Il blocco assignable nella specifica permette di
true;. Omettere il blocco ensures equivale a scrivere seguenti sono valide condizioni JML:
esplicitare i parametri (di tipo riferimento) che vengono
//@ ensures true;. Nella condizione del blocco
modificati dal metodo. Omettere assignable significa
signals possiamo fare riferimento all'oggetto eccezione (\forall T t; a(t); b(t)): per ogni oggetto del che tutti i parametri possono essere modificati.
e (il nome è arbitrario). tipo T se a(t) allora b(t)
(\exists T t; a(t); b(t)): esiste un oggetto del
Se un metodo non modifica nessuno dei parametri
Condizioni tipo T tale che a(t) e b(t) possiamo usare la keyword \nothing: //@ assignable
(\num_of T t; a(t); b(t)): restituisce il numero
\nothing;.
Le condizioni in JML (precondizioni e postcondizioni) di oggetti t tale che a(t) e b(t)
sono espressioni booleane di Java (con alcuni operatori Consideriamo il seguente metodo:
e quantificatori aggiuntivi) che non alterano lo stato degli Nel caso T non sia un tipo primitivo assumiamo t !=
oggetti che descrivono: non si possono usare null.
assegnamenti (=), incrementare le variabili intere (++), public static void metodo(S s, T t, U u) {
etc. Gli unici metodi che si possono invocare sono quelli ...
Operatori di aggragazione }
che non modificano lo stato degli oggetti a cui
appartengono o dei parametri che gli vengono passati (si
dicono metodi puri). Sia T un generico tipo di Java ed N uno dei tipi numerici 2
primitivi di Java (int, float, ...). Sia a(t) una
La postcondizione esplicita una serie di "effetti" Un metodo di un ADT si dice puro se:
Per specificare che s e t vengono modificati, ma non u, garantiti al termine dell'esecuzione del metodo che
possiamo aggiungere nella specifica: //@ assignable stiamo specificando, assumendo che la precondizione vale //@ assignable \nothing;
s, t;
fosse verificata. In JML è possibile definire gli unici metodi che invoca sono anche essi puri
separatamente la postcondizione che vale nel caso in non modifica nessun attributo (pubblico o privato)
cui il metodo esegua regolarmente dalle postcondizioni dell'ADT
Supponiamo di voler specificare che un array a[] venga che valgono nel caso in cui durante l'esecuzione del
modificato da un metodo di cui stiamo scrivendo la metodo si verifichino delle eccezioni. La prima si
specifica: esplicita attraverso il blocco ensures. La seconda I metodi puri si indicano in JML con la keyword /* @
attraverso signals (<tipo eccezione> e). Nel caso pure @ */. Ad esempio, supponiamo che il metodo
//@ assignable a[*];: ogni elemento di a può in cui la postcondizione sia true il metodo non dimensione restituisca la cardinalità di un ADT che
essere modificato garantisce nessun "effetto" al termine della sua rappresenta una collezione di oggetti. dimensione è
//@ assignable a[5];: solo l'elemento a[5] può esecuzione, quindi può "fare ciò che vuole": non c'è chiaramente un metodo puro, in quanto non modifica lo
essere modificato motivo di omettere la postcondizione. stato della collezione, per esplicitarlo in JML possiamo
//@ assignable a[1..8];: gli elementi a[1], usare la sintassi: public int /* @ pure @ */
a[2], ..., a[8] possono essere modificati dimensione();.
Astrazione procedurale
Anche un costruttore può essere puro se si limita ad
Commenti
Si dice astrazione procedurale la specifica di inizializzare gli attributi dell'oggetto senza apportare altre
un'operazione (anche complessa) definita su un dominio modifiche.
A volte è utile inserire all'interno delle condizioni JML di dati generici (parametri). In Java coincide con la
delle specifiche informali. Per farlo si utilizzano i specifica (ad esempio attraverso la notazione JML) di un
commenti, esprimibili attraverso la seguente sintassi: (* I metodi puri si dicono anche observer in quanto
metodo statico. Infatti un metodo statico rappresenta permettono di "osservare" lo stato di un ADT in un
<commento> *). Ciascun commento al momento della proprio un'operazione "priva di stato": il suo determinato momento.
valutazione della condizione è da intendersi con valore comportamento non dipende dai valori di attributi non
true. statici, ma è determinato unicamente dai parametri.
Classi pure
Semantica ADT: Abstract Data Type Una classe i cui metodi pubblici (e costruttori) sono tutti
puri si dice pura e si indica con la sintassi: public /*
Precondizione Si dice abstract data type (o ADT) un tipo di dato per @ pure @ */ class NomeClasse. Una classe pura è
cui tutti gli stati ammissibili e le operazioni che vi si una classe immutabile.
La precondizione è la condizione sulla base della quale possono effettuare (includendo il modo in cui
è definita la specifica: se la precondizione non dovesse quest'ultime permettono di passare da uno stato all'altro) Attenzione: quando specifichiamo che una classe è
essere rispettata allora non è detto che il metodo si sono descritti attraverso una specifica. La specifica di un pure NON è necessario che nella specifica si espliciti
comporti secondo la specifica (in generale non lo farà, ADT si compone di una sintassi e di una semantica. La che il suo stato non cambia (cioè che tutti gli observer
qualsiasi comportamento è ammesso). In JML è sintassi coincide con l'interfaccia del tipo di dato in restituiscono gli stessi risultati prima e dopo l'esecuzione
possibile esprimere una precondizione del metodo che questione (l'insieme di metodi pubblici che espone). Per del metodo). Questo risulterà utile in seguito per
stiamo specificando attraverso il blocco requires. Nel definire la semantica in maniera esaustiva occorre alleggerire la specifica in JML dei metodi.
caso in cui la precondizione sia true il metodo "non ha utilizzare un linguaggio di specifica formale come JML. A
una precondizione": la sua specifica è valida a differenza dell'astrazione procedurale, nello specificare
un ADT con JML, dobbiamo trattare anche le transizioni Specifica di un'operazione di un ADT in JML
prescindere dei parametri passati. Un metodo con
precondizione true si dice totale, altrimenti si dice di stato dell'oggetto dovute alle varie operazioni (cioè
dire ciò che cambia, come cambia e cosa invece rimane Valgono le regole sintattiche e semantiche già introdotte
parziale. per JML. La struttura della specifica rimane quella
invariato tra i valori che rappresentano lo stato
dell'oggetto a seguito dell'esecuzione di un certo usuale.
Postcondizione metodo).

3
Metodi puri
s.inserisci(x);
// Creator: }
Attenzione: quando specifichiamo un'operazione di un
// Dalla specifica s contiene tutti
ADT in JML, nella specifica possono comparire solo gli //@ ensures cardinalita() == 0; // e soli gli interi (x) in a
altri metodi (e attributi) pubblici e puri (dell'ADT), i public InsiemeDiInteri();
parametri formali dell'operazione (su cui possiamo return s;
invocare i rispettivi metodi pubblici e puri) e \result // Mutator: }
per fare riferimento al risultato restituito.
//@ ensures appartiene(x) &&
Esempio //@ cardinalita() == \old(cardinalita() + Categorie di operazioni
//@ (appartiene(x) ? 0 : 1)) &&
//@ (\forall int y; \old(appartiene(y));
Vediamo un esempio di ADT. //@ appartiene(y)); Possiamo classificare le operazioni (i metodi pubblici)
public void inserisci(int x); definite su un ADT nelle seguenti categorie:
Vogliamo descrivere attraverso la specifica un insieme
(mutabile) di interi su cui sono definite le seguenti //@ ensures !appartiene(x) && Creator: creano un nuovo oggetto del tipo definito
operazioni:
//@ cardinalita() == \old(cardinalita() - dall'ADT prendendo in input parametri che NON sono
//@ (appartiene(x) ? 1 : 0)) && altri oggetti del tipo definito dall'ADT
//@ (\forall int y; appartiene(y); Producer: creano un nuovo oggetto del tipo definito
costruzione: crea ed inizializza l'insieme come un //@ \old(appartiene(y))); dall'ADT prendendo in input un oggetto del tipo
insieme vuoto public void rimuovi(int x); definito dall'ADT
inserisci(x): aggiunge l'elemento x all'insieme } Mutator: modificano lo stato dell'oggetto
rimuovi(x): rimuove l'elemento x dall'insieme Observer: restituiscono un risultato di un tipo diverso
appartiene(x): restituisce vero se x appartiene da quello definito dall'ADT; solitamente non
all'insieme, falso altrimenti Osserviamo che NON tutti i metodi pubblici dell'oggetto
possono essere specificati formalmente (ovvero senza modificano l'oggetto e quindi sono dichiarati come
cardinalità: resituisce la cardinalità dell'insieme puri
scegli: restituisce uno tra gli elementi dell'insieme dover ricorrere ai commenti) in JML. Questo in
particolare è vero per alcuni degli observer, nel caso
d'esempio: appartiene. Per poter specificare Un tipo mutabile "adeguato" dovrebbe disporre di
Alcuni valori ammissibili per l'insieme sono: {1, 10, 35}, creator, observer e mutator.
{7}, ... formalmente questi metodi sarà necessario dichiarare un
rappresentante, cioè un oggetto che realizza lo stato
concreto dell'ADT, (spiegato in seguito) e fare riferimento Un tipo immutabile "adeguato" dovrebbe disporre di
public class InsiemeDiInteri { all'implementazione. crator, observer e producer.
// OVERVIEW: Insieme di interi illimitato
// e mutabile Un ADT deve consentire di utilizzare gli oggetti che Proprietà astratte (evolutive e invarianti)
descrive, senza conoscerne l'implementazione. Vediamo
// Observer:
come implementare, conoscendo solo la specifica, un
Si dicono proprietà astratte di un ADT tutte le proprietà
//@ ensures (* \result equivale a metodo che crea un InsiemeDiInteri a partire da un
di cui esso gode, che sono deducibili dalla specifica dei
//@ "x appartiene all'insieme" *); array di interi: suoi metodi pubblici. Si distinguono in:
public /* @ pure @ */
boolean appartiene(int x);
public static InsiemeDiInteri daArray(int[] a) Proprietà evolutive: specificano la relazione tra uno
throws NullPointerException { stato osservabile ed il successivo (ovvero specificano
//@ ensures \result ==
//@ (\num_of int x; ; appartiene(x)) come l'oggetto evolve). Un esempio di proprietà
if (a == null) { evolutiva è l'immutabilità, che può essere espressa
public /* @ pure @ */ int cardinalita();
throw new NullPointerException();
attraverso il concetto di stato corrente e successivo
}
//@ ensures appartiene(\result) && come segue: "Per ogni stato corrente dell'oggetto,
//@ cardinalita() > 0;
InsiemeDiInteri s = new InsiemeDiInteri(); per ogni operazione, detto stato successivo lo stato
//@ signals (EccezioneInsiemeVuoto e) in cui si trova l'oggetto dopo aver eseguito
// Dalla specifica s è vuoto
//@ cardinalita() == 0; l'operazione, lo stato corrente coincide con lo stato
public /* @ pure @ */ int scegli()
for (int x : a) { successivo" 4
throws EccezioneInsiemeVuoto;
un insieme di attributi privati. Ricordiamo che l'utente }
deve poter utilizzare l'ADT conoscendo esclusivamente return -1;
Properità invarianti: si tratta di proprietà che sono
la sua specifica, quindi non è necessario (anzi vedremo }
valide per qualunque stato ammissibile dell'oggetto. }
Nell'esempio InsiemeDiInteri, una proprietà essere una pratica scorretta) esporre il rappresentante
invariante è: cardinalita() >= 0. dello stato concreto (magari dichiarandolo pubblico).
Funzione di astrazione
Le proprietà evolutive NON sono rappresentabili Nell'esempio InsiemeDiInteri un rappresentante
direttamente in JML: non esiste una sintassi per molto semplice, ma efficace, è un ArrayList<Integer>
Si dice funzione di astrazione (o AF) una funzione (in
dichiararle esplicitamente, ma seguono dalle specifiche che contiene gli elementi dell'insieme. Vediamo una
senso matematico) che associa ad ogni stato concreto
dei singoli metodi. possibile implementazione:
ammissibile, rappresentato dal rep, lo stato astratto
(dell'ADT) che vi corrisponde. Nell'esempio
Al contrario, per rappresentare in JML una proprietà public class InsiemeDiInteri { InsiemeDiInteri lo stato concreto è un
invariante, anche detta invariante pubblico, si usa la private ArrayList<Integer> rep; ArrayList<Integer> come ad esempio [4, 1, 2,
seguente sintassi: 3]. L'AF associa [4, 1, 2, 3] all'insieme {1, 2, 3,
public boolean appartiene(int x) { 4} che è proprio lo stato astratto dell'ADT
return trova(x) != -1;
public class ClasseADT { corrispondente a tale ArrayList. Le funzioni di
}
//@ public invariant <condizione>; astrazione possono risultare più o meno complicate e
public int cardinalita() { dipendono dall'implementazione scelta. Solitamente
... return rep.size(); sono NON iniettive: più stati concreti corrispondono allo
} } stesso stato astratto. Nell'esempio [4, 1, 2, 3] e [1,
2, 3, 4] corrispondono entrambi all'insieme {1, 2,
public int scegli() throws 3, 4}.
Le proprietà astratte permettono agli utenti dell'ADT di EccezioneInsiemeVuoto {
fare assunzioni che ne semplificano l'utilizzo. if (rep.size() == 0) {
throw new EccezioneInsiemeVuoto();
Come dichiarare l'AF in JML
Dimostrare la validità di un invariante pubblico }
return rep.get(0); In JML è possibile dichiarare (NON implementare) la
} funzione di astrazione attraverso la seguente sintassi:
Dichiarare un invariante pubblico non è sufficiente per
garantirne la validità. Dobbiamo assicurarci, attraverso la public InsiemeDiInteri() {
specifica, che l'invariante sia valido per tutti gli stati rep = new ArrayList<>(); public class ClasseADT {
"iniziali" delle istanze dell'ADT: ovvero gli oggetti ottenuti } ...
invocando i creator. Successivamente è necessario // AF:
verificare che, a partire da un generico stato ammissibile public void inserisci(int x) { //@ private invariant
dell'oggetto, applicando una qualsiasi delle operazioni if (appartiene(x)) { return; } //@ <condizione>;
definite e assumendo vere le rispettive precondizioni, la rep.add(x); ...
} }
proprietà sia ancora valida. In particolare uno stato si
considera "ammissibile" se è raggiungibile applicando un
nunmero finito di volte le operazioni dell'ADT ad uno public void rimuovi(int x) {
int iX = trova(x); L'AF viene dichiarata all'interno del blocco JML private
degli stati iniziali. Non è necessario dimostrare che gli invariant, che ci consente di accedere, oltre che agli
if (iX == -1) { return; }
observer soddisfino l'invariante, dato che non modificano rep.remove(iX); attributi e metodi pubblici della classe, anche a quelli
lo stato dell'oggetto. } privati. Nella condizione JML siamo interessati ad
esplicitare la relazione (logica) che sussiste tra lo stato
Rappresentante // trova è un metodo di ausilio che cerca concreto e lo stato astratto corrispondente dell'ADT. Cioè
// x in rep e, se lo trova non vogliamo spiegare cosa la funzione fa (la sua
// restituisce l'indice corrispondente, implementazione) ma solo le proprietà che devono
Per implementare un ADT è necessario dichiarare un // altrimenti -1
rappresentante (o rep), ovvero una struttura dati in valere perchè uno stato concreto ed uno astratto siano
private /* @ helper @ */ int trova(int x) {
grado di rappresentare, attraverso i valori che assume, corrispondenti. 5
for (int i = 0; i < rep.size(); i++) {
lo stato concreto dell'ADT. In Java questo si traduce in if (rep.get(i) == x) { return i; }
@Override Analogamente si può definire l'RI come la proprietà che
public String toString() { permette di discernere tra i valori del rep che
Nell'esempio InsiemeDiInteri con un'ArrayList
return rep.stream().sorted() appartengono al dominio della funzione di astrazione (gli
come rep, a prescindere da come l'AF venga realizzata, .map(x -> x.toString())
vale che "un elemento x appartiene all'insieme s (stato stati concreti ammissibili, per cui esiste uno stato
.collect(Collectors
astratto) sse esiste un indice i compreso tra 0 e astratto corrispondente) da quelli che non vi
.joining(", ", "{", "}"));
rep.size() tale che rep.get(i) == x (stato }
appartengono (gli stati concreti inammissibili, per cui non
} esiste uno stato astratto corrispondente e per cui, di
concreto)". Lo stato concreto è rappresentato dal rep, a
conseguenza, la funzione di astrazione risulta indefinita).
cui possiamo accedere dato che è costituito da un
Ovviamente l'RI dipende fortemente dalle scelte
insieme di attributi privati (ricordiamo che siamo nel
Osserviamo che l'ordinamento della lista di interi prima implementative.
private invariant). Lo stato astratto invece non è
realmente memorizzato da nessuna parte, è possibile della stampa fa in modo che gli stati astratti
accedervi solo attraverso gli observer. Quindi nella corrispondenti a [1, 2, 3, 4] e [4, 3, 2, 1], che Per dichiarare l'invariante di rappresentazione in JML si
condizione JML compariranno gli attributi privati che sono uguali, siano rappresentati da un'unica stringa " usa la seguente sintassi:
costituiscono il rep ed gli observer che permettono di {1, 2, 3, 4}".
osservare lo stato astratto dell'ADT, legati tra loro public class ClasseADT {
attraverso delle formule logiche che risultano vere solo Invariante di rappresentazione ...
quando i valori assunti dagli attributi privati // RI:
corrispondono con lo stato astratto osservato. NON tutti gli stati concreti assunti dal rep sono //@ private invariant
Dichiariamo l'AF nell'esempio InsiemeDiInteri: rappresentanti ammissibili di uno stato astratto. //@ <condizione-sul-rep>;
...
Nell'esempio InsiemeDiInteri risulta evidente che i
}
public class InsiemeDiInteri { seguenti valori per il rep NON costituiscano degli stati
... ammissibili:
// AF: Nella condizione in JML sul rep compariranno appunto
//@ private invariant null solo gli attributi (privati) che costituiscono il rep. La
//@ (\forall int x; ; condizione risulterà vera sse il rep rappresenta uno
[1, null, 3]
//@ appartiene(x) <==>
stato concreto ammissibile.
//@ (\exists int i; 0 <= i &&
//@ i < rep.size(); rep.get(i) == x)); Ci sono però altre assunzioni (implicite) che facciamo
... nell'implementazione di InsiemeDiInteri che rendono Dichiariamo l'RI di InsiemeDiInteri:
} inammissibili anche altri stati concreti che potrebbero
sembrare validi. Ad esempio, quando rimuoviamo un
public class InsiemeDiInteri {
elemento dall'insieme, rimuoviamo dal rep al più un ...
Notiamo che siamo finalmente riusciti a specificare
elemento (in particolare rimuoviamo l'elemento che vale // RI:
formalmente il metodo appartiene che, prima
x con indice minimo, se esiste). Quindi, perché //@ private invariant
dell'introduzione del rep, era semplicemente descritto
l'implementazione funzioni correttamente, è imperativo //@ rep != null && !rep.contains(null) &&
attraverso un commento in JML.
che nel rep non compaiano duplicati (altrimenti la //@ (\forall int i; 0 <= i &&
rimozione non rimuoverebbe tutti gli elementi uguali ad //@ i < rep.size() - 1;
Come implementare l'AF x). Dunque, ad esempio, anche [1, 1, 2] è uno stato //@ (\forall int j; i < j &&
concreto inammissibile. //@ j < rep.size();
Per implementare l'AF solitamente si ricorre ad una //@ !rep.get(i).equals(rep.get(j))));
...
rappresentazione testuale (tramite una stringa) degli L'invariante di rappresentazione (o RI) è una proprietà }
stati astratti, quindi si ridefinisce toString. Nell'esempio invariante, che quindi deve essere verificata in tutti gli
InsiemeDiInteri una possibile implementazione stati osservabili dell'istanza dell'ADT (questo punto in
dell'AF (attraverso i paradigmi della programmazione particolare sarà chiarito in Validità e conservazione
funzionale) è quella che segue: dell'RI), privata, cioè che fa riferimento esclusivamente
agli attributi privati della classe, in particolare a quelli che
public class InsiemeDiInteri { costituiscono il rep, che deve essere soddisfatta perchè
lo stato concreto rappresentato dal rep sia ammissibile. 6
...
Si parla di esposizione del rep quando si fornisce Per ovviare a questi problemi di solito si ricorre ai copy
Validità e conservazione dell'RI all'esterno un riferimento al rep dell'ADT. Si tratta di una constructor. Ad esempio, per evitare l'esposizione del
pessima pratica di programmazione perché consente rep in comeArrayList:
Gli stati osservabili, cioè quelli per cui si verifica la agli utilizzatori dell'ADT di violare l'invariante di
validità dell'RI, sono gli stati in cui si trova il rep quando rappresentazione, agendo direttamente sul riferimento al
public class InsiemeDiInteri {
rep di cui dispongono. Avviene principalmente in due
nessun metodo pubblico è in esecuzione. Questo ...
perché, durante l'esecuzione di un metodo, l'invariante di modalità (che appaiono innocue): public ArrayList<Integer> comeArrayList() {
rappresentazione potrebbe essere temporaneamente return new ArrayList<Integer>(rep);
violato per motivi implementativi per poi essere Restituire il rep attraverso un metodo pubblico. }
ripristinato prima della fine dell'esecuzione. Consideriamo l'esempio InsiemeDiInteri e ...
supponiamo di voler implementare il metodo }

Ad esempio, consideriamo un'implementazione comeArrayList che restituisce una


alternativa inefficiente di inserisci in rappresentazione dell'insieme sotto forma di
InsiemeDiInteri: ArrayList<Integer>. Un'implementazione che
potrebbe risultare naturale è:

public class InsiemeDiInteri {


... public class InsiemeDiInteri {
public void inserisciIneff(int x) { ...
int iX = trova(x); public ArrayList<Integer> comeArrayList() {
rep.add(x); return rep;
if (iX != -1) { rep.remove(x); } }
} ...
... }
}

Sbagliato! Stiamo esponendo il rep.


inserisciIneff controlla se x sia già un elemento
dell'insieme, in caso affermativo lo inserisce in fondo Assegnare un riferimento ricevuto come parametro al
(con add) e rimuove il duplicato (con remove), altrimenti rep. Supponiamo ora di voler implementare il metodo
effettua solo l'inserimento. Nonostante nel primo statico daArrayList che riceve un
scenario, tra la chiamata ad add e quella a remove, l'RI ArrayList<Integer> e restituisce il corrispondente
risulti temporaneamente violato, dato che avremo un InsiemeDiInteri. Una possibile implementazione
elemento x duplicato in rep; l'implementazione è è:
comunque valida perché ripristina l'invariante prima del
termine dell'esecuzione del metodo.
public class InsiemeDiInteri {
...
Per verificare che l'RI sia valido e si conservi durante il public static InsiemeDiInteri
ciclo di vita dell'oggetto dobbiamo innanzitutto verificare daArrayList(ArrayList<Integer> lista) {
che valga dopo l'esecuzione di tutti i costruttori pubblici InsiemeDiInteri s = new InsiemeDiInteri();
(in tutti gli stati concreti iniziali). Poi dobbiamo dimostrare s.rep = lista;
che, supponendo che l'RI sia valido per un certo valore return s;
del rep (quindi supponendo di trovarci in uno stato }
concreto ammissibile), per ogni metodo pubblico della ...
classe, al termine dell'esecuzione di tale metodo, }
l'invariante sia ancora rispettato.
Anche in questo caso l'implementazione è sbagliata!
Esposizione del rep Non solo stiamo esponendo il rep, non stiamo nenche 7
controllando che lista soddisfi l'RI.
di tutti gli elementi appartenenti all'insieme (ciascuno che viene calcolato è la somma dei soli elementi positivi
considerato nella somma una ed una sola volta): dell'insieme e non la traccia dell'insieme. Infatti la
Principio di sostituzione ridefinizione di iterator in
InsiemeDiInteriIteratorPositivi viola il contratto
public static int traccia(InsiemeDiInteri s) {
della superclasse: NON itera su tutti e soli gli elementi
Secondo il principio open-closed un modulo (che in Java int somma = 0;
for (Iterator<Integer> iter = s.iterator(); dell'insieme, ma solo sui positivi. In conclusione
coincide con una classe) dovrebbe essere chiuso
iter.hasNext(); ) { InsiemeDiInteriIteratorOrdinato soddisfa il
rispetto alla modifica ed aperto rispetto all'estensione.
Cioè se vogliamo arricchire il comportamento di un certo somma += iter.next(); principio di sostituzione,
} InsiemeDiInteriIteratorPositivi NO.
modulo, non possiamo modificarne la specifica, bensì
return somma;
dobbiamo estenderlo con un sottomodulo che aggiunge
}
nuove funzionalità. Regole per soddisfare il principio
In Java, per realizzare l'estensione ricorriamo al Consideriamo le seguenti sottoclassi che ereditano da di sostituzione
meccanismo di ereditarietà. Dobbiamo però fare InsiemeDiInteri:
attenzione: NON tutte le sottoclassi ottenute tramite Per assicurarci che il contratto di una sottoclasse sia
ereditarietà realizzano un'estensione della superclasse. compatibile con quello della superclasse (e quindi che la
In particolare, perchè una sottoclasse B estenda la public class InsiemeDiInteriIteratorOrdinato sottoclasse soddisfi il principio di sostituzione) è
extends InsiemeDiInteri { sufficiente verificare che valgano le tre regole che
corrispondente superclasse A, gli oggetti di B devono
@Override
essere sostituibili a quelli di A; nel senso che gli oggetti seguono:
//@ ensures (* restituisce un iteratore che
di B possono essere utilizzati da un utente come oggetti //@ itera su tutti e soli gli elementi
della classe A, senza notare alcuna differenza. //@ dell'insieme, ognuno visitato una signature rule: una sottoclasse deve disporre di tutti i
//@ volta, in ordine crescente *); metodi della superclasse e le firme dei metodi della
Per formalizzare questo concetto di sostituibilità, public Iterator<Integer> iterator(); sottoclasse devono essere compatibili con quelle
} dei metodi della superclasse
introduciamo il principio di sostituzione di Liskov:
"Tutti gli oggetti della sottoclasse devono soddisfare il method rule: le chiamate ad un metodo della
contratto della superclasse", ma il contratto può essere sottoclasse si devono "comportare" come le chiamate
e al metodo corrispondente della superclasse
esteso per coprire ulteriori casi.
property rule: una sottoclasse deve preservare tutte
public class InsiemeDiInteriIteratorPositivi le proprietà astratte (vedi capitolo sul JML) degli
Consideriamo l'esempio InsiemeDiInteri introdotto
extends InsiemeDiInteri { oggetti della superclasse
nel paragrafo Esempio nel capitolo JML. Aggiungiamo
@Override
alla classe InsiemeDiInteri un Iterator che //@ ensures (* restituisce un iteratore che
permette di iterare sugli elementi dell'insieme senza La signature rule garantisce che la sintassi con cui
//@ itera su tutti e soli gli elementi operiamo sulla sottoclasse sia compatibile con la
garantire un ordine preciso (scriviamo solo la specifica): //@ positivi dell'insieme, ognuno visitato
sintassi della superclasse. La method rule garantisce
//@ una volta,
che il contratto (inteso come la specifica) di ciascun
//@ in un ordine qualsiasi *);
public class InsiemeDiInteri {
public Iterator<Integer> iterator();
metodo ereditato sia compatibile con il contratto del
...
} metodo originale. In particolare un'estensione si dice
//@ ensures (* restituisce un iteratore che pura se non cambia la specifica (aggiunge solo dei
//@ itera su tutti e soli gli elementi metodi). La property rule garantisce che la specifica
//@ dell'insieme, ognuno visitato una Il metodo traccia continua a funzionare sugli oggetti della sottoclasse nella sua interezza sia compatibile con
//@ volta, in un ordine qualsiasi *); quella della classe originale.
public Iterator<Integer> iterator(); della classe InsiemeDiInteriIteratorOrdinato
... (dato che vale la proprietà commutativa della somma). In
} particolare la ridefinizione di iterator di questa Signature rule
sottoclasse continua a soddisfare il contratto della
superclasse (dato che la superclasse ammette che gli La signature rule è verificabile staticamente dal
Supponiamo che un utente di InsiemeDiInteri voglia elementi vengano visitati in un ordine qualsiasi). Al compilatore di Java.
implementare il metodo traccia che calcola la somma contrario quando richiamiamo traccia con un oggetto 8
della classe InsiemeDiInteriIteratorPositivi, ciò
controvarianza dei parametri di input NON è supportata se rafforziamo la premessa indeboliamo
In particolare la signature rule "implementata" in Java è: in Java, dove, per fare un override, è necessario che PD l'implicazione: supponiamo che c sia più forte di a,
una sottoclasse deve avere tutti i metodi della coincida con PA. allora è sempre vero c ==> a. Allora, se a ==> b è
superclasse e la firma dei metodi della sottoclasse deve vero, per transitività c ==> b è vero, cioè abbiamo
essere identica a quella dei metodi corrispondenti della Consideriamo ora il valore restituito da a.f (sempre nel dimostrato che (a ==> b) ==> (c ==> b) e cioè
superclasse. Un metodo della sottoclasse può però caso in cui abbiamo passato ad usaA un oggetto d della che a ==> b è più forte di c ==> b.
avere meno eccezioni di quello della superclasse classe D). a.f restituirà un oggetto della classe RD, che,
(ricordiamo che in Java la firma di un metodo è costituita perché la signature rule sia verificata, deve poter essere Precondizione più debole
dal suo nome e dalla lista dei parametri, ma non include assegnato ad un oggetto di tipo RA come l'utente si
il tipo restituito, quindi non possiamo dichiarare nella aspetta. Quindi RD può coincidere con RA oppure può
stessa classe due metodi che hanno lo stesso nome e la Se la precondizione del metodo ereditato è più debole di
essere una sottoclasse di RA. Il fatto che se RD è una quella del metodo originale allora se è vera la
stessa lista di parametri anche se restituiscono un tipo
sottoclasse di RA, la signature rule è ancora soddisfatta, precondizione del metodo originale è anche vera quella
diverso). Notiamo che la signature rule in Java è più
restrittiva di quella enunciata nel caso generale, infatti si dice covarianza dei risultati. La covarianza dei del metodo ereditato e cioè possiamo richiamare il
esistono metodi che hanno firma compatibile ma NON risultati è ammissibile in Java a partire da Java 5. metodo ereditato in tutti i casi in cui potevano richiamare
identica. Per dimostrarlo occorre introdurre i concetti di quello originale; proprio come ci aspetteremmo! Quindi
controvarianza e covarianza. Method rule condizione necessaria perchè valaga la method rule è
la precondition rule: detta pre_sub la precondizione
del metodo ereditato e pre_super la precondizione di
Controvarianza e covarianza Perché la chiamata di un metodo di una sottoclasse
quello originale, pre_sub è più debole di pre_super e
abbia lo stesso effetto della chiamata del metodo della
cioè pre_super ==> pre_sub è sempre vera.
L'esempio che segue non è valido in Java, dato che ci classe originale, è sufficiente che il metodo ereditato
serve per illustrare i concetti di controvarianza e soddisfi la specifica del metodo originale. Questo NON è
covarianza che non sono entrambi validi nel linguaggio. verificabile staticamente dal compilatore. In termini non Se pre_sub non fosse più debole di pre_super
Consideriamo una superclasse A che definisce il metodo formali un metodo ereditato soddisfa la specifica del esisterebbe un caso in cui pre_sub è falsa e pre_super è
f : PA -> RA (dove PA è la classe dei parametri di f e metodo originale se ha una precondizione più debole vera. L'utente richiamando il metodo in quel caso (che
(richiede meno) ed una postcondizione più forte soddisfa la precondizione del metodo originale e quindi
RA è la classe degli oggetti restituiti da f). Sia D una
(promette di più). Formalizziamolo. risulta un caso valido di input) otterrebbe un risultato
sottoclasse di A che ridefinisce il metodo f come f :
inaspettato (dato che tale caso viola la precondizione del
PD -> RD. Consideriamo uno snippet di codice che
Condizioni forti e deboli metodo ereditato che è quello che viene effettivamente
utilizza un oggetto della classe A (dato che non facciamo invocato).
riferimento ad un linguaggio in particolare useremo una
sintassi inventata): Una condizione è più forte di un altra se è vera "in meno
casi". Questo concetto si formalizza in logica attraverso Postcondizione più forte
l'implicazione: una condizione a è più forte di una
method void usaA(A a) { condizione b se è sempre vera la condizione a ==> b. Alla fine dell'esecuzione del metodo ereditato ci
... aspettiamo che la postcondizione del metodo originale,
Questo introduce un ordinamento (non totale) delle
RA rA = a.f(pA); detta post_super sia soddisfatta. L'utente, nella sua
...
formule logiche dalla più forte alla più debole. In
particolare false è la condizione più forte in assoluto e implementazione, infatti fa riferimento solo alla
}
true la più debole in assoluto. postcondizione del metodo originale e non deve
preoccuparsi se in realtà ad essere invocato è il metodo
Supponiamo di passare come parametro in usaA un ereditato. Quindi, detta post_sub la postcondizione del
Vediamo l'effetto degli operatori logici sulla forza
oggetto d della classe D. Perchè la signature rule sia metodo ereditato, una condizione sufficiente
delle condizioni:
verificata, l'oggetto pA, che ha tipo dinamico PA deve (assumendo che la precondition rule sia stata rispettata)
poter essere accettato come parametro anche da D.f, perchè valga la method rule è che post_sub sia più
|| indebolisce: a ==> a || b (cioè a || b è più forte di post_super, ovvero post_sub ==>
che si aspetta un parametro di tipo PD. Quindi la classe debole di a) post_super. In questo modo, dato che il metodo
PD può coincidere con la classe PA o può essere un && rafforza: a && b ==> a (cioè a && b è più forte
supertipo della classe PA. Il fatto che se PD è una ereditato deve soddisfare la sua postcondizione,
di a) soddisferà automaticamente anche le postcondizione del
superclasse di PA, la signature rule è ancora soddisfatta, ==> indebolice: a ==> (b ==> a), si deduce 9
metodo originale.
si dice controvarianza dei parametri di input. La ricordando che b ==> a equivale ad !b || a
Notiamo che pre_sub è più debole di pre_super e che, anche post_super vale true, allora post_sub vale
In realtà questa condizione è più stringente del dato che && rafforza, vale post_sub ==> true)
necessario. L'utente invocherà il metodo solo nei casi (\old(pre_super) ==> post_super). Quindi i metodi
che soddisfano la precondizone del metodo originale ed ottenuti per estensione, se specificati attraverso JML, Eliminare le eccezioni
abbiamo visto che in generale la precondizione del soddisfano sempre la method rule dato che valgono
metodo ereditato può "ammettere più casi". Quindi ci la precondition rule e la full postcondition rule.
Abbiamo già spiegato che è possibile rimuovere alcune
basta che valga la full postcondition rule cioè che delle eccezioni quando si fa un override di un metodo,
post_sub implichi che post_super sia vera se Osserviamo che specificando i metodi con also: senza violare la signature rule. Dobbiamo però fare
pre_super era vera al momento della chiamata del molta attenzione che la method rule sia ancora
metodo, che in formule diventa: post_sub ==> Non possiamo rafforzare la precondizione: soddisfatta. Consideriamo la seguente classe:
(\old(pre_super) ==> post_super). Sulle slide la supponiamo pre_ext più forte di pre_super, allora
full postcondition rule è espressa come pre_ext ==> pre_super è sempre vera, allora
(\old(pre_super) && post_sub) ==> post_super public class InsiemeDiInteriEccezDuplicati {
!pre_ext || pre_super = true, allora pre_ext ...
(in realtà senza \old ma è più corretto aggiungerlo); le && !pre_super = false, allora pre_sub = //@ ensures !\old(appartiene(x)) &&
due formule sono equivalenti, infatti post_sub ==> pre_super || pre_ext = pre_super || //@ appartiene(x) && ...;
(\old(pre_super) ==> post_super) = !post_sub pre_ext && true = pre_super || pre_ext && //@ signals (EccezioneDuplicato e)
|| (!\old(pre_super) || post_super) = (pre_super || !pre_super) = pre_super || //@ \old(appertiene(x));
(!\old(pre_super) || !post_sub) || post_super public void inserisci(int x)
pre_ext && pre_super || pre_ext &&
= !(!\old(pre_super) || !post_sub) ==> throws EccezioneDuplicato;
!pre_super = pre_super && true || ...
post_super = (\old(pre_super) && post_sub)
pre_super && pre_ext || false = pre_super }
==> post_super.
&& (true || pre_ext) = pre_super && true =
pre_super
Specificare metodi che ne estendono altri in JML Non possiamo indebolire la postcondizione: che restituisce un'eccezione quando si prova ad inserire
supponiamo post_ext più debole di post_super, un elemento già presente nell'insieme. Per ipotesi
Per specificare un metodo che ne estende un altro in allora post_super ==> post_ext è sempre vera,
JML si utilizza la keyword also che si inserisce nella allora, nell'ipotesi che \old(pre_super) sia public class InsiemeDiInteri
specifica in questo modo: verificata, post_sub = (true ==> post_super) extends InsiemeDiInteriEccezDuplicati {
&& (\old(pre_ext) ==> post_ext) = ...
post_super && (\old(pre_ext) ==> post_ext) //@ ensures appartiene(x) &&
//@ also
= post_super (se post_super vale false allora //@ cardinalita() == \old(cardinalita() +
//@ requires pre_ext;
post_sub = post_super && ... vale false, se //@ (appartiene(x) ? 0 : 1)) &&
//@ ensures post_ext;
//@ (\forall int y; \old(appartiene(y));
post_super vale true, allora post_ext vale true, //@ appartiene(y));
allora anche \old(pre_ext) ==> post_ext vale public void inserisci(int x);
Notiamo che in requires ed ensures compaiono true, allora post_sub vale true). ...
pre_ext e post_ext invece di pre_sub e post_sub. Se volessimo semplicemente rafforzare la }
Questo perchè la semantica della specifica con also si postcondizione è sufficiente che pre_ext =
traduce in: pre_super e che post_ext sia più forte di
post_super: pre_sub = pre_super || pre_ext supponiamo che InsiemeDiInteri sia una
= pre_super || pre_super = pre_super e, sottoclasse di InsiemeDiInteriEccezDuplicati e
//@ requires pre_super || pre_ext;
supponendo che \old(pre_super) = che ridefinisca il metodo inserisci secondo la
//@ ensures (\old(pre_super) ==> post_super)
//@ && (\old(pre_ext) ==> post_ext); \old(pre_ext) sia verificata, post_sub = specifica che abbiamo introdotto originariamente (nel
capitolo JML). Secondo ciò che abbiamo detto
(\old(pre_super) ==> post_super) &&
InsiemeDiInteri soddisfa la signature rule. Un utente
(\old(pre_ext) ==> post_ext) = (true ==>
Quindi pre_sub = pre_super || pre_ext e di InsiemeDiInteriEccezDuplicati però, si aspetta
post_super) && (true ==> post_ext) =
post_sub = (\old(pre_super) ==> post_super) di ricevere un'eccezione se inserisce un elemento due
post_super && post_ext = post_ext (se
&& (\old(pre_ext) ==> post_ext). volte di seguito nell'insieme, ma questo NON avviene se
post_ext vale false allora post_sub = ... && il tipo dinamico dell'insieme è InsiemeDiInteri. 10
post_ext vale false, se post_ext vale true allora
Esempio di violazione di una proprietà invariante Esempio di violazione di una proprietà evolutiva
Quindi, in questo esempio, InsiemeDiInteri viola la
method rule e NON rispetta il principio di sostituzione. Consideriamo la classe InsiemeDiInteriPieno, Consideriamo la classe
ovvero un InsiemeDiInteri che non è mai vuoto: InsiemeDiInteriSenzaRimuovi che è uguale ad
Property rule InsiemeDiInteri ma non ha il metodo rimuovi.
InsiemeDiInteriSenzaRimuovi gode della seguente
public class InsiemeDiInteriPieno {
proprietà evolutiva: "il valore restituito da cardinalita
Una classe gode di due macrocategorie di proprietà // OVERVIEW: Insieme di interi illimitato
// e mutabile con almeno un elemento nello stato prossimo è maggiore o uguale a quello
astratte: le proprietà invarianti e le proprietà evolutive
restituito nello stato corrente" (l'insieme non si
(vedi capitolo sul JML). Le prime possono essere
//@ public invariant cardinalita() >= 1; "restringe"). Supponiamo che la classe
esplicitate in JML attraverso il costrutto public
InsiemeDiInteri estenda la classe
invariant, ma questo non avviene sempre; al contrario
//@ ensures (* restituisce un InsiemeDiInteriSenzaRimuovi aggiungendo, come
le seconde sono deducibili solo considerando le //@ InsiemeDiInteriPieno che rappresenta nell'esempio precedente, solo il metodo rimuovi definito
specifiche di tutti i metodi della classe, nel loro //@ l'insieme { i } *);
complesso. Perchè una sottoclasse soddisfi la property come al solito. Come nell'esempio precedente la
public InsiemeDiInteriPieno(int i); signature rule e la method rule non possono essere
rule, deve conservare le proprietà astratte della classe
che estende, per tutti gli stati astratti osservabili violate, dato che InsiemeDiInteri non ridefinice alcun
//@ ensures (\old(cardinalita() >= 2) ==>
tramite gli osservatori della classe originale. Non è //@ !appartiene(x)) && metodo. Chiaramente ad essere violata è la proprietà
infatti necessario che le proprietà astratte della classe //@ ((\old(cardinalita() == 1)) ==> evolutiva di InsiemeDiInteriSenzaRimuovi e di
originale valgano anche per la "porzione" di stato //@ (* l'insieme rimane invariato *)); conseguenza la property rule.
astratto della classe derivata che è osservabile tramite public void rimuoviSeNonVuoto(int x);
dei metodi presenti solo in quest'ultima. }

Ad esempio se una classe immutabile A viene estesa da Supponiamo che InsiemeDiInteri sia una
una classe B definita come segue: sottoclasse di InsiemeDiInteriPieno che aggiunge il
metodo rimuovi così come lo avevamo definito
public class B extends A { originariamente:
private int b;
public class InsiemeDiInteri
public void setB(int b) { this.b = b; }
extends InsiemeDiInteriPieno {
public int getB() { return b; }
} //@ ensures !appartiene(x) &&
//@ cardinalita() == \old(cardinalita() -
//@ (appartiene(x) ? 1 : 0)) &&
//@ (\forall int y; appartiene(y);
Nonostante B sia chiaramente mutabile, la "porzione" del
//@ \old(appartiene(y)));
suo stato astratto che muta (l'attributo b) non è public void rimuovi(int x);
osservabile con gli observer di A: quindi B soddisfa la }
property rule (non viola la proprietà evolutiva di
immutabilità rispetto allo stato osservabile dagli utenti di
A). Dato che InsiemeDiInteri non ridefinisce alcun
metodo di InsiemeDiInteriPieno, la signature rule e
A differenza della signature rule e della method rule, la method rule risultano soddisfatte. Grazie però al
nulla garantisce che la property rule sia stata rispettata, nuovo metodo rimuovi possiamo violare l'invariante
quindi è necessario valutarlo volta per volta in base alle pubblico di InsiemeDiInteriPieno svuotando
specifiche di tutti i metodi della classe. l'insieme. Quindi, in questo esempio, InsiemeDiInteri
viola la property rule e di conseguenza NON rispetta il
principio di sostituzione. 11
Per motivi di riutilizzo del codice, viene solitamente class Account {
preferito utilizzare un metodo alternativo per la creazione
Programmazione di un Thread, implementando invece l'interfaccia
private float balance;

funzionale java.lang.Runnable: public Account(float balanceIniz) {


concorrente }
balance = balanceIniz;

class MyRunnable implements Runnable {


La programmazione concorrente permette di svolgere @Override public void deposit(float money) {
più attività (dette task) in maniera simultanea. public void run() { synchronized(this) {
// Fai cose balance += money;
} }
L'esecuzione può avvenire in parallelismo fisico (ovvero }
mediante l'utilizzo di più processori o un processore }
... public void withdraw(float money) {
multi-core) o mediante la condivisione di un unico // Fa partire il thread e ritorna subito synchronized(this) {
processore, seguendo le decisioni dello scheduler new Thread(new MyRunnable()).start(); balance -= money;
(sistemi time-shared). }
}
Si può avere concorrenza: Oppure usando i paradigmi della programmazione }
funzionale:
A livello di processi: ogni processo ha un suo spazio
L'intrinsic lock:
di memoria riservato, la comunicazione fra di essi Runnable r = () -> {
deve avvenire mediante meccanismi di IPC (Inter- // Fai cose
process communication). Non è argomento del corso. }; mette in mutua esclusione sezioni di codice,
A livello di thread: un singolo processo può permettendo a un solo thread alla volta l'esecuzione,
contenere numerosi thread, che condividono tutti lo new Thread(r).start(); sospendendo gli altri thread chiamanti fino al
stesso spazio di indirizzamento e possono completamento.
comunicare mediante variabili condivise. é un lock rientrante: se il thread, quando raggiunge
O in modo ancora più compatto: una sezione critica, è già in possesso del lock,
continuerà la sua esecuzione senza venire sospeso.
La libreria standard Java mette a disposizione, oltre alla
new Thread(() -> { /* Fai cose */ }).start(); quando viene acquisito/rilasciato, forza la
primitiva di sistema Thread, anche una serie di primitive
sincronizzazione della cache locale del thread con la
per semplificare la programmazione concorrente.
memoria centrale, garantendo che tutte le variabili
Notiamo come, in entrambi i casi di crezione lette siano aggiornate.
Thread (estendendo Thread o implementando Runnable), il
thread sia libero di accedere a qualsiasi variabile globale Notiamo come, a causa della cache locale del thread
Per creare un thread, è sufficiente estendere la classe o passata nel costruttore, causando potenzialmente menzionata al terzo punto, sia necessario mettere in
java.lang.Thread, fare l'override del metodo run(), errori di interferenza se più thread accedono alle stesse sezioni sincronizzate anche metodi che leggono solo
istanziare la classe e chiamare il metodo start() (NB: variabili contemporaneamente. una variabile, senza modificarla. Questo perché,
start, non run): sebbene il metodo stesso non la modifichi, la variabile
potrebbe però essere cambiata esternamente da altri
Intrinsic lock thread, causando un desync tra la cache locale e la
class MyThread extends Thread { memoria centrale.
@Override Java, di default, associa a ogni oggetto un proprio "lock",
public void run() { ovvero un oggetto che permette di implementare mutua
// Fai cose esclusione tra i vari thread su sezioni di codice di
} interesse. Questo avviene mediante l'uso della keyword
} synchronized, specificando poi l'oggetto di cui si vuole
...
utilizzare il lock:
// Fa partire il thread e ritorna subito
new MyThread().start(); 12
Altre cose su cui riporre particolare attenzione quando si }
Esiste anche una forma più breve di scrivere il codice utilizza un intrinsic lock:
public void setGrade(int grade) {
precedente, mettendo la keyword direttamente sul this.grade = grade;
metodo: essendo il lock legato ad un'instanza di una classe, }
quando si utilizza synchronized in metodi statici non }
si ha a disposizione un istanza su cui sincronizzare.
class Account {
Si utilizza quindi l'istanza della classe stessa, ovvero:
private float balance;
Lo svantaggio è la mancanza della sezione critica
public Account(float balanceIniz) { stessa: un'operazione come x++ non è atomica, e se
public class ExampleClass {
balance = balanceIniz; non sincronizzata causerà problemi a prescindere dalla
public static void static1() {
} synchronized(ExampleClass.class) { presenza di volatile. Questo problema viene risolto
// sezione critica dall'utilizzo delle variabili atomiche, che sono
public synchronized void deposit( } implementate dalle classi AtomicBoolean,
float money) { } AtomicInteger, AtomicLong e AtomicReference<V>.
balance += money; // Di default Java effettua la stessa cosa
} // anche per synchronized posto su un metodo
// statico. I due metodi sono equivalenti class Counter {
public synchronized void withdraw( public static synchronized void static2() { private final AtomicInteger count =
float money) { // sezione critica new AtomicInteger();
balance -= money; }
} } public int get() {
} return count.get();
}
synchronized posto su un metodo non sarà
E' possibile chiamare il metodo synchronized su automaticamente ereditato da metodi che ne public void increment() {
qualsiasi oggetto di cui si ha una referenza (quindi count.getAndIncrement();
effettuano l'overriding. Bisognerà quindi richiedere
escludendo i primitivi): }
esplicitamente che anche il l'overriding method sia
synchronized. public void set(int count) {
class Account { count.getAndSet(count);
private float balance;
private final Object lock = new Object();
Variabili volatili e variabili }
}

... atomiche
public void deposit(float money) {
synchronized(lock) { La mancanza di un tipo atomico per double e float può
balance += money; Se non necessitiamo di una sezione critica, ma solo di essere sopperita mediante:
} forzare la sincronizzazione di una variabile della cache
} locale del thread con la memoria centrale, possiamo
utilizzare la keyword volatile sulla variabile di float: AtomicInteger con i metodi
public void withdraw(float money) {
Float.floatToIntBits(float) e
synchronized(lock) { interesse:
balance -= money; Float.intBitstoFloat(int)
} double: AtomicLong con i metodi
} class StudentGrade { Double.doubleToLongBits(double) e
} private final String matricola; Double.longBitsToDouble(long)
private volatile int grade;

Questo si rivela molto utile per evitare di sincronizzare public StudentGrade(String matricola) {
eccessivamente, andando a ridurre il parallelismo tra i this.matricola = matricola;
thread (cosa che nei TdE solitamente è richiesta). Si }
veda per esempio l'esercizio svolto sotto, dove si ha
public void getGrade() {
un'intera array di locks invece di utilizzare unicamente il 13
return grade;
singolo lock di this;
private int check(int component) { delle condizioni che devono essere vere prima che
if (component < 0 || component > 255) l'esecuzione di un metodo possa continuare:
oppure, se è necessario sommare, utilizzando
throw new IllegalArgumentException();
DoubleAdder:
return component;
} wait() throws InterruptedException: sospende
l'esecuzione di un thread fino a quando viene
class Account { risvegliato da un altro, rilasciando il lock al momento
public ImmutableRGB(int r, int g, int b,
private final DoubleAdder balance =
String name) { della sospensione e ri-acquisendolo al risveglio
new DoubleAdder();
this.red = check(r); notify(): sveglia un thread sospeso in attesa sul
public float get() { this.green = check(g); lock di questo oggetto per riprendere la sua
return (float) balance.sum(); this.blue = check(b); esecuzione
} this.name = name; notifyAll(): sveglia tutti i thread sospesi in attesa
}
sul lock di questo oggetto
public void deposit(float money) {
public int getRGB() {
balance.add(money); L'invocazione di questi metodi richiede che il thread
} return (red << 16) | (green << 8) | blue;
} invocante sia possessore del lock stesso.
public void withdraw(float money) {
balance.add(-money); public String getName() { Notiamo inoltre come il metodo wait sollevi l'eccezione
return name;
} InterruptedException, che avviene se il thread
}
} corrente viene interrotto (ad esempio perché viene
public ImmutableRGB invert() { richiesta la terminazione del programma).
Particolare attenzione va posta anche nella return new ImmutableRGB(255 - red,
dichiarazione di array: volatile int[] array = new 255 - green,
255 - blue,
int[20] dichiara un array di interi con referenza volatile, "Inverse of " + name);
non un array di interi volatili. Bisogna quindi utilizzare }
AtomicIntegerArray, AtomicLongArray o }
AtomicReferenceArray<E>.

Gli oggetti immutabili non necessitano di alcun tipo di


Variabili finali e oggetti immutabili sincronizzazione.

Le variabili final non hanno bisogno di essere


sincronizzate in alcuna maniera, in quanto, non potendo
Variabili static
cambiare, è impossibile che avvenga un desync tra la
cache locale e la memoria centrale. Al contrario delle variabili final, le variabili static non
sono necessariamente thread-safe, in quanto possono
essere lette e modificate da più classi e più thread
Questo ci porta agli oggetti immutabili: un oggetto si dice
contemporaneamente. Bisognerebbe quindi sempre
immutabile se ogni suo campo è dichiarato final e
sincronizzare l'accesso alle variabili static per evitare
ogni modifica dello stesso avviene mediante la
che si leggano valori non aggiornati.
creazione di un nuovo oggetto:

public class ImmutableRGB { wait, notify


e notifyAll
private final String name; (producer/consumer pattern)
// Values must be between 0 and 255.
private final int red;
private final int green; Java mette a disposizione nell'intrinsic lock anche delle
private final int blue; primitive per implementare dei Guarded Blocks, ovvero
14
Lock lock = new ReentrantLock();
Riprendendo l'esempio di un account bancario, se volessimo aspettare che l'utente abbia if (lock.tryLock()) {
soldi prima di effettuare un'operazione di prelievo: try {
// qui siamo in sezione critica
} finally {
class Account { // 'finally' per rilasciare il lock qualsiasi cosa succeda
private final Object lock = new Object(); // sia esecuzione normale che eccezioni
private float balance; lock.unlock();
}
public Account(float balanceIniz) { } else {
balance = balanceIniz; // azioni alternative dove il lock non è necessario
} }

public void deposit(float money) {


synchronized(lock) {
balance += money;
Executors
lock.notifyAll();
} L'utilizzo eccessivo/non limitato di thread può portare a problemi di performance. Per
} ovviare a questo problema, sono state introdotte le interfacce Executor,
ExecutorService e ScheduledExecutorService e i metodi statici nella classe
public void withdraw(float money)
Executors per istanziarne implementazioni concrete. Per esempio
throws InterruptedException {
synchronized(lock) { Executors.newFixedThreadPool(int) utilizza un numero fisso di thread a cui fare
// Notiamo l'utilizzo del while al posto eseguire le task.
// di un if: questo perché il thread può
// svegliarsi spontaneamente (su alcune
ExecutorService executor = Executors.newFixedThreadPool(5);
// architetture e/o OS specifici)
executor.submit(() -> {
while (balance - money < 0)
// Questo viene eseguito su uno dei 5 thread
lock.wait();
System.out.println("Executed on " +
balance -= money;
Thread.currentThread());
}
});
}
}

Collezioni concorrenti
Explicit lock
Sempre il package java.util.concurrent introduce anche dei tipi di collezioni che
Meccanismi di lock più sofisticati sono forniti dal package sono già predisposte all'utilizzo concorrente, senza necessitare di sincronizzazione. Di
java.util.concurrent.locks. Questo definisce un'interfaccia di base Lock che, oltre seguito una per tipo di collezione:
a permettere le stesse operazioni dell'intrinsic lock, aggiunge anche ulteriori metodi e
funzionalità: CopyOnWriteArrayList<E> implementa List<E>
ConcurrentHashMap<K, V> implementa ConcurrentMap<K, V> che implementa
Permette di chiamare lock e unlock anche in metodi separati Map<K, V>
Il lock è sempre esplicito, bisogna per forza pensare a chi ne avrà accesso ConcurrentHashMap.<K>newKeySet() implementa Set<K>
lockInterruptibly() throws InterruptedException: esegue la stessa ConcurrentLinkedQueue implementa Queue<E> (in maniera non bloccante)
operazione di lock, ma permette di interrompere l'esecuzione del thread quando ArrayBlockingQueue implementa BlockingQueue<E> che implementa Queue<E>
questo è stato sospeso. (aggiungendo operazioni bloccanti)
boolean tryLock(): acquisisce il lock solo se non è già utilizzato

15
Osserviamo per prima cosa come, non potendo Si modifichi il metodo getScore affinché sospenda il
cambiare né il costruttore né la definizione dei campi, chiamante se il voto dello studente cercato è minore o
Esercizio (TdE del 2022-07-21, possiamo scartare a priori variabili volatili e atomiche. uguale a zero. Si specifichi se e come sia necessario
esercizio 2 - punto a e b) Siamo quindi obbligati ad utilizzare intrinsic locking modificare altri metodi.
(strano).
Testo dell'esercizio Soluzione
Come seconda osservazione, notiamo come students
sia inizializzata nel costruttore e non sia poi più Questo è un esempio di consumer/producer standard:
Si consideri la seguente classe ScoreBoard per modificata. Questo vuol dire che, molto probabilmente,
memorizzare i voti di un insieme di studenti passato in l'accesso a questa non dovrà stare in sezione critica.
fase di costruzione: La condizione di wait sarà scores[i] <= 0
Bisogna modificare setScore per notificare
Come ultima cosa, per massimizzare il parallelismo, l'avvenuta modifica
public class ScoreBoard { dobbiamo evitare il conflitto dei dati tra medesimi
private String[] students; studenti, non tra studenti diversi: non dobbiamo quindi
private int[] scores; utilizzare lo stesso lock per ogni studente, dovremmo public class ScoreBoard {
avere un lock per studente. Non potendo dichiarare
public ScoreBoard(String[] stud) { nessun campo aggiuntivo, possiamo riutilizzare come ...
students = new String[stud.length]; intrinsic lock i valori dentro all'array students, in quanto
for(int i = 0; i < stud.length; i++) come detto in precedenza non vengono modificati. public int getScore() {
students[i] = stud[i]; for(int i = 0; i < students.length; i++) {
scores = new int[stud.length]; if(students[i].equals(stud)) {
} public class ScoreBoard { synchronized(students[i]) {
while(scores[i] <= 0)
public int getScore() { ... students[i].wait();
for(int i = 0; i < students.length; i++) { return scores[i];
if(students[i].equals(stud)) public int getScore() { }
return scores[i]; for(int i = 0; i < students.length; i++) { }
} if(students[i].equals(stud)) { }
return -1; synchronized(students[i]) { return -1;
} return scores[i]; }
}
public int setScore(String stud, int score) { } public int setScore(String stud, int score) {
for(int i = 0; i < students.length; i++) { } for(int i = 0; i < students.length; i++) {
if(students[i].equals(stud)) return -1; if(students[i].equals(stud)) {
scores[i] = score; } synchronized(students[i]) {
} scores[i] = score;
} public int setScore(String stud, int score) { students[i].notifyAll();
} for(int i = 0; i < students.length; i++) { }
if(students[i].equals(stud)) { }
synchronized(students[i]) { }
Domanda a) scores[i] = score; }
} }
Si introduca l'opportuno codice di sincronizzazione nei }
metodi getScore e setScore al fine di massimizzare il }
}
parallelismo, consentendo a thread diversi di accedere }
in parallelo ai dati di studenti diversi, evitando conflitti
nell'accesso ai dati (voto) del medesimo studente.
Domanda b)
Soluzione
16
public static <T> void sort( Avendo l'interfaccia funzionale un solo metodo,
possiamo usare direttamente una espressione lambda
Programmazione )
List<T> list, Comparator<? super T> c
per implementarla:

funzionale Il metodo sort prende come primo parametro una lista Comparator<Persona> comparator = (p1, p2) -> {
if(p1.getEta() > p2.getEta()) {
di oggetti di tipo T, e come secondo parametro un
Il paradigma di programmazione funzionale si basa sul return 1; }
oggetto di tipo Comparator, possiamo passare un else if(p1.getEta() < p2.getEta()) {
principio di considerare le funzioni come oggetti, e quindi
Comparator direttamente usando una espressione return -1; }
utilizzarli come tali. In questo modo, le funzioni possono
lambda: else return 0;
essere passate come parametri, restituite come risultati,
};
memorizzate in variabili, ecc. Le funzioni usate come
parametro senza specificare il nome della funzione, ma obj.method(
solo il comportamento che essa deve avere, sono dette (param1, param2) -> { è come se avessimo "assegnato una funzione ad una
funzioni anonime. // corpo della funzione variabile (comparator)". Possiamo quindi passare la
} variabile comparator come secondo parametro al
);
Espressioni lambda metodo sort: Collections.sort(persone,
comparator);. Oppure, possiamo passare direttamente
Nel nostro esempio il metodo sort sarà implementato una lambda (come mostrato nel capitolo precedente).
Supponiamo di avere una classe fatta in questo modo:
così:
Java fornisce delle <interfacce funzionali / il
class Persona { corrispondente metodo per eseguire la
private String nome; Collections.sort(persone, (p1, p2) -> {
/* Java conosce già il tipo di p1 e p2, funzione> nel package java.util.function quali:
private int eta;
quindi non è necessario specificarlo */
public Persona(String nome, int eta) { if(p1.getEta() > p2.getEta()) { Function<T, R> / .apply(T t): funzione che
this.nome = nome; return 1; } prende un parametro di tipo T e restituisce un oggetto
this.eta = eta; else if(p1.getEta() < p2.getEta()) {
di tipo R;
} return -1; }
Consumer<T> / .accept(T t): funzione che prende
else return 0;
}); un parametro di tipo T e non restituisce nulla;
public String getNome() {
Supplier<T> / .get(): funzione che non prende
return nome;
} parametri e restituisce un oggetto di tipo T;
Interfacce funzionali Predicate<T> / .test(T t): funzione che prende
public int getEta() { un parametro di tipo T e restituisce un booleano (true
return eta; o false);
} Nell'esempio di prima, nel metodo public static <T>
BiFunction<T, U, R> / .apply(T t, U u):
} void sort(List<T> list, Comparator<? super T>
funzione che prende due parametri di tipo T e U e
c), il secondo parametro è un oggetto di tipo
restituisce un oggetto di tipo R.
Comparator<T>, è una interfaccia funzionale.
e supponiamo di avere una lista di persone:
E le corrispondenti per i tipi primitivi int, double e long:
Una interfaccia funzionale è un'interfaccia che prevede
List<Persona> persone = new ArrayList<>(); di implementare un solo metodo. In questo caso,
persone.add(new Persona("Mario", 20)); l'interfaccia Comparator<T> prevede di implementare il IntFunction<R> / .apply(int v): funzione che
persone.add(new Persona("Luigi", 30)); metodo int compare(T o1, T o2), che impone un prende un parametro di tipo int e restituisce un
persone.add(new Persona("Pippo", 40)); ordine tra due oggetti di tipo T e restituisce 1, 0, -1 a oggetto di tipo R;
persone.add(new Persona("Pluto", 50)); IntConsumer / .accept(int v): funzione che
seconda che il primo parametro o1 sia rispettivamente
maggiore, uguale o minore del secondo. prende un parametro di tipo int e non restituisce
nulla;
Possiamo, ad esempio, ordinare la lista di persone per 17
età, usando il metodo sort della classe Collections:
Stream<U> map(<funzione da T a U>) prende in Per facilitare la creazione di Comparator esistono anche
IntSupplier / .getAsInt(): funzione che non input una funzione (Function<T, U>) e la applica ad una serie di metodi, che permettono di mappare oggetti
prende parametri e restituisce un oggetto di tipo int; ogni elemento dello stream. Si usa per trasformare gli a dei loro campi per il quale un ordine naturale è già
IntPredicate / .test(int v): funzione che prende elementi dello stream in altri elementi. Nel nostro definito (come ad esempio per stringhe e primitivi):
un parametro di tipo int e restituisce un booleano esempio, vogliamo trasformare le persone (già filtrate) in
(true o false); stringhe contenenti il nome e l'età: static <T,U extends Comparable<? super
ToIntFunction<T> / .applyAsInt(T v): funzione U>> Comparator<T> comparing(Function<?
che prende un parametro di tipo T e restituisce un super T,? extends U> keyExtractor): accetta
Stream<String> stream = persone.stream()
primitivo di tipo int: int applyAsInt(T value); .filter(p -> p.getEta() >= 30) una funzione che estrae un campo dall'oggetto dato
DoubleToIntFunction / .applyAsInt(double v): .map(p -> { e costruisce un comparatore che compara su tale
funzione che prende un parametro di tipo double e return p.getNome() + ": " + campo. Per esempio, il comparatore definito
restituisce un oggetto di tipo int... p.getEta().toString(); precedentemente (che utilizza l'età delle persone),
}); può essere scritto come:

Composizione di funzioni Comparator<Persona> comparator =


Otterrò uno stream, contenente le stringhe: "Luigi: 30",
(Stream<T>) "Pippo: 40", "Pluto: 50". Comparator.comparing(p -> p.getEta())

Possiamo concatenare delle funzioni che agiscono su Esistono anche metodi simili per mappare a valori Comparator<T> reversed(): Restituisce un
una Collezione per costruire una catena di funzioni, primitivi: comparatore che impone l'ordine inverso di quello
che filtrerà, mapperà e agirà sulla nostra collezione. corrente. Per esempio, per ordinare una lista di
IntStream mapToInt(<funzione da T a int>), persone per età in ordine decrescente:
Questa catena inizierà invocando il metodo stream() DoubleStream mapToDouble(<funzione da T a
sulla collezione. Nell'esempio: double>)
List<Persona> persone = new ArrayList<>();
LongStream mapToLong(<funzione da T a persone.add(new Persona("Mario", 20));
Stream<Persona> stream = persone.stream(); long>). persone.add(new Persona("Luigi", 30));
persone.add(new Persona("Pippo", 40));
Questi ritornano un'istanza di uno stream di valori persone.add(new Persona("Pluto", 50));
filter primitivi, permettendo di chiamare metodi aggiuntivi che Collections.sort(persone, Comparator
possono essere utili (vedi sotto). .comparing(p -> p.getEta())
.reversed());
Possiamo filtrare lo stream per far si che tutti gli // persone sarà [Pluto, Pippo, Luigi, Mario]
elementi della collezione soddisfino un certo distinct e sorted
Predicate<T>. Per farlo, usiamo il metodo Stream<T>
filter(<predicato>). Nel nostro esempio, vogliamo static <T extends Comparable<? super
Stream<T> distinct() elimina gli elementi
filtrare le persone che hanno più di 30 anni: T>> Comparator<T> naturalOrder() e
duplicati dello stream.
static <T extends Comparable<? super
T>> Comparator<T> reverseOrder():
Stream<Persona> stream = persone.stream() Stream<T> sorted() ordina gli elementi dello
.filter(p -> p.getEta() >= 30); restituiscono un comparatore di un oggetto T per il
stream secondo il loro ordinamento naturale. quale è già definito un proprio ordine naturale che
Funziona solo su tipi che implementano già compara in ordine naturale/inverso. Per esempio, per
La funzione lambda nel metodo filter sarà true per l'interfaccia Comparable (come ad esempio ordinare una lista di interi in ordine decrescente:
Integer e String). (l'esempio è nella facciata seguente)
tre persone: Luigi, Pippo e Pluto. Verrà quindi eliminato
Mario (che ha 20 anni) dal nostro stream.
Stream<T> sorted(Comparator<? super
T> c) ordina gli elementi dello stream in base
map
a quanto definito dal Comparator dato.
18
LongStream flatMapToLong(<funzione da T a operazione di riduzione, sapendo però in aggiunta se lo
List<Integer> a = new ArrayList<>(); LongStream). stream fosse vuoto o contenesse degli elementi:
a.add(20);
a.add(30); Una riduzione come la precedente su [30, 40, 50]
Questi ritornano un'istanza di uno stream di valori
a.add(40);
primitivi, permettendo di chiamare metodi aggiuntivi che restituisce ancora 120
a.add(50);
Collections.sort(a, Comparator.reverseOrder()); possono essere utili (vedi sotto). Una riduzione come la precedente su [0] restituisce
// a sarà [50, 40, 30, 20] ancora 0
  Una riduzione come la precedente su [] restituisce
Optional.empty() invece di 0
flatMap
reduce
collect
Stream<U> flatMap(<funzione da T a
La T reduce(Identità, <funzione binaria>) è
Stream<U>>) prende in input una funzione Siamo ora interessati ad avere una Collezione (List, Set,
un'operazione di aggregazione che restituisce un singolo
(Function<T, Stream<U>>), la applica ad ogni valore a partire da uno stream. ...) invece che uno stream. Per fare ciò, possiamo usare
elemento di tipo T dello stream. La funzione restituisce il metodo Collection<T> collect(<funzione che
un altro stream (in generale contentente elementi di tipo restituisce una Collettore>). Nel nostro esempio,
Si ha come primo parametro un valore iniziale (identità),
diverso U) per ogni elemento. Infine tutti gli stream vogliamo ottenere una lista di stringhe contenente nome:
e come secondo parametro una funzione binaria che
restituiti dalla funzione vengono concatenati in un unico prende in input due elementi dello stream e restituisce età delle persone (filtrate con età >= 30):
stream.
un altro elemento (dello stesso tipo) ottenuto a partire
dai due. La funzione verrà applicata induttivamente ad List<String> lista = persone.stream()
Nel nostro esempio, vogliamo trasformare ogni persona ogni elemento dello stream, fino ad ottenere un solo .filter(p -> p.getEta() >= 30)
(sempre filtrata con età >= 30) in una lista di stringhe elemento. .map(p ->
contenenti il nome e l'età, e poi concatenare tutti gli p.getNome() + ": " +
stream in un unico stream: p.getEta().toString()
Nel nostro esempio, vogliamo ottenere la somma delle
);
età delle persone (filtrate): .collect(Collectors.toList());
Stream<String> stream = persone.stream()
.filter(p -> p.getEta() >= 30)
Integer sommaEta = persone.stream()
.flatMap(p -> { Esistono diversi tipi di collettori già definiti:
.filter(p -> p.getEta() >= 30)
List<String> listaPerP;
.mapToInt(p -> p.getEta())
listaPerP = new ArrayList<>();
listaPerP.add(p.getNome());
.reduce( toList() e toSet() per avere una lista o un set
0, toCollection(Supplier<C> collectionFactory)
listaPerP.add(p.getEta().toString());
(eta1, eta2) -> eta1 + eta2 ); per avere un altro tipo di Collection o per avere
return listaPerP.stream();
}); una specifica implementazione di List o Set
Otterrò un intero: 120. Nelle varie iterazioni nello stream joining()
contentente le età [30, 40, 50], la reduce si applica joining(CharSequence delimiter)
Otterrò uno stream, contenente le stringhe: "Luigi", "30",
in questo modo: joining(CharSequence delimiter,
"Pippo", "40", "Pluto", "50".
CharSequence prefix, CharSequence suffix)
per concatenare un insieme di stringhe in una stringa
Esistono anche metodi simili per flat-mappare a valori 0 (valore iniziale) + 30 (primo elemento dello stream)
unica
primitivi: = 30 (risultato parziale),
30 (risultato parziale della iterazione precedente) +
40 (secondo elemento dello stream) = 70,
IntStream flatMapToInt(<funzione da T a
70 + 50 = 120.
IntStream),
DoubleStream flatMapToDouble(<funzione da
Esiste in aggiunta anche un overload che non richiede
T a DoubleStream)
un'identità Optional<T> reduce(<funzione 19
binaria>). Questo permette di effettuare la medesima
Possiamo usare il metodo forEach(<funzione da T a Optional.empty() per creare un Optional vuoto
min e max void>) per eseguire una funzione per ogni elemento (sostituisce il null).
dello stream. Nel nostro esempio, vogliamo stampare il
nome e l'età di ogni persona (filtrata con età >= 30): Sono poi disponibili i metodi per lavorare con gli
Optional<T> max(Comparator<? super T> c) e
Optional:
Optional<T> min(Comparator<? super T> c)
restituiscono rispettivamente l'oggetto massimo o persone.stream()
minimo all'interno dello stream (se presente), secondo .filter(p -> p.getEta() >= 30) ifPresent(<funzione>) che applica la
l'ordine definito dal Comparator dato. .forEach(p -> funzione in input solo se l'Optional non è
System.out.println(p.getNome() + ": " vuoto.
+ p.getEta().toString()));
// Restituisce la persona più anziana, Pluto
orElse(<val>) che restituisce il valore
Optional<Persona> piuAnziano = Arrays.asList(
dell'Optional se non è vuoto, altrimenti
new Persona("Mario", 20), IntStream, DoubleStream e LongStream restituisce il valore val in input.
new Persona("Luigi", 30),
new Persona("Pippo", 40),
new Persona("Pluto", 50)) Esistono delle specializzazioni della classe Stream per i Optional<U> flatMap(<funzione da T a
.stream() valori primitivi, ottenibili mediante opportune chiamate a Optional<U>) restituisce un Optional<U>,
.max(Comparator.comparing(p -> p.getEta())); un normale stream con mapToInt, flatMapToInt, etc.
applicando la funzione in input solo se
l'Optional non è vuoto; altrimenti ritorna un
Queste interfacce definiscono metodi aggiuntivi per Optional vuoto.
anyMatch, allMatch e noneMatch facilitare le operazioni di aggregazione, quali:

boolean anyMatch(<predicato>), Optional<T> OptionalDouble average()


allMatch(<predicato>) e Optional<T> OptionalInt max()
noneMatch(<predicato>) restituiscono rispettivamente OptionalInt min()
se almeno uno, tutti o nessuno degli elementi dello int sum()
stream rispetta il predicato;
Per convenienza questi sono scritti solo per IntStream,
List<Persona> persone = Arrays.asList( ma sono uguali per gli altri, basta semplicemente
new Persona("Mario", 20), sostituire int con double/long e OptionalInt con
new Persona("Luigi", 30), OptionalDouble/OptionalLong
new Persona("Pippo", 40),
new Persona("Pluto", 50));
// Restituisce vero, Optional<T>, OptionalInt,
// Pippo e Pluto sono abbastanza anziani
boolean any = persone.stream() OptionalDouble e OptionalLong
.anyMatch(p -> p.getEta() > 35);
// Restituisce falso,
L'oggetto Optional è un contenitore per un valore che
// Mario e Luigi sono più giovani
boolean all = persone.stream() può essere nullo. È usato per evitare di avere eccezioni
.allMatch(p -> p.getEta() > 35); di tipo NullPointerException.
// Restituisce falso,
// Pippo e Pluto sono più anziani Esistono anche le specializzazioni per tipi primitivi
boolean all = persone.stream() OptionalInt, OptionalDouble e OptionalLong. Il loro
.noneMatch(p -> p.getEta() > 35); utilizzo è pressoché identico alla normale classe
Optional.
forEach
Possiamo creare un Optional con il valore val con il
20
metodo Optional.of(val). Oppure con il metodo
Viene detta closure la combinazione di una funzione stringa corrispondente a una rappresenatazione testuale
Facciamo un esempio usando gli Optional e i tre insieme a una referenza allo stato che la circonda. di un numero intero
metodi visti in precendenza:
Detto in maniera diversa, data una funzione lambda, si public static List<Integer> addX
dice che questa "si chiuda attorno" (ovvero catturi) le (List<String> nums, int x) {
Optional<String> opt = Optional.of("ciao"); variabili all'interno del suo scope di definizione, List<Integer> plusX = new LinkedList<>();
permettendo di utilizzarle all'interno della lambda stessa. for(String numString : nums) {
// Stampa "ciao"
opt.ifPresent(s -> System.out.println(s)); number = Integer.valueOf(numString);
Con un esempio: if (number>0)
// Stampa "ciao" perché opt non è vuoto plusX.add(number + x);
}
System.out.println(opt.orElse("vuoto"));
final String prefix = "Z"; return plusX;
Predicate<String> pred = s -> }
/* La funzione in flatMap viene applicata
s.startsWith(prefix);
e restituisce un Optional<String>
contenente "ciao mondo",
Si riscriva il metodo utilizzando i costrutti della
quindi viene stampato "ciao mondo"
per effetto della orElse */
la lambda pred cattura la variabile prefix, utilizzandola al programmazione funzionale di Java 8.
System.out.println( suo interno, prendendo quindi il nome di closure.
opt .flatMap( Soluzione
s -> Optional.of(s + " mondo")) Per evitare effetti collaterali (side effects), viene
.orElse("vuoto")); consentito di catturare solo variabili final oppure Il metodo addX non fa altro che prendere in input una
effectively final, ovvero non dichiarata final ma usata lista di stringhe nums e un intero x, e restituire una lista
come se fosse tale. di interi ottenuta sommando x a tutti gli interi positivi
Optional<String> opt2 = Optional.empty();
presenti in nums (gli interi <= 0 nella lista nums non
// Non stampa nulla perché opt2 è vuoto String prefix = "Z"; vengono aggiunti nella lista plusX).
opt2.ifPresent(s -> System.out.println(s)); // Altre istruzioni che fanno cose
String nonFinalPrefix = prefix + "aaaa";
// Stampa "vuoto" perché opt2 è vuoto Quindi possiamo:
System.out.println(nonFinalPrefix);
System.out.println(opt2.orElse("vuoto")); nonFinalPrefix = "";
// prefix non è dichiarata final, ma non è applicare il metodo stream() alla lista di stringhe per
/* La funzione in flatMap non viene applicata // mai modificata: è effectively final iniziare la nostra "catena di funzioni",
e restituisce un Optional<String> vuoto, Predicate<String> pred = s -> convertire ogni stringa della lista di partenza in un
quindi viene stampato "vuoto" s.startsWith(prefix); intero (ritornando poi lo stream di interi), tramite una
per effetto della orElse */ // Errore di compilazione: nonFinalPrefix é map con parametro parametro il metodo
System.out.println( // modificata, quindi non è effectively final
opt2.flatMap( Integer.valueOf(numString),
Predicate<String> pred2 = s ->
s -> Optional.of(s + " mondo")) s.startsWith(nonFinalPrefix);
filtrare gli interi positivi,
.orElse("vuoto")); sommare x a ciascuno di essi.

Otterremo in output: Esercizio sulla programmazione public static List<Integer> addX


funzionale (TdE del 2019-02-18, (List<String> nums, int x) {
return nums.stream()
ciao esercizio 4 - punto c) .map(numString ->
ciao Integer.valueOf(numString))
ciao mondo .filter(number -> number > 0)
vuoto Testo dell'esercizio .map(number -> number + x)
vuoto .collect(Collectors.toList());
Si consideri il seguente metodo statico, che ha la }
precondizione che ciascun elemento di nums è una 21
Closures
Nell'UML, la classe astratta Creator, che ha bisogno di }
creare un oggetto Product, delega la creazione ad una
Design pattern classe ConcreteCreator (che estende Creator). La public class PDFDocumentFactory
extends DocumentFactory {
classe ConcreteCreator implementa il metodo @Override
I design pattern sono dei modelli di soluzione a problemi factoryMethod() che crea un oggetto public Document createDocument() {
comuni di progettazione software. Esistono diversi tipi di ConcreteProduct (che estende Product). return new PDFDocument();
design pattern, ma in generale si possono suddividere in }
4 categorie: creazionali, strutturali, comportamentali e Esempio }
architetturali.
public class Main {
Supponiamo di voler creare un programma per creare /* Crea n documenti dello stesso tipo
Pattern creazionali dei documenti. Il programma deve essere in grado di e ne setta il titolo */
creare documenti di diversi tipi (documenti di testo, di private Document[] createNDocuments
calcolo, pdf, ...) decisi a runtime dall'utente. Il (int n, DocumentFactory factory) {
I design pattern creazionali sono quelli che si occupano programma non può prevedere che tipo di documento Document[] documents=new Documents[n];
di risolvere problemi relativi alla creazione di oggetti. l'utente deciderà di creare, quindi il Factory Method è la for (int i = 0; i < n; i++) {
soluzione migliore. documents[i]
Factory Method = factory.createDocument();
documents[i]
public abstract class Document { .setTitle(String.valueOf(i));
Il Factory Method permette di creare oggetti senza private String title; }
specificare la loro classe concreta. Questo pattern è
particolarmente utile quando abbiamo bisogno di public abstract void return documents;
istanziare uno o più oggetti astratti per poi effettuarvi setTitle(String title); }
delle operazioni che non dipendono dal loro tipo ...
concreto (uguale per tutti gli oggetti). Come è ben noto } public static void main(String[] args) {
non è possibile costruire un oggetto astratto. Per ovviare Document[] docs;
a questo problema si usa il Factory Method che public class TextDocument extends Document { DocumentFactory factory;
@Override
permette al Client di specificare al metodo che dovrà
public void setTitle(String title) { // Creazione di n documenti di testo
effettuare tali operazioni, solo la factory con cui costruire
this.title = title + ".txt"; factory = new TextDocumentFactory();
gli oggetti astratti, il metodo poi potrà lavorare con gli } docs = createNDocuments(10, factory);
oggetti astratti e non sarà necessario creare un metodo ...
diverso (che effettua le stesse operazioni) per ciascun } // Creazione di n documenti PDF
tipo concreto, solo per avere accesso al costruttore del factory = new PDFDocumentFactory();
tipo concreto in questione. public class PDFDocument extends Document { docs = createNDocuments(5, factory);
@Override }
UML public void setTitle(String title) { }
this.title = title + ".pdf";
}
... In questo modo, il metodo nel Main crea n documenti
} allo stesso modo, senza dover conoscere il tipo di
documento da creare, grazie al Factory Method che
public abstract class DocumentFactory { richiama il metodo createDocument(). Sarà il Client a
public abstract Document createDocument();
decidere quale factory inserire nel metodo
}
createNDocuments(). Senza il Factory Method, ci
public class TextDocumentFactory sarebbero stati diversi metodi createNDocuments() per
extends DocumentFactory { ogni tipo di documento.
@Override
public Document createDocument() {
return new TextDocument(); 22
}
// Creazione interfaccia grafica PM
public interface Button { /* ... */ }
factory = new PMGUIFactory();
Abstract Factory button = factory.createButton();
public interface ScrollBar { /* ... */ }
scrollBar = factory.createScrollBar();
A differenza del Factory Method, l'Abstract Factory public interface GUIFactory {
permette di creare famiglie di oggetti correlati senza // Creazione interfaccia grafica Motif
public Button createButton();
specificare la loro classe concreta. factory = new MotifGUIFactory();
public ScrollBar createScrollBar();
button = factory.createButton();
}
scrollBar = factory.createScrollBar();
UML }
public class PMButton
}
<<Interface>>
implements Button { /* ... */ }
«interface»
AbstractFactory importa Client importa
AbstractProductA
createProductA()
public class PMScrollBar
createProductB()
In questo modo, il Main crea solo la classe
implements ScrollBar { /* ... */ }
importa ProductA1 ProductA2 ConcreteFactory della famiglia richiesta dall'utente e
public class PMGUIFactory richiama i metodi generici createButton() e
ConcreteFactory1 ConcreteFactory2 implements GUIFactory { createScrollBar() per creare la famiglia di oggetti,
istanzia
createProductA()
createProductB()
createProductA()
createProductB()
@Override ma sarà la ConcreteFactory a preoccuparsi di creare
«interface»
AbstractProductB
public Button createButton() { gli oggetti della famiglia corretta.
istanzia return new PMButton();
}
ProductB1 ProductB2
istanzia
istanzia @Override
public ScrollBar createScrollBar() {
return new PMScrollBar();
}
In questo diagramma, si hanno 2 famiglie di oggetti:
}
AbstractProductA e AbstractProductB, e i rispettivi
prodotti concreti ProductA1, ProductA2, ProductB1 e public class MotifButton
ProductB2. La classe astratta AbstractFactory ha implements Button { /* ... */ }
bisogno di creare una famiglia di oggetti, e delega la
creazione a 2 classi concrete ConcreteFactory1 e public class MotifScrollBar
ConcreteFactory2. Le classi concrete implementano i implements ScrollBar { /* ... */ }
metodi createProductA() e createProductB() che
public class MotifGUIFactory
creano rispettivamente un oggetto ProductA1 o
implements GUIFactory {
ProductA2, e un oggetto ProductB1 o ProductB2. Il @Override
Client, che ha bisogno di creare una famiglia di oggetti, public Button createButton() {
crea una classe ConcreteFactory e usa i metodi return new MotifButton();
createProductA() e createProductB() per creare la }
famiglia di oggetti.
@Override
public ScrollBar createScrollBar() {
Esempio return new MotifScrollBar();
}
Supponiamo di implementare un insieme di classi }
dedicate alla creazione di interfacce grafiche. I diversi
elementi grafichi possono essere creati secondo stili public class Main {
diversi (PM e Motif). Per ogni elemento grafico viene public static void main(String[] args) {
creata una classe astratta, per ogni stile ed elemento GUIFactory factory;
grafico viene creata una classe concreta. Il Client deve Button button;
ScrollBar scrollBar; 23
essere in grado di modificare lo stile dell'interfaccia
grafica a runtime, senza dover modificare il codice.
Per prima cosa, definiamo l'interfaccia Target, in questo }
esempio la chiamiamo TextPrinter, che lavora con il }
Pattern strutturali tipo di file TextFile:
TextPrinter e FilePrinterLibrary lavorano con
I pattern strutturali sono quelli che si occupano di come
le classi e gli oggetti vengono composti per formare public class TextFile { oggetti diversi, quindi, per adattare l'interfaccia di
private String name; FilePrinterLibrary a quella di TextPrinter,
strutture più grandi.
private String path; creiamo un Adapter:
private int size;
Adapter
public TextFile public class FilePrinterLibraryAdapter
(String name, String path, int size) { implements TextPrinter {
L'Adapter è un pattern che permette di adattare FilePrinterLibrary filePrinterLibrary =
un'interfaccia ad un'altra. Supponiamo di voler this.name = name;
this.path = path; new FilePrinterLibrary();
implementare una feature e, per farlo, ci serviamo di una
this.size = size;
libreria esterna. La libreria, però, lavora su dati e oggetti } @Override
diversi da quelli con cui lavoriamo noi. In questo caso, public void printText(TextFile textFile) {
possiamo creare un Adapter che si occupa di adattare public String getPath() { File file=new File(textFile.getPath());
l'interfaccia della libreria esterna all'interfaccia che return path;
usiamo. } filePrinterLibrary.printFile(file);
}
UML ... }
}
Client Target Adaptee
public interface TextPrinter { FilePrinterLibraryAdapter definisce il metodo
adaptee
request() methodToAdapt()
public void printText(TextFile textFile); printText() per poter implementare l'interfaccia
} TextPrinter. Implementiamo il metodo printText()
Adapter
Extends
utilizzando un oggetto FilePrinterLibrary e creando
- adaptee: Adaptee
un oggetto File a partire da un oggetto TextFile,
Definiamo anche l'interfaccia Adaptee (la libreria
+ request() this.adaptee.methodToAdapt()
quindi possiamo chiamare il metodo printFile() di
esterna), in questo esempio la chiamiamo
FilePrinterLibrary.
FilePrinterLibrary, che userà un tipo di file diverso
In questo diagramma, abbiamo Target che definisce da TextFile (File):
l'interfaccia che il Client usa, Adaptee che definisce public class Main {
l'interfaccia che vogliamo adattare (ad esempio una public static void main(String[] args) {
public class File { TextFile textFile = new TextFile(
libreria esterna), e Adapter, che estende Target, e usa private String path;
Adaptee per adattare il methodo methodToAdapt() al "test.txt", "/home/user", 100
);
metodo request() di Target. public File(String path) {
this.path = path; TextPrinter textPrinter =
Esempio } new FilePrinterLibraryAdapter();
public String getPath() { textPrinter.printText(textFile);
Supponiamo di voler implementare un programma che return path;
legge un file di testo e lo stampa a video. Per farlo, }
} }
usiamo una libreria esterna, che però lavora su oggetti
diversi da quelli che usiamo noi. In questo caso, ...
possiamo creare un Adapter che si occupa di adattare } In questo modo, il Main usa il tipo di file TextFile, sarà
l'interfaccia della libreria esterna all'interfaccia che poi l'Adapter che si occuperà di convertirlo in File per
usiamo. public class FilePrinterLibrary {
public void printFile(File file) { richiamare la funzione di libreria.
System.out.println( 24
"Printing file " + file.getPath());
modo semplice, in grassetto, ma anche cambiando il colore del testo, o tutti e due
Decorator insieme. Per farlo, usiamo un Decorator.

Il Decorator è un pattern che permette di aggiungere dinamicamente una o più Per prima cosa, definiamo l'interfaccia Component, in questo esempio la chiamiamo
funzionalità ad un sistema. TextView, che definisce il metodo draw():

UML public interface TextView {


public void draw();
}
Component

+ operation() Definiamo anche la classe ConcreteComponent, in questo esempio la chiamiamo


SimpleTextView, che implementa TextView (e stamperà semplicemente il testo che gli
viene passato):

Extends Extends
public class SimpleTextView implements TextView {
private String text;
ConcreteComponent Decorator
component
public SimpleTextView(String text) {
+ operation() - component this.text = text;
}
+ operation()
@Override
public void draw() {
System.out.println(text);
Extends Extends }
}
ConcreteDecoratorA ConcreteDecoratorB
Definiamo la classe astratta Decorator, in questo esempio la chiamiamo
+ operation() + operation()
TextViewDecorator, che estende TextView e usa TextView per aggiungere
addBehavior addBehavior
funzionalità:

In questo diagramma, abbiamo Component che definisce l'interfaccia che il Client usa, public abstract class TextViewDecorator implements TextView {
private TextView textView;
ConcreteComponent che implementa Component, e Decorator che estende Component
e usa Component per aggiungervi funzionalità, vi sono poi i vari ConcreteDecorator che public TextViewDecorator(TextView textView) {
estendono Decorator e realizzano (ciascuno) una funzionalità. this.textView = textView;
}
Si noti che l'attributo component di Decorator è di tipo Component, e non deve essere
@Override
per forza un ConcreteComponent, ma anche un altro Decorator (ConcreteDecoratorA
public void draw() {
o ConcreteDecoratorB), che a sua volta avrà di nuovo un component. Si può quindi textView.draw();
aggiungere quante funzionalità si vogliono al ConcreteComponent base. }
}
Esempio
L'attributo textView di TextViewDecorator è di tipo TextView, e (come detto prima)
Supponiamo di voler costruire un programma che gestisce un'interfaccia per la 25
può essere sia un SimpleTextView che un altro ConcreteDecorator.
visualizzazione di un testo. Il programma deve essere in grado di visualizzare il testo in
new SimpleTextView("Hello World!"), "\u001B[31m");
coloredTextView.draw();
Definiamo le classi ConcreteDecoratorA, in questo esempio la chiamiamo
ColoredTextView; e ConcreteDecoratorB, in questo esempio la chiamiamo // Stampa "Hello World!" in grassetto
BoldTextView, che estendono TextViewDecorator: TextView boldTextView = new BoldTextView(
new SimpleTextView("Hello World!"));
boldTextView.draw();
public class ColoredTextView extends TextViewDecorator {
private String color; // Stampa "Hello World!" in rosso e in grassetto
TextView coloredBoldTextView = new ColoredTextView(
public ColoredTextView(TextView textView, String color) { new BoldTextView(
super(textView); new SimpleTextView("Hello World!")
this.color = color; ),
} "\u001B[31m"
);
// Stampa il testo colorato coloredBoldTextView.draw();
@Override }
public void draw() { }
System.out.print(color);
super.draw();
System.out.print("\u001B[0m"); Con questa catena di Decorator è possibile aggiungere dinamicamente più funzionalità
}
al SimpleTextView base.
}

public class BoldTextView extends TextViewDecorator {


public BoldTextView(TextView textView) {
super(textView);
}

// Stampa il testo in grassetto


@Override
public void draw() {
System.out.print("\u001B[1m");
super.draw();
System.out.print("\u001B[0m");
}
}

Si noti che ColoredTextView e BoldTextView hanno nel loro costruttore l'istruzione


super(textView), che chiama il costruttore di TextViewDecorator per settare
l'attributo textView (che può essere sia un SimpleTextView che un ColoredTextView
o un BoldTextView).

public class Main {


public static void main(String[] args) {
// Stampa "Hello World!" normale
TextView textView = new SimpleTextView("Hello World!");
textView.draw();

// Stampa "Hello World!" in rosso 26


TextView coloredTextView = new ColoredTextView(
Definiamo le varie classi ConcreteStrategy, in questo public class Cassa {
esempio le chiamiamo EffettoCassaCurativa,
Pattern comportamentali EffettoCassaAvvelenata, ecc., che implementano
Runnable effettoCassa;

EffettoCassa: public Cassa(Runnable effettoCassa) {


Strategy this.effettoCassa = effettoCassa;
}
public class EffettoCassaCurativa
Strategy è un pattern che permette di cambiare implements EffettoCassa { public void apri() {
dinamicamente il comportamento di un sistema. @Override effettoCassa.run();
public void applicaEffetto() { }
UML System.out.println("Sei stato curato!"); }
}
Context Strategy
}
strategy Per il nostro uso, definiamo una Runnable, che è
contextInterface() algorithmInterface()
un'interfaccia funzionale che ha un solo metodo: run()
public class EffettoCassaAvvelenata
per eseguire il codice. A seconda dei tipi dei parametri e
implements EffettoCassa {
Extends
Extends
Extends @Override
del tipo del valore restituito, possiamo usare anche le
ConcreteStrategyA ConcreteStrategyB ConcreteStrategyC public void applicaEffetto() { altre interfacce funzionali spiegate nel capitolo
System.out.println("Sei avvelenato!"); "Programmazione funzionale". Il Main può quindi definire
algorithmInterface() algorithmInterface() algorithmInterface()
} le Runnable concrete tramite espressioni lambda e,
} richiamando il costruttore di Cassa, assegnarle a quella
generica per eseguire il giusto comportamento:
In questo diagramma, troviamo Strategy che definisce
un comportamento che le varie ConcreteStrategy Definiamo la classe Context, in questo esempio la
implementano, ognuno in modo diverso. Context usa chiamiamo Cassa, che usa un EffettoCassa generico e public class Main {
una Strategy generica, che verrà assegnata chiama il metodo apri(): public static void main(String[] args) {
// Crea una cassa curativa
dinamicamente dal client attraverso una
Cassa cassaCurativa =
ConcreteStrategy. Si noti che Strategy e
public class Cassa { new Cassa(() -> {
ConcreteStrategy sono classi che hanno un solo System.out.println("Sei stato curato!");
private EffettoCassa effettoCassa;
metodo, sono quindi interfacce funzionali. Possiamo });
quindi usare le espressioni lambda per definire le public Cassa(EffettoCassa effettoCassa) { /* equivalente a:
ConcreteStrategy senza doverle creare this.effettoCassa = effettoCassa; Runnable effettoCassaCurativa = () -> {
esplicitamente. } System.out.println("Sei stato curato!");
};
public void apri() { Cassa cassaCurativa =
Esempio (senza interfacce funzionali)
effettoCassa.applicaEffetto(); new Cassa(effettoCassaCurativa);
} */
Supponiamo di voler gestire un sistema di apertura di }
casse in un gioco. Le casse possono dare, una volta // Crea una cassa avvelenata
aperte, degli effetti al giocatore che le apre; ad esempio, Cassa cassaAvvelenata =
una cassa può curare il giocatore, un'altra può Il Main poi assegnerà una ConcreteStrategy new Cassa(() -> {
avvelenarlo, ecc. Usiamo uno Strategy. Per prima cosa, (EffettoCassaCurativa, EffettoCassaAvvelenata, System.out.println("Sei avvelenato!");
definiamo l'interfaccia Strategy, in questo esempio la ecc.) a quella generica (EffettoCassa) nel costruttore });
chiamiamo EffettoCassa, che definisce il metodo di Cassa.
// Apri le casse curativa e avvelenata
applicaEffetto():
cassaCurativa.apri();
Esempio (con interfacce funzionali) cassaAvvelenata.apri();
}
public interface EffettoCassa {
}
public void applicaEffetto(); Usando le interfacce funzionali, possiamo usare una
} sola classe Cassa: 27
VisualizzazioneGUI, ecc., che implementano : visualizzazioni) {
Visualizzazione: visualizzazione.aggiorna();
Observer }
}
Observer è un pattern che permette di notificare ad un public class VisualizzazioneConsole }
oggetto quando un altro oggetto cambia. implements Visualizzazione {
@Override
public void aggiorna() { Si noti che nei metodi per aggiungere o rimuovere una
UML
System.out.println( Visualizzazione, abbiamo l'interfaccia
"Aggiornamento della Visualizzazione come tipo di parametro. Questo ci
visualizzazione console"); permette di aggiungere dinamicamente qualsiasi tipo di
} Visualizzazione che implementa l'interfaccia
}
Visualizzazione.

public class VisualizzazioneGUI


implements Visualizzazione {
@Override
public void aggiorna() {
System.out.println(
"Aggiornamento della
In questo diagramma, troviamo Subject che è l'oggetto visualizzazione GUI");
}
che cambia e notifica gli Observer; gli Observer che
}
ricevono la notifica e si comportano conseguentemente.
Infine abbiamo i ConcreteObserver che implementano
Observer e definiscono il comportamento che si attiva Definiamo la classe Subject, in questo esempio la
quando Subject cambia. Inoltre Subject può essere chiamiamo Database, che ha un ArrayList di
astratta ed avere una ConcreteSubject che la estende. Visualizzazione e un metodo
aggiornaVisualizzazioni() che chiama il metodo
Esempio aggiorna() di ogni Visualizzazione:

Supponiamo di voler costruire un sistema di gestione di public class Database {


dati di un'azienda. L'azienda ha un database che private ArrayList<Visualizzazione>
contiene i dati dei dipendenti, e un sistema che permette visualizzazioni;
di visualizzare questi dati. Il sistema di visualizzazione
deve essere aggiornato ogni volta che il database public Database() {
cambia. Usiamo un Observer. visualizzazioni = new ArrayList<>();
}
Per prima cosa, definiamo l'interfaccia Observer, in
public void aggiungiVisualizzazione
questo esempio la chiamiamo Visualizzazione, che
(Visualizzazione visualizzazione) {
definisce il metodo aggiorna(): visualizzazioni.add(visualizzazione);
}
public interface Visualizzazione {
public void rimuoviVisualizzazione
public void aggiorna();
(Visualizzazione visualizzazione) {
}
visualizzazioni.remove(visualizzazione);
}
Definiamo le varie classi ConcreteObserver, in questo
protected void aggiornaVisualizzazioni() {
esempio le chiamiamo VisualizzazioneConsole, 28
for(Visualizzazione visualizzazione
public void transition(TCPState state) { }
connection.setState(state);
State } public void open() {
} state.open();
State è un pattern che permette di cambiare il }
comportamento di un oggetto in base allo stato in cui si public void close() {
trova. È molto simile a Strategy. Definiamo le varie classi ConcreteState, in questo state.close();
esempio le chiamiamo TCPListen, ecc., che estendono }
TCPState: public void acknowledge() {
UML state.acknowledge();
}
Context State public class TCPListen extends TCPState {
public TCPListen(TCPConnection connection) { public void setState(TCPState newState) {
+request() +handle() super(connection); this.state = newState;
} }
}
@Override
state.handle() ConcreteStateA ConcreteStateB
public void open() {
+handle() +handle() // open connection
this.transition(
new TCPEstablished(connection));
In questo diagramma, troviamo State che è l'interfaccia }
che definisce uno stato generico, ConcreteState che @Override
estende State e definisce il comportamento in quello public void close() { ... }
@Override
specifico stato e Context che è l'oggetto che cambia public void acknowledge() { ... }
stato. }

Esempio
Osserviamo la differenza fondamentale tra Strategy e
State. Alla fine dell'esecuzione del metodo open, avviene
Supponiamo di voler implementare il TCP. La sequenza
una transizione dello stato da TCPListen a
di azioni da compiere è rappresentabile con un automa a
TCPEstablished. Infatti sia in Strategy che in State il
stati finiti. La classe TCPConnection deve gestire
l'apertura della connessione, la chiusura e le varie fasi comportamento della classe cambia a runtime, la
intermedie. Usiamo uno State. differenza tra i due è che nel primo caso è l'utente che
decide quando cambiare il comportamento, nel secondo
caso il comportamento cambia perchè avviene una
Definiamo la classe astratta State, in questo esempio la transizione di stato che è completamente trasparente
chiamiamo TCPState, che definisce i metodi open(), all'utente della classe e dipende solo dallo stato di
close(), acknowledge() e transition(): partenza e dal metodo invocato.

public abstract class TCPState { Definiamo la classe Context, in questo esempio la


protected TCPConnection connection; chiamiamo TCPConnection, che ha un campo state di
tipo TCPState e i metodi open(), close(),
public TCPState(TCPConnection connection) { acknowledge() e setState():
this.connection = connection;
}
public class TCPConnection {
public abstract void open(); private TCPState state;
public abstract void close();
public abstract void acknowledge(); public TCPConnection() { 29
state = new TCPListen(this);
Definiamo la classe PasteCommand che implementa Command:
Command
class PasteCommand implements Command {
Il pattern Command permette di isolare la porzione di codice che effettua un'azione private Document doc;
(eventualmente molto complessa) dal codice che ne richiede l'esecuzione; l'azione è
incapsulata nell'oggetto Command. L'obiettivo è rendere variabile l'azione del client senza public PasteCommand(Document doc) {
però conoscere i dettagli dell'operazione stessa. Altro aspetto importante è che il this.doc = doc;
destinatario della richiesta può non essere deciso staticamente all'atto dell'istanziazione }
del command ma ricavato a runtime.
@Override
public void esegui() {
UML ...
}
}

Definiamo la classe Menu che contiene un insieme di MenuItem:

class Menu {
List<MenuItem> items = new ArrayList<>();

public void addMenuItem(MenuItem item) {


...
}
}

Definiamo la classe MenuItem che contiene un Command:

class MenuItem {
Command command;
La classe Invoker non implementa la richiesta direttamente ma la delega ad un oggetto public void setCommand(Command command) {
Command che la esegue. Il ConcreteCommand implementa Command e performa l'azione this.command = command;
sul Receiver. }

Esempio public void click() {


command.esegui();
}
Supponiamo di creare una classe Menu per eseguire operazioni all'interno di una }
applicazione. Un'istanza di Menu contiene un insieme di MenuItem, uno per ciascuna
azione da eseguire. Una istanza di Menu non deve conoscere i dettagli dell'applicazione
associata, né delle azioni associate ai MenuItem. Usiamo il pattern Command. Si noti che l'azione da compiere è slegata dagli oggetti che la invocano.

Definiamo l'interfaccia Command che definisce il metodo esegui():

interface Command {
void esegui();
} 30
riportato il CFG della funzione sqrt (esempio Per esempio, consideriamo la seguente funzione:
precedente):
Testing void p(int x, int y) {
if(x == 0 || y > 0)
y = y/x;
Testing strutturale (white-box) else
y = (-y)/x;
if: n<0 }
Il testing strutturale tiene conto della struttura interna else: n>=0
del programma con l'obiettivo di sollecitarne tutte le parti.
if
Si può entrare nel ramo if nei seguenti modi: x = 0, y
Criterio di copertura delle istruzioni qualsiasi e x != 0, y > 0
(statement coverage)
e nel ramo else: x != 0 e y <= 0.
Si vuole fare in modo che ogni istruzione del programma
z <= n
venga eseguita almeno una volta. Consideriamo la
seguente funzione: while z>n
Sono quindi sufficienti 3 casi di test: x = 0, y = -1, x
= 1, y = 1 e x = 1, y = -1.

int sqrt(int n) {
int i = 1, z = 1;
if(n < 0)
return -1;
while(z <= n) {
i = i + 1;
z = i * i; Ogni arco rappresenta una possibile diramazione. Si noti
} che ogni if e while è rappresentato da due archi, uno
return i - 1; per il caso true e uno per il caso false (anche se ad
} esempio il ramo else non è esplicitamente presente nel
codice).
Dobbiamo selezionare dei casi di test in modo che ogni
Il criterio edge coverage richiede che ogni arco del CFG
istruzione venga eseguita almeno una volta. possiamo
venga eseguito almeno una volta. Per fare ciò possiamo
scegliere i seguenti casi di test:
scegliere i seguenti casi di test:
n = -1 per eseguire il corpo dell'if
n = -1 per eseguire solo l'arco n < 0 (e giungere
n = 1 per eseguire il corpo del while
alla terminazione del programma)
n = 1 per eseguire il ramo else del primo if e per
Osservazione: se una funzione ha n return allora entrare nel while (e quindi eseguire tutti e due gli
avremo per forza almeno n casi di test. archi del while)

Criterio di copertura delle decisioni (edge Criterio di copertura delle decisioni e delle
coverage) condizioni (edge and condition coverage)
Si vuole coprire tutte le possibili diramazioni del L’insieme dei casi di test deve essere definito in modo
programma. Per fare ciò è utile costruire il control flow che ogni ramo del CFG venga attraversato almeno una
graph (o CFG) del programma in cui si evidenziono volta e sollecitando tutti i valori di verità delle
proprio tutte le diramazioni. Nella figura seguente è sottoespressioni che compaiono nelle condizioni 31
composte, tenendo conto della short circuit evaluation.
x <= 0, y <= 0 prendendo il secondo ramo else Domanda c)
Criterio di copertura dei cammini (path esterno e il secondo ramo else interno al ramo else
coverage) Definire un insieme minimo di casi di test per coprire tutti
Esercizio sul testing (TdE del 2020- i cammini (path coverage)
L’insieme dei casi di test deve garantire che ogni
possibile cammino (o percorso) che porti dal nodo
01-16, esercizio 4) Soluzione
iniziale al nodo finale del CFG sia attraversato almeno
una volta. Si consideri il seguente frammento di codice: Quindi, il corpo del for può essere eseguito 1 volta (se
si entra direttamente nell'if e si esegue il break) o 2
Attenzione: il numero di volte in cui si esegue un ciclo int f(int a, int b) { volte (se si entra nell'else, per poi eseguire break la
(un autoanello nel CFG) è rilevante. Perciò, se un ciclo if(a > b) { seconda iterazione). Per cui abbiamo 4 possibili cammini
può essere eseguito "infinite" volte, allora il criterio di System.out.println("A"); (quindi 4 casi di test):
copertura dei cammini non è applicabile. Ad esempio } else {
considerando la funzione di prima int sqrt(int n), n System.out.println("B"); a = 1, b = 0 per eseguire il primo if e il corpo del
può essere grande "quanto vogliamo" (in realtà non è }
for(int i = 0; i < 1000; i++) { for una volta
così, dato che è un intero a 32 bit, ma supponiamo che a = 1, b = 2 per eseguire il primo else e il corpo
sia così), pertanto il ciclo while può essere eseguito if(a > 0) break;
else a = 1; del for una volta
"infinite volte". Il criterio non è quindi applicabile.
} a = -1, b = 0 per eseguire il primo else e il corpo
} del for due volte
Consideriamo, per esempio, la seguente funzione: a = -1, b = -2 per eseguire il primo if e il corpo
del for due volte
int f(int x, int y) {
Domanda a)
int z = 0;
if(x > 0) { Definire un insieme minimo di casi di test per coprire
if(y > 0) { tutte le istruzioni (statement coverage)
z = x + y;
} else { Soluzione
z = x - y;
}
} else { Sono sufficienti 2 casi di test:
if(y > 0) {
z = x * y; a = 1, b = 0 per eseguire il ramo if del primo if
} else { e il break interno al for
z = x / y;
a = 0, b = 1 per eseguire il ramo else del primo
}
} if e sia a=1 che il break nel for
return z;
} Domanda b)

I possibili cammini sono 4: Definire un insieme minimo di casi di test per coprire
tutte le decisioni (edge coverage)
x > 0, y > 0 prendendo il primo ramo dell'if
esterno e il primo ramo dell'if interno al ramo if Soluzione
x > 0, y <= 0 prendendo il primo ramo dell'if
esterno e il secondo ramo else interno al ramo if Si noti che il ciclo for può essere eseguito al massimo 2
x <= 0, y > 0 prendendo il secondo ramo else volte e poi il break lo interrompe. Non è quindi possibile
esterno e il primo ramo dell'if interno al ramo else coprire la decisione i<1000. Tutte le altre decisioni sono 32
coperte dai 2 casi precedenti.
UML

33

Potrebbero piacerti anche