Università degli Studi Mediterranea di Reggio Calabria
Dipartimento di Ingegneria dell’Informazione, delle Infrastrutture e
dell’Energia Sostenibile
Corso di Laurea in Ingegneria dell’Informazione
Tesi di Laurea
Riuscirà Java a non farsi “rottamare” da Go?
Relatore
Candidato
Prof. Domenico Ursino
Francesco Surace
Anno Accademico 2015-2016
Indice
Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
Caratteristiche generali di Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1 Storia del linguaggio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Caratteristiche principali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 La programmazione orientata ad oggetti . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.1 Classi, oggetti e metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3.2 Incapsulamento, ereditarietà e polimorfismo . . . . . . . . . . . . . . .
1.4 La piattaforma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.1 Java Virtual Machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.2 La documentazione API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
7
8
9
10
11
12
12
15
Caratteristiche generali di Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1 Storia del linguaggio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Caratteristiche principali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 La compilazione e le librerie . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.2 Go è un linguaggio OOP? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 La sintassi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.1 Case sensitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.2 Le variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.3 I punti e virgola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.4 Strutture di controllo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
17
19
20
20
23
23
24
25
25
Java vs Go: gestione della concorrenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1 Presentazione del case study . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.1 Descrizione del contesto di riferimento . . . . . . . . . . . . . . . . . . . .
3.1.2 Diagramma dei casi d’uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.3 Progettazione concettuale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.4 Progettazione della componente applicativa . . . . . . . . . . . . . . . .
3.2 La programmazione concorrente in Java . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.1 Introduzione ai thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.2 Implementazione del case study . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3 La programmazione concorrente in Go . . . . . . . . . . . . . . . . . . . . . . . . . .
27
27
27
28
29
31
36
36
39
43
IV
Indice
3.3.1 Goroutine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3.2 Channel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3.3 Implementazione del codice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
45
46
49
Java vs Go: la gestione delle funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1 Il concetto di sottoprogramma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Gestione delle funzioni in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 I metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.2 Polimorfismo per metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3 Gestione delle funzioni in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.1 Le funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.2 Le funzioni anonime . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.3 Il return multiplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
51
51
52
54
58
58
60
60
62
Java Vs Go: la gestione delle interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1 La gestione delle interfacce in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.1 Le interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.2 L’ereditarietà multipla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.3 Classi astratte ed interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2 La gestione delle interfacce in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.1 Le interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.2 Strutture, puntatori e interfacce . . . . . . . . . . . . . . . . . . . . . . . . . .
5.3 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
63
63
63
67
71
72
72
74
76
Java vs Go: Riuso del software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.1 Il concetto di riuso di software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2 Riuso del software in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2.1 Ereditarietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2.2 Valutazioni sul riuso del software . . . . . . . . . . . . . . . . . . . . . . . . .
6.3 Riuso del software in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.3.1 Embedding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.3.2 Valutazioni sul riuso del software . . . . . . . . . . . . . . . . . . . . . . . . .
6.4 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
79
79
80
80
85
86
86
89
90
Conclusioni e uno sguardo sul futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
Ringraziamenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Riferimenti bibliografici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Elenco delle figure
1.1
1.2
1.3
1.4
1.5
1.6
Logo di Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Risultato del Listato 1.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Funzionamento della Java Virtual Machine . . . . . . . . . . . . . . . . . . . . . . .
Schematizzazione della Java Virtual Machine . . . . . . . . . . . . . . . . . . . . .
Documentazione API: lista dei package . . . . . . . . . . . . . . . . . . . . . . . . . . .
Documentazione API: contenuto dei package . . . . . . . . . . . . . . . . . . . . . .
8
11
13
14
15
16
2.1
2.2
2.3
2.4
Interesse per Golang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interesse per Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Risultato del Listato 2.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Risultato del Listato 2.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
18
19
22
22
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
3.10
3.11
3.12
Specifiche tecniche del calcolatore utilizzato per l’analisi . . . . . . . . . . . .
Diagramma dei casi d’uso per il nostro test . . . . . . . . . . . . . . . . . . . . . . .
Il modello Entità-Relazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Dizionario delle Entità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Dizionario delle Relazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Diagramma delle classi relativo al test di nostro interesse . . . . . . . . . . .
Deposito: Diagramma di sequenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ritiro: Diagramma di sequenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Bonifico: Diagramma di sequenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esecuzione del programma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Risultato del Listato 3.12 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Risultato del Listato 3.17. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
29
30
31
31
32
33
34
35
43
45
48
4.1
4.2
4.3
Risultato ottenuto dal Listato 4.4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Polimorfismo di tipo overload per i metodi println() . . . . . . . . . . . . . 56
Risultato del metodo in override . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.1
5.2
Risultato ottenuto dall’esecuzione del programma del Listato 5.8 . . . . 67
Risultato ottenuto dall’esecuzione del programma del Listato 5.18 . . . 71
6.1
6.2
Esempio di gerarchizzazione errata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
Esempio di gerarchizzazione corretta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
VI
Elenco delle figure
6.3
6.4
Risultato del programma riportato nel Listato 6.7 . . . . . . . . . . . . . . . . . 88
Risultato del programma riportato nel Listato 6.8 . . . . . . . . . . . . . . . . . 89
Elenco dei listati
1.1
1.2
1.3
2.1
2.2
2.3
2.4
2.5
2.6
2.7
2.8
2.9
2.10
2.11
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
3.10
3.11
3.12
3.13
3.14
3.15
3.16
3.17
3.18
3.19
Creazione di una classe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Creazione di un oggetto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di importazione di una libreria . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di una struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La funzione new . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Istanziazione consueta di una struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Struct e func . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di case sensitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Operatore di assegnazione diretta in Go . . . . . . . . . . . . . . . . . . . . . . . . . .
Variabile non inizializzata in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Assegnazione esplicita di un tipo ad una variabile in Go . . . . . . . . . . . .
Variabile non inizializzata in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Istruzione if-else in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ciclo for in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Metodo start() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Metodi per la gestione del flusso di un thread . . . . . . . . . . . . . . . . . . . . .
Il metodo synchronized . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di utilizzo dell’interfaccia Runnable . . . . . . . . . . . . . . . . . . . . . .
La classe ReadRun per la lettura del saldo . . . . . . . . . . . . . . . . . . . . . . . .
La classe DepositRun per la gestione del deposito . . . . . . . . . . . . . . . . .
La classe WithdrawRun per la gestione dei prelievi . . . . . . . . . . . . . . . . .
La classe TransferRun per la gestione dei bonifici . . . . . . . . . . . . . . . . .
La classe CurrentAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La classe Main() per l’avvio di tutte le attività . . . . . . . . . . . . . . . . . . . .
Esempio di goroutine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di canale sincrono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di canale asincrono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Gestore di un canale sincrono . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Direzione del canale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Implementazione del codice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Algoritmo per il calcolo del tempo di esecuzione in Java . . . . . . . . . . . .
Algoritmo per il calcolo del tempo di esecuzione in Go . . . . . . . . . . . . .
10
10
16
21
21
21
23
23
24
24
24
24
25
25
36
37
37
38
39
39
40
40
40
41
41
44
46
46
46
46
47
49
49
VIII
Elenco dei listati
4.1
4.2
4.3
4.4
4.5
4.6
4.7
4.8
4.9
4.10
4.11
4.12
4.13
4.14
4.15
4.16
4.17
5.1
5.2
5.3
5.4
5.5
5.6
5.7
5.8
5.9
5.10
5.11
5.12
5.13
5.14
5.15
5.16
5.17
5.18
5.19
5.20
5.21
5.22
5.23
5.24
5.25
5.26
5.27
6.1
6.2
6.3
Classe CurrentAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Definizione del metodo Withdraw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Implementazione del metodo Withdraw . . . . . . . . . . . . . . . . . . . . . . . . . . .
Invocazione del metodo Withdraw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
CurrentAccount senza polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di polimorfismo di tipo overload . . . . . . . . . . . . . . . . . . . . . . . . .
Classe InterestBearginAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Metodi ereditati dalla superclasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Override dei metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Override: main() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Definizione e implementazione di una func . . . . . . . . . . . . . . . . . . . . . . . .
Modalità con cui si può richiamare una funzione in Go . . . . . . . . . . . . .
Struct “collegata” a una func . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Valori e puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Variabile e funzione anonima . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Massimo e minimo in Go, return multipli . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di massimo e minimo in Java, return singolo . . . . . . . . . . . . . .
Esempio di metodo astratto in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Dichiarazione di interfaccia in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Implementazione nella classe Company . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Implementazione nella classe Private . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interfaccia Sortable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Classe CurrentAccount . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Classe SortingAlgorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Il main() del programma che si occupa dell’ordinamento . . . . . . . . . . .
Interfaccia che estende altre interfacce in Java . . . . . . . . . . . . . . . . . . . . .
Interfaccia CanRead . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interfaccia CanDeposit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interfaccia CanWithdraw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interfaccia CanTransfer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interfaccia CanInterest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Classe CurrentAccount che implementa le interfacce . . . . . . . . . . . . . . .
Classe InterestBearginAccount che estende CurrentAccount . . . . . .
Classe SubordinatedBond che implementa le interfacce . . . . . . . . . . . . .
Metodo main() che implementa le interfacce . . . . . . . . . . . . . . . . . . . . . .
Esempio di classe astratta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di Interfaccia in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Implementazione di una interfaccia in Go . . . . . . . . . . . . . . . . . . . . . . . . .
Istruzione equivalente in Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Istruzione equivalente in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interfaccia vuota . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Interfaccia vuota e puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Implementazione di una interfaccia in Go . . . . . . . . . . . . . . . . . . . . . . . . .
type e interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Superclasse Customer in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Sottoclasse Private . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Sottoclasse Company . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52
52
53
53
54
55
56
56
57
58
58
59
59
60
60
61
61
63
64
64
64
65
65
66
66
67
68
68
68
68
68
68
69
70
70
71
72
73
73
73
73
74
75
75
82
82
83
Elenco dei listati
6.4
6.5
6.6
6.7
6.8
6.9
Metodi della classe Object in override . . . . . . . . . . . . . . . . . . . . . . . . . . .
Esempio di embedding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Instaziazione di struct embedding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Dati principali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Embedding e interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Composizione in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
84
86
87
87
88
90
Introduzione
Le ragioni principali che hanno spinto l’umanità al progresso tecnologico sono state
la facilitazione e l’alleggerimento del lavoro in modo da poter aumentare l’efficienza
produttiva.
L’invenzione del calcolatore non si sottrae a tale principio; pensato in origine
per semplificare l’esecuzione di complicate operazione numeriche, è ormai in grado
di rispondere, se opportunamente orientato, alla maggior parte di problemi che si
possono porre davanti attraverso l’uso di un linguaggio di programmazione.
Lady Ada Lovelace realizzò, nel lontano 1837, quello che può essere definito
come il primo linguaggio di programmazione; sviluppato per la macchina analitica
di Charles Babbage, il linguaggio di Lovelace introdusse i concetti di ciclo ripetuto
e di variabile indicizzata. Tuttavia, tale linguaggio non mostrò le sue innovazioni in
ambito tecnologico poichè la macchina analitica su cui doveva “girare” non venne
mai portata a termine.
A rigore, i primi linguaggi di programmazione presero vita più di un secolo dopo;
essi erano scritti in short code, da cui prese ispirazione l’odierno assembly. La sola
struttura di controllo implementata nei primi linguaggi è il salto condizionato.
La maggior parte dei linguaggi di programmazione successivi cercarono di
“astrarsi” dal linguaggio di macchina, ottenendo le potenzialità di rappresentare
strutture di controllo e di dati più generali in modo da avvicinarsi a un linguaggio
a più alto livello di “astrazione”, e quindi al linguaggio “umano”.
Nasce, allora, l’esigenza di definire un insieme di procedure in modo da veicolare
la percezione che ha il programmatore nella stesura del linguaggio; nasce, cioè,
l’esigenza di stabilire un paradigma di programmazione.
Ogni linguaggio di programmazione è riconducibile ad almeno un particolare
paradigma di programmazione; alcuni tra i principali paradigmi sono:
•
•
•
il paradigma della programmazione modulare: si prefigge lo sviluppo di programmi raggrupati in moduli, tali per cui ogni modulo abbia una sua precisa
funzione;
il paradigma della programmazione funzionale: il flusso d’esecuzione è costituito
da una serie di funzioni che ricordano molto quelle matematiche;
il paradigma della programmazione procedurale: impone la creazione di blocchi
di codice individuati da un identificativo e definiti attraverso dei deliminatori;
tali blocchi di codice sono anche conosciuti come sottoprogrammi;
4
Introduzione
• il paradigma della programmazione strutturata: nato nell’ambito della programmazione procedurale, propone strutture di dati e di controllo invocabili non solo
con i salti incodizionati;
• il paradigma della programmazione orientata agli oggetti (Object Oriented Programming - OOP): migliora il paradigma della programmazione strutturata
attraverso la definizione di oggetti in grado di interagire fra loro.
In particolare, consideriamo il paradigma della programmazione orientata agli
oggetti; esso prevede di raggruppare le funzionalità e gli attributi che interessano una
particolare porzione di codice in un modello “astratto” chiamato classe. All’interno
di una classe possiamo instanziare gli oggetti, descritti mediante gli attributi e le
funzionalità presenti nella classe stessa.
Il concetto di classe deriva dal concetto di tipo di dato astratto, tipico del
paradigma della programmazione procedurale.
Il primo linguaggio a supportare il paradigma della programmazione orientata
agli oggetti è stato il Simula, sviluppato da Ole-Johan Dahl e Kristen Nygaard tra
il 1960 e il 1967; il linguaggio Simula ispirò fra i tanti, C#, C++, Ada, Effeil, Java,
etc.
Proprio con Java, il paradigma della programmazione orientata agli oggetti è
diventato quello dominante; contando, infatti, un numero di sviluppatori intorno ai
9 milioni, circa il 20% di quelli totali, e conquistando nel 2005 e nel 2015 il titolo di
“Language of the Year” per TIOBE (una famosissima compagnia che si occupa di
valutazione della qualità del software), Java è diventato, di fatto, il linguaggio più
conosciuto e affermato al mondo.
Tuttavia, il paradigma OOP impone al linguaggio Java una complessità strutturale rispetto ad altri linguaggi procedurali accompagnata ad una serie di limitazioni.
Ad esempio, cambiare le definizioni nelle superclassi può portare a una ridefinizione
a cascata nelle eventuali sottoclassi.
Per cercare di superare tali limiti di Java, Google chiese a Robert Griesemer,
Rob Pike e Ken Thompson di sviluppare un linguaggio di programmazione capace di
supportare, anche se in maniera atipica, il paradigma di programmazione orientata
agli oggetti, cercando, però, di snellirne il più possibile la complessità, attraverso una
sintassi essenziale e l’uso dei puntatori, nonchè, di superare i limiti dell’ereditarietà
tramite una gerarchia basata sulla composizione. Nasce, pertanto, il progetto Go.
Nelle intenzioni iniziali di Google, Go dovrebbe essere il linguaggio del futuro.
Ma è veramente plausibile che Go “rottami” Java?
La risposta non è assolutamente immediata, e necessita, pertanto, di un’analisi
approfondita.
Go, infatti, è un linguaggio recentissimo; si pensi che la Versione 1 è stata rilasciata appena quattro anni fa, ossia il 28 Marzo 2012; non ha avuto, pertanto, il
tempo di affermarsi nel mondo della programmazione.
Tuttavia, Google è una delle più grandi e importanti aziende al mondo; è plausibile, pertanto, che, sotto la spinta, anche economica, di questo colosso, Go possa
diventare in pochi anni un forte concorrente del linguaggio Java.
La presente tesi parte proprio dalle premesse appena descritte e si pone come
obiettivo quello di capire se, e fino a che punto, Go possa sostituire Java.
Data l’estrema complessità del compito, e per una maggiore obiettività, si è
scelto di dividere l’investigazione (svolta presso il laboratorio Barbiana 2.0) in tesi
Introduzione
5
e in antitesi; a noi è toccata la parte di “avvocato difensore” di Java, mentre un
altro collega si è assunto l’onere di “difendere” Go.
Lo studio dei due linguaggi prevede, inizialmente, una descrizione sintetica della
storia e delle caratteristiche principali da essi possedute. Java è stato sviluppato
per essere un linguaggio capace di supportare tutte le sfaccettature del paradigma
OOP e che possa, inoltre, garantire l’indipendenza dalla piattaforma, tramite la Java
Virtual Machine. Go, invece è un linguaggio compilato, che fa della velocità il suo
punto di forza; esso, pertanto, non prevede nè macchine virtuali nè la complessità
sintattica dei tipici linguaggi orientati agli oggetti.
Dopo un primo studio generale dei due linguaggi, la presente tesi si prefigge l’obiettivo di descrivere la gestione della programmazione concorrente tramite i thread
e le goroutine. Lo studio include, inoltre, la leggibilità del software e il riuso dello
stesso, attraverso un’elaboratà analisi della gestione delle funzioni, che ci porterà a
introdurre il concetto di polimorfismo, di ereditarietà singola, multipla e simulata,
di interfaccia e del suo utilizzo nella programmazione, etc.
Per studiare tutti questi aspetti, si modellerà un caso di studio basato sulle transazioni bancarie e si analizzerà, volta per volta, il comportamento dei due linguaggi
di programmazione, Java e Go.
Nel particolare, la tesi sarà strutturata come di seguito specificato:
•
•
•
•
•
•
•
Il Capitolo 1 descrive la storia e le caratteristiche principali di Java, con
una particolare attenzione sulla programmazione orientata ad oggetti e sulla
piattaforma.
Il Capitolo 2 illustra la storia e le caratteristiche principali di Go.
Il Capitolo 3 presenta il case study e analizza il comportamento dei due linguaggi
nella gestione della programmazione concorrente, tramite l’utilizzo dei thread e
delle goroutine.
Il Capitolo 4 descrive la gestione dei metodi e delle funzioni, dedicando una
maggiore attenzione al polimorfismo e ai tipi di ritorno.
Il Capitolo 5 tratta l’implementazione delle interfacce.
Il Capitolo 6 descrive il riuso del software, concentrandosi sulle gerarchizzazioni
tipiche dell’ereditarietà e sulla composizione tipica dell’embedding.
Infine, nel Capitolo 7, vengono tratte le conclusioni del lavoro svolto e viene dato
uno sguardo a possibili sviluppi futuri.
1
Caratteristiche generali di Java
Nel presente capitolo verranno trattate la storia e le caratteristiche principali di
Java. La tesi non si prefigge come obiettivo la realizzazione di un manuale e, di
conseguenza, le caratteristiche generali saranno trattate in maniera sintetica; in
particolare, verrà data maggiore attenzione alla programmazione ad oggetti e alla
piattaforma.
1.1 Storia del linguaggio
Nel 1991 il “Green Team” guidato da James Gosling della Sun Microsystems realizzò un linguaggio di programmazione per il controllo di elettrodomestici chiamato
Oak (“quercia”in inglese), capace di essere eseguito su diversi tipi di processori
indipendentemente dall’architettura.
In fase di progettazione, la Sun Microsystem decise di abbandonare la logica di
controllo degli elettrodomestici per creare un’azienda costola denominata FirstPerson Inc, con l’obiettivo di conquistare, attraverso la piattaforma Oak, il mercato
della TV interattiva.
Tuttavia, i tempi non erano ancora maturi, il progetto non ricevette finanziamenti adeguati e la FirstPerson cosı̀ come nacque morı̀. Si rendeva, pertanto, necessaria
l’elaborazione di una nuova strategia.
Fu solo nel 1994, con l’esplosione di Internet, che
“the team returned to work up a Java technology-based clone of Mosaic they
named “WebRunner” (after the movie Blade Runner), later to become officially
known as the HotJavaTM browser”. 1
Il 23 maggio 1995 John Gage, direttore dell’Ufficio della Scienza per la Sun Microsystems, e Marc Andreessen, cofondatore e vice presidente esecutivo di Netscape,
annunciarono al pubblico che il linguaggio di programmazione Java era realtà e che
sarebbe stata incorporato in Netscape Navigator, all’epoca il portale di Internet più
famoso al mondo.
La scelta del nome è tutt’oggi un mistero.
1
J.Byous, Java Technology: An early history, 2003, p.4
8
1 Caratteristiche generali di Java
Secondo una leggenda metropolitana, mai confermata, Java sarebbe l’acronimo
di Just Another Vacuum Acronym ossia “soltanto un altro vuoto acronimo”.
Secondo un’altra leggenda, più accreditata, il nome di Java deriva dall’omonimo caffè, del quale i tecnici al lavoro abusarono in fase di realizzazione (questo
spiegherebbe il logo del linguaggio, riportato in Figura 1.1).
Figura 1.1. Logo di Java
La Sun Microsystems nel 1995 decise, inoltre, di mettere subito disposizione
il kit di sviluppo JDK (Java Development Kit) e di definire il linguaggio da un
documento chiamato The Java Language Specification.
La prima edizione del documento (Java SE 1) è stata pubblicata nel 1996 e, ad
oggi, la versione più recente delle specifiche è la Java SE 8 Edition.
Grazie alle collaborazioni con Netscape, e successivamente con Oracle e IBM, e
grazie alla sua filosofia
“write once, run everywhere”2
Java è riuscito a conquistare un numero di sviluppatori intorno ai 9 milioni ed ad
aggiudicarsi, nel 2005 e nel 2015, il titolo “Language of the Year”, imponendosi,
quindi, come il linguaggio informatico più conosciuto e più utilizzato al mondo.
La tecnologia Java ha, ormai, invaso anche le nostra vita quotidiana, essendo
presente, per esempio, nei nostri smartphone, computer, tablet, smartcard, etc.
1.2 Caratteristiche principali
La sintassi di base di Java è basata principalmente sui linguaggi C e C++.
Non sono state, tuttavia, inseriti particolari elementi poiché considerati non
“performanti” a causa dei diversi errori che potrebbero portare in fase di programmazione. Ad esempio,
2
http://www.computerweekly.com/feature/Write-once-run-anywhere
1.3 La programmazione orientata ad oggetti
9
“unlike C and C++, the Java programming language has no goto statement;
identifier statement labels are used with break or continue statements appearing
anywhere within the labeled statement.”3
Java è un linguaggio ad alto livello basato sul paradigma di programmazione
orientata agli oggetti (di seguito, Object-Oriented-Programming OOP).
Java, grazie anche alla OOP, risulta essere più semplice rispetto a linguaggi
quale il C a differenza del quale evidenzia una gestione delle eccezioni molto chiara
ed efficiente e un sistema di gestione della memoria comodo e pratico, coordinato dal Garbage Collector. Quest’ultimo è un meccanismo che gestisce la memoria
in maniera automatica, mappandone le porzioni precedentemente allocate per poi
renderle libere quando non sono più utilizzabili.
Si solleva, quindi, il programmatore dalla deallocazione manuale, eliminando,
di fatto, l’aritmetica dei puntatori, garantendo, sicuramente una robustezza elevata
che, tuttavia, verrà pagata in efficienza e spreco di risorse.
Punto di forza di Java è l’essere gratuito e possedere una vastissima libreria di
classi standard organizzata in vari package.
Un altro punto di forza di Java è l’indipendenza della piattaforma, ossia la
possibilità di eseguire ogni applicazione su sistemi operativi radicalmente diversi.
Ciò è reso possibile grazie alla Java Virtual Machine.
Le facilitazioni della programmazione ad oggetti e l’indipendenza della piattaforma hanno permesso a tutti i programmatori, anche principianti, di cimentarsi
nello sviluppo di applicazioni Java, riuscendo ad ottenere, in breve tempo, buoni
risultati.
Tali peculiarità hanno agevolato notevolmente la diffusione del linguaggio.
Data l’importanza, descriveremo, in maniera più approfondita, la piattaforma e
la OOP in due sezioni a sè stanti.
1.3 La programmazione orientata ad oggetti
La OOP è un insieme di strumenti concettuali forniti dal linguaggio che consente
di definire oggetti in grado di interagire fra loro, permettendo di descriverne le
modalità di interazione e le relazioni di interdipendenza.
Gli obiettivi principali della OOP sono l’aumento della leggibilità del codice e
la facilitazione della gestione, specie di progetti di grandi dimensioni.
Questo tipo di approccio garantisce la modularità e il riuso del codice anche
in programmi molto differenti fra loro, facilitando notevolmente il lavoro degli
sviluppatori.
I linguaggi OOP possiedono caratteristiche identiche e si basano sui concetti
fondanti di classi, oggetti, metodi, incapsulamento, ereditarietà e polimorfismo.
Nelle prossime sottosezioni esamineremo in dettaglio tali concetti.
3
R Language
J. Gosling - B. Joy - G. Steele - G. Bracha - A. Buckley, The Java⃝
Specification Java SE 8 Edition, p.419
10
1 Caratteristiche generali di Java
1.3.1
Classi, oggetti e metodi
“Una classe è un’astrazione indicante un insieme di oggetti che condividono le
stesse caratteristiche e le stesse funzionalità. Un oggetto è un’istanza (ovvero,
una creazione fisica) di una classe.”4
La creazione di una classe avviene tramite la parola chiave class seguita dal nome della classe. All’interno della classe verranno inseriti gli attributi e le funzionalità
che compongono la stessa.
Per poter meglio spiegare questi concetti consideriamo l’esempio riportato nel
Listato 1.1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CurrentAccount {
private String iban;
private String number;
private float balance;
public CurrentAccount() {}
public CurrentAccount(String iban, String number, float balance) {
this.iban = iban;
this.number = number;
this.balance = balance;}
public String getIban() {
return iban;}
public void setIban(String iban) {
this.iban = iban;}
public String getNumber() {
return number;}
public void setNumber(String number) {
this.number = number;}
public float getBalance() {
return balance;}
public void setBalance(float balance) {
this.balance = balance;}
public float Deposit (float amount){
System.out.println("Conto corrente n."+getNumber()+"; Saldo "+getBalance()+"; Deposito: "+amount+" euro.");
setBalance(getBalance()+amount);
return getBalance();}}
Listato 1.1. Creazione di una classe
In tale listato abbiamo inserito come attributi il saldo, il numero di conto e
l’IBAN e come funzionalità il costruttore e il deposito. Il costruttore è un metodo
che viene eseguito ogni volta che sarà istanziato un oggetto.
Con il codice scritto finora non abbiamo definito nessun conto corrente, ma
solo l’“astrazione” dello stesso, e, quindi, non è possibile terminare con successo
l’esecuzione del programma. Occorre, pertanto, definire le creazioni fisiche realizzate
a partire dall’astrazione. Tale operazione viene mostrata nel Listato 1.2.
1
2
3
4
5
public class Main {
public static void main(String[] args) {
CurrentAccount CA1 = new CurrentAccount("ITkk", "12345",200);
float tmp=CA1.Deposit(500);
System.out.println("Nuovo Saldo: "+tmp+" euro"); }}
Listato 1.2. Creazione di un oggetto
L’istanza CA1 è proprio l’oggetto della classe CurrentAccount; potremmo, quindi,
eseguire il metodo Deposit definito precedentemente nella classe.
4
C. De Sio Cesari, Manuale di Java 7: Programmazione orientata agli oggetti con Java
Standard Edition 7, Hoepli Editore, 2012, p.32
1.3 La programmazione orientata ad oggetti
11
Compilando il programma, avremo il risultato riportato nella Figura 1.2.
Figura 1.2. Risultato del Listato 1.2
Possiamo, quindi, affermare che
“ogni oggetto appartiene a una classe e una classe definisce i metodi per i suoi
oggetti.” 5
Il metodo è, pertanto, una funzione associata in modo esclusivo ad una classe e
rappresenta un’operazione eseguibile sugli oggetti della stessa.
1.3.2
Incapsulamento, ereditarietà e polimorfismo
L’incapsulamento è la tecnica di nascondere il funzionamento di una classe in
modo da proteggerla dai cambiamenti che si produrrebbero nel caso di errato
funzionamento o errata implementazione.
Come detto nella sezione precedente, una classe è caratterizzata dai suoi attributi
e dalle sue funzionalità.
L’incapsulamento prevede, proprio, la suddivisione di una classe in due parti:
•
•
Interfaccia: descritta dall’insieme dei metodi dichiarati pubblici (set e get) e
che, quindi, sono invocabili dall’utente e accessibili alle altre classi;
Implementazione: descritta dall’insieme degli attributi di una classe, dichiarandoli privati, e quindi inaccessibili, all’utente e alle altre classi.
L’incapsulamento favorisce il riuso e la manutenzione del codice, migliorando
l’utilizzo delle classi, in quanto eventuali modifiche non coinvolgeranno le parti non
visibili all’esterno della classe.
È fondamentale, pertanto, utilizzare l’incapsulamento se vogliamo garantire
al nostro programma caratteristiche di robustezza, riusabilità e indipendenza del
codice.
L’ereditarietà consiste in una relazione per estensione o specializzazione, che si
stabilisce tra due classi aventi caratteristiche comuni.
Se bisogna implementare una classe B particolare, e abbiamo già creato una
classe A che descrive un concetto più generico, possiamo far ereditare alla classe B
il codice della classe A, facendo diventare, quindi, A superclasse di B.
L’ereditarietà è parte principale della OOP poiché permette una strutturazione
gerarchica nel programma garantendo leggibilità, riduzione dei tempi di sviluppo e,
soprattutto, riutilizzo del codice.
Senza l’ereditarietà classi con caratteristiche comuni andrebbero riscritte dall’inizio, appesantendo notevolmente il programma.
5
Cay Horstmann, Concetti di Informatica e fondamenti di Java (IV Edizione) Edizione
italiana a cura di Marcello Delpasso, Milano, Apogeo s.r.l., 2007, p.35
12
1 Caratteristiche generali di Java
Utilizzando l’ereditarietà potremmo, invece, evitare di scrivere il codice in
comune tra la classe e la superclasse.
In Java non è presente l’ereditarietà multipla poiché ritenuta problematica dal
punto di vista implementativo. Tale scelta avvantaggia la robustezza, limitando,
però, le potenzialità del linguaggio di programmazione. È, comunque, presente
una “scappatoia” per superare questo limite tramite le interfacce, simulando il
meccanismo di ereditarietà multipla.
In Java è, inoltre, presente una classe Object, che viene pensata come “astrazione” di un oggetto generico. Possiamo dire che in Java, la classe Object è la
superclasse di tutte le classi.
Il polimorfismo permette, invece, di raggruppare in un unico termine entità
differenti e aventi caratteristiche comuni. È un concetto potentissimo che ci permette
di scrivere del codice generico che si specializzerà in funzione dell’oggetto che lo
ha invocato. Attraverso l’utilizzo del polimorfismo il sistema è in grado di capire
automaticamente cosa fare, e si comporterà in maniera diversa per oggetti di tipo
diverso, garantendo, ancora una volta, il riuso del codice.
Se non si utilizzasse il polimorfismo dovremmo implementare all’interno di una
classe un costrutto del tipo switch-case che appesantirà e aumenterà la complessità
del codice.
1.4 La piattaforma
La piattaforma Java è un software realizzato dalla Sun Microsystems che rende
possibile la scrittura e l’esecuzione di programmi Java su diversi hardware, indipendentemente dalla loro architettura, rendendoli, pertanto, portabili su ciascun
dispositivo. La piattaforma mette a disposizione il compilatore Javac, la macchina
virtuale Java Virtual Machine e il set di librerie della documentazione API.
1.4.1
Java Virtual Machine
La Java Virtual Machine (JVM) è una macchina virtuale multithreading, “type
safety” (ovvero, previene gli errori di tipo) e con strutture dati basate sugli stack.
La JVM deve essere installata su una macchina reale per garantire il funzionamento di qualsiasi programma Java, indipendentemente dall’architettura.
Il codice sorgente (scritto in .java) viene, inizialmente, analizzato dal compilatore del linguaggio, il Javac, il quale produce il bytecode in stretta relazione con il
sistema operativo e con l’hardware in uso.
Il bytecode viene salvato in un file con estensione .class. La JVM legge il bytecode e lo interpreta secondo le caratteristiche della macchina sul quale è installato
ed esegue il programma.
Possiamo, quindi, riassumere il funzionamento della JVM con lo schema mostrato in Figura 1.3.
1.4 La piattaforma
13
Figura 1.3. Funzionamento della Java Virtual Machine
I file class vengono, poi, caricati nell’area dati di runtime da un meccanismo
chiamato class loader ogni volta è necessario utilizzare una specifica classe.
Per eseguirli, Java utilizza i componenti mostrati in Figura 1.4.
14
1 Caratteristiche generali di Java
Figura 1.4. Schematizzazione della Java Virtual Machine
Esaminiamo, ora, più in dettaglio tali componenti:
• Stack : è un’area di memoria con modalità di accesso LIFO (Last In, First Out).
Lo stack immagazzina le variabili locali di ogni metodo e tiene traccia di ogni
invocazione ai metodi stessi.
Esso utilizza tre registri per effettuare le sue operazioni, ovvero:
– Vars register : memorizza tutte le variabili locali dei metodi invocati;
– Frame register : memorizza le operazioni della pila stessa;
– Optop register : memorizza le istruzioni per le operazioni in bytecode.
• Area dei metodi : quando viene caricato un class file, la JVM elabora le sue
informazioni e le pone nell’area dei metodi. In essa risiede quindi il bytecode
da eseguire. L’indirizzo della successiva istruzione da prelevare è contenuto nel
registro Program Counter;
• Heap: appena il programma viene eseguito, la JVM carica nell’Heap, man mano
che vengono creati, tutti gli oggetti che il programma istanzia.
A differenza di C ++, Java non ha i puntatori per liberare la memoria
precedentemente allocata.
Questo meccanismo viene attuato automaticamente attraverso il Garbage Collector. Quest’ultimo è deputato alla rimozione degli oggetti non più utilizzati
dal programma man mano che questo è in esecuzione.
L’area di dati runtime, infine, interagisce con l’Execution Engine. Quest’ultimo
è responsabile dell’esecuzione delle istruzioni su diverse piattaforme.
1.4 La piattaforma
15
Le specifiche della JVM non pongono nessun vincolo sul procedimento, lasciando,
dunque, ampia libertà sull’algoritmo risolutivo.
Il grande vantaggio ottenuto dall’interdipendenza dell’architettura viene, però,
pagato in termini di ottimizzazione. Non è, ad esempio, possibile sfruttare le caratteristiche peculiari di un particolare sistema operativo, e la traduzione del bytecode
potrebbe causare lentezza nell’ esecuzione del programma Java.
1.4.2
La documentazione API
Java gode di una vastissima e preziosa libreria di classi standard. Il set delle librerie disponibili al programmatore è contenuto nella API (Application Programming
Interface), consultabile al sito https://docs.oracle.com/javase/7/docs/api/.
Lo scopo dell’API è quello di cercare di assicurarsi l’obiettivo di effettuare
un’“astrazione” a più alto livello, mettendo in comunicazione la parte hardware
con il programmatore, semplificando, cosı̀, lo sviluppo di programmi Java.
Le API consentono, quindi, di evitare allo sviluppatore di scrivere da zero tutte le funzioni necessarie al programma, permettendo quindi, di default, un ampio
riutilizzo del codice. Vediamo come è strutturata nel dettaglio la documentazione.
L’API è suddivisa in vari package che contengono le varie classi raggruppate per
campo di utilizzo; per ogni package viene specificata una breve descrizione degli
stessi(Figura 1.5).
Figura 1.5. Documentazione API: lista dei package
Selezionando un package si avrà accesso a tutte le entità da esso previste,
ordinate per tipo, come, ad esempio, interfacce, classi, eccezioni, etc. (Figura 1.6).
A ciascuna entità sarà allegata una breve descrizione contenente informazioni su
come poterla utilizzare.
16
1 Caratteristiche generali di Java
Figura 1.6. Documentazione API: contenuto dei package
Per poter utilizzare il set di librerie presenti nella documentazione API, basta
“importarle” con il comando import, come mostrato nel Listato 1.3.
1
import java.util.Scanner;
Listato 1.3. Esempio di importazione di una libreria
Attraverso questo comando viene importata la classe Scanner della libreria util,
permettendo di utilizzare l’inserimento da tastiera.
La libreria più importante presente nella documentazione è la java.lang.*.
Essa permette, ad esempio, di utilizzare i comandi String e System. Data la
presenza di classi fondamentali allo sviluppo, Java importa, automaticamente, tale
libreria, facilitando, ancora di più, il compito degli sviluppatori. L’API racchiude in
sé migliaia di librerie, ed è in continuo aggiornamento.
Questo è un grandissimo punto di forza e rappresenta uno dei fattori chiave che
ha permesso a Java di raggiungere il suo straordinario successo.
2
Caratteristiche generali di Go
In questo capitolo si illustreranno, allo stesso modo di come è stato fatto con Java,
la storia e le caratteristiche principali di Golang. Tuttavia, verrà data maggiore
attenzione alla sintassi.
2.1 Storia del linguaggio
Golang, conosciuto meglio con il nome Go, è un linguaggio di programmazione
realizzato da Robert C. Pike (sviluppatore di Unix e del linguaggio Limbo, collaboratore per la creazione dei sistemi Plan 9 e Inferno), Ken Thompson (inventore del
linguaggio B) e Robert Griesemer (collaboratore allo sviluppo di V8 JavaScript per
Google), su incarico della Google Inc.
Scopo del linguaggio di programmazione era quello di risolvere alcuni problemi
di sviluppo che la stessa azienda stava affrontando:
“The first problem is that networked systems are getting larger, with more
interaction between more pieces, and existing languages were not making those
interactions as smooth to express as they could be.[...] The second problem is
that programs are getting larger and development more distributed. A language
that works very well for a small group may not work as well for a large company
with thousands of engineers working on many millions of lines of code.” 1
I tre programmatori si riunirono il 21 Settembre 2007 per stilare gli obiettivi che il
linguaggio doveva raggiungere.
Decisero, quindi, di basarsi su un precedente lavoro correlato con Inferno per
creare un linguaggio che riuscisse ad avere le seguenti caratteristiche:
•
•
•
1
compilazione efficiente;
velocità di esecuzione;
facilità di programmazione.
http://www.pl-enthusiast.net/2015/03/25/interview-with-gos-russ-cox-and-sameerajmani/
18
2 Caratteristiche generali di Go
Nel Gennaio 2008, Ken Thompson iniziò a lavorare sul compilatore. Nel Maggio
dello stesso anno la Google incaricò, indipendentemente dal team originario, Ian
Taylor per la realizzazione di un prototipo riguardante lo standard per le librerie
e Russ Cox per implementare le stesse librerie. Go era diventato, finalmente, un
progetto a tempo pieno. Non solo i programmatori della Google parteciparono alla
realizzazione del progetto, ma anche
“many people from the community have contributed ideas, discussions, and
code”. 2
Il linguaggio di programmazione fu annunciato il 10 Novembre 2009 e venne
subito eletto “Program Language of the Year” dalla Tiobe nello stesso anno.
Google ha affrontato, inizialmente, grossi problemi di copyright per il nome
scelto. Esiste infatti, già dal 2003, un linguaggio di programmazione chiamato “Go!”,
sviluppato da McCabe e Keith Clark. Nonostante il dissenso dei due sviluppatori,
la questione non giunse mai in tribunale.
La Versione 1.0 di Golang è stata rilasciata, ufficialmente, nel 2012 e ha avuto
negli anni successivi e numerosi aggiornamenti, fino ad arrivare alla Versione 1.6 nel
Febbrario 2016.
Dalla Versione 1.5 è, inoltre, possibile includere nelle applicazioni dei sistemi
IOS e Android le librerie di Go.
Sembra chiaro, anche dalla conferenza GopherCon del 2015, che Google voglia
puntare su Go per conquistare il mercato delle app per gli smartphone.
Dal grafico in Figura 2.1 si può notare che, dopo una iniziale e tiepida accoglienza, negli ultimi due anni Go ha avuto una crescita di interesse esponenziale.
Figura 2.1. Interesse per Golang
Nel grafico, l’asse delle ascisse misura un intervallo di tempo, espresso in anni,
mentre l’asse delle ordinate misura il valore relativo alla keyword “Golang”.
2
https://golang.org/doc/faq#history
2.2 Caratteristiche principali
19
Il valore massimo riscontrato nell’intervallo viene riportato pari a 100, mentre
gli altri valori vengono calcolati e visualizzati in modo relativo a questo picco.
Il grafico risulta di impatto se, utilizzando lo stesso metro di ricerca, si inserisce
nell’asse delle ordinate la keyword “Java”(Figura 2.2).
Figura 2.2. Interesse per Java
La crescente attenzione e la spinta economica del colosso Google potrebbero
portare Go a raggiungere, nel prossimo futuro, lo stesso livello di mercato di Java.
2.2 Caratteristiche principali
Go è stato rilasciato dalla Google Inc. come progetto open source. La finalità del
progetto è ambiziosa; l’azienda di Montain View tenta di creare, attraverso il linguaggio Golang, uno strumento general-purpose, adattato alla programmazione di
sistema, capace di combinare la semplicità di programmazione di un linguaggio interpretato, tipica dei linguaggi dinamici, con la sicurezza e le funzionalità di un
linguaggio compilato, esibite, tipicamente, in un linguaggio statico.
Go è stato pensato, pertanto, come un linguaggio che deve offrire sia alta velocità
di esecuzione e di compilazione, sia facilità di programmazione.
Go supporta, a livello nativo, la programmazione concorrente, grazie all’implementazione delle goroutine e dei channel. Esse sono funzioni progettate per ottimizzare i tempi di compilazione che, se eseguite in maniera corretta, promettono un
apporto fondamentale per l’esecuzione e la comunicazione concorrente.
Il linguaggio, tuttavia, non è stato ottimizzato per la programmazione in parallelo. Lo stesso Rob Pike intitolò il suo intervento alla conferenza Waza, tenuta l’11
Gennaio 2012,
“Concurrency is not parallelism”3 .
3
https://vimeo.com/49718712
20
2 Caratteristiche generali di Go
L’assenza di parallelismo reale non deve essere pensata, necessariamente, come un limite. Un sistema multiprocessore, mentre esegue un programma, eseguirà
sempre altre istruizioni, all’infuori dello stesso, rendendo, di fatto, impossibile il
parallelismo reale all’interno di una stessa applicazione.
Golang non presentava, in origine, l’intercettazione delle eccezioni. Tuttavia,
sotto la spinta della community furono inseriti i comandi:
• panic: la funzione che ha causato lo stato di panic viene, istantaneamente,
bloccata;
• defer: ogni funzione deferred opera come se la funzione panic fosse terminata
correttamente.
Il blocco panic/defer è assolutamente assimilabile al blocco try/catch presente
in Java. Altri aspetti del linguaggio Go verranno approfonditi nelle sottosezioni
successive.
2.2.1
La compilazione e le librerie
Go è un linguaggio compilato. Esso non supporta la presenza di una macchina
virtuale con le funzioni di interprete; l’assenza di tale meccanismo, pur non permettendo l’indipendenza dell’architettura, consente una conversione in codice binario
del codice sorgente, in modo più o meno istantaneo, rendendo possibile la realizzazione di applicazioni con una velocità di compilazione eccellente. Tali caratteristiche promettono di ottenere buone prestazioni anche su hardware caratterizzati da
performance basse.
Di notevole interessante è la presenza nel file binario compilato della maggior
parte delle librerie necessarie alla realizzazione dei programmi. Non è necessario,
pertanto, l’installazione di librerie aggiuntive, se non quelle minime necessarie al
sistema. Tale proprietà, però, è la causa della creazione di file binari compilati di
dimensioni anche importanti.
Go possiede, infatti, nativamente, una vasto quantitativo di librerie, ciascuna
delle quali è ben fornita. Seppur non paragonabile alla documentazione API di Java,
Go presenta librerie che rispondo alle più bizzarre richieste dell’utenza. Il linguaggio,
per esempio, implementa la libreria net, fornita, come suggerisce il nome, di tutte
le primitive di rete, o, per esempio, la libreria zip, che supporta la scrittura o la
lettura degli archivi zip.
Come per Java, le librerie possono essere consulatibili sul sito ufficiale all’indirizzo https://golang.org/pkg/
2.2.2
Go è un linguaggio OOP?
Alla domanda diretta il sito ufficiale risponde:
“Yes e no. Although Go has types and methods and allows an object-oriented
style of programming, there is no type hierarchy. The concept of “interface”
in Go provides a different or approach that we believe is easy to use and in
some ways more general. There are also ways to embed types in other types to
provide something, but not identical, to subclassing.Moreover, methods in Go
2.2 Caratteristiche principali
21
are more general than in C++ or Java: they can be defined for any sort of data,
even built-in types such as plain, “unboxed” integers. They are not restricted
to structs. Also, the lack of a type hierarchy makes “objects” in Go feel much
more lightweight than in languages such as C++ or Java.” 4
Da tale risposta si può concludere che Go non è tipicamente un linguaggio OOP.
Esso permette lo stile OOP, ma, presentando un sistema di tipizzazione non gerarchico, non ne consente un approccio standard, come in Java. Tale caratteristica
velocizza la stesura del codice, poichè non è necessario riflettere sulle gerarchie tra tipi di dati, consentendo una “tipizzazione leggera” che non è facilmente riscontrabile
negli altri linguaggi OOP.
Anche in Go, infatti, è possibile definire dei tipi da parte dell’utente. Tale pratica,
comune nei linguaggi basati sul paradigma di OOP, si ottiene utilizzando le struct.
Le struct sono entità simili alle classi, precedentemente trattate nella sezione dedicata a Java. Da un punto vista prettamente morfologico, le struct sono costituite
dai campi, ossia una formazione di valori di tipo eterogeneo, accessibili singolarmente. Le struct sono definite completamente per mezzo della funzione new. Pragmaticamente, ogni struct è costruita come un blocco contiguo di memoria che descrive
i suoi elementi costitutivi. L’esempio riportato nel Listato 2.1 descrive l’aspetto
formale di una struct.
1
2
3
4
5
6
7
package main
import "fmt"
type CurrentAccount struct {
iban string
number string
balance float64 }
Listato 2.1. Esempio di una struct
Le struct sono, essenzialmente, tipi valore; pertanto, la funzione new restituisce
un puntatore ad un valore.
L’esempio nel Listato 2.2 descrive come poter utilizzare la funzione new.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
type CurrentAccount struct {
iban string
number string
balance float64}
func main() {
CA1 := new(CurrentAccount)
CA1.balance=1000
CA1.iban="ITkkABBBBBCCCCCXXXXXX"
CA1.number="12345"
fmt.Print(CA1)}
Listato 2.2. La funzione new
Tuttavia, difficilmente una struct viene instanziata come riportato nel Listato
2.2. L’istanziazione consueta è, infatti, quella riportata nel Listato 2.3.
1
2
3
4
4
package main
import "fmt"
type CurrentAccount struct {
https://golang.org/doc/faq#Is Go an object-oriented language
22
2 Caratteristiche generali di Go
5
6
7
8
9
10
11
iban string
number string
balance float64}
func main() {
CA1 :=CurrentAccount{iban:"ITkkABBBBBCCCCCXXXXXX",number:"12345",balance:1000}
fmt.Print(CA1) }
Listato 2.3. Istanziazione consueta di una struct
I due listati produrrano un output identico, a meno di un carattere “&” (indica
il riferimento). I risultati dei due listati sono riportati, rispettivamente, nelle Figure
2.3 e 2.4.
Figura 2.3. Risultato del Listato 2.2
Figura 2.4. Risultato del Listato 2.3
Interessante è notare come nelle struct non trovino spazio i metodi, che sono,
invece, parte integrante delle classi dei linguaggi tipicamente OOP.
Il linguaggio Go, pur non permettendo l’implementazione di tale caratteristica,
rende possibile il “collegamento” fra i metodi e un qualsiasi tipo di struct, tramite
l’utilizzo di puntatori.
I puntatori, familiari nei linguaggi C e C++, risultano del tutto estranei agli
sviluppatori di programmi che hanno sacrificato l’efficienza della programmazione in favore della facilità. I puntatori, infatti, garantiscono performance elevate,
soprattutto nell’ambito del controllo e della gestione della memoria.
Go, pur utilizzando i puntatori, cerca di trovare il giusto compromesso fra la
facilità e l’efficienza, allontanando il programmatore dalla loro aritmetica.
La memoria in Go, infatti, è gestita completamente dal Garbage Collector in maniera analoga a Java. Tale meccanismo, pertanto, terrà traccia delle aree di memoria
non più referenziate e le libererà automaticamente.
Come detto precedentemente, i puntatori permettono di “allegare” una struct
a un metodo. Il ricevente di una funzione ha bisogno di essere asserito sia nel
package della funzione stessa e che nel package di tutte le funzioni cui afferisce.
Inoltre, il suo nome deve essere utilizzato all’interno della funzione. Una struct
può, di conseguenza, essere arricchita tramite una funzione. Tale meccanismo è
notevolmente simile ai meccanismi delle classi, con l’eccezione che la funzione non
è inclusa nel corpo della struct.
Per poter meglio capire come “collegare” una funzione a una struct, si consideri
il Listato 2.4. Come si può notare, la func Deposit riferisce ad un puntatore alla
struct CurrentAccount. Nel main possiamo, quindi, utilizzare la funzione allo stesso
modo in cui può essere utilizzato un metodo nei linguaggi OOP.
2.3 La sintassi
23
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
type CurrentAccount struct {
iban string
number string
balance float64}
func (CA1 *CurrentAccount) Deposit(amount float64){
fmt.Println("Deposito:", amount)
CA1.balance+=amount
fmt.Println("Conto corrente n.",CA1.number,",IBAN:",CA1.iban,",Saldo:",CA1.balance ) }
func main() {
CA1 :=CurrentAccount{"ITkkABBBBBCCCCCXXXXXX","12345",1000}
fmt.Println("Conto corrente n.",CA1.number,",IBAN:",CA1.iban,", Saldo:",CA1.balance )
var amount float64
amount=2000
CA1.Deposit(amount) }
Listato 2.4. Struct e func
Dai listati presentati si osserva che la sintassi di base di Go, seppur simile a
quella di C++, C e Java, presenta meccaniche abbastanza differenti e offre discreti
vantaggi. Tratteremo la sintassi in maniera approfondita in una sezione a sè stante.
2.3 La sintassi
Dal punto vista sintattico Golang è stato pensato in maniera tale da alleggerire sia
la stesura di un programma sia il compito del programmatore.
2.3.1
Case sensitive
Come la maggior parte dei linguaggi di programmazione, Go è “case sensite”.
Le dichiarazioni di variabili omonime, che differiscono dell’iniziale maiuscola o
minuscola, sono considerate elementi completamente diversi.
Ad esempio, consideriamo il Listato 2.5; esso produrrà come risultato:
Amount= 3000 ; amount= 2000
1
2
3
4
5
6
7
8
9
package main
import "fmt"
func main() {
var amount float64
amount=2000
var Amount float64
Amount=3000
fmt.Println("Amount=",Amount,"; amount=", amount)}
Listato 2.5. Esempio di case sensitive
L’identificatore di una variabile non è da scegliere a cuor leggero. Se esso inizia
con la lettera maiuscola, come, ad esempio, Amount del Listato 2.5, la variabile sarà
visibile anche all’esterno del package. Viceversa, la visibilità di una variabile il cui
nome inizia con la lettere minuscola sarà limitata al package di appartenenza.
I package sono concetti sostanzialmente simili a quelli di Java.
24
2 Caratteristiche generali di Go
2.3.2
Le variabili
Per definire le variabili, Go utilizza, essenzialmente, le keyword:
• var: descrive le variabili, il cui valore può essere modificato;
• const: descrive le variabili, il cui valore deve rimanere costante.
Il compilatore Go presenta la caratteristica dell’Interferenza di tipo, ossia un
meccanismo che permette di dedurre, automaticamente, il tipo di una var o di una
const, basandosi, esclusivamente, sul valore attribuito in sede di dichiarazione.
Tale meccanismo si avvia attraverso l’operatore “:=”, chiamato operatore di
assegnazione diretta. Consideriamo l’esempio riportato nel Listato 2.6; il compilatore
riuscirà a decifrare il tipo della variabileamount assegnando ad essa il tipo float64.
1
2
3
4
5
package main
import "fmt"
func main() {
amount:=2000.6}
Listato 2.6. Operatore di assegnazione diretta in Go
A differenza di Java, se in fase di dichiarazione nessun valore viene dato alla
variabile, essa verrà inizializzata automaticamente al valore nullo (Listato 2.7).
1
2
3
4
5
6
package main
import "fmt"
func main() {
var amount float64
fmt.Println("il valore di amount è",amount) }
Listato 2.7. Variabile non inizializzata in Go
Il Listato 2.8 è equivalente al Listato 2.7 visto in precedenza.
1
2
3
4
5
6
7
package main
import "fmt"
func main() {
var amount
float64 amount = 0
fmt.Println("il valore di amount è", amount) }
Listato 2.8. Assegnazione esplicita di un tipo ad una variabile in Go
In Java, invece, se proviamo a implentare il codice riportato nel Listato 2.9, sarà
segnalato l’errore “Exception in thread ‘‘main java.lang.Error: Unresolved
compilation problem: The local variable amount may not have been initialized
at Main.main(Main.java:4)”
1
2
3
4
public class Main {
public static void main(String[] args){
float amount;
System.out.println(amount); }}
Listato 2.9. Variabile non inizializzata in Java
In Go, di default, tutte le variabili sono inizializzate. Tale meccanismo viene
utilizzato sia per ottimizzare i tempi di programmazione e sia per evitare le criticità
relative a eventuali errori dello sviluppatore, facilitando, quindi, la programmazione.
2.3 La sintassi
2.3.3
25
I punti e virgola
La grammatica formale dei linguaggi di programmazione usa concludere una sequenza di codice adoperando i punti e virgola “;”. Come si può notare dai listati
precedenti, i programmi Go possono omettere la maggior parte di questi punti e
virgola. La sintassi del codice detta i due assunti che regolano la possibilità di poter
omettere tale segni di interpunzione:
•
•
1 : La serie di istruzioni deve essere suddivisa in token (una serie di caratteri
individuata da caratteri di delimitazione), e il carattere di delimitazione della
riga deve essere un identificatore, un int, un float, etc., o ancora una keyword
come, ad esempio, return, break, etc.;
2 : prima della chiusura di una parentesi tonda, graffa o quadra.
2.3.4
Strutture di controllo
Le strutture di controllo sono costrutti sintattici la cui semantica afferisce al controllo del flusso di esecuzione di un programma, ovvero servono a specificare se, quando,
in quale ordine e quante volte devono essere eseguite le istruzioni che compongono
il codice sorgente in base alle specifiche di progetto del software da realizzare.
Go, naturalmente, presenta tali costrutti. Prendiamo, ad esempio, il costrutto
if-else (Listato 2.10).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
type CurrentAccount struct {
iban string
number string
balance float64 }
func (CA1 *CurrentAccount) Withdraw(amount float64){
if amount>CA1.balance{
fmt.Println("Si tenta di ritirate ",amount,", ma il saldo è minore dell’importo.")
}else{
fmt.Println("Ritiro:", amount)
CA1.balance-=amount
fmt.Println("Saldo:",CA1.balance ) }}
func main() {
CA1 := CurrentAccount{"ITkkABBBBBCCCCCXXXXXX","12345",1000}
var amount float64
amount=2000.8
CA1.Withdraw(amount) }
Listato 2.10. Istruzione if-else in Go
Rispetto a Java, la coppia di parentesi graffe è obbligatoria, anche in presenza di
una sola istruzione, mentre non è obbligatoria la coppia di parentesi tonde.
Ciò detto vale anche per altri costrutti, come, ad esempio, il ciclo for (Listato
2.11).
1
2
3
4
5
6
package main
import "fmt"
func main() {
for j := 0; j <= 5; j++ {
fmt.Println("il valore di j è ", j) }}
Listato 2.11. Ciclo for in Go
26
2 Caratteristiche generali di Go
Il codice di Go, a livello puramente sintattico, si dimostra molto più leggero della
concorrenza. Tale scelta di implementazione cerca rispettare l’obiettivo di perseguire
l’effecienza di compilazione e la velocità di esecuzione.
Il guadagno che si potrebbe ottenere, seppure trascurabile su operazioni semplici,
potrebbe risultare fondamentale per programmi di elevate dimensioni.
3
Java vs Go: gestione della concorrenza
Nel presente capitolo viene presentato, dapprima, il case study che si andrà ad analizzare, descrivendone il diagramma dei casi d’uso, quello entità-relazione, quello
delle classi e quello di sequenza. In seguito, verrà presentata un’introduzione generale sulla gestione della concorrenza nei linguaggi di programmazione Java e Go,
per poi implementare in codice il case study e, infine, trarre le conclusioni.
3.1 Presentazione del case study
3.1.1
Descrizione del contesto di riferimento
In questa sezione verrà presentato il case study di interesse. Analizzandolo, potremmo studiare, pertanto, i punti di forza e di debolezza dei due linguaggi Java e Go.
In questa analisi si utilizzerà un computer con le specifiche tecniche riportate in
Figura 3.1.
Figura 3.1. Specifiche tecniche del calcolatore utilizzato per l’analisi
Il case study di base riguarderà un tipico esempio di sistema bancario. Esso verrà
strutturato come di seguito specificato:
•
Customer : gli utenti che possono accedere alla banca sono di due tipi:
28
3 Java vs Go: gestione della concorrenza
– Private: rappresentano i privati cittadini. Di ogni Private interesserà nome,
cognome, codice fiscale e documento d’identità;
– Company: rappresenta le aziende. Di ogni Company interessano partita IVA
e nome dell’azienda.
Di ogni Customer saranno, inoltre, memorizzati l’identificativo, l’indirizzo, la
città, il Codice di Avviamento Postale, l’email, il telefono fisso e quello mobile.
Ogni utente è in possesso di uno o più conto correnti.
• Current Account: di ogni conto corrente interesserà l’IBAN, il numero di conto
e il saldo. Il conto corrente dà accesso alle transazioni bancarie;
• Transaction: ogni transazione sarà caratterizzata dall’identificativo e dall’importo. Le transazioni possono essere, essenzialmente, di due tipi:
– Internal : rappresentano le transazioni interne al conto, ossia:
· Deposit: rappresentano i depositi bancari effettuati sul conto;
· Withdraw : rappresentano i ritiri bancari effettuati dal conto;
– Transfer : descrivono i bonifici bancari effettuati da un conto all’altro. Dei
trasferimenti interessa inoltre l’IBAN del destinatario.
3.1.2
Diagramma dei casi d’uso
Il diagramma dei casi d’uso (UCD, Use Case Diagram) è lo schema grafico che
rappresenta sinteticamente l’andamento delle funzioni fornite dal sistema, cosı̀ come
vengono utilizzate dagli attori che interagiscono col sistema stesso.
Un attore, di conseguenza, specifica il ruolo assunto dall’utente che interagisce con l’argomento del diagramma nell’ambito del caso d’uso del sistema. Nel
diagramma, generalmente, si possono notare le seguenti relazioni:
• Inclusione: la relazione di inclusione viene rappresentata da una linea tratteggiata con indicazione dello stereotipo ≪include≫ e indica che la funzione rappresentata dal caso d’uso alla base delle freccia include completamente la funzione
rappresentata del caso d’uso alla punta della freccia.
• Estensione: La relazione di estensione è rappresentata da una linea tratteggiata
con indicazione dello stereotipo ≪extend≫, indica che la funzione rappresentata
dal caso d’uso estendente alla base della freccia può essere impiegata nel contesto
della funzione estesa alla punta, rappresentandone, quindi, un arricchimento.
Nel contesto di riferimento, tuttavia, trova spazio solo la relazione di inclusione.
Il diagramma è stato realizzato secondo lo standard UML tramite il tool
Enterprise Architect (Figura 3.2).
3.1 Presentazione del case study
29
Figura 3.2. Diagramma dei casi d’uso per il nostro test
3.1.3
Progettazione concettuale
Dopo aver definito il diagramma dei casi d’uso è possibile dedicarsi alla progettazione concettuale. Essa permette di rappresentare le entità e le relazioni del case
study. Tale progettazione deve essere indipendente dai dettagli dell’implementazione, come, ad esempio, la concorrenza, e deve chiarire il significato dei termini, spesso
confondibili, assicurando l’interpretazione corretta di quelli specificati.
Tali concetti ambigui, infatti, potrebbero portare ad errori in fase di progettazione del software.
Una volta modellati i termini, la progettazione diviene un modello stabile per
l’implementazione successiva del case study.
I concetti espressi nella progettazione concettuale sono utili, soprattutto, per la
realizzazione di un software che sfrutta la OOP.
Ci serviremo del modello Entità/Relazione (E/R) per descrivere la realtà di
interesse.
Il modello Entità/Relazione
Il modello E/R fornisce una serie di strutture che permettono di descrivere una
qualsiasi realtà, a prescindere dai criteri di organizzazione dei dati, in modo chiaro e comprensibile. Il modello è chiamato Entità/Relazione per i suoi costrutti
fondamentali, che sono:
•
•
Entità: rappresenta una classe di oggetti che hanno proprietà comuni ed esistenza
autonoma;
Relazione: rappresenta i legami logici intercorrenti fra due o più entità.
Accanto a questi due costrutti il modello presenta:
30
3 Java vs Go: gestione della concorrenza
• Attributi : struttura semplice che descrivere una proprietà di un’entità o di una
relazione;
• Cardinalità delle relazioni : indica il minimo e il massimo numero di occorrenze
di un’entità che possono partecipare ad una determinata relazione;
• Cardinalità degli attributi : indica il minimo e il massimo numero di occorrenze
di un attributo che possono partecipare ad un’entità;
• Identificatori : sono attributi che identificano in maniera univoca ogni occorrenza
di un’entità;
• Generalizzazioni : rappresentano legami logici esistenti fra due o più entità. Le
entità coinvolte si distinguono in entità padre ed entità figlie.
Lo schema Entità/Relazione della realtà di interesse è rappresentato in Figura
3.3.
Figura 3.3. Il modello Entità-Relazione
3.1 Presentazione del case study
31
Dizionario dei dati
Una volta effettuata la creazione dello schema Entità/Relazione, è necessario stilare
una documentazione di supporto per descrivere i termini rappresentati nello schema
stesso. Viene redatto, pertanto, il dizionario dei dati, suddiviso per entità e relazioni.
Il dizionario delle entità e quello delle relazioni, relativi al test di nostro interesse
sono, rispettivamente, rappresentati in Figura 3.4 e in Figura 3.5
Figura 3.4. Dizionario delle Entità
Figura 3.5. Dizionario delle Relazioni
3.1.4
Progettazione della componente applicativa
La progettazione della componente applicativa è lo stadio di progettazione dedicato
allo sviluppo delle componenti software del case study. Utilizzando lo standard
UML, la progettazione della componente applicativa, nel contesto di riferimento,
consisterà in due fasi fondamentali, ossia diagramma delle classi e diagrammi di
sequenza.
32
3 Java vs Go: gestione della concorrenza
Definizione del diagramma delle classi
Descrive il tipo degli oggetti che compongono il sistema e le relazioni statiche esistenti tra esse. Per ciascuna classe, inoltre, si specificano metodi e attributi. In
Figura 3.6 è riportato il diagramma delle classi del caso di studio di interesse.
Figura 3.6. Diagramma delle classi relativo al test di nostro interesse
3.1 Presentazione del case study
33
Definizione dei diagrammi di sequenza
Descrive una sequenza di azioni dove tutte le scelte sono prestabilite e, di conseguenza, dove non trovano spazio flussi alternativi di esecuzione. Il diagramma,
quindi, descrive le relazioni che intercorrono, in termini di messaggi, tra gli attori e
gli oggetti del sistema. Realizziamo, ora, i diagrammi di sequenza.
I diagrammi di sequenza relativi alle attività di Deposito, Ritiro e Trasferimento
sono rappresentati, rispettivamente, nelle Figure 3.7-3.9.
Figura 3.7. Deposito: Diagramma di sequenza
34
3 Java vs Go: gestione della concorrenza
Figura 3.8. Ritiro: Diagramma di sequenza
3.1 Presentazione del case study
35
Figura 3.9. Bonifico: Diagramma di sequenza
La “sezione critica” è la porzione di codice che dà accesso a una risorsa condivisa
fra i flussi di esecuzione.
36
3 Java vs Go: gestione della concorrenza
3.2 La programmazione concorrente in Java
La programmazione, idealmente, si struttura seguendo un standard di esecuzione
sequenziale. Tale modello è applicabile a un programma eseguito da un singolo
processo. Nella realtà, diversi processi potrebbero dover essere in esecuzione contemporaneamente. Risulta, pertanto, necessario che i processi stessi agiscano in
parallelo.
Fintantoché i processi non interferiscono fra loro, se in presenza di un sistema
multi-processore, non è necessario prestare una considerevole attenzione, e, pertanto, l’esistenza di più di processi in esecuzione non è condizione sufficiente per
alterare il paradigma della programmazione sequenziale.
Il problema si complica se le attività dei processi, eseguite in parallelo, si sovrappongono accedendo a risorse software, o hardware, condivise. I processi sono, allora, in concorrenza. La programmazione concorrente è proprio l’oggetto del presente
capitolo.
3.2.1
Introduzione ai thread
Definizione di thread
Ogni processo effettua più attività, sia in foreground, che in background. L’esecuzione di tali attività potrebbe causare lunghi tempi di attesa. Risulta, pertanto,
necessario “parallelizzare” il processo, costruendo “processi leggeri” detti thread.
Poichè i thread associati ad uno stesso processo condividono la stessa porzione di
codice, per poter effettuare il contest switch, è necessaria la presenza di tre elementi
fondamentali, ovvero:
• Program Counter ;
• Registri ;
• Stack.
Vediamo, nel dettaglio, come si presentano i thread nel linguaggio di programmazione Java. In Java l’esecuzione dei thread è a carico della JVM.
Per eseguire il thread bisogna seguire le istruzioni riportate nel Listato 3.3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EsempioThread extends Thread{
private static final int rpt=2000;
private static final int delay=1;
public void run () {
try {
for (int i=1;i<=rpt;i++){
//azione da eseguire
sleep(delay);
}}
catch (InterruptedException exception){
//azione di pulizia}}
}
Listato 3.1. Esempio di thread
Il corpo del thread è costituito dal metodo run(); all’interno del metodo, infatti,
occorre definire il codice che si vuole fare eseguire.
3.2 La programmazione concorrente in Java
37
Si implementa un ciclo for all’interno del quale è definito il comportamento del
thread e la variabile static final int rpt descrive il numero di volte che si vuole
provare ad eseguire il thread stesso.
Alla fine del token, è inserito un metodo sleep. Esso pone in uno stato dormiente
il thread attuale per un numero di millisecondi specificato nella variabile delay.
Tuttavia, far “dormire” un thread, seppur necessario alla risoluzione del programma,
può rivelarsi un’azione potenzialmente fatale, portando in deadlock tutto il sistema.
Si inserisce, pertanto, il corpo del codice in un blocco try-catch. Il thread in attesa
viene interrotto, generando una eccezione, che, dopo le opportune azioni di pulizia,
farà terminare il thread stesso.
Il thread, per essere eseguito, deve, naturalmente, essere avviato, tramite il
metodo start(), cosı̀ come mostrato nel Listato 3.2.
1
2
3
4
public class Main {
public static void main(String[] args){
EsempioThread e1=new EsempioThread();
e1.start()}}
Listato 3.2. Metodo start()
Cosı̀ come è stato avviato, è possibile fermare, sospendere e riprendere il flusso
di esecuzione di un thread, attraverso i metodi riportati nel Listato 3.3.
Tuttavia, tali metodi sono altamente sconsigliati, poichè agiscono in maniera
“brutale” sul thread stesso, causando, nella maggior parte dei casi, il deadlock del
sistema, a causa, ad esempio, del verificarsi di situazioni di “possesso e attesa”.
1
2
3
4
5
6
7
public class Main {
public static void main(String args[]){
EsempioThread e1 = EsempioThread();
e1.suspend();
e1.resume();
e1.stop();
}}
Listato 3.3. Metodi per la gestione del flusso di un thread
Multithreading
Un programma Java, quando viene avviato, presenta l’esecuzione del codice all’interno del metodo main().
La JVM istanzia un processo cui è associato un solo thread, ovvero il thread
responsabile dell’esecuzione dello stesso metodo main().
Da tale considerazione risulta chiaro che se all’interno del metodo main() viene
mandato in esecuzione anche solo un thread, in realtà, ne avremo in esecuzione
almeno due. Il programma Java in esecuzione è, pertanto, un programma multithreading. Tali thread vengono eseguiti in parallelo, come se fossero due processi
distinti. Come è semplice immaginare, la JVM permette l’esecuzione non solo di
due singoli thread, ma anche, di più thread in parallelo.
Su un singolo processore, il parallelismo è, in realtà, virtuale. I thread, infatti,
vengono eseguiti dalla CPU uno alla volta. L’esecuzione in ordine di thread multipli
su una singola CPU è detto scheduling. L’algoritmo di scheduling utilizzato da Java
è il “fixed priority”.
38
3 Java vs Go: gestione della concorrenza
I thread vengono, pertanto, schedulati in base alla loro priorità. Java permette,
inoltre, la modifica di tali priorità per mezzo del metodo setPriority. Di conseguenza, se più thread sono pronti per l’esecuzione, la CPU viene occupata dal thread
con priority maggiore. Se due o più thread hanno stessa priorità, l’algoritmo di
scheduling segue un modello di tipo round-robin, ossia un algoritmo dove la coda è
trattata in maniera circolare.
L’algoritmo di scheduling in Java, inoltre, è di tipo preemptive: se, in un determinato istante, il thread con priorità massima diventa eseguibile, allora lo stesso
viene scelto per occupare la CPU.
Se i thread in parallelo non hanno accesso a risorse condivise, essi sono “asincroni” e possono, quindi, evolvere indipendentemente l’uno dall’altro. Ma se i thread
hanno accesso una risorsa condivisa come, ad esempio, il case study di interesse,
bisogna sincronizzare l’accesso alla sezione critica, ponendo la mutua esclusione e
garantendo, pertanto, l’atomicità delle operazioni nella sezione critica stessa.
Il problema può essere risolto attraverso il metodo synchronized:
“A synchronized statement acquires a mutual-exclusion lock on behalf of the
executing thread, executes a block, then releases the lock. While the executing
thread owns the lock, no other thread may acquire the lock.”1
Il Listato 3.4 descrive un esempio di implementazione del metodo synchronized.
1
2
3
4
5
6
7
8
9
10
11
12
public synchronized void Deposit (float amount){
System.out.println(Deposito: "+amount+" euro.");
setBalance(getBalance()+amount);
notifyAll();}
public synchronized void Withdraw (float amount)throws InterruptedException{
while(getBalance()<amount){
System.out.println(Ritiro: "+amount+" euro. SALDO INSUFFICIENTE!");
wait();}
System.out.println(Ritiro: "+amount+" euro.");
setBalance(getBalance()-amount);
}}
Listato 3.4. Il metodo synchronized
All’interno di un oggetto che contiene delle sezioni sincronizzate è possibile notare i metodi wait(), notify() e notifyAll(), implementati nativamente nella
classe Object. A gestire le code wait set ed entry set è, ancora una volta la JVM.
• wait(): il thread che lo invoca viene sospeso, rilasciando il lock, e viene inserito
nella coda wait set;
• notify(): quando viene invocato, un thread viene estratto dalla wait set e
inserito in entry set;
• notifyAll(): tutti i thread della wait set vengono spostati nell’entry set.
Tali metodi permettono la cooperazione dei thread, garantendo, pertanto, le operazioni multithreading.
1
R Language
J. Gosling - B. Joy - G. Steele- G. Bracha - A. Buckley, The Java⃝
Specification Java SE 8 Edition, p.446
3.2 La programmazione concorrente in Java
39
Implementazione dell’interfaccia Runnable
Esistono due metodi per implementare i thread. Il primo, trattato nelle sezioni
precedenti, prevede di estendere la classe Thread. Tuttavia, l’estensione a tale classe
può risultare problematica poichè Java non supporta l’ereditarietà multipla.
Se, ad esempio, anzichè considerare le classi Deposit e Withdraw come classi
separate, le considerassimo come sottoclassi della superclasse Transaction, non
potremmo utilizzare i thread a loro associati. Java, pertanto, pone una “scappatoia”
implementando i thread nell’interfaccia Runnable. Nel Listato 3.5 viene descritto
un esempio.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Transaction implements Runnable {
private CurrentAccount CA;
private float amount;
private String type
private static final int rpt=2000;
private static final int delay=1;
public Transaction(CurrentAccount cA, float amount, String type) {
super();
CA = cA;
this.amount = amount;
this.type = type;
}
public void run () {
try {
for (int i=1;i<=rpt;i++){
if (type="Deposito)
CA.Deposit(amount, i);
if (type="Withdraw")
CA.Withdraw
sleep(delay);
}}
catch (InterruptedException exception){ }
}}
Listato 3.5. Esempio di utilizzo dell’interfaccia Runnable
Per far partire l’interfaccia Runnable si procede in maniera simile a come si è
visto con i metodi che estendono la classe Thread.
3.2.2
Implementazione del case study
Dato il diagramma delle classi, per studiare il comportamento della programmazione
concorrente si è deciso di considerare soltanto la classe CurrentAccount e le sue
transazioni Read, Deposit, Withdraw e Transfer. Per prima cosa costruiamo le
classi per la gestione delle transazioni relative alla lettura del saldo, al deposito, al
ritiro, e al trasferimento, descritte nei Listati 3.6-3.9.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ReadRun extends Thread{
private CurrentAccount CA;
private int rpt;
private int delay;
public ReadRun(CurrentAccount cA, int rpt, int delay) {
CA = cA;
this.rpt = rpt;
this.delay = delay; }
public void run () {
try {
for (int i=1;i<=rpt;i++){
CA.ReadBalance(i);
sleep(delay); }}
catch (InterruptedException exception){ }
}}
40
3 Java vs Go: gestione della concorrenza
Listato 3.6. La classe ReadRun per la lettura del saldo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DepositRun extends Thread{
private CurrentAccount CA;
private float amount;
private int rpt;
private int delay;
public DepositRun(CurrentAccount cA, float amount, int rpt, int delay) {
CA = cA;
this.amount = amount;
this.rpt = rpt;
this.delay = delay; }
public void run () {
try {
for (int i=1;i<=rpt;i++){
CA.Deposit(amount, i);
sleep(delay); }}
catch (InterruptedException exception){ }
}}
Listato 3.7. La classe DepositRun per la gestione del deposito
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WithdrawRun extends Thread{
private CurrentAccount CA;
private float amount;
private int rpt;
private int delay;
public WithdrawRun(CurrentAccount cA, float amount, int rpt, int delay) {
CA = cA;
this.amount = amount;
this.rpt = rpt;
this.delay = delay; }
public void run () {
try {
for (int i=1;i<=rpt;i++){
CA.Withdraw(amount, i);
sleep(delay);
}}
catch (InterruptedException exception){ }
}}
Listato 3.8. La classe WithdrawRun per la gestione dei prelievi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TransferRun extends Thread{
private CurrentAccount CAw;
private CurrentAccount CAd;
private float amount;
private int rpt;
private int delay;
public TransferRun(CurrentAccount cAw, CurrentAccount cAd, float amount, int rpt, int delay) {
CAw = cAw;
CAd = cAd;
this.amount = amount;
this.rpt = rpt;
this.delay = delay; }
public void run (){
try {
for (int i=1;i<=rpt;i++){
CAw.Withdraw(amount, i);
CAd.Deposit(amount, i);
sleep(delay);
}}
catch (InterruptedException exception){}
}}
Listato 3.9. La classe TransferRun per la gestione dei bonifici
Successivamente, costruiamo la classe relativa al conto corrente (Listato 3.10).
3.2 La programmazione concorrente in Java
41
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class CurrentAccount {
private String iban;
private String number;
private float balance;
public CurrentAccount() {}
public CurrentAccount(String iban, String number, float balance){
this.iban = iban;
this.number = number;
this.balance = balance; }
public String getIban() {
return iban;}
public void setIban(String iban) {
this.iban = iban;}
public String getNumber() {
return number;}
public void setNumber(String number) {
this.number = number;}
public float getBalance() {
return balance;}
public void setBalance(float balance) {
this.balance = balance;}
public synchronized void ReadBalance (int i){
System.out.println(Thread.currentThread().getName()+"i="+i+
"}:Conto corrente n."+number+". Saldo: "+getBalance()+" euro."); }
public synchronized void Deposit (float amount, int i){
ReadBalance(i);
System.out.println(Thread.currentThread().getName()+"i="+i+
"}:Deposito: "+amount+" euro.");
setBalance(getBalance()+amount);
ReadBalance(i);
notifyAll();}
public synchronized void Withdraw (float amount, int i)throws InterruptedException{
while(getBalance()<amount){
ReadBalance(i);
System.out.println(Thread.currentThread().getName()+"i="+i+
"}:Ritiro: "+amount+" euro. SALDO INSUFFICIENTE!");
wait();}
ReadBalance(i);
System.out.println(Thread.currentThread().getName()+"i="+i+
"}:Ritiro: "+amount+" euro.");
setBalance(getBalance()-amount);
ReadBalance(i);}}
Listato 3.10. La classe CurrentAccount
Infine, nel Listato 3.11, viene descritto il metodo main().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Main {
public static final int rpt=2000;
public static final int delay=1;
public static void main(String[] args) {
CurrentAccount CA1 = new CurrentAccount("ITK","A",10600);
CurrentAccount CA2 = new CurrentAccount("SHK","B",11500);
System.out.println("Conto corrente n."+CA1.getNumber()+". Saldo iniziale: "+CA1.getBalance()+
"euro\n");
System.out.println("Conto corrente n."+CA2.getNumber()+". Saldo iniziale: "+CA2.getBalance()+
"euro\n");
TransferRun t12=new TransferRun (CA1,CA2,200, rpt,delay);
TransferRun t21=new TransferRun (CA2,CA1,1200, rpt,delay);
ReadRun r1=new ReadRun(CA1, rpt, delay);
ReadRun r2=new ReadRun(CA2, rpt, delay);
DepositRun d1=new DepositRun(CA1,350, rpt, delay);
DepositRun d2=new DepositRun(CA2,400, rpt, delay);
WithdrawRun w1=new WithdrawRun(CA1,300, rpt, delay);
WithdrawRun w2=new WithdrawRun(CA2,700, rpt, delay);
t12.setName("{Bonifico, Conto corrente n."+CA1.getNumber()+"-> n."+CA2.getNumber()+", ");
t21.setName("{Bonifico, Conto corrente n."+CA2.getNumber()+"-> n."+CA1.getNumber()+", ");
r1.setName("{Lettura Saldo, Conto corrente n."+CA1.getNumber()+", ");
r2.setName("{Lettura Saldo, Conto corrente n."+CA2.getNumber()+", ");
d1.setName("{Deposito, Conto corrente n."+CA1.getNumber()+", ");
d2.setName("{Deposito, Conto corrente n."+CA2.getNumber()+", ");
42
3 Java vs Go: gestione della concorrenza
34
35
36
37
38
39
40
w1.setName("{Ritiro, Conto corrente n."+CA1.getNumber()+", ");
w2.setName("{Ritiro, Conto corrente n."+CA2.getNumber()+", ");
r1.start();r2.start();
d1.start();d2.start();
w1.start();w2.start();
t12.start();t21.start(); } }
Listato 3.11. La classe Main() per l’avvio di tutte le attività
Andiamo a vedere nel dettaglio il comportamento delle classi riportate nei listati:
• public class Main: Nel Listato 3.11 vengono, inizialmente, istanziati due oggetti della classe CurrentAccount. Tali oggetti possiedono come attributi iban,
number (ovvero, il numero di conto corrente) e balance (ovvero, il saldo iniziale). Successivamente, si stampano gli oggetti per dare una prima lettura. Fatto
ciò, si creano gli oggetti delle sottoclassi che estendono il thread e che servono per gestire le transazioni. In tali oggetti vengono inseriti, come attributi,
rispettivamente:
– In TransferRun: i due oggetti della classe CurrentAccount, l’importo da trasferire e le variabili static final rpt e delay, ossia il numero di ripetizioni
e il tempo di sleep, espresso in millisecondi per ogni thread;
– In DepositRun, WithdrawRun: un oggetto della classe CurrentAccount,
l’importo e le variabili static final rpt e delay;
– In ReadRun: Un oggetto della classe CurrentAccount e le variabili static
final rpt e delay.
Vengono, poi, rinominati i thread per avere una migliore lettura in fase di
esecuzione. Infine i thread vengono avviati tramite il metodo .start().
• public class ReadRun extends Thread: nel Listato 3.6, dichiarate le variabili
di ingresso, viene implementato il costruttore. Successivamente, viene inserito il
codice nel metodo run(). Si è pensato di inserire il codice di esecuzione del thread
in un ciclo for per far ripetere l’esecuzione rpt volte. Tutto il blocco viene poi
inserito nel consueto try - catch, in modo da far dormire il thread per delay
millisecondi. Quando il thread viene interrotto, l’eccezione viene catturata e il
thread viene terminato. Nel metodo run() è presente, inoltre, un’invocazione
al metodo ReadBalance della classe CurrentAccount, tramite l’oggetto CA, al
quale verrà passata la variabile i per una migliore lettura del codice in fase di
esecuzione.
• public class DepositRun extends Thread: il funzionamento è pressochè identico alla ReadRun. Il metodo invocato è, in questo caso, il DepositRun, al quale
verranno passate le variabili amount e i (Listato 3.7);
• public class WithdrawRun extends Thread: identico al DepositRun. Il metodo invocato è il Withdraw, al quale saranno passati sia l’amount che i (Listato
3.8);
• public class TransferRun extend Thread: In questo caso i metodi invocati
saranno due. Verranno, infatti, invocati, rispettivamente, il metodo Withdraw
per il mittente del bonifico e il metodo Deposit per il destinatario dello stesso.
(Listato 3.9);
• public class CurrentAccount: il Listato 3.10 descrive la classe CurrentAccount.
Il saldo del conto corrente è l’oggetto della concorrenza. Implementati i co-
3.3 La programmazione concorrente in Go
43
struttori e i metodi get e set di tale classe, passiamo ad analizzare gli altri
metodi.
– ReadBalance: riceve in ingresso la variabile i, sia dalla classe ReadRun che dai
metodi Deposit e Withdraw. In seguito, stampa il saldo del conto corrente;
– Deposit: invoca il metodo ReadBalance per una prima stampa, facendo visualizzare all’utente lo stato iniziale. Successivamente, effettua il versamento
tramite l’istruzione setBalance(getBalance()+amount); in seguito, reinvoca il ReadBalance. Infine, attraverso il metodo notifyAll(), risveglia i
thread in attesa.
– Withdraw: inizialmente vengono dichiarate le eccezioni del metodo, attraverso il comando throws InterruptedException. Finchè il saldo è minore
dell’importo, il thread che lo ha invocato viene messo in attesa, tramite il
metodo wait(). Viene in seguito risvegliato, attraverso il notifyAll() del
metodo Deposit. Se il saldo è maggiore rispetto all’importo viene effettuato
il ritiro tramite l’istruzione setBalance(getBalance()+amount). Analogalmente a quanto avviene per il metodoDeposit, il metodo Withdraw invoca,
al bisogno, la lettura del saldo, attraverso il metodo ReadBalance.
I metodi ReadBalance, Deposit e Withdraw sono sincronizzati tramite la presenza del metodo synchronized. Esso garantisce, pertanto, la mutua esclusione
all’accesso alla sezione critica.
Eseguendo il programma, avremo il risultato indicato in Figura 3.10
Figura 3.10. Esecuzione del programma
3.3 La programmazione concorrente in Go
3.3.1
Goroutine
La programmazione concorrente, nel linguaggio di programmazione Go, si incentra
sulle goroutine. Esse sono un’“astrazione” di un thread gestito dal Runtime di Go.
Pur essendo i costrutti che garantiscono la programmazione concorrente, le goroutine sono completamente diverse rispetto ai thread visti in Java. Esse sono,
sostanzialmente, funzioni in grado di agire in maniera concorrente ad altre funzioni. Infatti è, addirittura, possibile avere in esecuzione più goroutine su un singolo
thread.
Le goroutine sono assimilabili più al concetto di coroutine che di thread.
44
3 Java vs Go: gestione della concorrenza
Tuttavia, a differenza delle coroutine, garantiscono la realizzazione di concorrenza e parallelismo virtuale e comunicano tramite i channel, meccanismo che verrà
illustrato in dettaglio nella prossima sottosezione
In generale, i thread vengono schedulati dal Sistema Operativo a livello di kernel.
Tale operazione provoca in sequenza l’interruzione del thread corrente, il salvataggio
del corrispettivo valore nei registri, l’applicazione del contest switch, etc.; tutto ciò
comporta un notevole rallentamento del flusso di esecuzione.
Il Go runtime implementa il proprio scheduler specializzato nella gestione delle
goroutine concorrenti. A differenza dello scheduler a livello di kernel, il Go runtime non invoca, periodicamente, la schedulazione dei “processi leggeri”, ma ha
in sè dei meccanismi che vengono attivati da alcune “keyword” del linguaggio di
programmazione.
È importante sottolineare che la concorrenza di cui parliamo non implica necessariamente il parallelismo; nel caso “base”, non abbiamo un uso di più core simultaneamente, ma solo uno è fisicamente dedicato al programma, e solo una funzione
per volta può essere eseguita; tuttavia, esiste una concorrenza nella loro esecuzione.
Come detto nel capitolo precedente, Go non è ottimizzato per la programmazione
in parallelo.
“The number of CPUs available simultaneously to executing goroutines is controlled by the GOMAXPROCS shell environment variable. In earlier releases of
Go, the default value was 1, but as of Go 1.5 the default value is the number
of cores available. Therefore programs compiled after 1.5 should demonstrate
parallel execution of multiple goroutines.”2
Affinchè sia realizzato pienamente il parallelismo è necessario agire, pertanto,
sulla variabile GOMAXPROCS che gestisce il numero di threads a livello di sistema
operativo in grado di eseguire codice Go a livello utente.
La programmazione concorrente, in Go, viene effettuata, come detto in precedenza, tramite le goroutine e i channel.
L’implementazione delle goroutine, nel codice, è abbastanza semplice (Listato
3.12).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
package main
import (
"fmt"
"time" )
type CurrentAccount struct {
iban
string
number string
balance float64 }
func Deposit(S string, amount float64, balance float64) {
for i := 0; i < 5; i++ {
time.Sleep(1 * time.Millisecond)
balance += amount
fmt.Print(S, ": Il saldo è ", balance, "\n")
} }
func main() {
CA1 := CurrentAccount{"ITkkABBBBBCCCCCXXXXXX", "12345", 1000}
fmt.Println("Conto corrente n.", CA1.number, ",IBAN:",CA1.iban, ", Saldo:", CA1.balance)
var amount float64
amount = 5000
var Go string
Go = "Esempio Goroutine"
var No string
No = "Senza Goroutine"
var tmp = CA1.balance
https://golang.org/doc/faq/#Concurrency
3.3 La programmazione concorrente in Go
28
29
go Deposit(Go, amount, tmp)
Deposit(No, amount, tmp) }
45
Listato 3.12. Esempio di goroutine
Formalmente le goroutine vengono “lanciate”, semplicemente, dalla keyword
go seguita dall’invocazione di una funzione. Nel Listato 3.12, la funzione Deposit
esegue la somma algebrica fra l’importo e il saldo del conto corrente CA1.
In questo listato la funzione viene invocata due volte, poichè le goroutine, come detto precedentemente, devo essere eseguite in concorrenza con altre funzioni.
Quando l’esecuzione della funzione è ultimata, la goroutine termina semplicemente.
Il risultato finale è indicato in Figura 3.11
Figura 3.11. Risultato del Listato 3.12
Le goroutine hanno, inoltre, un’altra particolare proprietà. A differenza dei
thread di Java, quando il main ha terminato il suo compito, il programma non
attende la terminazione delle goroutine e, di conseguenza, esse vengono chiuse
“brutalmente”.
3.3.2
Channel
Per avere un’architettura concorrente sono necessari metodi di comunicazione tra i
vari processi. Per tale motivo si è resa necessaria l’implementazione dei channel.
I channel sono, essenzialmente, dei condotti tipati che mettono in comunicazione due funzioni, in esecuzione contemporanea, sia per sincronizzarne l’esecuzione
stessa, sia per la ricezione o l’invio di variabili. Il canale viene inizializzato attraverso la primitiva make, che specifica il canale in base al tipo di valori che è in grado
di veicolare. Opzionalmente, può essere specificata una capacità, ossia un buffer di
memoria che può contenere delle variabili.
L’accesso al buffer segue una modalità di tipo FIFO (First In, First Out). Se la
capacità è maggiore di zero, il channel sarà, allora, asincrono; in caso contrario, esso
sarà sincrono (Listati 3.13, 3.14). Nel caso di canale asincrono e nelle condizioni di
buffer non pieno e non vuoto, le operazioni di comunicazione hanno esito positivo
senza bloccarsi. Se la capacità è pari a zero o, in maniera del tutto equivalente,
è assente, la comunicazione ha successo solo quando mittente e destinatario sono,
effettivamente, sincronizzati.
46
3 Java vs Go: gestione della concorrenza
1
depositchan:= make(chan Deposit)
Listato 3.13. Esempio di canale sincrono
1
cj := make(chan int, 110)
Listato 3.14. Esempio di canale asincrono
Nel Listato 3.15, il canale riferisce a un particolare tipo di dato, nel nostro caso,
al dato Deposit
1
2
3
4
5
6
7
func PipeDeposit(depositchan chan Deposit){
var val Deposit
for{
val= <-depositchan
val.conto.deposito(val.amount)
}
}
Listato 3.15. Gestore di un canale sincrono
Quando un channel viene “passato” come parametro di funzione, è possibile specificare se il esso è specializzato soltanto nella ricezione o nella trasmissione di variabili.
L’operatore, a forma di freccia, riportato nel listato 3.16 specifica la direzione del
canale. Se nessuna direzione viene specificata, allora il canale è considerato bidirezionale. L’utilizzo dell’operatore aumenta notevolmente la type-safety del programma
in esecuzione. Possiamo, pertanto, immaginare la func dove viene mandato in ingresso un canale come un oggetto che prevede, essenzialmente, i metodi di ricezione
e tramissione. I channel supportano anche l’operazione di chiusura.
1
2
depositchan<- val // invio di un valore
val=<-depositchan //ricezione di un valore
Listato 3.16. Direzione del canale
3.3.3
Implementazione del codice
Nel Listato 3.17 viene riportata una possibile implementazione, in codice, del case
study di interesse.
Come spiegato in precedenza, le struct non contengono al loro interno metodi e funzioni; pertanto, possiamo definire, inizialmente, la struct CurrentAccount
esplicitando solo gli attributi.
Nel main() saranno subito instanziate le struct CA1 e CA2.
Subito dopo si “lanciano” legoroutine tramite il comando go func(); l’istruzione
è inserita in un ciclo for in modo da tentare di ripetere l’operazione un numero
specificato di volte (nel caso considerato, 2000 per ogni transazione).
Per poter creare i canali relativi alle transazioni, affianchiamo al CurrentAccount
le struct DepositStruct, WithdrawStruct e TransferStruct. Esse conterranno le
informazioni sui conto correnti che hanno avviato la procedura e sull’importo da
versare. La creazione di tali struct garantirà che i dati non verranno mai corrotti
da accessi concorrenti.
3.3 La programmazione concorrente in Go
47
All’interno del ciclo sono racchiuse le invocazioni di funzioni relative alle transazioni, ossia:
•
•
•
•
DepositRun: essa è la func relativa al deposito; ricevuti come parametri formali le variabili w, per poter tenere traccia dell’operazione, e amount, per indicare l’importo da depositare, crea un canale di DepositChannel attinente alla struct DepositStruct. Successivamente, stampa il saldo e istanzia la
struct relativa alla transazione da eseguire. Avvia, pertanto, la goroutine della
PipeDeposit; essa è una funzione intermedia che funge da collegamento fra la
struct DepositStruct e quella CurrentAccount. Allo stesso tempo si permette il passaggio dei valori, specificando la direzione del canale. La PipeDeposit,
infine, invoca la funzione Deposit , per poter eseguire la stessa transazione.
WithdrawRun: il funzionamento è identico alla DepositRun;
TransferRun: questa func riceve come parametri formali sia le variabili w e
amount che la struct CurrentAccount. La PipeTransfer, in questo caso, invoca
le funzioni Deposit e Withdraw relative, rispettivamente, al beneficiario e al
donatore.
ReadRun: è la func relativa alla lettura del saldo. Essa non riceve parametri
formali, ad eccezione della variabile w, per una maggiore lettura del risultato.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main
import (
"fmt"
"time" )
func (Car *CurrentAccount) ReadRun(w int) {
ReadChannel := make(chan CurrentAccount)
fmt.Println("GoSaldo - n◦ ", w)
Car.ReadBalance()
go PipeRead(ReadChannel) }
func (CAw *CurrentAccount) WithdrawRun(a float64, w int) {
WithdrawChannel := make(chan WithdrawStruct)
fmt.Println("GoRitiro - Conto corrente n.", CAw.number, "n◦ ", w, "importo:", a)
CAw.ReadBalance()
withdraw := WithdrawStruct{amount: a, CA: CAw}
go PipeWithdraw(WithdrawChannel)
WithdrawChannel <- withdraw
CAw.ReadBalance() }
func (CAd *CurrentAccount) DepositRun(a float64, w int) {
DepositChannel := make(chan DepositStruct)
fmt.Println("GoDeposito - Conto corrente n.", CAd.number,"Deposito n◦ ", w, "importo:", a)
CAd.ReadBalance()
deposit := DepositStruct{amount: a, CA: CAd}
go PipeDeposit(DepositChannel)
DepositChannel <- deposit
CAd.ReadBalance() }
func (CAwt *CurrentAccount) TransferRun(a float64, CAdt *CurrentAccount, w int) {
TransferChannel := make(chan TransferStruct)
fmt.Println("GoBonifico - Conto corrente n.", CAwt.number,"-> n.", CAdt.number, "n◦ ", w, "importo", a)
transfer := TransferStruct{CAw: CAwt, CAd: CAdt, amount: a}
go PipeTransfer(TransferChannel)
TransferChannel <- transfer
CAwt.ReadBalance()
CAdt.ReadBalance() }
type CurrentAccount struct {
iban
string
number string
balance float64 }
type TransferStruct struct {
CAw
*CurrentAccount
CAd
*CurrentAccount
amount float64 }
type DepositStruct struct {
amount float64
CA
*CurrentAccount }
type WithdrawStruct struct {
amount float64
CA
*CurrentAccount }
48
3 Java vs Go: gestione della concorrenza
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
func PipeRead(CAChannel chan CurrentAccount) {
var val CurrentAccount
for {
val = <-CAChannel
val.ReadBalance() } }
func PipeDeposit(DepositChannel chan DepositStruct) {
var val DepositStruct
for {
val = <-DepositChannel
val.CA.Deposit(val.amount) } }
func PipeWithdraw(WithdrawChannel chan WithdrawStruct) {
var val WithdrawStruct
for {
val = <-WithdrawChannel
val.CA.Withdraw(val.amount) } }
func PipeTransfer(TransferChannel chan TransferStruct) {
var val TransferStruct
for {
val = <-TransferChannel
val.CAw.Transfer(val.CAd, val.amount) } }
func (CA *CurrentAccount) Withdraw(amount float64) {
fmt.Println("Ritiro:", amount)
if amount > CA.balance {
fmt.Println("SALDO INSUFFICIENTE")
} else {
CA.balance -= amount } }
func (CA *CurrentAccount) ReadBalance() {
fmt.Println("Conto corrente n.", CA.number, "; Saldo:",
CA.balance) }
func (CA *CurrentAccount) Deposit(amount float64) {
fmt.Println("Deposito:", amount)
CA.balance += amount }
func (CAw *CurrentAccount) Transfer(CAd *CurrentAccount, amount float64) {
if amount > CAw.balance {
fmt.Println("SALDO INSUFFICIENTE")
} else {
CAw.Withdraw(amount)
CAd.Deposit(amount) } }
func main() {
CA1 := CurrentAccount{iban: "ITK1", number: "A", balance:10000}
CA2 := CurrentAccount{iban: "EH21", number: "B", balance:20000}
for w := 1; w < 2000; w++ {
go func() {
for {
CA1.WithdrawRun(500.0, w)
CA2.WithdrawRun(200.0, w)
CA2.ReadRun(w)
CA1.DepositRun(400.0, w)
CA2.DepositRun(300.0, w)
CA2.TransferRun(10.0, &CA1, w)
CA1.ReadRun(w)
CA1.TransferRun(60.0, &CA2, w)
}
}()
time.Sleep(1 * time.Millisecond) } }
Listato 3.17. Implementazione del codice
Il listato produrrà il risultato riportato in Figura 3.12
Figura 3.12. Risultato del Listato 3.17.
3.4 Conclusioni
49
3.4 Conclusioni
Grazie agli esempi mostrati è possibile effettuare alcune considerazioni sulla programmazione concorrente.
Go, pur essendo stato principalmente creato per velocizzare questo meccanismo,
non sembra garantire le prestazioni migliori.
All’aumentare delle goroutine e, quindi, dei “processi leggeri”concorrenti è possibile accorgersi che la programmazione concorrente mostra un significativo degrado
delle prestazioni quando il numero delle goroutine supera il numero di processori
che il calcolatore mette a disposizione.
Il problema principale sembra essere lo scheduler. Esso è gestito dal Go Runtime, il quale, non presentando un algoritmo ben definito come quello presente nella
schedulazione dei thread, non garantisce la gestione ottimale delle potenzialmente
numerosissime goroutine.
La Versione 1.5 ha provato a migliorare le prestazioni dello scheduler, implementando la variabile di ambiente GOMAXPROCS, in modo tale da impostare uno scheduler
su un numero di processori pari a quelli posseduti, effettivamente, dal calcolare. Lo
scheduling, tuttavia, per stessa ammissione del sito ufficiale:
“is not as good as it needs to be”3
Nel case study considerato si è, infatti, cercato di stressare il più possibile il
sistema, svolgendo 8000 ripetizioni del processo e un tempo di riposo pari a 1 ms.
Le goroutine non hanno ancora espresso le loro potenzialità e sembrano essere ben
lontani dal mantenere le promesse degli sviluppatori del linguaggio.
Pertanto, sembra inutile poter far “girare” decine di goroutine su ogni thread, se
già, per le specifiche tecniche del calcolatore utilizzato, poche decine di esse possono
rallentare notevolmente le prestazioni del sistema.
Sfruttando il semplice algoritmo riportato nel Listato 3.18, si è potuto notare,
infatti, come Java, a differenza di Go, riesca ad ottenere un tempo d’esecuzione
inferiore ai 2,8 secondi, pur non essendo un linguaggio compilato, e pur presentando
la JVM e un contest-switch dei processi invadente a livello di I/O.
1
2
3
4
long inizio = System.currentTimeMillis();
long fine = System.currentTimeMillis();
long tempo=(fine-inizio);
System.out.println ("Tempo di esecuzione ="+tempo+ " ms\n");
Listato 3.18. Algoritmo per il calcolo del tempo di esecuzione in Java
Utilizzando un algoritmo simile in Go (Listato 3.19), abbiamo ottenuto risultati
ben diversi.
1
2
3
start := time.Now()
elapsed := time.Since(start)
fmt.Println("Tempo di esecuzione=:", elapsed)
Listato 3.19. Algoritmo per il calcolo del tempo di esecuzione in Go
3
https://golang.org/doc/faq#Why GOMAXPROCS
50
3 Java vs Go: gestione della concorrenza
Il tempo di esecuzione del programma è riuscito a raggiungere un minimo di 3,5
secondi, ossia un tempo maggiore del 25% rispetto a quello registrato da Java.
Seppur nel case study considerato, Golang non si dimostra “performante” nei
tempi di esecuzione, lo è sicuramente nelle tecniche di implementazione. Interessantissima sembra la gestione dei channel condivisi. Essi, oltre a permettere la comunicazione fra le goroutine, pongono, in backgroud, meccanismi di lock per serializzare
l’accesso alle sezioni critiche e garantire, pertanto, la thread safety.
Siamo sicuri che, in futuro, risolti i problemi dello scheduler, Go possa soddisfare
le aspettative richieste.
4
Java vs Go: la gestione delle funzioni
In questo capitolo si illustrerà la gestione delle funzioni in Java e in Go. In particolare, verranno trattati il polimorfismo dei metodi, le funzioni anonime e i return
multipli.
4.1 Il concetto di sottoprogramma
Nelle stesura di un programma è possibile riscontrare problemi simili in porzioni
del codice diverse.
Una soluzione potrebbe essere replicare l’algoritmo risolutivo nelle porzioni di
codice dove è presente il medesimo problema. Tale soluzione, seppur appare la più
ovvia, non si rivela essere la più efficiente. Un eventuale errore nella stesura dell’algoritmo potrebbe portare, infatti, in “tilt” tutto il sistema. Inoltre, un eventuale
aggiornamento dell’algoritmo renderebbe obbligatorio l’aggiornamento di tutte le
sue repliche.
Una soluzione migliore è quella di realizzare un sottoprogramma costituito da
un algoritmo, separato dal flusso di istruzioni principale, dedicato alla risoluzione
di sottoproblemi simili o identici. Il programma, pertanto, potrà invocare il sottoprogramma qualora siano necessarie le funzionalità di quest’ultimo. Tramite tale
meccanismo è possibile garantire l’efficienza, la manutenzione, il riuso e, soprattutto,
la facilità di programmazione.
Per poter definire la funzione del sottoprogramma è necessario, innanzitutto,
dichiarare il nome della funzione, le variabili che devono essere poste in ingresso e il
risultato che si vuole restituire. Una volta dichiarata la funzione, quest’ultima deve
essere implementata; si stila, pertanto, l’algoritmo di risoluzione del sottoproblema.
La gestione di tali funzioni è proprio l’argomento principale del presente capitolo.
4.2 Gestione delle funzioni in Java
Nei linguaggi che supportano il paradigma OOP, la gestione dei sottoproblemi viene
affidata ai concetti di oggetti e di classe. Questi ultimi, a loro volta, utilizzano i
metodi per risoluzione degli stessi sottoproblemi.
52
4 Java vs Go: la gestione delle funzioni
4.2.1
I metodi
Come già accennato, nel primo capitolo, i metodi sono le funzionalità che vengono
messe a disposizione da una particolare classe.
Per poter meglio comprendere il concetto di metodo, si consideri la classe
CurrentAccount riportata nel Listato 4.1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class CurrentAccount {
private String iban;
private String number;
private float balance;
public CurrentAccount() {}
public CurrentAccount(String iban, String number, float balance) {
this.iban = iban;
this.number = number;
this.balance = balance;
}
public String getIban() {
return iban;
}
public void setIban(String iban) {
this.iban = iban;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public float getBalance() {
return balance;
}
public void setBalance(float balance) {
this.balance = balance;
} }
Listato 4.1. Classe CurrentAccount
All’interno della stessa classe viene definito il metodo Withdraw (Listato 4.2).
La definizione di un metodo, in Java, è molto simile alla definizione di variabile, con
la quale ha in comune numerose caratteristiche.
1
public float Withdraw (float amount)
Listato 4.2. Definizione del metodo Withdraw
Nella definizione, il termine Withdraw è detto identificatore. I parametri racchiusi tra parentesi tonde, subito dopo l’identificatore, sono definiti parametri formali ;
essi descrivono l’insieme di variabili che il blocco di codice riceverà in ingresso dal
programma chiamante, per poter portare a termine l’algoritmo risolutivo. Nell’esempio considerato, il metodo riceve in ingresso la variabile amount di tipo float. L’
identificatore e i parametri formali costituiscono la cosiddetta “firma” del metodo.
Dopo l’elaborazione delle variabili formali, il metodo restituirà, una variabile
d’uscita. Il tipo di variabile d’uscita è detta tipo di ritorno; esso è il termine prima
della “firma” e nel caso considerato il tipo di ritorno è float.
Terminata la fase di definzione, è possibile passare alla fase di implementazione,
ossia alla stesura del codice che elaborerà i parametri formali (Listato 4.3). In
tale listato è possibile notare la dichiarazione, all’interno del metodo stesso, della
varibiale tmp di tipo float. Le variabili dichiarate all’interno del metodo vengono
definite parametri attuali. L’algoritmo del metodo si arresta quando esso incontra
la keyword return. La variabile specificata subito dopo la parola chiave deve essere
dello stesso tipo del tipo di ritorno, specificato in fase di definizione.
4.2 Gestione delle funzioni in Java
53
1
2
3
4
5
6
7
8
9
public float Withdraw (float amount){
System.out.println("Ritiro: "+amount+" euro.");
float tmp;
if (getBalance()<amount){
System.out.println("Il saldo è insufficiente. Operazione annullata");
tmp=getBalance();}
else {tmp = getBalance()-amount;
setBalance(tmp);}
return tmp;}
Listato 4.3. Implementazione del metodo Withdraw
Esaminando il Listato 4.3 è possibile effettuare un’altra considerazione; nella struttura di controllo if, il metodo assegna al parametro tmp un valore ottenuto invocando il metodo getBalance() della classe CurrentAccount, riportato, precedentemente, nel Listato 4.1. Il metodo get(), a differenza del metodo Deposit, non
riceve in ingresso alcun parametro formale restituendo, in uscita, il float desiderato, ossia il saldo del conto corrente. Viene invocato, successivamente, il metodo
setBalance(tmp). Quest’ultimo riceve in ingresso il parametro formale tmp, ma
non presenta variabili in uscita. Oltre a metodi dove sono assenti i parametri formali, è, pertanto, possibile, definire dei metodi che non presentano tipo di ritorno,
ma eseguono semplicemente delle istruzioni.
I metodi presenti in una classe possono essere invocati anche all’esterno delle
classe di appartenenza. Ad esempio, invochiamo il metodo Deposit all’interno del
main(), come mostrato nel Listato 4.4.
1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
CurrentAccount CA1 = new CurrentAccount("ITK", "A",10600);
float amount1=500;
float amount2=12000;
float tmp1=CA1.Withdraw(amount1);
System.out.println("Saldo: "+tmp1);
float tmp2=CA1.Withdraw(amount2);
System.out.println("Saldo: "+tmp2); }}
Listato 4.4. Invocazione del metodo Withdraw
L’accesso al metodo Withdraw avviene tramite l’operatore “dot”. La struttura
sintattica è identica all’accesso alle variabili di un oggetto. Pertanto, sia i metodi,
sia gli attributi definiti public di una classe sono accessibili tramite tale operatore.
Quando viene avviato il programma descritto dal Listato 4.4, la macchina virtuale esegue, inizialmente, le istruzioni del metodo main() in maniera sequenziale.
Quando viene dichiarata la variabile tmp1, essa viene inizializzata tramite l’invocazione al metodo Withdraw. La macchina virtuale, alla chiamata al metodo, esegue
un “salto” alla parte di codice contenuta nel metodo stesso. Essa inizializza, di conseguenza, i parametri formali ed esegue le istruzioni del sottoprogramma finchè non
incontra la parola chiave return. A questo punto, essa effettua un nuovo “salto” per
tornare all’esecuzione del programma e assegna al valore tmp1 il valore di ritorno del
metodo Withdraw. Il risultato dell’operazione riportata nel Listato 4.4 è indicato in
Figura 4.1.
54
4 Java vs Go: la gestione delle funzioni
Figura 4.1. Risultato ottenuto dal Listato 4.4
4.2.2
Polimorfismo per metodi
I metodi possono essere poliformi. Il polimorfismo è un concetto che è stato importato nella OOP dalla realtà di tutti i giorni. Esso può essere definito come la facoltà
di assumere aspetti e forme differenti a seconda delle circostanze.
Il polimorfismo per metodi consente, pertanto, di utilizzare lo stesso identificatore per funzionalità che risolvono problematiche differenti.
Esso può essere realizzato, sostanzialmente, tramite due configurazioni:
• Overload, ossia sovraccarico.
• Override, ossia riscrittura.
Overload
L’overload è il polimorfismo che sfrutta la definizione dei metodi. In Java, infatti,
i metodi sono univocamente determinati dalle loro “firme”, ossia dall’insieme di
parametri formali e dall’identificatore. Pertanto, come è ovviamente possibile definire metodi con un diverso identificatore, ma identici parametri formali, è possibile
definire, anche all’interno di una stessa classe, metodi con lo stesso identificatore,
ma parametri formali ben distinti.
Per una maggiore lettura del codice, è preferibile utilizzare il polimorfismo per
metodi che hanno, concettualmente, identiche funzionalità, ma implementazioni
differenti.
Facciamo qualche esempio. Consideriamo la classe CurrentAccount, riportata
nel Listato 4.5.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CurrentAccount {
private String iban;
private String number;
private float balance;
public CurrentAccount() {}
public CurrentAccount(String iban, String number, float balance) {
this.iban = iban;
this.number = number;
this.balance = balance;
}
public String getIban() {
return iban;
}
public void setIban(String iban) {
this.iban = iban;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public float getBalance() {
return balance;
4.2 Gestione delle funzioni in Java
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
55
}
public void setBalance(float balance) {
this.balance = balance;
}
public float Deposit (float amount){
System.out.println("Deposito: "+amount+" euro.");
float tmp;
tmp = getBalance()+amount;
setBalance(tmp);
return tmp;}
public float Withdraw (float amount){
System.out.println("Ritiro: "+amount+" euro.");
float tmp;
if (getBalance()<amount){
System.out.println("Il saldo è insufficiente. Operazione annullata");
tmp=getBalance();}
else{ tmp = getBalance()-amount;
setBalance(tmp);
return tmp;}}}
Listato 4.5. CurrentAccount senza polimorfismo
Supponiamo che l’operazione di ritiro, svolta in una banca differente da quella di
appartenenza del conto corrente, sia automaticamente accompagnata da un costo di
transazione non trascurabile. Avremo, allora, bisogno di un metodo poliforme con
stessa identificazione, ma parametri formali differenti. L’esempio di polimorfismo di
tipo overload è riportato nel Listato 4.6.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public float Withdraw (float amount){
System.out.println("Ritiro: "+amount+" euro.");
float tmp;
if (getBalance()<amount){
System.out.println("Il saldo è insufficiente. Operazione annullata");
tmp=getBalance();}
else {tmp = getBalance()-amount;
setBalance{tmp};}
return tmp;}
public float Withdraw (float amount, float cost){
System.out.println("Ritiro: "+amount+" euro.");
float tmp;
if (getBalance()<amount+cost){
System.out.println("Il saldo è insufficiente. Operazione annullata");
tmp=getBalance();}
else {tmp = getBalance()-amount-cost;
setBalance{tmp};}
return tmp;}
Listato 4.6. Esempio di polimorfismo di tipo overload
Java presenta, nativamente, numerosi metodi che implementano un polimorfismo di tipo overload. Fra questi, degno di nota è il metodo println(). Con
l’identificatore println, infatti, non viene indicato un unico metodo, ma una famiglia di metodi con stesso identificatore, appunto println(), alla quale è possibile
passare stringhe, array, int, etc.; i metodi println() poliformi sono dieci e sono
descritti nella Figura 4.2.
56
4 Java vs Go: la gestione delle funzioni
Figura 4.2. Polimorfismo di tipo overload per i metodi println()
Override
L’override è ritenuto uno dei maggiori punti di forza del linguaggio di programmazione Java.
Tale meccanismo, come può suggerire il nome, è la caratteristica di una sottoclasse di poter riscrivere un metodo ereditato dalla superclasse.
Come è possibile intuire, una classe può utilizzare il polimorfismo di tipo override se, e solo se, estende un’altra classe; pertanto, l’override implica il concetto di
ereditarietà.
Cerchiamo di capire meglio il polimorfismo di tipo override; supponiamo che un
conto corrente possa essere di tipo fruttifero e, pertanto, matura un certo interesse annuale. Creiamo, allora, la sottoclasse InterestBearingAccount (Listato 4.7)
della superclasse CurrentAccount (Listato 4.5).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class InterestBearingAccount extends CurrentAccount {
private float percentage;
private year;
public float getPercentage() {
return percentage; }
public int getYear() {
return year; }
public void setPercentage(float
percentage) {
this.percentage = percentage; }
public void setYear(int year) {
this.year = year; }
public InterestBearingAccount() {
super();}
public InterestBearingAccount(String iban, String number, float balance, float percentage, int year) {
super(iban, number, balance);
this.percentage = percentage;
this.year=year; } }
Listato 4.7. Classe InterestBearginAccount
Dichiariamo, pertanto, un metodo che permette di calcolare, in euro, l’interesse
maturato e importiamo i metodi della superclasse (Listato 4.8).
1
2
public class InterestBearingAccount extends CurrentAccount {
private float percentage;
4.2 Gestione delle funzioni in Java
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
57
private int year;
public float getPercentage() {
return percentage; }
public int getYear() {
return year; }
public void setYear(int year) {
this.year = year; }
public void setPercentage(float percentage) {
this.percentage = percentage; }
public InterestBearingAccount() {
super();
// TODO Auto-generated constructor stub }
public InterestBearingAccount(String iban, String number, float balance, float percentage, int year) {
super(iban, number, balance);
this.percentage = percentage;
this.year=year; }
@Override
public float Deposit(float amount) {
// TODO Auto-generated method stub
return super.Deposit(amount);
}
@Override
public float Deposit(float amount, float transactioncost) {
// TODO Auto-generated method stub
return super.Deposit(amount, transactioncost);
}
@Override
public float Withdraw(float amount) {
return super.Withdraw(amount);
}
@Override
public float Withdraw(float amount, float cost) {
return super.Withdraw(amount, cost);
}
public float increment() {
float C=(balance*percentage*year)/100;
System.out.println("L’interesse maturato è: ");
return (C); }}
Listato 4.8. Metodi ereditati dalla superclasse
I metodi Withdraw della superclasse non considerano l’interesse maturato; pertanto, utilizzeremo il polimorfismo di tipo override per meglio “adattare” il metodo
alla sottoclasse InterestBearingAccount (Listato 4.9).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public float Withdraw(float amount) {
balance+=increment();
System.out.println("Saldo maturato:"+balance);
return super.Withdraw(amount);
}
@Override
public float Withdraw(float amount, float cost) {
balance+=increment();
System.out.println("Saldo maturato:"+balance);
return super.Withdraw(amount, cost);
}
public float increment() {
float C=(balance*percentage*year)/100;
System.out.println("L’interesse maturato è: "+C);
return (C); }}
Listato 4.9. Override dei metodi
Quando l’oggetto appartenente alla sottoclasse InterestBearingAccount invoca uno dei metodi Withdraw, sarà prima calcolato, in euro, il tasso di inte-
58
4 Java vs Go: la gestione delle funzioni
resse e aggiornato il saldo. Successivamente si procederà con l’algoritmo del metodo Withdraw specificato nella superclasse. Il metodo main(), strutturato come
riportato nel Listato 4.10, produrrà il risultato descritto in Figura 4.3.
1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
CurrentAccount CA2= new InterestBearingAccount("KI","B",5100, 22, 8);
float amount=12000;
float tmp=CA2.Withdraw(amount);
System.out.println("Saldo: "+tmp);}}
Listato 4.10. Override: main()
Figura 4.3. Risultato del metodo in override
Esistono regole precise per l’implementazione del polimorfismo di tipo override;
più specificatamente, esse riguarderanno:
• La “firma”: il metodo override appartenente alla sottoclasse deve avere la stessa
“firma” del metodo contenuto nella superclasse.
• Il tipo di ritorno: i tipi di ritorno del metodo della superclasse e della sottoclasse
devono coincidere.
• Gli indicatori di visibilità: il metodo della sottoclasse deve avere un indicatore
di visibilità pari o inferiore al metodo della superclasse.
4.3 Gestione delle funzioni in Go
In Go la risoluzione dei sottoproblemi viene gestita in una forma “ibrida” fra la
programmazione OOP e quella procedurale. Go presenta, infatti, le tipiche funzioni
della programmazione procedurale che, però, possono essere “collegate” alle strutture, tramite i puntatori, realizzando una gestione delle funzioni notevolmente simile
ai metodi delle classi in Java.
4.3.1
Le funzioni
La definizione delle func è piuttusto simile a quella presente nei metodi di Java
(Listato 4.11).
1
2
3
func Deposit (amount float64 , balance float64) float64{
balance+=amount
return balance}
4.3 Gestione delle funzioni in Go
59
Listato 4.11. Definizione e implementazione di una func
La funzione è introdotta dalla parola chiave func. Subito dopo trovano spazio
l’identificatore e i parametri formali, che, come in Java, racchiudono la “firma” della
funzione stessa. Nella definizione è, infine, presente il tipo di ritorno.
La parentesi graffa introduce l’algoritmo per la soluzione del problema. A differenza di Java, essa deve essere scritta, obbligatoriamente, nella stessa linea della
parola chiave func.
Data l’assenza di ereditarietà, il linguaggio Go non permette, tipicamente, il
polimorfismo, sia di tipo override che di tipo overload.
Come in Java è, invece, possibile avere funzioni dove i parametri formali e/o il
tipo di restituzione sono assenti.
Le modalità con cui si può richiamare una funzione sono mostrate nel Listato
4.12.
1
2
3
4
5
6
7
8
func main(){
ca1:=CA {iban:"A",number:"A",balance:5}
var c = ca1.balance
var amount float64
amount=100
var d = Deposit(amount, c)
ca1.balance=d
fmt.Println("saldo: ",ca1.balance)}
Listato 4.12. Modalità con cui si può richiamare una funzione in Go
Come detto in precedenza, è possibile “collegare” una funzione a una struct,
rendendo le definizioni di struttura e di funzione concetti molto simili alle definizioni
di classi e di metodi (Listato 4.13).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"
type CurrentAccount struct {
iban string
number string
balance float64
}
func (CA1 *CurrentAccount) Deposit(amount float64){
fmt.Println("Deposito:", amount)
CA1.balance+=amount
fmt.Println("Conto corrente n.",CA1.number,
",IBAN:",CA1.iban,", Saldo:",CA1.balance ) }
func main() {
CA1 :=CurrentAccount{"ITkkABBBBBCCCCCXXXXXX","12345",1000}
fmt.Println("Conto corrente n.",CA1.number,
",IBAN:",CA1.iban,", Saldo:",CA1.balance )
var amount float64
amount=2000
CA1.Deposit(amount) }
Listato 4.13. Struct “collegata” a una func
I puntatori hanno un funzionamento nettamente diverso dai valori. Nel Listato 4.14 utilizzeremo la funzione valzero, che assegna, per l’appunto, il valore 0
alla variabile e la funzione pntzero, che assegna il valore 0 al puntatore della variabile. Alla funzione valzero viene assegnato come parametro formale un dato
float64, mentre alla funzione pntzero viene assegnato un puntatore. Il passaggio
per riferimento è garantito dal parametro “&”.
Le due funzioni non presentano un tipo di ritorno. Quando viene invocata
valzero(amount), si porrà, pertanto, a zero soltanto la variabile attuale. Di conse-
60
4 Java vs Go: la gestione delle funzioni
guenza i valori di amount, prima e dopo la func, coincidono e sono pari sempre a
165. Quando, invece, viene invocata la funzione pntzero(amount), le cose si fanno
più interessanti; poichè sia la variabile formale che la variabile attuale sono riferite
dal medesimo puntatore, quando il suo valore viene posto al valore nullo, entrambe
le variabili saranno poste pari a 0. L’ultima istruzione fmt.Println(), dà accesso
all’indirizzo di memoria dove riferisce il puntatore.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"
func valzero(amountval float64) {
amountval = 0 }
func pntzero(amountpnt *float64) {
*amountpnt = 0 }
func main() {
var amount float64
amount = 165
fmt.Println("iniziale: ", amount)
valzero(amount)
fmt.Println("valzero: ", amount)
pntzero(&amount)
fmt.Println("pntzero: ", amount)
fmt.Println("puntatore di amount:", &amount) }
Listato 4.14. Valori e puntatori
4.3.2
Le funzioni anonime
Go suppporta, nativamente, le funzioni anonime. Tali costrutti, implementati nel
Java solo nell’ultima versione, promettono di avvicinare il programmatore alle
caratteristiche tipiche della programmazione funzionale.
Una funzione anonima (o lambda) non è una funzione standard e presenta,
essenzialmente, due caratteristiche:
• assenza di identificazione;
• assenza di invocazione
Tali peculiarità rendono, di fatto, anonima la funzione, nel vero senso della parola.
La funzione pur essendo anonima, non ha esistenza autonoma. Essa, pertanto, deve essere attribuita ad una variabile, oppure devono essere implementati, e
non solo definiti, i parametri formali. Nel Listato 4.15 è presente un esempio di
attribuzione a una variabile withdraw di funzione anonima.
1
2
3
4
5
6
7
8
package main
import "fmt"
func main() {
withdraw:= func (amount float64, balance float64) float64 {
return (balance-amount) }
fmt.Println(withdraw(341.4, 15072.0))
fmt.Println(withdraw(124.9, 4333.2)) }
Listato 4.15. Variabile e funzione anonima
4.3.3
Il return multiplo
Go è uno dei pochissimi linguaggi di programmazione in grado di supportare il
“return multiplo” delle funzioni.
4.3 Gestione delle funzioni in Go
61
Tale funzionalità può rivelarsi uno strumento fondamentale per aumentare sia
la velocità di programmazione sia quella di esecuzione.
La restituzione di valori multipli può rivelsarsi un meccanismo notevolmente
efficiente; per esempio, può essere utilizzata per ottenere i risultati d’esecuzione
di due strutture di controllo parallele al fine di garantire in uscita sia il flusso di
esecuzione corretto, sia quello errato. O, più semplicemente, può essere utilizzato
per ritornare due variabili di interesse.
Il Listato 4.16 presenta un tipico esempio di return multiplo. Supponiamo di
voler trovare, dato un gruppo prestabilito di conto correnti, quali fra questi abbiano il saldo maggiore e minore. La risoluzione del problema può essere gestita
semplicemente tramite, proprio, il return multiplo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main
import "fmt"
type CurrentAccount struct {
iban
string
number string
balance float64
}
func MaxMin(currentArrey []CurrentAccount) (string, string) {
max := currentArrey[0].balance
var numberMAX string
for _, v := range currentArrey {
if v.balance > max {
max = v.balance
numberMAX = v.number
}
}
min := currentArrey[0].balance
var numberMIN string
for _, v := range currentArrey {
if v.balance < min {
min = v.balance
numberMIN = v.number
}
}
return numberMAX, numberMIN
}
func main() {
CA1 :=
CA2 :=
CA3 :=
CA4 :=
CA5 :=
CA6 :=
CA7 :=
CA8 :=
CA9 :=
CurrentAccount{iban:
CurrentAccount{iban:
CurrentAccount{iban:
CurrentAccount{iban:
CurrentAccount{iban:
CurrentAccount{iban:
CurrentAccount{iban:
CurrentAccount{iban:
CurrentAccount{iban:
"ITKA1", number: "A", balance: 943.7}
"ITDA2", number: "B", balance: 343.0}
"IFTA3", number: "C", balance: 12343.2}
"ITDA4", number: "D", balance: 2341.8}
"IT8A5", number: "E", balance: 1000.3}
"ITDKA1", number: "F", balance: 9431.7}
"ITDAF2", number: "G", balance: 3400.0}
"IFT1A3", number: "H", balance: 1098.2}
"ITDAE4", number: "I", balance: 3041.8}
var currentArrey = []CurrentAccount{CA1, CA2, CA3, CA4, CA5, CA6, CA7, CA8, CA9}
var MAXBalance, MINBalance string
MAXBalance, MINBalance = MaxMin(currentArrey)
fmt.Println("Il conto con saldo maggiore è:", MAXBalance)
fmt.Println("Il conto con saldo mainore è:", MINBalance) }
Listato 4.16. Massimo e minimo in Go, return multipli
In Java non è presente la funzionalità di restituzione multipla. Per potere ottenere il medesimo risultato del Listato 4.16 si potrebbe utilizzare l’algoritmo presente
nel Listato 4.17.
1
2
3
4
5
6
7
8
9
10
11
import java.util.Vector;
public class MaxMin {
public static Vector<String> MaxMin(Vector<CurrentAccount> v){
String numberMax = null;
String numberMin = null;
float MAXBalance=V.get(0).getBalance();
for (int i=0;i<V.size();i++)
{
if (V.get(i).getBalance()>MAXBalance){
tmp=V.get(i).getBalance();
62
4 Java vs Go: la gestione delle funzioni
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
numberMax=V.get(i).getNumber();}
}
float MINBalance=V.get(0).getBalance();
for (int i=0;i<V.size();i++)
{
if (V.get(i).getBalance()<MINBalance){
MINBalance=V.get(i).getBalance();
numberMin=V.get(i).getNumber();}
}
Vector<String> MaxMin = new Vector<String>();
MaxMin.addElement(numberMax);
MaxMin.addElement(numberMin);
return MaxMin;
}
Listato 4.17. Esempio di massimo e minimo in Java, return singolo
4.4 Conclusioni
Nelle sezioni e sottosezioni precedenti abbiamo approfondito gli aspetti principali
della gestione delle funzioni sia in Java che in Go.
In Java la gestione delle funzioni si rivela molto chiara. Ogni metodo, infatti, ha
la propria classe, il proprio posto e la propria funzione. Tale meccanismo migliora
la leggibilità del codice e, quindi, anche la capacità di modificare ed estendere le
funzionalità del sistema stesso.
Inoltre, il polimorfismo per metodi garantisce, insieme alla chiarezza, anche, uno
strumento in grado di assicurare un riuso del codice elevatissimo.
La chiarezza e la facilità di programmazione vengono, però, pagati in termini
di pesantezza del codice stesso. L’inserimento dei metodi costruttori,di metodi
get e set cosı̀ come il polimorfismo stesso, se, da una parte, snelliscono alcuni
meccanismi, come ad esempio l’ereditarietà, dall’altra parte, rendono il linguaggio
particolarmente verboso, anche per programmi di semplice realizzazione.
In Golang la gestione delle funzioni non è ordinata come in Java. Anche se è
possibile implementare una programmazione che ricorda il paradigma OOP, tramite
i puntatori, i “metodi” sono all’esterno delle struct e, di conseguenza, risultano di
difficile lettura.
Inoltre, l’implementazione di alcuni costrutti, come ad esempio, le funzioni anonime, non risultano fondamentali al fine della programmazione. Oltretutto, a partire
dalla Versione SE 8, le stesse funzioni anonime sono state implementate in Java ,
arricchendo ancora di più il linguaggio.
La gestione delle funzioni è, in Go, sicuramente sintetica. Infatti, è possibile stilare pochissime righe di codice per ottenere operazioni complesse. Tale meccanismo
è in netta contrapposizione con Java; tuttavia, la verbosità di quest’ultimo non è pagata dal programmatore. I numerosi supporti software, quale, ad esempio, Eclipse,
mettono a disposizione degli sviluppatori tasti di accesso rapido che permettono,
con pochi click e in poco tempo, di costruire tutti i metodi necessari alla classe,
come, ad esempio, i metodi get, quelli set, i metodi costruttori , etc.
La presenza del return multiplo, però, rappresenta un vantaggio evidente e notevole del linguaggio di casa Google, rispetto a Java. Tuttavia, analogalmente a
quanto è avvenuto per le funzioni anonime, non dovrebbe stupire se, nel prossimo
futuro, anche Java aggiorni le sue specifiche per poter garantire tale peculiarità.
5
Java Vs Go: la gestione delle interfacce
In questo capitolo verrà discussa l’implementazione delle interfacce nel linguaggio Java e nel linguaggio Go. Tale discussione ci porterà, anche, ad introdurre il
concetto di ereditarietà multipla.
5.1 La gestione delle interfacce in Java
In questa sezione verrà trattata la gestione delle interfacce nel linguaggio di programmazione Java. Per poter meglio comprendere il comportamento delle interfacce,
sarà sviluppata una sottosezione dove verrà presentato il concetto di classe astratta.
5.1.1
Le interfacce
Le interfacce non sono tecnicamente delle classi, ma sono più assimilabile al concetto
di oggetti pubblici.
Non è possibile, infatti, dichiarare, all’interno di esse, le variabili di istanza, ma
solo delle costanti, ossia delle variabili del tipo public static final.
I metodi dell’interfaccia sono pubblici e astratti. È possibile, pertanto, definirli,
ma non è permesso implementarli.
Per poter intestare un metodo come astratto bisogna utilizzare il modificatore
abstract. Esso permette di modificare le funzionalità e le caratteristiche sia dei
metodi che delle classi. Tuttavia, non è possibile marcare come abstract le variabili.
Il metodo astratto viene definito dalla sua“firma” ed, eventualmente, dal tipo di
ritorno; non prevede, pertanto, al suo interno, il blocco di codice e, di conseguenza,
attraverso il modificatore abstract, sarà modificata la sintassi di base del metodo
stesso. Verranno, infatti, eliminate le parentesi graffe e verrà introdotta l’interpunzione tramite punto e virgola alla fine del token. Un esempio di metodo astratto è
riportato nel Listato 5.1.
1
public abstract void EsempioDiMetodoAstratto();
Listato 5.1. Esempio di metodo astratto in Java
64
5 Java Vs Go: la gestione delle interfacce
Il metodo, non presentando righe di codice, non potrà essere invocato nella
maniera tradizionale.
Sarà, pertanto, soggetto all’override delle classi che lo implementano, sulla base
di quanto indicato, nel capitolo precedente, nella sezione dedicata al polimorfismo.
Java imposta, automaticamente, i marcatori relativi ad un’interfaccia; pertanto,
è possibile non esplicitarli in fase di dichiarazione.
Pragmaticamente, le interfacce contengono al loro interno solo le informazioni
relative alle funzionalità minime che devono essere garantite dalla classe che le
implementa.
L’interfaccia, di conseguenza, si presenta come indicato nel Listato 5.2 e viene
implementata come riportato nei Listati 5.3 e 5.4.
1
2
3
4
public interface EsempioDiInterfaccia {
String P =" Privato";
String C = "Azienda";
void recognition(); }
Listato 5.2. Dichiarazione di interfaccia in Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Public class Company implements EsempioDiInterfaccia{
private String vatNum;
private String businessName;
public Company(String vatNumber, String businessName){
this.vatNum = vatNum;
this.businessName = businessName;}
public String getVatNum() {
return vatNum; }
public void setVatNum(String vatNum) {
this.vatNum = vatNum; }
public String getBusinessName() {
return businessName; }
public void setBusinessName(String businessName) {
this.businessName = businessName; }
@Override
public void Recognition() {
System.out.println(C); }}
Listato 5.3. Implementazione nella classe Company
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Private implements EsempioDiInterfaccia{
private String Surname;
private String Name;
public Private (String Surname, String Name){
this.Name = Name;
this.Surname = Surname;}
public String Surname() {
return Surname; }
public void Surname(String Surname) {
this.Surname = Surname; }
public String Name() {
return Name; }
public void Name(String Name) {
this.Name = Name; }
@Override
public void Recognition() {
System.out.println(P); }}
Listato 5.4. Implementazione nella classe Private
Dai listati precedenti, possiamo affermare che ogni classe che implementa l’interfaccia eredita tutti i metodi della stessa. Gli stessi metodi saranno riscritti, opportu-
5.1 La gestione delle interfacce in Java
65
namente, dalla classe che li implementa usufruendo del paradigma del polimorfismo
di tipo override.
Per capire meglio le potenzialità delle interfacce, è necessario sviluppare altri
esempi.
Si vuole realizzare un programma che, ricevuto un numero predefinito di conto
correnti, li ordina in modo crescente, dal saldo più a quello più alto. Per sviluppare
questo programma utilizzeremo l’interfaccia riportata nel Listato 5.5.
1
2
3
public interface Sortable {
public boolean balanceMinone (Object comparison);
public boolean greaterBalance(Object comparison);}
Listato 5.5. Interfaccia Sortable
Le classe CurrentAccount che implementa l’interfaccia deve garantire, pertanto,
i metodi balanceMinone e graterBalance (Listato 5.6).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class CurrentAccount implements Sortable{
private String iban;
private String number;
private float balance;
public CurrentAccount() {}
public CurrentAccount(String iban, String number, float balance) {
this.iban = iban;
this.number = number;
this.balance = balance; }
public String getIban() {
return iban; }
public void setIban(String iban) {
this.iban = iban; }
public String getNumber() {
return number; }
public void setNumber(String number) {
this.number = number; }
public float getBalance() {
return balance; }
public void setBalance(float balance) {
this.balance = balance; }
public synchronized void ReadBalance (int i){
System.out.println(Thread.currentThread().getName()+"i="+i+"}:Conto corrente n."+number+
". Saldo: "+getBalance()+" euro."); }
public synchronized void Deposit (float amount, int i){
ReadBalance(i);
System.out.println(Thread.currentThread().getName()+"i="+i+"}:Deposito: "+amount+" euro.");
setBalance(getBalance()+amount);
ReadBalance(i);
notifyAll();}
public synchronized void Withdraw (float amount, int i)throws InterruptedException{
while(getBalance()<amount){
ReadBalance(i);
System.out.println(Thread.currentThread().getName()+"i="+i+"}:Ritiro: "+amount+
" euro. SALDO INSUFFICIENTE!");
wait();}
ReadBalance(i);
System.out.println(Thread.currentThread().getName()+"i="+i+"}:Ritiro: "+amount+" euro.");
setBalance(getBalance()-amount);
ReadBalance(i) }
@Override
public boolean balanceMinone(Object comparison){
return getBalance()< ((CurrentAccount)comparison).balance; }
@Override
public boolean greaterBalance(Object comparison) {
return ! balanceMinone(comparison); }}
Listato 5.6. Classe CurrentAccount
66
5 Java Vs Go: la gestione delle interfacce
Attraverso il polimorfismo si sovrascrivono i metodi dell’interfaccia.
L’algoritmo di ordinamento è gestito dalla classe SortingAlgorithm, riportata
nel Listato 5.7.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SortingAlgorithm {
Sortable[] v=null;
public SortingAlgorithm(Sortable[] _v)
{v = _v.clone(); }
int lowPos(int j)
{
int lowestPosition=j;
int currentPosition=j++;
while (currentPosition<v.length)
{
if(v[currentPosition].balanceMinone(v[lowestPosition]) )
lowestPosition = currentPosition;
currentPosition++; }
return lowestPosition; }
Sortable[] selectionSortMINMAX()
{
for (int i=0; i<v.length - 1; i++)
{
int last = lowPos(i);
Sortable tmp = v[i];
v[i] = v[last];
v[last] = tmp;
}
return v; }}
Listato 5.7. Classe SortingAlgorithm
Il costruttore dell’algoritmo di ordinamento riceve in ingresso il vettore di tipo
Sortable, ossia un array di conto correnti “disordinati”. Il vettore viene clonato
attraverso il metodo clone() della classe Object, per non “sporcare” l’array in
ricezione.
L’ordinamento è di tipo selection sort, ossia prevede un’algoritmo che seleziona,
di volta in volta, il saldo minore nella sequenza di partenza e lo sposta in una
sottosequenza ordinata. Pertanto, in qualunque istante, la sequenza di interesse, è
divisa in due parti, ovvero:
• sottosequenza ordinata: occupa le prime posizioni della sequenza e contiene gli
elementi già ordinati dall’algoritmo;
• sottosequenza disordinata: costituisce l’insieme di elementi non ancora ordinati.
L’algoritmo per ordinare l’array v, di lunghezza v.lenght, fa scorrere l’indice
i da 0 a v.lenght-1 tramite un ciclo for. Si ricerca il conto corrente con saldo
minore nella sottosequenza disordinata, di intervallo [i, v.lenght] e, quando viene
identificato, lo si scambia con l’elemento i-esimo della sequenza. Il ciclo for è ripetuto v.lenght-1 volte, poichè è chiaro che l’ultimo elemento rimasto sarà il conto
corrente con il saldo maggiore. L’algoritmo viene ripetuto finchè l’array non è stato ordinato completamente. Quando l’algoritmo di ordinamento termina il proprio
compito, il vettore v viene restituito in uscita.
Nel main() viene, inizialmente, creato il vettore di conto correnti che si deve
ordinare. Successivamente, viene creato l’oggetto della classe SortingAlgorithm, al
quale si passa il vettore di array, precedentemente creato. Il vettore viene, pertanto,
ordinato e il suo valore viene passato, in fase di dichiarazione, all’array currentList.
Tramite un ciclo for verranno, infine, stampati i valori ordinati in base al saldo
(Listato 5.8).
1
2
public class Main {
public static void main(String[] args) {
5.1 La gestione delle interfacce in Java
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CurrentAccount[] currentArrey =
{new CurrentAccount("IBTSAN1","A" , 1280),
new CurrentAccount("ITKIBAN2", "B", 1342),
new CurrentAccount("SKIIBFATAN3", "C", 38),
new CurrentAccount("PDAR21KMER", "D", 389)};
SortingAlgorithm s = new
SortingAlgorithm(currentArrey);
CurrentAccount[] currentList = (CurrentAccount[])
s.selectionSortMINMAX();
System.out.println("Conti correnti ordinati in ordine
crescente rispetto al saldo:");
for (int i=0; i<currentList.length; i++)
System.out.println("Saldo: " +
currentList[i].balance+", Conto corrente n.: " +
currentList[i].number+", IBAN: " + currentList[i].iban);}}
67
Listato 5.8. Il main() del programma che si occupa dell’ordinamento
Eseguendo il programma, si avrà il risultato mostrato in Figura 5.1.
Figura 5.1. Risultato ottenuto dall’esecuzione del programma del Listato 5.8
5.1.2
L’ereditarietà multipla
Java, diversamente da altri linguaggi, come, ad esempio, C++, non supporta il concetto di ereditarietà multipla. Pertanto, una classe può estendere soltanto un’altra
classe.
È possibile, tuttavia, aggirare parzialmente il problema avvalendosi delle interfacce. In Java, infatti, l’implementazione delle interfacce è multipla; la classe,
pertanto, può implementare più interfacce, ciascuna delle quali contiene, al proprio
interno, le definizioni dei metodi che si vogliono implementare e sovrascrivere.
Inoltre, Java garantisce alle stesse interfacce l’ereditarietà multipla. Un’interfaccia può, quindi, ereditare il comportamento di una o più interfacce utilizzando la
keyword extends (Listato 5.9).
1
2
3
4
5
6
public interface Inheritance extends intrfc1, intrfc2 {
int statfin = 3;
void publastc1(i);
String P();
void publastc1();
}
Listato 5.9. Interfaccia che estende altre interfacce in Java
Le interfacce, pertanto, hanno sia l’importante caratteristica di poter estendere
altre interfacce e sia la peculiarità di poter essere implementate dalle classi.
Inoltre, tali implementazioni ed estensioni possono essere utilizzate, teoricamente, all’infinito. Tali meccanismi permettono, di fatto, la simulazione dell’ereditarietà
multipla.
L’ereditarietà multipla simulata è, inoltre, di tipo controllato; le interfacce possono dichiarare, soltanto, delle costanti e definire dei metodi astratti; pertanto,
68
5 Java Vs Go: la gestione delle interfacce
sarà garantita quella robustezza del codice che viene minata, invece, nei linguaggi
provvisti di una vera e propria ereditarietà multipla.
Modelliamo il case study di interesse per simulare un esempio di ereditarietà
multipla; per prima cosa creiamo le interfacce che si vogliono implementare nelle
varie classi (Listati 5.10-5.14).
1
2
public interface CanRead{
void ReadBalance(); }
Listato 5.10. Interfaccia CanRead
1
2
public interface CanDeposit extends CanRead{
float Deposit(float amount);}
Listato 5.11. Interfaccia CanDeposit
1
2
public interface CanWithdraw{
float Withdraw(float amount);}
Listato 5.12. Interfaccia CanWithdraw
1
2
public interface CanTransfer{
float Transfer(CurrentAccount C, float amount);}
Listato 5.13. Interfaccia CanTransfer
1
2
public interface CanInterest {
float Interest();}
Listato 5.14. Interfaccia CanInterest
Realizziamo, ora, le classi che devono implementare i metodi astratti di tali
interfacce. La prima classe da creare sarà la CurrentAccount. Essa implementa
tutte le interfacce, ad eccezione della CanInterest. Di conseguenza, è necessario
effettuare l’override dei metodi implementati (Listato 5.15).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CurrentAccount implements CanRead, CanDeposit, CanWithdraw, CanTransfer {
private String iban;
private String number;
private float balance;
public CurrentAccount() {}
public CurrentAccount(String iban, String number, float balance) {
this.iban = iban;
this.number = number;
this.balance = balance;
}
public String getIban() {
return iban;
}
public void setIban(String iban) {
this.iban = iban;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public float getBalance() {
return balance;
5.1 La gestione delle interfacce in Java
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
}
public void setBalance(float balance) {
this.balance = balance;
}
@Override
public float Deposit(float amount) {
System.out.println("Conto Corrente n."+getNumber()+", Deposito: "+amount+" euro.");
setBalance(getBalance()+amount);
return getBalance();
}
@Override
public void ReadBalance() {
System.out.println("Conto corrente n."+getNumber()+",
Saldo: "+getBalance());}
@Override
public float Transfer(CurrentAccount C, float amount) {
Withdraw(amount);
System.out.println("Bonifico: "+amount+" euro. Trasferimento da Conto corrente n."+getNumber()+
" al conto "+C.number);
return getBalance();
}
@Override
public float Withdraw(float amount) {
System.out.println("Conto corrente n."+getNumber()+", Ritiro: "+amount+" euro.");
setBalance(getBalance()-amount);
return getBalance();
}}
69
Listato 5.15. Classe CurrentAccount che implementa le interfacce
Verrà realizzata, poi, la classe InterestBearingAccount. La classe estende la
classe CurrentAccount ereditandone anche le interfacce. Inoltre, implementa l’interfaccia CanInterest, per poter calcolare l’interesse maturato. Di conseguenza, essa
deve garantire sia i metodi del CurrentAccount sia i metodi astratti implementati
dallo stesso, sia, infine, i metodi dell’interfaccia CanInterest (Listato 5.16).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class InterestBearingAccount extends CurrentAccount implements CanInterest {
private float percentage;
public InterestBearingAccount() {
super();
// TODO Auto-generated constructor stub
}
public InterestBearingAccount(String iban, String number, float balance, float percentage) {
super(iban, number, balance);
this.percentage=percentage;
// TODO Auto-generated constructor stub
}
public float getPercentage() {
return percentage;
}
public void setPercentage(float percentage) {
this.percentage = percentage;
}
@Override
public float Interest() {
System.out.println("Conto corrente n. "+getNumber()+
", Incremento: "+getPercentage()+"per cento");
setBalance((getBalance()*getPercentage())/100);
return getBalance();
}
@Override
public float Deposit(float amount) {
// TODO Auto-generated method stub
return super.Deposit(amount);
}
@Override
public void ReadBalance() {
// TODO Auto-generated method stub
super.ReadBalance();
}
@Override
public float Transfer(CurrentAccount C, float amount) {
// TODO Auto-generated method stub
return super.Transfer(C, amount);
}
70
5 Java Vs Go: la gestione delle interfacce
50
51
52
53
54
@Override
public float Withdraw(float amount) {
// TODO Auto-generated method stub
return super.Withdraw(amount);
} }
Listato 5.16. Classe InterestBearginAccount che estende CurrentAccount
Infine, nel Listato 5.17, viene realizzata la classe SubordinatedBond.
Essa implementerà le interfacce CanRead, CanInterest e CanWithdraw.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class SubordinatedBond implements CanRead, CanWithdraw,CanInterest{
private int id;
private float percentage;
private float balance;
public SubordinatedBond(int id, float percentage, float balance) {
super();
this.percentage = percentage;
this.balance = balance;
this.id=id;
}
public float getPercentage() {
return percentage;
}
public void setPercentage(float percentage) {
this.percentage = percentage;
}
public float getBalance() {
return balance;
}
public void setBalance(float balance) {
this.balance = balance;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public float Withdraw(float amount) {
ReadBalance();
System.out.println("Obbligazione n."+getId()+", Ritiro: "+amount+" euro.");
setBalance(getBalance()-amount);
return getBalance();
}
@Override
public void ReadBalance() {
System.out.println("Obbligazione n."+getId()+", Capitale: "+getBalance()+" euro.");}
@Override
public float Interest() {
System.out.println("Obbligazione n."+getId()+
",Incremento: "+getPercentage()+"per cento");
setBalance((getBalance()*getPercentage())/100);
return getBalance();}}
Listato 5.17. Classe SubordinatedBond che implementa le interfacce
Il main(), riportato nel Listato 5.18, produrrà il risultato indicato in Figura 5.2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SubordinatedBond implements CanRead, CanWithdraw, CanInterest{
private int id;
private float percentage;
private float balance;
public SubordinatedBond(int id, float percentage, float balance) {
super();
this.percentage = percentage;
this.balance = balance;
this.id=id; }
public float getPercentage() {
return percentage; }
public void setPercentage(float percentage) {
this.percentage = percentage; }
public float getBalance() {
return balance; }
public void setBalance(float balance) {
this.balance = balance; }
public int getId() {
return id; }
public void setId(int id) {
this.id = id; }
@Override
5.1 La gestione delle interfacce in Java
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public float Withdraw(float amount) {
ReadBalance();
System.out.println("Obbligazione n."+getId()+", Ritiro: "+amount+" euro.");
setBalance(getBalance()-amount);
return getBalance();
}
@Override
public void ReadBalance() {
System.out.println("Obbligazione n."+getId()+", Capitale: "+getBalance()+" euro.");}
@Override
public float Interest() {
System.out.println("Obbligazione n."+getId()+",
Incemento: "+getPercentage()+"per cento");
setBalance((getBalance()*getPercentage())/100);
return getBalance();}}
71
Listato 5.18. Metodo main() che implementa le interfacce
Figura 5.2. Risultato ottenuto dall’esecuzione del programma del Listato 5.18
Attraverso le interfacce siamo riusciti a far ereditare i metodi implementati in
una classe a diversi tipi di classi differenti. Abbiamo, pertanto, simulato un esempio
di ereditarietà multipla.
5.1.3
Classi astratte ed interfacce
Spesso si tende a confondere il concetto di interfaccia con quello di classe astratta. Sebbene, dal punto di vista logico, possono risultare simili, le interfacce sono
un’evoluzione delle stesse classi astratte.
Le classi astratte, in Java, vengono utilizzate per generalizzare le caratteristiche
in comune fra determinate classi, dando luogo ad una gerarchia ben definita.
Una classe astratta è, di conseguenza, una superclasse che implementa al suo
interno metodi astratti. Una classe abstract, analogamente a quanto accade per
le interfacce, non può essere instanziata; tuttavia, a differenza delle interfacce, può
presentare sia variabili non marcate come static final, sia metodi non public,
che metodi costruttori.
La classe astratta, a meno dell’istanziazione degli oggetti, è, a differenza delle
interfacce, una classe a tutti gli effetti.
Un esempio di classe astratta è riportato nel Listato 5.19.
1
2
3
4
5
6
public abstract class EsempioDiClasseAstratta {
private String attributo1;
private String attributo2;
private float attributo3 ;
public EsempioDiClasseAstratta(String attributo1, String attributo2, float attributo3) {
72
5 Java Vs Go: la gestione delle interfacce
7
8
9
10
this.attributo1 = attributo1;
this.attributo2 = attributo2;
this.attributo3 = attributo3;
}
Listato 5.19. Esempio di classe astratta
La superclasse EsempioDiClasseAstratta detta lo standard delle sottoclassi
che la estendono. Una classe che eredita i metodi astratti, definiti nella superclasse
astratta, deve, infatti, usufruire del polimorfismo di tipo override, oppure dichiarare
i metodi ereditati come astratti.
Sia le classi astratte che le interfacce si rivelano, pertanto, uno strumento fondamentale per la OOP; esse possegono, infatti, la caratteristica comune di “forzare”
le classi che le estendono, o che le implementano, ad ereditare o implementare i
comportamenti definiti in queste due entità.
Un’evidente differenza fra le classi astratte e le interfacce è la simulazione dell’eredità multipla. Le classi abstract, essendo classi, possono estendere una sola
classe per volta.
Possiamo, pertanto, affermare che, mentre una classe astratta non è altro che
un’astrazione troppo generica per poter essere istanziata nel contesto di riferimento,
un’interfaccia può essere definita come un’astrazione di tipo procedurale. Difatti, le
prime sono generalmente utilizzate per indicare entità generiche, come, ad esempio,
il concetto di animale, di mezzo di trasporto, di uomo, etc.; invece, le seconde
vengono utilizzate per indicare i comportamenti generici, come, ad esempio, il verso
di un animale, la guida di un mezzo di trasporto, etc.
Le interfacce e le classi astratte possono essere combinate. Il vantaggio sarà
quello di sfruttare, pienamente, e in situazioni diverse, tutte le sfaccettature del
polimorfismo, tramite l’uso di variabili eterogenee, invocazioni a metodi astratti
proprie delle classi abstract e standard di programmazione dettati dall’interfacce.
5.2 La gestione delle interfacce in Go
5.2.1
Le interfacce
La gestione delle interfacce rappresenta un altro fondamentale meccanismo che
consente al linguaggio di programmazione Go di “collegarsi” con i linguaggi che
supportano tipicamente il paradigma di programmazione OOP.
La dichiarazione formale di un’interfaccia è riportata nel Listato 5.20.
1
2
3
4
5
package main
import "fmt"
type Tripler interface {
Triple() float64 }
Listato 5.20. Esempio di Interfaccia in Go
Si introduce l’interfaccia con la parola chiave type. Successivamente, viene assegnato il nome alla stessa interfacca; la convezione, in Go, vuole che il nome scelto
termini con il suffisso “er ” o “able”. Subito dopo viene introdotta, come in Java, la
5.2 La gestione delle interfacce in Go
73
keyword interface e il token termina con la parentesi graffa, rigorosamente inserita sulla stessa linea. Infine, viene inserita la collezione di “firme” delle func, ossia
un insieme identificatori e parametri formali delle funzioni.
Due tipi di interfaccia saranno, pertanto, ritenute identiche dal compilatore se
presentano lo stesso insieme di funzioni, ossia func aventi stessa identificazione,
stessa definizione dei parametri formali e stesso tipo di ritorno.
L’ordine in cui sono riportate le “firme” risulta irrilevante; tuttavia, grazie al
fatto che Go è case sensitive, due interfacce saranno trattate in maniera diversa se
le func presentano identificatori che si scostano anche di solo una lettera maiuscola
o minuscola.
Per implementare un’interfaccia si procede come riportato nel Listato 5.21.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
type Tripler interface {
Triple() float64 }
type CurrentAccount struct {
balance float64
number string
iban string }
func (ca CurrentAccount) Triple() float64 {
return ca.balance*3
}
func main() {
CA := CurrentAccount{balance:56, number:"A", iban:"IB"}
c := Tripler(CA)
fmt.Println("Il saldo triplicato è :", c.Triple()) }
Listato 5.21. Implementazione di una interfaccia in Go
Il body dell’interfaccia racchiude la “firma” delle funzioni; nel caso considerato
sarà presente un’unica func Triple(), che prevede un tipo di ritorno float64.
Successivamente, viene introdotta la struct CurrentAccount.
Nel main(), vengono inizializzate la struct e una variabile c di tipo Tripler,
legata alla struct instanziata CA. L’istruzione riportata nel Listato 5.22 è assimilabile
all’istruzione Java riportata nel Listato 5.23.
1
c := Tripler(CA)
Listato 5.22. Istruzione equivalente in Go
1
public class CurrentAccount implements Tripler
Listato 5.23. Istruzione equivalente in Java
Come è possibile notare dal listato, a differenza di Java, le funzioni indicate nelle
interfacce Go non sono riportate all’interno del tipo che le implementa. Tuttavia,
come in Java, le func dell’interfaccia definiscono il comportamento standard per i
type che le implementano.
In Go è presente, inoltre, un tipo interfaccia che non specifica al proprio interno
alcuna “firma”; tale interfaccia viene definita interfaccia vuota, riportata nel Listato
5.24.
74
5 Java Vs Go: la gestione delle interfacce
1
interface{}
Listato 5.24. Interfaccia vuota
Le interfacce vuote vengono utilizzate, essenzialmente, per gestire valori di type
indefinito. Poichè ogni type può anche non riferirsi ad alcuna funzione, ne deriva che interfacce vuote possono essere implementate in qualsiasi tipo. L’istruzione fmt.Print è particolarmente indicata per l’utilizzo delle interfacce vuote, poichè, tipicamente, la sua funzione è proprio quella di stampare entità indefinite. Un’implementazione di interfaccia vuota sarà analizzata nella prossima
sottosezione.
5.2.2
Strutture, puntatori e interfacce
Nel Listato 5.25 viene esplicitato un tipico esempio di interfaccia vuota.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main
import "fmt"
type Private struct{
idCustomerP int
name string
surname string
taxCode string
documentNum string
address string
postalCode int
city string
email string
landline int
cellphone int }
func PrintAll(vals []interface{}) {
for _, val := range vals {
fmt.Println(val)
}
}
func main()
P1:=
P2:=
P3:=
{
Private{idCustomerP:1, name:"John", surname:"Snow"}
Private{idCustomerP:2, name:"Ramsey",surname:"Bolton"}
Private{idCustomerP:3, name:"Frank", surname:"Castle"}
surnames := []string{P1.surname, P2.surname, P3.surname}
vals := make([]interface{}, len(surnames))
for i, v := range surnames {
vals[i] = v
}
PrintAll(vals)
}
Listato 5.25. Interfaccia vuota e puntatori
Nel metodo main() vengono instanziati tre client relativi alla struct Private. In
fase di dichiarazione dei client, verranno specificati solo tre attributi del campo; gli
altri saranno inizializzati di default. Successivamente, saranno inseriti i cognomi dei
client in nell’array surnames.
A questo punto viene creata una variabile vals di tipo slice, attraverso l’istruzione make. Le slice sono legate, concettualmente, con gli array; esse sono costituite
da un riferimento (un puntatore) e dalla loro lunghezza.
Il riferimento, generalmente, è legato ad una porzione dell’array sottostante.
Tale segmento può anche coincidere con tutta la lunghezza dell’array cui riferisce.
A differenza degli array, la lunghezza di una slice è variabile e può, pertanto, essere
modificata dinamicamente. Le slice possono, anche, non occupare, effettivamente,
5.2 La gestione delle interfacce in Go
75
zone di memoria. Il contenuto di una slice è, essenzialmente, un puntatore che
riferisce alla sottosezione di memoria occupata dall’array sottostante.
Nell’esempio trattato, la slice si riferisce, inizialmente, ad una interfaccia vuota
e la sua lunghezza viene posta pari alla lunghezza dell’array sottostante surnames.
Successivamente, tramite un ciclo for, si scorrono tutti gli elementi appartenenti
all’array sottostante che vengono riferiti.
Di volta, in volta, gli elementi riferiti vengono passati all’interfaccia vuota.
Concluso il ciclo, viene passata l’interfaccia vuota e la dimensione dell’array
sottostante, vengono passate, tramite la slice, alla func PrintAll, come parametro
formale. Il body della funzione consiste in un ciclo for che permette di scorrere i
valori dell’interfaccia vuota per poi stamparli.
La gestione delle interfacce, nel linguaggio Go, presenta la caratteristica che
il type che implementa un’interfaccia non deve necessariamente esplicitare la sua
implementazione.
L’istruzione, appartenente al flusso d’esecuzione del Listato 5.21, vista nella
sottosezione precedente, e riportata nel Listato 5.26 per una maggiore chiarezza, rappresenta, sostanzialmente, un puntatore che dal Triple() si riferisce al
CurrentAccount.
1
2
func (ca CurrentAccount) Triple() float64 {
return ca.balance*3}
Listato 5.26. Implementazione di una interfaccia in Go
Le funzioni dell’interfaccia, per poter essere riutilizzate da altri type, devono, di
conseguenza, puntare, volta per volta, al type che le vuole implementare, altrimenti
saranno sempre riferite al type che le ha implementate per ultimo.
Affinchè un type sia accettato dall’interfaccia, deve essere puntato dalla func
Triple().
Ad esempio, introducendo, la struct SubordinatedBond, se non si effettua tale
operazione, la func dell’interfaccia punterà sempre alla struct CurrentAccount
e, in fase di esecuzione, il compilatore riporterà l’errore “SubordinatedBond does
not implement Tripler (missing Triple method)”
Bisognerebbe, pertanto, modellare il codice del Listato 5.21 come riportato nel
Listato 5.27.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
import "fmt"
type Tripler interface {
Triple() float64 }
type CurrentAccount struct {
balance float64
number string
iban string }
type SubordinatedBond struct{
id int
balance float64
percentage float64 }
func (ca CurrentAccount) Triple() float64 {
return ca.balance*3}
func (sb SubordinatedBond ) Triple() float64 {
return (sb.balance+(sb.balance*sb.percentage)*3/100)}
func main() {
CA := CurrentAccount{balance:56, number:"A",iban:"IB"}
76
5 Java Vs Go: la gestione delle interfacce
26
27
28
29
30
31
SB := SubordinatedBond{id:32, balance:1430, percentage:5}
c := Tripler(CA)
fmt.Println("Il saldo triplicato è:", c.Triple())
s:= Tripler(SB)
fmt.Println("Il capitale triplicato è:", s.Triple())
}
Listato 5.27. type e interfacce
In Go, una struct presenta func “reali” e un campo di attributi, mentre un’interfaccia presenta func “virtuali” e nessun campo di attributi. I due elementi, tramite
i puntatori, si possono legare perfettamente; la possibilità, tramite le interfacce, di
“obbligare” le struct a implementare le func rende Go un linguaggio capace di godere, seppur in maniera atipica, del concetto di polimorfismo. Le interfacce, inoltre,
permettono come in Java, la simulazione dell’ereditarietà multipla.
5.3 Conclusioni
Le interfacce possono essere viste come l’implementazione attesa di un programma
e la sua effettiva realizzazione. L’interfaccia è, infatti, l’entità che vincola le classi o
le struct che la implementano a supportare le funzioni in essa dichiarate. Esse sono
un elemento necessario, soprattutto dal punto di vista progettuale, poichè permettono di realizzare un controllo veloce ed efficace sul codice del programma. Oltre
a consentire un’immediata verifica del codice, le interfacce rappresentano un componente indispensabile per garantire sia estendibilità che riutilizzo del codice. Esse
permettono, di conseguenza, di migliorare la produttività della programmazione.
In Java, grazie al paradigma OOP, le porzioni di codice riutilizzabili sono ben
delineate. La presenza di file con estensione .java per ogni interfaccia, e in generale
per ogni classe, generato nel Workspace, rende il riutilizzo di questi componenti
semplice e immediato. Quando si utilizza un’interfaccia, sarà, pertanto, garantita
sia la leggibilità che la robustezza del codice.
Nel linguaggio di programmazione Go, le interfacce non trovano uno spazio isolato e ben definito all’interno del programma. Esse tendono a confondersi con il
codice stesso, rendendo, pertanto, la loro leggibilità poco chiara. Inoltre, l’utilizzo
dei puntatori e la presenza dei metodi da implementare discostati completamente
sia dal blocco che codifica l’interfaccia sia da quello che codifica la struct, rendono
le lettura del codice ancora più “astrusa”.
Go, nella gestione delle interfacce, ha, di certo, snellito il codice verboso del programmi tipicamente OOP. La sintesi prodotta, tuttavia, viene pagata in termini di
chiarezza. Se tale aspetto risulta trascurabile per un singolo programmatore (ognuno può riconoscere il proprio metodo di programmazione), risulta un incoveniente
di notevole proporzione se le interfacce devono essere riutilizzate da programmatori
diversi, anche appartenenti allo stesso team di sviluppo.
La scelta di riutilizzare il codice per velocizzare le operazioni di programmazione
potrebbe portare, paradossalmente, all’effetto opposto; l’interpretazione del codice
stesso potrebbe richiedere, infatti, un tempo di lavoro maggiore rispetto a quello
di sviluppo dello stesso programma. Inoltre, la scarsa leggibilità delle interfacce
potrebbe portare in errore gli stessi programmatori, aumentando la probabilità di
errata implementazione del codice stesso.
5.3 Conclusioni
77
Go, inoltre, non presentando un meccanismo reale di ereditarietà, non renderà
possibile l’esistenza contemporanea sia di estensioni che di implementazioni multiple
delle interfacce.
Java, anche da questo punto di vista, si dimostra migliore. La piena ereditarietà singola, la presenza delle classi astratte e, in generale, delle superclassi, la
multi-eriditarietà reale fra le interfacce e l’ereditarietà multipla simulata, tramite le
stesse combinate in modo chiaro, controllato e preciso, permettono di stilare quelle
generalizzazioni necessarie ad una buona programmazione.
6
Java vs Go: Riuso del software
In questo capitolo verrà presentata, inizialmente, un’introduzione generale per esplicare il concetto di riuso del software. Successivamente, si illustrerà la possibilità di
riuso nei linguaggi di programmazione Java e Go. In particolare, si approfondiranno
i concetti di ereditarietà ed embedding.
6.1 Il concetto di riuso di software
Un sistema software nasce, nella maggior parte dei casi, dal riuso di componenti
preesistenti. Occorre, pertanto, adattare le fasi di progettazione su un riutilizzo
sistematico del codice.
I vantaggi del riuso sono molteplici; la realizzazione di un software ex-novo causa,
infatti, costi notevoli sia in termini di tempo che in termini di denaro relativi,
soprattutto, allo sviluppo, al testing e alla manutenzione dello stesso.
Il riutilizzo del codice, inoltre, ci permetterebbe di garantire l’affidabilità, già
consolidata, dal sistema di appartenenza. Tuttavia, bisogna stare ben attenti nella
scelta dei software che si vogliono riutilizzare e nelle modalità di implementazione
degli stessi. In genere, è buona norma affidarsi a sistemi software che hanno già
garantito alta qualità nelle prestazioni e dei quali è esplicitato il codice sorgente;
infatti, se si riutilizzasse un’applicazione dove il codice non è dichiarato in maniera
esplicita, potremmo avere sia notevoli problemi di adattamento alle specifiche del
software che si vuole realizzare e sia impossibilità di aggiornamento del codice stesso.
Il riuso, di conseguenza, non può essere scriteriato; è necessaria, infatti, un’attenta pianificazione e valutazione per quanto concerne le componenti software che si
vogliono utilizzare. La pianificazione deve, innanzitutto, tenere conto delle criticità
del sistema, dell’integrazione delle componenti, delle tempistiche di utilizzo, delle
piattaforme su cui il software vuole garantire il servizio, e degli stessi servizi che si
vogliono fornire.
Esplicate tali premesse, analizziamo, ora, le possibilità di riuso del software
garantite dai linguaggi di programmazione Java e Go.
80
6 Java vs Go: Riuso del software
6.2 Riuso del software in Java
Durante lo sviluppo del software, nel linguaggio di programmazione Java, ci si accorge, frequentemente, che alcune porzioni del codice realizzato sono molto simili
fra loro.
Risulta, pertanto, molto vantaggioso adattare e manipolare codice già esistente, per poter rispondere più efficaciemente alle problematiche riscontrate. Java
garantisce il riutilizzo del codice, soprattutto grazie alla caratteristica di supportare il paradigma OOP, che prevede, fra le altre cose, la proprietà fondamentale
dell’ereditarietà.
6.2.1
Ereditarietà
Gerarchie
Come detto nella prima sezione, il riuso del software deve seguire un certo criterio.
Classi con attributi comuni, ma con un comportamento completamente diverso
porterebbero a un riuso del codice inefficiente e potenzialmente dannoso al punto
tale da rendere preferibile la cancellazione del codice finora stilato, piuttosto che
una sua eventuale rienterpretazione.
L’ereditarietà è, sicuramente, uno strumento importante e utile, tuttavia la sua
implementazione è affidata in prima istanza al programmatore. Pertanto, per poter
implementare tale meccanismo al meglio, è necessario, innanzitutto, elaborare una
pianificazione saggia e funzionale.
Nasce quindi l’esigenza di sviluppare una gerarchizzazione delle classi che
risponda ai criteri dell’ereditarietà.
Nel caso di studio che stiamo sviluppando nella presente tesi si è asserito che sia
privati cittadini che aziende potevano usufruire delle prestazioni della banca. Essi
hanno numerosi attributi in comune, ossia:
• identificatore: sia i privati che le aziende possono essere univocamente identificati
da un numero progressivo;
• citta, indirizzo, C.A.P : sia i privati cittadini che le aziende risiedono in una determinata città, in un determinato indirizzo, identificato dal Codice di Avviamento
Postale;
• telefono fisso, telefono mobile: sia il cittadino che l’azienda devono fornire i
rispettivi recapiti telefonici;
• e-mail : sia il cittadino che l’azienda devono, inoltre, fornire un indirizzo di posta
elettronica per eventuali comunicazioni via e-mail.
Supponendo che i dettagli anagrafici del privato non siano interessanti al fine
della progettazione, consideriamo, oltre agli attributi in comune, precedentemente
elencati, soltanto i campi businessName e vatNum della classe Company.
Un approccio privo di un’attenta pianificazione ci porterebbe a far ereditare
alla classe Private gli attributi della classe Company; tale modo di procedere, però,
produrrebbe una gerarchizzazione sostanzialmente errata (Figura 6.1).
6.2 Riuso del software in Java
81
Figura 6.1. Esempio di gerarchizzazione errata
Per evitare tali errori, è possibile, in prima analisi, utilizzare un test tanto
semplice quanto efficace, ossia la relazione “is a”.
La prima cosa da fare per stilare, opportunamente, una gerarchia è quella di
chiedersi se un determinato elemento fa parte della famiglia di un’altro elemento. Utilizzando tale relazione è immediata la comprensione che Private “isn’t a”
Company; si cerca, allora, di introdurre una classe che possa “astrarre” i concetti
comuni in modo da poter risolvere positivamente la relazione.
La classe che può effettuare tale “astrazione” è la classe Customer; infatti
Private “is a” Customer e Company “is a” Customer.
La gerarchizzazione, riportata in Figura 6.2, sarà, pertanto, quella corretta.
Figura 6.2. Esempio di gerarchizzazione corretta
Nel caso considerato, Customer sarà la superclasse delle sottoclassi Company e
Private.
La definizione delle superclassi e delle sottoclassi sarà espressa, nel dettaglio,
nella sottosezione successiva.
Superclassi e sottoclassi
Definita la gerarchia, è possibile determinare le superclassi e le sottoclassi di interesse. Quando una classe eredita il comportamento da un’altra, allora eredita i suoi
metodi, con tutte le caratteristiche che abbiamo trattato precedentemente, nella
sezione dedicata alla gestione delle funzioni.
82
6 Java vs Go: Riuso del software
È possibile, inoltre, introdurre specifici metodi necessari a determinare il comportamento della sottoclasse. Creiamo, allora, la classe Customer, ottenuta in fase
di gerarchizzazione (Listato 6.1).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Customer {
private int idCustomer;
private int postalCode;
private int landline;
private int cellphone;
private String address;
private String city;
private String email;
public Customer(){}
public Customer(int idCustomer, int
postalCode, int landline, int cellphone, String address,
String city, String email){
this.idCustomer = idCustomer;
this.postalCode = postalCode;
this.landline = landline;
this.cellphone = cellphone;
this.address = address;
this.city = city;
this.email = email;}
public int getIdCustomer() {
return idCustomer;}
public void setIdCustomer(int idCustomer) {
this.idCustomer = idCustomer;}
public String getAddress() {
return address;}
public void setAddress(String address) {
this.address = address;}
public String getCity() {
return city; }
public void setCity(String city) {
this.city = city; }
public int getPostalCode() {
return postalCode; }
public void setPostalCode(int postalCode) {
this.postalCode = postalCode; }
public String getEmail() {
return email; }
public void setEmail(String email) {
this.email = email; }
public int getLandline() {
return landline; }
public void setLandline(int landline) {
this.landline = landline; }
public int getCellphone() {
return cellphone; }
public void setCellphone(int cellphone) {
this.cellphone = cellphone; } }
Listato 6.1. Superclasse Customer in Java
Costruiamo, ora, le sottoclassi Private e Company, riportate, rispettivamente,
nei Listati 6.2 e 6.3. Per poter implementare una sottoclasse è necessario postporre
la parola chiave extends alla fine della dichiarazione della stessa classe, e subito
prima della parentesi graffa che chiude il token.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Private extends Customer{
private String name;
private String surname;
private String taxCode;
private String documentNum;
public Private() {}
public Private(int idCustomer, int postalCode, int landline, intcellphone, String address,
String city, String email, String name, String surname, String taxCode,
String documentNum) {
super(idCustomer, postalCode, landline, cellphone, address, city, email);
this.name = name;
this.surname = surname;
this.taxCode = taxCode;
this.documentNum = documentNum; }
public String getName() {
return name; }
public void setName(String name) {
this.name = name; }
6.2 Riuso del software in Java
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
83
public String getSurname() {
return surname; }
public void setSurname(String surname) {
this.surname = surname; }
public String getTaxCode() {
return taxCode; }
public void setTaxCode(String taxCode) {
this.taxCode = taxCode; }
public String getDocumentNum() {
return documentNum; }
public void setDocumentNum(String documentNum) {
this.documentNum = documentNum; } }
Listato 6.2. Sottoclasse Private
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Company extends Customer{
private String vatNum;
private String businessName;
public Company() { }
public Company(int idCustomer, int postalCode, int landline,int cellphone,
String address, String city, String email, String vatNum, String businessName) {
super(idCustomer, postalCode, landline, cellphone, address, city, email);
this.vatNum = vatNum;
this.businessName = businessName; }
public String getVatNum() {
return vatNum; }
public void setVatNum(String vatNum) {
this.vatNum = vatNum; }
public String getBusinessName() {
return businessName; }
public void setBusinessName(String businessName) {
this.businessName = businessName; }}
Listato 6.3. Sottoclasse Company
Se la superclasse prevede incapsulamento, le sottoclassi non potranno ereditare
i termini del costruttore, poichè essi saranno marcati come private.
Per poter risolvere questo problema, nel costruttore della sottoclasse sarà invocato il metodo costruttore della superclasse. Tale operazione è resa possibile poichè i
metodi d’accesso alle variabili, a differenza delle variabili stesse, sono marcati come
public.
Per effettuare tale operazione, si introduce la parola chiave super, inserendo
come parametri formali gli attributi in comune fra la superclasse e la sottoclasse.
Pertanto, anche se le sottoclassi Private e Company non possiedo, effettivamente, le variabili della superclasse, possono comunque implementarle, virtualmente,
attraverso l’incapsulamento. La sottoclasse sarà sempre obbligata a inserire il costrutto super all’inizio del proprio costruttore, anche in caso di metodo costruttore
vuoto. La parola chiave super obbliga, pertanto, il programmatore a seguire un
paradigma di programmazione tipicamente OOP.
Subito dopo aver invocato il costruttore della superclasse, verranno inseriti i
parametri appartenenti alle stesse sottoclassi.
84
6 Java vs Go: Riuso del software
La superclasse Object
L’idea principale della programmazione OOP riflette, essenzialmente, la quotidianità riscontrabile nel mondo reale. Mentre è semplice descrivere le caratteristiche e le
funzionaltà che appartengono, ad esempio, ad un’aereo, risulta estremamente complesso spiegare, nel dettaglio, gli intricati meccanismi che permettono a quell’aereo
di avere quel tipo di caratteristiche e quel tipo di comportamento.
Analogalmente, il paradigma OOP tenta di descrivere, nella maniera più semplice possibile, gli attributi e i metodi che uno specifico software implementa.
La documentazione API di Java è stata organizzata in modo tale da soddisfare
ogni sfaccettatura delle OOP. Le librerie, infatti, mettono a disposizione del programmatore una preesistente classe denominata Object che “astrae”il concetto di
oggetto stesso. Essa appartiene al package java.lang e può essere, tranquillamente,
definita come la superclasse di tutte le classi.
La classe Object occupa, pertanto, la cima di ogni piramide che si viene a creare
tramite l’utilizzo della gerarchizzazione. Possiamo, pertanto, affermare che, quando
viene realizzata una classe, implicitamente essa estende la classe Object.
Quando realizziamo una classe qualsiasi, essa implementerà, automaticamente,
tutti i metodi messi a disposizione da Object. I metodi di questa classe possono
essere, sostanzialmente, raggruppati in due tipologie:
• Metodi in override: Sono i metodi che possono essere sovrascritti dalle sottoclassi
di Object (Listato 6.4). Essi sono:
– clone(): permette di clonare un oggetto preesistente. Il metodo clone è
stato utilizzato nel Listato 5.7.
– equals(): è un metodo boolean che restituire true se i due oggetti ricevuti
in ingresso sono identici.
– finalize(): permette di garantire un uso corretto delle risorse esterne, non
gestibili correttamente dalla garbage collection.
– toString(): restituisce una stringa contenente le informazioni dell’oggetto.
• Metodi non in override. Essi sono:
– getClass(): restituisce la gerarchia e le interfacce collegate ad una particolare classe.
– notify(), notify(), wait(): le loro caratteristiche sono state esplicate nella
descrizione del Listato 3.10.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Classe {
@Override
protected Object clone() throws CloneNotSupportedException{
return super.clone(); }
@Override
public boolean equals(Object arg0) {
return super.equals(arg0); }
@Override
protected void finalize() throws Throwable {
super.finalize(); }
@Override
public int hashCode() {
return super.hashCode(); }
@Override
public String toString() {
return super.toString(); } }
6.2 Riuso del software in Java
85
Listato 6.4. Metodi della classe Object in override
6.2.2
Valutazioni sul riuso del software
Il linguaggio di programmazione Java è stato concepito, fin dal dall’origine, come
predisposto al riuso del software.
L’obiettivo dichiarato dal “Green Team” era, infatti, quello di realizzare un
sistema software basato su una macchina virtuale che potesse eseguire il bytecode
generato, indipendentemente dalla piattaforma.
L’indipendenza dell’architettura permette un riuso del codice che non ha pari
in altri linguaggi di programmazione; garantire le funzionalità di un programma,
a prescindere dalla piattaforma sui cui il software è implementato, evita sia la duplicazione che la reinterpretazione di un codice che deve essere adattato, volta per
volta, per le conversioni ad altre piattaforme. Tale meccanismo, pertanto, risponde,
perfettamente, al concetto di riutilizzo del software.
La presenza di una libreria vastissima e in costante aggiornamento evita, inoltre,
al programmatore, la stesura di codice per la risoluzione di problemi già risolti dalle
librerie.
Non sono soltanto la JVM e la documentazione API a garantire il riuso del
codice. Java, infatti, è stato pensato per essere un linguaggio OOP a tipizzazione
statica in modo da favorire la modularità e, soprattutto, il riuso.
Utilizzando i concetti tipici dei linguaggi OOP, Java suddivide i problemi che un
programma deve risolvere in classi di problemi; i programmi, pertanto, non saranno
costituiti da un blocco di codice unico, ma da classi che definiscono l’“astrazione”
degli oggetti che devono instanziare.
Tramite il meccanismo dell’incapsulamento, inoltre, si pone la netta suddivisione
fra le “astrazioni” degli oggetti, assicurando sia la robustezza che la modularità.
L’“astrazione” data dalle classi implica sia quella dei dati che quella dei metodi.
Quest’ultima rende possibile la risoluzione di problemi simili o identici, delegandoli
ad un unico sottoprogramma, senza dover, pertanto, scrivere porzioni duplicate di
codice.
I metodi, inoltre, essendo suddivisi per classi, sono di facile lettura. Il programmatore, pertanto, sarà incoraggiato ad usufruire di quelli, già definiti, favorendo,
ancora una volta, il riutilizzo del sofrware.
Per di più, l’implementazione dei metodi per le classi può essere garantita, in
fase di progettazione, tramite un oculato uso delle interfacce.
L’ereditarietà, inoltre, consente di estendere e specializzare il codice già scritto.
Tale meccanismo, come è stato possibile verificare nella sezione precedente, permette
di ereditare il campo di una classe, per poi estenderlo, aggiungendo, se è necessario,
altri attributi.
L’ereditarietà riguarda, anche, i metodi; possiamo, pertanto, utilizzare i metodi
dichiarati dalle superclassi e, in caso, sovrascriverli, tramite un polimorfismo di tipo
override, per adattarli e renderli compatibili con le specifiche della sottoclasse.
La strutturazione gerarchica, tipica dell’ereditarietà, assicura, ancora di più, la
leggibilità dei programmi e garantisce, pertanto, un’ulteriore riduzione dei tempi di
sviluppo grazie al riutilizzo del codice.
86
6 Java vs Go: Riuso del software
Non è, inoltre, da sottovalutare la presenza della classe Object. Essa, occupando
la cima della piramide, garantisce, automaticamente, il riutilizzo del software; ogni
classe, infatti, può sovrascrivere o usuifruire dei suoi utilissimi metodi, senza doversi
preoccupare di redigire un codice dedicato.
A permettere il riuso del software è, anche, la possiblità di spostare classi e
interfacce da un progetto all’altro, tramite un semplice e banale “copia e incolla”
dei file con estensione .java.
La JVM, la documentazione API, i file .java, l’implementazione delle interfacce, le classi, l’ereditarieta, l’incapsulamento e il polimorfismo, combinati insieme e
implementati con una sufficiente “astrazione”, garantiscono, senza dubbio, il riuso
ottimo del software nel linguaggio di programmazione Java.
6.3 Riuso del software in Go
6.3.1
Embedding
Nella scrittura di un codice di un programma Go è possibile riscontrare, cosı̀ come
in Java, la presenza di numerose struct caratterizzate dall’avere campi, fondamentalmente, simili o identici. Pertanto, è necessario un meccanismo che permetta di
snellire la stesura del codice e riutilizzare le componenti software, similmente a come
si è visto con il concetto dell’ereditarietà.
Tuttavia, Go non è tipicamente OOP e non presenta, di conseguenza, il concetto
di oggetto e classe in maniera definita. L’“astrazione”, pertanto, non è applicabile.
Infatti, un approccio basato sulle gerarchie dell’ereditarietà si rivela, in questo
caso, un approccio sostanzialmente errato.
L’assenza di classi comporta, naturalmente, l’assenza delle sottoclassi che le
estendono e delle superclassi che le includono.
Bisogna, pertanto, trovare una soluzione simile, ma alternativa, all’ereditarietà.
Lo stesso Rob Pike dà una possibile soluzione:
“if C++ and Java are about type hierarchies and the taxonomy of types, Go is
about composition.”1
Le relazioni che intercorrono fra le struct non si costruiscono per estensione,
ossia A“is a” B, bensı̀ per composizione, ossia A “has a” B.
Bisogna, pertanto, non far ereditare a una struct gli attributi di un’altra, ma
far si che essa la incorpori. Il meccanismo con cui far incorporare ad una struct le
procedure e i campi di un’altra è chiamato, nel linguaggio di programmazione Go,
“embedding”.
Per quanto riguarda il case study di interesse, il Listato 6.5 è equivalente ai
Listati 6.1, 6.2 e 6.3.
1
2
3
4
5
6
1
package main
import "fmt"
type Customer struct {
idCustomer int
postalCode int
https://www.youtube.com/watch?v=5kj5ApnhPAE
6.3 Riuso del software in Go
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
87
landline int
cellphone int
address string
city string
email string }
type Company struct {
Customer
vatNum string
businessName string }
type Private struct {
Customer
name string
surname string
taxCode string
documentNum string}
Listato 6.5. Esempio di embedding
Come è possibile notare, implementare l’embedding, dal punto di vista sintattico,
è molto semplice.
Basta inserire all’interno del campo di una struct, come primo elemento, il nome
della struct da incorporare.
Per poter instanziare una struct, si segue la procedura illustrata nel Listato 6.6.
1
2
3
4
func main() {
Cmp1 := Company{Customer{1,89100,0,328,"Via A n.1","Rho","[email protected]"}, "1212311","Golang Inc"}
Prv1 := Private{Customer{2,20141,0,333,11212,"Via B n.7","Roma","[email protected]"}, "Rob","Pike","RBKT","j542"}
}
Listato 6.6. Instaziazione di struct embedding
Per poter stampare le due istanze si utilizzerà il programma riportato nel Listato
6.7, il quale produrrà il risultato riportato in Figura 6.3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main
import "fmt"
type Customer struct {
idCustomer int
postalCode int
landline int
cellphone int
address
string
city
string
email
string }
type Company struct {
Customer
vatNum
string
businessName string }
type Private struct {
Customer
name
string
surname
string
taxCode
string
documentNum string }
func (c *Company) DatiPrincipali() {
fmt.Println("Cliente n.:", c.idCustomer, ", P.Iva:", c.vatNum, ", BusinessName:", c.businessName, ", telefono:",
c.cellphone, "\n") }
func (p *Private) DatiPrincipali() {
fmt.Println("Cliente n.:", p.idCustomer, ", Nome:", p.name,
",Cognome:", p.surname, ",Codice fiscale:", p.taxCode) }
func main() {
Cmp1 := Company{Customer{1, 2, 3, 4, "Samarren.3", "FDS",
"[email protected]"}, "X5", "Go Inc"}
Prv1 := Private{Customer{2, 20, 0, 333, "Via B n.7",
"Roma", "[email protected]"}, "Rob", "Pike", "RBKT", "j542"}
Cmp1.DatiPrincipali()
Prv1.DatiPrincipali() }
88
6 Java vs Go: Riuso del software
Listato 6.7. Dati principali
Figura 6.3. Risultato del programma riportato nel Listato 6.7
Naturalmente, è possibile utilizzare struct in embedding in sinergia con le interfacce. Nel Listato 6.8 viene descritto un esempio di come si possa realizzare tale
meccanismo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main
import "fmt"
type Transactionable interface {
Withdraw(float64) float64
Deposit(float64) float64 }
type CurrentAccount struct {
balance float64
number string
iban
string }
type InterestBearingAccount struct {
CurrentAccount
percentage float64 }
func (ca CurrentAccount) Withdraw(amount float64) float64 {
fmt.Println("Conto n.", ca.number, "Saldo:", ca.balance,
",Ritiro:", amount)
return (ca.balance - amount) }
func (iba InterestBearingAccount) Withdraw(amount float64) float64 {
var newBalanceInts float64
newBalanceInts = iba.balance +(iba.balance*iba.percentage)/100
fmt.Println("Conto n.", iba.number, ",Saldo+interessi:",newBalanceInts,
",Ritiro:", amount)
return (newBalanceInts - amount) }
func (ca CurrentAccount) Deposit(amount float64) float64 {
fmt.Println("Conto n.", ca.number, "Saldo:", ca.balance,",Deposito:", amount)
return (ca.balance + amount) }
func (iba InterestBearingAccount) Deposit(amount float64) float64 {
var newBalanceInts float64
newBalanceInts = iba.balance +(iba.balance*iba.percentage)/100
fmt.Println("Conto n.", iba.number, ",Saldo+interessi:",newBalanceInts, ", Deposito:", amount)
return (newBalanceInts + amount) }
func main() {
CA := CurrentAccount{balance: 560, number: "A", iban: "IB"}
c := Transactionable(CA)
IBA := InterestBearingAccount{CurrentAccount{1100, "B","ITK"}, 5}
d := Transactionable(IBA)
var amount float64
amount = 100
fmt.Println("Nuovo
fmt.Println("Nuovo
fmt.Println("Nuovo
fmt.Println("Nuovo
Saldo:",
Saldo:",
Saldo:",
Saldo:",
c.Withdraw(amount))
d.Withdraw(amount))
c.Deposit(amount))
d.Deposit(amount)) }
Listato 6.8. Embedding e interfacce
Il programma presenta due struct, ossia CurrentAccount e InterestBearingAccount;
La seconda struct incorpora, tramite il processo di embedding, i campi della struct
CurrentAccount.
Nel main() viene instanziato, inizialmente, un conto corrente. Successivamente, viene implementata l’interfaccia (similmente a come era stato fatto nel Listato
5.27), Transactionable, composta dalle “firme” delle func Withdraw e Deposit.
6.3 Riuso del software in Go
89
La struct deve, pertanto, essere riferita ai metodi dell’interfaccia; vengono, pertanto,
implementate le func Withdraw e Deposit, legate al CurrentAccount, tramite un
puntatore. Successivamente, viene instanziata la struct InterestBearingAccount.
Anch’essa implementa l’interfaccia, similmente a quanto visto da CurrentAccount,
tuttavia le func sono state adattate per calcolare, al momento del deposito e del
ritiro, l’interesse accumulato. In seguito, viene inizializzata la variabile amount e invocate le funzioni per le due struct alle quali sarà passata, come parametro formale,
la stessa variabile amount. Il programma produrrà il risultato indicato in Figura 6.4.
Figura 6.4. Risultato del programma riportato nel Listato 6.8
6.3.2
Valutazioni sul riuso del software
La composizione realizzata tramite embedding supporta, perfettamente, eventuali
aggiornamenti dei campi delle struct
Incorporare dei componenti da una struct, anzichè ereditarli, garantisce, infatti,
che, nel caso di aggiornamento delle specifiche di una struct componente, non si sia
necessario un eccessivo notevole aggiornamento dei campi relativi struct composte.
Attraverso la composizione, infatti, basterebbe non incorporare o incorporare in
parte, i campi della struct componente aggiornata.
Dal punto di vista teorico, l’embedding può accogliere facilmente futuri cambiamenti delle specifiche, rispetto al meccanismo dell’ereditarietà proprio del linguaggio
di programmazione Java.
L’embedding, infatti, prevede un approccio dinamico e assicura un riuso ottimale
del codice. L’ereditarietà, invece, utilizza un approccio statico; in caso di aggiornamento, per esempio, risulta difficile cambiare, implementare o eliminare l’interfaccia
di una superclasse, poichè tale modifica del codice si ripercuoterà, senza dubbio, su
tutte le sottoclassi, che andrebbero, pertanto, riviste e corrette.
Un riuso maggiore del codice avviene combinando l’embedding con le interfacce.
Come è stato possibile appurare, struct composte possono incorporare sia i campi che
le interfacce delle struct componenti, assicurando uno standard di implementazione
simile a quello che veniva garantito dalla combinazione di interfacce ed ereditarietà;
inoltre, attraverso l’embedding è possibile simulare sia la stessa ereditarietà, come
si è voluto fare nostro caso di studio e sia l’ereditarietà multilpla.
Tuttavia, l’embedding, non prevedendo una gerarchizzazione statica dei componenti è soggetto ha una difficile interpretazione e una difficile lettura del codice che
cozza con il concetto di riuso del software.
90
6 Java vs Go: Riuso del software
Inoltre, Go, a differenza di Java, è un linguaggio compilato. Non essendo provvisto di una macchina virtuale, non viene assicurata l’indipendenza dalla piattaforma
sottostante. Tale assenza comporta una riscrittura del codice in modo che quest’ultimo venga adattato alle specifiche della piattaforma su cui si vuole implementare
il servizio. Pertanto, anche da questo punto di vista, Go va contro alle buone norme
di riuso del software.
6.4 Conclusioni
Il riuso del software nel linguaggio di programmazione Java è il punto forte sul quale
hanno puntato gli sviluppatori. Come abbiamo potuto vedere nel dettaglio in questo
capito e in quelli precendenti, Java preferisce avere un linguaggio di programmazione
corposo ma chiaro, leggibile e facilmente riutilizzabile.
Tale prolissità del linguaggio non insidia la facilità di programmazione. I supporti
software e le librerie messi a disposizione del programmatore veicolano la corretta
implementazione delle classi e delle interfacce, che per la loro natura tipicamente
OOP, garantiscono, per definizione, un riuso del software ottimale.
Go, invece, ha compiuto una scelta differente; gli sviluppatori di casa Google
hanno improntato il linguaggio in modo tale che esso possa snellire la complessità di
programmazione, soprattutto dal punto di vista sintattico, permettendo di scrivere,
in poche righe, un programma equivalente a decine e decine di linee di codice in
linguaggio Java.
Entrambi gli aspetti sembrano essere condivisibili; tuttavia, dal punto di vista,
puramente, progettuale, e quindi di riuso del software, è doveroso evidenziare che
sacrificare la leggibilità per la sintesi, in progetti software corposi e improntati sul
lavoro di squadra, risulta essere controproducente.
Il lavoro di squadra, infatti, implica l’adottare un livello di “astrazione” condivisibile, sufficiente a stilare una documentazione chiara e precisa.
Java, attraverso i meccanismi dell’ereditarietà e grazie alla presenza delle classi
astratte, si presta particolarmente allo sviluppo di progetti importanti, in quanto le
stesse gerarchizzazioni statiche permettono una lettura comprensibile e fissano uno
standard di programmazione elevato per tutti gli sviluppatori del progetto.
Go utilizza, invece, un approccio dinamico; la presenza di elementi quali puntatori, interfacce vuote e sintesi estrema del codice rendono di difficile attuazione
un approccio standard; pertanto, il codice appare caratterizzato da una scarsa leggibilità e da una difficile comprensione, soprattutto per ciò che riguarda il riuso di
software preesistente.
Nella sezione precedente, si è affermato che l’embedding può risultare l’approccio
più conveniente da adottare, soprattutto in presenza di massicci aggiornamenti.
Tuttavia, anche la programmazione OOP supporta, perfettamente, il meccanismo di composizione. Una porzione di codice identica a quella scritta precedentemente nel Listato 6.5 viene riportata nel Listato 6.9.
1
2
3
4
5
public class Private{
private String
private String
private String
private String
name;
surname;
taxCode;
documentNum;
6.4 Conclusioni
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private Customer customer;
public Private() {}
public Private(String name, String surname, String taxCode,
String documentNum, Customer customer) {
super();
this.name = name;
this.surname = surname;
this.taxCode = taxCode;
this.documentNum = documentNum;
this.customer = customer;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public String getTaxCode() {
return taxCode;
}
public void setTaxCode(String taxCode) {
this.taxCode = taxCode;
}
public String getDocumentNum() {
return documentNum;
}
public void setDocumentNum(String documentNum) {
this.documentNum = documentNum;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
Listato 6.9. Composizione in Java
91
7
Conclusioni e uno sguardo sul futuro
Il percorso intrapreso nella stesura della tesi è maturato da un attento lavoro di
ricerca per poter elaborare al meglio un confronto fra le peculiariarità dei linguaggi
di programmazione Java e Go.
Inizialmente, sono state descritte la storia e le caratteristiche principali dei due
linguaggi; si è potuto, pertanto, effettuare un primo raffronto evidenziando il paradigma OOP e l’interdipendenza dell’architettura tipiche di Java nonchè la sintassi
semplice e sintetica di Go.
Successivamente, si è introdotto un caso di studio basato sul modello delle transazioni bancarie, per poter mettere in evidenza la diversa gestione della programmazione concorrente. Durante le fasi di lavoro è stato constatata la maggiore efficienza
in merito alle tempistiche di esecuzione dei thread di Java rispetto alle goroutine.
Un ulteriore confronto è stato svolto rispetto alla gestione delle funzioni e delle
interfacce; in queste fasi della tesi si è potuta notare una capacità di risposta ai
problemi posti sostanzialmente simile per i due linguaggi, ma una leggibilità maggiore per Java, grazie alla maggiore “astrazione” che esso riesce ad ottonere con il
supporto del paradigma OOP.
Tale confronto ci ha indotti, alla fine del nostro viaggio, ad espandere il concetto di riuso del codice e la sua implementazione nei due linguaggi. Java, in questo
caso, si dimostra superiore, anche se i meccanismi dell’embedding, tipici di Go,
risultano più “performanti” in struct in continuo aggiornamento rispetto all’ereditarietà. Tuttavia, Java può implementare entrambi i meccanismi, mentre Go ne è
impossibilitato.
In conclusione, possiamo affermare che non esiste un linguaggio perfetto. L’aumento delle specifiche dei calcolatori porta, inesorabilmente, ad un aumento del codice da scrivere; pertanto, serve una squadra numerosa in grado di poter riutilizzare
codice con una sufficiente “astrazione”, senza dare adito a potenziali incomprensioni. Tale chiarezza, negli ultimi anni, risulta garantita solo da Java. Ma esso è,
notoriamente, piuttosto verboso; pertanto, il codice da scrivere aumenta e il ciclo
ricomincia.
Go tenta di spezzare tale ciclo attraverso un linguaggio caratterizzato dalla sintassi leggera e dall’utilizzo dei puntatori. Ma proprio la sua sintassi, non permettendo una buona “astrazione”, causa scarsa lettura del codice che, pertanto, scoraggerà
lo sviluppo in team e, quindi, il riuso.
94
7 Conclusioni e uno sguardo sul futuro
Go, pertanto, fallisce, secondo il nostro punto di vista, nell’obiettivo di “rottamare” Java.
C’è ancora molto altro da dire per quanto riguarda i due linguaggi. Essi sono,
infatti, in continua evoluzione; recentemente Java sembra voler affiancare alla strada
del rispetto al paradigma OOP, una programmazione tipicamente funzionale (ne è
un esempio l’introduzione delle funzioni anonime), mentre Go tenta di migliorare la
strada della concorrenza e del parallelismo. Pertanto, sarà interessante vedere, nel
prossimo futuro, come si evolverà il rapporto fra questi due linguaggi.
Ringraziamenti
Desidero ringraziare tutti coloro che mi hanno aiutato nella stesura di questa tesi
di laurea: ringrazio, innanzitutto, il Professor Domenico Ursino, mio Relatore, che
mi ha guidato in questo percorso con la sua disponibilità e la sua professionalità.
Proseguo ringraziando tutti i colleghi e gli amici che mi hanno incoraggiato in
questi anni di università.
Vorrei, infine, ringraziare le persone a me più care: mio padre Giovanni, mia
madre Graziella e mia sorella Silvia; il loro sostegno e il loro affetto mi ha permesso di
raggiungere questo traguardo. A voi va tutto il mio affetto e tutta la mia gratitudine.
Riferimenti bibliografici
1. So why did they decide to call it Java? http://www.javaworld.com/article/
2077265/core-java/so-why-did-they-decide-to-call-it-java-.html, 1996.
2. Write once, run anywhere? http://www.computerweekly.com/feature/Write-oncerun-anywhere, 2002.
3. OSCON 2010: Rob Pike, “Public Static Voi”. https://www.youtube.com/watch?v=
5kj5ApnhPAE, 2010.
4. Rob Pike - “Concurrency Is Not Parallelism”. https://vimeo.com/49718712, 2012.
5. Overview of the four main programming paradigms.
http://people.cs.aau.
dk/~normark/prog3-03/html/notes/paradigms_themes-paradigm-overviewsection.html, 2013.
6. Oracle and IBM Collaborate to Accelerate Java Innovation Through OpenJDK. http:
//www.oracle.com/us/corporate/press/176988, 2015.
7. Programmazione a oggetti, cos’è e come funziona.
http://www.fastweb.it/
internet/programmazione-a-oggetti-cos-e-e-come-funziona/, 2015.
8. Informazioni sulla tecnologia Java. https://www.java.com/it/about/, 2016.
9. Java Platform, Standard Edition 7 API Specification. https://docs.oracle.com/
javase/8/docs/api/, 2016.
10. Java Platform, Standard Edition 8 API Specification. https://docs.oracle.com/
javase/8/docs/api/, 2016.
11. The Go Programming Language, Documentation. https://golang.org/doc/, 2016.
12. The Go Programming Language, Packages. https://golang.org/pkg/, 2016.
13. The Go Programming Language, The Go Project. https://golang.org/project/,
2016.
14. The Golang Programming Language, Frequently Asked Questions (FAQ). https:
//golang.org/doc/faq, 2016.
15. The Java Programming Language. http://www.tiobe.com/tiobe_index?page=Java,
2016.
16. J. Arlow and I. Neustadt. UML 2 e Unified Process - Analisi e progettazione ObjectOriented. McGraw-Hill, Milano, 2006.
17. P. Atzeni, S. Ceri, S. Paraboschi, and R. Torlone. Basi di Dati - Modelli e linguaggi
di interrogazione. McGraw Hill, Milano, 2009.
18. I. Balbaert. The Way to Go: A Thorough Introduction to the Go Programming
Language. iUniverse Inc, Bloomington, Indiana, U.S.A., 2012.
19. J. Byous.
Java Technology: An Early History.
https://www.santarosa.
edu/~dpearson/mirrored_pages/java.sun.com/Java_Technology_-_An_early_
history.pdf, 2005.
98
Riferimenti bibliografici
20. C. De Sio Cesari. Manuale di Java 7: Programmazione orientata agli oggetti con Java
Standard Edition 7. Hoepli, Milano, 2012.
21. O.J. Dahl. The Birth of Object Orientation: the Simula Languages. http://www.
olejohandahl.info/old/birth-of-oo.pdf, 2001.
22. H.M. Deitel, P.J. Deitel, R. Ranon, and A. Baruzz.
Java. Fondamenti di
programmazione. Apogeo S.R.L., Milano, 2006.
23. N. Deshpande, E. Sponsler, and N. Weiss.
Analysis of the Go runtime scheduler.
http://www.cs.columbia.edu/~aho/cs6998/reports/12-12-11_
DeshpandeSponslerWeiss_GO.pdf, 2015.
24. A. Donovan and B.W. Kernighan. The Go Programming Language. Financial
Times/Prentice Hall, 2015.
25. J. Gosling, B. Joy, G. Steele, G. Bracha, and A. Buckley. The Java Language Specification, Java SE 8 Edition (Java Series). Addison-Wesley Professional, Boston,
Massachusetts, U.S.A., 2014.
26. M. Hicks. Interview with Go’s Russ Cox and Sameer Ajmani. http://www.plenthusiast.net/2015/03/25/interview-with-gos-russ-cox-and-sameerajmani/, 2015.
27. C. Horstmann. Concetti di Informatica e fondamenti di Java 2 Tecniche avanzate(II
Edizione) Edizione italiana a cura di Marcello Delpasso. Apogeo S.R.L., Milano, 2002.
28. C. Horstmann. Concetti di Informatica e fondamenti di Java (IV Edizione) Edizione
italiana a cura di Marcello Delpasso. Apogeo S.R.L., Milano, 2007.
29. W. Kennedy, B. Ketelsen, and E.S. Martin. Go in Action. Manning Publications,
Greenwich, Connecticut, U.S.A., 2015.
30. T. Lindholm, F. Yellin, G. Bracha, and A. Buckley. The Java Virtual Machine
Specification: Java SE 8 Edition. Financial Times/Prentice Hall, 2014.
31. P. Niemeyer and D.Leuck. Learning Java: Edition 4. O’Reilly Media Inc, Sebastopol,
California, U.S.A., 2013.
32. R. Ramakrishnan and J. Gehrke. Sistemi di Basi di Dati. McGraw Hill, Milano, 2004.
33. P. Van Roy. Programming Paradigms for Dummies: What Every Programmer Should
Know. https://www.info.ucl.ac.be/~pvr/VanRoyChapter.pdf, 2016.