Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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 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;
... 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>.
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:
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:
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():
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.
}
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.
class Menu {
List<MenuItem> items = new ArrayList<>();
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. }
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