Sei sulla pagina 1di 252

Christian Nigro, Libero Nigro

Lezioni di
P rogrammazione Orientata
agli Oggetti in Java
conelementi di strutture di dati
e architettura dei calcolatori

S
Pitagora Editrice Bologna
Premessa ______ ______
Questo testo raccoglie gran parte delle lezioni del corso di Programmazione Orientata agli Oggetti (POO) che
Libero Nigro svolge da diversi anni presso il corso di laurea in Ingegneria Informatica dell’Llniversità della
Calabria. Le lezioni assumono che l’allievo abbia già seguito un primo corso di Fondamenti di Informatica e
dunque abbia già familiarizzato con i concetti fondamentali di algoritmo, calcolatore e risoluzione algoritmica di
problemi secondo lo stile procedurale. I primi due capitoli del testo, comunque, richiamano gli argomenti di
base della programmazione procedurale in Java, cioè i tipi primitivi, le strutture di controllo e la gestione di
strutture dati array unitamente allo sviluppo di diversi programmi dimostrativi. Dal terzo capitolo in poi si
approfondisce la programmazione orientata agli oggetti in Java e la messa a punto di classi ‘tagliate su
misura" delle applicazioni, organizzate in biblioteche di moduli riutilizzabili, robuste rispetto al verificarsi di
eccezioni, ed eventualmente dotate di interfaccia grafica di interazione (GUI). Lo studio della POO include i
meccanismi di programmazione mediante tipi generici, e approfondisce classi proprie della libreria di Java ed
in particolare il collection framework (liste, set e mappe) considerato il suo ruolo strategico ai fini delle
applicazioni. Successivamente si forniscono elementi di conoscenza riguardanti l’implementazione di
collezioni custom lineari (liste concatenate) e non lineari (alberi e grafi), le tecniche di programmazione
ricorsiva, si introducono i concetti di complessità degli algoritmi, si presentano algoritmi efficienti di
ordinamento, si discutono alcune strutture di dati e le nozioni dello unit testing. I meccanismi della POO
vengono messi in pratica attraverso il progetto e lo sviluppo di applicazioni non banali. In particolare, si mostra
una realizzazione ad oggetti di un sistema software che emula un calcolatore didattico (RASP) utilizzato per la
programmazione in assembler, ed una libreria di classi a supporto di programmi basati sui grafi. Un capitolo a
parte è dedicato ad un’introduzione alla programmazione multi-thread e al progetto di classi thread-safe, con
diversi esempi di programmi concorrenti. Chiudono il testo due appendici nelle quali rispettivamente vengono
studiati (a) la rappresentazione in bit delle informazioni, (b) il calcolatore didattico RASP.
Il testo si caratterizza per uno stile di presentazione “essenziale” ma rigoroso, e per la “prevalenza del codice",
ossia la scrittura dettagliata di programmi completi sui vari argomenti affrontati.
Si ritiene che il testo possa essere utile nel biennio di Ingegneria Informatica o dei corsi di studio in
Informatica.
Si ringraziano i colleghi ed amici Franco Cicirelli, Angelo Furfaro e Francesco Pupo per le utili discussioni su
molte parti del testo, che hanno consentito di migliorare la presentazione e rimuovere errori. Gli autori saranno
grati a quanti vorranno segnalare ogni altro errore inevitabilmente rimasto, inviando una email all’indirizzo
l.nigro@unical.it.

ISBN 88-371 -1893-7

co Copyright 2014 by Pitagora Editrice s.r.l., Via del Legatore 3, 40138 Bologna, Italy.

Tutti i diritti sono riservati, nessuna parte di questa pubblicazione può essere riprodotta, memorizzata o trasmessa per
mezzo elettronico, elettrostatico, fotocopia, ciclostile, senza il permesso dell'Editore.
Stampa: Pitagora Editrice s.r.l., Via del Legatore 3,40138 Bologna, Italy.
Codice: 49/19

http://www.pitagoragroup.it
e-mail: pited@pitagoragroup.it
Indice ______
Capitolo 1: ............................................................................................................................................................... 1
Concetti di programmazione procedurale in Java..................................................................................................1
Un primo programma:.................................................................................................................................... 1
Formato dell'output:.......................................................................................................................................3
Tipi di base.........................................................................................................................................................3
Conversioni di tipo e casting.............................................................................................................................. 4
Incremento/decremento e assegnamento con aritmetica................................................................................. 4
Algebra di Boole.................................................................................................................................................5
Proprietà dell’algebra di Boole...........................................................................................................................5
Operatori booleani e corto circuito.................................................................................................................... 6
La classe Math....................................................................................................................................................7
Classe Scanner (Java 5 o versione superiore).................................................................................................7
Il metodo printf di System.out (Java 5 o versione superiore)........................................................................... 8
Espressioni e assegnazione...............................................................................................................................8
Strutture di controllo...........................................................................................................................................8
Selezione a due vie (if-else)...............................................................................................................................9
Un esempio di programma:............................................................................................................................ 9
Compilazione/esecuzione del programma:..................................................................................................... 9
Ciclo di while (o a condizione iniziale).............................................................................................................10
Ciclo di while a condizione finale......................................................................................................................10
If-implicito (operatore ?)....................................................................................................................................11
Selezione n-aria (switch) Analisi dei casi possibili.......................................................................................... 11
Istruzione for..................................................................................................................................................... 11
Un programma per il calcolo della potenza an .................................................................................................12
Un programma per l'equazione di secondo grado.......................................................................................... 13
Massimo comun divisore ed algoritmo di Euclide............................................................................................13
Somma dei primi N numeri naturali..................................................................................................................14
Equivalenza di Gauss.......................................................................................................................................14
Calcolo del fattoriale.........................................................................................................................................14
Calcolo del mcm...............................................................................................................................................15
Calcolo del fattoriale affidato ad un metodo.....................................................................................................15
Un metodo potenza..........................................................................................................................................16
Un secondo metodo potenza...........................................................................................................................16
Un terzo metodo potenza.................................................................................................................................17
Un metodo che verifica se un intero positivo è primo......................................................................................17
Metodo di Newton per il calcolo della radice quadrata di un numero reale.................................................... 17
Sviluppo di un programma................................................................................................................................18
Programma Lotto:........................................................................................................................................18
Caso di studio: sviluppo di un programma Calendario per passi successivi.................................................. 20
Versione di massima del programma:........................................................................................................... 20
Programma completo:..................................................................................................................................22
Perfezionamenti:......................................................................................................................................... 23
Strutturazione in metodi:.............................................................................................................................. 24
Un altro caso di studio: Sottosequenza di dimensione massima.................................................................... 25
Un primo algoritmo:......................................................................................................................................25
Un programma Java:...................................................................................................................................26
Nuova versione del programma:...................................................................................................................27
Esercizi.............................................................................................................................................................28
Capitolo 2 :.............................................................................................................................................................29
Strutture dati array................................................................................................................................................29
Array monodimensionali o vettori.................................................................................................................... 29
i
Indice Indice

Caso di studio: mini-statistica sui voti di un campione di studenti.................................................................. 30 Altri esempi di passaggi di parametri.............................................................................................................. 76
Programma Statistica:..................................................................................................................................30 “Buon comportamento" sui parametri.............................................................................................................. 76
Calcolo della moda:.....................................................................................................................................31 Ancora sui costruttori di una classe................................................................................................................. 77
Prodotto scalare di due vettori:................................................................................................................... 35 Regole di visibilità dei nomi..............................................................................................................................77
Ricerca lineare:.......................................................................................................................................... 35 Classi e visibilità globale...................................................................................................................................78
Ricerca binaria:.......................................................................................................................................... 35 Argomenti variabili (vararg)..............................................................................................................................79
Un metodo per la ricerca binaria:.................................................................................................................36 System.out.printf e vararg................................................................................................................................79
Algoritmi di ordinamento...................................................................................................................................36 Enumerazioni....................................................................................................................................................79
Un metodo selectionSort:............................................................................................................................37
Un metodo bubbleSort:...............................................................................................................................38 Concetto di bean...............................................................................................................................................80
Un metodo insertionSort:............................................................................................................................39 Esercizi.............................................................................................................................................................80
Triangolo di Tartaglia........................................................................................................................................39 Capitolo 4 :.............................................................................................................................................................85
Array bidimensionali e matrici..........................................................................................................................40 Ereditarietà, dynamic binding e polimorfismo...................................................................................................... 85
Somma righe e colonne:.............................................................................................................................41 Una classe ContoBancario.............................................................................................................................. 85
Richiami di algebra lineare..............................................................................................................................42 Un conto bancario con fido.............................................................................................................................. 86
Progetto di un programma...............................................................................................................................45 Una classe ContoConFido erede di ContoBancario:...................................................................................... 86
Programma Matrici:.................................................................................................................................... 45 Il pronome super...............................................................................................................................................86
Quadrato magico..............................................................................................................................................46 Un’implementazione di ContoConFido con gestione dello “scoperto":............................................................ 86
Array multi-dimensionali...................................................................................................................................46 Relazione di ereditarietà...................................................................................................................................88
Esercizi.............................................................................................................................................................47 Assegnazione tra oggetti come “proiezione"................................................................................................... 88
Capitolo 3 :............................................................................................................................................................49 Tipo statico e tipo dinamico di un oggetto....................................................................................................... 89
Classi e oggetti.....................................................................................................................................................49 Assegnazione dal generale al particolare ? .................................................................................................... 89
Una classe Punto............................................................................................................................................ 49 Dynamic binding e polimorfismo..................................................................................................................... 90
Variabili di istanza........................................................................................................................................... 50 Ereditarietà e ridefinizione di metodi............................................................................................................... 90
Il pronome this................................................................................................................................................ 50 Ereditarietà singola...........................................................................................................................................91
Oggetti e riferimenti........................................................................................................................................ 51 Ereditarietà vs. composizione......................................................................................................................... 91
La costante nuli............................................................................................................................................... 52 L’antenato “cosmico" Object............................................................................................................................ 91
Oggetti incapsulati.......................................................................................................................................... 53 Strutture dati eterogenee..................................................................................................................................92
Una classe Triangolo...................................................................................................................................... 54 Riassunto modificatori......................................................................................................................................92
Overloading dei metodi................................................................................................................................... 56 Gestione casi “anomali" (preliminare)......................... 92
Una classe Poligono....................................................................................................................................... 56 Una classe BancaArray (facade)..................................................................................................................... 93
Caso di studio: un programma genetico........................................................................................................57 Un altro esempio di gerarchia di classi........................................................................................................... 94
Programma GiocoDellaVita:....................................................................................................................... 58 Una classe Contatore:.................................................................................................................................94
Una classe Razionale..................................................................................................................................... 60 Una classe ContatoreModulare specializzazione di Contatore:.......................................................................95
Ordine di esecuzione dei costruttori................................................................................................................ 96
Entità static..................................................................................................................................................... 62
Entità di istanza ed entità di classe................................................................................................................63 Gerarchie di classi e finalize.............................................................................................................................97
Progetto di classi di utilità............................................................................................................................... 64 Il metodo getClass() di Object......................................................................................................................... 97
Caso di studio: Una classe Retta.....................................................................................................................98
Caso di studio: una classe Data.....................................................................................................................65
Esperimenti casuali........................................................................................................................................ 67 Esercizi........................................................................................................................................................... 103
Librerie di classi riutilizzabili e packaging.......................................................................................................68 Capitolo 5 :........................................................................................................................................................... 105
Classi astratte e interfacce.................................................................................................................................. 105
Direttiva package............................................................................................................................................69
La variabile di ambiente classpath.................................................................................................................70 Una gerarchia di classi per figure geometriche piane....................................................................................105
Una classe astratta Figura:.........................................................................................................................105
Importazioni di classi......................................................................................................................................70
Una classe concreta Cerchio:..................................................................................................................... 106
Compilazione/esecuzione di programmi in presenza di package................................................................. 71 Una classe concreta Rettangolo:................................................................................................................ 106
Conflitti e risoluzione......................................................................................................................................72 Una classe astratta per il problema dell’ordinamento................................................................................... 107
Libreria di Java...............................................................................................................................................72 Ordinare razionali........................................................................................................................................... 109
Importazione statica (Java 5 o versioni superiori)..........................................................................................73 Limiti dell'approccio........................................................................................................................................ 110
Ambiente di sviluppo Eclipse (cenni)..............................................................................................................73 Il concetto di interfaccia..................................................................................................................................110
Passaggio di parametri ai metodi...................................................................................................................73 Razionali comparabili..................................................................................................................................... 110
Cosa succede in Java ? .................................................................................................................................74 La classe di utilità Array di poo.util................................................................................................................111
Parametri attuali..............................................................................................................................................75 Ordinamento di razionali comparabili............................................................................................................111
Nozione di record di attivazione o trame........................................................................................................75 Discussione.................................................................................................................................................... 112
ii iii
Indice Indice

Regole di “buon progetto” di una classe Java................................................................................................112 I metodi salva/ripristina..............................................................................................................................154


Un altro esempio d’uso delle interfacce.........................................................................................................112 Un programma GestioneAgendina:............................................................................................................. 155
L'interfaccia FiguraSolida:...........................................................................................................................114 Struttura dei comandi:................................................................................................................................155
Un’interfaccia come pacchetto di costanti......................................................................................................114 II metodo aggiungiNominativo:....................................................................................................................156
Esercizi........................................................................................................................................................... 115 I metodi comandi ed errore:........................................................................................................................156
Capitolo 6 :........................................................................................................................................................... 117 I metodi rimuoviNominativo e ricercaTelefono:............................................................................................ 156
I metodi ricercaPersona e mostraElenco:.....................................................................................................157
Classi stringhe di caratteri................................................................................................................................... 117
I metodi salva e carica:.............................................................................................................................. 157
Metodi di String di uso ricorrente....................................................................................................................117 II metodo quit:........................................................................................................................................... 158
Esempio.......................................................................................................................................................... 119 Ancora sulle classi enum................................................................................................................................158
Proliferazione di stringhe garbage..................................................................................................................119 Esempio 1: Una nuova classe Data...............................................................................................................159
Argomenti di un programma........................................................................................................................... 120 Esempio 2: Un tipo enumerato Operazione.................................................................................................. 161
Classi StringBuffer e StringBuilder.................................................................................................................121 Enumerazioni e singleton...............................................................................................................................162
La classe StringTokenizer..............................................................................................................................123 Esercizi...........................................................................................................................................................162
Tokenizzazione mediante uno Scanner.........................................................................................................123 Altre letture.....................................................................................................................................................162
Caso di studio: valutazione di un’espressione aritmetica intera................................................................... 124 Capitolo 9 :...........................................................................................................................................................163
Esercizi........................................................................................................................................................... 125 Collection framework e progetto di collezioni custom....................................................................................... 163
Capitolo 7 :........................................................................................................................................................... 127 L’interfaccia Collection<T>.............................................................................................................................164
Concetti sulle eccezioni...................................................................................................................................... 127 L’interfaccia lterator<T>..................................................................................................................................165
Gerarchia di classi di eccezioni......................................................................................................................128 Schema d'uso di un iterator............................................................................................................................165
Vincoli sulle eccezioni checked......................................................................................................................130 Pattern tipico della remove.............................................................................................................................166
Il blocco try-catch............................................................................................................................................131 Metodi aggiunti dall’interfaccia List<T> che estende Collection<T>............................................................. 166
Il blocco try-finally........................................................................................................................................... 131 Metodi dell’interfaccia Listlterator<T> che estende lterator<T>................................................................... 166
Flusso del controllo......................................................................................................................................... 132 Un esempio di add() in ordine:....................................................................................................................167
Caso di studio: risoluzione di un sistema di equazioni lineari........................................................................133 Costruzione di una lista ordinata di stringhe mediante Listlterator:................................................................168
Una classe astratta Sistema:......................................................................................................................133 Classi ArrayList<T> e LinkedList<T>.............................................................................................................168
Il metodo di Gauss..........................................................................................................................................133 Concetti della lista concatenata semplice..................................................................................................... 168
Una classe Gauss:..................................................................................................................................... 134 Costruttori di ArrayList:.............................................................................................................................. 169
Una classe Sistemasingolare:.................................................................................................................... 135 Metodi propri di ArrayList<T>:.....................................................................................................................169
Un esempio di main:.................................................................................................................................. 136 Metodi propri di LinkedList<T>:...................................................................................................................169
Esercizi........................................................................................................................................................... 136 Richiami su stack e coda................................................................................................................................170
Capitolo 8 :........................................................................................................................................................... 139 Esempio di gestione di uno stack:............................................................................................................... 170
Tipi di dati astratti...............................................................................................................................................139 La classe di utilità Collections........................................................................................................................171
Un esempio di ADT...................................................................................................................................... 139 L’interfaccia Set<T>........................................................................................................................................171
L’ADT Vector:............................................................................................................................................139 Le classi HashSet<T> e TreeSet<T>.............................................................................................................171
Semantica delle operazioni............................................................................................................................ 139 Organizzazione di un albero binario di ricerca.............................................................................................. 171
Un’implementazione di Vector basata su array..............................................................................................140 Tabelle hash e collisioni.................................................................................................................................172
Un Vector generico e parametrico............................................................................................... 144 Operazioni insiemistiche.................................................................................................................................172
Classi wrapper dei tipi primitivi.......................................................................................................................145 Concetto di Map(pa........................................................................................................................................173
Boxing/unboxing automatico dei valori dei tipi primitivi..................................................................................146 L’interfaccia Map<K,V>:............................................................................................................................. 173
Vector<T> generico e parametrico.................................................................................................................146 Le classi HashMap<K,V> e TreeMap<K,V>................................................................................................. 174
L'interfaccia Vector<T>:..............................................................................................................................146 Riallocazione di una tabella hash...................................................................................................................174
La classe ArrayVector<T>:..........................................................................................................................147 L’interfaccia Com paratore^..........................................................................................................................174
Tipi “grezzi” e tipi generici............................................................................................................................... 149 Un esempio d’uso di Comparator...................................................................................................................174
L’interfaccia Comparable generica:............................................................................................................. 150 Estensione ed istanziazione al “volo".............................................................................................................175
Tabelle e loro rappresentazione.....................................................................................................................150
La classe di utilità Arrays................................................................................................................................175
Caso di studio: un programma per la gestione di un’agendina telefonica.................................................... 151
L’interfaccia lterable<T>.................................................................................................................................176
Una classe Nominativo:..............................................................................................................................151
Agendina come ADT:................................................................................................................................. 152 Ciclo for-each..................................................................................................................................................176
Una classe AdendinaVector:.......................................................................................................................152 Caso di studio: il Crivello di Eratostene..........................................................................................................177
I metodi aggiungi/rimuovi:...........................................................................................................................153 L'interfaccia Crivello e una classe astratta CrivelloAstratto:..........................................................................178
I metodi cerca:...........................................................................................................................................153 Una classe CrivelloSet:.............................................................................................................................. 178
II metodo toString:...................................................................................................................................... 154 Vector<T> generico in versione iterabile........................................................................................................180
Le inner class..................................................................................................................................................181
IV
Indice Indice

For-each su oggetti Vector............................................................................................................................. 181 La classe RandomAccessFile....................................................................................................................... 217


Ancora sulle inner class.................................................................................................................................. 182 Ricerca binaria su un RandomAccessFile di interi ordinato:..........................................................................217
Progetto di collezioni custom..........................................................................................................................182 Insertion sort su un file di interi...................................................................................................................... 218
L'interfaccia Agendina con i commenti speciali per javadoc:......................................................................... 183 Flussi testuali..................................................................................................................................................219
Una classe astratta AgendinaAstratta:.........................................................................................................185 Lettura di stringhe da tastiera........................................................................................................................ 220
Classi collezioni concrete...............................................................................................................................187 Costruttori di Scanner.....................................................................................................................................221
Una classe AgendinaMap:..........................................................................................................................187 PrintStream (es. System.out).........................................................................................................................221
Una classe AgendinaSet:............................................................................................................................188 Flussi di oggetti...............................................................................................................................................221
Una classe AgendinaLL:.............................................................................................................................188 Classi per flussi di oggetti:......................................................................................................................... 222
Una classe AgendinaAL:................................................................................. 189 Salvataggio di un'agendina mediante serializzazione.................................................................................. 222
Osservazioni conclusive................................................................................................................................. 190 serialVersionUID.............................................................................................................................................223
Classi storiche della piattaforma Java............................................................................................................190 Un esempio di applicazione della serializzazione/esternalizzazione............................................................224
Caso di studio: Crivello di Eratostone basato su BitSet.................................................................................191 Una classe BancaAstratta:......................................................................................................................... 225
Esercizi........................................................................................................................................................... 192 Classi ContoBancario e ContoConFido esternalizzate:................................................................................ 225
Altre letture..................................................................................................................................................... 192 Tipi enumerati e serializzazione.................................................................................................................... 227
Capitolo 10:......................................................................................................................................................... 193 Gestione di file e classe File.......................................................................................................................... 227
Tipi generici......................................................................................................................................................... 193 Una classe ObjectFile con lettura anticipata................................................................................................. 228
Generici e sotto tipi......................................................................................................................................... 194 Caso di studio: fusione ordinata di file .......................................................................................................... 230
Wildcard.......................................................................................................................................................... 194 La classe MergeFile...................................................................................................................................231
Bounded wildcard........................................................................................................................................... 195 Caso di studio: ordinamento esterno di file per fusione naturale.................................................................. 232
Classi con più tipi generici.............................................................................................................................. 195 La classe NaturalMergeSort....................................................................................................................... 233
Metodi generici............................................................................................................................................... 195 Esercizi...........................................................................................................................................................236
Il metodo generico Collections.max:............................................................................................................196 Altre letture.....................................................................................................................................................237
Type erasure................................................................................................................................................... 197 Capitolo 13:.........................................................................................................................................................239
Restrizioni....................................................................................................................................................... 197 Elementi di programmazione dell’interfaccia utente grafica.............................................................................. 239
Una classe Pair<T>:.................................................................................................................................. 198 Gerarchia base di finestre..............................................................................................................................239
Un metodo generico minMax:.....................................................................................................................198 Event delegation model..................................................................................................................................239
Un altro metodo generico minMax:............................................................................................................. 199 Classificazione (parziale) degli eventi........................................................................................................... 240
Eliminazione di parametri tip i.........................................................................................................................199 Gerarchia delle classi di eventi di AWT......................................................................................................... 240
Regola Get/Put e wildcard.............................................................................................................................. 199 Interfacce di ascoltatori di eventi (Event Listener)........................................................................................ 241
Sotto tipi e covarianza....................................................................................................................................200 Adattatori (classi adapter)...............................................................................................................................242
Type erasure e metodi bridge........................................................................................................................201 Progetto di un ascoltatore...............................................................................................................................242
Wildcard capture.............................................................................................................................................203 Intercettazione e gestione dell’evento di chiusura:....................................................................................... 243
Tipi generici e codice legacy..........................................................................................................................203 Sistema di coordinate.....................................................................................................................................244
Esercizi...........................................................................................................................................................205 Gerarchia di componenti GUI........................................................................................................................ 244
Altre letture.....................................................................................................................................................206 Inizializzazione di una JFrame...................................................................................................................... 244
Capitolo 11:.........................................................................................................................................................207 Uso di JFrame e JPanel.................................................................................................................................245
Ingresso/uscita grafico........................................................................................................................................207 Una finestra di cambio euro-lire.................................................................................................................... 247
Show confirm dialog.......................................................................................................................................208 Codice Java per la finestra di cambio:........................................................................................................ 247
Selezione di un file e classe JFileChooser.................................................................................................... 209 Una finestra con repainting.............................................................................................................................248
Esempio..........................................................................................................................................................211 Repainting e mouse:..................................................................................................................................250
Esercizi...........................................................................................................................................................211 Altri componenti di GUI...................................................................................................................................252
Capitolo 12:.........................................................................................................................................................213 JTextArea................................................................................................................................................. 252
Flussi e file ..........................................................................................................................................................213 JCheckBox................................................................................................................................................ 252
Classi base per i flussi binari......................................................................................................................... 213 JRadioButton............................................................................................................................................ 252
Esempi di classi eredi concrete:..................................................................................................................213 JComboBox.............................................................................................................................................. 253
Copia di un file................................................................................................................................................214 JSlider.......................................................................................................................................................253
Crittografia elementare (cifrario di Giulio Cesare)......................................................................................... 214 JMenuBar, JMenu, JMenultem, JPopupMenu.............................................................................................253
Flussi bufferizzati............................................................................................................................................215 Un caso di studio - La classe AgendinaGUI................................................................................................. 254
Le interface DataOutput e Datalnput............................................................................................................. 215 La classe AgendinaGUI2................................................................................................................................256
Creazione di un file di interi............................................................................................................................215 Esercizi...........................................................................................................................................................256
Osservazioni...................................................................................................................................................216 Altre letture.....................................................................................................................................................256
vi vii
Indice Indice

Capitolo 14:.........................................................................................................................................................257 Esercizi...........................................................................................................................................................304


Introduzione alle espressioni regolari................................................................................................................ 257 Capitolo 18:.........................................................................................................................................................305
Esempi di regex..............................................................................................................................................257 Tecniche di programmazione ricorsiva.............................................................................................................. 305
Gruppi.............................................................................................................................................................258 Calcolo della potenza an.................................................................................................................................305
Espressione regolare di un numero realeJava.............................................................................................. 258 Analisi dell’esecuzione di potenza(2,3)......................................................................................................... 305
Il carattere punto 7 .........................................................................................................................................259 Esercizio.........................................................................................................................................................306
Abbreviazioni..................................................................................................................................................259 Ricorsione in coda (tail recursion)................................................................................................................. 306
Le classi Pattern e Matcher del packagejava.util.regex................................................................................ 259 Ricorsione e divide-et-impera........................................................................................................................ 307
Opzioni utili del metodo compile() di Pattern................................................................................................. 260 Torri di Hanoi..................................................................................................................................................308
Caso di studio: un programma di patternmatching....................................................................................... 260 Una soluzione Java:...................................................................................................................................308
Esercizi...........................................................................................................................................................261 Calcolo delle permutazioni.............................................................................................................................309
Altre letture.....................................................................................................................................................261 Problema delle N regine.................................................................................................................................310
Capitolo 15:.........................................................................................................................................................263 Una prima soluzione:.................................................................................................................................311
Usta concatenata................................................................................................................................................263 Una seconda soluzione:.............................................................................................................................312
Gestione di una lista in Java......................................................................................................................... 263 Stack ed Heap................................................................................................................................................314
Il metodo inserisci)........................................................................................................................................266 Conversione ricorsione-iterazione................................................................................................................. 315
Il metodo rimuovi()..........................................................................................................................................268 Un metodo ricorsivo per l'ordinamento: Merge Sort..................................................................................... 317
Casi della rimozione:.................................................................................................................................. 268 Complessità di Merge Sort............................................................................................................................ 318
Costruttore di copia........................................................................................................................................268 Il metodo di ordinamento QuickSort.............................................................................................................. 320
Un metodo main()...........................................................................................................................................269 Esercizi...........................................................................................................................................................322
NulIPointerException......................................................................................................................................269 Capitolo 19:.........................................................................................................................................................323
Caso di studio: progetto di una collezioneordinata........................................................................................270 Strutture dati ricorsive e non lineari................................................................................................................... 323
Una classe ListaOrdinataConcatenata<T>:..................................................................................................272 Lista concatenata e ricorsione....................................................................................................................... 323
Stack ADT e gerarchia di classi.................................................................................................................... 274 Albero binario..................................................................................................................................................326
Una classe StackAstratto<T>..................................................................................................................... 274 La classe AlberoBinarioDiRicerca<T>:........................................................................................................ 327
Una classe StackConcatenato<T>:............................................................................................................. 276 Albero binario degli operatori di un’espressione aritmetica.......................................................................... 331
Una classe StackArray<T>:....................................................................................................................... 277 Caso di studio.................................................................................................................................................332
Un’applicazione di test per lo stack:............................................................................................................279 Esempio di sessione:..........................................%....................................................................................335
Coda ADT e gerarchia di classi..................................................................................................................... 280 PostOrder iterativo..........................................................................................................................................336
Una classe CodaAstratta<T>:.....................................................................................................................280 Alberi n-ari......................................................................................................................................................336
Una classe CodaConcatenata<T>:.............................................................................................................282 Grafi................................................................................................................................................................337
Una classe BufferLimitato<T>:....................................................................................................................283 Altri concetti e definizioni:.......................................................................................................................... 338
Un’applicazione di test per la coda:.............................................................................................................285 Rappresentazione in memoria di un grafo.................................................................................................... 338
Una classe ListaDoppia..................................................................................................................................287 Liste di adiacenze...........................................................................................................................................339
Esercizi...........................................................................................................................................................289 Il grafo come abstract data type: un esempio...............................................................................................339
Progetto..........................................................................................................................................................290 Operazioni di visita.........................................................................................................................................341
Capitolo 16:.........................................................................................................................................................293 Visita in ampiezza:.....................................................................................................................................341
Sviluppo di un’applicazione: aritmetica di polinomi........................................................................................... 293 Visita in profondità:....................................................................................................................................341
Una classe Monomio:................................................................................................................................ 293 Raggiungibilità................................................................................................................................................342
Un’interfaccia Polinomio:........................................................................................................................... 294 Esempio di grafo di raggiungibilità:.............................................................................................................342
Un diagramma di classi UML:.................................................................................................................... 295 Esercizi...........................................................................................................................................................343
La classe PolinomioLL:.............................................................................................................................. 297 Progetto..........................................................................................................................................................344
La classe PolinomioConcatenato:...............................................................................................................298 Capitolo 20:.........................................................................................................................................................345
Un programma di test:............................................................................................................................... 299 Struttura dati Heap e HeapSort......................................................................................................................... 345
Esercizi...........................................................................................................................................................300 Heap - definizione e proprietà...................................................................................................................... 345
Capitolo 17:.........................................................................................................................................................301 Aggiunta di un elemento.................................................................................................................................345
Concetti di complessità degli algoritmi................................................................................................................301 Rimozione del minimo....................................................................................................................................346
Complessità “esatta" di selection sort............................................................................................................301 Efficienza delle operazioni di inserimento/rimozione.................................................................................... 347
Notazione big O (ordine di) e comportamento asintotico per n->oo............................................................. 302 Come sfruttare l’efficienza dell’heap ? .......................................................................................................... 348
Operatore Q. grande.......................................................................................................................................302 Possibili usi di una struttura dati heap........................................................................................................... 348
Operatore © grande.......................................................................................................................................302 Complessità di Heap Sort...............................................................................................................................349
Alcune complessità.........................................................................................................................................303 Caso di studio: implementazione di una classe Heap.................................................................................. 349
vili IX
Indice Indice

La classe PriorityQueue<E> di java.util........................................................................................................ 351 Test di unità e JUnit........................................................................................................................................411


Costruttori:................................................................................................................................................ 351 JUnit 4.x....................................................................................................................................................411
Metodi:...................................................................................................................................................... 351 Esercizi...........................................................................................................................................................413
Esercizi...........................................................................................................................................................351 Altre letture.....................................................................................................................................................413
Capitolo 21:.........................................................................................................................................................353 Capitolo 23:.........................................................................................................................................................415
Sviluppo di programmi ad oggetti.......................................................................................................................353 Introduzione alla programmazione multi-thread................................................................................................ 415
Sommario della notazione UML sulle classi.................................................................................................. 353 Una prima applicazione multi-thread............................................................................................................. 416
Relazioni (associazioni) tra classi e navigabilità:.......................................................................................... 354 Un generatore interrompibile......................................................................................................................... 417
Relazioni e molteplicità:............................................................................................................................. 354 Metodi della classe Thread............................................................................................................................ 418
Associazioni tutto/parti:.............................................................................................................................. 355 Merge sort multi-thread..................................................................................................................................419
Indice dei riferimenti incrociati in un testo..................................................................................................... 355 Stazione di monitoraggio................................................................................................................................420
La classe GestoreTesto:............................................................................................................................ 356 Una stazione “naif” .........................................................................................................................................422
Tipo astratto Indice:...................................................................................................................................358 Una stazione thread-safe...............................................................................................................................423
La classe Parola:.......................................................................................................................................358 La classe Monitoraggio:............................................................................................................................. 423
La classe IndiceAstratto:............................................................................................................................ 359
Mutua esclusione e sospensione...................................................................................................................424
La classe IndiceLinkato:............................................................................................................................. 360
La classe IndiceMappato:.......................................................................................................................... 360 Cenni al Java Memory Model........................................................................................................................ 425
Una concretizzazione custom di IndiceAstratto:...........................................................................................361 Produttore/Consumatore e BufferLimitato..................................................................................................... 425
L'applicazione Crosslndex:........................................................................................................................ 361 N Produttori M Consumatori.......................................................................................................................... 428
Un’implementazione della macchina RASP.................................................................................................. 362 Mailbox con risvegli FIFO.............................................................................................................................. 429
L’assemblatore a due passate:...................................................................................................................362 Mailbox con risvegli prioritari......................................................................................................................... 430
Organizzazione del programma:.................................................................................................................363 Il problema dei cinque filosofi.........................................................................................................................432
Analizzatore lessicale (classe Lexer):......................................................................................................... 364 Uso di blocchi synchronized...........................................................................................................................435
L’Assemblatore (classe Assembler):........................................................................................................... 365 Scambiatore sincrono.....................................................................................................................................436
La classe ObjectModule:............................................................................................................................ 370 Classi Produttore e Consumatore...............................................................................................................437
La classe Simbolo:.....................................................................................................................................371 Esempio di output:.....................................................................................................................................438
La classe TabellaSimboli:.......................................................................................................................... 372 Thread e sistema delle eccezioni.................................................................................................................. 439
La classe JRVM:........................................................................................................................................372 Concorrenza e collection framework............................................................................................................. 440
Una gerarchia di classi per la gestione dei grafi........ ...................................................................................376 La classe java.util.Vector<T> ........................................................................................................................ 442
Il grafo come abstract data type:.................................................................................................................376 Concorrenza e oggetti immutabili.................................................................................................................. 442
La classe Arco<N>:....................................................................................................................................378
Variabili volatili................................................................................................................................................443
La classe ArcoPesato<N>:......................................................................................................................... 378
La classe GrafoAstratto<N>:.......................................................................................................................379 Un esempio:.............................................................................................................................................. 443
L’interfaccia GrafoNonOrientato<N>:.......................................................................................................... 382 Metodi di Atomiclnteger..................................................................................................................................444
La classe GrafoNonOrientatoAstratto<N>:...................................................................................................382 Buffer limitato con risvegli FIFO, basato su Lock/Condition......................................................................... 445
La classe GrafoNonOrientatolmpl<N>:....................................................................................................... 383 Esercizi...........................................................................................................................................................447
L'interfaccia GrafoPesato<N>:....................................................................................................................385 Altre letture.....................................................................................................................................................448
L'interfaccia GrafoNonOrientatoPesato<N>:................................................................................................386 Appendice A:.......................................................................................................................................................449
La classe GrafoNonOrientatoPesatoAstratto<N>:........................................................................................ 386 Rappresentazione in bit delle informazioni........................................................................................................ 449
La classe GrafoNonOrientatoPesatolmpl<N>:.............................................................................................388 Sistemi di numerazione posizionali............................................................................................................... 449
L'interfaccia GrafoOrientato<N>:................................................................................................................391 Sistemi ricorrenti:.......................................................................................................................................449
La classe astratta GrafoOrientatoAstratto<N>:............................................................................................391 Conversioni di base di numeri naturali.......................................................................................................... 449
La classe GrafoOrientatolmpl<N>:.............................................................................................................. 392 Conversione di una frazione decimale in una base B /1 0 ............................................................................ 451
L'interfaccia GrafoOrientatoPesato<N>:..................................................................................................... 394 Codifica indiretta in b it....................................................................................................................................452
La classe astratta GrafoOrientatoPesatoAstratto<N>:..................................................................................394
Altri codici BCD...............................................................................................................................................453
La classe GrafoOrientatoPesatolmpl<N>:....................................................................................................396
La classe Peso:.........................................................................................................................................398 Stringhe di bit di lunghezza n .........................................................................................................................453
La classe Grafi del package poo.util:.......................................................................................................... 400 Aritmetica binaria............................................................................................................................................454
Esercizi...........................................................................................................................................................403 Rappresentazione dei numeri negativi.......................................................................................................... 455
Progetto..........................................................................................................................................................404 Rappresentazione per segno e modulo........................................................................................................ 455
Capitolo 22:.........................................................................................................................................................407 Rappresentazione per complementi diminuiti............................................................................................... 455
Concetti di unit testing.........................................................................................................................................407 Rappresentazione per complementi alla base.............................................................................................. 455
L’istruzione asserì...........................................................................................................................................408 Un metodo per rivelare i bit di un intero:..................................................................................................... 457
Esempi:.....................................................................................................................................................408 Operatori su interi a livello di b it.................................................................................................................... 458
Caso di studio.................................................................................................................................................409 Operatori di shift.............................................................................................................................................458
X XI
Indice

Sviluppo polinomiale e formula di Homer..................................................................................................... 459


Formato Floating Point IEEE 754.................................................................................................................. 459 Capitolo _______________________________________________________________________
Casi particolari................................................................................................................................................460 Concetti di programmazione procedurale in Java
Numeri denormalizzati................................................................................................................................... 461
Esercizi...........................................................................................................................................................461 Java è un moderno linguaggio orientato agli oggetti sviluppato dalla SUN Microsystems (oggi Oracle).
Appendice B:...................................................................................................................................................... 463 Supporta lo sviluppo di applicazioni per Internet (es. Applet) pur rimanendo un linguaggio per scopi generali. È
La macchina RASP.............................................................................................................................................463 portabile (praticamente) su tutte le piattaforme (Win, Solaris, Linux, MacOS, ...): "write once run anywhere".
Istruzioni di ingresso/uscita........................................................................................................................... 465 Utilizza un approccio misto compilazione-interpretazione che è alla base della portabilità: il codice sorgente
Istruzioni di spostamento............................................................................................................................... 465 Java è tradotto nel linguaggio, bytecode, di una macchina ipotetica (Java Virtual Machine o JVM)
Operazioni aritmetiche................................................................................................................................... 465 normalmente implementata a software (CPU-interprete di bytecode) ma realizzabile anche in hardware.
Controllo del flusso di esecuzione................................................................................................................. 466
Esempi di algoritmi RASP..............................................................................................................................466 È possibile programmare in Java mediante il "Java Development Kit" (JDK) o meglio lo "Standard
Rappresentazione in memoria di un programma.......................................................................................... 467 Development Kit" (SDK), attualmente nella versione 1.7 (Java 7). SDK è free e si può scaricare dal sito
Programma in linguaggio macchina numerico:............................................................................................469 http://iava.sun.com. Consiste di vari strumenti tra cui: javac (compilatore da Java a bytecode), java (interprete
Nomi simbolici e notazione Assembler.......................................................................................................... 469 del bytecode), javadoc, che genera informazioni HTML di documentazione. In Win SDK è utilizzabile "a riga di
Interpretazione di un programma in linguaggio macchina............................................................................ 470 comando" dall’interno di una finestra DOS (shell di sistema operativo).
Ciclo istruzione della CPU..............................................................................................................................471
Strumenti........................................................................................................................................................472 Java è un linguaggio ibrido: ammette tipi di base la cui gestione non è ad oggetti al fine di garantirne maggiore
Sintassi EBNF di Assembler RASP............................................................................................................... 472 efficienza elaborativa (es. l'aritmetica tra numeri reali). Per il resto il linguaggio è orientato agli oggetti e
Esempi di programmi in Assembler RASP:................................................................................................... 473
dunque estendibile mediante la programmazione di classi dipendenti dalle applicazioni.
Traduzione di un algoritmo Java in Assembler RASP.................................................................................. 474
Blocco:......................................................................................................................................................474
Assegnazione:.......................................................................................................................................... 475 In questo capitolo si riassumono i concetti fondamentali sui tipi primitivi, le istruzioni di controllo e i metodi di
Istruzione if:.............................................................................................................................................. 475 Java e si mostrano esempi di programmi secondo lo stile procedurale. Si danno per acquisite dal corso di
Istruzione if-else:........................................................................................................................................475 Fondamenti di Informatica le nozioni di calcolatore, memoria, algoritmo etc. Un programma sarà organizzato
Istruzione while:.........................................................................................................................................475 mediante una sola classe dotata del metodo main(), eventualmente supportato da ulteriori metodi static
Istruzione do-while:....................................................................................................................................476 appaiati col main e tra di loro (in Java come in C/C++ i metodi non si possono innestare aH'interno di altri
Istruzione for:............................................................................................................................................ 476 metodi), cui sono delegati sotto-compiti specifici. Per ragioni che saranno esposte nel cap. 3, tutti i metodi e le
Array di interi e accesso agli elementi:........................................................................................................ 476 variabili condivise tra i metodi di un programma procedurale devono essere dichiarate static. Un programma
Caso di studio.................................................................................................................................................476 procedurale Java corrisponde agevolmente ad un classico programma C: basta rimuovere i confini della
Istruzione di chiamata a metodo:................................................................................................................479 classe ed eliminare i modificatori static davanti ai metodi ed alle variabili condivise (se ce ne sono) che
Corpo di un metodo:..................................................................................................................................479 vengono cosi a costituire l’ambiente globale dell’applicazione.
Istruzione di ritorno da un metodo:..............................................................................................................479
Traduzione in assembler di un metodo ricorsivo........................................................................................... 480 Convenzioni sui nomi: un nome di classe/interfaccia dovrebbe iniziare con una lettera maiuscola; un nome di
Traduzione in assembler di un metodo tail recursive.................................................................................... 483 metodo o variabile dovrebbe iniziare con una lettera minuscola; in un nome composto ogni iniziale di nome
Esercizi...........................................................................................................................................................485 (dal secondo in poi) dovrebbe essere maiuscola; un nome di costante dovrebbe essere tutto maiuscolo e se
composto, dovrebbe usare il carattere tra i diversi nomi componenti.

Un primo programma:
public class Cambio!
public static void main( String []args ){//contiene l’algoritmo da eseguire
final doublé CAMBIO_EURO. URE=1936.27; //costante reale
System.out.printlnf'Cambio lire in euro”); //scrittura sul video o standard output
//crea un oggetto Scanner, se, per la lettura da tastiera
Scanner sc=new Scanner( System.in ); //System.in è la tastiera o standard input
System.out.print("Lire=");
int lire=sc.nextlnt (); //legge da se il prossimo intero
doublé euro=lire/CAMBICLEURO .LIRE; //converte le lire in euro
System.out.println(lire+“ lire equivalgono a ”+euro+" euro");
}//main
}//Cambio

xii 1
Capitolo^ Concetti di programmazione procedurale

Operazioni di i/o:
In alternativa si può utilizzare un ambiente integrato di sviluppo che consente, tra l’altro, l’editing e le
Scrittura su video di una stringa: operazioni di compilazione e run dei programmi. Un esempio di strumento integrato di sviluppo in Java è
System.out.printlnfCambio lire in euro"); Eclipse, liberamente scaricabile dal sito http://www.Eclipse.org/downloads. Esso è stato sviluppato in gran
System.out.print("Lire=“); //emissione di prompt parte in Java. Mediante plug-in Eclipse può servire come ambiente di sviluppo anche per altri linguaggi, es.
C++. Un altro strumento è NetBeans sviluppato da Oracle/Java. NetBeans è realizzato interamente in Java
Effetto sul video della seconda stampa (print):
Formato dell’output:
Lire=_ Per lo stesso input precedente si ha ora:
Lire= 13500 INVIO
si scrive la stringa senza mandare a capo il cursore. import java.util. Scanner; 13500 lire equivalgono a 6.97 euro
public class Cambio{
Lettura da tastiera di un intero mediante scanner: public static void main( String [jargs ){ ossia la conversione in euro è visualizzata
int lire=sc.nextlnt(); //nextlnt è un metodo di Scanner (si veda più avanti in questo capitolo). final doublé cambioEuroLire= 1936.27; con due sole cifre frazionarie
System.out.println(’’Cambio lire in euro");
Osservazioni: Scanner sc=new Scanner( System.in ); Commenti
• Java è case-sensitive: alfa, Alfa, aLfa etc. sono nomi diversi. System. out.print("Lire="); //commento che risiede su una linea
• Non sono disponibili operazioni primitive di I/O nel linguaggio. Comandi di I/O sono comunque ottenibili int lire=sc.nextlnt (); r
utilizzando classi della libreria di Java come Scanner. doublé euro=lire/cambioEuroLire; commento che può svilupparsi su piu
• Le variabili sono dichiarabili ovunque in un blocco e possono ammettere inizializzazione. System.printf(“%1.2f%n", euro ); linee a piacimento
} 7
Variabili dichiarate ma non inizializzate: }//Cambio
int a, b, c; /**
commento speciale per javadoc
Variabili e inizializzazione: 7
int a, b=2, c;
Tipi di base
L’inizializzazione può essere realizzata successivamente con l'istruzione di assegnazione: Tipi interi-,
a=3; byte (da-128 a 127) 8 bit
short (da-32768 a 32767) 16 bit
Si nota (modello di memoria) che una variabile possiede un nome che riferisce una cella di memoria che int (da -2 ’ 147’483’648 a +2’ 147'483’647) 32 bit
contiene un valore (che può essere indefinito). Il valore può essere cambiato con un’istruzione di long ( omissis ) 64 bit
assegnazione (operatore ’=’). Un programma che faccia uso di variabili indefinite è erroneo.
Operatori: + - * / %
Stringhe e operazione di concatenazione: Gli operatori moltiplicativi (',/,%) hanno maggiore priorità di quelli additivi (+,-). Se necessario, si
possono utilizzare (sotto) espressioni entro parentesi ( ) che vengono valutate sempre prima.
"Una stringa" // una stringa è racchiusa tra una coppia di “
"Una stringa" + " più lunga" Esempi di letterali ed espressioni intere:

Passi di sviluppo a riga di comando: int x=y+5*z;


int x=(y+5)*z;
C:\MiaDioedit Cambio.java (o altro editor, es. notepad di Win) INVIO $
C:\MiaDir>javac Cambio.java INVIO 6 è un valore int
In assenza di errori, si genera il file Cambio.class (bytecode) 6L o 6I è un valore long
C:\MiaDir>java Cambio INVIO
Lancia l’interprete sul bytecode di Cambio.class int y=10%8; //assegna 2 ad y. % calcola il resto della divisione intera
y=i o/8; //assegna 1 ad y. / calcola il quoziente della divisione intera
Esempio di run (esecuzione):

Lire= 13500 INVIO


13500 lire equivalgono a 6.972132129650671 euro
2 3
Capitolo^ Concetti di programmazione procedurale

Tipi reali: Tipi non numerici:


float (reali in singola precisione, circa 7 cifre significative in base 10) 32 bit boolean (valori false e true)
doublé (reali in doppia precisione, circa 15 cifre significative in base 10) 64 bit char (valori nell'alfabeto UNICODE che include, nelle prime 128 posizioni, l’alfabeto ASCII) - 16 bit
per questioni di internazionalizzazione
Operatori:
Stessi operatori degli interi ma ora con aritmetica floating point Esempi:
Funzioni matematiche (trigonometriche, esponenziale,...) della classe Math (si veda più avanti).
boolean trovato=false;
Esempi di letterali reali: char c=’A’;
Costanti caratteri sequenze di escape:
7 e 7.5 sono costanti reali (fixed point) di tipo doublé (default) ’\n’ nuova linea
7.5F o 7.5f sono costanti float Y tabulatore
Y denota il carattere \ etc.
1.2E-2 => 1.2*10-2 costante reale in formato esponenziale
(1.2 è la mantissa, -2 è l’esponente o caratteristica) Algebra di Boole

13.42E-30 =>0.00000000000000000000000000001342 Operatori: AND, OR, NOT


12E20 => 12*1020 =>1200000000000000000000 Valori: false, true
.23e+5
2E-4 Definizione degli operatori mediante tabelle di verità:

5/2 fornisce 2 (aritmetica intera) y


X y x AND y X x OR y X NOT x
5.0/2 fornisce 2.5 (aritmetica reale), si può anche scrivere (double)5/2 etc.
FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
Conversioni di tipo e casting FALSE TRUE FALSE FALSE TRUE TRUE
TRUE FALSE
TR U E FALSE FALSE TRUE FALSE TRUE
Successione dei tipi da più ristretto a più ampio (widening => conversione automatica di tipo):
TR U E TRUE TRUE TRUE TRUE TRUE
byte short int long float doublé
Proprietà dell’algebra di Boole
Esempi:
Dualità: Se una proprietà P è vera, allora è vera anche la duale P’ che si ottiene da P sostituendo ogni AND
byte x=2;
con OR, ogni OR con AND, FALSE con TRUE e TRUE con FALSE.
int y=x; //conversione automatica per widening
x=y; //NO! Incompatibilità di tipo 1) xAND FALSE = FALSE Proprietà associativa
x=(byte)y; //narrowing - richiede casting, può comportare perdita di informazione T) x OR TRUE = TRUE 6) x AND y AND z=(x AND y) AND z=x AND (y AND z)
6’) x OR y OR z=(x OR y) OR z=x OR (y OR z )
Incremento/decremento e assegnamento con aritmetica 2) x ANDTRUE=x
2') x OR FALSE=x Proprietà di distributiva
Su variabili intere o reali: 7) x AND (y OR z )=x AND y OR x AND z
Idempotenza 7’) x OR (y AND z)=(x OR y) AND (x OR z)
v=v+1; « v++: <=> v+=1; 3) x AND x = x
x=x-1; <=> x--; <=> x-=1; 3’) x OR x = x Involuzione
8) NOT( NOT x )=x
In generale: v op= expr; 4) x AND NOT x=FALSE Non esiste duale della 8.
4’) x OR NOT x = TRUE
dove op è un operatore aritmetico (+,-,*,/,%), equivale a: v = v op (expr); De Morgan
Proprietà commutativa 9) NOT( x AND y )= NOT x OR NOT y
5) x AND y = y AND x 9’) NOT( x OR y )= NOT x AND NOT y
5') x OR y = y OR x

4 5
Capitolo 1 Concetti di programmazione procedurale

Semplificazione di espressioni booleane: boolean positivo=n>0;

Si può dimostrare che vale la seguente proprietà: Il confronto n>0 genera true se n ha un valore maggiore di zero, false altrimenti. Il boolean generato è
10) x OR x AND y = x assegnato a positivo.

Infatti: boolean valido=n>=1 && n<=10;


x OR x AND y= (x AND TRUE) OR (x AND y)=x AND (TRUE OR y)=x AND TRUE=x
In questo caso alla variabile valido si assegna true se ne [1..10], false altrimenti.
Sussiste anche la proprietà duale:
10') x AND (x OR y)=x La classe Math

x AND y OR x AND NOT y = x Gran parte delle funzioni di Math accettano argomenti doublé e restituiscono un risultato di tipo doublé.

Infatti: Funzioni trigonometriche: sin(x), cos(x), tan(x), asin(v), acos(v), atan(v). Esempio:
x AND y OR x AND NOT y=
x AND (y OR NOT y) = x AND TRUE = x doublé x=Math.sin(2.32);

Operatori booleani e relazionali in Java:___________________________________ Esponenziale: exp(x) - calcola ex


Logaritmo: log(x) - calcola il logaritmo naturale di x
And. && (congiunzione logica) Operatori relazionali Potenza: pow(x,y) - calcola xv
Or. Il (disgiunzione logica)
Not ! (negazione logica) > maggiore Arrotondamento:
< minore ceil(x) - ritorna il min int >= x, es. Math.ceil(2.2)=3
Es. a>0 && a<b+2 >= maggiore o uguale floor(x) - ritorna il max int <=x, es. Math.floor(2.2)=2
a==b II a==c <= minore o uguale round(x) - equivale a (int)floor(x+0.5), es. Math.round(2.2)=2, Math.round(2.5)=3, etc
== uguaglianza
-Q
CIIO
II

!= diversità Valore assoluto: abs(x), x intero o reale


Massimo e minimo: max(x.y), min(x,y), x,y interi o reali
Gli operatori booleani sono meno prioritari di quelli relazionali che sono meno prioritari di quelli aritmetici. Numeri casuali: random()

Operatori booleani e corto circuito Ad ogni chiamata, random() ritorna un numero reale in [0..1[. I numeri sono uniformemente distribuiti
nell'Intervallo [0..[1.
Gli operatori && e II si accompagnano alla valutazione incompleta (corto circuito) di un'espressione booleana.
Classe Scanner (Java 5 o versione superiore)
Esempio: a>0 && b/a<c
import java.util.*; //package che esporta la classe Scanner
se a==0, è falso il primo pezzo (quindi tutta la and è falsa) il secondo pezzo b/a<c non viene valutato, cosi si
evita un’eccezione aritmetica per divisione per zero. Scanner s=new Scanner( System.in ); //per leggere da tastiera

Esempio: a<0 II Math.sqrt(a)>c Metodi disponibili:


s.nextLine() -- legge e ritorna un’intera linea (String) chiusa da INVIO
Se a<0, è true il primo pezzo (dunque è true tutta la or) il secondo pezzo non viene valutato e si evita ancora s.nextQ -- legge e ritorna, se esiste, la prossima parola (terminata da spazio o fine linea)
una volta un'eccezione, qui dovuta alla radice quadrata di un negativo. s.nextlnt() -- legge e ritorna, se esiste, il prossimo int
s.nextDouble() -- usa la tua fantasia
Nelle situazioni in cui si desidera espressamente la valutazione completa (ossia non corto circuitata) di una s.hasNext() -- ritorna true se esiste una prossima parola
espressione booleana, è sufficiente utilizzare & e I al posto rispettivamente di && e II. s.hasNextlnt(), s.hasNextDouble() -- usa la tua fantasia

I valori boolean sono normalmente generati a seguito di confronti. Ad esempio:

System.out.print(',n=‘);
int n=sc.nextlnt();
6 7
Capitolo 1 Concetti di programmazione procedurale

Il metodo printf di System.out (Java 5 o versione superiore) Selezione a due vie (if-else)

Scrittura del valore di una variabile reale x: Sintassr. Semantica:

System.out.printf( "x=%6.3f", x);


if( condizione )
scrive x in un campo minimo di 6 colonne, di cui 3 sono riservate alle cifre frazionarie. Tuttavia, se la parte istruzione; //parte then
intera di x richiede più di 2 cifre, essa otterrà comunque più colonne come necessario. [else
istruzione;] //la parte else può mancare
System.out.printf( "x=%e", x);
condizione è un'espressione booleana, es. un
x è scritto in formato esponenziale o scientifico. confronto tra espressioni aritmetiche.

System.out.printf( “i=%5d y=%6.2f\n", i, y );

Scrive l’intero i in un campo di 5 colonne (anteponendo spazi come necessario), quindi il valore del reale y e
infine va a capo.

printf accetta in generale più argomenti (numeri, stringhe, char...). Un esempio di programma: _________ __________________________________
import java.util.*;
Esiste anche il formato %s (es. %10s) per le stringhe di caratteri. Inoltre è possibile specificare il segno meno public class Guess{
(-) per richiedere l’allineamento a sinistra: %-8d etc. public static void main( String []args )(
Scanner s=new Scanner( System.in );
Espressioni ejissegnazione^ ___ ___ final int MAX=10;
int indovina = (int)(Math.random()*MAX)+1;
La valutazione di un’espressione, nel rispetto della priorità degli operatori presenti, genera un risultato System.out.printlnfSto pensando ad un numero tra 1 e ”+MAX);
tipicamente assegnato come valore ad una variabile: System.out.printf’quale ?“);
int risposta=s.nextlnt();
int x=a+b*c-2; if( risposta==indovina )
System.out.printlnfBRAVO!");
Anche l’assegnazione è una espressione il cui valore-risultato è il valore del suo lato destro. Ad es. else
System.out.printlnf'NO. Il numero era =”+indovina);
intx, y; }//main
y=x=a+b‘ c-2; }//Guess

si valuta l'espressione a+b*c-2 e sia v il risultato. Si assegna v ad x; il valore di x=v, cioè v, è quindi assegnato Compilazione/esecuzione del programma:_______________________________________________________
a y (catena di assegnazioni).
È sufficiente, al solito, editare il programma in un file testo Guess.java e collocare quest’ultimo ad es. in una
Nell'espressione a+b*c-2, si realizza prima il prodotto b*c; le operazioni equiprioritarie si valutano tipicamente directory primi^programmi innestata in c:\ (in Windows). Quindi si apre una finestra dos e si usano i comandi:
da sinistra a destra, per cui l’espressione precedente equivale a: (a+(b*c))-2.
c:\>cd primi programmi INVIO
Strutture di controllo c:\primi programmi>javac Guess.java INVIO
Blocco (o sequenza di istruzioni). Sintassi: c:\primi program m ava Guess INVIO

{ istruzione; istruzione;...} Perché vengano riconosciuti i comandi esterni javac/java è necessario che sia stata predisposta
appropriatamente la variabile di ambiente path. In Win, si apre il pannello di controllo, si va su sistema, quindi
Osservazione importante: su impostazioni di sistema avanzate, e quindi su variabili di ambiente. La variabile path può essere fissata per
il singolo utente o al livello di sistema a seconda dei bisogni. Di seguito si mostra la modifica della variabile
Un'istruzione Java può essere semplice (es. una assegnazione, un'operazione di i/o etc.) o un blocco. Una path per il singolo utente.
particolare istruzione semplice è quella nulla, terminata direttamente da

8 9
Capitolo 1 Concetti di programmazione procedurale

Settaggio di path (esempio): Se il corpo di un while o la parte then o else di un /Zete, ammettono più istruzioni, allora costituiscono un
blocco ed occorre avvilupparle tra una coppia di parentesi { e }. Esempi;
Modifica variabile utente
if( a>0 ){ int i= 1 , s=0; int i=1, s=0;
a -; while( i<=10 )( do{
Nome variabile: PATH b=c-d; s=s+i; s=s+i;
} i++; i++;
Valore variabile: C:'program Files\3ava\jdkl.7.0_5lV>m; } }while( i<=10 );

if( a>0 ){
OK Annulla
a--;
System.out.println( a );

Il valore di path è una lista di directory separate da e senza spazi. Tra le altre deve essere presente la sotto
directory bin dell’installazione locale del JDK, es: If-implicito (operatore ?) Selezione n-aria (switch)
c:\Programmi\Java\jdk1.6.0J 7\bin; Analisi dei casi possibili
if( a>b )
=>
m in=b;
Ciclo di while (o a condizione iniziale) ____ _____ if( a==1 ) z=0; switch( a ){
else
else if( a==2 ) x=c+d; case 1 : z=0; break;
m in=a;
Sintassr. Semantica: else if( a==3 ) z=b-f; case 2: x=c+d; break;
else if( a==4 ) d++; case 3: z=b-f; break;
o più succintamente: case 4: d++;
while( condizione )
istruzione; }
int m in =( a> b ) ? b : a;
Tutto ciò se è noto che a possa assumere solo i valori
istruzione (corpo del while) può essere una Ancora: da 1 a 4.
istruzione semplice o un blocco
int toggle = ( bit==0 ) ? 1 : 0; switch( a ){
int y = (y==2) ? y+1 : y+2; case 1 : case 2: x=c+d; break;
case 3: z=b-f; break;
default: d++;
Le parentesi ( e ) intorno alla condizione non
}
sono obbligatorie.
Un’istruzione while (a condizione iniziale) denota un ciclo che può essere ripetuto 0, 1 o più volte. Tutto
Qui i casi 1 e 2 hanno le stesse azioni: sono state fuse
dipende dal valore della condizione. Non si entra nel ciclo se la condizione è falsa già all’inizio.
le due alternative. L’alternativa default cattura i valori di a
diversi da 1,2 o 3.
Ciclo di while a condizione finale

Il discriminante di uno switch può essere un’espressione integer, boolean o char. A a partire dalla versione 7
del linguaggio, il discriminante può essere anche un oggetto stringa. Il blocco istruzioni di un’alternativa case è
terminato di norma da un’istruzione break. Se manca break, l’esecuzione prosegue con la prossima
alternativa, se esiste, e cosi via.

Istruzione for

for( inizializzazione: condizione_di^continuazione: passo)


istruzione; //semplice o blocco

Un’istruzione do-while rappresenta un ciclo ripetuto 1 o più volte. Il for definisce un campo di validità in cui è visibile la variabile di controllo; passo è una istruzione Java.

for( int j=2; j<=10; j=j+2 ) System.out.println(,,j=*'+j); //corpo del for costituito da un’istruzione semplice
10 11
Capitolo 1 Concetti di programmazione procedurale

Il calcolo della potenza potrebbe basarsi direttamente sui servizi di Math:


for( int j=0; j<10; j++ ){... omissis ...} //il corpo è un blocco
System.out.println("j=“+j); //ERRORE: la j del for non è visibile qui. Occorrerebbe dichiararla fuori dal for int potenza=(int)Math.pow(a,n);

Un programma per il calcolo della potenza an Un programma per ^equazione di secondo grado
import java.util.*; import java.util.*;
public class Potenza{ public class EQ2{
public static void main( String []args ){ public static void main( String [jargs ){
System.out.printlnfCalcolo della potenza aAn, a int, n int>=0"); System.out.printlnfEquazione di secondo grado");
Scanner sc=new Scanner( System.in ); Scanner sc=new Scanner( System.in );
System.out.print("a=“); int a=sc.nextlnt(); System.out.print("a=“); doublé a=sc.nextDouble();
System.out.print("n=M); int n=sc.nextlnt(); System.out.print("b="); doublé b=sc.nextDouble();
int potenza=1, contatore=n; System.out.print("c=“); doublé c=sc.nextDouble();
while( contatore>0 ){ doublé d=b*b-4‘ a*c; //calcola discriminante
potenza *=a: contatore--; if( d>=0 ){
} if( d==0){
System.out.println(a+"A”+n+"=“+potenza); System.out.println("Radici reali coincidenti");
}//main doublé x=-b/(2*a);
}//Potenza System.out.printf("x1 =x2=%1.2f\n",x);
}
else{//d>0
Si possono controllare eventuali errori nei dati come segue: doublé x1=(-b+Math.sqrt(d))/(2‘a); doublé x2=(-b-Math.sqrt(d))/(2‘ a);
System.out.printf("x1 =%1.2f\n",x1 ); System.out.printf(,,x2=%1,2f\n",x2);
import java.util.*;
public class Potenza{
public static void main( String []args ){ else//d<0
Scanner sc=new Scanner( System.in ); System.out.println("Non esistono radici reali");
System.out.print("a=“); int a=sc.nextlnt(); }//main
System.out.print("n="); int n=sc.nextlnt(); }//EQ2
if( a==0 && n==0 ){
System.out.printlnf'Forma indeterminata O'X)"); Massimo comun divisore ed algoritmo di Euclide
System.exit(-I); //terminazione del programma
n ed m sono due interi positivi: Altra formulazione:
} int n=..., m=...;
if( n<0 ){ int r=1 ; i r per partire" int n=..., m=...;
System.out.println("L'esponente n deve essere non negativo"); while( r!=0 ){ int r;
System.exit(-I); r=n%m; do{
} if( r!=0 ){ r=n%m;
int potenza=1, contatore=n;
n=m; m=r;//trasla if( r!=0 ){
while( contatore>0 ){
} n=m; m=r;
potenza *=a; contatore-;
} }
} System.out.println(... M C D è m ... ); }while(r!=0)
System.out.println(a+"A"+n+"="+potenza); System.out.println(... MCD è m ... );
}//main
Formulazione più compatta:
}//Potenza
int n=..., m=...;
int r;
Il ciclo di while può essere sostituito agevolmente con un ciclo di for a decrescere: do{
r=n%m;
int potenza=1; n=m; m=r;
for( int contatore=n; contatore>0; contatore- ) potenza '= a; }while( r!=0 )
System.out.println(... MCD è n ... );
12
13
Capitolo 1 Concetti di programmazione procedurale

Somma dei primi N numeri naturali L’istruzione while realizza un ciclo a conteggio che può essere ottenuto in alternativa con l’istruzione for:
Es. N=100
int fattoriale^;
int s=0; //sommatoria for( int i=2; i<=n; i++) fattoriale=fattoriale*i; //ciclo a crescere
for( int i=1; i<=100; ++i ){ System.out.println(n+"!="+fattoriale);
s=s+i;//o s+=i;
Oppure:
System.out.println("somma="+s); int fattoriale=1;
for( int i=n; i>=2; i-) fattoriale=fattoriale*i; //ciclo a decrescere
Equivalenza di Gauss System.out.println(n+“!="+fattoriale);
Per sommare i naturali da 1 a i 00 si può procedere anche come segue:
Calcolo del mcm __________________________________ .________________________________
1+ 100=101 import java.util.Scanner;
2+99 =101 public class MCM{
3+98 =101 public static void main( String [jargs ){
System.out.printlnfCalcolo del mcm tra due interi n ed m, n>0 ed m>0‘ );
50+51=101 Scanner s=new Scanner} System.in ); System.out.printfn^);
int n=s.nextlnt(); System.out.print(>m=');
e dunque: 1+2+3+...+100=50* 101 =( 100/2)*( 100+1 ) int m=s.nextlnt();
if( n<=0 II m<=0 ){
Più in generale, si può dimostrare per induzione l’equivalenza di Gauss: System.out.println}“Numeri non positivi");
N * { N + I) System.exit(-I);
1 + 2 + 3 + •••+ N = £ i =
}
int x=n, y=m; //variabili di comodo
Infatti:
while} n!=m ){
a) Passo base. L’equivalenza sussiste banalmente per N=1
if( n<m ) n=n+x;
b) Ipotesi induttiva. Supposta vera per N-1, si tratta di dimostrare che essa vale anche per N.
else m=m+y;
Ma:
}
„ (N-\)*N .. N * ( N + \) . £ -!. (N-\)*N
2^ i = 2^ ' + N = -— -— +N =— ---------- essendo 2 , • = — — per I ipotesi induttiva. System.out.println("mcm("+x+","+y+")="+n);
i* i i=i 2 2 M 2 }//main
J//MCM
Calcolo del fattoriale
Il fattoriale di un intero N>0 è definito come segue: N!=1 se N<=1; N!=N*(N-1)*(N-2)‘ ...*3*2*1 se N>1. Calcolo del fattoriale affidato ad un metodo
import java.util.Scanner;
import java.util.Scanner; public class Fattoriale}
public class Fattoriale} public static void rnain} String []args){
public static void main( String []args){ Scanner s=new Scanner} System.in );
System.out.printlnfCalcolo del fattoriale di un intero n non negativo”); System.out.print("n="); int n=s.nextlnt();
Scanner s=new Scanner} System.in ); System.out.print("n(>=0):”); int fatt=fattoriale(n); //chiamata del metodo
int n=s.nextlnt(); System.out.println(n+"!=M+fatt);
int fattoriale=1, i=2; }//main
while( i<=n ){ static int fattoriale} int n ){//dichiarazione del metodo
fattoriale=fattoriale*i; int fatisi;
i++; for( int i=n; i>=2; i- ) fatt *=i;
} return fatt;
System.out.println(n+"!=“+fattoriale); }//fattoriale
}//main }//Fattoriale
}//Fattoriale

14 15
Capitolo 1 Concetti di programmazione procedurale

Un metodo (o funzione) è un sottoprogramma che ha un nome, può ricevere 0, uno o più parametri (di dove il logaritmo è quello naturale (o in base e).
qualunque tipo) e restituisce 0 (void) o un risultato (di qualunque tipo). I parametri sono a tutti gli effetti variabili
locali che si aggiungono alle variabili proprie (es. int fatt;) eventualmente introdotte dal metodo. Esempi: Un terzo metodo potenza
static doublé potenza( doublé a, doublé n ){
static int metodo1( int x, float z ) if( a<=0 ){
( corpo1} System.out.printlnfpotenza: "+a+’’ atteso>0");
System.exit(-I);
static void metodo2( String x) }
( corpo2} return Math.exp( n'Math.log(a) );
}//potenza
static void metodo3() //parametri assenti
( corpo3} Un metodo che verifica se un intero positivo è primo
static boolean ePrimo( int n ){//n è assunto >1
Un metodo produce i suoi effetti computazionali quando viene invocato, es: //ritorna true se n è primo, false altrimenti
if( n==2 ) return true;
int a, b; float c ; ... if( n%2==0 ) return false;
int k=metodo1( a+b-1, c ); (*) int tentativo=3, limite=Math.round( Math.sqrt(n) );
while( tentativo<=limite && n%tentativo!=0 ) tentativo+=2;
metodo2(""); return tentativo>limite;
}//ePrimo
L'invocazione di un metodo costituisce un'istruzione e un'espressione dava.
Metodo di Newton per il calcolo della radice quadrata di un numero reale ______
Nella chiamata di metodol in (*) il valore dell’espressione a+b-1 (argomento) è copiato sul parametro formate static doublé sqrt( doublé x, doublé eps ){
x; similmente il valore di c è copiato sul parametro formale z. Per approfondimenti si veda il cap. 3. doublé E=1.0; //per partire
doublé vecchiaE;
Un metodo potenza do{
static int potenza( int a, int n ){ vecchiaE=E;
//ipotesi: i dati a ed n sono corretti E=(E+x/E)/2;
int pot=1 ; //esempio di variabile locale }while( Math.abs( vecchiaE-E )>=eps );
for( int i=0; i<n; i++ ) pot ‘ =a; return vecchiaE;
return pot; }//sqrt
}//potenza
Se E è una approssimazione della radice quadrata di x, una migliore approssimazione è data da: (E+x/E)/2.
Esempio di chiamata: int x=potenza(2,20); //calcola 2 elevato a 20. Segue un programma che legge da input un reale x e scrive la radice quadrata di x approssimata alla sesta
cifra frazionaria, calcolata con sqrt e con Math.sqrt.
Un secondo metodo potenza
static doublé potenza( int a, int n j[ import java.util.*;
if( a==0 && n==0 ){ public class TestSqrtj
System.out.printlnfpotenza: O'X) !'); static doublé sqrt( doublé x ){ /‘omissis*/}
System.out.exit(-I); public static void main( String []args ){
Scanner sc=new Scanner(System.in);
} System.out.print("double>’ );
int pot=1;
for( int i=0; i<Math.abs(n); i++ ) pot*=a; doublé x=sc.nextDouble();
if( n<0 ) return 1f/pot; if( x<0.0 ){ System.out.printlnfNumero negativo!”);System.exit(-1);}
return pot; //casting implicito doublé y=sqrt( x, 1.OE-6 ); //sei cifre di tolleranza
}//potenza System.out.printlnfRadice quadrata di “+x+" con 6 cifre frazionarie");
System.out.printffsqrt=%1,6f\n", y);
System.out.printff Math.sqrt=%1.6f\n”, Math.sqrt(x));
Se la base e/o l’esponente sono numeri reali, si può effettuare il calcolo della potenza ricorrendo
}//main
all’equivalenza:
}//TestSqrt
a" = e xp (lo g< /" ) => a" = cxp(/» * lo g */)
16 17
Capitolo 1 Concetti di programmazione procedurale

Sviluppo di un programma for( int h=k; h>=2; h - )


permutazioni *=h;
Problema: Calcolo della probabilità che esca un'assegnata cinquina al gioco del lotto. long combinazioni=disposizioni/permutazioni;
System.out.println( “Probabilità di una cinquinaV = 1f+combinazioni+" = "+(1f/combinazioni) );
Soluzione: Si determina il numero delle possibili cinquine, diciamolo c, quindi si valuta la probabilità come 1/c }//main
(casi equi-probabili). }//Lotto_____________________________________________________ ____________________

Si può valutare il numero delle disposizioni di 90 numeri a 5 a 5 (disposizioni semplici I)'*' ) e quindi
eliminare i casi ridondanti. Ad es. la cinquina 2-15-20-1-88 è la stessa di 20-1-15-88-2 etc. In sostanza, Per migliorare la struttura del programma Lotto, è conveniente organizzarlo in metodi. Si può introdurre un
cinquine che differiscono solo per l'ordine sono identiche. Il numero dei raggruppamenti di 5 oggetti che si metodo per ogni azione astratta significativa: calcolo delle disposizioni e calcolo delle permutazioni. Si ha
ottengono cambiandone solo le posizioni, sono le cosiddette permutazioni e sono 5! Dunque il numero delle quindi:
cinquine distinte è dato da numero cinquine c= I)*'I5 \ Tecnicamente il numero delle cinquine costituisce il
public class Lotto{
cosiddetto numero delle combinazioni semplici di 90 oggetti a cinque a cinque, es. indicato come c * ' . public static void main( String []args ){
Segue una prima versione “astratta” del programma. int n=90, k=5;
long disposizioni=disposizioniSemplici(n,k);
public class Lotto{ int permutazioni=fattoriale(k);
public static void main( String []args ){ long combinazioni=disposizioni/permutazioni;
int n=90, k=5; System.out.println
trova disposizioni di n oggetti a k a k ( “Probabilità di una cinquinaV = 1/“+
trova permutazioni di k oggetti combinazioni+" = “+(1f/combinazioni) );
long combinazioni=disposizioni/permutazioni; }//main
System.out.println( "Probabilità di una cinquina V = 1/"+combinazioni+“ = "+(1f/combinazioni) );
}//main static long disposizioniSemplici( int n, int k){
}//Lotto long disp=1;
for( int h=n; h>=n-k+1; h - )
Il programma Lotto contiene due azioni astratte che vanno raffinate. disp *=h;
return disp;
trova disposizioni di n oggetti a k a k }//disposizioniSemplici
che può essere concretizzata come segue:
static int fattoriale( int n ){
long disposizioni=1; int fatt=1;
for( int h=n; h>=n-k+1; h-- ) for( int h=n; h>=2; h - )
disposizioni *=h; fatt *=h;
return fatt;
Infatti, il numero totale delle cinquine è dato da: / ^ ’"=90‘89‘88‘87‘86, che si può facilmente giustificare {//fattoriale
considerando le possibilità che rimangono ad ogni estrazione di un numero da una urna. Al primo turno {//Lotto
esistono 90 possibilità. Al secondo 89 etc. Il calcolo del fattoriale è già stato mostrato. Si può dunque scrivere
l’intero programma: Benefici dei metodi sono:
• Il programma (main) è più compatto.
Programma Lotto: • I dettagli delle operazioni sono confinati nei metodi.
public class Lottof • Si può cambiare l'implementazione dei metodi (es. per ragioni di efficienza) senza alterare l’algoritmo che li
public static void main( String []args ){ utilizza. Ad es., la sostituzione del ciclo di for con un while nel metodo fattoriale è irrilevante ai fini del
int n=90, k=5; programma complessivo.
Il trova disposizioni di n oggetti a k a k
long disposizioni=1; Si vuole calcolare la durata di un mese m, intero tra 1 e 12, di un annoe [1901..2099], Ovviamente, la durata
for( int h=n; h>=n-k+1; h - ) del mese è già definita a meno di Febbraio nel qual caso essa dipende dal fatto se l’anno è bisestile o no.
disposizioni *=h; Nell’arco di anni considerato, il carattere bisestile è verificabile controllando se l'anno è divisibile per 4 o no.
lltrova permutazioni di k oggetti
int permutazioni=1;

18 19
Capitolo^ Concetti di programmazione procedurale

//determina durata del mese m


if( m==1 ) durata=31; Sviluppo azioni astratte:
else if( m==2 ){
if( anno%4==0 ) durata=29; Scanner sc=new Scanner( System.in );
else durata=28; System.out.print(‘ Fornisci un anno tra 1901 e 2099: ");
} int anno;
else if( m==3 ) durata=31; Il leggi l’anno
else if( m==4 ) durata=30; do{
else if( mese==5 ) durata=31; anno=sc.nextlnt();
else if( mese==6 ) durata=30; if( anno<1901 II anno>2099 )
else if( mese==7 ) durata=31 ; System.out.printlnfAnno errato. Ridarlo”);
else if( mese==8 ) durata=31; }while( anno<1901 II anno>2099 );
else if( mese==9 ) durata=30;
else if( mese==10 ) durata=31; Per convenzione si numerano i giorni della settimana da 0 (Lunedi) a 6 (Domenica).
else if( mese==11 ) durata=30;
else durata=31; //determina il giorno della settimana del primo dell'anno
int totaleAnni=anno-1901;
Anziché la cascata di if innestati si può usare un’istruzione switch come segue int totaleAnniBisestili=totaleAnni/4;
int totaleGiorni=totaleAnni*365+totaleAnniBisestili;
switch( m ){ int primoGiorno=totaleGiorni%7;
case 1: case 3: case 5: case 7: case 8:
case 10: case 12: durata=31; break; Si nota che se primoGiomo è 0 allora il primo dell’anno è Martedì. Occorre dunque “aggiustare” il risultato in
case 2: durata=(anno%4==0) ? 29: 28; break; modo da rimanere fedeli alla convenzione di sopra:
default: durata=30;
primoGiorno=(primoGiorno+1)%7;
for( tutti i mesi dell'anno ){
Caso di studio: sviluppo di un programma Calendario per passi successivi
)
Problema: Sapendo che il 1 gennaio del 1901 era Marledi. leggere un anno tra 1901 e 2099 e determinare e
visualizzare il suo calendario. Numerando i mese da 1 a 12 si ha:

Per semplicità si accetta un output del tipo: for( int mese=1; mese<=12; mese++ ){
Gennaio 2001 //durata mese
switch( mese )(
Lun Mar Mer Gio Ven Sab Dom case 1: case 3: case 5: case 7: case 8:
1 2 3 ... case 10: case 12: durataMese=31; break;
case 2: durataMese=(anno%4==0) ? 29 : 28; break;
Versione di massima del programma:___________________________ default: durataMese=30;
leggi l'anno }
determina il giorno della settimana del primo dell'anno
for( tutti i mesi dell'anno ){
determina la durata del mese corrente /Iscrivi mese ed anno
scrivi mese ed anno switch( mese ){
scrivi l'intestazione settimanale case 1: System.out.printlnfGennaio ”+anno); break;
genera offset primo giorno del mese case 2: System.out.printlnfFebbraio ”+anno); break;
for( tutti i giorni del mese ){
scrivi il numero del giorno del mese case 12: System.out.printlnfDicembre ”+anno);
avanza giorno della settimana }
) System.out.println(); //lascia una riga bianca
separa mesi consecutivi

20 21
Capitolo 1 Concetti di programmazione procedurale

llscrivi l'intestazione settimanale //scrivi mese ed anno


switch( mese ){
System, out.println case 1: System.out.println("Gennaio "-ranno); break;
(" Lun Mar Mer Gio Ven Sab Dom“); case 2: System.out.printlnfFebbraio "+anno);break;
case 3: System.out.printlnfMarzo "+anno);break;
a genera offset primo giorno del mese case 4: System.out.printlnfAprile "+anno);break;
for( int g=0; g<primoGiorno; g++ ) case 5: System.out.println("Maggio "+anno);break;
System.out.printf "); //stringa di 4 spazi case 6: System.out.printlnfGiugno "+anno);break;
case 7: System.out.println("Luglio "+anno);break;
case 8: System.out.println(“Agosto “+anno);break;
for( tutti i giorni del mese ){...} case 9: System.out.println("Settembre "+anno);break;
case 10: System.out.println(‘Ottobre “+anno);break;
si specializza: case 11 : System.out.printlnfNovembre "+anno);break;
case 12: System.out.println("Dicembre "-ranno);
for( int g=1 ; g<=durataMese; g++ ){
//scrivi il numero del giorno del mese
System.out.printf(“%4d", g); System.out.println(); //lascia una riga bianca
//avanza giorno della settimana //scrivi l'intestazione settimanale
primoGiorno=(primoGiorno+1)%7; System.out.printlnf Lun Mar Mer Gio Ven Sab Dom");
if( primoGiorno==0 ) System.out.println();
} //genera offset primo giorno del mese
System.out.println(); System.out.println(); for(int g=0; g<primoGiorno; g++ )
System.out.print(" “);
Programma completo: ___________
import java.util.*; for( int g=1 ; g<=durataMese; g++ ) { //per tutti i giorni del mese
public class Calendariof //scrivi il numero del giorno del mese
public static void main( String []args ){ System.out.printf("%4d", g);
System.out.printlnfProgramma Calendario"); //avanza giorno della settimana
Scanner sc=new Scanner( System.in ); primoGiorno=(primoGiorno+1 )%7;
System.out.print(“Fomisci un anno tra 1901 e 2099 :*); if( primoGiorno==0 )
int anno; System.out.println();
//leggi l’anno }
do{ System.out.println(); System.out.println(); //separa mesi consecutivi
anno=sc.nextlnt(); }//per tutti i mesi dell’anno
if( anno<1901 II anno>2099 )
System.out.println("Anno errato. Ridare l'anno"); }//main
}while( anno<1901 II anno>2099 ); }//Calendario
//determina il giorno della settimana del primo dell'anno
int totaleAnni=anno-1901 ; Perfezionamenti:____________________________________________________________________________
int totaleAnniBisestili=totaleAnni/4; Per rendere più chiaro il programma calendario, può essere utile introdurre nella classe Calendario le costanti
int totaleGiorni=totaleAnni‘ 365+totaleAnniBisestili; dei giorni:
int primoGiorno=totaleGiorni%7; I/O indica Martedì
primoGiorno=(primoGiorno+1)%7; //riporto nel sistema 0(Lun)..6(Dom) final int LUNEDILO, MARTEDI=1, MERCOLEDI=2, GIOVEDI=3, VENERDI=4, SABATO=5, DOMENICA=6;
for( int mese=1 ; mese<=12; mese++ ){//per tutti i mesi dell'anno
//determina la durata del mese corrente in questo modo diventano più espressivi alcuni test come: if( primoGiorno==LUNEDI ) ... Un ulteriore
int durataMese; miglioramento si ottiene con l’introduzione di metodi corrispondenti alle azioni-astratte.
switch( mese ){
case 1: case 3: case 5: case 7: case 8: case 10: case 12: durataMese=31; break;
case 2: durataMese=(anno%4==0)7 29 : 28; break;
default: durataMese=30;

22 23
Capitolo 1 Concetti di programmazione procedurale

Strutturazione in metodi:___________________________________________ return durata;


import java.util.*; }//calcolaDurataEScriviMese
public class Calendario!
public static void main( String []args ){ static int scriviCalendarioDelMese( int durataM, int primoG ){
System.out.println(“Programma Calendario"); //scrivi l'intestazione settimanale
int anno=leggiAnno(); System.out.printlnf Lun Mar Mer Gio Ven Sab Dom“);
int primoGiorno=determinaPrimoGiorno( anno ); //genera offset primo giorno del mese
for( int mese=1; mese<=12; mese++ ){ for(int g=0; g<primoG; g++ )
int durataMese=calcolaDurataEScriviMese( mese, anno ); System.out.print(" “);
primoGiorno=scriviCalendarioDelMese( durataMese, primoGiorno ); for( int g=1 ; g<=durataM; g++ ){//per tutti i giorni del mese
separaMesiConsecutivi(); //scrivi il numero del giorno del mese
} System.out.printf( ’%4d“, g);
}//main //avanza giorno della settimana
static int leggiAnno(){...} primoG=(primoG+1)%7;
static int determinaPrimoGiorno( int anno ) {...} if( primoG==0 )
static int calcolaDurataEScriviMese( int mese, int anno ){...} System.out.println();
static int scriviCalendarioDelMese( int durataM, int primoG ){...} }
static void separaMesiConsecutivi(){...} return primoG;
}//Calendario }//scriviCalendarioDelMese

static int leggiAnno(){ static int determinaPrimoGiorno( int anno ) { static void separaMesiConsecutivi(){
Scanner sc=new Scanner( System.in ); int totaleAnni=anno-1901; System.out.println(); System.out.println(); Ilo un meccanismo di salto pagina etc
System.out.println int totaleAnniBisestili=totaleAnni/4; }//separaMesiConsecutivi
("Fornisci un anno tra 1901 e 2099 int totaleGiorni=
int anno; totaleAnni‘ 365+totaleAnniBisestili; Un altro caso di studio: Sottosequenza di dimensione massima
do{ int primoGiorno=totaleGiorni%7; Leggere n quindi n bit (interi Oo 1), ogni bit separato dal prossimo mediante spazi, e determinare e scrivere la
anno=sc.nextlnt(); I/O indica martedì lunghezza dell'ultima sottosequenza di dimensione massima costituita da cifre uguali.
if( anno<1901 II anno>2099 ) //aggiusto nel sistema 0(Lun)..6(Dom)
System.out.printlnfAnno errato. Ridare l'anno"); primoGiorno=(primoGiorno+1)%7; Es.
}while( anno<1901 II anno>2099 ); return primoGiorno: N=10
return anno; }//determinaPrimoGiorno 1001110001
}//leggiAnno Output: lung max=3 di bit 0

static int calcolaDurataEScriviMese( int mese, int anno )( Un primo algoritmo:________________________ _________________________________________________
int durata;
switch( mese ){ inizializza
case 1: System.out.printlnf'Gennaio "+anno); durata=31 ; break; for( tutte le n cifre ){
case 2: System.out.printlnf'Febbraio Vanno); leggi cifra corrente
durata=(anno%4==0) ? 29 : 28; break; if( cifra corrente == cifra precedente )
case 3: System.out.printlnfMarzo Vanno); durata=31 ; break; aumenta lung sottosequenza corr
case 4: System.out.println("Aprile Vanno); durata=30; break; else//fine sottosequenza corrente
case 5: System.out.printlnfMaggio ”+anno); durata=31 ; break; if( lung sottosequenza corr >= lung max ){
case 6: System.out.printlnf'Giugno "+anno); durata=30; break; aggiorna lung max sottosequenza
case 7: System.out.printlnfLuglio "+anno); durata=31 ; break; ricorda il tipo di cifra della sottoseq. max
case 8: System.out.printlnfAgosto ‘Vanno); durata=31 ; break; }
case 9: System.out.println(“Settembre 'Vanno); durata=30; break; cifra precedente=cifra corrente
case 10: System.out.printlnfOttobre Vanno); durata=31; break; }//for
case 11; System.out.printlnfNovembre Vanno); durata=30; break; scrivi risposta
default: System.out.printlnfDicembre "+anno): durata=31 ;
}
System.out.println(); //lascia una riga bianca
25
Capitolo^ Concetti di programmazione procedurale

Un programma Java:________________________________________________________________________
import java.util.*; //for
public class Sequenza! if( lungCorr >= lungMax ){//verifica lung max
public static void main( String []args ){ lungMax=iungCorr; tipoCifra=cifraPrecedente;
System.ou/.println(“Programma Sequenza di bit"); }
Scanner sc=new Scanner! System.in ); System.out.printlnf'Sottosequenza max di "+lungMax+" bit ”+tipoCifra);
System.ou/.print("Quanti bit? “);
int n; Per evitare di dover duplicare il test sulla lung max di sottosequenza, si può riscrivere il ciclo di elaborazione
do{ delle cifre in modo da elaborare un'intera sottosequenza di cifre uguali e subito dopo testare la sua lunghezza:
n=sc.nextlnt();
if( n<=0 ) System.oivf.printlnfll numero deve >0. Ridarlo"); inizializza
}while( n<=0 ); for( tutte le cifre ){
System.OL/t.println(-Fornisci ora “+n+" bit separati da spazi"); processa un'intera sottosequenza di cifre uguali
int cifraCorrente, cifraPrecedente=sc.nextlnt(); verifica lunghezza ultima sottosequenza
int lungCorr=1, lungMax=1 ; }
int tipoCifra=cifraPrecedente; scrivi risposta
for( int i=0; i<n-1; i++ ){
cifraCorrente=sc.nextlnt(); Nuova versione del programma:
if( cifraCorrente == cifraPrecedente ) lungCorr++; import java.util.*;
else if( lungCorr >= lungMax ){ public class Sequenza2{
lungMax=lungCorr; public static void main( String [jargs ){
tipoCifra=cifraPrecedente; lungCorr=1 ; System.out.printlnf'Programma Sequenza di bit");
} Scanner sc=new Scanner! System.in ); System.out.printfQuanti bit? “);
cifraPrecedente=cifraCorrente; int n;
}//for do{
System.oiyf.println(“Sottosequenza max di "+lungMax+ " bit “+tipoCifra); n=sc.nextlnt();
}//main if( n<=0 ) System.out.printlnfll numero deve essere >0. Ridarlo");
}//Sequenza }while( n<=0 );
System.out.println("Fornisci ora "+n+" bit separati da spazi");
Lanciando il programma su diversi casi di input (casi di test) si può verificare che esso fornisce sempre una int cifraPrecedente=sc.nextlnt();
risposta corretta tranne quando la sottosequenza di lunghezza massima è l’ultima o tutto l'input è costituito da int i=1; //conta il numero dei dati letti
cifre uguali. int cifraCorrente=-1; //inizializzazione fittizia
int lungCorr=1, lungMax=1;
n=10 int tipoCifra=cifraPrecedente;
1 1000 10 10 1 for(;;){
Sottosequenza max di 3 bit 0 (OK) //processa una intera sottosequenza
for(;;){
n=10 if( j==n ) break;
1010101111 cifraCorrente=sc.nextlnt();
Sottosequenza max di 1 bit 0 (ERRORE) i++; //conta lettura
if( cifraCorrente==cifraPrecedente ) lungCorr++;
n=10 else break;
1111111111 }
Sottosequenza max di 1 bit 1 (ERRORE) //verifica l'ultima sottosequenza
if( lungCorr >= lungMax ) { lungMax=lungCorr; tipoCifra=cifraPrecedente; lungCorr=1 ;}
Il problema nasce dal fatto che nei casi indicati non viene più realizzato il test di aggiornamento della if( i==n ) break;
lunghezza max in quanto il ciclo finisce. Come provvedimento si può banalmente aggiungere il test di verifica cifraPrecedente=cifraCorrente;
anche fuori ciclo come segue: }//for
System.out.println("Sottosequenza max di "+lungMax+" bit "+tipoCifra);
}
}//Sequenza2
26 27
Capitolo 1

Il programma Sequenza2 utilizza istruzioni for(;;) che non specificano nè l’inizializzazione, nè la condizione di Capitolo 2: __
continuazione, nè il passo. Si tratta di cicli potenzialmente infiniti. L’uscita da un tale ciclo si ottiene con Strutture dati array
l’istruzione break. Più condizioni possono esistere (si veda il ciclo che processa un’intera sottosequenza) per
abbandonare il ciclo.
Tutti i dati utilizzati sino a questo punto sono dati elementari o atomici. Un intero, un reale, un booleano, etc.
sono valori primitivi e indivisibili. Esistono situazioni in cui si desidera trattare con aggregati di dati o dati
L’istruzione break fa uscire dal ciclo (for, while etc.) più interno racchiudente break, o dall'istruzione switch
composti. Gli array e le stringhe (studiate nel cap. 6) sono esempi di dati composti o strutturati.
racchiudente.
Array monodimensionali o vettori
Esercizi1234567890
Un array monodimensionale è una collezione omogenea di dati. Ad es. un array di 5 interi si può introdurre
1. Calcolare il MCD tra due interi positivi n ed m col seguente algoritmo (sottrazioni ripetute): come segue:
MCD(n,m) =n se n=m
=MCD(n-m,m) sen>m
int []a=new int[5]; //le parentesi [] possono anche seguire il nome dell’array: int a(]=...
=MCD(n,m-n) se n<m
2. Leggere un intero positivo N e scrivere l’N-esimo numero della serie di Fibonacci così definita: 1 1 2 3 5 8 L’immagine di memoria dell'oggetto a creato ma non ancora inizializzato è la seguente:
13 ... cioè i primi due numeri sono 1 ed 1; dal terzo in poi ogni numero è la somma dei due numeri che
immediatamente lo precedono
3. Leggere un intero positivo N, quindi N interi e calcolare e scrivere la loro somma.
4. Come il precedente, ma con una sequenza chiusa da un numero negativo.
5. Leggere una sequenza di interi sino al primo negativo e contare quante volte succede che un numero è
maggiore del precedente.
6. Scrivere un programma che legga un intero positivo n>1 e restituisca, con la mediazione del metodo
ePrimo() fornito nel testo, l’informazione se n è primo o no.
7. Applicando le regole di De Morgan, si riscrivano in veste equivalente le seguenti espressioni boolean:
!( x && !y )
!( dato>0 && datoci00 )
!( cor!=null && cor.info.compareTo(x)<0 )
8. Dimostrare che l’espressione booleana x1 && x2 II x2 && x3 II !x1 && x3 si può semplificare in x2 && x3.
La sola dichiarazione non crea un array:
9. Dimostrare per induzione che ] T r = I ’ + 2 ’ +... + h ’ = |n(M + I) ( 2 h + l ) | / 6 , dove nè un naturale.
i int a[]; -► ?
10. Al lato di una carreggiata stradale esiste un sensore per il rilevamento del traffico. Il sensore è collegato ad
un calcolatore ed invia un segnale ogni qualvolta passa un veicolo. Il calcolatore riceve altresì dei segnali di a=new int[5]; //crea l’array non inizializzato
tempo provenienti da un orologio interno al calcolatore, un segnale per ogni unità di tempo. L’orologio è a[0]=0; //pone zero nel 1 elemento
programmabile in modo da pianificare un intervallo di tempo di osservazione del traffico. Un segnale di veicolo
è codificato con il valore 2, un segnale di tempo con il valore 1. L’orologio invia un valore 0 al termine del Per azzerare tutto l’array a si può scrivere un ciclo come segue:
periodo di osservazione. Siccome il sensore è lontano dal calcolatore, si può verificare che un segnale di
veicolo arrivi distorto, ossia sotto forma di un valore maggiore di 2. Nell’ipotesi che solo un tipo di segnale per for( int i=0; i<5; i++) a[i]=0;
volta possa verificarsi, si vuole scrivere un programma che elabori i segnali di cui sopra e alla fine emetta: (a)
il numero dei veicoli transitati; (b) il numero degli errori registrati; (c) l’intervallo massimo di assenza di veicoli; meglio:
(d) il periodo totale di campionamento. Ad es. se al programma perviene la sequenza di input:
for( int i=0; ka.length; i++ ) a[i]=0;
12111322211111221110
a.length restituisce la capacità o dimensione dell’array. Gli indici validi vanno da 0 a a.length-1.
in uscita si dovrà avere: (a) 6 (b) 1 (c) 5 (d) 12.
Altro esempio di inizializzazione:

for( int i=0; ka.length; i++ )


a[i]=i‘ 2+1;

a[0]=0*2+1=1
a[1]=1*2+1=3
a[2]=2*2+1=5 etc.
28 29
Capitolo 2 Strutture dati array

public static void main( String []args ){


Il contenuto dell’array a può essere letto da input (tastiera) mediante uno scanner se come segue: System.out.printlnf'Statistica voti di un campione di studenti");
System.out.print (“Quanti studenti ? “);
for( int i=0; ka.length; i++ ) int n=sc.nextlnt();
a[i]=sc.nextlnt(); int voti[]=new intfn];
leggiVoti( voti );
La scrittura su output del contenuto di a si può ottenere come segue: int min=trovaMin( voti );
int max=trovaMax( voti );
for( int j=0; j<a.length; j++) float media=trovaMedia( voti );
System.out.print( a[j]+" " ); int moda=trovaModa( voti );
System.out.println(); float ds=deviazioneStandard( voti, media );
scriviRisultati( n, min, max, media, moda, ds );
Il frammento di codice che segue mostra il contenuto inverso di a: }//main
}//Statistica
for( intj=a.length-1; j>=0; j—)
System.out.print( a0)+" ' ); Stesura dei metodi:
System.out.println();
static void leggiVoti( int []v )( static int trovaMax( int []v ){
L'utilizzo di un indice esterno all’intervallo [0..a.length-1] solleva l'eccezione IndexOutOfBoundsException che //per semplicità non si fanno controlli int Max=v[0);
di norma arresta l'esecuzione del programma. System.ouf.println("Fornire “+ for( int k=1; k<v.length; k++ )
v.length+“ voti tra 18 e 30“); if( v[k]>Max ) Max=v[k];
Altri esempi di array: for( int i=0; icv.length; i++ ) return Max;
v[i]=sc.nextlnt(); }//trovaMax
doublé x[]=new double[10]; }//leggiVoti
boolean crivello[|=new boolean[100);
char []c=new char[20]; static int trovaMin( int []v )( static float trovaMedia( int v[] ){
int Min=v[0]; //partenza float media=0f;
for( int j=1; j<v.length; j++ ) for( int i=0; i<v.length; i++ )
Il tipo degli elementi di un array può essere un qualsiasi tipo (o classe) dava. if( v[j]<Min ) Min=v[jj; media=media+v[i];
return Min; return media/v.length;
Caso di studio: mini-statistica sui voti d[un campione di studenti^__________________ }//trovaMin }//trovaMedia

Problema: Leggere un intero n e quindi leggere n voti universitari. Calcolare e stampare:


• Il voto min Calcolo della moda:
• Il voto max static int trovaModa( int Qv ){
• Il voto medio //ipotesi: i dati sono corretti
• La moda (ossia il voto più frequente) int frequenze[]=new int[13|;
• La deviazione standard, ossia la radice quadrata della varianza=(1/n)*i:(l=i,n) (x,-media)2 //frequenze[0] è il numero dei 18, frequenze[1] è
(detta anche scarto quadratico medio). //il numero dei 19 etc
//azzera i contatori delle frequenze
Si leggono gli n voti in un array e si ottengono i risultati richiesti mediante successive scansioni del vettore. for( int i=0; kfrequenze.length; i++) frequenze[i]=0;
Associando ad ogni calcolo un metodo diverso, si può costruire il seguente programma. Si tenga presente che //accumula le frequenze dei voti
i tipi dei parametri e il tipo di ritorno di un metodo possono essere qualunque tipo esprimibile in Java e dunque for( int i=0; i<v.length; i++ ) frequenze[v[i]-18]++;
anche tipi array. //trova max in frequenze
int max=frequenze[0], indiceMax=0;
Programma Statistica:__________________________________________ ________________ for( int i=1; i<frequenze.length; i++ )
import java.util.*; if( frequenze[i]>max ){
public class Statistica! max=frequenze[i]; indiceMax=i;
static Scanner sc=new Scanner( System.in ); //scanner globale }
//gli altri metodi... return indiceMax+18;
}//trovaModa
30 31
Capitolo 2^ Strutture dati array

frequenza è stata già calcolata correttamente. Indichiamo con fMax la frequenza massima temporanea, con
Il metodo trovaModa utilizza un oggetto array locale frequenze che ha tanti elementi per quanti sono i possibili fCorr la frequenza corrente e con moda la moda temporanea.
voti e dunque 13. Il voto 18 è associato alla prima posizione. Un generico voto x è associato all'indice x-18.
L’array frequenze è riempito con i numeri che esprimono le ripetizioni di ciascun voto. Così in frequenze[0] static int trovaModa( int []v ){
sarà registrato il numero delle volte che è presente 18 nel campione dei voti etc. //ipotesi: v contiene almeno un voto
int fMax=1, moda=0; //inizializza
Dopo aver inizializzato (caricato) l’array frequenze, è sufficiente trovare il massimo dei suoi elementi. Tutto ciò for( int i=0; kv.length; i++ ){//per tutti i voti
fornisce la moda. Più esattamente, si trova il massimo e la sua posizione (indiceMax). Il voto modale sarà dato int fCorr=1 ; //frequenza di v[i]
da indiceMax+18. for( int j=i+1; jcv.length; j++ )
if( v[i]==v[j] ) fCorr++;
static float deviazioneStandard( int []v, float m ){ if( fCorr>fMax ){
float varianza=Of; fMax=fCorr; //freq. Max corrente
for( int i=0; kv.length; i++ ) moda=v[i]; //moda corrente
varianza = varianza+( v[i]-m )* (v[i]-m ); }//if
varianza = varianza/v.length; }//for
return (float)Math.sqrt( varianza ); return moda;
}//deviazioneStandard }//trovaModa

static void scriviRisultati( int n, int min, int max, float media, int moda, float ds ){ Il calcolo della moda può essere perfezionato osservando che:
System.out.printlnf'Risultati del campione");
System.out.println(); • se la moda corrente ha una frequenza superiore a n/2, dove n è il numero dei voti, essa non può più
System.out.printlnfNumero dei voti= "+n); essere migliorata, dunque le iterazioni del ciclo esterno possono essere arrestate
System.out.printlnfVoto Min= "+min); • se la moda corrente ha una frequenza superiore al numero degli elementi rimasti, è improduttivo
System.out.printlnfVoto Max= “+max); continuare la ricerca
System.out.printff'Voto Medio=%5.2f\n“, media ); • se l'elemento v[i] è uguale alla moda corrente non ha senso ripetere per esso il ciclo interno di calcolo della
System.out.println("Voto modale= ”+moda); frequenza corrente.
System.out.printf("Deviazione Standard=%1.2f\n", ds );
}//scriviRisultati static int trovaModa( int []v ){
int fMax=1, moda=0;
A questo punto il programma è completo e si può passare a verificarne il funzionamento. ciclo esterno:
for( int i=0; kv.length; i++ ){
Una nuova stesura dei metodi si può ottenere osservando, ad es., che la ricerca del minimo o del massimo jf( v[i]==moda ) continue ciclo_estemo;
voto si può riscrivere trovando la posizione (indice) del minimo o del massimo. Se tale posizione è iMin (per il int fCorr=1; //frequenza di v[i]
minimo) allora il minimo voto è v[iMin] e cosi via. Tale accorgimento è già stato adottato nel calcolo della for( int j=i+1 ; jcv.length; j++ )
moda. if( v[i]==v[j] ) fCorr++;
if( fCorr>fMax ){
Si nota che è più ricca di informazione la ricerca dell'indice del minimo o del massimo rispetto alla ricerca del fMax=fCorr; //freq. Max corrente
valore minimo o massimo. moda=v(i); //moda corrente
}//if
static int trovaMin( int (]v ){ static int trovaMax( int []v ){ if( fMax>v.length/2 II fMax>v.length-(i+1)) break;
int iMin=0; //ipotesi int iMax=0; }//for
for( int j=1; jcv.length; j++ ) for( int k=1; k<v.length; k++ ) return moda;
if( v[j]<v[iMin] ) iMin=j; if( v(k]>v[iMax] ) iMax=k; }//trovaModa
return v[iMin); return v[iMax];
}//trovaMin }//trovaMax L'istruzione

Di seguito si mostra un differente algoritmo per il calcolo della moda che evita l’introduzione dell’array continue ciclo_esterno;
frequenze e calcola direttamente la frequenza massima e il voto modale. Detta i una posizione sull’array v dei
voti, si determina la frequenza di v[i] andando a contare quante volte v[i] si ripete nel sottovettore comanda immediatamente la prossima iterazione del ciclo di for davanti il quale è stata posta l’etichetta
v[i+1:v.length-1]. Si osserva che se per caso il valore v[i] si è già presentato prima della posizione i, la sua ciclo esterno, saltando tutte le istruzioni che seguono continue sino alla graffa di chiusura del for. Simile a
continue è l'istruzione di uscita multilivello
32 33
Capitolo 2 Strutture dati array

break etichetta; Prodotto scalare di due vettori: _____________


static doublé prodottoScalare( doublé []a, doublé []b ){
che comanda l’uscita anticipata dal ciclo etichettato con etichetta. Dunque si possono ottenere uscite if( a.length!=b.length ){
multilivello. System. out.printlnfArray incompatibili.”);
System. exit(-1);
I metodi di Statistica ricevono un parametro array di interi, es. v, in cui sono contenuti i voti del campione. Al }//if
tempo della chiamata, es. di trovaMedia etc., l’array voti del main è trasmesso al parametro formale v di doublé ps=0;
trovaMedia. In realtà il riferimento all’oggetto array è copiato in v per cui sia voti che v puntano allo stesso for( int j=0; j<a.length; j++ ) ps=ps+a[i]*b[i];
array (si veda la figura che segue che illustra una situazione di passaggio per riferimento). return ps;
}//prodottoScalare

Ricerca lineare:
static int ricercaLineare( int []v, int x ){
//ritorna l’indice di v dove è presente x
Ilo -1 se x non è presente
int i=0;
while( kv.length && v[i]!=x ) i++;
if( kv.length ) return i;
return -1;
}//ricercaLineare

area dati di Il metodo ritorna l’indice della prima occorrenza di x in v, o -1 se x non è presente nel vettore. Il numero di
deviazioneStundard operazioni eseguite nel caso peggiore è proporzionale ad n (n è il numero di elementi dell'array), ossia il
‘‘tempo di esecuzione" T rL è dell’ordine di n: TRL(n)=0(n) (complessità lineare, si rimanda al cap. 17 per le
I parametri int []v e float m del metodo deviazioneStandard sono passati rispettivamente: v per riferimento, in definizioni formali di complessità e le notazioni utilizzate).
quanto oggetto array, m per valore, trattandosi di un dato di un tipo di base. In v è copiato il riferimento di voti
del main. In m il valore di media del main. Ogni modifica a v si ripercuote su voti. Ogni modifica ad m resta Ricerca binaria: ________
confinata nel metodo. Se l'array è ordinato, es. per valori crescenti, si può utilizzare un algoritmo di ricerca più efficiente. Detti inf e
sup due indici che delimitano l'area di ricerca su un array a, si considera l’indice medio med e ci si confronta
con l’elemento a[med]. Se a[med] è uguale all'elemento obiettivo x la ricerca ha termine con successo. Se,
invece, a[med]>x ha senso proseguire la ricerca solo tra gli elementi da inf a med-1. Se a[med]<x, la ricerca
può essere continuata interessando solo gli elementi da med+1 a sup. Nel primo caso è sufficiente ridefinire
sup a med-1. Nel secondo si può ridefinire inf a med+1. In ogni caso la ricerca può proseguire, con la stessa
tecnica, nella nuova area delimitata da inf a sup. La ricerca termina con fallimento quando si svuota l’area di
ricerca, ossia inf diventa maggiore di sup.

in d ic i 0 1 2 3 4
a 7 10 18 34 55

area dati di trovaMedia


x>a[ m ed] in f m ed sup

0 1 2 3 4
I concetti relativi al passaggio dei parametri saranno ripresi e approfonditi nel cap. 3.
7 10 18 34 55
Se si conoscono a priori i valori si può creare ed inizializzare un array rapidamente come segue: in f sup
x- a [m e d |
m ed
int []a={ 2, -5, 0}; //new implicita

Risulta: a.length=3, a[0]=2, a[1]=-5, a[2]=0.


34 35
Capitolo^ Strutture dati array

0 1 2 3 4 3 2 4 5 10
*
7 10 18 34 55 j
sup inf

2 3 5 4 10
qui inf>sup e la ricerca ha
termine con insuccesso
array ordinato
Un metodo per la ricerca binaria:
static int ricercaBinaria( int []a, int x ){ Un metodo selectionSort:
llprecondizione: a è ordinato per valori crescenti static void selectionSort( doublé a[] )[
int inf=0, sup=a.length-1, med; for( int j=a.length-1; j>0; j-- ){
boolean trovato=false; //cerca il massimo tra a[0]..a[j]
while( inf<=sup && Itrovato ){ int iMax=0;
med=(inf+sup)/2; for( int i=1; i<=j; i++ )
if( a[med]==x ) trovato=true; if( a[i]>a[iMax] ) iMax=i;
else if( a[med]>x ) sup=med-1 ; //scambia a[iMax] con a[j]
else inf=med+1; doublé park=a[iMax];a[iMax]=a[j];a[j]=park;
}//while }//for
if( trovato ) return med; }//selectionSort
return -1;
}//ricercaBinaria Il metodo di ordinamento a bolle (bubble sort) realizza successive scansioni dell’array sino all’ordinamento. In
ciascuna scansione si confrontano coppie di elementi consecutivi scambiandoli immediatamente se non
Il numero delle operazioni eseguite nel caso peggiore (tempo di esecuzione T rb) è proporzionale al log? n rispettano la relazione d’ordine. Il numero massimo delle scansioni è n-1 se n è la dimensione dell’array. Dato
(complessità logaritmica): TRB(n)=0(log2 n). RB è uno degli algoritmi più veloci. Con 32 test si riesce a stabilire l'array (3, 10, 5, 4, 2} le scansioni effettuate sono le seguenti (nella prima scansione si evidenziano le coppie
se un elemento è presente in un vettore di 232 (4 Giga) elementi. Si pensi al problema della ricerca di un che richiedono scambio):
nominativo nell’elenco telefonico di New York (circa 10 milioni di abitanti).
0 1 0 1
Algoritmi di ordinamento 3 10 5 4 2 3 5 4 2 10
Realizzano una permutazione degli elementi di un array in modo tale, ad es., che la successione risulti
crescente (o non decrescente). Un primo metodo elementare è selection sort (ordinamento per selezione).
Inizialmente si considera tutto l’array. Si cerca l’indice del massimo, quindi si scambiano il massimo con 3 5 10 4 2 seconda
3 4 5 2 10
l’ultimo elemento. A questo punto si prende in esame tutto l’array tranne l’ultimo elemento e si riapplica la p rim a
sca n sio n e
tecnica (selezione del massimo e scambio con il nuovo ultimo elemento). Si ripete sino a che la parte da sca n sio n i’
esaminare è costituita dal solo primo elemento dell'array: 3 5 4 10 2
3 4 2 5 10
3 10 5 4 2
* 3 5 4 2 10
j

3 2 5 4 10
★ 0 1 2 3 4
j 0 1 2 3 4
3 4 2 5 10 3 2 4 5 10
3 2 4 5 10 terza quarta ed

sca n sio n e
*j
3 2 4 5 10 2 5 10 sca n sio n e
3 ! 4

36 37
Capitolo 2 Strutture dati array

Un metodo bubbleSort:_______________________________ __________

co
5 >1CL _ 4 2
static void bubbleSort( doublé []a ){
for( int j=a.length-1 ; j>=1 ; j—) i=3 \
for( int i=0; i<j; i++ )
- X
if( a[i]>a[i+1] ){//scambia a[i] e a[i+1]
doublé park=a[i];
a(i]=a[i+1]; a[i+1]=park;
3 4 5 10 2
}//if
}//bubbleSort i=4

Bubble sort può essere "ottimizzato" in modo da fermarsi subito dopo una scansione che non realizzi alcuno Si confronta 4 con 10 e si sposta 10 nella locazione iniziale (buco) di 4. Si confronta 4 con 5 e si sposta 5 nella
scambio. locazione iniziale di 10. Si confronta 4 con 3 e si ferma la ricerca: x va inserito alla destra di 3. Dopo aver
sistemato il 4, si prosegue col prossimo elemento a[4]==2 e si ripetono le operazioni.
static void bubbleSort( doublé []a ){
for( int j=a.length-1; j>=1; j - ){ Un metodo insertionSort____________________________________________________________________
int scambi=0; //contatore scambi static void insertionSort( doublé)] a )(
for( int i=0; i<j; i++ ) for( int i=0; ka.length; i++ ){
if( a[i]>a[i+1] ){//scambia a[i) e a[i+1] doublé x=a[i]; int j=i;
doublé park=a[i]; while( j>0 && a[j-1]>x ){
a[i]=a[i+1]; aO]=aù-1];B
a[i+1]=park; scambi++;
} a[j]=x;
if( scambi==0 ) break;
} )//insertionSort
}//bubbleSort
Nel caso peggiore (array iniziale ordinato in modo opposto a quanto desiderato) tutti e tre i metodi di
Un’ulteriore “ottimizzazione” consiste nel limitare la prossima scansione al sottovettore tra il primo elemento e ordinamento hanno complessità quadratica 0(n2) (si veda il cap. 17), ossia il numero di operazioni necessarie
la posizione dell’ultimo scambio. Per ottenere questo occorre salvarsi l’indice dell’ultimo_scambio e sfruttare per portare a termine l’ordinamento è proporzionale al quadrato del numero di elementi dell’array. I
una "virtù" del ciclo di for secondo cui il passo è un’istruzione. provvedimenti discussi per bubble sort migliorano il comportamento dell’algoritmo in situazioni favorevoli. Per
n non piccolo e nelle ipotesi peggiori, bubble sort è meno efficiente di selection sort in quanto quest’ultimo
static void bubbleSort( doublé []a ){ realizza meno scambi.
int ius=0;//indice ultimo scambio - inizializzazione fittizia
for( int j=a.length-1 ; j>=1 ; j=ius ){ Triangolo di TartagliaSi
int scambi=0; //contatore scambi Si mostra un algoritmo che genera le prime 10 righe (numerate da 0 a 9) del triangolo di Tartaglia. Si utilizzano
for( int i=0; i<j; i++ ) due array di 10 interi a e b. Ogni nuova riga è costruita su b. La riga precedente è contenuta in a. Prima di
if( a[i]>a[i+1] ){//scambia generare la nuova riga, si copia il contenuto di b su a utilizzando il metodo arraycopy di System:
doublé park=a[i]; a[i]=a[i+1];
a[i+1]=park; scambi++; ius=i; System.arraycopy( //5 parametri
} array_sorgente, indice partenza su array sorgente,
if( scambi==0 ) break; array_ dest, indice partenza_ su_array^dest,
} numero elementi da _copiare );
}//bubbleSort
//frammento di programma
Un terzo metodo elementare di ordinamento è quello per inserimento (insertion sort). Esso considera un int a[]=new int[10];
elemento alla volta del vettore, sia x il generico elemento, e ricerca per esso la posizione corretta nella int b[]=new int[10];
porzione dell’array a sinistra della posizione di x. Sposta di un posto a destra immediatamente ogni elemento for(int i=0; i<10; i++){//per tutte le righe
dell’array che risulti maggiore di x. La posizione di inserimento è quella a destra del primo elemento trovato b[0]=1; b[i]=1;
non maggiore di x. Di seguito si considera l’insertion sort dell'elemento x=a[i]=4, i=3. for( int j=1 ; j<i; j++ ) b[j]=a(j]+a[j-1];
for( int j=0; j<=i; j++)
System.out.print(b[j]+"\t“);
38 39
Capitolo 2 Strutture dati array

System.out.println(); Puntualizzazioni:
System.arraycopy( b. 0, a. 0, i+1 );
}//for • m[i][j] denota l’elemento all'incrocio tra la riga i e la riga j (indici supposti corretti)
• m[i] denota un’intera riga, 0<=i<m.length, ossia un oggetto array monodimensionale
Array bidimensionali e matrici^ • m.length dà il numero delle righe
Un oggetto array bidimensionale si introduce come segue: • m[0].length o m[i].length restituiscono il numero di elementi della riga 0 o della riga i di m.

int [][]m=new int[4][6]; Come per i vettori, anche le matrici possono essere create ed inizializzate “al volo”:

4 è il numero delle righe; 6 il numero delle colonne. Si tratta di una matrice rettangolare 4x6 della matematica. int [][]m={ {1,0,2}, {-2,1,5}}; //new implicita: si notino gli elementi-aggregati di riga

in d ici d i colonna crea una matrice 2x3 di interi in cui:

1 2 3 m[0][0]==1, m[0][1]=0. m[0][2]==2


0
m[1][0]==-2, m(1][1]==1, m[1][2]=5
in d ici
di 1 Altri esempi:
riga
2
m
—, £ doublé [](]a=new double[4}[4];
3 boolean [][]h=new boolean[3][5];
r A
elem ento m in u i a è una matrice quadrata. Una sua possibile inizializzazione è:
m |2 ||M i indice di riga
l J j indice di colonna
\ ) lor( int i=0; ka.length; i++ )
for( int j=0; j<a[i).length; j++ )
Un array bidimensionale è in realtà un array di array, ossia un array in cui ogni elemento è a sua volta un array a[i]0]=Math.random(); //numero casuale tra 0 e 1
monodimensionale. Seguono alcune tipiche operazioni sulle matrici.
Costruzione di una matrice identità:
Azzeramento del contenuto di m:
for( int i=0; i<4; i++ ) for( int i=0; ka.length; i++ )
for( int j=0; j<6; j++ ) for( int j=0; j<a[0].length; j++ )
m|i][j]=0; a [i]D ]= (i= j)?1 :0 ;

Equivalentemente:
tor( int i=0; km.length; i++ )
for( int j=0; j<m[0].length; j++ ) //si suppone m[0].length == m[i].length
m[i](j]=0;

Lettura per righe da input:


for( int r=0; r<m.length; r++ )
for( int c=0; c<m[0].length; c++ )
m[r][c]=sc.nextlnt(); //se è uno Scanner

Scrittura del contenuto per righe:


for( int i=0; km.length; i++ ){ doublé [][]a=new double[10][10];
for( int j=0; j<m[0].length; j++ )
System.out.printf(“%5d", m[i][j]); doublé []r=new double[a.length];
System.out.println(); doublé []c=new doublefa.length];
}//for esterno
for( int i=0; ka.length; i++ ){
r[i]=0; c[i]=0;
40 41
Capitolo 2 Strutture dati array

for( int j=0; ka.length; i++ ){ Moltiplicazione:


r[i]=r[i)+a[i][jl;
c[i]=c[i]+a[j][i];

Costruzione di una matrice triangolare riutilizzando ciclicamente il contenuto di un vettore.

int [][]m=new int[30][30];


int [jv=new int[17];
//inizializza v es. con valori casuali in [0..50[
for( int i=0; i<v.length; i++) c[i][j] è il prodotto scalare tra la riga i di a e la colonna j di b. Esempio di moltiplicazione:
v[i]=(int)(Math.random()*50);
//inizializza m a tutti zero
for( int i=0; km.length; i++)
for( int j=0; j<m[0].length; j++ ) m[i][j]=0;

//riempie il triangolo superiore di m


int k=0; //indice su v
for( int i=0; km.length; i++ )
for( int j=i; j<m[i].length; j++ ){
m[i][j]=v[k); k=(k+1 )%v.length;
}
//visualizza contenuto di m

Il riutilizzo ciclico del vettore v si basa sull 'aritmetica modulare sull’indice k: k=(k+1)%v.length. Quando k vale L’elemento neutro rispetto all’addizione è la matrice nulla costituita da tutti zeri. L'elemento neutro rispetto alla
v.length-1, il lato destro (k+1 )%v.length si valuta a 0. In tutti gli altri casi, il lato destro si valuta a k+1. È moltiplicazione è la matrice unitaria o identità che ha tutti 1 sulla diagonale principale, 0 in ogni altra posizione.
possibile combinare l’azzeramento del triangolo inferiore ed il riempimento del triangolo superiore come
segue: Sono assegnate due matrici quadrate a e b create ed inizializzate. In s e p si vogliono costruire la matrice
somma e la matrice prodotto di a e b.
int k=0;
for( int i=0; km.length; i++)
for( int j=i; j<m[i].length; j++ ){ doublé [][]s=new double[a.length][a.length];
m(j][i]=0;//azzera triangolo inferiore doublé [)[]p=new double[a.length][a.length];
m[i][j]=v[k]; k=(k+1)%v.length;
} Matrice somma:
for( int i=0; ks.length; i++ )
Richiami di algebra lineare for( int j=0; j<s[i].length; j++ )
Neìl’insieme delle matrici quadrate NxN di numeri (interi o reali) sono definite le seguenti operazioni: s[i][j)=a[i][j]+b[i][j];

Addizione (sottrazione): Matrice prodotto:


e |i||j|= a |i||j|+ b |i||j| for( int i=0; kp.length; i++ )
for( int j=0; j<p[i].length; j++ ){
doublé ps=0;
for( int k=0; k<p.length; k++ )
ps=ps+a[i][k]‘ b[k]0);
p[i](j]=ps;
}

42 43
Capitolo 2 Strutture dati array

Due matrici rettangolari apxq e bqxr si dicono compatibili rispetto alla moltiplicazione. La loro matrice prodotto ha
dimensioni: Un metodo equivalente al precedente è il seguente:

3 p Xq X b qxr = Cpxr static boolean simmetrica( doublé [][]m ){


for( int i=0; km.length; i++ )
Di seguito si propone l'algoritmo della moltiplicazione in un metodo che riceve due matrici di doublé, ne for( int j=0; j<i; j++ )
verifica la compatibilità e restituisce, se esiste, la matrice prodotto. Si ipotizza che le matrici in gioco non siano if( m(i)[j]!=m[j][i] ) return false;
irregolari nelle righe, ossia tutte le righe hanno (come di norma) la stessa lunghezza. return true;
[//simmetrica
static doublé [][] prodotto( doublé a[J[], doublé [][]b ){
if( a[0].length != b.length )( Progetto durn programma
System.out.printlnfMatrici incompatibili."); Si devono leggere da input due matrici a e b quadrate 4x4 di numeri reali e calcolare la matrice:
System.exit(-I);
} c=at-a*b*
doublé prod[][]=new double[a.length][b[0].length];
for( int i=0; kprod.length; i++ ) dove a1denota la matrice trasposta di a etc.ll programma deve infine visualizzare le matrici a, b e c e scrivere
for( int j=0; j<prod[i].length; j++ ){ un messaggio se c è simmetrica o no. Una versione astratta del programma che rimanda a specifici metodi è:
doublé ps=0;
for( int k=0; k<a[0].length: k++ ) public class Matricif
ps=ps+a[i][k]**b[k][j]; public static void main( String []args ){
prod[i][j]=ps; dichiarazione e creazione di: a b c at bt tmp
} leggi( a ); leggi( b );
return prod; scrivi( a ); scrivi( b );
}//prodotto at=trasposta( a ); bt=trasposta( b );
tmp=moltiplica( a, bt );
Una matrice mt è trasposta di m se le righe di mt coincidono con le colonne di m e viceversa: tmp=moltiplicaScalare( tmp, -1 );
c=addiziona( at, tmp );
mt[i]D) == m[j][i] v i, j scrivi( c );
if( simmetrica( c ) ) System.out.printlnfMatrice simmetrica”);
Costruzione matrice trasposta: else System.out.printlnfMatrice non simmetrica");
}//main
doublé m[][]=new double[10][10]; [//Matrici

doublé mt[][]=new double[10][10]; Si osserva che.

for( int i=0; i<m.length; i++ ) • il metodo leggi( m) riceve un oggetto array m già allocato dal main e provvede a riempirlo attraverso letture
for( int j=0; i<m[i].lenqth; i++ ) da input.
mt[i][j]=m[j][i); • Il metodo scrivi( m ) riceve un oggetto array m e ne visualizza il contenuto per righe su output.
• Il metodo double(][] trasposta( m ) riceve una matrice m e costruisce e ritorna una nuova matrice col
Una matrice quadrata è simmetrica se coincide con la sua trasposta. Un metodo di verifica: contenuto trasposto di m.
• Il metodo double[][] moltiplica( m i, m2 ) riceve due oggetti matrici già creati e costruisce e ritorna una
static boolean simmetrica( doublé [][]m ){ nuova matrice contenente il prodotto di m1 per m2.
boolean esito=true; //ottimismo
• Il metodo double[][] addiziona( m1, m2 ) è analogo a moltiplica ma costruisce e ritorna la matrice somma di
ciclo esterno:
m1 più m2.
for( int i=0; km.length; i++ )
• Il metodo double[][] moltiplicaScalare( m, s ) costruisce e ritorna una matrice caricata con gli elementi di m
for( int j=0; j<i; j++ )
ciascuno moltiplicato per lo scalare s.
if( m[i][j]!=m[j)[i] ){
esito=false; break ciclo esterno;
Programma Matrici:________________________________________________________________________
}
return esito; public class Matrici{ //da completare come esercizio
[//simmetrica static void leggi( doublé [][]m ){...[//leggi
static void scrivi( doublé [][]m ){...[//scrivi
44 45
Capitolo 2 Strutture dati array

static double[][] trasposta( doublé [][]m ){...}//trasponi Ovvie estrapolazioni sono:


static doublejjj] moltiplica( doublé [][]rr»1, doublé [][]m2 ){...}//moltiplica
static double[][] addiziona( doublé [][]m1, doublé [][]m2 ){...}//addiziona t.length - lunghezza della prima dimensione
static double[](] moltiplicaScalare( doublé [][]m, doublé s ){...}//moltiplicaScalare t[0].length - lunghezza della seconda dimensione
static boolean simmetrica( doublé [][]m ){...}//simmetrica t[0][0].length - lunghezza della terza dimensione.
public static void main( String []args ){//come prima
}//main Esercizi __ _____________ _____________
}//Matrici T. Modificare il metodo leggiVoti()del programma di statistica dei voti, in modo da controllare che ogni voto
letto appartenga effettivamente aH’intervallo 18..30. In caso contrario, non accettare il voto e rileggerlo. Nel
Quadrato magico main occorre accertarsi che il numero n dei voti sia >0.
È una matrice quadrata in cui la somma degli elementi di una qualsiasi riga, colonna o diagonale maggiore 2. Scrivere un metodo che riceve un array di interi, elimina tutti gli zeri presenti e restituisce un nuovo array
(principale e secondaria) è sempre la stessa (costante magica). Si conoscono algoritmi per costruire q.m. NxN con gli elementi diversi da zero. Una strategia per l’eliminazione degli zeri è la seguente: si scandisce l'array in
allorquando avanti. Ogni volta che si trova uno zero si spostano tutti gli elementi che lo seguono, di un posto a sinistra e si
aggiorna la lunghezza effettiva.Per evitare di modificare l’array originario, si lavora su una copia costruita al
• Nè dispari tempo di chiamata del metodo.
• La matrice contiene tutti i numeri interi tra 1 e N2. 3. Come il precedente ma evitando gli spostamenti: gli elementi non zero si copiano su un array locale che è
poi restituito.
Di seguito si propone un algoritmo che inizia ponendo 1 nella cella mediana dell'ultima riga, quindi attribuisce 4. Scrivere un metodo che riceve un array di interi e lo compatta eliminando i duplicati, separatamente nelle
ordinatamente i restanti interi mediante una visita sempre a SUD-EST in senso ciclico, interpretando la due ipotesi: a) array disordinato; b) array ordinato. Ad es. se il numero 5 è presente 12 volte, dopo la
matrice come struttura spaziale toroidale. Quando il movimento a SUD-EST è impedito si va a NORD dove la compattazione il 5 è presente una sola volta.
cella è certamente libera. 5. Dire quale compito risolve il seguente metodo calcola(). Fornire quindi un possibile main che ne faccia uso:

//acquisisci N e controlla che sia dispari >1 static intp calcola( int []v ){
int N=...; int i=0, j=0;
int qM[][]=new int[N][N]; for( int k=1; kcv.length; k++ )
//imposta tutte le celle di qM nello stato libero if( v[k]<v[i] ) i=k;
for( int i=0; i<N; i++ ) else if( v[k]>v(j] ) j=k;
for( int j=0; j<N; j++ ) qM[i][j]=0; int []a=new int[2];
a[0]=v[i]; a[1)=v[j);
int rig=N-1, col=N/2; return a;
for( int k=1; k<=N*N; k++ ){ }//calcola
qM[rig][col]=k;
//esiste via libera a sud-est ? 6. Completare la stesura di tutti i metodi del programma Matrici.
if( qM[(rig+1 )%N][(col+1 )%N]==0 ){//Si. 7. Scrivere un metodo che riceva una matrice di interi m e determini il primo punto di sella, se esiste, in m. Di
rig=(rig+1 )%N; col=(col+1 )%N; tale punto occorre ritornare le coordinate eriga,colonna>. Se il punto di sella non esiste, si deve ritornare
}
else //No. Vai a nord <-1,-1 >.
rig=rig-1;
}//for Un punto di sella è un elemento m[i][j] che è contemporaneamente minimo sulla riga i e massimo sulla
visualizza qM colonna j della matrice.Una possibile intestazione del metodo è la seguente:

static int[] puntoDiSella( int []m ){...}


Array multi-dimensionali
Più in generale Java consente di trattare array multidimensionali con le stesse tecniche delle matrici. Ad in cui le coordinate riga colonna sono ritornate mediante un array di 2 interi.
esempio, la dichiarazione:

doublé [][][]t=new double[3][5][10];

introduce un array a tre dimensioni. Per lavorare su di esso occorre utilizzare tre indici: t[i][j][k] dove ie [0..2],
je [0..4] e ke [0..9],

46 47
Capitolo 3:_____ ________
Classi e oggetti
Maggiore potenza e flessibilità deriva ai programmi Java dalla capacità di poter programmare nuove classi di
oggetti “tagliate su misura” delle applicazioni. Una classe descrive le caratteristiche comuni (dati e operazioni)
di una famiglia di oggetti (tipo di dati astratto). Di seguito si considerano diversi esempi di classi e istanze di
classi (o oggetti).

Una classe Punto ___ _________ _____________________ _____


Si propone una classe Punto per applicazioni geometriche nel piano X-Y. Ogni punto si caratterizza per le
sue coordinate x e y. La classe introduce tre metodi costruttori: a) il costruttore di default che inizializza x ed
y con le coordinate delTorigine; b) il costruttore normale che riceve parametricamente i valori delle coordinate
x ed y del punto; c) il costruttore di copia che inizializza il nuovo punto come copia di un punto ricevuto come
parametro. I metodi costruttori hanno il nome della classe e nessun tipo di ritorno. Servono per creare ed
inizializzare oggetti della classe. I metodi accessori getX() e getY() restituiscono rispettivamente il valore
della coordinata x e della coordinata y. Il metodo mutatore sposta() riceve due valori nuovaX ed nuovaY e
aggiorna il punto a queste nuove coordinate. Il metodo distanza riceve un punto p come parametro e calcola
e restituisce la distanza tra l’oggetto corrente e p. Infine, il metodo toString() ritorna una stringa con i valori
delle due coordinate del punto.

class Punto{
private doublé x, y; //variabili di istanza

public Punto(){ x=0; y=0;} //costruttore di default


public Punto( doublé x, doublé y ) { this.x=x; this.y=y;} //costruttore “normale"
public Punto( Punto p )( x=p.x; y=p.y;} //costruttore di copia

//metodi accessori
public doublé getX(){ return x ;}
public doublé getY(){ return y ;}

public void sposta( doublé nuovaX, doublé nuovaY ){//esempio di metodo mutatore
x=nuovaX; y=nuovaY; //a chi si riferiscono x e y ?
}//sposta

public doublé distanza( Punto p ){


return Math.sqrt((p.x-x)‘ (p.x-x)+(p.y-y)*(p.y-y));
}//distanza

public String toString(){


return V + x + V + y + V ;
}//toString
}//Punto

La classe Punto si suppone definita in un file Geometria.java contenente anche la seguente classe col main:

public class Geometria!


public static void main( Stringi] args ){
Punto pO=new Punto(); //nell’origine
Punto p1=new Punto] 2, 5 );
Punto p2=new Punto( -3,4 );
49
Capitolo 3 Classi e oggetti

doublé d12=p1.distanza(p2); (°) public doublé distanza( Punto p ){


System.out.println( pi ); (00) return Math.sqrt((p.x-this.x)*(p.x-this.x)+(p.y-this.y)*(p.y-this.y));
pO=pi; }//distanza
p1.sposta( 10, 20 );
System.out.printlnj pO ); (00°) public String toString(){
}//main return V+this.x+V'+this.y+V;
}//Geometria }//toString

In uno stesso file .java possono essere presenti più classi ma una sola può essere public. Il nome della classe In molti casi l’uso di this può rimanere implicito al fine di non appesantire la notazione. Esistono situazioni,
public, es. dotata del metodo main, fornisce il nome del file, es. Geometria.java. tuttavia, nelle quali il pronome this dev’essere usato in modo esplicito. Ad esempio, è possibile riscrivere i
metodi costruttori della classe Punto come segue:
Il programma può essere posto in esecuzione lanciando l'interprete (java) sul nome del file Geometria, dopo
averlo compilato: public Punto(){ this(0,0);} (1 ) rimanda al costruttore “normale”

c:\primLprogrammi>javac Geometria.java INVIO public Punto( doublé x, doublé y )( (2)


c:\primLprogrammi>java Geometria INVIO this.x=x; this.y=y;
}
Il programma dichiara e crea tre oggetti (o istanze) di classe Punto pO, p1 e p2, utilizzando in un caso il public Punto( Punto p ) { this( p.x, p.y );} (3)
costruttore di default (per pO), negli altri due casi il costruttore normale. Sulla linea (°) si fa uso della notazione
ad oggetti. Sull’oggetto p1 (ricevitore) si invoca il metodo distanza(), passando a quest’ultimo l’oggetto p2 Nei casi (1) e (3) l’uso funzionale del pronome this() consente di rimandare le operazioni del costruttore
come argomento. Si dice pure che a p1 viene inviato un messaggio distanza() con parametro p2. In generale (rispettivamente di default e di copia) al costruttore "normale". Nel caso (2) invece, l’uso di this permette di
la notazione qualificata è: disambiguare le variabili di istanza dai parametri, qui volutamente nominati esattamente come le variabili di
istanza. Ogni uso non qualificato di x ed y nel costruttore normale (2) si riferisce ai parametri. Gli usi di x ed y
oggetto.metodo( parametri ) qualificati mediante this si riferiscono invece alle variabili di istanza dell'oggetto in fase di costruzione. Per
scrivere su output il contenuto (stato) di un oggetto Punto, si potrebbero utilizzare i metodi accessori come
Quando mancano i parametri, occorre scrivere metodoQ. segue: System.out.printlnfV+pl.getXO+Y+pl.getYO+V). In alternativa si può utilizzare il metodo toString()
che ritorna sotto veste di stringa lo stato dell’oggetto this: System.out.println( p1.toString() ).
Variabiljdi istanza
I dati (campi) x e y sono presenti in ogni oggetto (istanza) creato a partire dalla classe Punto. Si chiamano In considerazione del suo uso ricorrente, il compilatore consente l'uso implicito di toString. Nell’istruzione:
variabili di istanza. È importante riflettere che i punti pO, p1, p 2 ,... pur strutturalmente simili, hanno comunque System.out.println( p1 ); si chiede al metodo println di scrivere su output l’oggetto p1. È il compilatore che
uno stato indipendente. Spostando p1 provvede, dietro le quinte, ad invocare il metodo toString su di esso. Tale accorgimento è stato usato sulle
linee (00) e (00°) del programma.
p1.sposta( 3,-7 );
Oggetti e riferimenti ________ ______
cambiano i valori delle variabili di istanza di p1. Gli altri punti non sono influenzati da questa operazione. É utile considerare l’immagine di memoria associata ai tre oggetti Punto appena creati:

II pronome this
È una parola riservata del linguaggio. Denota implicitamente, all’interno dei metodi di una classe, l’oggetto-
ricevitore su cui è stato invocato il metodo. Ad esempio, all'interno del metodo distanzaQ, invocato sulla linea pO
(°) del programma, this denota p1 ed i campi x ed y si riferiscono alle variabili di istanza di p1. Per chiarezza si
potrebbero riscrivere i metodi di Punto equivalentemente come segue:
P1
//metodi accessori
public doublé getX(){ return this.x;} P2
public doublé getY(){ return this.y;}

public void sposta( doublé nuovaX, doublé nuovaY ){//esempio di metodo mutatore
this.x=nuovaX; this.y=nuovaY; Risulta evidente una questione fondamentale. In Java le variabili di tipi oggetti, contengono riferimenti alle
}//sposta aree di memoria degli oggetti, create ed inizializzate dai metodi costruttori. Pertanto, i termini oggetto e
riferimento, in Java, si possono usare come sinonimi.

50 51
Capitolo 3 Classi e oggetti

L’istruzione in questo modo, l’oggetto precedentemente puntato da p2 diventa irraggiungibile e dunque la sua memoria
recuperabile dal garbage collector (si veda la figura che segue):
p1.sposta( 3,-7 );

comporta le conseguenze illustrate di seguito:

pO

P1

qarbaqe
P2
Nota:
1 In Java gli array e le stringhe sono oggetti. Essi sono creati esplicitamente con l’operatore new o mediante
L'assegnazione un aggregato costante implicitamente a tempo di dichiarazione es.
int a[]={1,2,4}; String s=”Java is fantastic";
p0=p1; * Il loro tempo di vita è regolato dalla permanenza dei loro riferimenti. Quando un oggetto non è piu
attivamente riferito, la memoria da esso occupata è raccolta (per essere riutilizzata) dal garbage collector.
modifica l’immagine di memoria come segue:
Oggetti incapsulati
x Nella classe Punto le variabili di istanza x e y sono state battezzate private, ossia inaccessibili dall’esterno. La
privatezza dei dati di un oggetto, costituisce sempre una decisione strategica di programmazione. Tutto ciò
pO y contribuisce a progettare gli oggetti come entità incapsulate, in cui non è possibile modificare lo stato interno
(involontariamente o accidentalmente) se non tramite l’invocazione di metodi (si veda anche la figura che
X segue, in cui la freccia esterna indica l’invocazione di un metodo). Gli oggetti incapsulati contribuiscono a
P1 realizzare programmi più chiari e sicuri perché gli oggetti si comportano, rispetto al loro uso, come black-box-.
y
per attivare le funzionalità degli oggetti non esiste altro modo che inviare ad essi messaggi. In presenza di
malfunzionamenti, essi possono essere ricercati neH'implementazione di metodi specifici delle classi utilizzate.
P2 X

L'oggetto precedentemente riferito da pO diventa garbage (spazzatura) e la sua memoria verrà


automaticamente recuperata a cura del garbage collector di Java. pO e p1 riferiscono ora lo stesso oggetto. Si
dice che pO è un alias di p1. Ogni modifica attuata tramite pO è immediatamente visibile da p1 e viceversa. Ad
esempio:

p0.sposta( 10, 20 ); Se p è un oggetto Punto, le scritture p.x e p.y denotano l’accesso ai campi (variabili di istanza) x ed y di p. Tali
System.out.println( p1 ); accessi sono rigorosamente proibiti se avvengono dall’esterno della classe (es. nel main). Anziché p.x e p.y
occorre scrivere p.getX() e p.getY(), ossia si deve fare uso dei metodi accessori getX()/getY() che consultano
comporta la stampa di <10,20>. lo stato dell’oggetto, senza modificarlo. L'accesso ai campi di un oggetto è, invece, ovviamente possibile
dall’interno della classe. Tutto ciò è stato sfruttato, ad es., nella scrittura del metodo costruttore di copia di
La costante nuli ________ Punto, al cui interno si accede direttamente ai campi x ed y del parametro p senza l’appesantimento delle
nuli è un nome predefinito e riservato di Java. Denota assenza di puntamento. Un oggetto può essere chiamate ai metodi accessori; stessa cosa succede nel metodo (accessore) distanza().
“dimenticato” ponendo a nuli il suo riferimento, es.
Considerato che un triangolo è individuato dai suoi tre punti-vertici, si può agevolmente progettare una classe
p2=null; Triangolo che si basa sulla classe Punto, nel senso che ne utilizza le funzionalità. Per creare un triangolo
occorre passare i suoi tre punti. In alternativa si può creare un triangolo a partire da un altro triangolo
(costruttore di copia). In questo esempio non è significativo il costruttore di default che quindi non viene
52 53
Capitolo 3 Classi e oggetti

introdotto. La classe Triangolo che segue si suppone collocata all’interno dello stesso file Geometria.java che problema è legato al controllo dei fenomeni di aliasing. Assegnando i parametri punto alle variabili di istanza,
contiene la classe Punto e la classe Geometria col main. in realtà si copiano i riferimenti nei parametri p 1, p2 e p3 alle variabili this.p1, this.p2 e this.p3, come mostrato
nella figura che segue, relativa allo scenario:
Una classe Triangolo _________
Punto pa=new Punto(2,5);
class Triangolo! Punto pb=new Punto(3,7);
private Punto p1, p2, p3; //vertici Punto pc=new Punto! 10,-2);
private doublé a, b, c; //lunghezze dei lati
public Triangolo! Punto p i, Punto p2, Punto p3 ) { //costruttore normale Triangolo t=new Triangolo! pa, pb, pc );
a=p1.distanza(p2);
b=p2.distanza(p3);
c=p3.distanza(p1);
//verifica esistenza triangolo
if( a>=b+c II b>=a+c II c>=a+b ){
System.out.printlnf’Triangolo inesistente");
System. exit(-1);
}
this.p1=new Punto( p1 );
this.p2=new Punto! p2 );
this.p3=new Punto( p3 );
}//costruttore

public Triangolo! Triangolo t ){//costruttore di copia


p1=new Punto! t.p1 ); p2=new Punto! t.p2 ); p3=new Punto! t.p3 );
this.a=t.a; this.b=t.b; this.c=t.c;

public doublé getA(){ return a ;}


public doublé getB(){ return b ;} p1 p2 p3
public doublé getC(){ return c ;}

public doublé perimetro!)! return a+b+c; }//perimetro Porre in aliasing i tre vertici del triangolo con i punti passati al costruttore, può essere causa di fenomeni
subdoli come quello descritto di seguito. Se dopo la costruzione di t, si effettua uno spostamento mettiamo del
public doublé area(){ punto pa: pa.sposta( 12, 5 ); l’operazione si ripercuote immediatamente sul triangolo t che cosi vede cambiare
doublé s=this.perimetro()/2; //semi-perimetro il suo perimetro e/o area. Addirittura, il triangolo potrebbe non sussistere più geometricamente!
return Math.sqrt(s*(s-a)*(s-b)*(s-c)); //formula di Erone
}//area Per separare ogni futura interazione tra i tre punti specificati come vertici al costruttore, ed i vertici del
triangolo, è sufficiente impedire l'aliasing copiando esplicitamente i parametri punto come mostrato nei
public String toString(){ costruttori della classe Triangolo, utilizzando il costruttore di copia di Punto (si veda la figura nella pagina
return "Triangolo con vertici: "+p1+" "+p2+" "+p3; seguente).
}//toString
È utile riassumere la struttura risultante del file Geometria.java:
//altri metodi...
}//Triangolo class Punto

Al tempo di costruzione di un oggetto triangolo vengono calcolate le lunghezze dei tre lati a, b e c, utilizzati per ^ class Triangolo
verificare la condizione di esistenza geometrica del triangolo (punti non allineati). Se il triangolo non esiste, il
costruttore normale scrive un messaggio sul video e fa terminare il programma con System.exit(-I) (il -1 per
convenzione indica terminazione per una situazione di errore). Se il triangolo esiste, allora vengono copiati i f public class Geometria
tre vertici utilizzando il costruttore di copia della classe Punto. Perché questa copia? Cosa succede se, public static void main
banalmente, si assegnano i parametri punto alle variabili di istanza: this.p1 =p1 ; this.p2=p2; this.p3=p3; ? Il
54 55
Capitolo 3^ Classi e oggetti

class Poligono{
private Punto []v; //vertici
public Poligono( Punto [] v ){
if( v.length<3 )(
pa System.out.printlnfPoligono inesistente"): System.exit(-I);
}
//ipotesi: i vertici formano un poligono convesso
pb this.v=new Punto[v.length];
for( int i=0; kv.length; i++ )
this.v[i]=new Punto( v[i] );
pc }
public Poligono( Poligono p ){
v=new Punto[p.v.length];
for( int i=0; kv.length; i++ ) v[i]=new Punto( p.v[i] );
Variabili di istanza }
del triangolo t dopo public doublé perimetro(){
copia dei vertici Il lasciato come esercizio
(assenza di aliasing) }//perimetro
public doublé area(){
Il lasciato come esercizio
}//area
public String toString(){
String s=“Poligono con vertici:
for( int i=0; kv.length; i++ ){
p1 p2 p3 s+=v[i); s+=‘ ';
}
Overloading dei metodi return s;
In una classe Java possono comparire più metodi (anche costruttori) con lo stesso nome (overloading o }//toString
metodi “sovraccarichi”). }//Poligono
Java richiede che i metodi overloaded siano differenziati dal numero e/o tipi dei parametri in modo che sia
sempre univoco distinguere, staticamente, quale metodo va invocato. Nel metodo toString di Poligono, quando si concatena ad s il vertice v[i], il compilatore invoca (implicitamente)
il metodo toString di Punto. Si tratta di un fatto generale: un oggetto parte di una stringa, si valuta alla stringa
Una classe Poligono restituita dal suo metodo toString.
Di seguito si propone una semplice classe Poligono, collocata nel file Geometria.java. Per semplicità si
considerano poligoni convessi. A tempo di costruzione si passa un array di punti (almeno 3) che definiscono i Modificare il main di prova del programma Geometria in modo da creare qualche oggetto Poligono e
vertici del poligono. Ovviamente il perimetro del poligono si può agevolmente calcolare valutando le misure dei verificarne il funzionamento.
lati (distanze tra coppie consecutive di vertici) e sommandole. Per quanto riguarda la determinazione dell’area
ci si può riferire alla figura che segue: Caso di studio: un programma genetico
]Gioco della vita: J. H. Conway): Si considera una matrice nxm di caratteri rappresentante un foglio
quadrettato (universo o mondo virtuale). Ogni quadretto può essere occupato o meno da un organismo (il
carattere indica presenza, 7 l'assenza). Partendo da una configurazione iniziale di organismi, essa evolve
nel tempo (generazioni successive) in accordo alle seguenti regole genetiche:
• un organismo sopravvive nella generazione successiva, se ha due o tre vicini
• un organismo muore, cioè lascia la cella vuota nella generazione successiva, se ha più di tre o meno di
due vicini
• un organismo nasce in una cella precedentemente vuota, se la cella è circondata da tre organismi vicini.

Si presenta un intero programma che simula il gioco della vita utilizzando una classe GiocoDellaVita che
mantiene il mondo virtuale su una matrice mappa (variabile di istanza) ed ammette i seguenti metodi:

- costruttore, che riceve n ed m (nr righe e nr colonne della matrice di char)


56 57
Capitolo 3 Classi e oggetti

- public void aggiungiOrganismof int /', int j ) che aggiunge un organismo (“ ’) nella cella <i,j> nuovaMappa=new char[n][m);
- public void prossimaGenerazionef) che genera la prossima generazione a partire da quella attuale for(int i=0; i<n; i++) for(int j=0; j<m; j++) mappa[i][j]='.';
- private int contaVicini( int i, int j ) che conta il numero degli organismi presenti nell’intorno della cella }
<i,j> public void aggiungiOrganismo( int i, int j ){
- public String toString() che ritorna sotto forma di stringa il contenuto di mappa. if( i<0 II i>=n II j<0 II j>=m ) throw new HlegalArgumentException();
mappa[i]U]='*';
La classe GiocoDellaVita introduce una seconda variabile di istanza nuovaMappa, che è un’altra matrice nxm }//aggiungiOrganismo
di caratteri, il cui contenuto è definito a partire da mappa a cura del metodo prossimaGenerazione(). L’uso di
nuovaMappa serve a garantire il sincronismo (simultaneità) nella definizione della prossima generazione: il private int vicini( int i, int j ){
futuro dipende strettamente dal presente e non dal futuro stesso. nuovaMappa, a fine metodo int cont=0;
prossimaGenerazione(), va copiata su mappa (il futuro diventa il nuovo presente). Tenendo conto che il if( i>0 && mappa[i-1](j]=='*' ) cont++; //NORD
contenuto precedente di mappa diviene non più utile, mappa può essere riutilizzata come nuovaMappa per la if( i>0 && j<m-1 && mappa[i-1][j+1 ]=='*' ) cont++; //NE
prossima generazione. In sostanza è opportuno, a fine metodo prossimaGenerazioneQ, scambiare i riferimenti if( j<m-1 && mappa[i][j+1]=='*' ) cont++; //EST
alle matrici mappa e nuovaMappa, per prepararsi alla successiva generazione. if( i<n-1 && j<m-1 && mappa[i+1 ][j+1 ]=='*' ) cont++; //SE
if( i<n-1 && mappa[i+1][j]=='*' ) cont++; //SUD
if( i<n-1 && j>0 && mappa[i+1][j-1 ]=='*' ) cont++; //SO
if( j>0 && mappa[i][j-1]==’*' ) cont++; //OVEST
if( i>0 && j>0 && mappa[i-1)[j-1 ]=='*' ) cont++; //NO
return cont;
}//vicini

public void prossimaGenerazione(){


for( int i=0; i<n; i++ )
for( int j=0; j<m; j++ )(
int v=vicini( i, j );
if( mappa[i]0]=='*' ) nuovaMappa[i][j]=( v==2 II v==3 ) ? ' * ’ :
else nuovaMappa[i][j]=( v==3 ) ? '* ' :
Configurazione iniziale 1a Generazione 2a Generazione
}
//scambia mappa con nuovaMappa
Un sotto problema ricorrente è il calcolo del numero degli organismi vicini ad una cella <i,j>. È utile riferirsi alla
char[][] tmp=mappa: mappa=nuovaMappa; nuovaMappa=tmp;
figura che segue: )//prossimaGenerazione
Nord
public String toString(){
M ,j-1 i- IJ i-1,j+1 String s="';
Est for( int i=0; i<n; i++ ){
i.j-1 u i.j+1
for( int j=0: j<m; j++ ) s+=mappa[i][j];
i+1,j-1 i+1 ,j i+1,j+1
s+='\n';
Sud )
return s;
Segue un'implementazione della classe GiocoDellaVita inserita in un file omonimo .java. La classe è lasciata }//toString
allo studio individuale.
public static void main( String(] args )j//test
GiocoDellaVita goknew GiocoDellaVita(5,7);
Programma GiocoDellaVita:__________________________________________________________ System.out.prinfln(gol);qol.aggiungiOrganismo(0,2); gol.aggiungiOrganismo(0,5);
public class GiocoDellaVita{ gol.aggiungiOrgamsmo(0,6): gol.aggiungiOrganismoM ,0): gol.aggiungiOrganismo(2,3);
private char [][]mappa; gol.aggiunqiOrganismo(2,5):gol.aggiungiOrganismo(j,0): gol.aggiungiOrganismo(j,4);
System.out.println(gol);
private char [jjjnuovaMappa; qol.prossimaGenerazioneO;
private int n, m; System.out.println(gol);
public GiocoDellaVita( int n, int m ){ qol.prossimaGenerazioneO;
if( n<=0 II m<=0 ) throw new HlegalArgumentExceptionQ; //segnala eccezione se n ed m sono invalidi System.out.println(gol);
}//main
this.n=n; this.m=m; }//GiocoDellaVita
mappa=new char(n][m);
58 59
Capitolo 3 Classi e oggetti

Il metodo viciniQ è stato dichiarato private. Si tratta di un metodo ausiliario, o di uso interno, la cui finalità è public Razionale add( Razionale r ){
facilitare l’implementazione dei metodi della classe. int mcm=(r.DEN*DEN)/mcd(r.DEN,DEN);
int num=(mcm/DEN)‘ NUM + (mcm/r.DEN)‘ r.NUM;
Il costruttore ed il metodo aggiungiOrganismo(i.j) sollevano un’eccezione lllegalArgumentException() se, return new Razionale( num, mcm );
rispettivamente, le dimensioni della mappa o le coordinate della cella <i,j> in cui collocare un organismo sono }//add
illegali. La segnalazione di questa eccezione produce un effetto simile alla chiamata System.exit(-I) in quanto
provoca, di norma, la terminazione del programma. Il meccanismo di gestione delle eccezioni è descritto nel public Razionale sub( Razionale r ){
cap. 7. int mcm=(r.DEN*DEN)/mcd(r.DEN,DEN);
int num=(mcm/DEN)‘ NUM - (mcm/r.DEN)‘ r.NUM;
Una classe Razionale return new Razionale( num, mcm );
Java non dispone nativamente del concetto di numero razionale, come rapporto cioè di due numeri interi. }//sub
Tuttavia, col meccanismo delle classi si può progettare un tipo Razionale unitamente alle quattro operazioni
aritmetiche associate. La classe Razionale genera oggetti immutabili. Ogni operazione aritmetica non modifica public Razionale mul( Razionale r ) { return new Razionale( NUM'r.NUM, DEN'r.DEN ); }//mul
this ma crea e restituisce un nuovo razionale. A tempo di costruzione si controlla che il denominatore non sia
nullo, diversamente si fa terminare il programma (si potrebbe sollevare in alternativa un’eccezione). Un public Razionale mul( int s ) { return new Razionale( NUM's, DEN ); }//mul
eventuale segno negativo del denominatore è sempre trasferito al numeratore. Una frazione è mantenuta
ridotta ai minimi termini, ossia i valori assoluti di numeratore e denominatore sono resi primi fra loro. public Razionale div( Razionale r ) { return new Razionale( NUM’ r.DEN, DEN'r.NUM ); }//div

class Razionale! public static int razionaliCreati(){ return contatore;}


private final int NUM, DEN;
private static int contatore=0; private int mcd( int x, int y ){//pre: x>0 && y>0
do{
public Razionale( int num, int den ){ int r=x%y;
if( den==0 ){ x=y; y=r
System.out.printlnfDenominatore nullo!”); }while( y!=0 );
System.exit(-I); return x;
} }//mcd
if( num!=0 ){
int n=Math.abs( num ), d=Math.abs( den ); public String toString(){
int cd=mcd( n, d ); num=num/cd; den=den/cd; if( DEN==1 ) return '"'+NUM;
} if( NUM==0 ) return "0”;
if( den<0 ){ return ""+NUM+V+DEN;
num *= -1; }//toString
den *= -1;
} protected void finalize(){ contatore-;}
this.NUM=num; this.DEN=den; }//Razionale
contatore++;
} Essendo i campi NUM e DEN final (costanti), si potrebbero benissimo rendere public ed evitare cosi i metodi
public Razionale( Razionale r ){//costruttore di copia accessori getNum() e getDen(). Tuttavia: è sempre buona norma evitare di esporre i dettagli
this.NUM=r.NUM; sull'organizzazione interna di una classe ai suoi clienti, per non introdurre nei clienti eccessive dipendenze.
this.DEN=r.DEN;
contatore++; La classe Razionale soddisfa la proprietà invariante che numeratore e denominatore sono mantenuti primi tra
} loro. Tutto ciò è importante nella prospettiva di dover confrontare per uguaglianza due razionali come 2/3 e
4/6. In virtù dell’invariante, l’uguaglianza potrà essere verificata senza incertezze.
public int getNum(){ return NUM;}
public int getDen(){ return DEN;} La classe Razionale ammette due metodi mul() in overloading il cui uso è chiaramente distinto dal tipo del
parametro. La seconda mul() è utile ad es. per ottenere il razionale opposto di un dato razionale:

r2=r1.mul(-1);

60 61
Capitolo 3^ Classi e oggetti

Ovviamente, in virtù di questa osservazione, la classe potrebbe ammettere la add e tralasciare la sub
(perchè?). Un campo static come contatore è condiviso tra tutti le istanze della classe. Si tratta di una variabile di classe,
anziché una variabile di istanza. Si rifletta che l’uso di una normale variabile di istanza non consentirebbe di
Attenzione: si sottolinea che ai fini del corretto overloading conta solo il numero ed i tipi dei parametri, non il contare gli oggetti razionali. Infatti, una tale variabile, inizializzata a zero, varrebbe sempre 1 dopo la creazione
tipo di ritorno dei metodi. L’overloading è risolto a tempo di compilazione. di un qualunque oggetto. La figura che segue illustra il meccanismo dei campi static:

Di seguito si mostra una classe TestRazionali con un main che legge due razionali r1 ed r2 da tastiera, calcola
i razionali somma, sottrazione, prodotto e quoziente e scrive sul video tutti i razionali in gioco. La classe
TestRazionali è parte del file TestRazionali.java nel quale si suppone collocata anche la classe Razionale.
contatore (campo static)
public class TestRazionali{ è una variabile di classe
public static void main(String []args){
Scanner s=new Scanner( System.in );
System.out.println(“Fornisci un numero razionale"); 2
Razionale
System.out.print(“numeratore=“); int n=s.nextlnt(); contatore
System.out.print(“denominatore=“); int d=s.nextlnt();
Razionale r1=new Razionale( n, d ); Il campo contatore appartiene alla
System.out.printlnfFornisci un altro razionale"); classe Razionale ed è condiviso da
System.out.print(“numeratore=“); n=s.nextlnt(); tutti gli oggetti razionali r1, r2 ...
System.out.print(“denominatore=u); d=s.nextlnt();
Razionale r2=new Razionale( n, d ); Per conoscere il valore del contatore, la classe Razionale rende disponibile il metodo razionaliCreati(). Poiché
System.out.printlnfrl =“+r1 ); questo metodo accede esclusivamente ad un campo static, esso stesso è dichiarato static (si tratta di un
System.out.printlnf'r2=“+r2); metodo avente rilevanza di classe). Essendo un metodo di classe, razionaliCreati() non può fare uso del
Razionale r3=r1 ,add( r2 ); pronome this implicitamente noto ai normali metodi aventi rilevanza di istanza o di oggetto. L'invocazione di un
System.out.println(r1+”+"+r2+"=“+r3); metodo static si può ottenere in due modi:
//esercizio: completare con le altre operazioni sub mul d iv ...
}//main a) attraverso un oggetto:
}//TestRazionali
int quantici ,razionaliCreati();
L’istruzione che segue calcola la somma di tre razionali r1, r2 ed r3 e pone il risultato in r4:
b) tramite la classe (notazione preferita):
Razionale r4=r1.add(r2.add(r3));
int quanti=Razionale.razionaliCreati();
in cui risultano concatenate due invocazioni del metodo add(). Prima si applica add() ad r2 con parametro r3, il
risultato è quindi passato come parametro al metodo add() invocato su r1. A questo punto è chiaro che tutti i metodi della classe Math sono statici, cosi come sono statici i metodi della
classe System etc.
La classe Razionale fa uso di un metodo privato mcd per calcolare il massimo comun divisore con l’algoritmo
di Euclide (si riveda il cap. 1) di due interi positivi. Una versione alternativa di mcd, più sintetica, è costituita dal L'essere static il metodo main() consente all'interprete di comandi di Java di far partire un’applicazione
seguente metodo ricorsivo (si auto-invoca). La ricorsione è approfondita nel cap. 18. lanciando, dopo aver caricato in memoria la classe-applicazione, su di essa appunto il main() senza creare
preliminarmente un oggetto.
private int mcd( int x, int y )(
if( y==0 ) return x; Entità di istanza ed entità di classe
return mcd( y, x%y ); Campi non static come NUM e DEN nella classe Razionale, e metodi come add, sub etc. costituiscono entità
}//mcd (o attributi) d'istanza, dal momento che hanno rilevanza su ogni singolo oggetto. Tali entità presuppongono
l’uso di this.
Entità static
La classe Razionale dispone di un meccanismo per contare il numero di oggetti razionali creati sino ad un Campi static come contatore e metodi come razionaliCreati, costituiscono entità di classe, ossia che hanno
certo istante. A questo proposito è stato introdotto un campo static contatore inizializzato a zero. Ad ogni rilevanza di classe, indipendentemente da qualsiasi istanza. Si rifletta che il loro significato sussiste anche
creazione, il contatore è incrementato. Siccome in Java la creazione degli oggetti è esplicita, mentre la loro quando nessun oggetto Razionale è stato ancora creato. Dunque non si basano su this. La tabella che segue
rimozione è implicita ed affidata al garbage collector, è stato ridefinito il metodo finalize() che è invocato su un riassume i vincoli di accesso dei metodi di una classe.
oggetto garbage giusto prima che il garbage collector ne raccolga la memoria, finalize decrementa il contatore.
62 63
Capitolo 3^ Classi e oggetti

Il metodo può metodo non static metodo static


accedere a? (o di istanza) (o di classe) Similmente, il calcolo di mcm nei metodi add()/sub() può essere direttamente basato sul metodo Mat.mcm().
Resta da risolvere il problema di come automatizzare il riuso di classi come Mat presso tutte le classi che ne
variabili o Si No possono avere bisogno. Risponde a queste esigenze il meccanismo di packaging che verrà presentato più
metodi non avanti in questo capitolo.
static
(o di istanza) Un problema ricorrente e delicato è quello del confronto tra due doublé. Dato il numero limitato di cifre a
variabili o Si Si disposizione (rispetto alla matematica) e l’imprecisione derivante dalla rappresentazione in base 2 (si veda
metodi static anche l'appendice A), è ingenuo confrontare direttamente per uguaglianza due doublé x1 e x2 o interrogare se
(o di classe) x è 0 etc. Meglio è verificare se la differenza (in valore assoluto) tra x1 e x2 è inferiore ad un’assegnata
tolleranza (es. EPSILON=1 .OE-14). Mat può agevolmente rendere disponibile un metodo per il confronto tra
Un metodo static non può invocare direttamente un metodo non static. Tuttavia, esso (es. il main) può creare doublé, che verifica se due doublé sono sufficientemente prossimi al punto da poterli ritenere praticamente
un oggetto della classe, e su questo oggetto invocare un metodo di istanza. uguali. Vediamo come:

Progetto di classi di utilità package poo.util;


Una classe di utilità (es. Math, System etc.) contiene di norma solo metodi statici che risolvono sotto problemi public final class Mat{
ricorrenti. Ad es., il calcolo del massimo comun divisore e del minimo comune multiplo può essere rimosso private Mat(){}
dalla classe Razionale e reso disponibile in una classe di utilità che esporta appunto i metodi/servizi private static doublé EPSILON= 1.OE-14;
mcd()/mcm() etc. Detta Mat una tale classe, il suo progetto è schematizzato di seguito. Mat utilizza la classe di public static boolean sufficientementeProssimi( doublé x1, doublé x2 ){
eccezioni HlegalArgumentException() (maggiori dettagli nel cap. 7). if( Math.abs( x1-x2 )<=EPSILON) return true;
return false;
package poo.util; }//sufficientementeProssimi
public final class Mat{
private Mat(){} public static doublé getEpsilon(){ return EPSILON,}
public static int mcd( int n, int m ){ public static void setEpsilon( doublé EPSILON ){Mat.EPS/LO/V=EPSILON;}//setEpsilon
if( n<=0 II m<=0 ) throw new lllegalArgumentException(); ... mcd, m cm ,...
return mcd_euclide( n, m ); }//Mat
}//mcd
private static int mcd_euclide( int n, int m ){Un ed m già controllati Una classe di utilità come Mat, analogamente alla classe Math di Java, non richiede di essere istanziata. I suoi
if( m==0 ) return n; servizi (metodi static o di classe) possono essere invocati direttamente sulla classe. Per questa ragione, la
return mcd_euclide( m, n%m ); classe Mat definisce esplicitamente il metodo costruttore di default con un corpo vuoto, e lo rende privato.
}//mcd_euclide Come conseguenza i clienti della classe non possono creare oggetti-istanze di Mat.
public static int mcm( int n, int m ){
if( n<=0 II m<=0 ) throw new IHegalArgumentExceptionQ; Caso di studio: una classe Data
return (n*m)/mcd_euclide(n,m); Di seguito si riporta una classe Data utile per rappresentare date temporali. L'anno zero corrisponde a quello
}//mcm della nascita di G. Cristo. Data costituisce un ulteriore esempio di classe che, come Razionale, genera oggetti
immutabili.
}//Mat
import java.util.*;
A questo punto, il costruttore della classe Razionale anziché basarsi sul metodo locale mcd() (che può essere public class Data{
rimosso) può invocare l’omonimo metodo di Mat: public static final int GIORNO=0, MESE=1, ANNO=2;
private final int G, M, A;
public Razionale( int num, int den ){
if( den==0 ){ System.out.println("Denominatore nullo!"); System.exit(-I);} public Data(){ //inizializza this col giorno di oggi
if( num!=0 ) { int n=Math.abs( num ), d=Math.abs( den ); GregorianCalendar gc=new GregorianCalendar();
int cd=Mat.mcd( n, d ); num=num/cd; den=den/cd; G=gc.get( GregorianCalendar.DAY_OF_MONTH );
} M=gc.get( GregorianCalendar.MONTH )+1;
if( den<0 ){ num *= -1; den *= -1;} A=gc.get( GregorianCalendar.YEAR );
this.NUM=num; this.DEN=den; }//Data
contatore++;
}
64 65
Capitolo 3 Classi e oggetti

public Data( int g, int m, int a ){ public Data giornoPrima(){


if( m<1 II m>12 II g<1 II g>durataMese(m,a) Il a<0 ) //lasciato come esercizio
throw new HlegalArgumentException(); return nuli; //provvisorio
this.G=g; this.M=m; this.A=a; }//giorno Prima
}//Data
public int distanza( Data d ){
public Data(Data d){ G=d.G; M=d.M; A=d.A; }//Data //ritorna la distanza in giorni tra this e d
//lasciato come esercizio
public static boolean bisestile( int a ){//servizio indipendente da this return -1;//prowisorio
if( a<0 ) throw new IHegalArgumentException(); }//distanza
//definizione generale di anno bisestile: divisibile per 4, ma quando
//è fine secolo dev'essere divisibile anche per 400 public String toString(){
if( a%4!=0 ) return false; return G+V+M+Y+A;
if( a%100==0 && a%400!=0 ) return false; }//toString
return true;
}//bisestile public static void main( String []args ){//solo per test
Data d=new Data();
public static int durataMese( int m, int a ){//servizio indipendente da this System.out.println("Oggi e’ il “+d);
if( m<1 II m>12 II a<0 ) throw new IHegalArgumentException(); System.out.println("Domani e' il "+d.giornoDopo());
int durata; d=new Data( 28, 2, 2000 );
switch( m ){ System.out.println("ll giorno dopo il "+d+" e' il "+d.giornoDopo());
case 1: case 3: case 5: case 7: case 8: d=new Data( 28, 2,2008 );
case 10: case 12: durata=31; break; System.out.println(“ll giorno dopo il "+d+" e' il "+d.giornoDopo());
case 2: durata= bisestile(a) ? 29:28; break; d=new Data( 28, 2, 2009 );
default: durata=30; System.out.printlnCII giorno dopo il "+d+" e' il “+d.giornoDopo());
}//switch if( Data.bisestile(2008) )
return durata; System.out.printlnCII 2008 e' un anno bisestile");
}//durataMese System.out.printlnfGiorno di “+d+" = "+d.get(Data.GIORNO));
System.out.println("Mese di ”+d+" = "+d.get(Data.MESE));
public int get( int cosa ){ System.out.println("Anno di "+d+" = “+d.get(Data.ANNO));
//se cosa non ha senso, si ritorna -1 }//main
switch( cosa ){ }//Data
case GIORNO: return G;
case MESE: return M; Si nota che il costruttore di default è stato definito in modo da creare un oggetto Data corrispondente al giorno
case ANNO: return A; corrente. Le informazioni della data odierna sono ottenute a partire dalla classe GregorianCalendar disponibile
default: return -1; nel package java.util. Siccome in GregorianCalendar i mesi sono numerati da 0 a 11, si corregge il mese
} ottenuto sommandogli 1. La proprietà di anno bisestile è verificata in un metodo di servizio (static) che riceve
}//cosa un anno e restituisce true se l’anno è bisestile. In generale, un anno è bisestile se è divisibile per 4. Tuttavia,
se trattasi di fine secolo, esso è bisestile se è divisibile anche per 400.
public Data giornoDopo(){
int durata=durataMese(M,A); La classe Data è dotata di un main di prova, al fine di testarne il funzionamento. Tutte le classi di Java
int g l, m i, a l; possono contenere un metodo main per scopi di verifica. In questo modo la classe Data è pubblica ed è
if( G==durata ){ l’unica classe del file Data.java.
g i= i;
if( M==12 ) { m1=1; a1=A+1;} Esperimenti casuali
else{ m1=M+1; a1=a;} Si propone una classe Monetina con un campo intero faccia, due costanti pubbliche TESTA e CROCE
} (inizializzate rispettivamente a 0 ed 1) e i metodi:
else{ g1=G+1; m1=M; a1=A;} • costruttore di default (senza parametri) che lancia inizialmente la monetina per impostare la faccia
return new Data( g1,m1,a1 ); • lanciai) che fissa casualmente la faccia della monetina (tra TESTA e CROCE)
}//giorno Dopo • getFaccia() che ritorna la faccia corrente della monetina
• toStringQ che ritorna come stringa la faccia corrente della monetina
66 67
Capitolo 3 Classi e oggetti

Una classe Monetina:_______________________________________________________________________ Il linguaggio Java risulta già dotato di un’ampia libreria di classi testate, la cui conoscenza, in molte situazioni
ciass Monetina{ pratiche, può consentire di evitare di introdurre proprie classi con funzionalità simili:
public static final int TESTA=0, CROCE=1;
private int faccia; se esiste una classe di libreria che risponde alle esigenze dell'utente, è sempre conveniente riutilizzare questa
public Monetina(){ lancia!); }//costruttore di default classe.
public void lancia(){
faccia=(Math.random()<0.5) ? TESTA : CROCE; Anche il programmatore può costruirsi proprie librerie. Risponde a questo scopo il meccanismo dei package.
}//lancia Un package corrisponde ad una directory del file System.
public int getFaccia(){ return faccia;}
public String toString(){ In tutti i programmi precedenti in cui si è ignorato il meccanismo di packaging, in realtà si è fatto uso del
return (faccia==TESTA)? “testa”:”croce"; cosiddetto package di default o package anonimo che si valuta implicitamente alla directory corrente in cui si
}//toString sta lavorando. Ad es. avendo posto i primi programmi in c:\primi_programmi, si è fatto tacitamente uso del
}//Monetina package di default “primiprogrammi”. L’uso del package di default permette di porsi nella directory di lavoro
con una Shell, e di editare (utilizzando un qualsiasi editor di testi, es. notepad di Windows) compilare ed
Similmente a quanto fatto nella classe Data, si esportano i dati interi TESTA e CROCE. La soluzione è eseguire programmi a riga di comando. Da questo momento in poi si suggerisce di sistemare le classi in file
perfettamente accettabile in quantoTESTA e CROCE sono costanti (final int). Si tratta in particolare di costanti separati e di collocare tali file in sotto directory distinte di una directory di progetto.
static, aventi cioè rilevanza di classe. Nessun oggetto monetina dispone di una propria copia di TESTA e
CROCE, ciò che costituirebbe uno spreco inutile dimemoria. Il fenomeno casuale di lancio di una monetina si Si definisce directory di progetto o directory di riferimento o directory base o semplicemente “anchor pomf,
appoggia banalmente al metodo Math.random(). una qualsiasi directory del file System nella quale si intende sviluppare e conservare classi Java.

Un programma basato su Monetina:____________________________________________________________ Per esemplificare, di seguito si fa riferimento alla directory di progetto c:\poo-java al cui interno si crea la sotto
Il programma che segue crea due monetine m1 ed m2 e le lancia ripetutamente sino a che una delle due non directory poo contenente quindi sotto directory quali: geometria, razionali, date, esempi, util etc. che ospitano
realizza tre teste consecutive. Il programma visualizza le monetine dopo ogni lancio e quindi fornisce classi “affini" tra loro, ossia che svolgono compiti correlati.
l’indicazione della monetina vincente o della situazione di parità. Si utilizzano due contatori contai e conta2
che sono incrementati quando esce testa e resettati quando esce croce. c:\poo-java\
poo\
public static void main( String [largs ){Iles. appartenente alla classe Monetina - geometria\
final int OBIETTIVO=3; Punto.java. Triangolo ja v a ,...
Monetina m1=new Monetina!); - razionali
R azionale.java,...
Monetina m2=new Monetina!);
int conta 1=0, conta2=0; - date\
D ata.java....
//esperimento casuale
- giochi\
while( contai<OBIETTIVO && conta2<OBIETTIVO ){ Monetina.java,
m1.lancia!); m2.lancia();
System.out.println("m1 ="+m1);
System.out.println(“m2 = "+m2);
conta 1=(m1.getFaccia()==m1.TESTA) ? contai+1 : 0;
conta2=(m2.getFaccia()==m2.TESTA) ?conta2+1 :0; Convenzione: si suggerisce di designare le directory con nomi minuscoli; in presenza di nomi composti si può
}//while utilizzare l’underscore o il segno
if( contai<OBIETTIVO ) System.out.println(“m2 vince!");
else if( conta2<OBIETTIVO ) System.out.printlnfml vince!"); Direttiva package
else System.out.printlnfParità!"); Per riutilizzare una classe è importante collocarla in una sotto directory della directory di progetto e specificare
}//main esplicitamente questa posizione nel file come segue:

Librerie di classi riutilizzabili e packaging package poo.geometria;


Sino a questo momento, i programmi Java sono stati creati all’intero di file che contengono uno o più classi di public class Punto{...}
cui quella pubblica è dotata del metodo main.
La direttiva package va collocata nella prima linea non commento del file.
Per facilitare il riutilizzo delle classi, e dunque per migliorare l'attività di programmazione e renderla più
ingegneristica, è fondamentale mettere a punto librerie (o meglio biblioteche) di classi riusabili. Una classe massimamente riutilizzabile è programmata come classe pubblica. In modo analogo:
68 69
Capitolo 3 Classi e oggett)

package poo.geometria; Nell’ipotesi che TR voglia avvalersi anche dei servizi della classe di utilità Mat, si può operare come segue:
public class Triangolo!...}
import poo.razionali.Razionale;
L'appartenere allo stesso package poo.geometria fa si che nel file Triangolo.java si possa utilizzare senza altri import poo.util.Mat;
accorgimenti la classe Punto. public class TR{...}

La variabile di ambiente classpath Ciascuna linea di importazione specifica una classe. Si consideri ora una classe ipotetica
Il nome del package cui appartengono le classi Punto e Triangolo è poo.geometria ed è in relazione diretta CalcoliGeometrici.java di poo\esempi che deve utilizzare Punto, Triangolo, Poligono, Cerchio etc. di
con la strutturazione in sotto directory della directory di progetto. Un ‘7’ nel nome del package corrisponde a poo.geometria. Oltre che poter specificare (buona norma) ciascuna singola importazione, si può procedere
una T nella definizione del percorso del file. anche con l’uso deH’importazione aperta, ossia con wildcard (carattere jolly) come segue:

Diciamo Client una qualsiasi classe che necessita di utilizzare una classe di libreria come Punto, Triangolo, import poo.geometria.*;
Razionale etc. Una classe Client, ovviamente, può essere collocata ovunque nel file System. È necessario public class CalcoliGeometrici{...}
quindi fornire informazioni al compilatore (javac) e all'interprete (java) circa il posizionamento delle classi di
libreria che si intendono utilizzare. Risponde a questo scopo la variabile di ambiente classpath che va Questa importazione fa sì che CalcoliGeometrici possa avvalersi di una qualsiasi classe del package
utilizzata in aggiunta ed indipendentemente dalla variabile di ambiente path. Il valore di classpath è una lista di poo.geometria. Tuttavia nessun aggravio è recato al file, nel senso che le classi di poo.geometria non
directory o file jar, separati da e senza spazi. Ogni directory è una directory di progetto o anchor point. utilizzate non causano alcun incremento della dimensione del file CalcoliGeometrici.java.
Attenzione: nel classpath vanno posti solo anchor point o directory base, es.
Compilazione/esecuzione di programmi in presenza di package
...;c:\poo-java;... Ci si porta nella directory di progetto (es. c:\poo-java). Per compilare un file si utilizza il percorso relativo a
partire dall’anchor point di lavoro, es:
Esempio di settaggio di classpath:_____________ _____ ____________________
c:\poo-java>javac poo\geometria\Triangolo.java INVIO
Modifica variabile utente
Per eseguire un programma, es. per mandare in esecuzione il main di test di cui è dotato Triangolo:

N o m e v a r ia b ile : CLASSPATH c:\poo-java>java poo.geometria.Triangolo INVIO

V a lo r e v a r ia b ile : > s e - v ,o r k s p a c e V s s tr a - j a d e 'b m ; c : y > o o - ja v a ^ Si nota che il compilatore richiede l’uso del percorso relativo con i separatori Y , mentre l’interprete vuole
esattamente la specificazione del package come preambolo del nome del file (.class) contenente il main.
OK A n n u lla
Per automatizzare le operazioni di compilazione ad es. di gruppi di file e di esecuzione di un’applicazione
specifica, si possono predisporre due file batch, es. compila.bat e run.bat, da mettere nella directory di
progetto. Per creare questi file si può, in una Shell, procedere come segue:
Come per path, le directory di progetto (o più genericamente di libreria) verranno consultate secondo la
successione da sinistra a destra definita dal classpath.
c:\poo-java>copy con: compila.bat INVIO
javac poo\geometria\‘ .java INVIO compila tutti i file in geometria
Importazioni di classiiS
javac poo\esempi\*.java INVIO compila tutti i file in esempi
Si consideri una classe TR contenente il main, posta nella sotto directory esempi di poo, che deve utilizzare la pause aspetta il consenso dell’utente
classe Razionale. TR è una classe applicativa. Essa stessa può basarsi sul package di default (ossia non è CTRL-Z INVIO completa la creazione
necessario che TR contenga la direttiva package). Per accedere a Razionale deve importare la classe come
segue: Similmente:
c:\poo-java>copy con: run.bat INVIO
import poo.razionali.Razionale; java poo.esempi.CalcoliGeometrici INVIO manda in esecuzione CalcoliGeometrici.class
public class TR{. pause aspetta il consenso dell'utente
CTRL-Z INVIO completa la creazione
La clausola di importazione specifica il package di appartenenza della classe Razionale, ma omette la
specificazione dell’anchor point. Gli anchor point (directory di progetto o file jar) sono elencati nel classpath. La modalità di sviluppo discussa sopra, che prevede di collocarsi in una specifica directory di progetto, in
Mentre può esistere una ed una sola linea di package (necessariamente la prima linea del file), più linee di realtà non abbisogna del classpath se le classi importate sono parte di package che corrispondono a sotto
importazione possono essere presenti in dipendenza delle classi da utilizzare. directory della directory di progetto.
70 71
Capitolo 3 Classi e oggetti

La collocazione di tali package nel file System è definita in sede di installazione del JDK, ed è nota al
Gli strumenti di Java, es. javac/java, automaticamente discendono in profondità nelle sotto directory della compilatore e all’interprete di Java, per cui non occorre preoccuparsi di mettere gli anchor point della libreria di
directory di progetto. Cosi, riferendo una classe identificata come poo.razionali.Razionale, il compilatore Java nel classpath.
innanzi tutto cerca se esiste una sotto directory poo della directory di progetto, quindi se essa esiste, la sotto
directory poo\razionali sino alla classe Razionale. Stante la relativa frequenza d’uso, il package java.lang che assiste logicamente l'uso del linguaggio, è
importato implicitamente, dunque mai serve scrivere in un proprio programma: import java.lang.*;
Il classpath diventa indispensabile allorquando la ricerca di cui al passo precedente fallisce: vale a dire che
nella directory di progetto corrente non esiste poo o pur esistendo non esiste in essa la sotto directory Importazione statica (Java 5 o versioni superiori)
razionali etc. Per classi come java.lang.Math costituite da diversi metodi statici, è possibile specificare l'importazione statica
dei metodi:
In altri termini, il classpath è consultato quando si compilano/eseguono programmi daH’interno di una directory
di progetto e si importano classi appartenenti a package esterni, che sono parte cioè di altre directory di import static java.lang.Math.*;
progetto. In questi casi, gli strumenti di Java, per risolvere le classi, attingono al classpath cercando,
nell’ordine, nelle directory di progetto elencate nel classpath. in questo modo diventa possibile riferirsi ai nomi dei metodi di Math senza il prefisso di classe:

Solo quando la ricerca fallisce definitivamente, il compilatore o l'interprete segnalano che non è stato possibile doublé x=(-b+sgr/(pow(b,2)-4*a*c))/(2*a)*S
i
trovare una certa classe.
Ambiente di sviluppo Eclipse ( c e n n i ) _______ _______
Conflitti e risoluzione Si discutono brevemente le modalità di sviluppo programmi in Java quando si utilizza un ambiente integrato
E chiaro da quanto precede, che più file con lo stesso nome (es. Triangolo.java) possono esistere in diverse come Eclipse. Eclipse semplifica le cose nascondendo diversi dettagli. Si sottolinea, tuttavia, che esistono casi
directory di progetto e in diversi package. Il meccanismo di packaging, infatti, serve a stabilire spazi di nomi in cui occorre uscire da Eclipse e compilare/eseguire manualmente, per cui le considerazioni precedenti sono
(namespace) e regole per accedere a questi nomi, favorendo la generalità di definizione dei nomi. Si consideri fondamentali.
il seguente esempio di importazione:
Concetti base in Eclipse sono workspace e progetto. Un workspace (spazio di lavoro) in pratica è una
import java.util.*; directory collocata in un punto qualsiasi del file System. Un progetto Java è una sotto directory di un
import sde.timer.*; workspace. Un progetto, in sostanza, corrisponde ad una directory di progetto innestata nel workspace.
All’intemo di un progetto sono poi annidati i package che lo definiscono. All’interno dei package sono inserite
Entrambi i package java.util e sde.timer definiscono una classe Timer. A questo punto se nella classe Client è le classi etc.
presente una dichiarazione del tipo: Timer t=new Timer(...); non è chiaro a quale classe ci si riferisca, se a
quella di java.util o a quella di sde.timer. Si ha una situazione di conflitto di nomi (clash), la cui risoluzione In Eclipse la compilazione del file corrente nell’editor, è automatica. Dopo ogni modifica, Eclipse ricompila.
spetta al programmatore, ad es. utilizzando l'importazione risoluta di singole classi dai vari package: L’esecuzione va richiesta espressamente.

import java.util.Scanner;... Lavorando con Eclipse, l’uso del classpath rimane completamente trasparente e l’utente non deve fare nulla al
import sde.timer.Timer; riguardo. Sino a che lo sviluppo utilizza package interni ad uno stesso progetto, per le ragioni spiegate in
precedenza, si può ignorare il classpath.
o l'importazione qualificata:
Le cose cambiano quando si desidera importare classi in un progetto, ma tali classi appartengono a package
import java.util.*; di progetti diversi da quello corrente. Anziché ricorrere al classpath, si generano file jar corrispondenti ai
progetti contenenti package importati e si includono tali jar come librerie nel progetto Client che ne richiede
sde.timer.Timer t=new sde.timer.Timer(...); l’uso (Build-path). Le librerie incluse rappresentano di fatto il classpath da consultare per il progetto corrente.

che specifica la classe qualificandola col package di appartenenza. Passaggio di parametrijii metodi ______________ ______________________ __________________
Si è già anticipato nel cap. 1 che un parametro formale può utilizzare il passaggio per valore (o per copia) o il
Libreria di Java passaggio per variabile (o per riferimento). Segue un esempio C++:
Le classi della libreria di Java sono raccolte in package quali:
- java.lang (contiene le classi fondamentali quali Math, String, System, etc.), void calcola( int x, float &y ){...};
- java.util (contiene classi quali Scanner, le classi collezioni che saranno studiate nel seguito, etc.)
- java.io (contiene le classi per i flussi e i file) x è un parametro formale affetto dal passaggio per valore; y è un parametro formale che segue il passaggio
- java.net (contiene le classi per l'internetworking) per riferimento.

Si consideri ora l’invocazione:


72 73
Capitolo 3 Classi e oggetti

passare per valore un riferimento e dunque l’effetto (con gli oggetti) è simile al passaggio per variabile. Si
oggetto.calcola(a,b); consideri l’invocazione di un metodo m(Punto p, int v) con gli argomenti pO e z come in figura:

in cui a è int e b è float: a e b sono detti argomenti o parametri attuali (o effettivi).

am biente
chiam ante 2.5 b
(es. in a ili)

rife rim e n to

am biente — t ■

m e tod o 3
c u lio lii
Se ora si esegue p.sposta(10, 20) nel corpo del metodo m, cosa accade esattamente?
int calcola( int x, float &y )
Il messaggio p.sposta( 10,20) invocato all’interno del metodo m, cambia lo stato di p e di pO (p e pO sono in
aliasing). A seguito della terminazione del metodo, il punto pO risulterà spostato nel piano XY rispetto a dove si
x riceve il valore di a. Dopo questo, ogni uso di x in calcolai...), in particolare ogni modifica di x, cambia solo x trovava prima della chiamata (effetto collaterale o side-effect).
ma non la variabile a.
Parametri attuali
y riceve la variabile (l’indirizzo di) b. Ogni uso di y equivale ad un uso di b. Dunque se calcola(...) modifica y, la Il parametro attuale corrispondente ad un parametro formale valore può essere in generale un’espressione
modifica si ripercuote direttamente su b. (dunque include come casi particolari una costante, una variabile etc.), es.:

Si comprende che con i parametri variabili è possibile modificare l’ambiente chiamante, ossia restituire (al di là o.calcola(a‘ 2+t, b);
del meccanismo della return) uno o più “risultati" al chiamante:
Il parametro attuale di un parametro formale variabile deve essere sempre una variabile.
a 3 0 b
ambiente
chiamante
Nozione di record di attivazione o trame
(es main) L'insieme delle locazioni di memoria associate ai parametri e alle variabili locali di un metodo definisce un’area
di memoria detta record di attivazione (RA) o trame o semplicemente area dati del metodo. Il record di
attivazione è creato al tempo di invocazione del metodo e rimosso al tempo di uscita del metodo. I record di
attivazione sono allocati in cima allo stack (comportamento UFO o “a pila di piatti") del programma Java. Gli
oggetti, invece, sono allocati nell’heap (memoria a “mucchi") del programma Java. Di seguito si schematizza il
metodo modello di memoria relativo al metodo void m( Punto p, int v ) invocato dal main coi parametri attuali pO e z:
calcola x 24 y
dopo x++ dopo y=0
oggetto Punto heap

ambiente
chiamante
(es. main)

Al termine dell’invocazione di calcola(a,b); la variabile a risulta immutata, b invece è cambiata:

Cosa succede in Java ? ___ _______________


Java non dispone sintatticamente di un simbolo come & per specificare parametri variabili. In Java tutti i
RA di m(...)
parametri sono passati per valore. Tuttavia, siccome gli oggetti sono riferimenti, passare un oggetto equivale a stack
Record di Attivazione (RA)
del main
74 75
Capitolo 3 Classi e oggetti

Mentre per un approfondimento del modello di memoria dei programmi Java si rinvia a più avanti nel corso, Ancora sui costruttori di una classe
qui si nota che al tempo di uscita dal metodo m si rimuovono dalla cima dello stack le locazioni del RA di m. A È possibile progettare una classe senza costruttori espliciti. In questo caso, Java fornisce automaticamente un
questo punto la cima dello stack contiene il RA del main (chiamante). costruttore di default (senza parametri) che si limita ad inizializzare le variabili secondo una regola molto
semplice: un int è inizializzato a 0, un boolean a false, un oggetto a nuli etc. (inizializzazione di defaulf).
La gestione degli oggetti nell’heap, invece, è legata all’esistenza di riferimenti. Sino a che esiste una variabile
oggetto (sullo stack) che punta ad un oggetto nell’heap, quest’ultimo oggetto permane. Quando cessano tutti i Tuttavia, se l'utente introduce almeno un costruttore esplicito, il linguaggio non fornisce più il suo costruttore di
riferimenti all'oggetto, la sua memoria viene raccolta dal garbage collector ed il buco si rende disponibile per default. In questi casi, se utile, il costruttore di default va espressamente progettato ed inserito a cura del
una nuova allocazione. programmatore. La classe

Nota: il ricevitore di un messaggio (this) è passato implicitamente come extra parametro e fa parte dell’area class Coppia{
dati di un metodo, anche se ciò non viene evidenziato. int x;
float y;
Altri esempi di passaggi di parametri
Il metodo C++ che segue scambia i valori di due variabili (parametri attuali):
che potrebbe essere una classe ausiliaria aH’interno di un package, è dotata del solo costruttore di default
void scambia( int &x, int &y ){ fornito da Java. È possibile, pertanto, creare e manipolare oggetti di classe Coppia:
int park=x;
x=y; Coppia c=new CoppiaQ; //invoca il costruttore di default “standard”
y=park;
}//scambia c.x=0;...

Tracciare un modello di memoria relativo al caso in cui il metodo è invocato con le variabili int s (che vale 2) e La classe
int t (che vale -15) e verificare l’effettivo scambio dei valori di s e t. Con riferimento al metodo Java:
class AltraCoppia{
void scambia( Punto p1, Punto p2 ){ int x; float y;
Punto park=p1; public AltraCoppia( int x, float y )(
p1=p2; this.x=x; this.y=y;
p2=park; }
}//scambia

nell’ipotesi che esso venga invocato da un main che crea due oggetti punto p (di coordinate 2 e 5) e q (di non specifica un costruttore di default ma solo uno “normale". Non è più possibile contare sul costruttore di
coordinate -3 e 7), verificare graficamente col modello di memoria l’effetto della chiamata scambia(p.q). default fornito da Java:

“Buon comportamento^sui parametri float z=...;


Poiché in Java i parametri formali di un metodo sono affetti esclusivamente dal passaggio per valore, essi si AltraCoppia c=new AltraCoppia( 3, z ); //ok
comportano esattamente come variabili locali inizializzate al tempo della chiamata del metodo. AltraCoppia c1=new AltraCoppia(); //errore

I parametri formali ricevono come valori iniziali i valori dei parametri attuali. Regole di visibilità dei nomi______________________________________ _____
Dicesi blocco una zona di testo di un programma normalmente racchiusa in una coppia { }. Il corpo di un
Per motivi di chiarezza e per non perdere i valori iniziali dei parametri, può essere opportuno in molti casi metodo, il testo di una classe etc. sono esempi di blocchi. È anche blocco il corpo di un for, pur se costituito
"battezzare" final i parametri formali in modo da impedire accidentali modifiche agli stessi. Nel caso si desideri da una singola istruzione non avviluppata tra { e }.
modificare i valori ricevuti, è sufficiente introdurre delle variabili locali come segue:
Un blocco può introdurre dichiarazioni locali di nomi di variabili o di metodi (in realtà solo un blocco di classe
public int m( final int x, final Punto p ){ ammette metodi). In Java le dichiarazioni possono essere specificate dove servono, dunque giusto prima del
int y= x;...; y+=p.getY();... loro uso. Valgono le seguenti regole di definizione del campo di validità dei nomi:
}//m
Regola 1: un nome introdotto in un blocco B1 corpo di una classe è visibile in tutto il blocco B1 e penetra
Si osserva che final protegge il valore del parametro formale ma non l’oggetto da esso puntato (come nel caso automaticamente in un blocco innestato B11 laddove l’entità risulta visibile a meno di non essere mascherata
del parametro formale p). da un nome identico introdotto da B11.

76 77
Capitolo 3 Classi e oggetti

Regola 2: un nome di un locale (parametro o dato locale) introdotto in un blocco-corpo di un metodo, è visibile
nel blocco dal punto di dichiarazione sino alla fine del blocco. Non possono esistere due o più locali con lo Esistono altri modificatori (protected e [implicito) package) che impattano ancora sul campo d’azione dei nomi
stesso nome (unicità delle dichiarazioni locali), anche in blocchi interni innestati nel metodo (si veda la figura di una classe. Di essi si parlerà più avanti nel testo.
che segue).
r- bi , Argomenti variabili (vararg)
x, y Le entità di B l, cioè x e y, A partire dalla versione 5, Java consente di programmare metodi il cui numero dei parametri è variabile. Tutto
penetrano automaticamente nei
ciò si ottiene utilizzando i tre punti... Questa possibilità, se presente, deve essere l’ultima parte della sezione
y blocchi innestati B l 1, B2 e B12,
dove sono visibili ed utilizzabili dei parametri di un metodo. Java raccoglie i parametri variabili, tutti dello stesso tipo, in un array e rende
82 quest'ultimo disponibile al metodo come indicato nell’esempio che segue:
X
Un blocco più interno come B ll o B2
non può mai ridichiarare un locale di public class Demo{
B l (o di B ll) , se B l è il corpo di un public static void main( String []args ){
metodo
B12 int m1=max( 1,2,3 );
int m2=max( 10, 2, 30, 40 );
System.out.println(,m1="+m1+'' m2="+m2 );
}
public static int max( in t... x ){Ila tutti gli effetti: int []x
Si capisce che un nome (static o non static) introdotto in una classe è automaticamente visibile nei metodi int m=x[0);
della classe a meno di mascheramenti. for( int i=1; kx.length; i++ ) if( x[i]>m ) m=x[i);
return m;
Il nome di un locale di un metodo, non può essere ridefinito in nessun caso aH'interno del metodo. }//max
}//Demo
Il mascheramento di una variabile della classe può essere risolto, in un metodo, come segue. Ad esempio, se
una variabile d’istanza si chiama x e ci si trova in un metodo che introduce un parametro o un dato locale x, Anche l'intestazione del metodo main può essere riscritta con un vararg di String: void main( String ... args ){}.
l’utilizzo non qualificato del nome x si riferisce al dato del metodo (nome più recentemente introdotto); la
notazione this.x consente, invece, di riferirsi alla x campo della classe. Se la variabile è static, allora il System.out.printf e vararg
mascheramento può essere risolto con la notazione NomeClasse.nome statico. Il metodo System.out.printf fa uso di un numero variabile di argomenti. La sua intestazione è:
Esempio: static void printf( String s, Object... args );
class C{ in cui la stringa s fornisce le informazioni di formato, i rimanenti oggetti (in numero variabile) rappresentano le
int x=5; entità da stampare, args è un array di Object anche quando l’utente specifica dati di tipi primitivi. In questi casi
il compilatore Java esegue automaticamente l'autoboxing (si veda il cap. 8) ossia trasforma i dati primitivi in
void m( int x ){ oggetti utilizzando le classi wrapper Integer, Doublé etc.
int z=x+this.x; //risoluzione del mascheramento mediante this
float w=z+y;... Enumerazioni
} L’utilizzo di costanti come TESTA e CROCE nella classe Monetina, o di GIORNO, MESE ed ANNO nella
float y=3.4;
classe Data etc., si accompagna in generale a potenziali situazioni di errore dal momento che eseguendo
}//C
aritmetica su tali costanti si può ottenere un valore non ammissibile.
L’uso non qualificato di x in m si riferisce al parametro. La notazione this.x consente esplicitamente di riferirsi
A partire dalla versione 5 di Java, sono state introdotte le enumerazioni ossia classi implicite di cui vengono
alla variabile di istanza. La y, pur essendo dichiarata alla fine della classe, è comunque visibile in tutta la
enumerati tutti e soli i possibili oggetti (o istanze) che possono esistere della classe. Le enumerazioni sono utili
classe e dunque anche nel metodo m. in tutte quelle situazioni in cui si vuole esprimere un numero finito di possibilità.
Classi e visibilità globale Le direzioni cardinali (NORD, NORD .EST, EST, .... NORDJDVEST), i giorni della settimana (LUNEDI, ....
In virtù dell’esportazione public di una entità e (variabile, costante, metodo) introdotta in una classe C, diventa DOMENICA), i mesi dell’anno etc. sono altri esempi in cui sono in gioco costanti che non dovrebbere avere usi
possibile allargare il campo di azione di e dalla singola classe C a tutte le classi che importano C. Si dice, diversi da quello di specificare appunto le direzioni, i giorni etc. Fare aritmetica su questi valori è
pertanto, che un attributo public ha visibilità globale. semanticamente scorretto. Con le enumerazioni viene imposto il “buon comportamento” rendendo impossibili
le operazioni aritmetiche di cui si è detto. La sintassi è chiarita di seguito:
Il modificatore private impedisce la visibilità globale in quanto restringe il campo di azione di un attributo alla
sola classe che lo dichiara. public enum Esiti (TESTA, CROCE);
78 79
Capitolo 3 Classi e oggetti

L'enumerazione Esiti è di fatto una classe di cui TESTA e CROCE sono le due uniche istanze ammesse. Per Il simbolo j è l'unità immaginaria. Per definizione j 1 = -1 . Un numero complesso corrisponde ad un vettore nel
riferirsi alle costanti, si scrive Esiti.TESTA o Esiti.CROCE. Segue una nuova versione della classe Monetina. piano complesso (asse reale delle ascisse e asse immaginario delle ordinate).
package poo.giochi; Si dice modulo del numero complesso z la norma del vettore, ossia la sua lunghezza:
• public class Monetina{
public enum Esiti (TESTA, CROCE};
private Esiti faccia;
public Monetina(){ lancia();}
public void lancia(){ Un numero complesso degenera in un numero reale se la parte immaginaria è nulla. Si dice complesso
faccia=(Math.random()<0.5) ? Esiti.TESTA : Esiti.CROCE; coniugato di un numero z il numero complesso avente la stessa parte reale di z ma opposta parte
}//lancia immaginaria. Per - = 4 + j5 il complesso coniugato è: ~ = 4 - >5.
public Esiti getFaccia(){ return faccia;}
public String toString(){ Detti z1=-3+j5, z2=4-j7, il numero complesso somma di z1 e z2 si ottiene sommando algebricamente
return (faccia==Esiti.TESTA)7 "testa":"croce"; rispettivamente le parti reali e le parti immaginarie:
}//toString
}//Monetina z 1+z2=(-3+j5)+(4-j7)= 1-j2

Concetto di bean Similmente si costruisce il numero complesso differenza:


Una classe Java si dice bean se tutti i suoi attributi dati (detti talvolta anche proprietà) dispongono dei
corrispondenti metodi get/set. Per convenzione, se il nome dell’attributo è x, il metodo accessore di z 1-z2=(-3+j5)-(4-j7)=-3+j5-4+j7=- 7+j 12
consultazione dovrebbe chiamarsi getX() ed il metodo mutatore setX( nuovo_valore_dLx ). Si capisce che per
un bean potrebbe essere fornito solo il costruttore di default. In un ambiente integrato di sviluppo la Il numero complesso prodotto si costruisce agevolmente moltiplicando i due polinomi e poi ricordando la
generazione dei metodi get e set di una classe può essere automatica. definizione di unità immaginaria:

Esercizi ____________________ _________ z 1‘z2=(-3+j5) ‘(4-j7)=- 12+j21+j20-f35=- 12+j41+35=23+j41


1. Nella classe Triangolo sono presenti metodi accessori per conoscere le lunghezze dei lati. Nell’ipotesi che
si aggiungano altri tre metodi accessori getP1(), getP2(), getP3() che restituiscono rispettivamente i tre punti- Il quoziente di due numeri complessi si costruisce come segue:
vertici del triangolo, fornire un’implementazione di questi metodi in modo da evitare problemi di aliasing.
2. Completare e testare la classe Poligono. z\ - 3 + y‘5 ( - 3 + J5)(4 + j l )
3. Modificare la classe Triangolo con una enumerazione Tipo che definisce il tipo del triangolo tra isoscele, :.2 ~ 4 - j l ~ ( 4 - j l )(4 + j l )
equilatero e scaleno. Aggiungere quindi alla classe il metodo public Tipo tipo(){...} che ritorna il tipo di
triangolo costruito. ossia si moltiplica numeratore e denominatore per il complesso coniugato del denominatore. È facile verificare
4. Completare e testare lo sviluppo dei metodi della classe Data.
5. Modificare la classe Data in modo da sostituire le costanti intere pubbliche GIORNO, MESE ed ANNO con che z * z = \z\2, per cui:
una enumerazione Elemento contenente gli stessi nomi come valori. L’enumerazione Elemento consente di
riformulare il metodo get(): int get( Elemento e )(...}. Si rifletta che ora non è più possibile invocare get() con z\ ( - 1 2 - 7 2 1 + /2() + j 235) _ - 1 2 - 7 - 3 5 - 4 7 - j _ _47 _ . 1
un valore illegale dell’elemento della data da ottenere. L’istruzione switch del linguaggio è stata estesa in z2 ~ 41+ 7J 16 + 49 65 65 J 65
modo da ammettere come discriminante un dato di un tipo enumerato, che per definizione è dotato di un
numero discreto di valori. In queste situazioni non sono possibili errori (si riveda il caso in cui in precedenza si La classe Complex dovrebbe rendere disponibili:
restituiva -1 se il parametro cosa aveva un valore intero illegale). • costruttori (normale e di copia)
6. Sviluppare una classe Complex le cui istanze rappresentano numeri complessi. Dalla matematica è noto
• metodi accessori getRe() e getlm() che rispettivamente restituiscono la parte reale e la parte immaginaria
che un numero complesso z è costituito da una parte reale (re) ed una parte immaginaria (im). Un esempio di
del numero complesso
numero complesso è z=4+j5 in cui +4 è la parte reale di z, +5 la parte immaginaria di z.
• doublé modulo() che ritorna il modulo del numero complesso this
• Complex coniugatoQ che ritorna il numero complesso coniugato di this
• Complex add( Complex z )
• Complex sub( Complex z )
• Complex mul( Complex z )
• Complex div( Complex z ), che rispettivamente costruiscono e ritornano il numero complesso somma,
sottrazione, prodotto e quoziente tra this e z.
80 81
Classi e oggetti
Capitolo 3

• Complex mul( doublé s ) che costruisce e ritorna il numero complesso che si ottiene moltiplicando this per System.out.println( java.util.Arrays.toString( a ) );
lo scalare s (si moltiplicano per s tanto la parte reale quanto la parte immaginaria) System.out.println) java.util.Arrays.toString( b ) );
• il metodo toString(). }//main
}//CiriProvo
Progettare la classe e scrivere un main di test che verifichi il funzionamento di tutti i metodi previsti.
tracciare le aree dati e predire l’output generato quando la classe CiriProvo è posta in esecuzione. Si nota che
7. Data la classe: il metodo della libreria di Java Arrays.toString ritorna sotto veste di stringa l’array ricevuto parametricamente,
pronto per essere stampato. Tutto ciò evita in molti casi di programmarsi in proprio un ciclo di stampa.
public class C{
private int x, y;
public C( int x, int y ) { this.x=x; this.y=y;}
public void m( int x ){x += this.x+y;this.y = x-1 ;}//m
public String toString(){ return "x="+x+,,y="+y;}
public static void main( Stringi) args ){
C c=new C(2,7); int a=10; c.m(a);
System.out.println(c);
System.out.println("a=,+a);
}//main
}//C

tracciare le aree dati e predire l’output generato quando C è posta in esecuzione.

8. Data la classe:

public class CiriProvo{


private int[] a;

public CiriProvo( int[] a ){


this.a=new int(a.length);
System.arraycopy( a, 0, this.a, 0, a.length );
}

public void ciProvo( int[] b ){


if( a.length!=b.length ) throw new HlegalArgumentException();
for( int i=0; ka.length; i++ ) a[i] = (a[i]+b[i])%2;
for( int i=0; ka.length; i++ ) bji] = (a[i]==1 && b[i]==1)?1:0;
}//ciProvo

public String toString(){


String s="";
for( int i=0; ka.length; i++ ) s+=a[i);
s+=‘\n’;
return s;
}//toString

public static void main( String)] args ){


int []a={1,0,1,1};
int []b={0,1,1,0};
CiriProvo mo=new CiriProvo ( a );
mo.ciProvo(b);
System.out.println(mo);
82 83
Capitolo 4:_______________________________________________________________________
Ereditarietà, dynamic binding e polimorfismo

Una proprietà importante delle classi di Java è la possibilità di progettare una nuova classe per estensione di
una classe esistente, dunque per differenza. Tutto ciò permette di concentrarsi sulle novità introdotte dalla
nuova classe e di ereditare quelle che si mantengono inalterate, favorendo in tal modo la produttività del
programmatore.

Di seguito si considera una classe ContoBancario che definisce le usuali operazioni di deposito e prelievo. Un
conto è identificato da un numero espresso mediante una String, e si caratterizza per il suo bilancio. Non è
permesso al bilancio di andare “in rosso”, ossia un prelevamento oltre il valore del bilancio non viene
consentito. A questo scopo il metodo preleva() ritorna un valore boolean che è true se l’operazione si conclude
con successo, false altrimenti. Metodi accessori permettono di conoscere il numero di conto e il valore
corrente del bilancio.

Una classe ContoBancario


package poo. banca;
import java.io.*;
public class ContoBancario{
private String numero;
private doublé bilancio=0;
public ContoBancario( String numero ){//primo costruttore
this.numero=numero;
}
public ContoBancario( String numero, doublé bilancio ){//secondo costruttore
this.numero=numero; this.bilancio=bilancio;
}
public void deposita( doublé quanto ){ //pre(condition: quanto>0
bilancio=bilancio+quanto; Ilo bilancio += quanto;
}//deposita
public boolean preleva( doublé quanto ) { //pre: quanto>0
if( quanto>bilancio ) return false;
bilancio -= quanto;
return true;
}//preleva
public doublé saldo(){ return bilancio;}
public String conto(){ return numero; }//conto
public String toString(){
return String.format( "conto=%s bilancio=E %1.2f", numero, bilancio );
}//toString
}//ContoBancario

In un main, ad es., si può avere:

ContoBancario cb=new ContoBancario("51/554422H,1000);

cb.depositai 200 );

if( eh.prelevai 250 ) ) “corri alla posta a pagare la bolletta dell'ENEL"

System.out.println( cb );
85
Capitolo 4 Ereditarietà dynamic binding polimorfismo

Un conto bancario con fido public ContoConFido( String numero, doublé bilancio ){
ContoBancario va bene per i clienti “ordinari". La banca dispone di un altro tipo di conto, ContoConFido, super( numero, bilancio );
riservato a clientela selezionata, che ammette l’andata in rosso controllata da un fido. Chiaramente }
ContoConFido mantiene molte caratteristiche di ContoBancario ma in più introduce delle differenze (fido, public ContoConFido( String numero, doublé bilancio, doublé fido ){
bilancio in rosso etc.). Java consente di programmare una classe come ContoConFido per specializzazione super( numero, bilancio ); this.fido=fido;
(estensione o extends) della classe esistente ContoBancario: }
public void deposita( doublé quanto )(
Una classe ContoConFido erede di ContoBancario: __ //pre: quanto>0
package poo.banca; if( quanto<=scoperto ) { scoperto-=quanto; return;}
import java.io.*; doublé residuo=quanto-scoperto;
public class ContoConFido extends ContoBancario { scoperto=0;
private doublé fido=1000; //default super.deposita( residuo );
public ContoConFido( String numero ) { super( numero );} }//deposita
public ContoConFido( String numero, doublé bilancio ){ public boolean preleva; doublé quanto ){
super( numero, bilancio ); //pre: quanto>0
if( quanto<=saldo() ){
}
public ContoConFido( String numero, doublé bilancio, doublé fido ){ super.preleva( quanto );
super( numero, bilancio ); this.fido=fido; return true;
} }
public boolean preleva( doublé quanto ){ if( quanto<=saldo()+fido-scoperto ){
//pre: quanto>0 doublé residuo=saldo();
if( quanto<=saldo()+fido ){super.preleva(quanto); return true;} super.preleva( residuo );
return false; scoperto+=quanto-residuo;
}//preleva return true;
public doublé fido(){ return this.fido;} }
public void nuovoFido( doublé fido ){this.fido=fido;} return false;
public String toString(){ }//preleva
return String.format( super.toString()+“ fido=E %1.2f\ fido ); public doublé fido(){ return this.fido; }//fido
public void nuovoFido( doublé fido ){
}
}//ContoConFido this.fido=fido;
}//nuovoFido
Il pronome super public doublé scoperto(){
return scoperto;
Si nota l’uso del pronome super per riferirsi alla super classe, ad esempio per invocare esplicitamente un
}//scoperto
costruttore della super classe cui si delega parte del processo di costruzione. Quando super è usato per questi
public String toString(){
scopi dev’essere la prima istruzione del costruttore. Si nota ancora che l’implementazione mostrata consente
return String.format( super.toString()+" fido=E %1.2f scoperto=E %1.2f“, fido, scoperto );
effettivamente che il bilancio possa diventare negativo, pur nei limiti del fido. Per altro, essendo private il
}//toString
campo bilancio di ContoBancario, ogni sua modifica va ottenuta mediante i metodi di ContoBancario.
}//ContoConFido
Un'implementazione di ContoConFido con gestione dello “scoperto":_________________________________
Seguono alcuni esempi d’uso:
In questa versione della classe ContoConFido, il bilancio materialmente non diventa mai <0. L'andata in rosso
è riflessa dal valore positivo di una variabile di istanza scoperto. È necessario ridefinire non solo preleva() ma ContoBancario c1=new ContoBancario("51/12345",2000);
anche deposita() per mantenere aggiornato lo scoperto. Questa nuova versione di ContoConFido garantisce ContoConFido c2=new ContoConFido("52/334455", 10000,5000);
l’invariante di classe di ContoBancario: bilancio^.
c1.depositalo);
public class ContoConFido extends ContoBancario{
private doublé fido=1000; c2.deposita(2000);
private doublé scoperto=0;
public ContoConFido( String numero )( c1.preleva( 240 );
super( numero ); c2.preleva( 13000 );
}

86 87
Capitolo 4 Ereditarietà dynamic binding polimorfismo

c1 ,fido(); //errore: ContoBancario non conosce il concetto di fido


L’assegnazione da particolare a generale corrisponde, ad es., alla proiezione di un punto dello spazio
c2.nuovoFido( 8000 ); cartesiano (con coordinate x, y e z) sul piano X-Y (la coordinata z è ignorata). Nella situazione effettiva di
System.out.println( "nuovo fido="+c2.fido() ); Java, a seguito dell’assegnazione cb=ce, cb punta all’oggetto composito riferito da ce. Tuttavia, cb lo vede con
gli “occhiali” imposti dalla sua classe di appartenenza ContoBancario. Pertanto i campi fido e scoperto, anche
Relazione di ereditarietà se effettivamente presenti nell’oggetto puntato da cb, sono ignorati.

Tipo statico e tipo dinamico di un oggetto


generale ContoBancario Super classe (o classe base) Dopo l'assegnazione cb=ce, ogni uso di preleva() si riferisce alla sotto classe. Si dice che cb ha tipo statico
(legato cioè alla dichiarazione) ContoBancario e tipo dinamico (guadagnato in seguito all'assegnazione)
s ContoConFido. Il tipo statico dice cosa si può fare su cb. Il tipo dinamico dice quale particolare metodo va in
i s - ci estende esecuzione, se uno della super classe o uno della sotto classe. Prima dell’assegnazione, cb.preleva(...) si
riferisce al metodo della super classe. Dopo l’assegnazione, cb.preleva(...) invoca di fatto la versione di
preleva di ContoConFido.
particolare ContoConFido Sotto classe (o classe derivata)
Assegnazione dal generale al particolare ?

Si dice che ContoConFido è-un (is-a) ContoBancario, solo un pò più specializzato. ContoConFido è una sotto­
classe (o classe derivata), ContoBancario una super-classe (o classe base). La relazione di ereditarietà da
ContoConFido a ContoBancario è una relazione di generalizzazione (si veda anche il cap. 21).

La relazione di ereditarietà è ben definita se un oggetto della classe derivata si può utilizzare in tutti i contesti
in cui è atteso un oggetto della classe base (principio di sostituibilità dei tipi). In fondo: un conto confido è un
conto bancario, solo un pò più particolare. Tuttavia: un conto bancario non è un conto con fido. Se
un’applicazione richiede un conto con fido, non gli si può dare un conto bancario semplice! La parantela
esistente tra classe base e classe derivata consente quanto segue:

ContoBancario cb=new ContoBancario(...);

ContoConFido ce=new ContoConFido(...);

cb=ce; //assegnazione dal particolare al generale OK

Assegnazione tra oggetti come “proiezione”_____ Non si può assegnare un oggetto da generale al particolare, es. ce=cb. Tutto ciò si può subito comprendere
riflettendo che cb non ha campi e valori corrispondenti ai campi particolari introdotti dalla classe conto con
fido. Riferendoci nuovamente ad oggetti punto, non ha senso proiettare un punto dal piano cartesiano X-Y
cb=ce; nello spazio, dal momento che non è definita la coordinata z.

num ero < C = 52/12345 num ero Tuttavia, se cb ha tipo dinamico ContoConFido, si può di fatto cambiare punto di vista ("paio di occhiali”) su cb
in modo da vederlo come ContoConFido e quindi accedere a tutte le funzionalità di ContoConFido:
bilancio < = 20000 bilancio

fido
if( cb instanceof ContoConFido )(
< C = 5000
ce=(ContoConFido)cb; //casting
0 scoperto ce.nuovoFido(5000);
cb ce

assegnazione OK anche se fido e scoperto


non hanno corrispettivi in cb Su una variabile cb di classe (tipo statico) ContoBancario possono essere richieste sempre e solo le
funzionalità della classe cui appartiene. Ma se cb ha tipo dinamico ContoConFido, invocando un metodo
ridefinito in ContoConFido come preleva/deposita, di fatto si esegue la versione del metodo di ContoConFido.
(a) situazione logica (b) situazione effettiva in Java Se cb ha tipo dinamico ContoConFido (cosa controllabile con l’operatore instanceof) è possibile cambiare il
88 89
Capitolo^ Ereditarietà dynamic binding polimorfismo

punto di vista su cb (casting) in modo da vederlo effettivamente come un oggetto di ContoConFido. Dopo il Ereditarietà singola
cambiamento di punto di vista si possono richiedere le funzionalità estese della sottoclasse. In Java ogni classe può essere erede di una sola classe (ereditarietà singola). Tutto ciò permette la
costruzione di gerarchie di classi secondo una struttura ad albero, in cui ogni classe appartiene ad un solo
Dunque: percorso sino alla radice

ce=cb;

non ha senso e dà errore, ma

ce=(ContoConFido)cb;

è ok a meno che cb non disponga del tipo dinamico ContoConFido; tipicamente, per evitare l’eccezione
ClassCastException, si fa precedere il test

if( cb instanceof ContoConFido ){ L’esistenza di una gerarchia di classi, accresce le possibilità di polimorfismo. Ad es., oggetti di classe E hanno
//su ((ContoConFido)cb) si possono richiedere operazioni secondo l’allargamento del punto di vista a come tipi possibili: E, B ed A. Ciò è dovuto alla relazione di ereditarietà. Ad una variabile di classe A è
//ContoConFido; eventualmente: possibile assegnare un oggetto di una qualsiasi sottoclasse B, C, D, E, F ... dunque il tipo dinamico di un
ce=(ContoConFido)cb; etc. oggetto di classe A può essere una qualsiasi delle classi elencate.
}
Ereditarietà vs. composizione
Dynamic binding e polimorfismo Occorre sempre riflettere bene se una relazione di ereditarietà sia opportuna o se piuttosto non rappresenti
Il dynamic binding (collegamento dinamico) si riferisce alla proprietà che invocando un metodo su un oggetto una “forzatura", alla luce del principio di sostituibilità dei tipi.
come cb, dinamicamente possa essere eseguita la versione del metodo definita in ContoBancario o quella
definita in ContoConFido, in dipendenza del tipo dinamico posseduto da cb. Ad es. una Linea (segmento) è caratterizzata da due punti. Progettando la classe Linea come erede della
classe Punto, contando sul fatto che un punto proviene dalla superclasse, un altro lo si può aggiungere, si
Il termine polimorfismo significa "più forme” ed esprime la proprietà che un oggetto possa appartenere a più commette un errore grossolano. Infatti, una Linea non è un Punto piuttosto è composta (has-a) da due punti.
tipi. Assegnando cb=ce, l’oggetto cb acquisisce un altro tipo (diventa polimorfo). Il polimorfismo di cb si può Dunque anziché ricorrere alla estensione, è opportuno programmare Linea come abbozzato di seguito:
verificare come segue
class Linea(
if( cb instanceof ContoBancario ) è TRUE Punto p1, p2; //composizione mediante attributi
ifj cb instanceof ContoConFido ) è TRUE

A ben riflettere, dynamic binding e polimorfismo sono le due facce di una stessa medaglia. Proprio perchè
sussiste il polimorfismo, si ha l'effetto del dynamic binding. L’antenato “cosmico” Object*•
In Java, ogni classe eredita direttamente o indirettamente da Object (radice di tutte le gerarchie di classi).
Ereditarietà e ridefinizione di metodiiS Quando una classe non specifica la clausola extends, in realtà ammette implicitamente la clausola
Si è detto che il progetto di ContoConFido ridefinisce i metodi deposita e preleva già presenti nella super
classe ContoBancario. Occorre prestare attenzione che per essere una vera ridefinizione, occorre extends Object
normalmente rispettare la sua intestazione (signature). Se cambia qualcosa nell’intestazione (nome del
metodo, tipi dei parametri), allora si tratta di overloading anziché di ridefinizione (overriding). La comune discendenza da Object si manifesta in diverse questioni, es. stile di programmazione,
polimorfismo.
Perchè funzioni correttamente il dynamic binding/polimorfismo, è necessario osservare l’esatta intestazione
dei metodi che, ad es., per preleva significa I metodi seguenti ammettono già un’implementazione in Object che necessariamente è generica. Essi vanno
di norma ridefiniti per avere un significato “tagliato su misura” delle nuove classi:
@Override
public boolean preleva( doublé quanto ){...} • String toStringO - ritorna lo stato di this sotto forma di stringa
• boolean equals( Object x ) - ritorna true se this ed x sono uguali. Object definisce l’uguaglianza in modo
L'annotazione (una sorta di commento speciale) @Override disponibile dalla versione 5 di Java in poi, superficiale: due oggetti sono uguali se sono in aliasing, ossia condividono lo stesso riferimento
permette al compilatore di controllare ed eventualmente segnalare problemi, durante una ridefinizione. • int hashCode() - ritorna un hash code (numero intero unico) per this.
L'annotazione è facoltativa.

90 91
Capitolo 4 Ereditarietà dynamic binding polimorfismo

Si mostra una ridefinizione del metodo equals() nella classe ContoBancario, che basa l'uguaglianza degli Una classe BancaArray (facade)
oggetti sul contenuto degli stessi (nozione profonda di uguaglianza): Si mostra di seguito una semplice classe che simula il comportamento di una banca. La clientela è
memorizzata su un array di conti bancari (super classe). L’array è ri-allocato con capacità doppia se al tempo
©Override di aggiunta di un conto la struttura è satura. Quando si rimuove un conto, si scala a sinistra di un posto tutto il
public boolean equals( Object o )( contenuto dell’array. La ricerca di un conto si fonda sulla ridefinizione del metodo equals.
if( !(o instanceof ContoBancario) ) return false;
if( o==this ) return true; package poo.banca;
ContoBancario c=(ContoBancario)o; import java.io.lOException;
return numero.equals( c.numero ); public class BancaArray{
}//equals private ContoBancario [jclientela;
private int size=0, capacita;
Il metodo si basa solo sul numero di conto: due conti correnti sono uguali se si riferiscono allo stesso numero public BancaArray(){ this( 50 );}
di conto. Questa formulazione va bene anche per ContoConFido. Si nota che il test public BancaArray( int capacita ){
this.capacita=capacita;
if( !(o instanceof ContoBancario) ) return false; clientela=new ContoBancario(capacita);
}
include automaticamente anche il caso in cui o fosse nuli. public int size(){ return size;}
public void aggiungiConto( ContoBancario cb ){
Strutture dati eterogenee if( size==capacita ){
In virtù delle proprietà della relazione di ereditarietà, risulta ad es. che dichiarando ContoBancario []vecchiaC=clientela; //alias
clientela=new ContoBancario[capacita*2];
Object []v=new Object[10]; System.arraycopy( vecchiaC, 0, clientela, O.size );
capacita ’ = 2;
si possono memorizzare in v oggetti di qualunque classe concreta. L’array è pertanto eterogeneo. L’utente }
può comunque scoprire a runtime il tipo di un elemento con l’operatore instanceof: clientela[size]=cb; size++;
}//aggiungiConto
if( v[i] instanceof String )... public void rimuoviConto( ContoBancario cb ){
rimuoviConto( indexOf( cb ) );
Riassunto modificatori }//rimuoviConto
Gli attributi di una classe (campi o metodi) possono avere un modificatore tra public void rimuoviConto( int i ){
• public se sono esportati a tutti i possibili Client if( i<0 II i>=size ) return;
• private se rimangono ad uso esclusivo della classe for( int j=i+1; j<size; j++ )
• protected se sono esportati solo alle classi eredi clientela[j-1]=clientela[j];
}//rimuoviConto
• (nulla) se devono essere accessibili all’interno dello stesso package (familiarità o amicizia tra classi).
public int indexOf( ContoBancario cb ){
Attenzione: gli attributi protected sono accessibili anche nell’ambito del package di appartenenza.
for( int i=0; ksize; i++ )
if( clientela[i].equals( cb ) ) return i;
Una classe può essere public se è esportata per l’uso in altri file, non avere il modificatore public se il suo uso
return -1;
è ristretto al package (eventualmente anonimo) di appartenenza. Una classe può essere final se non può
}//indexOf
essere più estesa da classi eredi. Similmente, un metodo final non può essere più ridefinito nelle sottoclassi.
public ContoBancario getConto( int i ){
In una ridefinizione di metodo è possibile ampliare il suo modificatore ma non restringerlo. Ad es. nella super
if( i<0 II i>=size ) { return nuli;}
classe il metodo potrebbe essere protected e nella sotto classe public, ma non viceversa.*S i
return clientelati];
}//getConto
Gestione casi “anomali” (preliminare)
public String toString(){
Si è visto che preleva(), deposita() ricevono una quantità che è attesa (precondizione) >0. Se cosi non è si String s="‘ ;
potrebbe dare una segnalazione diagnostica e terminare il programma. In alternativa si può sollevare for( int i=0; i<size(); i++ ){
un'eccezione come segue: s+=clientela[i]+’,\n";
}
public void deposita( doublé quanto )( return s;
if( quanto<=0 ) throw new IHegalArgumentException(); }//toString
... //come prima
}//deposita
92 93
Capitolo 4 Ereditarietà dynamic binding polimorfismo

public void salva( String nomeFile ) throws lOException {//da sviluppare public int getValore(){ return valore;}
{//salva public void incrementa(){ valore++; {
public void caricai String nomeFile ) throws lOException {//da sviluppare public void decrementa(){ valore--; {
{//carica public String toString(){ return "Evalore;}
public static void main( String [] args ){ public boolean equals( Object o ){
ContoBancario c1=new ContoBancario("51/2233",2000); if( !(o instanceof Contatore ) ) return false;
ContoBancario c2=new ContoConFido("53/1122", 10000); ifj o==this ) return true;
ContoConFido c3=new ContoConFido("53/1713",20000); Contatore c=(Contatore)o;
Banca b=new Banca(); return this.valore==c.valore;
b.aggiungiConto(cl); b.aggiungiConto(c2); b.aggiungiConto(c3); }
System.out.println(b); {//Contatore
int i=b.indexOf( c2 );
b.rimuoviConto( i ); Un oggetto Contatore può assumere qualsiasi valore intero (positivo/negativo/zero) e può anche traboccare
System.out.println(b); (overflow/underflow). Si propone ora una classe erede ContatoreModulare che perfeziona Contatore e si
} fonda sul concetto di modulo, per cui attinto il valore modulo-1 ritorna da zero e viceversa. Ad esempio, un
{//BancaArray contatore decimale (cioè modulo 10) assume tutti i valori tra 0 e 9. Un incremento da 9 fa ritornare a 0. Un
decremento da 0 fa ritornare a 9.
Poiché il main è programmato direttamente nella classe BancaArray che è parte esplicita del package
poo.banca, per mandare in esecuzione il programma da riga di comando occorre mettersi nella directory di Una classe ContatoreModulare specializzazione di Contatore:_____ _________________________________
progetto in cui è contenuta poo e fornire il nome del package come prefisso del nome del file: package poo.contatori;
public class ContatoreModulare extends Contatore{
>java poo.banca.BancaArray INVIO protected int modulo;
public ContatoreModulare(){
Se invece il main è parte di una classe che appartiene al package anonimo (che si valuta alla directory //invoca implicitamente il costruttore di default della super classe Contatore
corrente), per mandarlo in esecuzione è sufficiente lanciare l’interprete col solo nome del file col main, System.out.printlnfContatoreModulare: costruttore di default"); modulo=10;
direttamente dalfinterno della directory contenente la classe dell'applicazione: }
public ContatoreModulare( int modulo, int vai ){
>java Prog super( vai );
if( vakO II modulo<1 II val>=modulo ) throw new IHegalArgumentException();
Un altro esempio di gerarchia di c la s s i___ __________ System.out.printlnfContatoreModulare: costruttore normale"); this.modulo=modulo;
Si considera una classe Contatore che fornisce l'astrazione di un contatore, ossia una variabile intera che può }
essere incrementata/decrementata. La classe dispone di tre costruttori: (1) quello di default che inizializza a public ContatoreModulare( ContatoreModulare cm ){
zero il contatore: (2) quello normale che imposta il valore iniziale del contatore con il valore di un parametro; super( cm.valore );
(3) quello di copia che imposta il contatore dal valore di un altro contatore. Per semplicità il campo valore è System.out.printlnfContatore: costruttore di copia");//demo
dichiarato protected (esportato cioè alle classi eredi). this.modulo=cm.modulo;
}
Una classe Contatore:________ ______ ___ _ ___ _
package poo.contatori; public int getModulo(){ return modulo;}
public class Contatore{ public void incrementa(){ valore=(valore+1)%modulo;}
protected int valore; public void decrementa(){ valore=(valore-1+modulo)%modulo;}
public Contatore(){ //stampa demo public String toString(){ return super.toString()+" modulo: "+modulo;}
System.out.printlnfContatore: costruttore di default"); valore=0; public boolean equals( Object o ){
} if( !(o instanceof ContatoreModulare ) ) return false;
public Contatore( int vai ){//stampa demo ifj o==this ) return true;
System.out.printlnfContatore: costruttore normale"); this.valore=val; ContatoreModulare c=(ContatoreModulare)o;
} return c.getValore()==valore && c.modulo==modulo;
public Contatore( Contatore c ){//stampa demo {//equals
System.out.printlnfContatore: costruttore di copia"); {//ContatoreModulare
this.valore=c.valore;
} Un main di test è mostrato di seguito:

94 95
Capitolo 4 Ereditarietà dynamic binding polimorfismo

public class TContatorij Tutto ciò spiega, tra l’altro, che chiamate tipo super(...) o this(...) che invocano esplicitamente un costruttore
public static void main( String [jargs ){ devono essere la prima azione di un costruttore. In assenza di un’invocazione esplicita super(...), un
Contatore c1=new Contatore( 10 ); costruttore di una sotto classe richiede la presenza e ne invoca l’esecuzione implicitamente, del costruttore di
ContatoreModulare cm=new ContatoreModulare(); default della super classe.
ContatoreModulare cm1=new ContatoreModulare( 8, 0 );
System.out.printlnf 10 incrementi da "+cm1.getValore()); L’ordine di esecuzione dei costruttori rappresenta l’ordine di inizializzazione delle sotto parti di oggetti che
for( int i=0; i<10; i++ ){ formano un oggetto composito come un’istanza di classe C. Tutto ciò diventa rilevante e può essere sorgente
cm1.incrementa(); System.out.println( cm1 ); di malfunzionamenti in presenza di ridefinizioni di metodi che sono invocati nei costruttori. Si consideri un
metodo m() definito in A e ridefinito in C, e si supponga che m venga invocato in un costruttore di A. Creando
System.out.printlnf’10 decrementi da “+cm1.getValore()); un oggetto di classe C, parte prima il costruttore di A che invoca m, che essendo ridefinito, per dynamic
for( int i=0; i<10; i++ ){ binding prescrive l’esecuzione della versione m di C. Il problema è che in questa situazione si possono
cm1.decrementa(); System.out.println( cm1 ); verificare errori in quanto la parte di oggetto di classe C non è stata ancora inizializzata al tempo in cui la
} versione m di C è invocata!
}//main
}//TContatori È utile riassumere, infine, i passi dettagliati del processo di esecuzione di un costruttore:
1) Si inizializzano ai valori di default tutti i campi dell’oggetto (anche composito, ossia istanza di una sotto
Output prodotto: classe): un campo numerico è posto a zero, un boolean è posto a false, un carattere è posto al valore nullo
(valore minimo UNICODE ’\u0000'), il riferimento ad un oggetto è posto a nuli
Contatore: costruttore normale 2) Si invoca il costruttore della super classe
Contatore: costruttore di default //chiamato implicitamente 3) Si eseguono le istruzioni di inizializzazione previste sui campi dell’oggetto
ContatoreModulare: costruttore di default 4) Si esegue il metodo costruttore della classe dell’oggetto.
Contatore: costruttore normale //chiamato esplicitamente con super(...)
ContatoreModulare: costruttore normale Gerarchie declassi e finalize _________
10 incrementi da 0 Si è già detto che su un oggetto irraggiungibile (garbage), prima di recuperarne la memoria da parte del
10 decrementi da 2
1 modulo: 8 1 modulo: 8 garbage collector, viene invocato, se esiste, il metodo finalize() che tipicamente potrebbe interessarsi a
2 modulo: 8 0 modulo: 8 rilasciare risorse (aree di memoria) precedentemente allocate da parte del sottostante sistema operativo, es.
3 modulo: 8 7 modulo: 8 associate a file aperti, a connessioni di rete aperte etc. Se l’oggetto è di una sotto classe come C, solo il
4 modulo: 8 6 modulo: 8 metodo finalize di C è invocato. Al fine di assicurare un corretto svolgimento delle azioni di finalizzazione, è
5 modulo: 8 5 modulo: 8 sempre buona norma di programmazione che un metodo finalize() preveda esplicitamente l’esecuzione del
6 modulo: 8 4 modulo: 8 finalize della super classe: super.finalize().
7 modulo: 8 3 modulo: 8
0 modulo: 8 2 modulo: 8
Il metodo getClass() di Object
1 modulo: 8 1 modulo: 8
0 modulo: 8 In Java anche le classi sono oggetti. La classe di una classe è un oggetto di classe Class. Il metodo
2 modulo: 8 getClass() di Object ritorna dinamicamente l’oggetto Class associato ad un’istanza. In tale oggetto sono
contenute tutte le informazioni relative alla classe dell’istanza. Su di esse si basa l'introspezione (reflection), la
Ordine di esecuzione dei costruttori capacità, cioè, di poter inferire a runtime quanti e quali costruttori sono disponibili, gli attributi presenti ed i loro
Un oggetto di classe C (si veda la figura) ingloba le componenti dati che derivano da B e da A. tipi, i metodi con annessi parametri e tipi di ritorno etc. Di seguito ci si limita ad osservare che il metodo
L’inizializzazione dei dati dell'oggetto composito ha inizio a partire dalla superclasse A e prosegue in giù sino a getClass() potrebbe essere utilizzato, in qualche caso, come alternativa di instanceof. Data la gerarchia di
C (“dall’alto verso il basso”). classi:

y
ordine di esecuzione
B dei costrutton B

t :
c

96 97
Capitolo 4 Ereditarietà dynamic binding polimorfismo

e le istruzioni public doublé distanza( Punto p ) { return Math.sqrt((p.x-x)*(p.x-x)+(p.y-y)*(p.y-y)); }//distanza


public String toString(){ return "<“+String.format(“%1.2^x)+","+String.format("%1.2f''ly)+ V ; }//toString
A a=new A(); public boolean equals( Object p ){
B b=new B(); if( !(p instanceof Punto) ) return false;
C c=new C(); if( p==this ) return true;
Punto pt=(Punto)p;
a=c; return Mat.sufficientementeProssimi(x,pt.x) && Mat.sufficientementeProssimi(y,pt.y);
}//equals
il tipo dinamico di a può essere scoperto anche attraverso un test come quello che segue: }//Punto

if( a.getClass()==C.class )... a riferisce un oggetto di classe C Sulla base della classe Punto si presenta ora il progetto e l’implementazione di una classe Retta appartenente
allo stesso package poo.geometria, fornita per lo studio individuale. Le situazioni di errore sono segnalate
oppure: mediante la generazione di eccezioni runtime (l'approfondimento delle eccezioni è nel cap. 7). Una retta può
essere costruita fornendo due punti, o il coefficiente angolare e l'intercetta q con l'asse y, o mediante un punto
if( a.getClass().getName().equals("C”) )... ed il coefficiente angolare, o fornendo i tre coefficienti a, b e c dell'equazione implicita. Per completezza è
previsto anche il costruttore di copia. I confronti tra reali sfruttano la classe di utilità poo.util.Mat. Infinito è
L’oggetto class di un'istanza è ovviamento unico. Pertanto se ad a si assegna c, allora a viene legato assunto pari a Double.MAX_VALUE.
all’oggetto class C.class che dentro di sé ingloba (per via dell’ereditarietà) gli attributi di B e di A. Tutto ciò
lascia intendere che gli usi di instanceof e di getClass in generale non coincidono. Dopo a=c; si ha: package poo.geometria;
import poo.util.Mat;
a instanceof C => true public class Retta{
a instanceof B => true private Punto p1, p2;
a instanceof A => true private doublé m, q;
public Retta( Punto p1, Punto p2 ){
a.getClass==C.class => true if( p1.equals(p2) )
a.getClass==B.class => false throw new RuntimeExceptionfPunti coincidenti");
a.getClass==A.class => false this.p1=new Punto(p1);
this.p2=new Punto(p2);
Caso di studio: Una classe Retta if( Mat.sufficientementeProssimi(p1.getX(), p2.getX()) ){
Segue una versione più completa della classe Punto appartenente al package poo.geometria. La nuova //retta verticale
versione include una ridefinizione del metodo equals(). m=Double.MAX_VALUE;
q=Double.MAX VALUE;
package poo.geometria; }
import poo.util.Mat; else{//(y2-y1 )/(x2-x1 )=(y-y1 )/(x-x1 )
m=(p2.getY()-p1 ,getY())/(p2.getX()-p1 .getXQ);
public class Puntof q=-pt getX()*((p2.getY()-p1 ,getY())/
private doublé x, y; (p2.getX()-p1.getX()))+p1.getY();
public Punto(){//costruttore di default }
this(0,0); }
}
public Punto( doublé x, doublé y ){//costruttore normale public Retta( doublé m, doublé q )(
this.x=x; this.y=y; iti Mat.sufficientementeProssimi( Math.abs(m),Double.MAX VALUE ) Il
} Mat.sufficientementeProssimi( Math.abs(q),Double.MAX VALUE ) )
public Punto( Punto p ){//costruttore di copia throw new RuntimeExceptionfRetta indeterminata");
this(p.x.p.y); this.m=m;this.q=q;
} //retta certamente NON verticale
public doublé getX(){ return x ;} if( orizzontale() ){
public doublé getY(){ return y ;} p1=new Punto(0,q);//esempio
public void sposta( doublé nuovaX, doublé nuovaY ){ p2=new Punto(3,q);//esempio
x=nuovaX; y=nuovaY; }
}//sposta
98 99
Capitolo 4 Ereditarietà dynamic binding polimorfismo

else{ //retta obliqua: intercette con gli assi public Retta( Retta r ){
p1=new Punto(0,q); this.m=r.m; this.q=r.q;
p2=new Punto(-q/m,0); this.p1=new Punto( r.p1 );
this.p2=new Puntoj r.p2 );
}
public Retta( doublé m, Punto p ){ public doublé getCoefficienteAngolare(){ return m; }//getCoeffAngolare
if( Mat.sufficientementeProssimi(Math.abs(m),Double.MAX VALUE) ){//retta verticale
this.m=m; this.q=Double.MAX VALUE; public doublé getTermineNoto(){ return q; }//getTermineNoto
p1=new Punto(p);
p2=new Punto( p.getX(), p1.getY()+1 ); //esempio public boolean parallela( Retta r ){
} if( this.verticale() && r.verticale() ) return true;
else{ //(y-y1)=m(x-x1) return Mat.sufficientementeProssimi( this.m, r.m );
this.m=m; this.q=-m*p.getX()+p.getY(); }//parallela
p1=new Punto(p);
p2=new Punto(0,q); //esempio public boolean perpendicolare( Retta r ){
if( this.verticalej) && r.orizzontale() llthis.orizzontale() && r.verticale() ) return true;
if( this.verticalej) Il r.verticale() ) return false;
return Mat.sufficientementeProssimi( this.m, -1/r.m );
public Retta( doublé a, doublé b, doublé c ){ }//perpendicolare
//retta assegnata in veste implicita: ax+by+c=0
if( Mat.sufficientementeProssimi(b.O) ){ public boolean verticale(){
if( Mat.sufticientementeProssimi(a,0) ) if( Mat.sufficientementeProssimi( Math.abs(m),Double.MAX VALUE ) )return true;
throw new RuntimeExceptionfRetta indeterminata"); return false;
//retta verticale }//verticale
this.m=Double.MAX VALUE;
this.q=Double.MAXJ/ALUE; public boolean orizzontale(){
if( Mat.sufficientementeProssimi(c,0) ){ if( Mat.sufficientementeProssimi(m,0) ) return true;
p1=new Punto(); return false;
p2=new Punto(0,1);//esempio }//orizzontale
}
else{ public boolean obliqua(){ return !this.orizzontale() && !this.verticale(); }//oblique
p1=new Punto(-c/a, 0);
p2=new Punto(-c/a, 1); //esempio public boolean interseca( Retta r ) { return Ithis.parallela(r);}//interseca

public Punto puntolntersezione( Retta r ){


else{ if( this.parallela(r) ){
if( Mat.sutficientementeProssimi(a,0) ){//orizzontale if( this.equalsjr) )
this.m=0; this.q=-c/b; throw new RuntimeException("lnfinite intersezioni’);
p1=new Punto(0,q); throw new RuntimeExceptionfNessuna intersezione’ );
p2=new Punto(1,q);//esempio
} doublé x=0, y=0;
else{//obliqua if( this.obliqua() && r.obliqua() ){
this.m=-a/b; this.q=-c/b; x=(r.q-this.q)/(this.m-r.m);
//intercette con gli assi y=this.m*x+this.q; //usando la prima equazione
p1=new Punto(0,q); }
p2=new Punto(-q/m,0); else if( this.verticale() ){
x=this.p1.getX();
y=r.m‘ x+r.q;

100 101
Capitolo 4 Ereditarietà dynamic binding polimorfismo

else if( r.verticale() ){ Retta r6=new Retta(Double.MAXJ/ALUE, new Punto(3,5));


x=r.p1.getX(); System.out.println(r6);
y=this.m*x+this.q; Retta r7=new Retta( 2, 5, -4 ); //forma implicita
} System.out.println(r7);
else if( this.orizzontale() && r.verticale() ){ Retta r8=new Retta( 2,0, 3 );
x=r.p1.getX(); System.out.println(r8);
y=p1.getY(); }//main
} }//Retta
else if( this.verticale() && r.orizzontale() ){
x=p1.getX(); Esercizi
y=r.p1.getY(); 1. Nelle due classi ContoBancario e ContoConFido non v’è traccia del tasso di interesse e del costo delle
} operazioni. Modificare la classe ContoConFido in modo da includere un campo che esprime il numero di
return new Punto(x,y); operazioni (depositi/prelievi) che la banca consente ad un cliente senza spese. Quindi un campo che esprime
}//puntolntersezione il costo di una singola operazione (es. 1.5 euro). Aggiungere un ulteriore campo che consenta di contare le
operazioni effettuate. Prevedere metodi accessori/mutatori corrispondenti a questi campi, ed un metodo
public String toString(){
if( this.verticale() ) return "x=“+String.format("%1.2f',p1.getX()); void valutaCostoOperazioniQ
else if( this.orizzontale() ) return "y="+String.format(M%1.2f"tq);
String s="y="+m+V; che viene invocato dalla banca periodicamente ed effettua un aggiornamento del conto sulla base delle
if( q>0 ) s+ = V ; operazioni effettuate.
s+=q; 2. Sviluppare un’altra sotto classe ContoRisparmio di ContoBancario, che introduce il tasso di interesse
return s; praticato dalla banca, metodi accessori/mutatori sul tasso ed un metodo mutatore void interessiMaturati() che
}//toString la banca invoca periodicamente per aggiornare il conto con gli interessi maturati.
3. Cosa succede se si rimuove il costruttore di default nella classe Contatore e tutto il resto rimane inalterato ?
public boolean equals( Object o ){ Discutere e verificare sperimentalmente.
if( !(o instanceof Retta) ) return false; 4. Cosa succede se in ContatoreModulare si aggiunge un costruttore che accetta solo un parametro
if( o==this ) return true; interpretato come modulo, si toglie il costruttore di default da ContatoreModulare e si toglie il costruttore di
Retta r=(Retta)o; default da Contatore ?
return Mat.sufficientementeProssimi( this.m, r.m ) &&Mat.sufficientementeProssimi( this.q, r.q ); 5. La classe Object dispone di un costruttore di default. Fornire esempi relativi alla sua invocazione implicita.
}//equals 6. Progettare una gerarchia di classi avente Punto come classe base. Una prima classe erede può essere
Cerchio che aggiunge il campo raggio e metodi accessori/mutatori get/set, un metodo per restituire il
public static void main( Stringi] args ){//Demo perimetro, un metodo per restituire l'area, etc. Un’ulteriore classe erede è Cilindro, derivata da Cerchio, con
Retta r=new Retta( new Punto(2,3), new Punto(7,4) ); metodi per restituire l’area della superficie laterale, totale, volume etc.
System.out.println(r);
System.out.println(Bm=‘'+r.qetCoefficienteAnqolare()+" q=“+r.qetTermineNoto() ); L’esercizio 6 suggerisce di ottenere Cerchio per estensione di Punto. In alternativa la classe Cerchio potrebbe
Retta r1=new Retta(-0.2, 3); non usare l’ereditarietà ma basarsi piuttosto sulla composizione, prevedendo un proprio campo centro di tipo
System.out.println(rl); Punto. Aggiungendo ora esplicitamente alla classe Cerchio i metodi getX()/getY(), sposta(), distanza() previsti
Retta r2=new Retta( new Punto(3,5), new Punto(3,8) ); nella classe Punto, si può facilmente verificare che la funzionalità derivante dall’ereditarietà è perfettamente
System.out.println(r2); mantenuta: ogni invocazione di un metodo che fa riferimento a Punto, è delegata all’oggetto Punto centro, es.
System.out.println( r.interseca(r2) );
System.out.println(“Punto di intersezione tra "+r+" e '+r2+" = "+r.puntolntersezione(r2)); public void sposta( doublé nuova^x, doublé nuova_y ){centro.sposta( nuova jc, nuova _y ); }//sposta
System.out.printlnf Punto di intersezione tra "+r1+" e "+r2+" = "+r1.puntolntersezione(r2));
Retta r3=new Retta( 0, 4 ); //retta orizzontale In più la classe Cerchio rimane “libera” in quanto non più vincolata dalla relazione di ereditarietà da Punto. La
Retta r4=new Rettaj new Punto(3,0), new Punto(3,2) ); composizione può risultare più flessibile dell'ereditarietà che è una relazione statica.
System.out.println( "r3: “+r3+" perpendicolare a r4: "+r4+" H+r3.perpendicolare(r4) );
if( r3.verticale() ) System.out.println(r3+" e' verticale"); 7. Predire l’output generato dal seguente programma Java:
if( r3.orizzontale() ) System.out.println(r3+" e' orizzontale");
if( r4.verticale() ) System.out.println(r4+" e' verticale");
if( r4.orizzontale() ) System.out.println(r4+" e' orizzontale");
Retta r5=new Retta(new Punto( 5,7), new Punto(5,12) );
System.out.println(r5);
102 103
Capitolo 4

class A{ Capitolo 5^_______________


int y=3;
Classi astratte e interfacce
public A(){
System.out.println("costruttore di default di A");
Una gerarchia di classi per figure geometriche piane
m ();
Si considerano le comuni figure piane come cerchio, quadrato, rombo, trapezio etc.Volendo organizzare le
} figure in modo da facilitarne l’utilizzo nelle applicazioni, si può riflettere che tutte posseggono almeno una
public void m(){
dimensione, es. il raggio per il cerchio, il lato per il quadrato o il rombo, la base e l'altezza per un rettangolo e
System.out.printlnf'A.m y=“+y);
cosi via. Per “imparentare" le figure si può concepire una classe base Figura che poi ogni figura particolare
} può estendere e specializzare. In Figura si può introdurre una dimensione (doublé) e i metodi che certamente
hanno senso su tutte le figure.
class B extends A{
int x=5;
public void m(){
System.out.printlnf'B.m x="+x);
}
public B(){ //invoca implicitamente il costruttore di default di A
System.out.printlnfcostruttore di default di B“);

}//B
Identificare una gerarchia di classi come quella di cui si sta discutendo è sempre un fatto importante: infatti si
public class AB{
può introdurre nella classe base (qui Figura) tutti quegli elementi (attributi e metodi) comuni a qualunque
public static void main( String[] args ){
erede. In questo modo si evitano ridondanze e si garantisce ad ogni classe derivata di possedere i “connotati"
new B();
di appartenenza ad una stessa “famiglia".
J//AB Si rifletta ora che prevedendo una dimensione (cioè un lato) nella classe Figura, non si sa bene cosa essa
voglia dire. Per un cerchio si tratterà del suo raggio, per un quadrato del suo lato, per un rettangolo magari la
Cosa succede nel programma precedente se il solo costruttore disponibile in A ammette un parametro k di tipo
sua base etc. Quindi metodi come perimetro() ed area() previsti in Figura non si possono dettagliare in quanto
int ?
manca l’informazione su come interpretare la figura.

Si dice che una classe come Figura è astratta (abstract) proprio perchè ancora incompleta. Spetta poi alle
classi eredi concretizzare tutti quegli aspetti previsti in Figura ma al momento astratti. Segue una specifica
della classe astratta Figura, posta nel package poo.figure:

Una classe astratta Figura:__________________________________________________________________


package poo.figure;
public abstract class Figurai
private doublé dimensione;
public Figura( doublé dim ){
if( dim<=0 ) throw new HlegalArgumentException();
this.dimensione=dim;
}
protected getDimensione(){ return dimensione;}
public abstract doublé perimetro();
public abstract doublé area();
}//Figura

Una classe astratta come Figura non è istanziabile, ossia non si possono creare oggetti della classe. Allora a
cosa serve ? Serve come base per progettare classi eredi.

104 105
Capitolo 5 Classi astratte e interfacce

Per etichettare che una classe è astratta si premette al nome class il modificatore abstract. In una classe public Rettangolo( Rettangolo r ){
astratta uno o più metodi sono di norma astratti. Una classe erede di Figura è concreta se implementa (ne super( r.getDimensione() );
fornisce cioè il corpo) tutti i metodi abstract. Se qualche metodo rimane ancora astratto, anche la classe erede this.altezza=r.altezza;
è astratta e spetta ad un ulteriore erede implementare i rimanenti metodi abstract etc. }
public doublé getBase(){ return getDimensione();}
Si nota che in una classe astratta possono essere presenti campi dati (es. dimensione) e metodi concreti. Ad public doublé getAltezza(){ return altezza;}
esempio getDimensione(), utile solo per le classi eredi (esportazione protected), è concreto. Di seguito si public doublé perimetro(){ return 2*getDimensione()+2*altezza;}//perimetro
mostra una classe Cerchio erede di Figura. Essa interpreta la dimensione come raggio. public doublé area(){ return getDimensione()*altezza;}//area

Una classe concreta Cerchio:________ ________ ________________________ public String toString(){


package poo.figure; return “Rettangolo: base=“+getDimensione()+” altezza=“+altezza;
import poo.util.Mat; }//toString
public class Cerchio extends Figurai
public Cerchio( doublé raggio )( super(raggio);} public boolean equals( Object x )(
public Cerchio( Cerchio c ){ super(c.getDimensione());} if( !( x instanceof Rettangolo) ) return false;
public doublé getRaggio(){ return getDimensione();} if( x==this ) return true;
Rettangolo r=(Rettangolo)x;
public doublé perimetro(){ return 2*Math.PI‘ getDimensione(); }//perimetro return Mat.sufficientementeProssimi( r.getDimensione(), this.getDimensione() f&&
public doublé area(){ Mat.sufficientementeProssimi( r.getAltezza(), this.altezza );
doublé r=getDimensione(); }//equals
return rYMath.PI; }//Rettangolo
}//area
public String toString(){ Si mostra ora un metodo areaMassimaQ che riceve un array di figure e scrive in uscita l’area massima e il tipo
return “Cerchio: raggio=“+getDimensione(); di figura avente l’area massima. L’array contiene oggetti Figura (la super classe):
}//toString
public boolean equals( Object x )( public static void areaMassima( Figura []f ){
if( !(x instanceof Cerchio) ) return false; doublé am=0; Figura fam;
if( x==this ) return true; for( int i=0; kf.length; i++ ){
Cerchio c=(Cerchio)x; doublé a=f[i].area(); //dynamic binding
return Mat.sufficientementeProssimi( this.getDimensione(), c.getDimensione() ); if( a>am ) { am=a; fam=f[i];}
}//equals }
}//Cerchio System.out.printlnfArea massima="+am);
System.out.printlnf'La figura con area massima e’ "+fam);
Essendo privato il campo dimensione di Figura, si è fatto ricorso ai metodi getDimensione()/setDimensione() }//areaMassima
per accedervi da dentro Cerchio. Il metodo equals() necessariamente è peculiare di ogni classe erede, e per
questa ragione non è stato previsto in Figura. Similmente per il metodo toString(). In altre situazioni può Il metodo areaMassima potrebbe appartenere ad una classe con il main come segue:
essere invece conveniente anticipare nella super classe una implementazione dei metodi equalsQ,
hashCode() e toString(). Segue un’altra classe concreta: Rettangolo. public class Applicazione{
public static void areaMassima(){...}
Una classe concreta Rettangolo:______________________________________________________________ public static void main( String [jargs ){
package poo.figure;
import poo.util.Mat; Figura []a={ new Cerchio(4), new Rettangolo(2,5),...};
public class Rettangolo extends Figura{ areaMassima( a );...
protected doublé altezza; }//main
}//Applicazione
public Rettangolo( doublé base, doublé altezza ){
super( base ); Una classe astratta per il problema deH’ordinamento
if( altezza<=0 ) throw new HlegalArgumentException(); Di seguito sivaluta la possibilità di risolvere il problema deH’ordinamento di un array di oggetti prevedendo una
this.altezza=altezza; classe che fornisce un metodo di ordinamento che si fonda su un criterio di confronto da specializzare di caso
} in caso. In fondo, la logica dell’ordinamento è sempre la stessa, indipendentemente dalla tipologia degli

106 107
Capitolo 5 Classi astratte e interfacce

oggetti trattati, purché si definisca quando un oggetto o1 precede (è minore), segue (è maggiore) o è uguale Si può verificare facilmente che il metodo compareTo può fare a meno delle due istruzioni di confronto come
ad un altro oggetto o2. segue:

La classe proposta, Sortable, posta nel package poo.sortable, è astratta nel metodo protetto compareTo() cui protected int compareTo( Sortable y ){
è affidato il significato di confronto tra oggetti. Il metodo static sort(), invece, è concreto e realizza Intero i=(lntero)y;
l’ordinamento per selezione (come esempio) di un array di oggetti Sortable. La soluzione pretende che si return this.x-i.x;;
programmino classi eredi di Sortable al fine di fornire versioni concrete del metodo compareTo() utilizzato, per }//compareTo
dynamic binding, dal metodo sort().
Una classe applicativa che utilizza le classi precedenti è TestSortable mostrata di seguito:
package poo.sortable;
public abstract class Sortablef public class TestSortable{ //una classe di test
protected abstract int compareTo( Sortable x ); public static void print( Intero []a ){
public static void sort( Sortable []v ){ for( int i=0; i<a.length; i++ )
for( int j=v.length-1; j>0; j - ){ System.out.print(a[i]+B");
int iMax=0; System.out.println();
for( int i=0; i<=j; i++ ) }
if( v[i].compareTo(v[iMax))>0 ) iMax=i; public static void main( String []args ){//demo
//scambia * Intero a()=new lntero[10],
Sortable park=v[j]; for( int i=0; i<10; i++ ) a[9-i]=new Intero(i); //esempio di inizializzazione
v[j]=v[iMax]; System.out.println("Vettore iniziale");
v[iMax]=park; print(a);
}//for Sortable.sort( a ); //invocazione del metodo di ordinamento
}//sort System.out.println(”Vettore dopo ordinamento");
}//Sortable print(a);
}
Si assume il seguente comportamento del metodo astratto compareTo() : }//TestSortable

if( o1.compareTo(o2) opre!0 )... Ordinare razionali


L'approccio può essere facilmente applicato anche per ordinare oggetti razionali, adattando la classe
dove opreI può essere: >, <, ==. >0 indica che o1 è maggiore di o2; <0 indica che ol è minore di o2; ==0 Razionale a Sortable. In questo caso il metodo compareTo riduce i due razionali allo stesso denominatore
indica che o1 è uguale ad o2 (consistenza con equals). quindi confronta i relativi numeratori:

Segue un esempio d’uso di Sortable per l’ordinamento di un array di interi. Si appronta una classe Intero public class Razionale extends Sortable{
erede di Sortable che ridefinisce il metodo compareTo secondo il confronto matematico: //come prima
public int compareTo( Sortable x ){
import poo.sortable.*; Razionale r=(Razionale)x;
class Intero extends Sortable{ int mcm=Mat.mcm( this.denominatore, r.denominatore );
private int x; int n1 =(mcm/this.denominatore)*this.numeratore;
public lntero( int x ) {this.x=x;} int n2=(mcm/r.denominatore)‘ r.numeratore;
public int get(){ return x ;} if( n1<n2 ) return -1;
public void setjint x){ this.x=x;} if( n1>n2 ) return 1;
protected int compareTo( Sortable y ){ return 0;
Intero i=(lntero)y; }//compareTo
if( this.xci.x ) return -1; }//Razionale
if( this.x==i.x ) return 0;
return 1; In un main si potrebbe avere:
}//compareTo
public String toString(){ return ”"+x;} Razionale []v=new Razionale[20];
}//lntero si riempie v con 20 oggetti razionali
Sortable. sort( v );
for( int i=0; i<v.length; i++ ) System.out.println( v[i] );
108 109
Capitolo 5 Classi astratte e interfacce

Limiti dell’approccio Dal fatto che Razionale estende (implicitamente) Object, discende che i razionali sono anche di tipo Object.
L’approccio non è applicabile se una classe i cui oggetti si vogliono ordinare, è già legata in una gerarchia di Dal fatto che Razionale implementa Comparable, deriva che gli oggetti razionali sono anche comparabili,
ereditarietà e dunque non può estendere Sortable. Es. volendo ordinare oggetti di classe Impiegato, si ossia di tipo Comparable (aumento del polimorfismo). Un array di Comparable è dunque un array di oggetti sui
potrebbe trovare che Impiegato già estende Persona e dunque non può estendere Sortable ... Sarebbe utile quali è definito il criterio di confronto.
l’ereditarietà multipla cosi che Impiegato potrebbe estendere Persona e Sortable ... Ma Java ammette solo
l’ereditarietà singola. La classe di utilità Array di poo.util
Array è progettata per fornire alcuni metodi di uso ricorrente sugli array, es. i metodi di ordinamento, ricerca
In più, se si vuole cambiare il metodo di ordinamento, si può progettare una classe erede di Sortable, es. etc. Di seguito si illustra un frammento della classe (una versione completa è fornita a parte) con i dettagli dei
quickSortable, che lascia astratto compareTo() ma ridefinisce il metodo sort() secondo quickSort. Le classi metodi di ordinamento selectionSort e bubbleSort (si riveda il cap. 2):
eredi che vogliono sfruttare quickSort possono derivare da Sortable ma devono utilizzare il metodo statico di
ordinamento di QuickSortable. package poo.util:
public final class Array{//versione completa fornita a parte
Il concetto di interfaccia private Array(){}
Anche se non è possibile per una classe ereditare simultaneamente da più di una super classe, Java
introduce un meccanismo per “simulare” l'eredità multipla: le interfacce. public static void selectionSort( Comparable []v ){
for( int j=v.length-1; j>0; j-- ){
Una classe può estendere una sola classe ma può implementare zero, una o più interfacce. int indMax=0;
for( int i=1; i<=j; i++ )
Un'interfaccia (interface) è una raccolta di intestazioni (segnature) di metodi. In più essa può ammettere if( v[i].compareTo(v[indMax])>0 )
definizioni di attributi costanti e tipi innestati (si veda più avanti nel testo). Le segnature di metodi sono indMax=i;
definizioni astratte pur senza il modificatore abstract. Un’interfaccia, cosi come una classe astratta, non è //scambia v[indMax] con v[j]
istanziabile. Una classe che implementi un'interfaccia deve fornire un’implementazione di tutti i metodi definiti Comparable park=v[j]; v[j]=v[indMax];
nell’interfaccia, diversamente la classe è astratta. v[indMax]=park;
}
In java.lang è definita la seguente interfaccia: }//selectionSort

public interface Comparable{ public static void bubbleSort( Comparable []v ){


public int compareTo( Object x ); int ius=0;//inizializzazione fittizia
}//Comparable for( int j=v.length-1; j>0; j=ius )(
int scambi=0;
Per massima generalità, compareTo() lavora su Object. compareTo(x) ritorna <0, ==0, >0 a seconda che for( int i=0; i<j; i++ )
l’oggetto this sia rispettivamente minore, uguale o maggiore di x. if( v[i].compareTo(v[i+1])>0 ){
//scambia v[i] con v[i+1]
Nota: il modificatore public davanti ai metodi di un’interfaccia è opzionale. Esso è sottinteso se assente. Comparable park=v[i);
v[i)=v(i+1j; v[i+1]=park;
Razionali comparabili ______ __ __ scambi++;
ius=i;//indice ultimo scambio
package poo.razionali; }
public class Razionale implements Comparable{ if( scambi==0 ) break;
//... come prima }//for esterno
public int compareTo( Object x ){ }//bubbleSort
Razionale r=(Razionale)x;
int mcm=(this.denominatore*r.denominatore)/mcd(this.denominatore, r.denominatore); public static int ricercaBinaria( Comparable []v, Comparable x ){...}
int n1=(mcm/this.denominatore)‘ this.numeratore;
int n2=(mcm/r.denominatore)*r.numeratore; }//Array
if( n1<n2 ) return -1;
ifj n1>n2 ) return 1; Ordinamento di razionali comparabili
return 0; In un main si può avere:
}//compareTo
}//Razionale
110 111
Capitojo 5 Classi astratte e interfacce

Razionale []v={ //esempio di caricamento In questa situazione, potrebbe essere conveniente introdurre una interfaccia FiguraPiana con i due metodi per
new Razionale(2,3), new Razionale(4,7), calcolare perimetro ed area. Imponendo a Cerchio, Triangolo e Poligono di implementare questa stessa
new Razionale(2,8), new Razionale(3,9)}; interfaccia, di fatto si ottiene di “imparentarle” e di considerarle in modo omogeneo, ad esempio
Array.selectionSort( v ); conservandone oggetti in un array di FiguraPiana. Tutto ciò si può esprimere con il seguente diagramma UML
for( int i=0; icv.length; i++ ) System.out.println( v[i] ); (si veda il cap. 21):

L’array v può essere scritto su output anche ricorrendo al servizio java.util.Arrays.toString( array ):

System.out.println( java.util.Arrays.toString( v ) );

Discussione
L’uso delTinterfaccia Comparable rende possibile approntare una classe di utilità come Array che esporta i più
comuni algoritmi di ordinamento e ricerca (lineare e binaria). Diverse varianti sono disponibili di uno stesso
metodo (overloading): ad es. oltre a selectionSort che accetta un array di Comparable, c’è una versione che La relazione con il rombo indica che Triangolo contiene 3 (molteplicità della relazione) punti. La linea
accetta un array di int e un’altra che accetta un array di doublé. Inoltre, altre tre versioni sono presenti che tratteggiata terminante con una freccia bianca indica che Triangolo implementa l’interfaccia FiguraPiana.
accettano l’array e la sua dimensione specifica (size) cosi consentendo di lavorare su array incompletamente Cerchio estende Punto e implementa FiguraPiana. Ovviamente, un’interfaccia può far parte di unpackage
riempieto. esplicito ed essere raccolta in un file. Essa va compilata come le classi. Ponendo FiguraPiana in
poo.geometria si ha:
Questo modo di opeare, come si vedrà nel seguito, è ampiamento sfruttato dalla libreria di Java (API). Per
avvalersi di un metodo qualsiasi di ordinamento di Array, è sufficiente che una classe applicativa implementi package poo.geometria;
Comparable. public interface FiguraPiana{
doublé perimetro();
Quando una classe implementa Comparable, si dice che i suoi oggetti dispongono dell'ordinamento naturale. doublé area();
}//FiguraPiana
Si ribadisce che l’approccio basato sull'interfaccia lascia libera una classe di ereditare da una super classe
come opportuno. Non sussistono più i limiti riscontrati con il metodo basato sulla classe astratta Sortable. package poo.geometria;
public class Triangolo implements FiguraPianaf
Le interfacce possono essere costruite anche per estensione (extends). Se l’interfaccia 12 estende 11, allora
banalmente in I2 si ritrovano tutte le intestazioni di metodi di 11 più quelle previste da I2. public doublé perimetro(){...}
public doublé area(){...}
Regole dT'buon progetto” di una classe Java }//Triangolo
Alla luce delle conoscenze sin qui acquisite, si può dire che il progetto di una classe, per generalità, dovrebbe:
• prevedere il metodo boolean equals(Object x) etc. per Poligono e Cerchio
• prevedere il metodo String toString()
• prevedere il metodo int hashcode() che ritorna un intero identificativo unico dell'oggetto. Se due oggetti Il discorso può proseguire ulteriormente come segue. Da Cerchio si può derivare Sfera che è una figura
sono uguali secondo equalsQ, allora il loro hashcode dev’essere uguale. Tuttavia oggetti non uguali solida. A questo punto si potrebbe definire un'interfaccia FiguraSolida che extends FiguraPiana ed aggiunge
possono avere lo stesso valore di hashcode. Per definire i metodi equals() e hashCode() occorre prestare metodi come doublé areaLaterale() e doublé volume() etc. Si suppone che il centro di una sfera appartenga al
attenzione ai campi (di norma immutabili) che identificano un oggetto, es. per una persona potrebbero piano X-Y, che un cilindro sia appoggiato sul piano X-Y etc.
essere cognome e nome o il campo codice fiscale, per uno studente la matricola etc. Per esempi si
rimanda più avanti nel corso
• implementare l’interfaccia Comparable e dunque il metodo compareTo, se si prevede che gli oggetti
debbano essere assoggettati ad ordinamento o comunque a confronti (es. per ragioni di ricerca).

Un altro esempio d’uso dellejnterfacce


Un’interfaccia consente di accomunare classi che diversamente resterebbero isolate e dunque trattate ad hoc.
Consideriamo una semplice gerarchia: dalla classe Punto si deriva Cerchio, che in più aggiunge il raggio.
Ricordiamo che in precedenza abbiamo definito una classe Triangolo che contiene tre punti, una classe
Poligono (supposto convesso) che contiene n>=3 punti. Triangolo e Poligono non estendono Punto ma
contengono (relazione di composizione has-a) punti. Ovviamente queste tre classi Cerchio, Triangolo e
Poligono non condividono nulla (eccetto che derivano da Object).

112 113
Capitolo 5 Classi astratte e interfacce

Ovviamente in FiguraSolida si ritrova il perimetro che non ha senso in una figura a 3 dimensioni. Per usare queste costanti in una classe cliente, basta "implementare” l'interfaccia come segue:
Implementando FiguraSolida in Sfera si potrebbe codificare perimetro() in modo da generare un errore
(eccezione). Il metodo area(), invece, si può intendere che calcoli l’area totale. Nel caso della sfera area package poo.util;
laterale e area totale coincidono. In un Cilindro, altro possibile erede di Cerchio, le due aree sono distinte. public class Client implements Costanti{//demo
public static void main( Stringi] args ){
L’interfaccia FiguraSolida: _________________________________ ___________ System.ouf.printf(''PI=%1.5f%n",P/);
System. ouf.printf("e=%1.5f%n", 5);
public interface FiguraSolida extends FiguraPiana{ System.0(v/.printf(’’SQRT(2)=%1,5f%n",SQRT 2);
doublé areaLaterale(); System.ouf.printf("SEC_PEFLDAY=%5d%n'',S5C_P5fl_D/4y);
doublé volume(); }//main
}//FiguraSolida }//Client

Seguono alcune classi concrete: Esercizi


1. Programmare altre classi eredi di Figura come Quadrato, Rombo, Triangolo,Trapeziolsoscele, etc. La
public class Sfera extends Cerchio implements FiguraSolidaj classe Triangolo potrebbe esportare un metodo per conoscere il tipo di triangolo etc.
2. Programmare una classe Cono che estende Cerchio e implementa l’interfaccia FiguraSolida.
public doublé perimetro(){ throw new UnsupportedOperationException();} 3. Analizzare le dichiarazioni che seguono ed individuare e motivare ogni situazione di errore presente.
public doublé area(){ return 4‘ Math.PI‘ raggio*raggio;}
public doublé areaLaterale(){ return 4*Math.PI‘ raggio*raggio;} interface T 1{ void foo();}
public doublé volume(){ return (4*Math.Prraggio‘ raggio*raggio)/3;} interface T2{ int foo(int i);}
}//Sfera interface T3{ int foo();}
interface T4 extends T1, T3{]
public class Cilindro extends Cerchio implements FiguraSolida{ class A{
private doublé altezza; public int foo() { return 0;}
}//A
public doublé perimetro(){ throw new UnsupportedOperationException();} class B extends A implements T1, T2{
public doublé area(){ return areaLaterale()+2*raggio*raggio*Math.PI;} public void foo() { System.out.printlnCvoid foo()“; }
public doublé areaLaterale(){ return 2*Math.PI*raggio*altezza;} public int foo(int i) { return 1;}
public doublé volume(){ return raggio*raggio*Math.PI*altezza;} }//B
}//Cilindro class C extends A implements T3{
public int foo() { return 0;}
A questo punto si è visto una gerarchia di classi ancorata sulla classe astratta Figura e una collezione di classi }//C
di figure basate su interfacce e punti. Qual è l’approccio migliore ? Non c’è una risposta assoluta: tutto class D extends A implements T 1{
dipende dalle esigenze dell’applicazione. Se sono sufficienti le dimensioni, si può optare per la prima }// D
soluzione, se servono i punti-vertici si può passare all'altra organizzazione. Class E extends A implements T3,T4{
}//E
È possibile “riconciliare" le due gerarchie di classi di figure ? La risposta è si. È sufficiente che la classe
astratta Figura implementi l’interfaccia FiguraPiana. Diventa cosi possibile introdurre ad es. un array di
FiguraPiana in cui si possono collocare oggetti-figure dell’una o dell’altra gerarchia.

Un’interfaccia come pacchetto di costanti ___________


Come caso particolare un’interfaccia potrebbe essere utilizzata per esporre alcune costanti di uso frequente.
Vediamo un esempio.

package poo.util;
public interface Costanti (
final doublé P/=3.14159; //pi greco
final doublé 5=2.71828; //numero di Nepero
final doublé SQPT_2= 1.41421; //radice quadrata di 2
final int SEC_PEfì_DAY=86A00\ //secondi in un giorno
}//Costanti
114 115
Capitolo 6:
Classi stringhe di caratteri

Si è già detto che in Java le stringhe e gli array sono oggetti, dunque sono allocati nell’heap a seguito di una
operazione new (implicita o esplicita).

String s="casa”; //creazione implicita mediante aggregato di caratteri

Mentre le classi degli array sono sconosciute al programmatore e note solo al compilatore, le stringhe
appartengono alla classe String di java.lang, che fornisce diversi metodi per la loro manipolazione (si
consultino le API di Java). Gli oggetti String sono immutabili: una volta creata una stringa, essa non può più
essere modificata. Tuttavia, data una variabile String s si può sostituire in s il riferimento ad un oggetto stringa
con il riferimento ad un altro oggetto String.

Gli oggetti String sono dotati del confronto naturale (compareTo()) o lessicografico (il primo carattere diverso
da sinistra, se esiste, tra due stringhe s1 ed s2, stabilisce se s1 precede o segue o s2). Il confronto è case-
sensitive. Es. “casa".compareTo(“casaBlanca") è <0 (il carattere nullo dopo la seconda 'a' di casa, precede B’)
etc. Si ricorda che lo spazio ' ' precede le lettere e le cifre, e che le lettere maiuscole 'A'..7 ’ precedono le
minuscole 'a’..’z' (si suggerisce di consultare l’alfabeto UNICODE che nelle prime 128 posizioni include
l'alfabeto ASCII).

Anche equals() è case-sensitive. Esiste equalslgnoreCase() che verifica l’uguaglianza ignorando il caso dei
caratteri.

Metodi di String di uso ricorrente


String s=new Stringi); //crea una stringa vuota, equivalente a ""

String s1=new Strmgf'Java is fantastici"); //utilizzo esplicito del costruttore di copia


I caratteri di s1 sono indiciati a partire da 0 a s1.length()-1 (s1.length()==18)

char charAtf indice ) ritorna il carattere alla posizione indice: 0<=indice<length()

s1.charAt(3)=='a’, s1.charAt(6)==’i’ etc.

int indexOf( char x )


ritorna il primo indice nella stringa dove ricorre il carattere in x (muovendosi in avanti), o -1 se x non c’è
int indexOff char x, int da)
come il precedente, ma partendo dalla posizione da

int lastlndexOf( char x )


ritorna l’ultimo indice nella stringa this del carattere x, o -1 se x non c’è
int lastlndexOff char x, int da )
come il precedente, ma partendo a ritroso dalla posizione da

int i=s1.indexOf(‘ ’); //ad i si assegna l’indice 4


i=s1.indexOf(‘ ’,i+1); //ad i si assegna l’indice 7

int indexOff string s )


ritorna il primo indice di partenza della sottostringa string, -1 se string non è sottostringa
int indexOff string s, int da )
come il precedente ma la ricerca parte dalla posizione da
117
Capitolo 6 Classi stringhe di caratteri

Esempio
int j=s1.indexOf("is“); II] prende il valore 5 Input si legge una linea contenente cognome e nome di una persona. Il cognome può essere preceduto da
j=s1.indexOf("tast",j+1); Ila j si assegna 11 spazi. Il nome può essere seguito da spazi. Tra cognome e nome esiste almeno uno spazio.

String substring( int da, int a ) Output, si deve scrivere l'iniziale del nome seguita da quindi da uno spazio e quindi dal cognome.
ritorna la sottostringa di this tra gli indici da e a (escluso)
String substring( int da ) Esempio di input: Gosling James INVIO
come il precedente ma sino a length() (escluso) Output corrispondente: J. Gosling

String s2=s1 .substring(0,4); //s2 prende come valore “Java" public class TestString (
int i=s1.lastlndexOf(‘ ‘); lf\ punta al secondo spazio public static void main( Stringo args ){
String s3=s1 .substring(i); //sottostringa da i a fine stringa: “fantastici" Scanner sc=new Scanner(System.in);
System.out.printlnfFornisci cognome e nome di una persona ");
String toUpperCase(), String toLowerCase() String linea=sc.nextLine(); //legge sino al fine linea
s1=s1.toUpperCase(); //cambia la stringa in s1 con un’altra che è s1 tutta in maiuscolo linea=linea.trim(); //elimina spazi iniziali e finali
int i=linea.indexOf(' '); //trova primo spazio
static String valueOf( tipo_di_base o Object ) String cognome=linea.substring(0,i); //estrae cognome
ritorna la stringa corrispondente ad un valore di un tipo di base o un tipo oggetto //salta spazi
while( i<=linea.length() && linea.charAt(i)==' ' ) i++;
String s3=String.valueOf( 150); //s3 prende la stringa "150", etc. String nome=linea.substring(i); //estrae nome
s3=String.valueOf( new Razionale(3,5) ); //s3 prende il toString del razionale System.out.println(nome.charAt(0)+“. ”+cognome);
}//main
String trim() }//TestString
String s=" a bad world Sir! -.trim(); Il s prende la stringa "a bad world Sir!"
È possibile in alternativa avvalersi di lastlndexOf come segue:
char[] toCharArray()
ritorna la stringa di caratteri sotto forma di array di char public class TestString (
public static void main( Stringo args ){
String toString() System.out.println(“Fornisci cognome e nome di una persona “);
Scanner sc=new Scanner(System.in);
int compareTof String ) String linea=sc.nextLine();
int compareTolgnoreCase(String) linea=linea.trim();
int i=linea.indexOf(' ');
if( "casa".compareTo("baco") < 0 ) è false in quanto "casa" segue "baco" String cognome=linea.substring(0,i);
i=linea.lastlndexOf(‘ '); //trovato “in avanti” a partire da 0
boolean matchesf String regex ), String replaceAllf String regex, String other ) //i=linea.lastlndexOf(‘ ',linea.Iength());
saranno approfonditi più avanti nel corso, parlando delle espressioni regolari //fa la ricerca “a ritroso" a partire dalla fine
String nome=linea.substring(i+1 );
String concat( String other ) System.out.println(nome.charAt(0)+". ”+cognome);
restituisce una nuova stringa ottenuta concatenando a this la stringa denotata da other }//main
}//TestString
s1=s1 ,concat(" Ok Watson?"); //s1 ora vale "Java is fantastici Ok Watson?"
Proliferazione di stringhe garbage
Equivalente a: s1+=“ Ok Watson?"; L’immutabilità delle stringhe comporta che molti metodi costruiscono una nuova stringa e la ritornano, cosi che
un vecchio oggetto string venga buttato via e sia rimpiazzato da uno nuovo. Si osservi la seguente cascata di
static String format( String, Object... v ) operazioni, alla luce del modello di memoria:
ritorna una stringa in cui i valori (di tipi primitivi o Object) del vararg v sono formattati secondo la stringa
formato (primo parametro di format). Si riveda il metodo System.out.printf nel cap. 1. Es. String s="La“; (1)
s=s+“ tana "; (2)
String s=String.format("x=%1.2f y=%5d", x, y) dove x è un doublé di cui interessano 2 cifre frazionarie, y è int. s+=" del lupo": (3)

118 119
Capitolo 6 Classi stringhe di caratteri

int []v=new intfargs.length];


for( int i=0; kv.length; i++ )( v[i]=lnteger.parselnt( args[i] );}
System.out.println( Arrays.toString( v ) ); //v iniziale
poo.util.Array.bubbleSort(v);
System.out.println( Arrays.toString( v ) ); IN ordinato
}//main
(2) }//TestArguments

Si è detto che gli argomenti di un programma sono stringhe poste sulla linea di comando, separate da spazi.
Nel caso in cui certi spazi devono far parte di una stessa stringa argomento, è sufficiente racchiudere il tutto
Al tempo (2) s non riferisce più l’oggetto stringa contenente "La" quanto il nuovo oggetto stringa nato dalla tra “ e “:
concatenazione (2) che contiene "La tana". Similmente al tempo della terza concatenazione s riferisce il nuovo
oggetto String appena creato col valore “La tana del lupo”. Gli oggetti creati (1) e (2) sono diventati garbage e c:\poo-java\java poo.string.Prog "A e B” "C e D“ INVIO
la loro memoria può essere raccolta dal garbage collector.
In questo caso vengono trasmessi al main di Prog due argomenti:

"A e B" e "C e D"

La possibilità di passare argomenti ad un programma daH’interno di Eclipse si basa sulla costruzione di una
“configurazione di run" (si sceglie l'opzione Run As->Run Configurations ...) in cui si specifica il progetto, il
package e la classe col main da lanciare, e gli argomenti da fornire (nel tab Arguments->Program arguments:).
Eventuali opzioni da passare alla Java Virtual Machine (JVM) si possono inserire nel tab VM arguments.

Classi StringBuffer e StringBuilder


Sono due classi di java.lang di stringhe mutabili che utilizzano un array sottostante scalabile dinamicamente.
In assenza di ambiente multithread (si veda più avanti nel corso) è preferibile (in quanto più efficiente)
utilizzare uno StringBuilder. Rispondono a stessi metodi quali (per ulteriori dettagli, consultare le API di Java):
Nonostante ci sia il garbage collectore, molto spesso è importante controllare la generazione “disinvolta” di
oggetti stringhe garbage, che degradano le prestazioni dell’applicazione. Quanto detto per gli oggetti String char charAt( indice )
può essere ripetuto anche per altri tipi di oggetti. significato come per la classe String
void setCharAt(indice i, char x)
Argomenti di un programma sostituisce il carattere in posizione i con quello x
È noto che il metodo main riceve un array di String. I valori di questo array sono specificati, nella riga di void inserti int pos, valore di un tipo di base o oggetto )
comando che lancia l’esecuzione, subito dopo il nome del programma, separandoli da spazi: inserisce a partire da pos, 0<=pos<=length(), il valore di un tipo di base o oggetto, convertiti a stringa
void delete( int inizio, int fine )
c:\poo-java\java poo.string.TestArguments 10 8 12 -3 5 7 INVIO rimuove tutti i carattere da inizio a fine-1
int indexOf( String s )
In questo caso viene chiesta l'esecuzione del main della classe TestArguments e viene costituito un array di int lastlndexOf( String s )
stringhe es. args con i valori che seguono il nome del programma: args[0]=“10” args[1]="8” args[2]=“12” ...Il significato come per la classe String
programma può quindi consultare l’array di stringhe ottenuto e prelevarvi i valori (String) trasmessi. Nel StringBuilder appendi valore di un tipo di base o oggetto )
programma che segue si costruisce un array di interi i cui valori sono forniti come argomenti del programma, si aggiunge alla fine della stringa un valore di un tipo di base o un oggetto
ordina l’array e si mostra il contenuto. In generale, l’uso di argomenti in un programma ha senso quando sono int length()
in gioco pochi valori. Diversamente, occorre effettuare le usuali operazioni di input es. attraverso uno Scanner come per String
void setLengthf len )
package poo.string; fissa la lunghezza a len; es. se len è 0, "resetta" o svuota la stringa; len deve essere >=0
import java.util.*; void clear()
public class TestArguments { svuota la stringa
public static void main( String []args ){ String toStringQ
if( args.length==0 ){ void reversel)
System.out.println("Argomenti assenti"); System.exit(-I); inverte il contenuto della stringa, “casa” è sostituita con “asac”

120 121
Capitolo 6 Classi stringhe di caratteri

Il metodo appendo riceve un valore di un tipo primitivo (int, char, boolean, doublé etc.) o un oggetto, lo La classe StringTokenizer
converte a stringa di caratteri e concatena questi caratteri al contenuto attuale dello string builder (array) se Spesso si ha una stringa e si desidera frammentarla nei suoi costituenti (token). L'esempio classico è una
necessario espandendone prima la dimensione. linea di testo costituita da parole alfanumeriche, separate una dall’altra da spazi bianchi o segni di
punteggiatura. Ovviamente, lavorando con i metodi di String si potrebbe agevolmente ottenere la
L’uso oculato di uno StringBuilder suggerisce di dimensionarlo inizialmente ad una capacità opportuna per scomposizione. Tuttavia, la classe StringTokenizer di java.util permette una soluzione più intuitiva. I costruttori
evitare la riallocazione dell’array sottostante che avrebbe effetti simili a quelli visti con le stringhe garbage. Il più interessanti sono due:
costruttore di default StringBuilder() crea uno string builder con capacità iniziale di 16 caratteri. Il costruttore
StringBuilder(capacity) crea uno string builder con una capacità iniziale voluta. Il metodo accessore int StringTokenizer( String string, String delimitatori )
capacityf) consente di ispezionare il valore corrente della capacità. StringTokenizer( String stnng, String delimitatori, boolean ritornoDelimitatori )

Si considera una classe C che ammette un array a di doublé ed un intero size (dimensione effettiva di Nel primo caso i delimitatori consentono di individuare il prossimo token, ma per il resto sono saltati. Nel
riempimento dell’array) come variabili di istanza. Si vuole scrivere il metodo toString() di C. Non esiste una secondo caso, se il boolean ritornoDelimitatori è true, i delimitatori non solo sono utilizzati per ottenere i token,
soluzione “perfetta-. Ogni volta che si invoca il toString() occorre creare una nuova String che contenga lo ma essi stessi sono ottenibili come token. Se il boolean è false, il secondo costruttore è equivalente al primo. Il
stato dell’oggetto this sotto veste di stringa. Si mostra una soluzione “classica” basata sulla concatenazione di metodo che ritorna il prossimo token da uno string tokenizer è
oggetti String, ed una basata su uno StringBuilder dimensionato appropriatamente. Gli elementi di a vengono
separati da 7 e avviluppati tra una [ e una ]. L’uso di uno StringBuilder può essere favorevole dal punto di vista String nextToken()
temporale
Il metodo che controlla se esistono altri token nella stringa è:
public class C{
private doublé []a; //creato nel costruttore boolean hasMoreTokensQ
private int size; //dimensione effettiva di a
Segue un esempio di tokenizzazione di una linea letta da tastiera:
public String toString(){ //soluzione basata su concatenazione di String
String s=[", import java.util.*;
for( int i=0; ksize; i++ ){
s+=String.format( “%1.2f",a[i] ); Scanner sc=new Scanner( System.in );
if( ksize-1 ) s+=\ //,+spazio String linea=sc.nextLine();
} StringTokenizer st=new StringTokenizer( linea, * ) ; //esempio di separatori
s+=T;
return s; while( st.hasMoreTokens() )( //tokenizzazione
}//toString String tk=st.nextToken();
}//C System.out.printlnfToken ottenuto: “+tk );

public class C{
private doublé []a; //creato nel costruttore Tokenizzazione mediante uno Scanner
private int size; //dimensione effettiva di a Oltre che mediante uno StringTokenizer, la suddivisione in token di una stringa-linea può essere ottenuta
tramite la classe Scanner e metodi associati. Nell'ipotesi che i delimitatori dei token siano caratteri non
public String toString(){ //soluzione basata su StringBuilder alfabetici, si può operare come segue
StringBuilder sb=new StringBuilder(500); //esempio
sb.append(‘[‘); String linea=...
for( int i=0; ksize; i++ ){ Scanner sl=new Scanner( linea ); //scanner aperto sulla stringa linea
sb.append( String.format( H%1.2f',a[i] ) ); sl.useDelimiter("[AA-Za-z]+"); //fissa i delimitatori con una espressione regolare - si veda il cap. 14
if( i<size-1 ) sb.append(", "); //,+spazio
} while( sl.hasNext() ){ //tokenizzazione
sb.append(’]’); String tk=sl.next();
return sb.toString(); processa tk
}//toString }
}//C
Mentre per l’approfondimento delle espressioni regolari si rimanda al cap. 14, si nota la flessibilità e sinteticità
nella definizione dei delimitatori. I metodi di Scanner sono utilizzati per ottenere in sequenza i token, saltando i
delimitatori.
122 123
Capitolo 6 Classi stringhe di caratteri

Esercizi
Anziché manipolare token String con la coppia hasNext()/next() si possono scandire interi o doublé con 1. Leggere una stringa da tastiera e verificare, indipendentemente dal caso delle lettere, se essa è palindroma
hasNextlnt()/nextlnt(), hasNextDouble()/nextDouble(). o meno. Una stringa è palindroma se si legge identicamente da sinistra a destra e viceversa. È palindroma
“anna", non è palindroma “anno".
La richiesta di un token che non esiste (es. hasNextQ è false), solleva l’eccezione NoSuchElementException(). 2. Leggere da tastiera una parola del vocabolario italiano e scrivere su output, una per linea, tutte le sillabe
che la compongono. Ad es. per la parola “difficile” si deve avere “d ir l i ” “ci” “le", per la parola “guaina" la
Caso di studio: valutazione di un’espressione aritmetica intera decomposizione è “gua" “i" “na” etc. Si suggerisce di consultare le regole fondamentali di sillabazione in
L'input di un programma è costituito da un’espressione intera con gli usuali operatori binari +,-,* e /. Per italiano ed implementarle (anche in versione parziale) nel programma.
semplicità, non sono ammessi spazi e la valutazione procede strettamente da sinistra a destra, senza 3. Modularizzare il programma valutatore di espressioni aritmetiche intere visto in questo capitolo
considerare le precedenze matematiche degli operatori. Ad esempio: 30+40*2 si valuta a 140 e non a 110. Gli organizzandolo in tre metodi: il main(), un metodo int valutaOperando(StringTokenizer st) ed un metodo int
operandi sono interi senza segno. valutaEspressionefStringTokenizer st). Il main esegue un ciclo di interazione, emette un prompt “> “ a fronte
del quale l'utente può immettere un’espressione, o digitare per terminare il programma. La valutazione
Il programma ottiene un’espressione (da riga di comando o da tastiera), la tokenizza nei suoi costituenti dell’espressione è il compito assegnato a valutaEspressione(st), che per valutare un singolo operando si
(numeri e segni di operazioni), la valuta e quindi scrive il risultato su output. Gli operatori fungono anche da appoggia a valutaOperando(st). I metodi valutaOperando(st)/valutaEspressione(st) ricevono l'oggetto
separatori degli operandi; essi vanno espressamente restituiti al programma per la valutazione del risultato. (provvisto di stato) Stringiokenizer aperto sull’espressione corrente.
4. Generalizzare il programma di cui all’esercizio 3 in modo da ammettere altresì sotto espressioni racchiuse
Semplice valutatore di espressioni aritmetiche: ____ entro parentesi ’(’ e ’)’. Un’espressione tra parentesi è sempre valutata prima. In questo modo è possibile
package poo.string; priorizzare gli operatori. Il metodo valutaOperando(st) invoca (ricorsione) valutaEspressione(st) ogni qualvolta
import java.util.*; incontra una ‘(‘, ossia un operando è costituito da una sotto espressione racchiusa tra ’(‘ e ’)’. La valutazione di
public class Espressione { una sotto espressione termina non appena si incontra un operatore del tipo ’)’. Cosi come per l'esercizio 3,
public static void main( String []args )( ipotizzare un input completo e corretto. Per la gestione di situazioni eccezionali si rimanda al prossimo
String espr=null; capitolo.
if( args.length==1 ){ 5. Leggere da tastiera una riga di testo linea, quindi un numero intero lun (supposto non minore di
espr=args[0); linea.length()) che esprime una lunghezza di linea desiderata. Generare una nuova riga di testo giustificata di
} lunghezza lun. La stringa giustificata deve avere lo stesso contenuto di caratteri di linea, non avere eventuali
else{ spazi iniziali, deve risultare di lunghezza lun. A questo scopo i “buchi" tra le parole vanno “allargati” in modo da
Scanner sc=new Scanner(System./n); raggiungere la lunghezza lun. Ad es. se lun=25 e linea è la seguente riga di testo:
System. ou/.print("Espr>K);
espr=sc.nextLine(); linea:
} | L | a | 111 a | n | a d e u P 0
Stringiokenizer st=new StringTokenizer( espr,“+-T,true ); //notare il terzo parametro true
int ris=lnteger.parse/n/(st.nextToken()); la nuova riga giustificata dovrà essere:
while( st.hasMoreTokens() ){
char op=st.nextToken().charAt(0);//ottiene l’operatore giustificata:
int num=lnteger,parselnt( st.nextToken() ); L a t a n a d e I I u P 0
switch(op){
case '+': ris=ris+num; break; Infatti, la lunghezza effettiva di linea è 16, per cui per giustificarla occorre “spalmare" 25-16=9 spazi bianchi tra
case ris=ris-num; break; i tre buchi presenti (tra “La” e ‘lana”, tra “tana e del" e tra “del" e "lupo”). Dunque ogni buco dovrà essere
case '*': ris=ris'num; break; espanso di tre spazi. In generale, ovviamente, il numero di spazi da distribuire non è un multiplo dei buchi
default: ris=ris/num; presenti, per cui non tutti i buchi saranno espansi della stessa quantità. Su suggerisce di utilizzare una classe
} di stringhe mutabili e di testare accuratamente il programma nei vari casi possibili.
}
System.ou/.println(espr+“=“+ris);
}//main
}//Espressione

124 125
Capitolo 7:__________ _
Concetti sulle eccezioni

Un metodo Java o termina normalmente producendo il suo risultato (eventualmente void) o termina in modo
eccezionale restituendo al chiamante una eccezione per segnalare che nelle condizioni in cui il metodo è stato
invocato (con i valori specificati dei parametri) esso non è grado di generare un risultato valido.

Si parla di servizio normale e servizio eccezionale prodotto da un metodo a seconda che esso termini
normalmente o in veste eccezionale.

In Java le eccezioni sono oggetti, ossia istanze di classi particolari. È possibile sollevare, catturare e gestire
un’eccezione (exception handling) generata da un metodo, dopo di che, eventualmente, il metodo potrebbe
essere ri-invocato o comunque la computazione del programma potrebbe continuare verso la sua conclusione.
Un caso particolare di eccezioni sono gli Errar, che modellano situazioni serie di errori che un programma non
si aspetta di poter gestire (catturare e ricoverare) (es. OutOfMemory errar).

Una situazione in cui può nascere un'eccezione si verifica nella classe Razionale allorquando nel costruttore si
riceve un denominatore nullo. Si può procedere come segue:

package poo.razionali;
public class Razionale implements Comparablef
private int numeratore, denominatore;
public Razionale( int num, int den ) throws DenominatoreNullo!
if( den==0 ) throw new DenominatoreNullo();
if( num!=0 ){//riduzione ai minimi termini
int n=Math.abs( num ), d=Math.abs( den );
int cd=Mat,mcd(n, d );
num=num/cd; den=den/cd;
}
if( den<0 ){ num *= -1; den *= -1;}
this.numeratore=num;
this.denominatore=den;
}//costruttore

}//Razionale

La classe DenominatoreNullo può essere programmata nello stesso package poo.razionali:

package poo.razionali;
public class DenominatoreNullo extends Exceptionf
public DenominatoreNullo(){}
public DenominatoreNullo( String msg ){
super( msg );
}
}//DenominatoreNullo

Una classe eccezione si qualifica tale perché ad es. eredita da Exception. Per il resto potrebbe avere dei
campi dati etc. come tutte le normali classi. Tali dati potrebbero essere trasmessi dal punto in cui si solleva
l’eccezione al punto in cui si effettua la gestione. In molti casi, comunque, una classe eccezione può ridursi ai
due metodi costruttori come indicato per DenominatoreNullo.

127
Capitolo 7 Concetti sulle eccezioni

Un esempio di cattura e gestione dell’eccezione DenominatoreNullo è mostrato di seguito:


Se l’eccezione è di tipo unchecked allora non esiste alcun obbligo sul programmatore. Tuttavia, se l’errore si
//in un main verifica, il programma termina con una segnalazione diagnostica.
Scanner sc=new Scanner( System.in );
Razionale []v=new Razionale[10); Come scegliere tra eccezioni checked o unchecked ?
//caricamento di v
int i=0, n=0 /‘ fittizio*/, d=0 /‘ fittizio*/; Generalmente si usano eccezioni del tipo RuntimeException in tutti quei casi in cui è il programmatore che
loop: while( i<v.length ){ può, con una sua disattenzione, generare l’errore. In queste circostanze, con un test (if) è possibile evitare
System.out.print("numeratore= “); n=sc.nextlnt(); l’insorgere dell’errore. Si usano, invece, eccezioni del tipo Exception se l’errore non dipende dal
System.out.print(“denominatore= "); d=sc.nextlnt(); programmatore ma piuttosto da condizioni esterne al programma, ad es. situazioni di errore durante
try{ operazioni di I/O, di accesso a file, di accesso alla rete di comunicazione etc.
v[i]=new Razionale(n, d); llpuò sollevare l’eccezione DenominatoreNullo
}catch(DenominatoreNullo e){//cattura e gestione eccezione Le classi di eccezioni predefinite IndexOutOfBoundsException, ClassCastException, NulIPointerException,
System.out.println(“Denominatore nullo!"); IHegalArgumentException, etc. sono eredi di RuntimeException.
System.out.println("Ridare il razionale"); continue loop;
} L’aver programmato DenominatoreNullo come classe erede di Exception è stata una scelta di esempio, per
i++; //conta questo razionale scopi dimostrativi. Siccome in realtà l’errore è perfettamente evitabile dall'utente, si poteva utilizzare la strada
}//while unchecked. Vediamo come.

L’istruzione continue loop consente di continuare immediatamente il ciclo etichettato (il while) saltando tutte le package poo.razionali;
istruzioni che seguono (nell’esempio la i++) sino alla fine del corpo del ciclo. Dunque, in caso di eccezione public class DenominatoreNullo extends RuntimeException{
DenominatoreNullo, la i non viene incrementata e si torna a leggere una nuova coppia <numeratore, public DenominatoreNulloQO
denominatore> per la stessa posizione i dell’array v. public DenominatoreNullo( String msg ){super( msg );}
}//DenominatoreNullo
Gerarchia di classi di eccezioni
package poo.razionali;
public class Razionale implements Comparable{
private int numeratore, denominatore;
public Razionale( int num, int den ) (
if( den==0 ) throw new DenominatoreNullo();
//throw new RuntimeException(“DenominatoreNullo”);

}//costruttore

}//Razionale

Si nota che è possibile utilizzare direttamente la classe RuntimeException, passando una stringa-messaggio
al suo costruttore. Questa alternativa, comoda, evita in molti casi di dover introdurre una propria classe di
eccezioni.

In presenza di un’eccezione runtime, il main che carica l’array di razionali si modifica come segue:

Scanner sc=new Scanner( System.in );


Razionale []v=new Razionateli0];
Le eccezioni sono oggetti di classi che possono essere programmate o come eredi di Exception o come eredi //carcamento di v
di RuntimeException.Le eccezioni che derivano da Exception sono dette checked (o controllate), quelle che int i=0, n=0 /‘ fittizio*/, d=0 /‘fittizio*/;
derivano da RuntimeException unchecked(o non controllate). loop: while( icv.length ){
System.out.print('numeratore= "); n=sc.nextlnt();
Quando un’eccezione è del tipo checked allora il programmatore non può ignorarla: è necessario aggiungere System.out.print(“denominatore= “); d=sc.nextlnt();
la clausola throws NomeEccezione alla segnatura del metodo e, presso il chiamante del metodo, occorre if( d==0 ){
utilizzare il blocco try-catch o propagare l’eccezione (si veda più avanti). System.out.printlnfDenominatore nullo!");
128 129
Capitolo? Concetti sulle eccezioni

System.out.printlnfRidare il razionale"); continue loop; if( d==0 ){


} System.out.println(“Denominatore nullo!");
v(i]=new Razionale(n, d); //il costruttore non può più sollevare l’eccezione System.out.println("Ridare il razionale");
i++; //conta questo razionale continue loop;
}//while }
v[i]=new Razionale(n, d); //non può sollevare l’eccezione checked
Nel nuovo main l’eccezione DenominatoreNullo è evitata con il test preliminare. Naturalmente, se non ci si i++; //conta questo razionale
cautela (svista del programmatore che “assume” un denominatore sempre non nullo e dunque non esegue il }//while
test if(d==0) ... ), allora se l’eccezione RuntimeException viene sollevata, essa causa la terminazione }//main
anticipata del programma. Si nota esplicitamente che anche un’eccezione di tipo RuntimeException può
essere catturata e gestita con un blocco try-catch. In questo caso, pur essendo checked l'eccezione, si è deciso di non catturarla né gestirla, dal momento che il
test di denominatore nullo è eseguito prima di creare il razionale, cosi come è stato fatto per il caso di
Vincoli sulle eccezioni checked eccezione runtime. È evidente che l’eccezione non può più generarsi. Tuttavia, il carattere checked impone, in
Se un metodo m2 di un oggetto o2, invocato dall’interno di un metodo m 7 di un oggetto o1, può sollevare assenza di gestione, di etichettare il metodo come uno che può sollevare (meglio propagare)
un’eccezione checked allora: DenominatoreNullo. Queste considerazioni valgono anche quando un metodo chiamante è incapace di gestire
l’eccezione e quindi non ha altra strada che propagarla.
• o si avviluppa l’invocazione di m2 in un blocco try-catch
• o si dichiara che il metodo m / può a sua volta propagare l’eccezione. Naturalmente, il fenomeno della propagazione può riguardare anche eccezioni unchecked per le quali:
• non esiste l’obbligo di dichiarare, nella testata dei metodi, che essi sollevano eccezioni unchecked
La propagazione di un’eccezione può essere esplicita anche dall’interno di un blocco catch con l’istruzione • non esiste l’obbligo di catturare e gestire le eccezioni unchecked mediante blocchi try-catch
throw. Di seguito si schematizzano i due possibili comportamenti. • non esiste l’obbligo per un metodo che non cattura un’eccezione unchecked, di dichiarare che esso
ripropaga l’eccezione al chiamante
Strada 1: gestione Strada 2: propagazione
Ovviamente, l’assenza di obblighi mentre semplifica la scrittura del codice, non significa “tranquillità": se
accade un’eccezione unchecked non gestita, il programma termina in errore!
tipo ritorno mf(params){ tipo_ritorno mt(params) throws TipoEccezione {
Il blocco try-catch
try{ o2 m2(parametri); Può includere più istruzioni ciascuna delle quali può sollevare una o più eccezioni. Può ammettere più
o2.m2(parametri); clausole catch per catturare e gestire ciascuna possibile eccezione generata aH’intemo del corpo try-catch.
}catch(TipoEccezione e){ Può ammettere una (opzionale) clausola finally che, se presente, specifica codice che viene eseguito dopo la
}//m1
azioni ‘correttive" gestione di una qualsiasi eccezione e comunque sempre all’uscita del blocco try (anche senza che si siano
verificate eccezioni)
In questo caso, se m2 si conclude in modo
eccezionale, non essendoci gestione in m1,
l'eccezione è tacitamente propagata al try{
chiamante di m1 etc istruzione J1;
La clausola throws può specificare una lista di istruzione_i2;
nomi di classi di eccezioni, separate da
Dopo la gestione dell’eccezione, m2 throws CE1, CE2,...
potrebbe essere re-invocato }catch(ClasseEccezioneA eA){ gestione e A }
catch(ClasseEccezioneB eB){ gestione e B }

Se DenominatoreNullo estende Exception, si può anche avere: [finally! azioni finali}]

public static void main( String []args ) throws DenominatoreNullo! //scelta di non trattare l'eccezione Attenzione: L’uso in una clausola catch di una classe base di eccezioni (es. Exception) consente di catturare
Scanner sc=new Scanner( System.in ); più di un tipo di eccezioni. Ovviamente, in questi casi, l'exception handler può avvalersi di instanceof per
Razionale []v=new Razionale! 10]; scoprire il tipo specifico dell'oggetto eccezione e intraprendere le azioni correttive corrispondenti
//carcamento di v
int i=0, n=0, d=0; Il blocco try-finally
loop: while( icv.length ){ É utile, indipendentemente dalla gestione di eccezioni, per pianificare l’esecuzione di un blocco di istruzioni e
System.out.print(“numeratore= "); n=sc.nextlnt(); quindi (in ogni caso) concludere con delle azioni finali (es. chiusura di un file, di una connessione di rete,
System.out.print("denominatore= "); d=sc.nextlnt(); apertura di un lucchetto di sincronizzazione etc.):

130 131
Capitolo 7 Concetti sulle eccezioni

Caso di studio: risoluzione di un sistema di equazioni lineari


istrl;
istr2; Si considerano sistemi di n equazioni lineari in n incognite. È noto che esistono più algoritmi risolutivi, es.
mediante triangolazione di Gauss, regola di Cramer, uso della Matrice Inversa, etc. Per consentire ad un
}finally{ programma di avvalersi di una qualunque tecnica risolutiva, avendo sempre a disposizione uno schema
azioni finali comune di riferimento, si introduce una classe astratta in cui il metodo fondamentale risolvi() è
} necessariamente astratto.

Si nota che le azioni del corpo finally {} sono eseguite qualunque sia il modo di uscita dal blocco try, es. anche Una classe astratta Sistema:
tramite una return. package poo.sistema;
public abstract class Sistemai
Flusso del controllo private int n;
Si consideri la catena dinamica di chiamate a metodi: public int getN(){ return n ;}
public Sistema( doublé [][]a, doublé []y )(
o1.m1(...)->o2.m2(...)->o3.m3(...) if( a.length != y.length )
throw new RuntimeExceptionfSistema Inconsistente");
In assenza di eccezioni, m1 si sospende in attesa che m2 finisca il suo compito, m2 a sua volta si sospende in for( int i=0; ka.length; i++ )
attesa che m3 finisca il suo compito etc. Quando m3 termina, riprende m2. Quando m2 termina riprende m1. if( a[i].length != a.length )
throw new RuntimeExceptionfSistema Inconsistente");
Tutto ciò è normale. this.n=a.length;
}
La relazione chiamante->chiamato è importante non solo per la restituzione dei risultati ma anche in presenza public abstract double[] risolvi();
di eccezioni. }//Sistema
fo rw a rd -----*
Il costruttore di Sistema si occupa di controllare che il sistema sia ben definito, ossia che la matrice a dei
m !(...) i n 2 ( ... ) m 3(...)
coefficienti sia effettivamente quadrata e che la dimensione comune di righe e colonne è uguale alla
dimensione del vettore y dei termini noti.

La classe Sistema memorizza solo la dimensione n del sistema, ma non gli array. Ogni particolare metodo
realizzativo usa uno schema ad hoc per i dati (es. Gauss usa una matrice n‘ (n+1) etc.). Il metodo risolviQ
ritorna il vettore di n incognite x, se il sistema è determinato. Diversamente il metodo si conclude sollevando
l'eccezione unchecked “SistemaSingolare".

Il metodo di Gauss
È noto dalla matematica che un sistema di n equazioni lineari A*X=Y si trasforma in uno equivalente se:

• si scambiano due righe (due equazioni)


• si moltiplica una riga per uno scalare
• si sostituisce una riga r con il risultato della combinazione lineare tra r ed un'altra riga r1moltiplicata per uno
scalare c: r=r± rc
Se m3 anziché fornire il servizio ordinario genera un’eccezione e1, allora tale eccezione (che sostituisce il
normale risultato) viene riportata ad m2 nel punto dove m2 chiama m3. Se in questo punto è presente un
L'algoritmo di Gauss si compone di due fasi: (a) triangolazione in avanti, (b) sostituzione a ritroso. Durante la
exception handler, allora è possibile che m2 risolva il problema e magari re-invochi m3 con nuovi parametri
fase (a) si rende la matrice dei coefficienti A triangolare superiore, ossia si azzerano (attraverso combinazioni
(senza che m1 ne sappia niente). Ma se in m2 non è presente alcun gestore di eccezione, allora anche m2 lineari) i coefficienti al di sotto della diagonale principale. È importante che le combinazioni lineari coinvolgano
termina in errore e l’eccezione viene portata ad m1 nel punto dove m1 chiama m2. Se m1 non tratta anche i termini noti. Durante la fase (b), il sistema triangolare viene risolto partendo dall'ultima equazione: si
l’eccezione allora essa viene propagata ancora a monte, al suo chiamante, sino a raggiungere eventualmente calcola x[n-1], si sostituisce il valore di x[n-1] nella penultima equazione e si calcola quindi x[n-2] etc. sino a
il main che se propaga ancora allora fa si che il programma termini definitivamente in errore. La figura x[0|.
precedente sintetizza il flusso del controllo in assenza di gestione delle eccezioni.
Per facilitare la scrittura del codice, è utile copiare il vettore dei termini noti Y come ultima colonna della
matrice dei coefficienti che diviene rettangolare n‘ (n+1).

132 133
Capitolo 7 Concetti sulle eccezioni

Fissata una posizione diagonale <j,j>, occorre assicurare che a[j](j] sia diverso da 0. Se non lo è si cerca //sottrai dalla riga i-esima la riga j-esima moltip per coeff
(pivoting) una riga p, se esiste, tra j+1 ed n-1, tale che a[p][j] sia diverso da 0 (il calcolo numerico suggerisce for( int k=j; k<n+1; k++ ) a[i][k] = a[i][k]-a[j][k]*coeff;
che una scelta migliore, che riduce gli errori, consiste nel trovare la riga p tale che a[p][j] sia massimo in valore }
assoluto nella colonna j tra le righe da j+1 a n-1). Se una tale riga non esiste, allora il sistema è singolare }//for interno azzeramento
(ammette infinite soluzioni). }//for esterno su j
}//triangolazione
Dopo aver eseguito eventualmente il pivoting, si procede ad azzerare i coefficienti nella parte bassa della
colonna j, cioè sulle righe da j+1 sino ad n-1. Detta i una tale riga, perché a[i][j] diventi 0 (nell'ipotesi che già protected doublet] calcoloSoluzione(){
non lo sia) è sufficiente valutare il coeff=a[i][jJ/a[j][j], quindi sottrarre (combinazione lineare) dalla riga i la riga j //a è triangolare superiore
moltiplicata per coeff. Considerato che a sinistra della colonna j ed al di sotto della diagonale principale già int n=this.getN();
risulta azzerata la matrice, la combinazione lineare può limitarsi ad investire le colonne dalla j-esima alla n- doublé []x=new double[n];
esima (ultima colonna della matrice a, contenente i termini noti). for( int i=n-1; i>=0; i-- ){
//secondo membro inizializzato al valore del termine noto
Per modularità, la triangolazione è affidata ad un metodo ausiliario triangolazione(). Anche il calcolo delle doublé sm=a[i][n];
incognite è affidato ad un metodo calcoloSoluzioneQ che ritorna il vettore delle incognite. Gli altri dettagli for( int j=i+1 ; j<n; j++ ) //trasporto al 2 membro dei termini relativi ad incognite già calcolate
dovrebbero essere auto-esplicativi. sm = sm - a[i][j]*x[j];
x[i]=sm/a[i][i];
Una classe Gauss: }
package poo.sistema; return x;
import poo.util.*; }//calcoloSoluzione
public class Gauss extends Sistema{
protected doublé [][]a; @Override
public Gauss( doublé [][]a, doublé []y ){ public doublet] risolvi() {
super( a, y ); triangolazione();
//genera matrice n*(n+1) dei coeff+termini noti return calcoloSoluzione();
doublé [][] copia=new double[a.length][a.length+1 ]; }//risolvi
for( int i=0; ka.length; i++ )(
System.arraycopy(a[i], 0, copia[i], 0,a[0]. length); public String toString(){
copia[i][a.length]=y[ij; StringBuilder sb=new StringBuilder(500);
} for( int i=0; ka.length; i++ ){
this.a=copia; for( int j=0; j<=a.length; j++ ){
} sb.append( String. format(‘ %5.2f‘, a[i][j]) ); //esempio
sb.appendf ');
protected void triangolazione(){
//rende a triangolare superiore } .
int n=this.getN(); return sb.toString();
for( int j=0; j<n; j++ ){ }//toString
if( Ma\.suf1icientementeProssimi(a[j][j],0D) ){//pivoting
int p s j+ 1 ; }//Gauss
for( ; p<n; p++ )
if( !Mat.suf1icientementeProssimi(a[p][j],OD) ) break; Una classe Sistemasingolare:
if( p==n ) throw new SistemaSingolareQ; package poo.sistema;
//scambia riga p con riga j public class Sistemasingolare extends RuntimeException]
double[] tmp=a[j); a[j]=a[p]; a[p]=tmp; public SistemaSingolare(){)
}//pivoting public SistemaSingolare( String msg ){ super(msg);}
//azzera elementi sulla colonna j, dalla riga (j+1)-esima all'ultima }//SistemaSingolare
for( int i= j+ 1 ; i<n; i++ )(
if( MalsutticientementeProssimi(a[i][j],OD) ){ Sistemasingolare è definita come eccezione unchecked. Tuttavia, verificare a priori che il sistema è non
doublé coeff=a[i][j]/a[j][j]; singolare (o equivalentemente che il determinante della matrice dei coefficienti a è diverso da 0) non è triviale
dal momento che calcolare il determinante è un lavoro paragonabile alla risoluzione del sistema. Tutto ciò
spiega la struttura di main proposta di seguito.
134 135
Capitolo 7 Concetti sulle eccezioni

2. Sviluppare una versione “fault-tolerant” (tollerante, cioè, al verificarsi di eccezioni) del programma valutatore
Un esempio di m a i n : ____________________________________ ______________________ interattivo di cui all’esercizio 3 del capitolo precedente. Il programma dovrebbe emettere una segnalazione di
import java.util.*; “Espressione malformata’’ non appena la valutazione dell’espressione corrente dovesse sollevare
import poo.sistema.*; un’eccezione unchecked, es. una NoSuchElementException (generata quando fallisce l’ottenimento del
public class SEL{ prossimo token da parte del metodo nextTokenQ di StringTokenizer) o NumberFormatException (generata
public static void main( String []args ) throws SistemaSingolare{ quando fallisce il metodo lnteger.parselnt( string ) in quanto la stringa parametro non contiene un intero).
System.out.printlnfSistema di equazioni lineari risolto con GAUSS"); 3. Come 2. ma con riferimento al programma di cui all’esercizio 4. del capitolo precedente. In questo caso il
Scanner sc=new Scanner( System.in ); programma deve controllare anche il corretto accoppiamento delle parentesi tonde che avviluppano sotto
System.out.printfdimensione del sistema="); espressioni.
int n=sc.nextlnt(); 4. Progettare una classe GaussDiagonale erede di Gauss che diagonalizza la matrice a, ossia azzera anche
doublé [][]a=new double[n][n]; gli elementi al di sopra della diagonale principale, e rende unitari i coefficienti diagonali. In questo caso i valori
doublé [jy=new double(n); delle incognite coincidono con i valori dei termini noti risultanti dal processo di diagonalizzazione.
doublé [jx=null; 5. Scrivere un metodo di utilità nella classe poo.util.Matrix, doublé determinante( double[][] a ), che calcola e
//lettura matrice ritorna il determinante della matrice a supposta quadrata (se non lo è occorre sollevare un’eccezione
System.out.printlnfFornisci gli "+n+"x"+n+'1elementi della matrice a per righe"); unckecked). Utilizzare l'algoritmo della triangolazione di Gauss e contare gli scambi di righe effettuati per
for( int i=0; i<n; i++ ) “sanare’’ gli zeri diagonali. Alla fine della triangolazione, il prodotto degli elementi della diagonale principale
for( int j=0; j<n; j++ ) { fornisce il valore del determinante, il cui segno va corretto moltiplicandolo per (-1 fumerò scambi $i osserva che:
System.out.print("a["+i+N,”+j+"]="); • Il determinante è nullo se uno zero diagonale non è eliminabile
a[i][j]=sc.nextDouble(); • Il metodo determinante( doublé a ) deve lavorare su una copia locale di a.
} 6. Utilizzare il metodo determinante di cui all’esercizio precedente, per progettare una classe Cramer erede di
System.out.println(); Sistema che risolve un sistema di equazioni lineari col metodo di Cramer.
System.out.println("Fornisci i(gli) "+n+" termini noti"); 7. Sviluppare un metodo di servizio double[][] matrlcelnversa( doublé [][] a ) nella classe di utilità
for( int i=0; i<n; i++ ){ poo.util.Matrix, che riceve una matrice quadrata a (se a non è quadrata si solleva una eccezione) e ritorna la
System.out.printfy["+i+"]=“); sua inversa, se esiste. Se a non è invertibile, il metodo deve sollevare un’eccezione di tipo
y[i]=sc.nextDouble(); MatriceNonlnvertibile. Come è più opportuno dichiarare MatriceNonlnvertibile: checked o unchecked? Il
} metodo matricelnversa() può conseguire, in un caso, il suo obiettivo utilizzando l’algoritmo di Gauss-Jordan.
Sistema s=new Gauss(a,y); Detta n la dimensione di a, si costruisce localmente una matrice b di dimensione nx2n. Nella prima parte nxn
System.out.println( s ); di b si ricopia la matrice a. Nella seconda parte nxn di b si imposta invece la matrice identità di ordine n. A
try{ questo punto si diagonalizza la sotto matrice nxn nella prima parte di b, avendo cura di rendere unitari gli
x=s.risolvi(); elementi diagonali ed estendere le combinazioni lineari alle intere righe di lunghezza 2n. A fine
}catch( Sistemasingolare e ){ diagonalizzazione, se il processo ha avuto successo, nella prima parte nxn di b si è riprodotta una matrice
System.out.printlnfSistema Singolare!"); identità di ordine n, nella seconda metà nxn di b si è generata invece la matrice inversa di a, che può essere
System.exit(-I); estratta e restituita.
} 8. Realizzare una classe erede della classe astratta Sistema, che risolve un sistema di n equazioni lineari in n
System.out.println( s ); //visualizza sistema triangolare incognite con il metodo della matrice inversa.
//scrivi risultati
System.out.printlnfVettore delle incognite");
for( int i=0; i<n; i++ ) System.out.printt("x["+i+"]=%1.2f%n“,x[i]);
}//main
J//SEL

Come si vede, anche se Sistemasingolare è unchecked, l’eccezione è catturata dal main e la sua “gestione”
consiste nella visualizzazione di un messaggio e nell’arresto dell’esecuzione. Senza il try-catch, si otterrebbe
un effetto analogo ma il programma terminerebbe per una eccezione non trattata.

Esercizi __
1. Nell'ipotesi che la classe DenominatoreNullo sia erede di Exception, esistono conseguenze sulla
dichiarazione dei metodi add, sub etc. che al loro interno richiamano il costruttore di Razionale. Considerando
che durante un’operazione aritmetica tra razionali, non può mai generarsi un risultato avente denominatore
nullo, in che modo si possono riscrivere “al minimo" i metodi add, sub, mul etc.?

136 137
Capitolo 8: ___
Tipi di dati astratti

Spesso le applicazioni utilizzano dati strutturati (aggregati) che si caratterizzano per le operazioni che si
debbono eseguire sui dati e non per il modo in cui questi aggregati sono rappresentati in memoria. Tutto ciò
introduce il concetto di tipo di dati astratto (ADT o abstract data type) che in Java è esprimibile in modo
naturale con una interfaccia o una classe astratta. Si tratta di un pacchetto di metodi (contratto) specificati
unicamente mediante le loro intestazioni. Un ADT è poi concretizzabile (implementabile) in diversi modi, es.
mediante array ma non solo. Una classe, da questo punto di vista, rappresenta un costrutto per implementare
un tipo di dati astratto.

Un esempio di ADTS i
Si vuole realizzare una nozione di array (vector) “più comoda” per le applicazioni, rispetto all'array nativo di
Java. Gli array nativi sono strutture dati compatte e statiche e tendono ad introdurre problemi quando si vuole
aggiungere un elemento e l'array è pieno, o quando si vuole eliminare un elemento senza creare buchi.

Un vector è pensato scalare automaticamente di dimensione ogni volta che serve, e farsi carico
trasparentemente delle eventuali operazioni di spostamento di elementi a seguito di inserimenti o rimozioni.

In quanto segue si definisce un ADT Vector mediante un’interfaccia collocata nel package poo.util. Gli
elementi sono assunti Object per generalità. Successivamente, l'utilizzo del meccanismo dei generici di Java
consentirà di migliorare in flessibilità e sicurezza la definizione ed uso dei vector.

L’ADT Vector:
package poo.util;
public interface Vector{
public int size();
public int indexOf( Object elem );
public boolean contains( Object elem );
public Object get( int indice );
public Object set( int indice, Object elem );
public void add( Object elem );
public void add( int indice, Object elem );
public void remove( Object elem );
public Object remove( int indice );
public void clear();
public boolean isEmpty();
public Vector subVectorj int da, int a );
}/A/ector

Semantica delle operazioni

int sizeQ
ritorna il numero di elementi presenti nel vettore. Gli elementi del vettore, similmente agli array, hanno indici
tra 0 e size()-1

int indexOf( Object elem )


ritorna l'indice della prima occorrenza di elem nel vettore, o -1 se elem non è presente. Si basa sul metodo
equals degli elementi.

139
Capitolo 8 Tipi di dati astratti

boolean containsf Object elem ) private void espandi(){


ritorna true se elem è presente almeno uno volta nel vettore, false altrimenti. Si basa sul metodo equals Object []arrayDoppio=new Object[array.length*2];
degli elementi. System.arraycopy( array, 0, arrayDoppio, 0, size );
array=arrayDoppio;
Object get( int indice ) }//espandi
ritorna l’elemento alla posizione indice del vettore. Sostituisce la notazione vindice] degli array nativi.
Solleva un'eccezione IndexOutOfBoundsException se indice non è compreso tra 0 e size()-1. private void contrai(){
Object []arrayMezzo=new Object[array.length/2];
Object set( int indice, Object elem ) System.arraycopy( array, 0, arrayMezzo, 0, size );
suppone il valore di indice compreso tra 0 e size()-1. Sostituisce l'elemento alla posizione indice con elem, array=arrayMezzo;
e ritorna il precedente elemento. Solleva un'eccezione IndexOutOfBoundsException se l'indice non è Incontrai
valido.
public int size(){ return size;}
void add( Object elem )
aggiunge elem come ultimo elemento del vector, espandendo la struttura se necessario. public int indexOf( Object elem ){
for( int i=0; ksize; i++ )
void add( int indice, Object elem ) if( array[i).equals( elem ) ) return I;
aggiunge elem alla posizione indice, spostando preliminarmente di un posto a destra tutti gli elementi da return -1;
indice in poi. Espande la struttura se necessario. Solleva una eccezione IndexOutOfBoundsException se }//indexOf
indice non è compreso tra 0 e sizeQ.
public boolean contains( Object elem ) { return indexOf( elem ) != -1;}
void removef Object elem )
elimina, se esiste, la prima occorrenza di elem dal vector. public Object get( int indice ){
if( indice<0 II indice>=size )
Object removej int indice ) throw new lndexOutOfBoundsException();
elimina l'elemento alla posizione indice, e lo ritorna. Solleva un'eccezione IndexOutOfBoundsException se return array(indice);
indice non è compreso tra 0 e size()-1. }//get

void clearQ public Object set( int indice, Object elem ){


svuota il vector. Dopo l'operazione size() vale 0. if( indice<0 II indice>=size )
throw new lndexOutOfBoundsException();
boolean isEmptyQ Object old = arrayfindice];
ritorna true se size()==0. arrayfindice] = elem;
return old;
Vector subVectorj int da, int a ) }//set
crea un nuovo vector e vi copia gli elementi dalla posizione da alla posizione a (esclusa) di this. Solleva
un'eccezione se gli indici non sono validi: da deve essere in [0,size()-1], a in [O.sizeQ] public void add( Object elem ){
if( size==array.length ) espandi();
Un’implementazione di Vector basata su array array[size] = elem;
package poo.util; size++;
public class ArrayVector implements Vector{ }//add
private int size;
private Object [Jarray; //contenitore degli elementi public void add( int indice, Object elem )(
public ArrayVector( int capacita ){ if( indice<0 II indice>size )
if( ca pa citalo ) throw new lllegalArgumentException(); throw new lndexOutOfBoundsException();
array=new Object[capacita]; if( size==array.length ) espandi();
size=0; for( int I = size-1; i>=indice; i-- )
} array[i+1] = arrayp];
arrayfindice] = elem;
public ArrayVector(){ this(20);} //capacità di default size++;
}//add
140 141
Tipi di dati astratti
Capitolo 8

sb.append(']’);
return sb.toString();
public void remove( Object elem ){
}//toString
int I = indexOf( elem );
if( I == -1 ) return;
public int hashCode(){
remove( I );
final int MOLT=41;
}//remove
int h=0;
for( int i=0; i<size; i++ )
public Object remove( int indice ){
h=h*MOLT+array[i].hashCode();
if( indice<0 II indice>=size )
return h;
throw new lndexOutOIBoundsException();
}//hashCode
Object old=array[indice];
for( int I = indice+1 ; ksize; i++ )
public static void main( Stringj] args ){//demo
array[i-1] = array[i];
//prima parte
size--; array[size]=null;
Vector v = new ArrayVector(); //capacità default
if( size<array.length/2 ) contrai();
for( int i=10; i>0; i-- )
return old;
v.add( new Integer(i) );
}//remove
System.out.println(v);
v.clear();
public void clear(){
for( int i=10; i>0; i-- )
for( int i=0; i<size; i++ ) array(i]=null;
v. add( 0,new Integer(i) );
size=0;
System.out.println(v);
}//clear
Vector sv=v.subVector(4,10);
System.out.println(sv);
public boolean isEmpty(){ return size==0;}
//seconda parte
Vector w = new ArrayVector();
public Vector subVector( int da, int a ){
if( da<0 II da>=size II a<0 II a>size II da>=a ) Scanner sc=new Scanner( System.in );
throw new RuntimeException(); for(;;){
Vector v = new ArrayVector( a - da ); System.out.print(“String( solo INVIO per terminare) : “);
for( int j=da; j<a; j++ ) String s=sc.nextLine();
v.add( arrayO] ); if( s.length()==0 ) break;
return v; boolean flag=false;
}//subVector int indice=0;
while( indice<w.size() && Iflag ){
public boolean equals( Object x ){ String str = (String)w.get(indice); //casting necessario
if( !(x instanceof Vector) ) return false; if( str.compareTo(s)>=0 ) flag=true;
ifj x==this ) return true; else indice++;
Vector v = (Vector)x; }
if( this.size!=v.size() ) return false; w. add( indice, s );
for( int i=0; icthis.size; ++I ) }
if( !array[i).equals(v.get(i)) ) return false; System.out.println(w);
return true; }//main
}//equals
}//ArrayVector
public String toString(){
StringBuilder sb=new StringBuilder(200); L'implementazione mantiene nel campo size il numero effettivo di elementi presenti. Il valore di size indica il
sb.append(‘[‘); primo slot libero, se esiste, dell'array dove realizzare una add(elem). Le espansioni/contrazioni dell’array sono
for( int i=0; i<size; ++I ){ curate rispettivamente dai metodi ausiliari privati espandi e contrai. Il primo è invocato quando size coincide
sb.append(array[i]); con la length di array. Il secondo entra in gioco quando il valore di size è trovato inferiore a metà della
if( i<size-1 ) sb.append(“, “); lunghezza dell'array.
}
142 143
Capitolo 8 Tipi di dati astratti

L’aggiunta di un elemento in un posto intermedio (add(/nc(/ce,elem)) o la rimozione di un elemento da una sicuramente un Integer, prelevandolo da w esso è certamente una String. Non serve più il casting da Object a
posizione di assegnato indice (remove(indice)) comportano rispettivamente uno scorrimento a destra (per classe specifica. Il codice diventa più snello, e si mantiene sicuro nella tipizzazione.
evitare problemi di sovrascrittura, si parte da size-1 a decrescere sino a indice) e a sinistra (da indice+1 sino
alla size-1) del contenuto dell’array. Nell'operazione di remove, la vecchia ultima posizione dell’array è posta Classi wrapper dei tipi primitivi
al valore nuli. Tutto ciò è fatto per favorire l’identificazione di oggetti garbage “il più presto possibile”. Poiché il tipo parametro formale T di una classe generica come ArrayVector<T> denota una qualsiasi classe
Similmente, mentre il metodo clear potrebbe limitarsi a porre a zero il valore di size, tutte le slot Java, va da sé che il meccanismo dei generici non permette di utilizzare direttamente i tipi primitivi (che non
precedentemente attive sono poste a nuli per eliminare riferimenti “attivi” inutili agli elementi. sono classi). Non si può scrivere ArrayVector<int> ma solo ArrayVector<lnteger>.

Il metodo hashCodeQ utilizza una “tecnica canonica" per costruire l’intero identificativo unico di un oggetto (qui Per generalità il linguaggio, nel package java.lang, introduce alcune classi predefinite che sono associate ai
un Vector): si combinano gli hash code degli elementi componenti, utilizzando un opportuno fattore di shuffling tipi primitivi (classi wrapper): ad int corrisponde Integer, a byte->Byte, a short ->Short, a long >Long, a float
(mescolamento) rappresentato da un numero primo. -»Float, a double->Double, a char->Character, a boolean->Boolean. Le classi numeriche (es. Integer,
Doublé etc.) sono eredi della classe astratta Number.
Poiché gli elementi di un vector sono Object, tutti i tipi di oggetti, istanze cioè di una qualsiasi classe, possono
essere memorizzati. Vector è una struttura dati potenzialmente eterogenea: possono essere inseriti oggetti Per ovvie ragioni, un oggetto di una classe wrapper è immutabile perché rappresenta una costante di un tipo
String unitamente ad oggetti razionali, oggetti punti etc. Lavorare con un tale tipo di struttura non pone primitivo sebbene sotto forma di oggetto.
problemi sino a che si richiedono operazioni comuni a tutte le classi: toString, equals() etc. (si veda la prima
parte del main di prova). Per applicare metodi specifici di un particolare tipo di oggetto, occorre identificare il Le classi wrapper offrono metodi e attributi di utilità generale. Ad es., tutte sono Comparale e sono provviste
suo tipo dinamico (con instanceof) e quindi (mediante casting) applicare il punto di vista della relativa classe. di: toString(), equals(), hashCode(), compareTo() etc.

Naturalmente, le applicazioni normalmente richiedono vector omogenei: o vector di String, o vector di razionali Alcuni metodi della classe Integer sono richiamati di seguito.
etc. Ottenere una tale omogeneità è responsabilità del programmatore. All'atto dell'ottenimento di un oggetto,
occorre passare comunque da Object al tipo specifico degli elementi (si veda la seconda parte del main di Integer i=new lnteger(5); 1/5 è wrappato (boxing) in i
prova in cui si inseriscono stringhe nel vector w in ordine alfabetico). Integer j=lnteger.valueOf(6); //metodo statico di costruzione

Un Vector generico e parametrico________ _______________ __ int x=\.intValue()', //ottiene la costante int contenuta in i (unboxing)
A partire dalla versione 5 Java ha introdotto il meccanismo dei generici, ossia la possibilità di programmare
una classe/interfaccia (o anche singoli metodi) in veste generica in uno o più tipi (parametri tipi formali). Ad es. int num=lnteger.parse/n/f /23'j;//converte ad int una stringa
l'ADT Vector diventa più flessibile e sicuro se viene riprogettato in veste generica con un tipo parametrico T. Si //naturalmente si solleva una eccezione se la string non contiene un intero
scrive:
Le costanti Integer.MINJ/ALUE e Integer.MAX^VALUE denotano il minimo/massimo intero disponibili ai
public interface Vector<T>{ programmi Java (int si basa su 32 bit e i complementi a due, si veda l’appendice A).

}/A/ector Metodi simili esistono nelle classi Doublé, Long etc. Es. Double.parseDouble(str) converte una stringa doublé
nel corrispondente valore doublé etc. Si rimanda alle API della libreria di Java per altri dettagli.
La notazione Vector<T> significa che la struttura dati (aggregato) è composta di elementi tutti di uno stesso
tipo T al momento non meglio specificato. In pratica T sta per una qualsiasi classe Java. Anche la classe Alcuni metodi di Character sono mostrati di seguito:
ArrayVector<T> che implementa Vector<T> è una specificazione di classe generica nel tipo T.
Character c1=new Character(’A’); //costruttore normale - boxing
Il particolare tipo degli elementi di un Vector va fornito al tempo di dichiarazione di una variabile-oggetto, es. Character c2=Character.valueOf(’B’); //metodo statico di costruzione
char c=c1 .charValue();//unboxing
Vector<lnteger> v=new ArrayVector<lnteger>(); //v è un vector di Integer
static boolean isLowerCase( char ),
Vector<String> w=new ArrayVector<String>(); //w è un vector di String static boolean isUpperCasef char ),
static boolean isDigitf char ),
Nello stesso programma (es. nello stesso main) possono esistere due o più oggetti vector il cui tipo degli static boolean isLetterf char ),
elementi può essere diverso (come per v e w). v ha un parametro tipo attuale che è Integer; w ha parametro static boolean isLetterOrDigitf char )
tipo attuale che è String etc.

Con una tale organizzazione, si ottengono diversi benefici sulla programmazione. Il compilatore garantisce
che in v non si possano inserire elementi che non siano oggetti Integer (omogeneità), cosi come in w elementi
che non siano stringhe. Inoltre, in virtù della parametricità, quando si preleva un elemento da v, esso è
144 145
Capitolo 8 Tipi di dati astratti

Esempio: La classe ArrayVector<T>:


package poo.util;
String linea=...; int i= 0;... import java.util.Arrays;
while( i<linea.length() &&(‘a’<=linea.charAt(i) && linea.charAt(i)<=’z’ Il public class ArrayVector<T> implements Vector<T>{
'A’<=linea.charAt(i) && linea.charAt(i)<=’Z’ Il ‘0’<=linea.charAt(i) && linea.charAt(i)<=’9’ ) ) i++; private int size;
private T []array;
il ciclo di while si può riscrivere più succintamente come segue: public ArrayVector( int capacita ){
if( ca pa citalo ) throw new IHegalArgumentException();
while( i<linea.length() && Character.isLetterOrDigit( linea.charAt(i) ) i++; array=(T[]) new Objectfcapacita];
size=0;
Boxing/unboxing automatico dei valori dei tipi primitivi
Per semplificare il compito del programmatore, l’uso del meccanismo dei generici di Java è assistito dalle public ArrayVector(){ this(20);}
operazioni automatiche di boxing/unboxing di dati dei tipi primitivi in/da oggetti delle corrispondenti classi
wrapper. Ad es. public int size(){ return size;}

Vector<lnteger> v=new ArrayVector<lnteger>(); public int indexOf( T elem ){


v.add( 5 ); //boxing automatico del letterale 5. Equivale a: v.add( new lnteger(5) ); for( int i=0; i<size; i++ )
if( array[i].equals( elem ) ) return I;
int x=v.get(0); //unboxing 146utomatic. Equivale a: x=v.get(0).intValue(); return -1;
}//indexOf
Tuttavia si ribadisce che i parametri tipi sono classi e mai direttamente tipi primitivi. È il compilatore che con il
boxing/unboxing automatico facilita le cose. public boolean contains( T elem ) { return indexOf( elem ) != -1 ;}

Vector<T> generico e parametrico public T get( int indice ){


La programmazione di una classe con tipi parametrici è vincolata da alcune semplici regole. Il nome di un tipo if( indicecO II indice>=size ) throw new lndexOutOfBoundsException();
parametro formale T (Java suggerisce l’uso di singole lettere maiuscole come T, E, K etc.) può essere return array[indice];
utilizzato come un qualsiasi nome di classe ma, data l’assenza di informazioni specifiche sulla classe (es. }//get
costruttori) non è possibile istanziare T (cioè usare l’operatore new). Oggetti di tipo T possono essere
ricevuti/restituiti da metodi. Su di essi è possibile invocare metodi standard di Object quali equals(), public T set( int indice, T elem ){
hashCode(), toString(). Nemmeno la creazione di un array di tipo T è consentita. Tuttavia, la difficoltà può if( indice<0 II indice>=size )
essere aggirata creando un array di Object e poi castizzando tale array al tipo (T[]). throw new lndexOutOfBoundsException();
T x = array[indice];
Mentre per un approfondimento della programmazione generica si rimanda a più avanti nel corso, di seguito si arrayfindice] = elem;
riportano i dettagli del progetto in versione generica del tipo astratto Vector<T> e della classe concreta return x;
ArrayVector<T> al fine di illustrare le considerazioni di cui sopra. La nuova implementazione rinuncia ai metodi }//set
di ausilio espandiQ e contrai() in quanto ottiene l'equivalente funzionalità mediante il metodo
java.util.Arrays.copyOf( array_source, dim ) (si veda anche il cap. 9) che crea e ritorna un array dello stesso public void add( T elem ){
tipo di array_source, di capacità dim (maggiore/minore di array„source.length), e copia gli elementi di if( size==array.length ) array=Arrays.copyOf(array,2‘ array.length);
array_source nel nuovo array nel quale le posizioni vacanti sono poste a nuli. arrayfsize] = elem;
size++;
L’interfaccia Vector<T>: }//add
package poo.util ; public void remove( T elem );
public interface Vector<T>{ public T remove( int indice ); public void add( int indice, T elem ){
public int size(); public void clearj); if( indicecO II indice>size )
public int indexOf( T elem ); public boolean isEmpty(); throw new lndexOutOfBoundsException();
public boolean contains( T elem ); public Vector<T> subVector( int da, int a ); if( size==array.length ) array=Arrays.copyOf(array,2‘ array.length);
public T get( int indice ); }//Vector for( int I = size-1; i>=indice; i- )
public T set( int indice, T elem ); array[i+1] = array[i];
public void add( T elem ); arrayfindice] = elem;
public void add( int indice, T elem ); size++;
}//add
146 147
Capitolo 8 Tipi di dati astratti

public void remove( T elem ){ public int hashCode(){


int I = indexOf( elem ); final int MOLT=41;
if( I == -1 ) return; int h=0;
remove( I ); for( int i=0; i<size; i++ )
}//remove h=h‘ MOLT+array[i].hashCode();
return h;
public T remove( int indice ){ }//hashCode
if( indice<0 II indice>=size ) throw new lndexOutOfBoundsException();
T old=array[indice]; public static void main( Stringo ar9s ){//DEMO
for( int I = indice+1 ; ksize; i++ ) //prima parte
array[i-1] = array[i]; Vector<lnteqer> v = new ArrayVector<lnteqer>(); //capacità default
size--; array[size]=null; for( int i=10; i>0; i- )
if( size<array.length/2 ) array=Arrays.copyOf(array,array.length/2); v.add( i ); //auto boxing
return old; System.out.println(v);
}//remove v.clear();
for( int i=10; i>0; i-- )
public void clear(){ v. add( 0, i );
for( int i=0; ksize; ++I ) array[i]=null; System.out.println(v);
size=0; Vector<lnteger> sv=v.subVector(4,10);
}//clear System.out.println(sv);
//seconda parte
public boolean isEmpty(){ return size==0;} Vector<String> w = new ArrayVector<String>();
Scanner sc=new Scanner( System.in );
public Vector<T> subVector( int da, int a ){ for(;;){
if( da<0 II da>=size II a<0 II a>size II da>=a ) throw new RuntimeExceptionQ; System.out.print("String( solo INVIO per terminare) : “);
Vector<T> v = new ArrayVector<T>( a - da ); String s=sc.nextLine();
for( int j=da; j<a; j++ ) v.add( array[j] ); if( s.length()==0 ) break;
return v; boolean flag=false;
}//subVector int indice=0;
while( indice<w.size() && Iflag ){
public boolean equals( Object x ){ String str = w.get(indice); //non serve più il casting
if( !(x instanceof Vector) ) return false; if( str.compareTo(s)>=0 ) flag=true;
jf( x==this ) return true; else indice++;
Vector v = (Vector)x; }
if( this.size!=v.size() ) return false; w. add( indice, s );
for( int i=0; i<this.size; i++) }
if( !array[i].equals(v.get(i)) ) return false; System.out.println(w);
return true; }//main
}//equals }//ArrayVector

public String toString(){ Tipi “grezzi” e tipi generici


StringBuilder sb=new StringBuilder(200); Quando una classe/interfaccia come Vector<T> o ArrayVector<T> è usata senza l'informazione esplicita di
sb.append(‘[‘); tipo ( es. Vector v=new ArrayVector( 10); ), si sottintende Object come tipo degli elementi e la classe denota un
for( int i=0; i<size; i++ ){ tipo grezzo con tutte le conseguenze di cui si è discusso a proposito degli aggregati generici ed eterogenei
sb.append(array[i]); basati su Object.
if( i<size-1 ) sb.appendf, “);
} NeH'implementazione di ArrayVector<T> si è fatto uso della versione grezza di Vector nel metodo equals(),
sb.append(‘]’); laddove essa è perfettamente compatibile con i bisogni di confronto di uguaglianza degli elementi. Maggiori
return sb.toString(); dettagli verranno forniti nel capitolo 10 approfondendo il meccanismo dei generici.
}//toString
148 149
Capitolo 8 Tipi di dati astratti

Normalmente, l'utilizzo di una classe/interfaccia generica e parametrica è opportuno che awenga in veste Una tabella può essere rappresenta in Java come array di oggetti, in cui gli oggetti sono istanze di una classe
tipata e non in forma grezza che ammette come campi le colonne della tabella.

L'interfaccia Comparable generica:________________________________________ Caso di studio: un programma per la gestione di un’agendina telefonica
public interface Comparable<T>{ Un'agendina è un altro esempio di tabella, ossia un elenco di nominativi. I nominativi si suppongono mantenuti
int compareTo( Tx ); in ordine alfabetico (prima per cognome e a parità di cognome per nome). Per semplicità si ignorano le
}//Comparable omonimie: si suppone che non esistano due nominativi con lo stesso nome e cognome e telefono diversi.

Come si vede, l’interfaccia Comparable che presiede alla definizione del confronto naturale degli oggetti, essa Si vuole scrivere un intero programma che consenta di:
stessa è generica. Pertanto, ad una «classe è data la possibilità di introdurre il confronto utilizzando
Comparable non di Object ma del tipo specifico della classe. Si guadagna in sicurezza e semplicità. Esempio: • Inserire un nuovo nominativo nella tabella
• Eliminare un nominativo dalla tabella
public class Data implements Comparable<Data>{ • Cercare il numero di telefono (prefisso+telefono) di una persona
• Cercare il nominativo di un assegnato numero di telefono
public int compareTo( Data d ){//si evita una operazione di casting • Salvare/ripristinare l'agendina su/da file
if( this.equals(d) ) return 0;
if( this.A<d.A II this.A==d.A && this.M<d.M II this.A==d.A && this.M==d.M && this.G<d.G ) Per raggiungere l'obiettivo si appronta prima una classe, es.Nominativo, che descrive un generico nominativo
return -1; ossia una riga della tabella. La classe Nominativo, inserita nel package poo.agendina, genera oggetti
return +1; immutabili.
}//compareTo
Una classe Nominativo:
}//Data package poo.agendina;
public class Nominativo implements Comparable<Nominativo>{
Tabelle e loro rappresentazione private String cognome, nome;
Una tabella è una collezione di informazioni strutturate. Un esempio classico è il registro anagrafe di un private String prefisso, telefono;
comune. Per ogni persona, il registro memorizza: cognome, nome, data di nascita, sesso, stato civile etc. public Nominativo( String cognome, String nome,
String prefisso, String telefono ){
Una tabella è logicamente rappresentabile come una griglia righe-colonne. Una versione ridotta del registro this.cognome=cognome; this.nome=nome;
anagrafe è la seguente: this.prefisso=prefisso; this.telefono=telefono;
Cognome Nome DataNascita Sesso StatoCivile }
//metodi accessori
Bianchi Fabio 10/11 M Sposato
public String getCognome(){ return cognome;}
/1945
public String getNome(){ return nome;}
Rossi Mario 12/03 M Celibe public String getPrefisso(){ return prefisso:}
persona /1985 public String getTelefono(){ return telefono:}
public boolean equals( Object x )(
if( !(x instanceof Nominativo) ) return false:
if( x==this ) return true;
Nominativo n=(Nominativo)x;
return this.cognome.equals(n.cognome) && this.nome.equals(n.nome);
}//equals
Le colonne sono associate alle varie informazioni componenti. Le righe individuano le entità, in questo caso public int compareTo( Nominativo n ){
persone. Prendendo un'intera colonna si possono osservare, ad es., tutti i possibili cognomi esistenti nella if( this.cognome.compareTo(n.cognome)<0 II
tabella. Prendendo un'intera riga s'identifica una registrazione (da cui il termine registro) e quindi this.cognome.equals(n.cognome) && this.nome.compareTo(n.nome)<0 ) return -1;
semanticamente una persona.iS if( this.equals(n) ) return 0;
return +1;
Si dice chiave di una tabella un sottinsieme delle colonne i cui valori identificano univocamente un'entità. Ad }//compareTo
esempio, nel registro anagrafe di un piccolo comune, una persona potrebbe essere univocamente determinata public String toString(){
dal cognome, nome e data di nascita return cognome+" “+nome+" “+prefisso+”-"+telefono;
}//toString

150 151
Capitolo 8 Tipi di dati astratti

public int hashCode(){ public void svuota(){ tabella.clear();}


int PRIMO=43; public void aggiungi( Nominativo n ){...}//aggiungi
int h=cognome.hashCode(); public void rimuovi! Nominativo n ){...}//rimuovi
h=h*PRIMO+nome.hashCode(); public Nominativo cerca! Nominativo n ){...}//telefonoDi
return h; public Nominativo cerca! String prefisso, String telefono ){...}//personaDi
}//hashCode public String toString(){...}//toString
}//Nominativo ...//altri metodi
}//AgendinaVector
A questo punto si progetta un'interfaccia (ADT) per gestire un'agendina telefonica, ossia un elenco (tabella) di
nominativi. I metodi aggiungi/rimuovi:
public void aggiungi! Nominativo nm ){
Agendina come ADT:_______________________________________________________________________ //cerca indice i per l'inserimento di nm in elenco
package poo.agendina; int i=0;
import java.io.*; while( ksize && tabella.get(i).compareTo(nm)<0 ) i++;
public interface Agendina{ //se già' esiste, si sovrascrive
public int size(); if( ksize && tabella.get(i).equals(nm) ){
public void svuota(); tabella.set(i.nm); return; //aggiornamento
public void aggiungi( Nominativo n ); /i
public void rimuovi( Nominativo n ); //inserisci nm
public Nominativo cerca( Nominativo n );//per cognome-nome tabella.add(i,nm);
public Nominativo cerca( String prefisso, String telefono ); ^/aggiungi
public void salva(String nomeFile) throws lOException;
public void ripristina(String nomeFile) throws lOException; public void rimuovi! Nominativo nm ){
}//Agendina //ricerca n nell'elenco, secondo l'ordinamento naturale
int i=Array.ricercaBinaria(tabella,nm);
Il metodo aggiungi riceve un nominativo da aggiungere all’agendina. Per l’ipotesi di assenza di omonimie, un if( i==-1 ) return;
nominativo già presente non deve essere ri-aggiunto. Si preferisce, in questi casi, intendere che l'aggiunta sia tabella.remove(i);
piuttosto una richiesta di aggiornamento del nominativo pre-esistente. }//rimuovi

I metodi cerca e rimuovi basati su un parametro Nominativo, assumono che il nominativo trasmesso abbia Entrambi i metodi sfruttano il fatto che la tabella dei nominativi è ordinata e la loro esecuzione preserva questa
sufficienti informazioni per effettuare i confronti. Si sottolinea che il nominativo trasmesso tipicamente proprietà. Il metodo rimuovi, in particolare, si avvale del metodo di servizio ricercaBinaria della classe di utilità
corrisponde ad un nominativo fittizio in cui solo il cognome e nome sono significativi. D’altra parte, l’oggetto poo.util.Array che ritorna l'indice del nominativo ricercato o -1 se esso non si trova nella tabella.
restituito dal metodo cerca è un oggetto completo dell'agendina, dunque provvisto anche di prefisso e
telefono. L’immutabilità degli oggetti nominativi è chiave per evitare di costruirsi una copia di un nominativo I metodi cerca:
prima di restituirlo. Se il nominativo cercato non esiste, allora il metodo ritorna nuli. public Nominativo cerca( Nominativo nm ){
int i=Array.ricercaBinaria( tabella, nm );
Una concretizzazione del tipo Agendina si può agevolmente ottenere utilizzando l’ADT Vector<T> e la classe if( i==-1 ) return nuli;
concreta ArrayVector<T>. Infatti, un’agendina si può modellare naturalmente come un vector di nominativi. return tabella.get(i);
}//telefonoDi
Una classe AdendinaVector:___ ___________ ____________
package poo.agendina; public Nominativo cerca( String prefisso, String telefono ){
import java.io.*; for( int i=0; i<tabella.size(); i++ ){
import poo.util.*; Nominativo nm=tabella.get(i);
public class AgendinaVector implements Agendina! if( nm.getPrefisso().equals(prefisso) && nm.getTelefono().equals(telefono) )
private Vector<Nominativo> tabella; return nm;
public AgendinaVector(){ this(100);} }
public AgendinaVector( int n ){ return nuli;
if( n<=0 ) throw new IHegalArgumentException(); }//cerca
tabella=new ArrayVector<Nominativo>(n);
} La seconda variante di cerca utilizza una coppia prefisso telefono per la ricerca e ritorna il primo nominativo
public int size(){ return tabella.sizeQ;} con queste informazioni, o nuli se nessun nominativo soddisfa la ricerca.
152 153
Capitolo 8 Tipi di dati astratti

Un programma GestioneAgendina:_______________________________
Il metodo toString:__________________________________________________________________________ import poo.agendina.*;
public String toString(){ //esempio import poo.inout.*;
StringBuilder sb=new StringBuilder(500); import java.util.*;
for( int i=0; i<tabella.size(); i++ ) public class GestioneAgendina{
sb.append(tabella.get(i)+"\n"); //ambiente globale
return sb.toString(); static Agendina agenda=new AgendinaVectorQ;
}//toString static String linea;
static StringTokenizer st;
Anticipando concetti che saranno approfonditi nel seguito (cap. 12), si propone una realizzazione dei metodi static Scanner sc=new Scanner( System.in );
salva(...) e ripristina(...) che rispettivamente copiano su/da file il contenuto dell’agendina. Entrambi i metodi, public static void main( String []args ){
siccome lavorano su file, dunque su oggetti del file System del sistema operativo, devono trattare l'eccezione System.out.printlnf'Programma Agendina Telefonica"); comandi();
checked lOException definita nel package java.io. ciclo: for(;;){
System. out.print(V);
I metodi salva/ripristina linea=sc.nextLine();
public void saiva[ String nomeFile ) throws IOException{ st=new StringTokenizer(linea, “ ");
PrintWriter pw=new PrintWriter( new FileWriter(nomeFile) ); char comando=st.nextToken().charAt(0);
for( int i=0; i<tabella.size(); i++ ) switch( comando )(
pw.println( tabella.get(i) ); //si sfrutta il toString di Nominativo case 'Q': quit(); break ciclo;
pw.close(); case 'A': aggiungiNominativo(); break;
}//salva case 'R': rimuoviNominativo(); break;
case T : ricercaTelefono(); break;
public void ripristina(String nomeFile) throws lOExceptionf case 'P': ricercaPersonaQ; break;
tabella.clear(); case 'E': mostraElenco(); break;
BufferedReader br=new BufferedReader( new FileReader( nomeFile )); case ’S': salva(); break;
String linea=null; case C': carica(); break;
StringTokenizer st=null; default: erroreQ;
String cog, nom, pre, tei;
for(;;H }//for
linea=br.readLine(); }//main
if( linea==null ) break; //fine file ...//metodi
st=new Stringiokenizer(linea, “ -"); }//GestioneAgendina
cog=st.nextToken(); nom=st.nextToken(); pre=st.nextToken(); tel=st.nextToken();
Nominativo n=new Nominativo(cog,nom,pre,tel); this.aggiungi n ); Strutturajjei comande___________
} >A(ggiungi cog nom pre tei INVIO
br.close(); >R(imuovi cog nom INVIO
}//ripristina >T(elefonoj1i cog nom INVIO
>P(ersona_di pre tei INVIO
Si nota che il metodo ripristina utilizza uno StringTokenizer per frammentare una linea del file testo contenente >E(lenco INVIO
l’agendina, ed assume lo schema di salvataggio corrispondente al metodo toString della classe Nominativo >S(alva nome file INVIO
(un separa il prefisso dal telefono). Se si verifica un’eccezione, il metodo ripristina non effettua alcuna >C(arica nomefile INVIO
gestione e semplicemente propaga l’eccezione al metodo chiamante. Per poter utilizzare le classi PrintWriter e >Q(uit INVIO
BufferedReader, occorre importare java.io.* nella classe AgendinaVector.
Poiché l’ambiente globale di AgendinaTelefonica è realizzato mediante dati static, i metodi ausiliari (richiamati
Per completare l'applicazione, occorre approntare una classe col main che ad es. dialoga interattivamente con dall'interno dello switch( comando )) devono necessariamente essere dichiarati essi stessi static. Si nota in
l'utente. Di seguito si propone un'applicazione completa che prende un comando alla volta dall'utente a fronte particolare il globale StringTokenizer st condiviso tra il main e i metodi ausiliari. Questa scelta di progetto
di un prompt, ed esegue un'operazione sull'agendina. Un comando è una singola lettera maiuscola seguita da consente di definire i metodi ausiliari senza parametri dal momento che ogni metodo può ottenere gli
0,1 o più argomenti (stringhe). argomenti da st. Ogni eccezione sollevata dal tokenizer è catturata e convertita in una segnalazione sul video.

154 155
Capitolo 8 Tipici dati astratt)

Il metodo aggiungiNominativo: Nominativo n=agenda.cerca( new Nominativo(cog, nom, " , "") );


static void aggiungiNominativo(){ if( n==null ) System.out.println("Nominativo inesistente!");
try{ else System.out.println(n.getPrefisso()+"-"+n.getTelefono());
String cog=st.nextToken().toUpperCase(); }catch( Exception e ) { System.out.printlnfDati incompleti!");}
String nom=st.nextToken().toUpperCase(); }//ricercaTelefono
String pre=st.nextToken();
String tel=st.nextToken(); I metodi ricercaPersona e mostraElenco:
Nominativo n=new Nominativo( cog,nom,pre,tel ); static void ricercaPersona(){
agenda.aggiungi( n ); try{
}catch( Exception e ){ String pref=st.nextToken();
System.out.printlnfDati incompleti"); String tel=st.nextToken();
} Nominativo n=agenda.cerca( pref, tei );
}//aggiugiNominativo if( n==null ) System.out.printlnf'Nominativo inesistente!");
else System.out.println(n.getCognome()+" ’ +n.getNome());
I metodi comandi ed errore: }catch( Exception e )(
static void comandi(){ System.out.printlnfDati incompleti!");
System.out.println(); }
System.out.println("Comandi ammessi:"); }//ricercaPersona
System.out.println("A(ggiungi cog nom pre tei");
System.out.println("R(imuovi cog nom"); static void mostraElenco(){
System.out.println("T(elefono cog nom"); System.out.println( agenda );
System.out.println("P(persona pre tei"); }//mostraElenco
System.out.println("E(lenco");
System.out.println("S(alva nomefile"); I metodi salva e carica:
System.out.println("C(arica nomefile"); static void salva() throws IOException{
System.out.println("Q(uit“); String nomeFile=null;
System.out.println(); try{
}//comandi nomeFile=st.nextToken();
}catch( Exception e ){
static void errore(){ System.out.printlnfDati incompleti!");
System.out.println (“Comando sconosciuto!"); return;
comandi();
}//errore try{
agenda.salva( nomeFile );
Ad ogni errore, il programma richiama quali siano i comandi leciti, e per il resto prosegue. Tutto ciò è il risultato }
dell’exception handling: le eccezioni sono catturate e gestite. Per terminare il programma occorre digitare il catch( lOException ioe ){
comando Q(uit. System.out.printlnf Errore salvataggio");
}
I metodi rimuoviNominativo e ricercaTelefono:___________________________________________________ }//salva
static void rimuoviNominativo(){
try{ static void carica() throws IOException{
String cog=st.nextToken().toUpperCase(); Strinq nomeFile=null;
String nom=st.nextToken().tollpperCase(); try{
agenda.rimuovi( new Nominativo( cog, nom,"", "" ) ); //creazione nominativo fittizio per ricerca nomeFile=st.nextToken();
}catch(Exception e){ System.out.println("Dati incompleti!");} }catch( Exception e ){
}//rimuoviNominativo System.out.printlnfDati incompleti!");
return;
static void ricercaTelefono(){ }
try{
String cog=st.nextToken().toUpperCase();
String nom=st.nextToken().toUpperCase();
156 157
Capitolo 8 Tipi dicati astratti

File f=new File( nomeFile ); public enum Stagione { PRIMAVERA, ESTATE, AUTUNNO, INVERNO}
if( !f.exists() ){
System.out.printlnf'File inesistente!");return; Una enum è una classe speciale, le cui istanze sono ristrette ad essere tutte e sole quelle specificate nella sua
definizione (tra le parentesi { e }). Nessuna nuova istanza può essere creata. Ogni enum definisce il suo
try{ proprio namespace. Nessuna aritmetica è possibile sui valori di una enum. Di seguito si richiamano i metodi
agenda.ripristina( nomeFile ); per elaborare i valori dei tipi enumerati.
}catch(IOException e){
System.out.println(“Nessuna apertura!"); TipoEnum.valuesQ
} ritorna un array con i possibili valori della enum, memorizzati nell'ordine di elencazione
}//carica
int ordinato
Un nome di file inesistente non crea problemi in fase di scrittura (metodo salva) in quanto verrà comunque ritorna il numero ordinale (posizione) di un valore enumerato, un int compreso tra 0 e values().length-1
creato, ma non è accettabile in una fase di caricamento. Come si vedrà nel cap. 12, è possibile verificare
l’esistenza di un file con l’ausilio di un oggetto di classe File (appartenente sempre a java.io) ed il metodo int compareTof valoreEnum )
exists(). fornisce il confronto naturale tra i valori di una enum

Il metodo quib_______________________________ ______________ String nameO


static void quit() throws IOException{ ritorna il nome di un valore enum
System.out.printf'Vuoi salvare il contenuto dell'agenda prima di terminare(y/n)?K);
String yesno=sc.nextLine(); TipoEnum valueOlf nome_valore_enum )
if( yesno.toLowerCase().equals("y") ){ riceve la stringa nome di un valore enum e la converte al valore enum corrispondente
String nomeFile=null;
do( equals( valoreEnum ), hashCodeQ, toStringO
try{ usa la tua fantasia.
System.out.printfnome file=");
nomeFile=sc.nextLine(); Essendo una classe, una enum può essere eventualmente dotata di costruttori, campi dati e metodi come
}catch( Exception e ){ nomeFile=null;} mostrano gli esempi che seguono, di cui il primo è relativo ad una riformulazione della classe Data introdotta
if( nomeFile==null II nomeFile.lengfh()==0 ){ nel cap. 3.
System.out.println("Errore di input. Ridare il nome del file.");
} Esempio 1: Una nuova classe Data
}while( nomeFile==null II nomeFile.length()==0 ); package poo.date;
try{ import java.util.*;
agenda.salva( nomeFile );
}catch( lOException ioe ){ public class Data implements Comparable<Data>{
System. out.printlnf’Errore salvataggio!"); private final int G, A;
private final Mese M;
public enum Tipo{ GIORNO, MESE, ANNO};
}//quit public enum Mese{
GENNAIO( 31),
Il metodo quit è stato progettato per intercettare una richiesta di chiusura del programma, che potrebbe essere FEBBRAIO( 28){
anche involontaria, e consentire all'utente di procedere con il salvataggio dei dati in memoria, se ciò è public int durata( int anno ){
richiesto. Uscendo “silenziosamente", infatti, si perderebbero tutti i nominativi esistenti nella tabella in if( anno%4!=0 II anno%100==0 && anno%400!=0 )
memoria. In presenza di errori di input (es. l’utente batte INVIO senza fornire i caratteri del nome) il metodo return durata;
richiede nuovamente il nome del file. return durata-*-! ;

Ancora sulle classi enum


Un tipo enumerato (si veda anche il cap. 3) è un tipo i cui valori sono un insieme fissato di costanti, come i MARZO( 31),
giorni della settimana, le stagioni in un anno etc. Nelle ultime versioni di Java un tipo enumerato si introduce APRILE(30), MAGG/0(31), GIUGNO(30), LUGLIO(31),
mediante una enum: AGOSTO(3]), SETTEMBRE(30), 07T0ERE(31), NOVEMBRE(30),
DICEMBRE( 31);
public enum Giorno { LUNEDI, MARTEDÌ, MERCOLEDÌ. GIOVEDÌ, VENERDÌ, SABATO, DOMENICA } protected int durata; //variabile di istanza
158 159
Capitolo 8 Tipi di dati astratti

Mese( int durata ){//costruttore Mese m=this.M;


this.durata=durata; if( this.G==1 ){//determina mese precedente
} if( this.M.compareTo(Mese.G£/V/VA/O)>0 )
public int durata( int anno ){//metodo di istanza m=Mese.va/ues()[M.ordinal()-1];
return durata; else{
} m=Mese. DICEMBRE]
}//Mese a=this.A-1;

Il campo mese M è di un tipo enumerato Mese, che è dotato di un costruttore che inizializza la durata del q=durataMese{r(],a);
mese. In sede di elencazione dei valori enumerati, si specifica il parametro del costruttore che esprime la }
durata. Per febbraio si ipotizza (temporaneamente) un anno non bisestile. La durata del mese è memorizzata else g=this.G-1;
nella variabile di istanza durata, e viene restituita dal metodo di istanza durata() che riceve l'anno come return new Data(g,m,a);
parametro. Si nota che esiste una versione di default del metodo durata() che è ridefinita in corrispondenza }//giornoPrima
solo del valore FEBBRAIO, in modo da attuare la correzione richiesta dagli anni bisestili. La ridefinizione è
attuata con una sottoclasse anonima della classe Mese, definita “al volo". Per gli altri metodi, si veda l’esercizio 4. in fondo al capitolo.

Seguono i tre costruttori della classe Data: Esempio 2: Un tipo enumerato Operazione
public enum Operazione {
public Data(){ //costruttore di default PIU, MENO, PER, DIVISO;
GregorianCalendar gc=new GregorianCalendar(); doublé opera( doublé x, doublé y ){//realizza l’operazione aritmetica associata alla costante enumerata
G=gc.get( GregorianCalendar.DAY_OF MONTH); switch( this ) {
M=Mese.va/ues()[gc.get( GregorianCalendar .MONTH )]; case PIU: return x + y;
A=gc.get( GregorianCalendar. YEAR ); case MENU: return x - y;
}//Data case PER: return x * y;
case DIVISO: return x / y;
public Data( int g, Mese m, int a )( //su M non sono possibili errori }
if( a<0 II g<1 II g>d/ra/a/Wese(m,a) ) throw new IHegalArgumentException(); throw new RuntimeExceptionfOperazione sconosciuta op: “ + this);
this.G=g; this.M=m; this.A=a; }//opera
}//Data }//Operazione

public Data( Data d ){ L’eccezione tipicamente sorge quando qualche nuova costante è aggiunta aH’enumerazione ma si dimentica
G=d.G; M=d.M; A=d.A; di aggiornare l’istruzione switch prevedendo le corrispondenti alternative. Il tipo enumerato Operazione può
}//Data essere reso robusto rispetto a queste situazioni adottando un approccio differente:

Il costruttore di default di Data, che inizializza la data come data odierna ottenuta tramite un’istanza di default public enum Operazione!
gc di java.util.GregorianCalendar, assegna al campo mese M il valore ottenuto accedendo all'array P IU (V)
Mese.valuesQ in corrispondenza dell'ordinale fornito dal campo mese di gc, che è un valore tra 0 ed 11. { doublé opera (doublé x, doublé y) ( return x + y ; }},
MENO(“-")
Il servizio static durataMese() si semplifica come segue: { doublé opera (doublé x, doublé y) { return x - y ; }},
PERD
public static int durataMese( Mese m, int a ){ { doublé opera (doublé x, doublé y) { return x * y ; }},
if( a<0 ) throw new IHegalArgumentException(); DIVISO(T)
return m.durata( a ); { doublé opera (doublé x, doublé y) { return x / y ; }};
}//durataMese private final String simbolo;
Operazione( String simbolo ) { this.simbolo = simbolo;} //costruttore
L’uso dei valori enumerati del tipo Mese è chiarito ulteriormente dal seguente metodo che calcola il giorno public String toStringQ { return simbolo;}
prima di una data assegnata (this): abstract doublé opera( doublé x, doublé y );
{//Operazione
public Data giornoPrima(){
if( G==1 && M==Mese.GEA//VA/0&& A==0 ) throw new RuntimeExceptionfPrima data1'); Si nota che si è evitato lo switch definendo un metodo abstract operaQ che va necessariamente concretizzato
int g, a=this.A; per ogni costante del tipo. Non è più possibile aggiungere qualche nuova costante al tipo e dimenticare di
160 161
Capitolo 8

definire il suo metodo operaQ. Un’altra particolarità della nuova versione del tipo enumerato Operazione è Capitolo 9:_______________________________________
costituita dalla ridefinizione del metodo toString() che ritorna il valore del campo simbolo e non il nome della
costante (default sulle enum). Segue un possibile main: Collection framework e progetto di collezioni custom

public static void main(String[] args) {//demo Nel package java.util sono presenti alcune interfacce e classi “pronte per l'uso” (framework) che consentono di
doublé x = Doublé.parseDouble( args[0] ); lavorare su collezioni generiche di elementi. Le classi disponibili aiutano il programmatore nella risoluzione
doublé y = Doublé.parseDouble( args[1] ); (es. prototipazione) di comuni e ricorrenti situazioni in cui il problema si accompagna naturalmente a strutture
for( Operazione op : Operazione.values() ) dati del tipo successioni o sequenze come liste, insiemi (set) o funzioni (map).
System.out.printf("%f %s %f = %f%n“, x, op, y, op.opera(x, y));
}//main Le due figure che seguono riassumono una parte del collection framework attraverso diagrammi UML (si veda
anche il cap. 21) la cui comprensione è intuitiva. Le collezioni di tipo lista o set originano dall'interfaccia
Enumerazioni e singleton Collection che è estesa dalle interfacce specifiche List e Set. Le mappe derivano dall’interfaccia base Map. Le
classi astratte “iniziano" ad implementare le interfacce cui si riferiscono. Le classi finali concrete forniscono
Esistono situazioni nelle quali si desidera che di una certa classe possa esistere una ed una sola istanza
invece una realizzazione completa delle collezioni. Si osserva che le mappe o funzioni sono gestite attraverso
(singleton). Un tipico scenario è la messa a punto di un oggetto “ambiente globale" in cui sono mantenuti dati
una gerarchia di classi separata da quelle delle liste e set. Le classi concrete con bordo nero sono normali
(modificabili o meno) condivisi dai rimanenti oggetti di un'applicazione. È possibile con poca fatica realizzare
classi (stanziabili: LinkedList, ArrayList, HashSet, TreeSet, HashMap, TreeMap. Le classi concrete con bordo
una classe in versione singleton come segue:
sottile sono classi di utilità (o di servizio) e contengono metodi statici: Collections e Arrays.
final class ClasseSingleton{ «interface» A .......................
private ClasseSingleton(){) AbstractCollection
Collection «implements»
private static ClasseSingleton unicalstanza=null; — z x -------
-z s -
... //campi dati come richiesto dalla applicazione + metodi accessori/mutatori
public static ClasseSingleton getlnstance(){ «extends»
if( unicalstanza==null )
unicalstanza=new ClasseSingleton(); «interface» A AbstractSet
si
return unicalstanza; Set zX
} «interface»
{//ClasseSingleton List

I Client possono ottenere il riferimento all’istanza unica invocando il metodo static getlnstance() sulla classe
singleton, quindi farne uso come al solito. Tuttavia esiste una strada più semplice e sicura per realizzare «interface»
singleton: i tipi enumerati. È facile convincersi che è sufficiente introdurre un’enumerazione con una sola Iterator
costante, in presenza di campi e metodi come opportuno: — zx—

enum ClasseSingleton{ UNICA ISTANZA:...}

Esercizi «interface» Collezioni liste e set


1. Sviluppare i metodi equals() e hashCodeQ nella classe AgendinaVector. Listlterator
2. Prevedere nell’interfaccia Vector<T> due nuovi metodi:
void retainAII( Vector<T> v )
void removeAII( Vector<T> v )
II primo mantiene in this tutti e soli gli elementi che appartengono anche al parametro v. Il secondo elimina da
this tutti e soli gli elementi di v. Implementare i metodi nell’ambito della classe ArrayVector<T>.
3. Modificare il metodo mostraElenco() del programma GestioneAgendina in modo da mostrare non più di 20
righe per volta su video e di avanzare dopo il ricevimento di un comando INVIO da parte dell’utente.
4. Implementare tutti i metodi (si riveda il cap. 3) della nuova versione della classe Data basata sul tipo
enumerato Mese, compresi equals(), hashCode() e toString().

Altre letture
E possibile approfondire i tipi enumerati ad es. sul testo:
Collections Arrays
J. Bloch, Eftective Java, Addison Wesley, 2ndEdition, 2008.
162 163
Capitolo 9 Collection framework e collezioni custom

Una collezione denota genericamente una successione o insieme di elementi. Le classi collezioni sono
parametriche nel tipo T degli elementi. I metodi toArray() restituiscono un array contenente gli elementi della collezione. La seconda variante
toArray(array) crea e restituisce un array il cui tipo dinamico è fornito dal parametro. L’array parametro è
Una lista è una collezione nella quale: utilizzato anche per ritornare il contenuto della collezione, a patto che esso abbia sufficiente capacità. Nel
(a) è definito un ordine totale: si sa qual è il primo elemento, il secondo etc. A questo proposito, gli elementi di caso abbia capacità superiore, dopo l’ultimo elemento si pongono nuli.
una lista possono essere rintracciati in base al loro indice, un intero tra 0, 1,..., size()-1 dove size() esprime
la cardinalità della lista Un esempio:_______________________
(b) possono sussistere duplicati di elementi (le liste possono essere bag o multi-insiemi). List<lnteger> li=new ArrayList<lnteger>();
li.add( 4 ); li.add(2); li.add(2); li.add(-1); li.add(10);
Un set è una collezione basata sul significato di insieme matematico, cioè: System.out.println( li );
(c) non ha importanza l’ordine
(d) non sussistono duplicati: se l'insieme contiene già x, l'aggiunta nuovamente di x non modifica il set. Output generato: [4, 2, 2,-1,10] riflette l’ordine di inserimento degli elementi. Il 2 è duplicato.

Per fare uso delle collezioni è importante conoscere le interfacce Collection, List, Set, Iterator, Listlterator, Set<lnteger> si=new HashSet<lnteger>();
Map etc. Segue una vista parziale di Collection. si.add( 4 ); si.add(2); si.add(2); si.add(-1); si.add(10);
System.out.println( si );
L’interfaccia Collection<T>
boolean add( T elemento ); Output generato: [2, 4,10,-1] non riflette alcun ordine degli elementi inseriti. Non sussistono duplicati.
boolean addAII( Collection<T> c );
void clear(); L’interfaccia lterator<T>
boolean isEmpty(); boolean hasNextQ;
boolean contains( T elemento ); T next();
boolean containsAII( Collection<T> c ); void remove();
int hashCode();
lterator<T> iterator(); L'interfaccia è utile per “navigare" sulla collection elemento per elemento, dal primo all’ultimo. Chiamare next()
boolean remove( T elemento ); quando hasNext() ritorna false, solleva un'eccezione NoSuchElementException (erede di RuntimeException).
boolean removeAII( Collection<T> c );
boolean retainAII( Collection<T> c );
int size();
Object[] toArray(); xl x2 x3
<E> E[] toArray( E[] array );

Il metodo size() ritorna il numero degli elementi presenti nella collezione. clear() svuota la collezione. isEmpty()
ritorna true se la collezione è vuota (size()==0). iterator() ritorna un iteratore su questa collezione (si veda più h a s N e x t()= tru e tru e tru e tru e false
avanti).

I metodi add(), addAIIQ, remove(), removeAII() ritornano un boolean che vale true se la collezione risulta next() porta avanti di una posizione l’iteratore (cursore/freccia) e ritorna l’oggetto“attraversato” es xO.
modificata a seguito dell'operazione, false altrimenti. I metodi addAIIQ e containsAII() corrispondono all'unione
e alla verifica di sottoinsieme. Schema d’uso di un iterator ______
lterator<f> it=collezione.iterator(); //ottiene un iteratore da collezione
removeAII() toglie dalla collezione this tutti gli elementi della collezione c ricevuta parametricamente (calcolo while( it.hasNext() ){
dell'insieme differenza). T x=it.next();
elabora x
retainAII() lascia nella collezione this tutti e soli gli elementi che sono contenuti anche nella collezione c }
ricevuta parametricamente (calcolo dell'insieme intersezione).
Il metodo remove() consente di rimuovere l’elemento corrente della collezione, rilasciato dall’ultima next().
I metodi contrassegnati con (*) sono opzionali, nel senso che una classe concreta che implementi Collection
può scegliere se rendere o meno disponibile uno di questi metodi.

Ovviamente, al livello di Collection, il significato preciso di add(), cioè in che posizione venga aggiunto un
elemento, non è noto.
164 165
Capitolo 9 Collection framework e collezioni custom

Pattern tipico della remove Il metodo add( x ) di Listlterator aggiunge x giusto prima del cursore. next() non è influenzata dall'Inserimento.
Iteratoci" > it=collezione.iterator(); previous() ritorna l'elemento appena aggiunto (si veda la figura che segue).

while( it.hasNext() ){
T x=it.next(); xO xl x2 xk prima
if( x è da rimuovere )
it.remove();

add(y )
L’effetto della remove si risente anche sulla collezione. Un’invocazione it.remove() non preceduta da una
chiamata it.next() solleva un’eccezione di tipo UlegalStateException (erede di RuntimeException). L’eccezione
è generata anche a seguito di due chiamate consecutive di remove(), non inframmezzate cioè da una dopo
invocazione di next(). In queste situazioni lo stato dell’iteratore è appunto illegale ai fini dell’effettuazione di
una rimozione.
fi
Metodi aggiunti dall’interfaccia List<T> che estende Collection<T> _______ ____ Il metodo set( x ) sostituisce x all'elemento corrente (definito dall’ultima operazione next() o previous()
eseguita). I metodi hasPrevious() e previous() sono duali di hasNext() e next(), e consentono di attraversare
void add( int indice, T elemento );
void addAII( int indice, Collection<T> c ); una lista a ritroso, es.:
T get( int indice );
Listlterator<T> lit=lista.listlterator( lista.size() );//cursore dopo l’ultimo elemento
int indexOf( T elemento );
while( lit.hasPrevious() ){
int lastlndexOf( T elemento );
Listlterator<T> listlterator(); T x=lit.previous();
Listlterator<T> listlterator( int da ); elabora x;
T remove( int indice );
T set( int indice, T elemento );
Un Listlterator può essere “acceso" a partire dall’inizio della lista (default) o specificando esplicitamente
List<T> subList( int da, int a );
l’indice di partenza. Nell’esempio, volendo realizzare un’iterazione backward, si è specificata la fine della lista.
Questi metodi si basano sull’indicizzazione degli elementi supportata da List. Il metodo add( x ) definito in
Se il cursore è alla fine della lista, add(x) aggiunge x come ultimo elemento della lista. Se il cursore è prima
Collection, aggiunge l’oggetto x alla fine (coda) della lista.
del primo elemento, add(x) aggiunge x in testa alla lista. In questo caso rientra anche lo scenario di lista vuota.
Se il cursore è all'Interno della lista, add(x) aggiunge x prima del cursore, la cui posizione rimane inalterata. Le
Il nuovo metodo add( indice,x ) aggiunge x nella posizione indice che può valere da 0 a size(). add(0,x)
osservazioni che precedono sono rilevanti quando si è interessati ad un inserimento in ordine. Infatti, per
richiede di aggiungere x in testa. add(size(),x) richiede di aggiungere x in coda. L'operazione comporta
scoprire la posizione di inserimento occorre necessariamente avanzare oltre l’elemento corrente. Se questo
(logicamente) lo spostamento di un posto a destra di tutti gli elementi pre-esistenti nelle posizioni da indice
segue x, allora prima di comandare add(x) occorre riportare indietro (se il movimento è basato su next()) o in
sino a size()-1.
avanti (nel caso di movimento previous()) il cursore prima deH’inserimento. Il tutto è chiarito dal frammento di
codice che segue
get( indice ) ritorna l’oggetto in posizione indice: 0<indice<size()-1.
Un esempio di add() in o r d i n e : ____________ _________________ ___________________________
set( indice,x ) sostituisce l’oggetto in posizione indice con x. Il precedente oggetto è ritornato.
List<lnteger> l=new LinkedList<lnteger>();
0<indice<size()-1.
int x=...; //elemento da inserire in ordine crescente
Il metodo remove( indice ) rimuove e ritorna l’oggetto in posizione indice: 0<indice<size()-1. Listlterator<lnteger> lit = l.listlteratorQ; //ottiene un list iterator da I, posizionato all'inizio (default)
boolean flag = false; //vale true dopo inserimento
Metodi deirinterfaccia Listlterator<T> che estende lterator<T> ___ ____ _________________
while( lit.hasNext()&& Iflag )(
Oltre a quelli di lterator<T> sono disponibili i seguenti nuovi metodi: int y=lit.next();
boolean hasPreviousQ if( y>=x ){//trovata posizione di inserimento
T previous() lit.previous();
int previouslndex() lit.add( x );
int nextlndex() flag=true;
void set( T elemento )
add( T elemento )
if( Iflag ) lit.add( x ); //aggiunta in coda o in lista vuota
166 167
Capitolo 9 Collection framework e collezioni custom

In modo analogo si procede quando il movimento è backward, ossia si utilizza previous(). Il programma che
segue legge da riga di comando una sequenza di stringhe e le inserisce in ordine alfabetico in una lista di
stringhe. Alla fine si visualizza la lista ottenuta.

Costruzione di una lista ordinata di stringhe mediante Listlterator:___________________________________


import java.util.*;
public class TestListlterator{
public static void main( Stringo args ){
List<String> l=new LinkedList<String>();
for( int i=0; kars.length; ++i ){ Rimuovendo ora l’elemento 9 si ha:
String x=args[i]; //dati provenienti dalla riga di comando
boolean flag=false; //true ad inserimento effettuato
Listlterator<String> lit=l.listlterator( l.size() );
while( lit.hasPrevious() && Iflag ){
String s=lit.previous();
if( s.compareTo(x)<=0 ) { lit.next(); lit.add(x); flag=true;}
}//while
if( Iflag ) lit.add(x);
}//for
System.out.println(l);
}//main Si intuisce che le operazioni di inserimento e rimozione sono portate a termine mediante “aggiusto” di
}//TestListlterator puntatori, senza spostare elementi.

La booleana flag diventa true non appena l’elemento x è inserito durante un’iterazione del ciclo di while. LinkedList usa in realtà uno schema a doppio puntatore: al successore e al predecessore, e permette anche la
Quando ciò non è vero, all’uscita del while si provvede ad aggiungere x in testa. In questi casi, infatti, x è scansione backward (si rimanda al cap. 15 per maggiori dettagli).
minimo rispetto a tutti gli elementi presenti o la lista è vuota.
Costruttori di ArrayList:
Classi ArrayList<T> e LinkedList<T>____________ ArrayList()
Sono classi concrete che appoggiano la lista rispettivamente su un array nativo (che può espandersi e costruisce un array list con una capacità iniziale di default
contrarsi dinamicamente) e su una lista concatenata (si veda più avanti). Il comportamento di un ArrayList è ArrayList( Collection<T> c )
identico a quello di un ArrayVector precedentemente studiato. costruisce un array list a partire dalla collezione c (gli elementi di c sono posti nell’array list this nell’ordine
stabilito dal corrispondente iterator)
È possibile operare su ArrayList/LinkedList con uno stesso insieme di metodi, tuttavia l’inserimento in e la ArrayListf int capacitalniziale )
rimozione da una posizione intermedia comportano (come ben noto) lo spostamento di elementi sull’array list, costruisce un array list con assegnata capacita iniziale.
ma un semplice aggiusto di puntatori su una linked list. La ricerca binaria ha senso su un array list ordinato ma
non su una linked list ordinata. Un array list incorpora e nasconde un array che dinamicamente può espandersi e contrarsi a piacere. Occorre
distinguere tra capacità e dimensione di un array list. La capacità è la lunghezza dell'array sottostante. La
È importante riflettere sull’effetto di queste operazioni e scegliere oculatamente di volta in volta tra ArrayList e dimensione (size()) è il numero effettivo di elementi presenti nella lista.
LinkedList. ArrayList e LinkedList aggiungono metodi specifici rispetto a quelli previsti in List.
Metodi propri di ArrayList<T>:
Concetti della lista concatenata semplice protected void removeRange( int da, int a );
Gli elementi adiacenti non sono contigui come nell'array, ma esplicitamente concatenati mediante riferimenti o rimuove tutti gli elementi che vanno dalla posizione da alla posizione a (esclusa). Essendo protected, il
puntatori (si veda anche il cap. 15): metodo è direttamente accessibile da una classe erede-specializzazione di ArrayList
void trimToSizeO
fissa la capacità alla dimensione attuale dell’array list. Utile quando si ritiene che un array list abbia
raggiunto una situazione di stabilizzazione.
n u li
Metodi propri di LinkedList<T>:
void addFirst( T elemento)
void addLast( T elemento )
Aggiungendo il 6 tra il 2 e il 9 si ha: T getFirst()
168 169
Capitolo 9 Collection framework e collezioni custom

T getLast() La classe di utilità Collections


T removeFirst() Contiene alcuni metodi di utilità (static) sulle liste (ArrayList o LinkedList). Si citano in particolare i seguenti
T removel_ast() (per altri dettagli si rimanda alle API di Java):

Anche se le operazioni richiamate da questi metodi sono ottenibili utilizzando metodi di List (es. l.getFirst() è public static int binarySearch( List Is, Object x ) - versione raw
equivalente a l.get(O) etc.), i metodi specifici di LinkedList consentono di operare direttamente e più ipotizza che la lista Is sia ordinata. Ritorna l’indice (posizione) di x (tra 0 e l.size()-1) in Is o un numero
efficientemente alle due estremità (testa e coda) della lista concatenata. negativo (non necessariamente -1 ) se x non è presente. Il metodo assume gli elementi di Is ed x siano
confrontabili. L'efficienza della ricerca binaria è effettivamente ottenuta solo se Is è un ArrayList
Nota: pur essendo possibile utilizzare gli indici su una LinkedList, il loro uso è efficiente su un ArrayList ma
non su una LinkedList. Dovendo realizzare un inserimento in posizione intermedia di un elemento, è utile public static void sort( List Is ) - versione raw
operare con gli indici su un ArrayList, ma con un Listlterator su una LinkedList. Ogni operazione con un indice ipotizza che gli elementi in Is siano oggetti di classi comparabili. Ordina la lista Is secondo il confronto
invocata su una LinkedList comporta il ri-posizionamento (mediante un ciclo) di un cursore sulla lista. stabilito da compareTo(). Il metodo garantisce un'efficienza 0(n*log(n)) (si veda il cap. 17).

Richiami su stack e coda _______________ ____ _ Si nota che tanto binarySearch() quanto sort() sono metodi generici e dunque operano su liste generiche e
In uno stack le operazioni di inserimento (push) e rimozione (pop) avvengono esclusivamente alla fine della parametriche, in cui il tipo degli elementi T si suppone provvisto del metodo di confronto (si veda il cap. 8).
struttura (top o testa). La gestione è LIFO - Last Input First Output. Esempio tipico di stack è la catasta di piatti
o di libri o di pratiche in un ufficio etc. Esistono varianti dei due metodi che oltre a passare la lista etc. accettano anche un oggetto Comparator che
stabilisce il criterio di confronto da seguire, in alternativa al confronto naturale.
In una coda (queue) gli elementi entrano da un estremo (coda della lista) ed escono dall'altro (testa della lista).
La gestione è FIFO - First Input First Output. Tipico esempio è la gestione di una fila disciplinata di persone L’interfaccia Set<T>
davanti ad uno sportello postale o bancario etc. Estende l’interfaccia Collection<T>, ma non introduce nuovi metodi. Il metodo add(T x) aggiunge, senza
creare duplicati, x aH'insieme. L’operazione si basa sul metodo equals per stabilire se x è già presente, ma
inserisci Stack non segue necessariamente un ordine particolare di inserimento.

' Come anticipato, removeAII(Collection<T> c) e retainAII(Collection<T> c) modificano il set this rispettivamente


; con l’insieme differenza e intersezione rispetto alla collezione parametro c.
estrai testa
Le classi HashSet<T> e TreeSet<T>
Coda (o Queue) Sono due classi concrete per operare sui set. Costruttori esistono per creare un set a partire da un altro
M jv; generato con l'altra classe. Es.
estrai- 'ifii i inserisci
TreeSet<lnteger> ts=new TreeSet<lnteger>( hs );
testa coda
dove hs è un HashSet<lnteger> etc.

Esempio di gestione di uno stack: HashSet velocizza l’accesso agli elementi dell’insieme. Tuttavia, l’attraversamento del contenuto di un hash
LinkedList<String> stack=new LinkedList<String>(); set con un iteratore restituisce gli elementi in un ordine qualsiasi. TreeSet mantiene in ordine naturale
stack.addFirst(“uno”); //aggiunta in testa (interfaccia Comparable) il contenuto di un set mediante una struttura ad albero binario ordinato (si veda più
stack.addFirst(“due”); avanti). In generale è più efficiente un HashSet; tuttavia, se l’ordine è importante, occorre utilizzare un
stack.addFirst(“tren); TreeSet.
while( !stack.isEmpty() ) //svuotamento
System.out.println( stack.removeFirst() ); Organizzazione di un albero binario di ricerca
Gli elementi sono memorizzati, come nel caso della lista concatenata, aH’interno di oggetti nodi esplicitamente
concatenati gli uni agli altri mediante riferimenti o puntatori. Nel caso dell’albero binario (si veda anche il cap.
Output prodotto: 19), la struttura è gerarchica del tipo padre-figlio. Un padre ammette al più due figli.
tre Proprietà ricorsiva dell’albero binario di ricerca: ogni nodo memorizza un elemento che è non minore dei suoi
due predecessori (sotto albero sinistro) e non maggiore dei suoi successori (sotto albero destro). L’organizzazione
uno supporta in modo naturale la ricerca binaria. Quanto più l’albero è bilanciato, tanto più è possibile sfruttare
l’efficienza della ricerca binaria.

170 171
Capitolo 9 Collection framework e collezioni custom

Set<lnteger> s2=new HashSet<lnteger>();


s2.add(new lnteger(2));
s2.add(new lnteger(3));
s2.add(new lnteger(6));
System.out.println(s2); s2=[2, 6,3]
Set<lnteger> s3=new HashSet<lnteger>(s1);
Set<lnteger> s4=new HashSet<lnteger>(s1);
s3.removeAII(s2); System.out.println("s1 -s2=“+s3); s1-s2=[1,7]
s4.retainAII(s2); System.out.println(’’s1 *s2="+s4); s1*s2=[3]

Si segnala l’esistenza nel collection framework di un’ulteriore classe concreta sui set detta LinkedHashSet<T>.
Essa si basa su una tabella hash ed in più mantiene in lista concatenata gli elementi aggiunti al set.
LinkedHashSet ha prestazioni di accesso paragonabili a quelle di un HashSet ed in più garantisce l'ordine di
Tabelle hash e c o l l i s i o n i _______________________________ __ __________ _____ inserimento (non l’ordine naturale come fa TreeSet) degli elementi durante un'iterazione. Il costo temporale di
La classe HashSet memorizza gli oggetti in una tabella (array) in cui la posizione di un elemento è determinata un'iterazione di un oggetto HashSet è proporzionale alla capacità della tabella hash, mentre nel caso di un
dal suo hash code. Se h è l’hash code dell’oggetto x da inserire o ritrovare, la posizione associata ad x può oggetto LinkedHashSet è proporzionale al numero effettivo degli elementi presenti.
essere determinata come segue:
Concetto di Map(pa
int h=x.hashCode(); Una map(pa è una particolare collezione in cui gli elementi sono coppie (o corrispondenze):
if( h<0 ) h=-h;
int indice = h%tabella.length; <chiave, valore>
cchiave, valore>
Poiché è inevitabile che oggetti diversi possano dar luogo allo stesso hash code o corrispondere comunque <chiave, valore>
alla stessa posizione (collisione), la tabella memorizza gli elementi distinti aventi lo stesso numero di hash in
posizioni vicine a quella associata al numero di hash o usando liste concatenate (bucket) esterne:
Una map(pa realizza una funzione, data la chiave si vuole (possibilmente in modo veloce) rintracciare il valore
associato. In una map(pa non sussistono duplicati. In altre parole, le entrate (coppie) esistenti sono associate
a chiavi distinte. Tuttavia, uno stesso valore può essere associato a chiavi diverse. L’aggiunta di una nuova
coppia con una chiave già presente, determina il rimpiazzamento del valore pre-esistente. Sia la chiave (key)
che il valore (value) sono oggetti. In realtà, l’interfaccia Map<K,V> è generica in due tipi parametrici: K è il tipo
delle chiavi, V il tipo degli elementi.

L’interfaccia Map<K,V>:_____________________________________________________________________
void clearf);
svuota la mappa this
boolean containsKeyf K chiave );
ritorna true se esiste in this una coppia con questa chiave, false altrimenti
collisioni risolte collisioni risolte all’esterno boolean containsValue( V valore );
all'interno della della tabella con liste di trabocco ritorna true se almeno una coppia esiste in this con questo valore
tabella (array di bucket) V put( K chiave, V valore );
aggiunge alla mappa this una nuova corrispondenza cchiave,valorex Aggiorna un'eventuale
Operazioni insiemistiche corrispondenza già esistente con questa chiave rimpiazzando il valore esistentecon quello fornito, e
Set<lnteger> s1=new HashSet<lnteger>(); restituendo il vecchio valore. Il metodo ritorna nuli se lachiave non è già presente in this
s1.add(new lnteger(1 )); V get( K chiave );
s1.add(new lnteger(3)); ritorna l’oggetto associato a questa chiave nella mappa this, o nuli se la chiave non è presente
s1.add(new lnteger(7)); boolean isEmpty();
System.out.println(sl); s1=[1,3,7] ritorna true se la mappa this è vuota
voidputAII( Map<K, V> m );
esegue la put su this di tutte le coppie in m
V remove( K chiave );
elimina nella mappa this la corrispondenza cchiave,valore> se chiave è presente, e ritorna il valore
172 173
Capitolo 9 Collection framework e collezioni custom

associato class MioComparatore implements Comparator<String>{


int size(); public int compare( String s1, String s2 )(
ritorna il numero delle coppie nella mappa this if( s1.length()<s2.length() ) return -1;
Collection<V> values(); if( s1.length()>s2.length() ) return +1;
restituisce una collezione dei soli valori presenti in this. Si possono scandire tali valori con un iterator return s1.compareTo(s2);
Set<K> keySet(); }//compare
restituisce un set delle sole chiavi presenti in this. {//MioComparatore

Le classi HashMap<K,V>eTreeMap<K,V>___________ In un main si può dunque avere:


HashMap si basa su una tabella hash in cui, similmente a quanto visto per HashSet, la distribuzione delle
coppie cchiave, valore> avviene in base al codice hash della chiave. Risultano velocizzate le operazioni di List<String> a= new ArrayList<String>();
accesso agli elementi, ma non viene garantito alcun ordine delle coppie. TreeMap si appoggia ad una struttura a.add(“strada”); a.addfcasa’’); a.addfbanana’’);
ad albero che sebbene sia meno efficiente di una HashMap, è in grado di garantire l’ordinamento naturale a-addCoro"); a.addCdado");
delle coppie in base alla chiave. Esistono costruttori, nelle due classi, che consentono di passare da una System.out.println( a );
rappresentazione all’altra. Collections.sort( a ); //ordina a secondo l’ordine naturale delle stringhe
È disponibile altresì la classe concreta LinkedHashMap<K,V> dotata di caratteristiche analoghe a quelle System.out.println( a );
descritte in precedenza con riferimento alla classe LinkedHashSet. Il costruttore Collections.sort( a, new MioComparatore() ); //ordina a secondo MioComparatore
LinkedHashMap<K,V>(capacitàlniziale, fattoreDiRiempimento, ordineDiAccesso) consente, ponendo a true System.out.println( a );
l’ultimo parametro (ordineDiAccesso), di pianificare anziché l'ordine di inserimento (default) delle coppie,
l ’ordine di accesso. Dopo ogni operazione get/put, la coppia è posta in coda alla lista delle coppie. Estensione ed istanziazione al “volo"
È possibile evitare l’introduzione di una classe a parte che implementa Comparator ricorrendo ad una classe
Riallocazione di una tabella hash anonima, ossia facendo uso dell’estensione ed instanziazione al volo come mostrato di seguito.
La gestione di un HashSet o una HashMap prevede la riallocazione della struttura dati quando il riempimento
raggiunge una certa soglia espressa come percentuale della capacità della tabella (es. 75%). La riallocazione, List<lnteger> a=Arrays.asList( 10, 3, 7, 4 ); //asList ha un parametro vararg
controllabile mediante un parametro di un costruttore (fattoreDiRiempimento), può rendersi necessaria per Collections.sort( a ); //ordina la lista a per valori crescenti
garantire l’efficienza degli accessi. System.out.println( a );
Coliections.sort( a, new Comparator<lnteger>(){
L’interfaccia C o m p a ra to ri public int compare( Integer x, Integer y ){
La classe Coliections offre i metodi sort e binarySearch come servizi di utilità su una lista di oggetti. Tali return y-x;
metodi richiedono che gli oggetti siano Comparable. {//compare
}); //ordina la lista a per valori decrescenti
Cosa succede se si desidera ordinare una lista secondo un criterio diverso da quello espresso dal metodo System.out.println(a);
compareTo? 0 se la classe degli elementi non implementa affatto Comparable ? Si potrebbe ridefinire il
metodo compareTo ad es. progettando una classe erede. In alternativa, le API di Java forniscono l'interfaccia Il meccanismo è utilizzabile non solo per implementare al volo un’interfaccia ma anche per estendere una
Comparator con il metodo compare. Esiste una nuova versione dei metodi sort e binarySearch che accettano classe preesistente e ridefinire qualche metodo di interesse. In ogni caso viene introdotta una classe anonima
la lista ed un oggetto Comparator, ossia un’istanza di una classe che implementa Comparator. "consumata sul posto”.

public interface Comparator<T>{ La classe di utilità Arrays


int compare(T o1, To2 ); Appartiene, come Coliections, alle classi del Collection Framework di Java. Fornisce metodi di utilità -dunque
}//Comparator tutti static- in versione overloaded, ad es. per ordinare un array i cui elementi siano di un qualunque tipo di
base o oggetto muniti dell’operazione di confronto (Comparable) o per la ricerca binaria (binarySearch) etc.
Il metodo compare(x1 ,x2) ritorna <0 se x1 precede x2, 0 se x1 è uguale a x2, >0 altrimenti.
Esempio:
Un esempio d’uso di Comparator ____ ___________
L’ordinamento di una lista di String si basa sul confronto lessicografico delle stringhe e dunque sul confronto int []a=new int[10{;
naturale stabilito dal metodo compareTo() di String. Volendo ordinare una lista di String per lunghezza caricamento di a
crescente ed a parità di lunghezza in ordine alfabetico si può procedere come segue. Si crea una classe es. doublé []b=new double[20{;
MioComparatore che implementa l’interfaccia Comparator in modo da fissare il confronto in base alla caricamento di b
lunghezza e quindi al confronto lessicografico tra due stringhe. Si invoca Collections.sort passando la lista e Arrays.sort( a ); //ordina a per valori crescenti
un oggetto di classe MioComparatore. Arrays.sort( b ); //ordina b per valori crescenti

174 175
Capitolo 9 Collection framework e collezioni custom

Dal momento che gli oggetti di classe Razionale sono confrontabili, un array w di razionali pieno in ogni
posizione (dunque da 0 a w.lenght-1) può essere ordinato come segue: è possibile limitarsi al ciclo implicito di iterazione:

Arrays.sort( w ); //ordina tutto l’array w for( int x : Is ){


//x è la variabile di iterazione + auto unboxing, Is è la collezione che fornisce l’iteratore
oppure si può ordinare un range consecutivo di posizioni: elabora x;
}
Arrays.sort( w, 0, size-1 ); //ordina w tra gli indici 0 e size-1
Nel corpo del for si può utilizzare direttamente x. Resta implicita la struttura e l'uso (avanzamento)
Per gli array di oggetti è possibile utilizzare il metodo sort() o binarySearch() che consentono di passare anche dell’iteratore. Attenzione: poiché l’uso dell’iteratore è sottinteso, non è possibile effettuare una rimozione di un
un oggetto Comparator nel caso si voglia utilizzare un criterio di confronto diverso da quello naturale. Altri elemento durante un for-each: in questi casi serve l’uso esplicito dell'iteratore.
metodi da segnalare sono:
Piu in generale, il nuovo for-each si può sintetizzare come segue:
String toString( array di un tipo primitivo o di oggetti );
ritorna sotto forma di String il contenuto di array (da 0 a length-1 ) for( TipoElemento elemento: collection ){//collection deve essere iterabile
elabora elemento;
T[]copyOf( T[] array, nuovaJength ); }
ritorna una copia di array, la cui dimensione è nuovajength. Eventuali slot liberi sono posti a nuli. Il
metodo può essere utilizzato per scalare (espandere/contrarre) un array Per estrapolazione, la nuova versione del ciclo di for è disponibile anche su un comune array, es.

int hashCode( array di un tipo primitivo o di oggetti); int []a=new int[10];...


ritorna un hashCode corrispondente al contenuto dell'array
oltre che:
boolean equals( array 1, array2 );
ritorna true se arrayl e array2 sono uguali elemento per elemento. Gli array possono essere di un tipo for(int i=0; ka.length; i++){... a [i]...}
base o di oggetti
ora è anche possibile scrivere:
List<T>asListf T... a )
ritorna l'array a di elementi di tipo T sotto veste di lista. for( int x: a ){... x ...}

L’interfaccia lterable<T> in cui si sottointende che x assuma ordinatamente i valori degli elementi da a[0] ad a[a.length-1].
Espone un solo metodo come segue:
Per altre esigenze, es. scansione a ritroso, avanzamento della variabile di controllo non in modo unitario etc.
public interface lterable<T>{ occorre programmare un classico ciclo di for.
lterator<T> iterator()
}//lterable Caso di studio; il Crivello di Eratostene
Dalle scuole medie è noto il metodo del Crivello di Eratostene per determinare tutti i numeri primi esistenti tra 2
Una classe si dice iterabile, se essa implementa l’interfaccia Iterable. In queste condizioni il compilatore ed un massimo positivo N. Si inizializza un insieme cnvello con tutti gli interi da 2 ad N. Si inizializza un
conosce a priori che un qualunque oggetto della classe dispone di un iteratore che si può ottenere invocando il insieme primi a vuoto. Si iterano le seguenti due fasi sino al raggiungimento della situazione di crivello vuoto:
metodo standard iterator(). Da ciò derivano diverse conseguenze e semplificazioni come la nuova forma di ricerca del prossimo minimo in crivello; esso è certamente primo, e lo si aggiunge a primi
ciclo for-each che presuppone l’utilizzo di un iteratore. eliminazione da crivello del minimo edi tutti i suoi multipli
Al termine dell’algoritmo, l’insieme primi contiene tutti e soli i primi cercati.
Tutte le classi collezioni del collection framework sono iterabili.
Esempio di applicazione: N=10
Ciclo for-each _____
Anziché scrivere, ad es. crivello=[2,3,4,5,6,7,8,9,10], primi=[]
estrazione minimo: 2 primi=(2]
for( Iteratorelnteger> i=ls.iterator(); i.hasNextQ; )( eliminazione dei multipli di 2: crivello=[3,5,7,9]
Integer x=i.next(); estrazione minimo: 3 primi=[2,3]
elabora x; eliminazione dei multipli di 3: crivello=[5,7]
} estrazione minimo: 5 primi=[2,3,5]
176 177
Capitolo 9 Collection framework e collezioni custom

eliminazione dei multipli di 5: crivello=[7] public int size(){ return primi.size();}


estrazione minimo: 7 primi=[2,3,5,7] public void filtrai)!
eliminazione dei multipli di 7: crivello^] while( Icrivello.isEmptyO ){
fine: primi=[2,3,5,7] //1 fase: estrazione del minimo da crivello
int minimo=crivello.iterator().next(); //minimo è certamente primo
L’interfaccia Crivello e una^classe astratta CrivelloAstratto:________ _ primi.add( minimo );
package poo.eratostene; //2 fase: eliminazione del minimo e dei suoi multipli da crivello
public interface Crivello extends lterable<lnteger>{ //definisce Crivello come ADT int multiplo=minimo;
public int size(); while( multiplo<=N ){
public void filtrai); crivello.remove( multiplo );
}//Crivello multiplo+=minimo;

package poo.eratostene;
public abstract class CrivelloAstratto implements Crivello! }//filtra
public int size(){
int c=0; public lterator<lnteger> iterator()l
for( int x: this ) c++; //for-each e auto unboxing return primi.iterator();
return c; }//iterator
}//size
public String toString(){ public static void main( String [jargs ){//main di prova
StringBuilder sb=new StringBuilder( 1000); Crivello cE=new CrivelloSet! 1000 );
int c=0; cE.filtra();
for( int x: this ){//for-each e auto unboxing System.out.println(cE);
sb.append(String.format("%8d",x)); }//main
c++;
if( c%8==0 ) sb.append('\n’); }//CrivelloSet
}
return sb.toString(); Output generato:
}//toString
2 3 b 7 11 13 17 19
}//CrivelloAstratto 23 29 31 37 41 43 47 b3
b9 61 6 7 71 73 19 83 89
Il metodo toString di CrivelloAstratto predispone 8 primi per linea. Dopo 8 primi, si comanda l'andata a capo 97 101 103 107 109 113 127 131
137 139 149 Ibi lb / 163 16 1 173
appendendo sullo string builder il carattere new line '\n‘. 179 181 191 193 197 199 211 223
227 229 233 239 241 261 2b7 263
Nota: In CrivelloAstratto, i metodi non nominati, cioè iterator() e filtrai), sono abstract implicitamente. Di seguito 269 271 2 II 281 283 293 307 311
313 317 331 337 34 / 349 3b3 3b9
si mostra una classe concreta, CrivelloSet, erede di CrivelloAstratto che si basa sull'uso di set. 367 373 3 19 383 389 39 / 401 409
419 421 431 433 439 443 449 4b7
461 463 46 7 4 79 48 1 491 499 b03
Una classe CrivelloSet:______________________________________________________________________ b09 b21 b2 3 b4 1 b4 / bb 7 b6 3 b69
package poo.eratostene; b 71 b7 / b8 7 b9 3 b99 601 60 1 613
617 631 641 64 7 6b3
import java.util.*; 619 643 6b9
661 6 73 67 / 683 691 /01 / 09 719
public class CrivelloSet extends CrivelloAstratto! 72 7 733 139 /43 7bl Ibi 761 769
private Set<lnteger> crivello=new LinkedHashSet<lnteger>(); 113 /8 / 19 1 809 811 821 823 82 /
829 839 8b3 8b7 8b9 863 8 77 881
private Set<lnteger> primi=new LinkedHashSet<lnteger>(); 883 887 907 911 919 929 93 7 941
private final int N; 94 7 9b3 96 7 971 977 983 991 99 /

public CrivelloSet! int N ){ Considerato che l’ordine di inserimento degli elementi in crivello e in primi coincide con l’ordinamento naturale,
if( N<2 ) throw new RuntimeExceptionCN minore di 2“); l'uso di LinkedHashSet evita il ricorso al “più pesante" TreeSet. Resta comunque garantito, ad ogni iterazione
this.N=N; del ciclo nel metodo filtra!), l'estrazione del minimo da crivello ciò che permette di ottenere l’insieme dei primi
for( int i=2; i<=N; i++ ) crivello.add(i); disposti appunti dal più piccolo al più grande.
}//costruttore

178 179
Capitolo 9 Collection framework e collezioni custom

Vector<T> generico in versione iterabile Le inner class


package poo.util; L'iteratore di ArrayVector è stato ottenuto programmando una inner class (classe interna) Iteratore che
public interface Vector<T> extends lterable<T> { implementa lterator<T>. L’inner class è privata e dunque inaccessibile dall’esterno. Quando si chiede
public int size(); l’iteratore ad un vector, si istanzia la inner class e si ritorna l’istanza creata. Il Client nulla sa dell’identità della
public int indexOf( T elem ); classe dell’oggetto iteratore restituito se non il fatto che essa implementa il contratto lterator<T>.
public boolean contains( T elem );
public T get( int indice ); L’implementazione della inner class Iteratore si basa su un semplice cursore (indice) corrente inizializzato a -1
public T set( int indice, T elem ); ad indicare che si trova prima del primo elemento. Durante il suo ciclo di vita, corrente o vale -1 o riferisce un
public void add( T elem ); elemento del vector (array) che è già stato restituito. Pertanto, il metodo hasNext() ritorna true o perché
public void add( int indice, T elem ); corrente vale -1 ma size()>0 o perché corrente è minore size()-1: in entrambi i casi esiste certamente almeno
public void remove( T elem ); un prossimo elemento restituibile da next(). Il metodo next() solleva un’eccezione NoSuchElementException
public T removef int indice ); se hasNextQ ritorna false. Altrimenti, prima incrementa corrente, quindi restituisce l'elemento della collezione
public void clearQ; in posizione corrente. In più viene posta a true la variabile di istanza rimuovibile per esprimere che in questo
public boolean isEmpty(); momento potrebbe essere possibile un’esecuzione di remove (esiste l’elemento corrente). Il metodo remove()
public Vector<T> subVector( int da, int a ); solleva un’eccezione UlegalStateException se rimuovibile vale false. Altrimenti, prima pone a false rimuovibile
}/A/ector (per impedire due invocazioni consecutive di remove) quindi elimina l'elemento corrente e arretra la variabile
corrente (che così o diventa -1 o punta ad un elemento già restituito).
package poo.util;
import java.util.*; È importante riflettere che, nell’esempio, un’istanza della inner class vive solo in presenza di un’istanza della
public class ArrayVector<T> implements Vector<T>{ outer class (cioè ArrayVector). Sussiste una sinergia che consente alla istanza della inner class di riferire (per
visibilità) i campi o variabili di istanza dell’oggetto dell'outer class. Iteratore accede ai dati di ArrayVector, es.
public lterator<T> iterator(){ return new lteratore();} size ed array.

private class Iteratore implements lterator<T>{//inner class Esiste un riferimento nascosto nell'istanza della inner class che punta all'istanza della outer class in cui la
private int corrente=-1; prima è annidata.
private boolean rimuovibile=false;
public boolean hasNext(){ Quando ci si trova in un metodo della inner class, il pronome this denota, naturalmente, l’oggetto-istanza della
if( corrente==-1 ) return size>0; inner class. Java consente di specificare l’oggetto della outer class in cui l’oggetto inner è annidato con la
return corrente<size-1; notazione OuterClass.this. Ad es., il metodo removeQ di Iteratore delega la rimozione al metodo removeQ
}//hasNext della outer class ArrayVector con l’istruzione: ArrayVector.this.remove(corrente) dove corrente è il cursore.
public T next(){
if( !hasNext() ) throw new NoSuchElementException(); For-each su oggetti Vector
corrente++; Essendo iterabile, diventa possibile scrivere un for-each anche su un oggetto vector come segue:
rimuovibile=true;
return array[corrente]; Vector<lnteger> a=new ArrayVector<lnteger>();
}//next //esempio di caricamento di a
public void remove(){ for(int i=0; i<10; i++) a.add(i); //con auto boxing di i
if( Irimuovibile ) throw new HlegalStateExceptionQ;
rimuovibile=false; for( int x: a )
ArrayVector.this.remove( corrente ); System.out.print(x+“ "); //for-each
corrente-; System.out.println();
}//remove
}//lterator In alternativa si può utilizzare esplicitamente l’iteratore:

}//ArrayVector lterator<lnteger> it=a.iterator();


while( it.hasNext() )(
int x=it.next();
System.out.print(x+" ");
}
System.out.println();

180 181
Capitolo 9 Collection framework e collezioni custom

Ancora sulle inner class


In alcune situazioni (si veda più avanti nel corso), la inner class non necessita di accedere ai dati della outer Qualche altro metodo abstract può essere introdotto, es. un metodo factory del tipo
class. In questi casi è opportuno evitare l’aggravio di memoria dovuto al riferimento nascosto tra istanza inner
e corrispondente istanza outer, programmando la classe inner in versione static come segue: CollezionelF<T> createQ

public class O uter... ( che ritorna un oggetto il cui tipo dinamico è poi quello di una specifica classe erede. Il metodo factory() è utile
variabili o campi di Outer per pre-implementare metodi della classe astratta che ritornano un oggetto del tipo CollezionelF<T>.
metodi di Outer
Nel ridefinire (©Override) il metodo factory create() in una sotto classe, diciamola CollezioneConcreta<T>, è
private static class lnner{ //non c'è più il riferimento ad Outer.this possibile specificare come tipo di ritorno la classe erede CollezioneConcreta<T> più specifica e non
variabili o campi di Inner CollezionelF<T> (più generale), mentre non è possibile fare alcun cambiamento ai tipi di eventuali parametri
metodi di Inner presenti. Questa proprietà è detta covarianza del tipo di ritorno dei metodi Java e mantiene inalterato il legame
} ridefinizione-dynamic binding.

}//Outer Dopo aver predisposto la classe CollezioneAstratta<T>, si possono sviluppare classi concrete eredi di
CollezioneAstratta<T> e che si fondano o sull’uso di una classe collezione di java.util o su qualche soluzione
In questo esempio (si tenga presente che la inner class può essere piazzata dovunque nella classe Outer: particolare desiderata dal programmatore. Ad es., in una classe erede di CollezioneAstratta<T>, il
prima delle variabili o dei metodi o alla fine etc.) daH’interno di Inner non si possono accedere le variabili o i programmatore potrebbe memorizzare gli elementi su un array nativo di Java e farsi carico di tutte le
metodi di istanza di Outer che presuppongono il this di Outer (cioè Outer.this). operazioni di gestione (si riveda la classe ArrayVector<T>), o su una lista concatenata a puntatori espliciti (si
veda più avanti nel corso). In alternativa ci si può fondare su una classe del collection framework di java.util.
Si nota infine, che una inner class può essere annidata non solo aH’interno di un’altra classe, ma anche dentro
un metodo per fornire funzionalità al metodo. Una classe concreta erede di CollezioneAstratta<T> fornirà di norma un'implementazione della struttura di
iterazione. Ovviamente, se si utilizza una classe del collection framework, essa è già dotata dell’iteratore per
Progetto di collezioni custom cui le cose si semplificano (almeno potenzialmente). Diversamente, mediante una inner class privata, si
Le classi collezioni di java.util possono essere direttamente impiegate per ottenere una soluzione di problemi implementa l’iteratore da fornire al Client quando si invoca il metodo iteratorQ (si riveda la classe
applicativi. In altri casi, tali classi possono essere utilizzate come componenti di classi collezioni definite dal ArrayVector<T> in versione iterabile).
programmatore (collezioni custom), più naturali al problema.
Nel caso dell’agendina telefonica, l'ADT è l’interfaccia Agendina che si rende iterabile estendendo
Ad es. l'agendina telefonica rappresenta una classe collezione custom, suggerita dal dominio applicativo, la lterable<Nominativo>. Si può quindi progettare e (parzialmente) implementare AgendinaAstratta che
cui concretizzazione può basarsi sull’uso delle classi del collection framework di Java. L'agenda può essere implementa Agendina e concretizza metodi quali: toStringQ, equalsQ, hashCode(), remove( nominativo ),
implementata utilizzando una List o un Set o una Map. Per quanto discusso in precedenza, l'implementazione cerca( prefisso, telefono ) etc. Non possono essere concretizzati, invece, metodi quali aggiungi nominativo ),
mediante una mappa appare come la più "naturale" al problema. iterator() etc. che necessariamente dipendono dalle scelte implementative della classe erede concreta, ossia
dal tipo di rappresentazione adottata per la collezione custom.
Poiché a priori può non essere chiaro quale sia la scelta implementativa più conveniente, di seguito si
suggerisce una strategia di progetto che lascia aperta la scelta di una specifica realizzazione di una collezione Come esempio dimostrativo si propone una gerarchia di classi per l’agendina telefonica. Con l’occasione si
custom. La strategia è stata delineata nel problema del crivello di Eratostene e consiste nell’introdurre esemplifica anche l'introduzione dei commenti speciali utili per ottenere, con lo strumento javadoc, una
inizialmente una interfaccia (tipo astratto o ADT) che definisce quali siano le operazioni desiderate sulla documentazione html delle API sviluppate.
collezione custom. Sia CollezionelF questa interfaccia. In molti casi tale interfaccia è generica nel tipo T degli
elementi, ed iterabile. L’interfaccia Agendina con i commenti speciali per javadoc:
package poo.agendina;
Successivamente si progetta una classe astratta, diciamola CollezioneAstratta<T>, anch’essa generica nel import java.io.’ ;
tipo T degli elementi, che implementa l’interfaccia CollezionelF<T> e provvede a concretizzare quanti più
metodi è possibile. Non tutti i metodi di CollezionelF<T> sono concretizzabili nella classe astratta in quanto r
alcuni necessariamente dipendono dalla scelta finale di rappresentazione dell’ADT che è parte delle decisioni * Tipo di dato astratto che descrive un'agendina telefonica.Gli elementi sono di tipo Nominativo.
di progetto di una classe concreta erede di CollezioneAstratta<T>. Ad es., nella classe astratta il metodo * Non si ammettono le omonimie. L'agendina è supposta mantenuta ordinata per cognome crescente e a
lterator<T> iterator() è necessariamente abstract. Tuttavia esso può essere utilizzato per concretizzare * parità di cognome per nome crescente.
qualche altro metodo, es. m(params), che si fonda suH’iteratore. Dopo tutto, una classe erede fornirà una * ©author Libero Nigro
concretizzazione di iteratorQ e quindi per dynamic binding sarà questo metodo che verrà utilizzato durante 7
ogni esecuzione di m(). Un tipico caso è il metodo toString() che costruisce e restituisce una stringa public interface Agendina extends lterable<Nominativo>{
corrispondente alla collezione custom mediante l'iteratore. I metodi di CollezionelF<T> non implementati in
CollezioneAstratta<T>restano automaticamente abstract.
182 183
C ap ito lo 9 Collection framework e collezioni custom

/**
* Restituisce il numero di nominativi dell'agenda. Una classe astratta AgendinaAstratta:___________________
* ©return il numero di nominativi in agenda. package poo.agendina;
7 import java.util.*;
public int size(); import java.io.*;
/**
* Svuota il contenuto dell'agendina. public abstract class AgendinaAstratta implements Agendinaf
7 public int size(){
public void svuota(); int conta=0;
r for( Nominativo n: this ) conta++;
* Aggiunge un nominativo all'agenda. Non si ammettono le omonimie. L'aggiunta avviene in ordine return conta;
* alfabetico crescente del cognome ed a parità1di cognome in ordine alfabetico del nome. }//size
* ©pararti n il nominativo da aggiungere
7 public void svuota(){
public void aggiungi Nominativo n ); lterator<Nominativo> it=this.iterator();
/** while( it.hasNextQ ) {
* Rimuove un nominativo dall'agenda. it.next(); it.remove();
* ©param n il nominativo da rimuovere dall'agenda. }
7 }//svuota
public void rimuovi( Nominativo n );
/** public void rimuovi( Nominativo n ){
* Cerca un nominativo uguale ad n. lterator<Nominativo> it=this.iterator();
* ©param n il nominativo da cercare, significativo solo per cognome e nome. while( it.hasNext() ) {
* ©return il nominativo dell'agenda uguale ad n o nuli se n non e' in agenda. Nominativo x=it.next();
7 if( x.equals(n) ) { it.remove(); break;}
public Nominativo cerca( Nominativo n ); if( x.compareTo(n)>0 ) break;
/** }
* Cerca un nominativo nell'agenda, di assegnato prefisso e numero di telefono. }//rimuovi
* ©param prefisso
* ©param telefono public Nominativo cerca( Nominativo n ){
* ©return il nominativo trovato o nuli for( Nominativo x: this ){//ricerca lineare "ottimizzata"
7 if( x.equals(n) ) return x;
public Nominativo cerca( String prefisso, String telefono ); if( x.compareTo(n)>0 ) break;
r }
* Salva il contenuto dell'agenda su file. return nuli;
* @param nomeFile il nome esterno del file per il salvataggio. {//cerca
* @throws lOException
7 public Nominativo cerca( String prefisso, String telefono ){
public void salva(String nomeFile) throws lOException; for( Nominativo x: this )
r if( x.getPrefisso().equals(prefisso) &&
* Ripristina il contenuto dell'agenda, a partire da un file. x.getTelefono().equals(telefono) ) return x;
* ©param nomeFile il nome esterno del file da cui attingere. return nuli;
* @throws lOException es. se il file non esiste }//cerca
7
public void ripristina(String nomeFile) throws lOException; public String toString(){
StringBuilder sb=new StringBuilder(IOOO);
}//Agendina for( Nominativo x; this ){
sb.append( x );sb.append('\n');
È possibile creare la documentazione html, ad es. daH'intemo di Eclipse, esportando il file in formato javadoc e }
configurando javadoc con il suo pathname di installazione (es. c:\Programmi\Java\jdk...\bin\javadoc) e return sb.toStringQ;
specificando la directory da utilizzare per memorizzare tutti i file generati. }//toString
184 185
Capitolo 9 Collection framework e collezioni custom

br.close();
public int hashCode(){ if( okLettura ){
final int MOLT=43; this.svuota();
int h=0; for( Nominativo n: tmp ) this.aggiungi(n);
for( Nominativo n: this )h=h‘ MOLT+n.hashCode(); }
return h; else throw new IOException(); //ripropaga eccezione
}//hashCode }//ripristina

public boolean equals( Object x ){ }//AgendinaAstratta


if( !(x instanceof Agendina) ) return false;
if( x==this ) return true; Il metodo ripristina() utilizza una LinkedList<Nominativo> tmp in cui vengono tentativamente ripristinati i
Agendina a=(Agendina)x; nominativi letti dal file di tipo testo. La lista è poi copiata sull’agendina, dopo averla prima svuotata. Tutto ciò è
if( this.size()!=a.size() ) return false; fatto per fronteggiare eventuali malformazioni del file.
lterator<Nominativo> i1=this.iterator();
lterator<Nominativo> i2=a.iterator(); Il metodo equals() ritorna true se due agendine hanno la stessa cardinalità (size) e ordinatamente hanno
while( i1.hasNext() ){ uguali nominativi al loro interno.
Nominativo n1=i1.next();
Nominativo n2=i2.next(); Classi collezioni concrete
if( In1.equals(n2) ) return false; È stata già presentata la classe AgendinaVector basata su un vector di nominativi, che ora deve estendere
} AgendinaAstratta e non implementare direttamente Agendina. La struttura di iterazione di AgendinaVector può
return true; rimandare semplicemente a quella già disponibile su un ArrayVector:
}//equals
public lterator<Nominativo> iterator(){
public void salva(String nomeFile) throws IOException{ return tabella.iteratorQ;
PrintWriter pw=new PrintWriter( new FileWriter(nomeFile)); }//iterator
for( Nominativo n: this ) pw.println(n);
pw.close(); Per dimostrare l'uso delle classi collezioni di Java si riportano di seguito le implementazioni di altre quattro
}//salva classi concrete: AgendinaMap, AgendinaSet, AgendinaLL ed AgendinaAL che rappresentano rispettivamente
l’agendina mediante una Map, un Set, una LinkedList ed un ArrayList. Tali implementazioni sfruttano i metodi
public void ripristina(String nomeFile) throws IOException{ disponibili di caso in caso e costituiscono usuali opzioni di progetto.
BufferedReader br=new BufferedReaderf new FileReader(nomeFile) );
String linea=null; Una classe AgendinaMap:___________________________________________________________________
StringTokenizer st=null; package poo.agendina;
List<Nominativo> tmp=new LinkedList<Nominativo>(); import java.util.*;
//tmp e' utile per far fronte a malformazioni del file public class AgendinaMap extends AgendinaAstrattaf
boolean okLettura=true; private Map<Nominativo,Nominativo tabella=new TreeMap<Nominativo,Nominativo>();
for(;;){ @Override
linea=br.readLine(); public int size(){ return tabella.size(); }//size
if( linea==null ) break; //eof di br @Override
st=new StringTokenizer(linea," -"); public void svuota(){ tabella.clearQ; }//svuota
try{ ©Override
String cog=st.nextToken(); public void aggiungi Nominativo n ) { tabella.put(n.n); ^/aggiungi
String nom=st.nextToken(); ©Override
String pre=st.nextToken(); public void rimuovi( Nominativo n ) { tabella.remove( n ); }//rimuovi
String tel=st.nextToken(); @Override
tmp.add( new Nominativo( cog, nom, pre, tei ) ); //aggiunge in coda public Nominativo cerca( Nominativo n ) { return tabella.get( n ); }//cerca
}catch(Exception e){ ©Override
okLettura=false; public lterator<Nominativo> iterator(){ return tabella.values().iterator(); }//iterator
break; }//AgendinaMap
}

186 187
Capitolo 9 Collection framework e collezioni custom

AgendinaMap memorizza la tabella dei nominativi su una TreeMap. Per semplicità, un oggetto nominativo è ©Override
utilizzato sia come chiave che come valore. Come chiave si sfrutta l'ordinamento naturale fornito dalla classe public lterator<Nominativo> iterator(){ return tabella.iterator(); }//iterator
Nominativo. Il metodo aggiungi, basandosi sul metodo put di Map, automaticamente aggiorna una coppia pre­ ©Override
esistente avente la stessa chiave del nominativo da aggiungere. Il metodo iterator è collegato a quello della public void svuota(){ tabella.clear(); }//svuota
Collection restituita dal metodo values della mappa. Una rimozione con l’terator ha effetto anche sulla mappa. ©Override
public void aggiungi! Nominativo n ){
Una classe AgendinaSet : _________________ //aggiunge n in ordine, evitando le omonimie
package poo.agendina; Listlterator< Nominativo lit=tabella.listlterator();
import java.util.*; boolean flag=false;
while( lit.hasNextQ && Iflag ){
public class AgendinaSet extends AgendinaAstratta{ Nominativo x=lit.next();
private Set<Nominativo> tabella=new TreeSet<Nominativo>(); if( x.equals(n) ) { lit.set(n); flag=true;}
else if( x.compareTo(n)>0 ) { lit.previousQ; lit.add(n); flag=true;}
©Override }
public int size(){ return tabella.size(); }//size if( Iflag ) lit.add(n);
@Override Raggiungi
public void svuota(){ tabella.clear(); }//svuota ©Override
©Override public int size(){ return tabella.sizeQ; }//size
public void aggiungi( Nominativo n ) { tabella.remove(n); tabella.add(n); ^/aggiungi }//AgendinaLL
©Override
public void rimuovi! Nominativo n ) { tabella.remove( n ); }//rimuovi In questo caso la tabella è mantenuta su una lista concatenata. Da ciò deriva che è inutile ridefinire i metodi
©Override cerca! nominativo ) e rimuovi! nominativo ) che si riducono ad una ricerca lineare. Il metodo aggiungi sfrutta
public Nominativo cerca! Nominativo n ){ un Listlterator<Nominativo>.
if( tabella.contains(n) ){
lterator<Nominativo> i=tabella.iterator(); Una classe AgendinaAL:____________________________________________________________
while( i.hasNext() ){ package poo.agendina;
Nominativo q=i.next(); import java.util.*;
if( q.equals(n) ) return q; public class AgendinaAL extends AgendinaAstrattaf
private List<Nominativo> tabella;
public AgendinaAL(){ this(100);}
return nuli; public AgendinaAL! int n ){
}//cerca if( n<=0 ) throw new HlegalArgumentException();
©Override tabella=new ArrayList<Nominativo>(n);
public lterator<Nominativo> iterator(){ return tabella.iteratore(); }//iterator }
©Override
}//AgendinaSet public int size(){ return tabella.size(); }//size
©Override
La classe AgendinaSet memorizza la tabella su un TreeSet che mantiene l'ordine naturale dei nominativi e public void svuota(){ tabella.clear(); }//svuota
velocizza le operazioni di ricerca. Siccome in un set non viene aggiunto un elemento che sia già presente (in ©Override
base al metodo equals), nel metodo aggiungi nominativo prima si provvede a rimuovere il nominativo n public void aggiungi! Nominativo n )!
ricevuto parametricamente, quindi si aggiunge n. Tali operazioni garantiscono l'aggiornamento di un int i=Collections.binarySearch( tabella, n );
nominativo pre-esistente. Un altro commento può essere fatto con riferimento al metodo cerca!) di un if( i>=0 ){tabella.set(i.n); return;}
nominativo. In realtà sebbene sia veloce la verifica se un dato nominativo è presente o meno nel set (tramite il i=0;
metodo contains()), è poi comunque necessario scandire il tree set con un iteratore per trovare e restituire il while( i<tabella.size() ){
nominativo effettivo nella tabella. Nominativo x=tabella.get(i);
if( x.compareTo(n)>0 ) break;
Una classe AgendinaLL: i++;
package poo.agendina; }
import java.util.*; tabella.add( i, n );
public class AgendinaLL extends AgendinaAstratta{ Raggiungi
private List<Nominativo> tabella=new LinkedList<Nominativo>();
188 189
Capitolo 9 Collection framework e collezioni custom

@Override BitSetf int capacitàiniziale )


public void rimuovi( Nominativo n ){ costruttore di un oggetto BitSet
int i=Collections.binarySearch( tabella, n ); int lengthQ
if( i<0 ) return; indice dell’ultimo bit, più uno
tabella.remove(i); boolean get( int i )
}//rimuovi ritorna il bit di indice i, come boolean (0-false, 1-true)
@Override void set( int i )
public Nominativo cerca( Nominativo n ){ pone ad 1 il bit di indice i
int i=Collections.binarySearch( tabella, n ); void clear( int i )
if( i<0 ) return nuli; pone a 0 il bit di indice i
return tabella.get(i); void and( BitSet b )
}//cerca modifica this con la and-logica bit-a-bit con b (si veda l’Appendice A)
@Override void or( BitSet b )
public lterator<Nominativo> iterator(){ return tabella.iterator(); }//iterator modifica this con la or-logica bit-a-bit con b (si veda l’Appendice A)
}//AgendinaAL void xor( BitSet b )
modifica this con l’or-esclusivo bit-a-bit con b (si veda l’Appendice A)
L'uso di un ArrayList come tabella consente di velocizzare il metodo cercaQ che qui sfrutta il metodo void andNotf BitSet b )
binarySearch della classe Collections. Anche il metodo aggiungi si avvale della ricerca binaria. Allorquando la pone a 0 quei bit di this che sono ad 1 in b.
ricerca indica che il nominativo è già presente, il suo aggiornamento è portato a termine con il metodo set. Per
il modo di operare di Collections.binarySearch, tuttavia, nel caso in cui il nuovo nominativo deve essere Caso di studio: Crivello di Eratostone basato su BitSet___ __
aggiunto, l’indice restituito da binarySearch non è utile ed occorre cercare da capo la posizione di inserimento. Si riporta una concretizzazione della classe CrivelloAstratto basata su un BitSet. Si nota che in questa
formulazione, un intero è presente nel crivello se il suo bit corrispondente è 1. Per ragioni di efficienza, i
Osservazioni conclusive j numeri primi vengono lasciati direttamente nel crivello. Il ciclo di ricerca del prossimo primo (minimo) nel
AgendinaMap rappresenta la soluzione più efficiente e sintetica in quanto una map è naturale al problema metodo filtra() utilizza una variabile intera i. Il crivello diventa logicamente vuoto quando i supera la radice
dell’agendina. AgendinaAL mantiene ordinata la tabella mediante insertion sort ad ogni chiamata di aggiungi quadrata di N. Il prossimo minimo è trovato non appena si identifica l’indice del prossimo bit 1. Altri dettagli
nominativo. Si è evitato deliberatamente l’uso (qui inappropriato) del Listlterator e le operazioni cerca/rimuovi dovrebbero essere auto-esplicativi.
utilizzano la ricerca binaria fornita dalla classe di utilità java.util.Collections.
package poo.eratostene;
Per semplicità nessuna classe concreta ridefinisce i metodi toString, hashCode e equals implementati dalla import java.util.*;
classe astratta. Si nota che il metodo toString della super classe garantisce uniformità di visualizzazione del public class CrivelloBitSet extends CrivelloAstratto{
contenuto della tabella, quale che sia la classe erede utilizzata. private BitSet crivello;
private final int N;
Classi storiche della piattaforma Java public CrivelloBitSet( int N ){
Sin dalle prime versioni del linguaggio, il package java.util contiene alcune classi che attualmente, alla luce del if( N<2 ) throw new RuntimeExceptionfN minore di 2’’);
collection framework, risultano obsolete. Ad es., la classe Vector<T>, aggiornata in modo da includere this.N=N;
l’implementazione di List<T>, ha funzionalità oggigiorno ottenibili con un ArrayList<T> in versione crivello=new BitSet( N+1 );
sincronizzata (si veda anche il cap. 23), la classe Stack<T> erede di Vector, ha un comportamento che può for( int i=2; i<=N; i++ )
essere agevolmente riprodotto da una linked list, la classe Hashtable<K,V> è oggi sostituibile con una crivello.set( i );
HashMap<K,V>, le Enumeration<T> sono ora rimpiazzabili con gli iteratori, etc. }
public void filtra(){
Mentre per uno studio dettagliato di tali classi più altre proprie del collection framework non discusse per int i=2, limite=(int)Math.round( Math.sg/t(N) );
brevità in questo capitolo, si rimanda alla bibliografia suggerita alla fine del capitolo, di seguito si considera la while( i<=limite ){
classe BitSet che implementa in modo efficiente un array di bit (0 o 1, si veda anche l’Appendice A), evitando il //1 fase: ricerca del prossimo minimo
ricorso ad un “più lento” ArrayList<Boolean> o il farsi carico direttamente della gestione dei singoli bit di un int if( crivello.get(i) ){
o un long (si consulti l’Appendice A). Gli oggetti BitSet si possono vedere come insiemi di interi, da 0 sino ad //2 fase: eliminazione dei multipli del minimo i
un massimo positivo. Ogni intero ammissibile coincide con un indice del bit set, e si può agevolmente int multiplo=i+i; //primo multiplo
impostare, resettare, controllare il bit corrispondente con i metodi a disposizione. while( multiplo<=N ){
crivello.clear( multiplo );
multiplo+=i;
}
}
190 191
Capitolo 9

i=(i==2) ? i+1 : i+2; //dopo 2, si va sempre di dispari in dispari Capitolo 10:


Tipi generici
}//filtra
A partire dalla versione 5, Java ha introdotto la possibilità di trattare le cosiddette classi/interfacce generiche
public lterator<lnteger> iterator(){
(simili solo in apparenza ai tempiale di C++), parametriche in uno o più tipi. In più è supportata la nozione di
return new lteratore();
metodi generici o parametrici in uno o più tipi.
}//iterator
Le classi collezioni di java.util sfruttano il meccanismo dei generici che, come si è visto per l’ADT Vector<T>,
private class Iteratore implements lterator<lnteger>{
può essere usato per programmare anche proprie classi. Un esempio di classe generica è una classe
//TODO - vedi esercizi
collection in cui il tipo degli elementi è fissato dalla dichiarazione:
}//lteratore
List<lnteger> ls=new ArrayList<lnteger>();
public static void main( String []args ){
Crivello cE=new CrivelloBitSet( 1000);
L'oggetto Is è un oggetto List i cui elementi sono di tipo Integer. La dichiarazione ha diverse conseguenze:
cE.filtra();
System.ouf.println(cE);
• Il compilatore individua come errori tutti i tentativi di inserire in Is oggetti di altre classi (es. String)
}//main
}//CrivelloBitSet • Il compilatore conosce in anticipo che ogni oggetto ottenuto da Is (anche attraverso un iteratore) è di tipo
Integer e dunque solleva dalla necessità di effettuare casting:
Esercizi
Integer i=ls.get(0);
1. Data un oggetto / es. di tipo ArrayList<lnteger>, dire in che modo si può “disambiguare" l'operazione
invece di:
/.remove(5) che può intendersi come “rimuovi l’elemento di indice 5" o “rimuovi l’elemento 5". Quale significato
i=(lnteger)ls.get(0);
di default usa Java ?
2. Aggiungere alla classe CrivelloAstratto i metodi hashCode() e equals().
Quando sono in gioco elementi dei tipi di base, Java supporta (come è stato anticipato nel cap. 8) l ’auto-
3. Sviluppare altre classi concrete eredi di CrivelloAstratto basate su ArrayList o LinkedList (cosa è "meglio”?)
boxing e l'auto-unboxing, evitando cosi di dover in proprio provvedere a costruire, ad es., l’oggetto Integer
e su mappa.
durante un inserimento, e di avere la mediazione di un oggetto Integer durante un’estrazione di valore:
4. Riscrivere la classe AgendinaSet in modo da basarsi su un HashSet anziché su un TreeSet. L’uso di un
HashSet può velocizzare le operazioni di ricerca (implicite nei metodi contains, remove etc.) ma obbliga a
ls.add(0, new lnteger(23)); o più semplicemente:
fornire un proprio iteratore in grado di generare la sequenza ordinata dei nominativi. Si suggerisce di introdurre
ls.add(0,23); //auto-boxing di 23
in AgendinaSet una inner class Iteratore che implementa l’interfaccia lterator<Nominativo> e fornisce la
int x=ls.get(0).intValue(); o più direttamente:
richiesta struttura di iterazione. La inner class Iteratore può conseguire il suo scopo ottenendo, in fase di
x=ls.get(0); //auto-unboxing di ls.get(0)
costruzione, un oggetto TreeSet a partire dall’oggetto tabella HashSet di AgendinaSet, e realizzare le
operazioni hashNext(), next() e remove() delegandole ad un oggetto iteratore del TreeSet. Si sottolinea che al
Il meccanismo dei generici di Java è più sofisticato di quanto mostrato sinora. Programmando una classe
tempo di una remove() sull’iteratore, oltre che effettuare l’operazione sull’iteratore del TreeSet, occorre
NuovaClasse<T> si potrebbe desiderare che T non sia una classe qualsiasi, ma ad es. una erede di
propagare la rimozione anche aH'oggetto HashSet di AgendinaSet.
Razionale o che implementa un’interfaccia come Comparable o tutte e due le cose. Si parla di tipi generici
5. Scrivere una classe astratta VectorAstratto<T> che implementa l’interfaccia Vector<T> iterabile e
“legati” o bounded.
concretizza quanti più metodi è possibile. Adattare la classe ArrayVector<T> già realizzata come erede di
VectorAstratto<T>. Sviluppare una nuova classe LinkedVector<T> erede di VectorAstratto<T> che
public class NuovaClassecT extends Razionale>{)
implementa il concetto di vettore mediante una LinkedList<T> di java.util.
public class NuovaClassecT extends Comparable<T»{)
6. Scrivere i dettagli della inner class Iteratore nella classe CrivelloBitSet.
public class NuovaClassecT extends Razionale & ComparablecT»()
Altre letture
Attenzione che si usa sempre extends (sia per l’ereditarietà che per l’implementazione di interfaccia) e che il
Le classi del collection framework possono essere approfondite oltre che sulle API della libreria di Java, ad es. carattere & combina i bound multipli (non si può usare la virgola).
nel testo:
Definizione: Una dichiarazione di classe o interfaccia generica (es. VectorcT>, ArrayVectorcT>) in uno o più
Cay S. Horstmann, Gary Cornell, Core Java, Volume 1-Fundamentals, 8th Edition. Prentice-Hall, 2008. tipi si dice tipo generico.

Un tipo parametro di un tipo generico costituisce un parametro formale di tipo tipo.

L’uso di una dichiarazione generica costituisce un 'invocazione, in tutto simile ad una invocazione di un metodo
allorquando si passano i valori dei parametri su cui il metodo dovrà lavorare. In una invocazione di un tipo
192 193
C ap ito lo 10 Programmazione mediante tipi generici

(classe/interfaccia) generico, occorre fornire parametri tipi effettivi (o tipi argomenti). Si ottiene quindi La natura dell'errore si può comprendere osservando che una List<?> è una lista di oggetti il cui tipo è non
un’istanza del tipo generico detta tipo parametrizzato, es. ArrayVector<lnteger> dove Integer è un tipo effettivo specificato, e non è da intendersi Object o String etc.
o tipo argomento.
L'uso di un tipo generico come List<?> non consente modifiche: non è possibile aggiungere un elemento di
Generici e sotto tipi una qualsivoglia classe.
Si consideri lo spezzone di codice che segue:
Bounded wildcard
List<String> ls=new ArrayList<String>(); Si considerano classi di figure geometriche come Cerchio, Quadrato, Rettangolo, Rombo etc. Tutte queste
List<Object> lo=ls; //aliasing tra lo ed Is classi possono essere derivate da una classe astratta Figura che ad es. definisce metodi comuni come area(),
perimetro(), draw() etc. che richiedono di essere ridefiniti nelle varie sotto classi.
Mentre è perfettamente legittimo assegnare ad Is che è una List<String> un oggetto ArrayList<String>, la
seconda linea che intende stabilire un aliasing su Is con la visione di una lista di Object lo, viene segnalata Si pensi ora ad un metodo che riceve una List di figure perchè deve svolgere un compito come calcolare la
come errore dal compilatore. Perchè? In fondo l’intuito sembra suggerire che una lista di stringhe è una lista di figura di area massima, o visualizzare (draw) tutte le figure etc.
Object...
Si potrebbe approntare un metodo che abbia come parametro una List<Figura> ma allora non è possibile
L’errore nasce dal fatto che pur essendo String una sotto classe (sotto tipo) di Object, una List<String> non è passare una List<Cerchio> o una List<Quadrato> etc. Scrivere che il parametro è una List<?> non è
un sotto tipo di List<Object>. soddisfacente in quanto si rischia di accettare una lista di oggetti che non sono figure e dunque nulla hanno a
che fare con l’area e cosi via.
Se fosse lecito accettare l’aliasing proposto, tramite lo si potrebbe modificare la lista di stringhe Is con oggetti
Object che non sono String dunque corrompendo l’integrità di Is. Il compilatore previene questi errori La soluzione in questi casi è la seguente:
interpretando correttamente che List<String> non è sotto tipo di List<Object>.*S
i
void elaboraFigure( List<? extends Figura> I ){
Wildcard___________________________ for( Figura f: I ) elabora f;
Si consideri un metodo che riceve una lista di oggetti e ne fa una stampa su output. In prima approssimazione, }//elaboraFigure
per garantire generalità d’uso al metodo si potrebbe scrivere qualcosa come segue:
Si intuisce che ora il parametro è una lista di un tipo non meglio specificato ma erede di Figura (o Figura
void stampaLista( List<Object> lo ){ stessa). Diventa possibile chiamare il metodo con una lista di cerchi, o di quadrati etc. Si parla di bounded
for( Object x: lo ) wildcard perchè il tipo denotato da ? non può essere qualsiasi ma è costretto ad essere una classe erede di
System.out.println( x ); Figura. Bisogna comunque prestare attenzione che in un metodo come elaboraFigure non è possibile
}//stampaLista effettuare modifiche, es.:

Tuttavia, il metodo proposto è alquanto inflessibile perchè accetta solo una lista di Object. Passando ad es. I.add( new Rettangolo(...) ); //errore in compilazione
una List<String> si ha un errore per quanto descritto sopra. Tuttavia, in casi come questi si potrebbe
desiderare maggiore flessibilità dal meccanismo dei generici. A questo proposito è disponibile il concetto di Tutto ciò in quanto si potrebbe corrompere la tipizzazione dell’oggetto lista trasmesso.
tipo anonimo mediante il carattere ? (wildcard). Segue una nuova formulazione del metodo stampaLista:
Classi con più tipi generici
void stampaLista( List<?> lo ){ Una classe potrebbe essere generica in più di un tipo. Esempio: l'interfaccia Map di java.util è generica in due
for( Object x: lo ) tipi: Map<K,V>, quello della chiave e quello del valore. Es.
System.out.println( x );
}//stampaLista Map<String, ? extends Figura> registro=...

In questo caso, l'uso di ? dice al compilatore che il tipo degli elementi della lista è anonimo (non conosciuto) e introduce una mappa in cui la chiave è String ed il valore è un oggetto-istanza di una classe erede di Figura.
diventa ora possibile passare al metodo una lista di stringhe o di oggetti punti etc. Nel metodo, gli elementi di
lo sono visti come Object. Non solo: da una List<?> l’operazione di get() ritorna un Object etc. Ma attenzione Per convenzione, si indica con T (singola lettera maiuscola) un parametro tipo generico. Se ne occorrono due,
che frammenti di codice come quello che segue sono subdoli e il compilatore li marca come errore: si può usare una lettera vicina a T come S. Se una classe C è generica in due tipi si scrive (come per Map):
class C<T,S>. Sulle collezioni spesso (si consulti la libreria di Java) il tipo degli elementi è indicato con E, una
List<?> lo=new ArrayList<String>(); chiave con K etc.
lo.add( new ObjectQ ); //errore in compilazione
lo.add( ‘‘bisaccia" ); //errore in compilazione Metodi generici _____
Possono essere parte di classi generiche ma anche di normali classi non generiche. Rappresentano metodi in
cui esiste una dipendenza tra i tipi dei parametri e/o il tipo del risultato da uno o più tipi formali.
194 195
Capitolo 10 Programmazione mediante tipi generici

classe di T o T stesso), la notazione <? super T> indica un lower bound sul tipo sconosciuto, che deve essere
Un metodo generico rappresenta una scrittura per un insieme di situazioni diverse. Anche in questo caso si un super tipo di T (o T stesso).
parla di invocazione per riferirsi al momento in cui il metodo è attualizzato nei tipi generici. A differenza delle
classi generiche, l’invocazione può in molti casi omettere l’esplicitazione dei tipi parametri attuali tra parentesi Type erasure
< e >. Il compilatore, infatti, è in grado di inferire, dalla chiamata del metodo, i tipi attuali che si sostituiscono a Una differenza sostanziale tra il meccanismo dei generici di Java e i template di C++ consiste nel fatto che i
quelli formali. Il programmatore, tuttavia, può anche specificare i tipo attuali con la notazione: generici esistono per il compilatore ma non per la Java Virtual Machine (JVM), ossia a run time. Per la JVM,
Classe/Oggetto.<tipiattuali>metodo(parametri). tutti gli usi di una classe generica corrispondono ad una e una sola classe non generica in cui tutte le
occorrenze dei tipi generici sono sostituite con tipi pre-esistenti (es. Object). Es. nel codice:
Il metodo generico Collections.max:
La classe di utilità java.util.ColTections rende disponibile un metodo che ritorna il massimo in una collezione di List<String> I1=new ArrayList<String>();
oggetti ricevuta parametricamente. La versione non generica (raw) del metodo è: List<lnteger> I2=new ArrayList<lnteger>();

public static Object max( Collection c ){} qual è il tipo dinamico dì 11 ed I2 ? L’istruzione:

Nelle nuove API il metodo è generico nel tipo T delle componenti della collezione ricevuta. Una prima System.out.println( M.getClass()==l2.getClass() );
formulazione è:
scrive true; in alternativa si potrebbe usare instanceof.
public static <T> T max( Collection<T> c ){}
In realtà, a run time, entrambi gli usi ArrayList<String> e ArrayList<lnteger> fanno riferimento ad ArrayList.
Si noti lo sforzo di esprimere che max è generico nel tipo T utilizzato sia come tipo delle componenti della Dunque, il compilatore risolve le invocazioni parametrizzate eliminando i generici. Come indicazione di
collezione sia come tipo del risultato. Tuttavia la formulazione è ancora imprecisa. Ad esempio, il metodo massima, il tipo T generico è spesso sostituito, come ci si aspetta, con Object. Se si hanno dichiarazioni
presuppone che sugli oggetti di tipo T debba essere effettuato il confronto, dunque che risulti implementata bounded, il primo bound viene usato ai fini della type erasure.
l’interfaccia Comparable. Un miglioramento è mostrato di seguito unitamente ad un’indicazione del relativo
body: Conseguenza della type erasure: il test di tipo con tipi generici non ha senso:

public static <T extends Comparable<T» T max( Collection<T> c ){ if( x instanceof List<String> )... va riscritto come: if( x instanceof List )...
T m=null;
for( T t: c ){ Altra conseguenza della type erasure: il compilatore aumenta i controlli di tipo, ma la generazione di codice,
if( m==null II t.compareTo(m)>0 ) m=t; rimuovendo i tipi generici, introduce (trasparentemente) i casting della programmazione con tipi “grezzi".
}
return m; In una dichiarazione di tipo generico come quella che segue:
}//max
<T extends Comparable & Serializable>
Un esempio d’uso potrebbe essere:
T è rimpiazzato a run time da Comparable. Anche se questo fatto può essere perfettamente legittimo, occorre
List<String> ls=new ArrayList<String>(); considerare che allorquando si utilizza una definizione generica (di una classe/interfaccia o di un metodo)
String sm=Collections.max( Is ); //il compilatore inferisce che T è String presente nelle API di Java, cura è stata posta affinchè la type erasure consenta di riottenere esattamente la
definizione della entità in versione non generica disponibile nelle precedenti versioni della libreria.
Scrittura esplicita equivalente: String sm=Collections.<String>max( Is );
Le considerazioni di cui sopra permettono di capire che una definizione “più accurata" del metodo max di
In realtà l’ultima formulazione del metodo generico max è ancora, seppur non necessariamente, troppo Collections è quella che segue:
restrittiva. Infatti, si dice che il tipo generico T deve corrispondere ad una classe che implementa Comparable
tra oggetti di tipo T. A ben riflettere, potrebbe sussistere una gerarchia di classi come A<-B<-C per cui la public static <T extends Object & Comparable<? super T » T max( Collection<T> c ) {...}
collezione è di oggetti C ma il confronto è stabilito da A. Anche in una circostanza come questa, il metodo
dovrebbe essere invocabile correttamente passando una collezione di C. Tutto ciò si esprime scrivendo: in cui l'aggiunta di Object come primo bound tiene conto appunto della type erasure. Questo vincolo può non
sussistere nel progetto di classi definite dal programmatore.
public static <T extends Comparable<? super T » T max( Collection<T> c ) {...}
Restrizioni
La notazione <? super T> significa un tipo (sconosciuto) che è un super tipo (super classe) di T. Mentre la Il meccanismo dei generici di Java impone che non si possano costruire array il cui tipo degli elementi sia
scrittura <? extends T> introduce un upper bound sul tipo richiamato dal wildcard (che deve essere una sotto generico. Come conseguenza, dovendo scrivere ad es. un metodo che riceve una collection di oggetti di tipo T
e restituisce il minimo ed il massimo della collection:
196 197
Capitolo 10 Programmazione mediante tipi generici

public <T extends Object & Comparable<? super T> TQ minMax( Collection<T> c ){...} Si mostra ora un'altra versione del metodo minMax in cui il parametro c è un array generico nel tipo degli
elementi:
occorre stare attenti in quanto all'interno di minMax non c’è modo di creare un array di due elementi del tipo
generico T. La costruzione T[] a=(T[]) new Objectfn] (utilizzata ad es. in ArrayVector<T> nel cap.8) non crea in Un altro metodo generico minMax:____________________________________________________________
realtà un array di T, ma un array di Object. Il casting causa comunque un warning di unchecked conversion. public static <T extends Object & Comparable<? super T»Pair<T> minMax( T[] c ){
Sino a che le esigenze sono solo di memorizzazione/prelevamento nel/dal array di oggetti T all’interno della T min=c[0], max=c[0];
stessa classe (ArrayVector), non ci sono problemi. Le difficoltà sorgono nei casi come minMax che deve for( T t: c ){
restituire esattamente un array di T. Si può approntare una classe generica, esempio Pair<T>, in cui collocare if( t.compareTo(min)<0 ) min=t;
due elementi di tipo T e formulare il metodo in modo che restituisca non T[] ma Pair<T>: else if( t.compareTo(max)>0 ) max=t;
}
public <T extends Object & Comparable<? super T»Pair<T> minMax( Collection<T> c ){...} Pair<T> pair=new Pair<T>(min,max):
return pair:
È utile riepilogare, a questo punto, le restrizioni introdotte dai tipi generici: }//minMax

• Non si possono costruire (operatore new) oggetti di un parametro tipo T. Segue un esempio d’uso dei metodi minMax:
• Non si possono creare array di elementi di un parametro tipo T.
• Non si possono usare parametri tipi generici aH’intemo di metodi static (per l’assenza del legame con this) List<String> ls=new ArrayList<String>();
o blocchi di inizializzazione static in una classe generica. Come conseguenza ls.add(”una”); ls.add("due’’); ls.add("tre"); ls.add("quattro"); ls.add(”zeta"); ls.add("bari'');
• Non si possono creare classi singleton generiche (si veda il pattern singleton nel cap. 8). Attenzione che System.out.println( Is );
questa restrizione non impedisce di scrivere metodi generici statici come max{) visto in precedenza. Pair<String> p=minMax( Is ); //qui il compilatore inferisce che T è String
• Le restrizioni di cui sopra sono “ordinarief: esse possono essere aggirate mediante ricorso alla reflection System.out.println( p );
(introspezione), ossia ricorrendo alle classi e ai metodi del package java.lang.reflec.
Stringo as={"una","due","tre","quattro","zeta","bari"};
Una classe Pair<T>: p=minMax( as ); //basato su array
class Pair<T> { System.out.println( p );
private T primo;
private T secondo; Eliminazione di parametri tipi _____
public Pair( T primo, T secondo ){ Spesso è possibile (si consulti anche la libreria di Java) convertire l’intestazione di un metodo generico
this.primo=primo; this.secondo=secondo; eliminando un parametro tipo mediante l’uso del wildcard r>.. Esempio:
}
public T getPrimo(){ return primo;} public static <T> int ricercaLineare( Vector<T> v, int x ){
public T getSecondo(){ return secondo;} for( int i=0; i<v.size(); i++ )
public void setPrimo( T primo ) { this.primo=primo;} if( v.get(i).equals(x) ) return i;
public void setSecondo( T secondo ) { this.secondo=secondo;} return -1;
public String toString(){ return ""+primo+" ”+secondo; ) }//ricercaLineare
}//Pair
Il metodo si può riscrivere senza il parametro <T> come segue:
Un metodo generico minMax:_________________________________________________________________
public static <T extends Object & Comparable<? super T » Pair<T> minMax( Collection<T> c ){ public static int ricercaLineare( Vector<?> v, int x ){
T min=null, max=null; for( int i=0; i<v.size(); i++ )
for( T t: c ){ if( v.get(i).equals(x) ) return i;
if( min==null ) { min=t: max=t;} return -1;
else{ }//ricercaLineare
if( t.compareTo(min)<0 ) min=t;
else if( t.compareTo(max)>0 ) max=t; Regola Get/Put e wildcard
Come indicazione di carattere generale, quando una collezione generica in T è usata solo in lettura
(operazioni Gef) allora può essere utile specificare la genericità come <? extends T>. Quando la collezione
Pair<T> pair=new Pair<T>( min, max ); dev’essere solo scritta (operazioni Puf), è utile specificare la genericità come <? super T>. Quando la
return pair; collezione è usata in lettura/scrittura, può essere opportuno non utilizzare wildcard. Le operazioni Get sono già
}//minMax state commentate in esempi precedenti. Di seguito si mostra un esempio di operazioni Put.
198 199
Capitolo H) Programmazione mediante tipi generici

Sussiste, tuttavia, una differenza spiacevole di comportamento tra tipi generici ed array, che ha ripercussioni
List<? super lnteger> ln=new ArrayList<Number>(); sul tempo di individuazione di errori legati a violazioni di tipizzazione. Mentre nel caso di un tipo
List<lnteger> li=new ArrayList<lnteger>(); parametrizzato come 11 il compilatore segnala immediatamente come errore ogni tentativo di aggiungere ad
ln=li; esso un elemento:
ln.add(2);
I1.add(3), //errore a tempo di compilazione
Siccome In è una lista il cui tipo degli elementi è un super tipo di Integer, dunque può essere Integer o Number I1.add(2.3d); //errore a tempo di compilazione
0 Object, sicuramente si potrà aggiungere ad In degli interi. Non è possibile invece aggiungere un reale o un
oggetto di una qualsiasi classe, es. String. nel caso degli array, un’assegnazione di valore che viola il tipo degli elementi dell'array può essere scoperta
solo a tempo di esecuzione. Ad es., siccome l’array di oggetti ao di cui sopra, a seguito dell'assegnazione ad
Il metodo generico copy della classe di utilità Collections copia il contenuto di una lista sorgente su una lista esso di as, ha tipo degli elementi String, un’assegnazione come quella che segue:
destinazione. La lista destinazione deve avere una lunghezza almeno pari alla lunghezza della lista sorgente.
L’intestazione di tale metodo segue la regola Get/Put: ao[0]=2.3d;

public static <T> void copy( Liste? super T> destinazione, Liste? extends T> sorgente ); mentre passa indenne ai controlli del compilatore, solleva un'eccezione (unchecked) ArrayStoreException a
tempo di esecuzione.
Un esempio di invocazione è fornito di seguito:
Type erasure e metodi bridge
Liste? super lnteger> ln=Arrays.asList(20,30,60); //si usa un varag come array La type erasure comporta che in alcuni casi il compilatore deve intervenire per ripristinare il polimorfismo come
List<lnteger> li=Arrays.asList(2,6); previsto nella classe iniziale generica, introducendo alcuni metodi detti metodi bridge (ponte). Si consideri il
Collections.<lnteger>copy( In, li ); //Integer può essere omesso in quanto deducibile dal compilatore seguente frammento della classe Data:

Il contenuto di In dopo la copia è: [2,6,60]. public class Data implements Comparable<Data>{


private final int G, M, A;
Una variante del metodo sort di Collections riceve una lista da ordinare ed un oggetto comparatore.
L’intestazione del metodo è la seguente: public int compareTo( Data d ){
if( A==d.A && M==d.M && G==d.G ) return 0;
public static <T> void sort( List<T> Is, Comparato^? super T> c ); if( A<d.A II A==d.A && M<d.M II A==d.A && M==d.M && G<d.G )
return -1;
La lista Is è usata in lettura/scrittura e non fa uso di wildcard. return 1;
}//compareTo
Sotto tipi e covarianza
Un tipo (classe o interfaccia) B è sotto tipo di A se B estende o implementa A. Si è detto che una List<String> }//Data
non è un sotto tipo di una List<Object>, similmente un Vector<ContoConFido> non è un sotto tipo di
Vector<ContoBancario> etc. Tuttavia, un array di String è un sotto tipo di un array di Object: A seguito della type erasure, la classe Data viene trasformata come segue:

String []as={“casa","abaco”,"dado”,"lupo”}; public class Data implements Comparable{


Object []ao=as; //qui l’assegnamento è possibile
}//Data
Si dice che i tipi generici non sono covarianti: anche se B estende A, List<B> non è un sottotipo di List<A> etc.
Nella classe cosi ottenuta sono presenti due metodi compareTo: quello introdotto dal programmatore nella
1tipi array sono, invece, covarianti: se B estende A, allora B[] è sotto tipo di A[] e si può assegnare un oggetto versione iniziale generica, cioè compareTo( Data ), e quello dovuto al polimorfismo introdotto da “implements
B[] ad un oggetto A[], Comparable", cioè compareTo( Object ). Al livello di JVM la classe affetta da type erasure fa riferimento al
metodo compareTo( Object ). Per far funzionare correttamente la classe trasformata per la JVM, il compilatore
Si può ritrovare la covarianza tra tipi generici usando il wildcard ? come segue: introduce tacitamente un metodo bridge come segue:

Liste? extends Number> I1=new ArrayList<Number>(); public int compareTo( Object o )( return this.compareTo( (Data)o );}
List<lnteger> I2=new ArrayList<lnteger>();
11=12; //ok che delega il compito di stabilire il confronto al metodo compareTo( Data ) previsto dal programmatore.

Come altro esempio si considerino le classi che seguono:


200 201
Capitolo N) Programmazione mediante tipi generici

abstract class A<T>{ Come si vede, il metodo clone di C ritorna un oggetto di classe C (covarianza del tipo di ritorno), ma rimane
abstract T m ( T a ) ; pur sempre una ridefinizione di clone di Object. Il compilatore introduce un metodo bridge Object clone() che
}//A richiama il clone() specificato dal programmatore. Segue un esempio d’uso:

class B extends A<String>{ C o=new C();


@Override o.setX( 10 );
String m( String s ) { return s ;} C c=o.clone();
}//B System.out.println( “c.class="+c.getClass().getName()+" “+c );

La classe B estende il tipo parametrizzato A<String>. La dichiarazione del metodo m in B è effettivamente il messaggio o.clone() invoca logicamente il metodo clone() di Object che per dynamic bmding richiama il
(come il compilatore conferma) una ridefinizione del metodo m di A, basata sulla genericità. A seguito della metodo bridge della classe C che quindi fa ritornare una copia dell’oggetto this. Si stampa:
type erasure, il metodo astratto di A diventa: Object m( Object a ). Similmente, dopo l'erasure, scompare il tipo
parametro String in A<String>, ma resta il metodo String m( String s ) nella classe B, che a rigore non c.class=poo.bridge.C x=10.
costituisce più una ridefinizione del metodo m di A. Anche in questo caso il compilatore ripristina
correttamente il polimorfismo introducendo nella classe B un metodo bridge come segue: Wildcard capture
È stato già puntualizzato che, ad es., nel progetto di metodi di servizio spesso è possibile eliminare un tipo
Object m( Object o ) { this.m( (String)o );} generico sostituendolo con il wildcard ?. Un altro esempio è il metodo reverse di Collections che riceve una
lista e ne inverte il contenuto:
È facile prevedere che l’istruzione di stampa che segue:
public static void reverse( List<?> lis )
A a=new B();
System.out.println( a.m( new ObjectQ ) ); Il problema è che su lis, a causa del wildcard, si possono effettuare letture ma non scritture. In casi come
questi, può essere utile la tecnica detta “wildcard capture" ossia la cattura del wildcard con un nome di tipo
solleva una ClassCastException in quanto il metodo bridge richiama il metodo String m( String s ) di B con un generico, utilizzando la mediazione di un metodo privato ausiliario:
oggetto Object che non può essere castizzato a String. L’istruzione che segue, invece, non genera
ClassCastException: public static void reverse( List<?> lis )(
reverseHelper( lis ); //usa la wildcard capture
System.out.println( a.m( “casa” ) ); }//reverse

Si osserva ancora che i metodi bridge possono anche essere scorrelati dalla genericità. Si consideri il metodo private static <T> void reverseHelper( List<T> lista ) { //qui il wildcard è catturato come T
int i=0, j=lista.size()-1;
protected Object clone() throws CloneNotSupportedException; //eccezione checked while( i<j ){
T park=lista.get(i); lista.set(i, lista.get(j));
della classe Object, utilizzabile per ottenere un clone di un oggetto x, attraverso una copia byte-per-byte lista.set(j, park);
(“copia superficiale") dell’oggetto x. La copia superficiale copia perfettamente i campi dei tipi primitivi, ma i++; j--;
introduce alias dei campi oggetto, limitandosi a copiare i riferimenti. L’oggetto clone riceve lo "stesso” stato }
dell'oggetto originale x ma ha dinamica dipendente da quella di x. È evidente che una classe applicativa }//reverseHelper
potrebbe avere interesse a ridefinire il metodo clone in modo da perfezionare la copia come “copia profonda",
ossia clonando i campi oggetto e cosi via ricorsivamente. Una classe che voglia ridefinire il metodo clone deve Gli aspetti implementativi propri di reverseHelper non influenzano l’intestazione pubblica del servizio reverse
implementare l’interfaccia tag (sprovvista cioè di metodi) Cloneable come segue: che può quindi rimanere compatta come indicato sopra.

class C implements Cloneable{ Tipi generici e codice legacy


private int x;
public void setX( int x ) { this.x=x;} Data l’enorme quantità di codice Java costruito prima deH'introduzione dei generici (apparsi nella versione 1.5
public String toString(){ return “x=’’+ x;} del linguaggio), è probabile che nuovo codice basato sui tipi generici debba utilizzare (/ritemperare con) codice
@Override legacy o già esistente. In linea di principio, tale interoperabilità non crea di norma particolari problemi per cui è
public C clone() throws CloneNotSupportedException! sempre raccomandabile sviluppare nuovo codice Java in modo da avvalersi, dove possibile, dei tipi generici.
C c=(C)super.clone(); //sempre richiesto
//perfezionamenti di c secondo la loqica di copia profonda Interagendo con “tipi grezzi" più che altro si tratta di comprendere esattamente la natura degli "unchecked
return c; warning” che inevitabilmente si generano in queste situazioni. Per dare un’idea delle problematiche, di seguito
}//clone si considera una classe Legacy che gestisce un oggetto Vector (una classe storica di Java, il cui uso risulta un
}//C
202 203
Capitolo 10 Programmazione mediante tipi generici

pò più pesante di ArrayList, si veda il cap. 23) supposto contenente stringhe. Attualmente Vector è un tipo
generico. Esercizi
1. Definire un'interfaccia lnsieme<T> generica nel tipo T degli elementi ed iterabile, corrispondente al concetto
class Legacy{ di insieme matematico di oggetti (non sono ammesse le repliche, né è importante l'ordine). Insieme deve
private Vector v; esporre (almeno) i seguenti metodi:
public void setVector( Vector v ){ boolean eVuotof)
this.v=new VectorQ; boolean appartiene(T elem)
Enumeration en=v.elements(); boolean aggiungi! T elem )
while( en.hasMoreElements() ) aggiunge elem all’insieme, ritorna true se la struttura cambia a seguito dell'aggiunta, false altrimenti
this.v.addElement( en.nextElement() ); boolean rimuovi( T elem )
}//setVector lnsieme<T> unione( lnsieme<T> altro )
costruisce e ritorna l’insieme unione tra this e altro
public Vector getVector(){ return new Vector( v ); }//getVector lnsieme<T> intersezione( lnsieme<T> altro )
public String toString(){ return v.toString(); }//toString costruisce e ritorna l’insieme intersezione tra this e altro
}//Legacy lnsieme<T> differenza! lnsieme<T> altro )
costruisce e ritorna l’insieme differenza “this-altro’’, costituito cioè dagli elementi appartenenti a this ma non
Ovviamente la variabile di istanza v di Legacy è un vettore di Object. Le istruzioni che seguono usano Legacy: ad altro
lnsieme<T> differenzaSimmetrica( lnsieme<T> altro )
Legacy legacy=new LegacyQ; costruisce e ritorna l’insieme differenza simmetrica tra this e altro, ossia “this-altro” unito a “altro-this”.
Vector<String> vs=new Vector<String>( Arrays.as/./'st(,,uno",,,due',,"tre") ); Progettare quindi una classe astratta lnsiemeAstratto<T> che implementa lnsieme<T> e concretizza quanti
legacy.setVector( vs );// (1 ) più metodi è possibile.
2. Progettare una classe lnsiemeVector<T> che estende lnsiemeAstratto<T> e utilizza la classe
vs=legacy.getVector(); // (2) poo.util.Vector<T> per concretizzare il tipo astratto Insieme.
System.ouf.println( legacy ); 3. Scrivere una classe lnsiemeAL<T> che estende lnsiemeAstratto<T> ed utilizza un ArrayList di java.util per
concretizzare l’Insieme.
Nel punto (1) si passa un Vector<String> ad un Vector. Anche se la classe Legacy si aspetta un Vector di 4. Scrivere una classe lnsiemeLL<T> che estende lnsiemeAstratto<T> ed utilizza una LinkedList di java.util
stringhe, i rischi di passare un vector con altri tipi di oggetti al suo interno esistono. Tuttavia, a ben riflettere, per concretizzare l’Insieme.
tali rischi non sono superiori a quelli che sussistono, in un codice non generico, allorquando si trasmette al 5. Scrivere una classe lnsiemeSet<T> che estende lnsiemeAstratto<T> ed utilizza un HashSet di java.util per
metodo setVector un oggetto “raw” di classe Vector. concretizzare l'insieme. L’implementazione dovrebbe ridefinire i metodi della classe astratta che possono
essere resi più efficienti dall’uso di un hash set.
Nel punto (2) si richiede una copia del contenuto del vector interno a legacy. Il compilatore segnala in questa 6. Scrivere una classe lnsiemeMap<T> che estende lnsiemeAstratto<T> ed utilizza una HashMap di java.util
operazione un ovvio unchecked warning, in quanto si pretende di passare dal tipo Vector (quello restituito da per concretizzare l'insieme.
getVector) ad un Vector<String>. Dopo tutto, la classe Legacy potrebbe aver ridefinito il contenuto del vector 7. Scrivere una classe lnsiemeHash<T> che estende lnsiemeAstratto<T> e utilizza un array di capacità
amministrato e magari esso non contiene più solo oggetti String. Ma se le informazioni a disposizione del costante comunicata da un parametro ricevuto a tempo di costruzione, gestito come “tabella hash”. Si ricorda
programmatore concernenti la classe Legacy confermano che essa non altera la tipizzazione degli elementi che in una tabella hash la posizione di inserimento (e quindi di ricerca) di un elemento dipende dallo
del vector, allora l’unchecked warning è innocuo ed inutile. In casi come questi è lecito sopprimere hashCode() dell’elemento (si considera il valore assoluto dello hash code e si prende il resto della divisione
l’unchecked warning con l'annotazione @SuppressWarnings(“unchecked”) da collocare prima della testata del intera tra questo valore e la capacità della tabella hash (normalmente espressa da un numero primo per
metodo che contiene l’istruzione (2): diminuire il rischio di collisioni), per definire la posizione nell’array “designata” per l’elemento). Utilizzare come
liste bucket (per gestire le collisioni) delle linked list di java.util.
@SuppressWamings(“unchecked") 8. Il metodo sum di java.util.Collections riceve una collection di numeri, ne calcola la somma e la restituisce.
public static void main( Stringi] args ){ Specificare, alla luce della regola Get/Put, l'intestazione e mostrare una possibile stesura del corpo del
Legacy legacy=new LegacyO; metodo.
Vector<String> vs=new Vector<String>( Arrays.as/./s/CunoV'dueV'tre") ); 9. Realizzare un metodo di servizio che riceve una collezione cn di numeri ed un numero intero n, e aggiunge
legacy.setVector( vs ); alla collezione i primi n numeri interi, partendo da 0.
10. Fornire una concretizzazione del metodo Collections.copy. Con riferimento alle istruzioni:
vs=legacy.getVector();
System.ou/.println( legacy ); List<Object> lob=Arrays.<Object>asList( 10, 20.3, "tre'' );
}//main List<lnteger> lint=Arrays.<lnteger>asList( 2, 3 );
Collections.copy( lob, lint );
System.out.println( lob );

204 205
Capitolo 10

dire cosa viene stampato. Verificare quindi, alla luce della regola Get/Put, l’equivalenza delle seguenti Capitolo 11:______________________________________________________________________
istruzioni di invocazione del metodo copy e la scelta di default del compilatore: Ingresso/uscita grafico
Collections.copy( lob, lint ); In alternativa all’uso di un oggetto Scanner e di System.out.println(), è possibile richiedere, con pochissima
Collections.<lnteger>copy( lob,lint ); fatica, un’operazione di lettura mediante un input dialog, e un'operazione di scrittura mediante un show
Collections.<Number>copy( lob, lint ); message dialog.
Collections.<Object>copy( lob, lint );
Utile è la classe JOptionPane del package javax.Swing, e i due metodi di servizio (static) showlnputDialog() e
11. Rendere clonabile la classe Triangolo di cui al cap. 3, che memorizza i tre vertici in tre variabili di istanza showMessageDialog(). Segue un esempio d’uso relativo all'acquisizione di un intero:
Punto. Prestare attenzione che un’operazione di clone di un oggetto Triangolo deve realizzare una copia
profonda. Testare l'implementazione ottenuta. import javax.Swing.*;
12. Scrivere due versioni di un metodo generico bubbleSort nella classe di utilità poo.util.Array che riceve un
vettore di oggetti e la ordina. In una prima versione il metodo riceve un oggetto poo.util.Vector il cui tipo degli int x;
elementi deve ammettere il confronto. In un altro caso il metodo riceve un oggetto poo.util.Vector di un tipo for(;;){
generico ed un oggetto Comparator parametrico da utilizzare per le operazioni di confronto. String input=JOptionPane.showlnputDialog("Fornire il valore intero di x”);
try{
Altre letture x=lnteger.parselnt( input );
Le problematiche sui tipi generici di Java 5 e versioni superiori possono essere approfondite, ad es., su: break;
}catch( RuntimeException e ){
C.S. Horstmann, G. Cornell, Core Java, Voi. I-Fundamentals, 8,hEdition, Prentice Hall, 2008. JOptionPane.showMessageDialog( nuli, “Nessun intero. Ripetere..." );

G. Bracha, Generics in thè Java Programming Language, 2004, http://java.sun.eom/j2se/1.5/pdf/generics-


tutorial.pdf
Il primo argomento nuli di showMessageDialog indica che non c’è il parent di questo message dialog, ossia
A. Langer, Java generics, http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html una finestra madre cui questo dialog è legato.

Input dialog

Messaggio sé ^ T
1
i

( 1 ) Nessun intero. Ripetere...

OK

Message dialog
206 207
Capitolo 11 Ingresso/uscita grafico

Se la lettura dell'intero x non va a buon fine: Le intenzioni dell’utente si possono controllare mediante il valore intero restituito da showConfirmDialog().
• perchè viene digitata una stringa non riconducibile ad un int Esistono al riguardo le seguenti costanti simboliche static:
• perchè l’utente preme Annulla per annullare l’operazione
• perchè l’utente preme OK senza aver digitato nulla nel campo di testo YES_OPTION
NO OPTION
si solleva una eccezione unchecked (che è NumberFormatException se fallisce la conversione ad int) che CANCEL_OPTION
viene catturata dopo di che si chiede all'utente di ridare l’input. OK_OPTION valore restituito quando sul dialog si clicca su Ok
CLOSED_OPTION valore restituito quando si chiude il dialog senza scelta
Nel message dialog occorre confermare con Ok per ritornare a visualizzare l’input dialog.
Selezione di un file e classe JFileChooser
Il metodo showMessageDialog(parent.string) ha due parametri: il primo denota il componente grafico parent Spesso un programma deve acquisire il nome esterno di un file, completo di pathname. Si potrebbe benissimo
(qui non esistente per cui è posto a nuli), il secondo è la stringa che si desidera visualizzare nel message utilizzare un input dialog e digitare path+nomefile es. C:\poo-file\file1.dat.
dialog box.
In alternativa si può ricorrere ad un oggetto di classe JFileChooser che consente di navigare sul file System e
Per visualizzare il dato letto si esegue: JOptionPane.showMessageDialog(null,“x=‘+x); selezionare il nome del file (ordinario o directory) con pochi click di mouse. JFileChooser è comodo in quanto
permette di risalire al pathname assoluto del file. Inoltre consente di specificare il tipo del file (es. pdf, doc etc.)
in modo da restringere la scelta tra file dello stesso tipo.

import javax.Swing.filechooser.*;

String nomeFile=null, pathNameCompleto=null;


JFileChooser jfc=new JFileChooser();
int vai = jfc.showOpenDialog(null); //nuli per il parent
if( vai == JFileChooser.APPROVE_OPTION ) {
pathNameCompleto = jfc.getSelectedFile().getAbsolutePath();
JOptionPane.showMessageDialog(null,“Hai scelto il file: “+pathNameCompleto);

else if( vai == JFileChooser.CANCEL_OPTION ){


Show confirm dialog JOptionPane.showMessageDialog(null,"Hai annullato la scelta del file");
Uno show confirm dialog consente di fare una scelta tra Si, No e Annulla come esemplificato di seguito:

■ I■
selezionare una opzione *----------- 9

n B locchi appunti di O n e M o t e d Fax f i Le m ie C o n v e rs a c i


? Sei disposto a selezionare un file? dCAPCOM r i File ricevuti d M ATLAB
f i C hristian l~1 HP P h o to s m a rt P ro je c ts f i My G am es
f~1 C onvertX To D V D f i I.E. D o w n lo ad s C lO v i
Sì No Annulla n C n te n o n G a m e s f i In g e g n e n a d P ro g e tto D ata Mmi|
n C y b e rlin k f i J a v a W o rk s p a c e s d Prolog
— -- ---------------- -l
l~ 1 EA G a m e s d K ONAM I d S cann ed D ocum en
int i=-1; < ___________________________ J____ M
do{
N om e file:
i=JOptionPane.showConfirmDialog( nuli,"Sei disposto a selezionare un file?" );
jf( i==JOptionPane.NO OPTION ) System.exit(-I); lip o file: Tutti i file
if( i!=JOptionPane.YES_OPTION )
JOptionPane.showMessageDialog(null,”Devi rispondere SI o NO ...”); Apri A nnulla
}while(i!=JOptionPane.YES_OPTION );

208 209
Capitolo 11 Ingresso/uscita grafico

La finestra di dialogo di figura appare come conseguenza delle istruzioni:


Esempio ______________
JFileChooser jfc=new JFileChooser(); Lo screen shot che segue illustra un file chooser guidato a mostrare i file PDF e TXT a partire dalla directory
int vai = jfc.showOpenDialog(null); c:\poo-file:

|fc=new JFileChooser("c:\\poo-file“);
Il costruttore di default di JFileChooser crea il file chooser "puntato" alla directory di default. Un altro FileNameExtensionFilter filtro =
costruttore permette di inizializzare il file chooser su una specifica directory del file System. Ad es. new FileNameExtensionFilter("Documenti PDF o TXT", "pdf", "txt");
jfc.setFileFilter(filtro);
JFileChooser jfc=new JFileChooser(“c:\\poo-file”); vai = jfc.showOpenDialog(null);

apre la navigazione a partire da c:\poo-file. In alternativa si potrebbe specificare un oggetto File anziché una
String per il path alla directory. Il dialog che abilita la navigazione sul file System è showOpenDialogO che ha
come unico parametro il parent (qui è nuli).

La scelta dell’utente sullo show open dialog è un intero assegnato alla variabile vai che può essere interrogato
per verificare l'approve o cancel option come mostrato. Il metodo getSelectedFile() dell’oggetto JFileChooser()
jfc ritorna un oggetto File (si veda più avanti nel corso) da cui si può estrarre il path name completo
(getAbsolutePath()), il nome senza pathname (getName()) etc. del file selezionato. Eventualmente si può
specificare un filtro per limitare la ricerca ai soli file di interesse come illustrato di seguito:

intj=-1;
do{
j=JOptionPane.showConfirmDialog(null,"Vuoi scegliere un file di tipo pdf o txt?“);
if( j==JOptionPane.NO OPTION ) System.exit(-1); //esempio
if( j!= JOptionPane.YES_OPTION )
JOptionPane.showMessageDialog(null,"Devi rispondere SI o NO ...");
}while(j!=JOptionPane.YES OPTION);

jfc=new JFileChooser("c:\\poo-file“);
FileNameExtensionFilter filtro =
new FileNameExtensionFilterfDocumenti PDF o TXT", "pdf", "txt"); Esercizi __ ______
jfc.setFileFilter(filtro); 1. Leggere un intero positivo, verificare se esso è perfetto e scrivere in uscita un messaggio che dica se il
vai = jfc.showOpenDialog(null); numero è perfetto o no. Si ricorda che un numero intero positivo è perfetto se esso è uguale alla somma dei
if( vai == JFileChooser.APPROVE OPTION ) { suoi divisori propri. Ad es. 6 è perfetto in quanto 6=3+2+1. L’ingresso/uscita deve basarsi su finestre di dialogo
nomeFile=jfc.getSelectedFile().getAbsolutePath(); grafiche.
JOptionPane.showMessageDialog(null,"Hai scelto il file: ' + nomeFile); 2. Leggere un intero positivo n, quindi una matrice quadrata nxn di interi, verificare se essa costituisce un
} quadrato magico (si riveda il cap. 2) e scrivere un messaggio corrispodente. Se n non è positivo, il programma
else if( val== JFileChooser.CANCEL_OPTION ){ deve visualizzare un messaggio di errore. L'ingresso/uscita deve basarsi su finestre di dialogo grafiche.
JOptionPane.showMessageDialog(null,”Hai annullato la scelta del file"); 3. Modificare il programma di Gauss (si veda il cap. 7) in modo che le operazioni di ingresso/uscita avvengano
} con la mediazione di finestre di dialogo grafiche.

Si nota che per default, il file chooser consente di navigare su tutte le specie di file (file ordinari e directory). Se
l’interesse è verso una specie particolare si utilizza il metodo

void setFileSelectionMode( int modo )

dove modo può essere fissato tramite le costanti statiche e pubbliche della classe JFileChooser:
FILES_AND_DIRECTORY o FILES_ONLY.Ad es. volendo restringere la navigazione ai soli file ordinari:

jfc.setFileSelectionMode( JFileChooser.FILES^ONLY );
210 211
I

Capitolo 12:_____________________________
Flussi e file

Dicesi flusso (o stream) una successione di dati prelevati da una certa sorgente o forniti a una certa
destinazione. La sorgente può essere: un file, la tastiera, una connessione di rete etc. La destinazione può
essere un file, il video, una connessione di rete etc.

Il package java.io consente di lavorare in modo uniforme con i flussi di ingresso/uscita indipendentemente
dalla loro sorgente/destinazione.

Lo zoo delle classi di java.io è molto ricco (più di sessanta classi). Tuttavia è possibile imparare rapidamente a
gestire i flussi più comuni attraverso esempi.

I flussi possono essere binari (o non interpretati a priori), tipati, testuali, ad oggetti.

Classi base per i flussi binari _______


InputStream (classe astratta)
abstract int read() throws lOException
int read(byte[] b) - si basa su read()
int read(bytefl b, int off, int len) - si basa su read()
int available()
void close()

OutputStream (classe astratta)


abstract void write( int b ) throws lOException
void write(byteQ b) - si basa su write( un byte )
void write(byte[] b, int off, int len) - si basa su write(...)
void flushj)
void closeQ

Entrambe le classi sono astratte e si basano su due metodi fondamentali (astratti) read() per InputStream,
write() per OutputStream. Spetta alle classi eredi concretizzare i metodi astratti. Altri metodi concreti delle
classi base sono invece realizzati in termini dei metodi astratti.

L'operazione read() è bloccante per il thread che la esegue, se nessun byte è disponibile al momento sullo
stream in lettura. read() ritorna un intero tra 0 e 255. Se il flusso è terminato, la read() ritorna -1. Per
distinguere questo valore da tutti i "normali” valori di un byte, il tipo di ritorno di read() è appunto int e non byte.

L'operazione write() riceve come parametro un int in quanto, in generale, un’espressione che coinvolge byte è
comunque di tipo int. Del risultato vengono presi unicamente gli 8 bit (byte) meno significativi, mentre i restanti
24 sono ignorati.

Esempi di classi eredi concrete:_______________________________________________________________


FilelnputStream
FileOutputStream

213
Capitolo 12 Flussi e file

Tali classi consentono rispettivamente di lavorare in ingresso e uscita su file al “livello di byte”. La visione a static byte crittografa( int d, int chiave ) { return (byte)(d+chiave );}
byte è la più bassa possibile. Dopo tutto, un qualunque file è sempre costituito da una successione di byte }//Crittografia
(visione non interpretata del loro contenuto).
Il programma riceve a riga di comando un intero, atteso in valore assoluto >=3, da utilizzare come chiave.
Copia di ungile __ L'operazione di crittografia consiste nel generare una copia del file sorgente, in cui ad ogni carattere si somma
import java.io.*; la chiave. Per decrittare un file crittografato, si utilizza lo stesso programma con la chiave negativa.
public class Copia{
public static void main( String []args ) throws lOException { Flussi bufferizzati
InputStream source=new FilelnputStreamff 1.dat"); Le applicazioni in generale possono richiedere che le operazioni di scrittura/lettura su/da file siano mediate da
OutputStream dest=new FileOutputStreamff2.dat"); un buffer in modo tale che le interazioni col disco avvengano a livello di blocco di byte e non di singoli dati.
int dato; //notare la dichiarazione
for(;;){ L'utilizzo di un buffer fa sì che scrivendo, ad es., un byte esso venga copiato sul buffer e non immediatamente
dato=source.read(); sul file. Il contenuto del buffer verrà riversato sul file quando il buffer è pieno o quando si richiede un flush() sul
if( dato==-1 ) break; //end of file di f1 flusso. La chiusura di un flusso comporta automaticamente il flushing del contenuto residuo del buffer.
dest.write( dato ); Considerazioni simili si possono ripetere per le operazioni di lettura: il prossimo dato verrà prelevato dal buffer,
}//for se questo è non vuoto. Quando vuoto, il buffer viene riempito con dati provenienti dal file etc.
source.close();dest.close();
}//main Volendo lavorare con flussi bufferizzati binari, es. file, sono utili le classi BufferedlnputStream e
}//Copia BufferedOutputStream che hanno un costruttore per specificare la dimensione del buffer, diversamente (il che
va bene in molti casi) si utilizza una dimensione di default. L’uso di tali classi è esemplificato di seguito:
Il ciclo di lettura dal file source si può scrivere equivalentemente utilizzando il metodo availableQ che
restituisce il numero di byte disponibili per la lettura nello stream. InputStream in=new BufferedlnputStream( new FilelnputStream(nomefile) );

for(;;){ Le interface DataOutput e Datalnput


if( source.available()==0 ) break; Definiscono un pacchetto di metodi per l’i/o tipato.
dato=source.read();
dest.write( dato ); DataOutput (parziale): Datalnput (parziale):
}//for writeByte( int b ) byte readByte();
writelnt( int i ) int readlnt()
Si nota che la creazione dell’oggetto source equivale all’apertura del file “f1.dat” in lettura. La creazione writeShort( short s ) short readShort()
dell’oggetto dest corrisponde all’apertura in scrittura del file “f2.dat”. Dopo aver lavorato con un file, occorre writeLong( long I ) long readLong()
sempre verificarne la chiusura (metodo close()). Il metodo close() restituisce al sistema operativo sottostante writeFloat( float f ) float readFloat()
risorse allocate al tempo dell’apertura del file. writeDouble( doublé d ) doublé readDouble()
writeChar( char c ) char readChar();
Crittografia elementare (cifrario di Giulio Cesare) writeBoolean( boolean b ) boolean readBoolean()
import java.io.*; writeChars( String s ) String readUTF()
public class Crittografia{ writeUTF( String s )
public static void main( String []args ) throws lOException ( // compact Unicode Text Format
if( args.length==0 ){ System.out.printlnf'Attesa chiave”); System.exit(-1 );}
int chiave=lnteger.parselnt( args[0] ); //assunta in valore assoluto >=3 Classi di flussi tipati:
InputStream source=new FilelnputStream(“c:\\poo-file\\source.dat“);
OutputStream dest=new FileOutputStream(“c:\\poo-file\\dest.dat"); DataOutputStream
int dato; - eredita da OutputStream e implementa DataOutput.
<or(;;){
dato=source.read(); DatalnputStream
if( dato==-1 ) break; - eredita da InputStream e implementa Datalnput.
dest.write( crittografa( dato, chiave ) );
} Creazione di un file dj jnteri
source.close(); import java.io.*;
dest.close(); import java.util.*;
}//main
214 215
Capitolo 12 Flussi e file

public class Crea{ Un file aperto in lettura deve già esistere sul file System, eventualmente con un contenuto vuoto. Un file aperto
public static void main( String []args ) throws lOException { in scrittura può pre-esistere o meno, dal momento che il programma ne riscriverà completamente il contenuto.
//per semplicità non si usa la bufferizzazione Per creare da file System un file vuoto si può procedere, in una Shell dos, come segue:
DataOutputStream dos=new DataOutputStream( new FileOutputStream("c:\\poo-file\\f3.dat'') );
System.out.println("Fornisci una serie di interi uno per linea. Solo INVIO termina"); c:\poo-file>copy con: f3.dat INVIO
Scanner sc=new Scanner( System.in ); CTRL-Z INVIO
for(;;){
System.out.print("int>"); CTRL-Z INVIO specifica la fine dei dati di input.
String input=sc.nextLine();
if( input.length()==0 ) break; Una proprietà importante delle classi di java.io è la composizione dei flussi. Ad es., un DataOutputStream è
dos.writelnt( lnteger.parselnt( input ) ); stato costruito a partire da un FileOutputStream. Il risultato è che il DataOutputStream ottenuto è un file tipato
} e non più un semplice file di “byte grezzi” (raw byte). In realtà è possibile lavorare sul DataOutputStream
dos.close(); ottenuto sia al livello di byte che più ad alto livello in veste tipata, es. come file di interi.
//visualizza contenuto di f3.dat
DatalnputStream dis=new DatalnputStream( new FilelnputStream(“c:\\poo-file\\f3.dat") ); Il carattere tipato di un file non è comunque espresso in modo preciso dalle dichiarazioni, nè è importante
System.out.println(); l'eventuale estensione del nome del file.
System.out.printlnfContenuto del file");
int x=0; È sempre il programmatore che deve garantire che il file corretto è utilizzato da un programma.
tor(;;){
try{ La classe RandomAccessFile ___ ___
x=dis.readlnt(); È una classe base, ed implementa le due interfacce Datalnput e DataOutput. Per polimorfismo, laddove è
}catch( EOFException e ){ break;} atteso ad es. un DataOutputStream, si può equivalentemente passare un RandomAccessFile (raf). I file ad
System.out.println( x ); accesso diretto possono essere letti e scritti contemporaneamente.
}//for
dis.close(); Attenzione: non è possibile spostare gli elementi in un raf, ossia non è possibile inserire un nuovo elemento in
}//main un punto intermedio. Un file ad accesso diretto può essere aperto a sola lettura (“r”) o in lettura-scrittura (“rw”)
}//Crea come mostrato di seguito.

Il programma Crea legge da tastiera una successione di numeri interi terminata da una linea vuota (tappo). In un raf è disponibile l'indicizzazione dei byte componenti. Gli indici possibili sono: [0..length()-1]. Metodi
Ogni numero è quindi scritto su un file tipato di interi aperto mediante un oggetto DataOutputStream a partire propri di RandomAccessFile sono:
da un FileOutputStream. A fine creazione, il file prima è chiuso quindi ri-aperto in lettura tramite un
DatalnputStream il cui contenuto è mostrato sul video, un intero per riga di output. long getFilePointer()
ritorna la posizione della testina sul file, ossia un indice che può valere da 0 a length() (uno oltre la fine del
Si nota che mentre per un InputStream la condizione di “fine file" è intercettata quando si legge -1 (o file)
equivalentemente quando il metodo available() ritorna zero), su un file tipato si raggiunge la fine del file
quando l’ultima lettura (nel caso del programma è una readlnt()) fallisce e solleva un’eccezione long lengthQ
EOFException. Nel programma presentato, la gestione della EOFException consiste banalmente in una break ritorna il numero di byte del file
che fa uscire dal ciclo di for di lettura degli interi.
void seek( long pos )
Osservazioni pos è atteso tra 0 e length(). Pone la testina all’inizio del byte di indice pos.
Nella specificazione di una costante stringa che esprime il nome con path di un file, la barra rovesciata va
raddoppiata: ... new FilelnputStream(“c:\\poo-file\\f3.dat"). Tale raddoppio non va eseguito quando si fornisce Una volta spostata la testina su una posizione pos del file, allora se pos è all'inizio di un elemento, è possibile
es. da tastiera il nome di un file con path. Tutto ciò è legato al fatto che in un programma Java il carattere ‘V comandare una read/write tipata; se pos è alla file del file (pos==length()) è possibile comandare solo una
anticipa sequence di escape, es. ’\n’ significa carriage-return/line-feed. Per esprimere che si desidera proprio il write tipata. La testina si sposta automaticamente dopo un’operazione di read o write.
significato di ‘V occorre raddoppiarlo. Da input, invece, queste considerazioni non si applicano.
Ricerca binaria su un RandomAccessFile di interi ordinato:_________________________________________
Per rendere più flessibile la classe Crea è conveniente non usare una costante stringa ma leggere Si presenta un metodo esiste() che riceve il nome di un file tipato di interi ordinato ed un intero x, e ritorna true
preliminarmente ad es. da tastiera il nome del file. se x appartiene al file, false altrimenti. Il file tipato è aperto come RandomAccessFile a sola lettura. L’algoritmo
di ricerca binaria procede considerando gli indici al livello degli interi (come se il raf fosse un array di int).
Ottenuto un indice di intero, lo si trasforma a indice di byte moltiplicandolo per 4. Infatti, il primo intero ha
indice 0, cosi come il suo primo byte. Il secondo intero ha indice 1 e 4 è l’indice del suo primo byte etc.
216 217
Capitolo 12 Flussi e file

if( flag ){
static boolean esiste( String nome, int x ) throws IOException{ <or(;;){
RandomAccessFile f=new RandomAccessFile( nome, Y ); tmp.writelnt( y );
long inf=0, sup=(f.length()/4)-1; boolean result=false; pos=raf.getFilePointer();
for(;;){ if( pos==raf.length() ) break;
if( inf>sup ) break; y=raf.readlnt();
int med=(inf+sup)/2; }//for
f.seek( med*4 ); }//if(flag)
int elem=f.readlnt(); tmp.close(); raf.close();
jf( elem==x ) { result=true; break;} }//inserisci
if( elem>x ) sup=med-1; }//AggiornamentoSelettivo
else inf=med+1;
} Il file temporaneo tmp è stato mappato sul file fisico “tmp" del file System. Non avendo utilizzato il path
f.close(); completo per il file esterno, esso risiede nella directory di default (o directory di lavoro). In ambiente Eclipse, la
return result; directory di lavoro coincide con il progetto corrente. Pertanto, “tmp" viene creato nella directory poo-java del
}//esiste workspace in uso.

Insertion sort su un file di interi Le operazioni di “manutenzione" di sistema, ossia la rimozione del file originario e la ridenominazione del file
Sia f un file di interi ordinato per valori crescenti. Sia x un intero da aggiungere ad f rispettando l’ordine. Si temporaneo col nome del file originario, possono essere anche realizzate dall'interno del programma Java con
crea un file temporaneo tmp su cui si copiano tutti gli elementi di f minori di x, quindi si scrive x, quindi si la mediazione di oggetti di classe File (si veda più avanti per i dettagli).
copiano i restanti elementi di f.
Fluss[ testuali_________ _____ __________________ ______________________________
Il programma AggiornamentoSelettivo assume che, a fine operazione, il programmatore provveda, operando Contengono caratteri stampabili (lettere, cifre, segni di punteggiatura, spazi, ...) più le marche di fine linea.
al livello di sistema operativo, a cancellare il file originario e a ridenominare il file temporaneo con il nome del L’esatta composizione di una marca di fine linea dipende dal sistema operativo. Ad es. su Windows è la
file originario. combinazione di due caratteri di controllo: carriage-retum e line-feed. Una marca di fine linea è evocata in
uscita dalla sequenza di escape ’\n'.
import java.io.';
import java.util.*; Un flusso testuale può essere visto come un testo, ossia una successione di linee. È possibile
public class AggiornamentoSelettivo{ ispezionare/modificare un file testo con un comune editor di testo (es. notepad di Windows).
public static void main( String []args ) throws IOException{
Scanner sc=new Scanner( System.in ); Esistono delle gerarchie di classi apposite per i flussi di testo, basate rispettivamente su Reader e Writer. Due
System.out.printfnome file=”); classi concrete spesso utilizzate sono BufferedReader e PrintWriter. PrintWriter può essere combinata con la
String nome=sc.nextLine(); classe BufferedWriter per ottenere la bufferizzazione durante le operazioni di uscita. Interessante è anche la
System.out.print(“intero da aggiungere"); classe PrintStream che offre alcune semplificazioni rispetto a PrintWriter. System.out è un oggetto
int x=sc.nextlnt(); PrintStream.
inserisci nome, x );
}//main BufferedReader (lista parziale dei metodi) PrintWriter (parziale)
static void inserisci String nome, int x ) throws IOException{ String readLine() void print[ln]( String s )
RandomAccessFile raf=new RandomAccessFile( nome, “r" ); void close() void print[ln]( tipo^dibase x )
DataOutputStream tmp=new DataOutputStream( new FileOutputStreamftmp”) ); void println()
long pos=0; void flushQ
int y=0; void close()
boolean flag=false;
while( pos<raf.length() && Iflag ){ Per esemplificare l'uso di file di tipo testo, si considera la classe AgendinaAstratta (cap. 9) e i metodi
y=raf.readlnt(); salva/ripristina:
if( y>x ) flag=true;
else{ tmp.writelnt( y ); pos=raf.getFilePointer();} public void salva( String nomeFile ) throws IOException{
}//while PrintWriter pw=new PrintWriter( new FileWriter(nomeFile) );
tmp.writelnt( x ); //scrivi sicuramente x for( Nominativo n: this ) pw.println( n ); //si scrive su pw il toString di n
pw.close();
}//salva
218 219
Capitolo 12 Flussi e file

Naturalmente, o si cattura la potenziale lOException connessa con l'apertura di br con un blocco try-catch o si
All'atto pratico è utile creare il PrintWriter in versione bufferizzata come segue: completa la dichiarazione della testata del main (o del metodo in cui ci si trova) con la clausola throws
lOException.
PrintWriter pw=new PrintWriter( new BufferedWriter( new FileWriter(nomeFile) ) );
Costruttori di Scanner
public void ripristina(String nomeFile) throws IOException{ Un oggetto Scanner può essere aperto su System.in per leggere dati da tastiera. In generale sono disponibili i
ButteredReader br=new ButteredReader( new FileReader(nomeFile) ); seguenti costruttori di Scanner (lista parziale):
String linea=null;
StringTokenizer st=null; Scanner( File f )
LinkedList<Nominativo> tmp=new LinkedList<Nominativo>(); Scanner( InputStream is )
//tmp e' utile per far fronte a malformazioni del file Scanner( String s )
boolean okLettura=true;
for(;;){ Quale che sia la sorgente dei dati, i metodi di lettura sono sempre gli stessi: next(), nextLine(), etc.. L’apertura
linea=br.readLine(); di uno Scanner su una stringa (si riveda il cap. 6) è utile per la sua decomposizione in token. Se l'input stream
if( linea==null ) break; //eof di br è un file, allora le letture attingono dal file etc.
st=new StringTokenizer(linea,“
try{ PrintStream (es. System.out)
String cog=st.nextToken(); String nom=st.nextToken(); È capace di rendere un OutputStream (da cui deriva) idoneo per stamparvi dati primitivi in modo conveniente
String pre=st.nextToken(); String tel=st.nextToken(); e testuale. Non solleva lOException. Piuttosto una situazione di errore setta un flag sull’oggetto PrintStream
tmp.add( new Nominativo( cog, nom, pre, tei ) ); //aggiunge in coda che è interrogabile col metodo checkError. Costruttori esistono per creare un PrintStream con la capacità di
}catch(Exception e){ auto-flushing-, ad ogni println, o emissione di un byte-array o di una marca di fine linea ‘\n’. I caratteri
okLettura=false; break; corrispondenti alla stampa di un dato primitivo sono emessi in forma di byte, codificati secondo le convenzioni
} del sistema operativo locale utilizzato. In altre parole, qui non serve ricorrere a FileWriter. Una lista parziale dei
} metodi disponibili è riportata di seguito:
br.close();
if( okLettura ){ PrintStream ( File f )
this.svuota(); PrintStream( OutputStream out ),
for( Nominativo n: tmp ) this.aggiungi(n); PrintStream( OutputStream out, boolean autoflush ),
} PrintStream( String nomefile )
else throw new IOException();
}//ripristina void print[ln]( tipo di dati primitivo o oggetto ), //versioni overloaded
void printf( String format, Object... ),
FileReader associa ad un file di tipo testo un convertitore che trasforma caratteri ASCII generati sul file System void printf( Locale I, String format, Object... ),
locale (es. WinXP) in caratteri UNICODE richiesti da Java. void flush(), void close()

FileWriter associa ad un file di tipo testo un convertitore che trasforma caratteri UNICODE in caratteri ASCII Le usuali modalità d'uso di PrintStream sono quelle viste con riferimento a System.out.
richiesti dal file System locale (es. WinXP).
Flussi di oggetti
Quando fallisce la lettura di una linea da un buffered reader, si ritorna una stringa nuli. È possibile salvare il contenuto di una agendina anche utilizzando il concetto di flusso di oggetti e connesso
meccanismo di serializzazione. Si aggiunge alla testata della classe Nominativo che essa implementa altresì
FileReader va sostituito con InputStreamReader quando il BufferedReader è "attaccato" alla tastiera (si veda l'interfaccia Serializable (che è senza metodi). Considerato che gli oggetti interni a Nominativo (stringhe) sono
più avanti per un esempio). essi stessi già serializzabili, diventa possibile per il compilatore Java far diventare un oggetto nominativo una
sequenza di byte suscettibile di memorizzazione (persistenza) e ripristino.
Lettura di stringhe da tastieraiS
Si può evitare l'uso della classe Scanner come segue: Il nome serializzazione deriva dal fatto che ad ogni oggetto (riferimento) viene associato un numero seriale
univoco tale che se l’oggetto, per via di aliasing, dovesse essere re-incontrato durante lo stesso processo di
BufferedReader br=new BufferedReader( new lnputStreamReader( System.in ) ); serializzazione, solo il riferimento al suo numero seriale viene re-introdotto sul flusso di oggetti. In altre parole,
System.out.printfFomisci una stringa^); l'uso dei numeri seriali permette di serializzare gli oggetti una volta sola quando si processa una rete di oggetti
String linea=br.readLine(); comunque complessa.

220 221
Capitolo 12 Flussi e file

Vincolo: la classe degli oggetti serializzati (qui Nominativo) non dovrebbe cambiare tra il momento in cui si serialVersionUID
realizza la serializzazione ed il momento in cui si ripristinano gli oggetti serializzati (deserializzazione) Ovviamente è inevitabile che le classi evolvano nel tempo (es. si re-implementano alcuni metodi, si
aggiungono/eliminano campi e/o metodi etc). Tutto ciò può determinare problemi a deserializzare oggetti
Classi per flussi di oggetti:___________________________________________________________________ precedentemente salvati, cioè relativi a una versione “vecchia” della classe.
ObjectOutputStream
estende OutputStream e implementa, tra l ’altro, l'interlaccia DataOutput Nei limiti del possibile, Java consente di mantenere “compatibilità" tra versioni diverse delle classi, rendendo
void writeObject( Object o ) //scrive l’oggetto o in forma serializzata possibile la deserializzazione con “responsabilizzazione” del programmatore. Il programmatore può farsi
void close() generare il numero seriale unico associato ad una versione di una classe con l'utility (presente nel JDK)
serialver, da lanciare dalla directory di progetto ad es. come segue:
ObjectlnputStream
estende InputStream ed implementa, tra l'altro, l'interfaccia Datalnput serialver poo.agendina.Nominativo INVIO
Object readObject() //legge e deserializza un oggetto
void close() Lo strumento serialver (eventualmente anche in veste grafica se attivato con l'opzione -show) fornisce un
valore long associato alla classe (trascurando i campi static e transient) es.
Salvataggio di un’agendina mediante serializzazione
Per esemplificare, si re-implementano i metodi salva/ripristina della classe AgendinaAstratta usando flussi di
oggetti.

public void salva(String nomeFile) throws IOException{


ObjectOutputStream oos=new ObjectOutputStream( new FileOutputStream( nomeFile ) );
for( Nominativo n: this ){
oos.writeObject( n ); A questo punto, si può “forzare" la compatibilità introducendo nella classe il numero seriale originario, es.
}
oos.close(); public class Nominativo extends Comparable<Nominativo>, Serializable {
}//salva
private static final long serialVersionUID = -4686878918093450474L;
public void ripristina( String nomeFile ) throws lOException (
ObjectlnputStream ois=new ObjectlnputStream( new FilelnputStream( nomeFile ) ); }//Nome classe
this.svuota();
Nominativo n=null; La deserializzazione di un oggetto-istanza di una certa classe può ancora avvenire, entro certi limiti, pur se la
for(;;){ classe “è cambiata".
try{
n=(Nominativo)ois.readObject(); Se un campo ha cambiato tipo rispetto alla versione precedente, il processo di serializzazione è in realtà
} incompatibile.
catch( ClassNotFoundException e1 ) { throw new IOException();}
catch( ClassCastException e2 )( throw new IOException();} Se l’oggetto serializzato ha più campi di quanti ne risultano nella nuova versione della classe, gli extra-campi
catch( EOFException e3 ){ break;} vengono ignorati durante la deserializzazione.
this.aggiungi( n );
}//for Se l’oggetto serializzato ha meno campi di quanti ne ha la nuova versione della classe, i nuovi campi
ois.close(); dell’oggetto deserializzato verranno posti al loro valore di default (nuli per un oggetto, 0 per un campo
}//ripristina numerico etc.) ciò che può creare problemi nei metodi che si aspettano valori inizializzati opportunamente. La
“cura” consiste nel customizzare la deserializzazione (si veda più avanti).
Si nota che al tempo della deserializzazione, il metodo readObject() ritorna un Object che va castizzato al tipo
Nominativo. Ovviamente, in presenza di errori sul file, questa operazione potrebbe dar luogo ad un’eccezione NeH’ambiente integrato Eclipse, non appena si specifica che una classe implementa Serializable, ad essa
ClassCastException se l’oggetto non è di classe Nominativo. Se la classe Nominativo non è disponibile per viene associato un warning che ricorda che la classe non dispone ancora del serialVersionUID. Cliccando sul
essere caricata in memoria, si solleva un’eccezione ClassNotFoundException. Entrambe queste eccezioni warning è possibile richiedere l'inserimento in automatico del numero seriale (ciò che corrisponde ad eseguire
sono semplicemente propagate nell'esempio del metodo ripristina. Come per i flussi tipati, la fine del file è il tool serialver) da parte del compilatore.
annunciata da una eccezione EOFException.
Il meccanismo della serializzazione è piuttosto complesso anche se il suo utilizzo, in molti casi pratici, risulta
semplice. Occorre tener presente che il ripristino degli oggetti da un flusso di oggetti deve avvenire allo stesso

222 223
C ap ito lo ^ Flussi e file

modo rispetto a come è stato effettuato il salvataggio. Salvando un array di 3 oggetti, al ripristino occorre Una classe BancaAstratta:___________________________________________________________________
prelevare in un “unico colpo” un array di 3 oggetti e non tre oggetti separatamente. Implementa l'interfaccia Banca e concretizza gran parte dei metodi, sfruttando al solito l’iteratore. Per
semplicità si riportano solo i metodi salva() e carica() che, similmente al caso dell’agendina, utilizzano la
Non vengono serializzati campi di un oggetto che siano static o transient. Il modificatore transient va usato, ad serializzazione.
es.t quando alcune variabili di istanza si riferiscono a classi non serializzabili. In questi casi è opportuno
customizzare la serializzazione. Nella classe serializzabile si ridefiniscono i metodi writeObject() e package poo.banca;
readObject(), solo in versione private, come segue: import java.io.*;
import java.util.*;
private void writeObject( ObjectOutputStream out ) throws IOException{ public abstract class BancaAstratta implements Banca{
out.defaultWriteObject(); //per attivare il meccanismo di serializzazione di base
scritture su out customizzate public void salva( String nomeFile ) throws IOException{
}//writeObject ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(nomeFile));
for( ContoBancario c: this ) oos.writeObject(c);
private void readObject( ObjectlnputStream in ) throws IOException{ oos.close();
in.defaultReadObject(); //per attivare il meccanismo di deserializzazione di base }//salva
letture da in customizzate
}//readObject public void carica( String nomeFile ) throws lOExceptionf
ObjectlnputStream ois=new ObjectlnputStream( new FilelnputStream(nomeFile) );
Una customizzazione “più radicale" consiste neirimplementare l’interfaccia Externalizable (anziché ContoBancario cb=null;
Serializable) e ridefinire i metodi public: this.svuotaQ;
f°r(;;){
public void readExternal( Objectlnput in ) throws lOException try{
public void writeExternal( ObjectOutput out ) throws lOException cb=(ContoBancario)ois.readObject();
this.aggiungiConto(cb);
In questo caso la classe definisce i suoi propri meccanismi per salvare/ripristinare lo stato dell’oggetto }catch( ClassNotFoundException e1 )(
(facendosi carico anche dello stato della superclasse etc.) sul/dal flusso di oggetti. La serializzazione di base e1.printStackTrace();
si limita a registrare il descrittore della classe sul flusso. Per tutto il resto, vale la ridefinizione di writeExternal(). throw new IOException();
}catch( ClassCastException e2 )(
Durante la lettura di un oggetto esternalizzato, si garantisce che venga prima creato un oggetto della classe e2.printStackTrace();
target con il costruttore di default, ogni altra inizializzazione dipende da quanto scritto in readExternal(). throw new IOException();
L’esternalizzazione può essere preferibile, per ragioni di efficienza, alla serializzazione standard quando sono }catch( EOFException e3){
in gioco grandi quantità di dati. break;

Un esempio di applicazione della serializzazione/esternalizzazione


Di seguito si considera una gerarchia di classi per la gestione di conti correnti bancari. In precedenza (cap. 4) ois.close();
sono stati introdotti due tipi di conti: ContoBancario e ContoConFido (che estende ContoBancario). Al fine di {//carica
lavorare col concetto di banca è utile l’interfaccia iterabile che segue:
}//BancaAstratta
package poo.banca;
import java.io.*; Classi ContoBancario e ContoConFido esternalizzate:S i
public interface Banca extends lterable<ContoBancario>{ Si possono sviluppare (e sono disponibili) diverse classi concrete eredi di BancaAstratta basate ad es. sulle
int size(); classi collezioni di java.util. In questa sede non ci sofferma su tali classi ma si illustra la predisposizione delle
void svuotaQ; classi ContoBancario e ContoConFido in modo da renderle esternalizzabili, dunque assoggettabili a
void aggiungiConto( ContoBancario cb ); serializzazione custom.
void rimuoviConto( ContoBancario cb );
void rimuoviConto( int i ); package poo.banca;
int indexOf( ContoBancario cb ); import java.io.*;
ContoBancario getConto( int i ); public class ContoBancario implements Externalizablef
void salva( String nomeFile ) throws lOException; private String numero;
void carica( String nomeFile ) throws lOException; protected doublé bilancio=0;
}//Banca public ContoBancario()(} //necessario per il processo di esternalizzazione
224 225
Capitolo 12^ Flussi e file

public ContoBancario( String numero ) { this.numero=numero;}


public ContoBancarioj String numero, doublé bilancio ){ Tipi enumerati e serializzazione
this.numero=numero; this.bilancio=bilancio; Un altro vantaggio recato alle applicazioni dalle classi enum, che contribuisce alla loro semplicità e sicurezza,
} è costituito dal fatto che le costanti enumerate sono automaticamente serializzabili e dunque ad es.
salvabili/recuperabili su/da file di oggetti. Come caso particolare, anche un oggetto singleton (si veda il cap. 8)
public void readExternal( Objectlnput in ) throws IOException{ è immediatamente pronto per la serializzazione/deserializzazione senza mai violare il contratto che della
numero=in.readUTF(); classe singleton, cioè che solo un’istanza possa esistere di momento in momento.
bilancio=in.readDouble();
}//readExternal Gestione di file e classe File
La classe File consente di lavorare con i file dal punto di vista del file System, ad es. per verificare se un file
public void writeExternal( ObjectOutput out ) throws IOException{ esiste su directory, per modificare (cancellare, ridenominare etc.) un file su directory, per gestire il sistema di
out.writeUTF( numero ); directory etc.
out.writeDouble( bilancio );
}//writeExternal File f=new File(l.daf);
if( f.exists() )...
}//ContoBancario ifj f.deleteQ )... il file è stato effettivamente cancellato ...
f.renameTo( new FileC'g.dat”) );
Si nota come le ridefinizioni di readExternal() e writeExternalQ di ContoBancario si fanno carico, con i metodi
rispettivamente di Datalnput e DataOutput, di salvare/ripristinare i dati dell'oggetto secondo il formato Attenzione: prima di una delele, renamelo etc. occorre assicurarsi che i file siano chiusi. In più, per
prescelto (es. UTF per la stringa numero). ridenominare un file, occorre usare un nome di file non esistente. Se il file esiste, si può (se è lecito) prima
cancellarlo. Vediamo la parte finale del metodo inserisci(String nome, int x) che realizza un aggiornamento
package poo.banca; selettivo su un file di interi:
import java.io.*;
public class ContoConFido extends ContoBancario{ tmp.writelnt( x ); //scrivi sicuramente x
private doublé fido=1000; if( flag ){
private doublé scoperto=0; for(;;){
public ContoConFido(){} //necessario per il processo di esternalizzazione tmp.writelnt( y );
public ContoConFido( String numero ) { super( numero );} pos=raf.getFilePointer();
public ContoConFido( String numero, doublé bilancio ) { super( numero, bilancio );} if( pos==raf.length() ) break;
public ContoConFido( String numero, doublé bilancio, doublé fido ){ y=raf.readlnt();
super( numero, bilancio ); this.fido=fido; }//for
}//if(flag)
tmp.close(); raf.close();
public void readExternal( Objectlnput in ) throws IOException{ File f=new File(nome); f.delete();
super.readExternal(in); //ripristino dati super classe File ff=new File(“tmp”);
fido=in.readDouble(); ff.renameTo( f );
scoperto=in.readDouble(); }
}//readExternal }//AggiornamentoSelettivo

public void writeExternal( ObjectOutput out ) throws IOException{ Altri metodi della classe File (lista parziale) sono i seguenti:
super.writeExternal(out); //salvataggio dati super classe
out.writeDouble(fido); boolean isFile()
out.writeDouble(scoperto); ritorna true se il file è un normale file, false altrimenti
}//writeExternal
boolean isDirectory()
}//ContoConFido usa la tua fantasia

A questo punto le classi dei conti bancari sono predisposte per il processo di esternalizzazione che è void deleteOnExit()
innescato dai metodi salva() e caricaQ della classeBancaAstratta. Si nota che nessuna informazione emerge chiede che il file venga cancellato al termine del programma (uscita dalla Java Virtual Machine)
da BancaAstratta circa il fatto se il processo di serializzazione è quello standard o uno customizzato. Questi
dettagli, invece, sono parte delle classi di oggetti coinvolti nel salvataggio/ripristino.
226 227
Capitolo 12^ Flussi e file

String getAbsolutePath() public ObjectFile( String nomeFile, Modo modo ) throws IOException{
ritorna il path name assoluto di questo file/directory this.nomeFile=nomeFile;
this.modo=modo;
String getName() buffer=null;
ritorna il nome del file/directory del path name if( modo==Modo.LETTURA )(
try{
Si nota che un oggetto File può essere passato ad un costruttore di una classe stream (es. FilelnputStream, this.ois=new ObjectlnputStream( new FilelnputStream(nomeFile) );
etc.) in luogo della stringa del nome esterno del file (es. "f.dat"). buffer=(T)ois.readObject(); //unchecked conversion warning
}catch( Exception e ){
Una classe ObjectFile con lettura anticipata_________________ buffer=null;
Per semplificare la gestione di file tipati, di seguito si propone una classe ObjectFile generica nel tipo T
supposto serializzabile, appartenente al package poo.file che assume di lavorare, per generalità, su file di
oggetti serializzati. Una particolarità della classe è la disponibilità anticipata del prossimo elemento del file. Ad else
es., subito dopo un'apertura, se il file non è vuoto, il primo elemento del file è già disponibile per essere this.oos=new ObjectOutputStream( new FileOutputStream(nomeFile) );
ispezionato. Un'istanza di ObjectFile può essere aperta (e riaperta dinamicamente) in lettura/scrittura e quindi }
essere manipolata mediante i metodi seguenti: public String getNomeFile(){ return nomeFile;}
public Modo modo(){ return modo;}
void open( Modo modo ) throws lOException public void open( Modo modo ) throws lOExceptionj
apre il file in accordo al modo, che può essere LETTURA o SCRITTURA (valori del tipo enum Modo) close();
this.modo=modo;
void close() throws lOException buffer=null;
usa la tua fantasia if( modo==Modo.LETTURA ){
try{
boolean eof() this.ois=new ObjectlnputStream( new FilelnputStream(nomeFile) );
ritorna true se è stata raggiunta la fine fisica del file buffer=(T)ois.readObject(); //unchecked conversion warning
}catch( Exception e ){buffer=null;}
T peek() }
ritorna il prossimo elemento del file, se esiste, senza avanzare la testina sul file. Se eof() è true, ritorna nuli else
this.oos=new ObjectOutputStream(new FileOutputStream(nomeFile) );
void get() throws lOException }//modo
avanza la testina alla prossima posizione del file; ridefinisce il prossimo elemento. Assume l'apertura in public void close() throws IOException{
LETTURA. Se eof() è true, solleva una lOException if( modo==Modo.LETTURA ) ois.close();
else oos.close();
void put( T o ) throws lOException }//close
scrive o, in forma serializzata, come ultimo elemento del file. Assume l’apertura in SCRITTURA public boolean eof(){
if( modo==Modo.SCfì/TTURA ) return true;
String toString return buffer==null;
ritorna sotto forma di stringa il contenuto del file. }//next
public T peek() throws IOException{
Altri dettagli si possono consultare direttamente sul codice Java fornito di seguito. if( eof() ) throw new EOFException();
return buffer;
package poo.file; }//peek
import java.io.*; public void get() throws IOException{
public class ObjectFilecT extends Serializable>{ if( eof() ) throw new EOFException();
public enum Modo{ LETTURA, SCRITTURA}; try{
private ObjectlnputStream ois; buffer=(T)ois.readObject(); //unchecked conversion warning
private ObjectOutputStream oos; }catch( Exception e ){
private String nomeFile; buffer=null;
private T buffer; //anticipa prossimo elemento del file }
private Modo modo; }//get

228 229
Capitolo 12 Flussi e file

public void put( T x ) throws IOException{ generale un residuo su uno dei due file. Per completare la fusione basta ricopiare il residuo su f3. Segue un
if( modo==Modo.LETTURA ) throw new lOExceptionQ; possibile scenario, corrispondente al momento in cui è stato inserito il primo elemento su f3.
try{
oos.writeObject( x ); f 1:
}catch( Exception e ){ 3 5 13 14 20 30
throw new lOExceptionQ; A
f2:
}//put 2 4 8 12
A
public String toString(){ 13:
StringBuilder sb=new StringBuilder(500);
E
sb.appendff); A
try{
ObjectlnputStream is=new ObjectlnputStream( new FilelnputStream(nomeFile) ); Quando tutti gli elementi di f2 sono stati inseriti in f3, rimane un residuo su f1 che va ricopiato su f3 per
T x=null; completare l’operazione:
for(;;){
try{ f1:
x=(T)is.readObject(); //unchecked conversion warning 3 5 13 14 20 30
}catch( EOFException eof ){ A
break; f2:
}catch( ClassNotFoundException cnf ){ 2 4 8 12
return nuli;
} f3:
sb.append( x ); 2 3 4 5 8 12
sb.appendQ, "); A

}
if( sb.length()>0 ){ sb.setLength( sb.length()-2 );} //rimuove i caratteri " in eccesso Il risultato finale è:
sb.appendf]');
is.closeQ;
}catch(Exception e){
8 12 13 14 20 30

00
3 4 5
return nuli;
} Si presenta di seguito una classe MergeFile che realizza l’algoritmo della fusione ordinata. La classe si basa
return sb.toString();
su oggetti ObjectFile. Il prossimo elemento di un file è ispezionabile col metodo peek(). Per avanzare si invoca
}//toString
get(). Gli altri dettagli sono lasciati allo studio del lettore.
}//ObjectFile
La classe MergeFile
La variabile di istanza buffer contiene il prossimo elemento del file, ossia l’elemento che logicamente si trova
alla destra della testina, buffer è gestito implicitamente con tecnica anticipata. Comandando una get(), si package poo.file;
avanza la testina e se il file non è terminato si riempie automaticamente buffer e cosi via. Quando il file è import java.util.*;
aperto in scrittura, il predicato eof() è sempre true dal momento che la testina si trova sempre dopo l'ultimo import java.io.*;
oggetto del file, e buffer è nuli. Si nota, infine, che in tutti i punti dove si assegna valore a buffer dal file, è public class MergeFilej
presente un warning per unchecked conversion da Object a T (tipo generico della classe). Il rischio di public static void main( String... args) throws lOExceptionf
eccezione è ineliminabile dal momento che l’utente potrebbe aver fornito erroneamente il nome del file. System.out.printlnQFusione ordinata di due file di interi f1 ed f2 in un file f3");
Scanner sc=new Scanner( System.in );
Caso di studio: fusione ordinata di file System.out.printQnome esterno di f1 = “);
String nomeF1=sc.nextLine();
Sono dati due file f1 ed f2 tipati, es. di interi, supposti ordinati per valori crescenti. Si vuole costruire un terzo
System.out.printCnome esterno di f2 = ”);
file di interi f3 con l'unione degli elementi di f1 ed f2, disposti in ordine crescente. Il risultato si può ottenere
String nomeF2=sc.nextLine();
mediante l’algoritmo della fusione ordinata (merge sort). Si fanno partire due indici uno su f1 ed uno su f2
System.out.printQnome esterno file fusione f3 = *);
(possono essere le testine). Si confrontano i due elementi correnti. Il più piccolo si copia sul terzo file, e si
String nomeF3=sc.nextLine();
avanza sul file da cui esso proviene. Si continua cosi sino a che f1 o f2 termina. A questo punto rimane in
230 231
Capitolo 12 Flussi e file

ObjectFile<lnteger> f1=new ObjectFile<lnteger>( nomeFI, ObjectFile.Modo.LETTURA );


System.out.println("Contenuto di fi:'); Un’ultima fusione ordinata restituisce la sequenza ordinata del file:
System.out.println( f1 );
ObjectFile<lnteger> f2=new ObjectFile<lnteger>( nomeF2, ObjectFile.Modo.LETTURA ); -5 -2 0 1 2 3 7 15 16 17 18 19 J
System.out.printlnfContenuto di f2:”);
System.out.println( f2 ); Non essendo possibile realizzare le operazioni di fusione all’intemo dello stesso file, si possono utilizzare due
ObjectFile<lnteger> f3=new ObjectFile<lnteger>( nomeF3, ObjectFile.Modo.SCRITTURA ); file temporanei sui quali scaricare alternativamente i segmenti del file di partenza. Tutto ciò facilita l’operazione
int x1, x2; di fusione di segmenti ed è alla base di una potenziale fonte di efficienza: due o più segmenti depositati su uno
while( !f1.eof() && !f2.eof() ){ stesso file temporaneo possono "saldarsi" naturalmente, determinando un segmento più grande. Ad es., il file:
x1=f1.peek(); x2=f2.peek();
if( x1<x2 ){//minimo tra x1 ed x2 proviene da f1 | 2 10 12 | | 4 18 | | 14 25 | [ 20 |
f3.put( x1 ); f1.get();
} consiste di quattro segmenti. Ripartendoli sui due file temporanei si ha:
else{ //minimo proviene da 12
f3.put( x2 ); f2.get(); tempi:
; 2 10 12 14 25 |

//gestione residuo su f1 temp2:


while( !f1.eof() ){ 4 18 20 |
f3.put( f1.peek() ); f1.get();
} E evidente che a seguito dei fenomeni di “saldatura", tempi e temp2 contengono in realtà ciascuno un solo
//gestione residuo su f2 segmento. La fusione di questi due segmenti rilascia la sequenza ordinata sul file primario.
while( !f2.eof() ){
f3.put(f2.peek() ); f2.get(); L’algoritmo naturai merge sort itera due fasi fondamentali:
} • distribuzione (o suddivisione), che carica i segmenti del file primario sui due file di appoggio
f1.close(); f2.close(); f3.close(); • ricombinazione, che prende coppie di segmenti, uno dal primo file temporaneo, l’altro dal secondo file di
System.out.printlnfContenuto di f3:"); appoggio, ne effettua la fusione e deposita il segmento risultante sul file primario. Naturalmente è agevole
System.out.println( f3 ); aggregare alla fase di ricombinazione il conteggio dei segmenti risultanti sul file primario. Allorquando il
}//main numero dei segmenti diventa 1, l’algoritmo termina.
}//MergeFile
Si riporta di seguito una classe NaturalMergeSort che si avvale di oggetti ObjectFile per realizzare le
Caso di studio: ordinamento esterno di file per fusione naturale ____ ______________________ operazioni descritte sopra. Si nota che nella fase di distribuzione, il file primario va aperto in lettura ed i file
Per ordinare un file si può pensare di copiarne il contenuto ad es. su un ArrayList, quindi ordinare l’ArrayList temporanei in scrittura. Nella fase di ricombinazione, il file primario va aperto in scrittura e quelli temporanei in
col metodo sort di Collections e quindi ricopiare l’ArrayList sul file. Considerato che la dimensione di un file lettura. Tutto ciò, unitamente al comportamento della lettura anticipata del prossimo elemento di un file,
può essere molto grande, può essere necessario ordinarlo esternamente, ossia direttamente sul disco, senza agevola la scrittura della classe. L’algoritmo è confinato nel metodo risolvi() che rimanda poi a metodi ausiliari
trasferirne preliminarmente il contenuto in memoria interna in una struttura dati collezione. In quanto segue si privati l’effettuazione di sottocompiti specifici.
discute l'algoritmo detto naturai merge sort, ossia ordinamento esterno per fusione naturale. L’idea base
dell’algoritmo è che un file, se non è già ordinato, consiste di sottosequenze consecutive (o segmenti) La classe NaturalMergeSort
ordinate. Ogni segmento è ordinato al suo interno. Male che vada, un segmento può avere lunghezza unitaria.
Fondendo coppie di segmenti consecutivi, si determinano segmenti più grandi ed il loro numero diminuisce. package poo.file;
Per esemplificare, si consideri il seguente file di interi in cui sono evidenziati 5 segmenti ordinati: import java.io.*;
import java.util.*;
| 15 17 19 | [2 16 | | -5 7 18 | 0 j | -2 1 3[ public class NaturalMergeSortcT extends Serializable & Comparable<? super T » {
private ObjectFile<T> f, tempi, temp2;
Fondendo il primo col secondo segmento ed il terzo col quarto si ha: private String nomeFile;
public NaturalMergeSort( String nomeFile ) throws lOExceptionj
|2 15 16 17 19 | | -5 0 7 18 l | -2 1 3 [ this.nomeFile=nomeFile;
f=new ObjectFile<T>( nomeFile, ObjectFile.Modo.LETTURA );
ossia si determinano tre segmenti. Fondendo ora il primo col secondo si ha: tempi =newObjectFile<T>( "c:\\poo-file\\temp1", ObjectFile.Modo.SCRITTURA );
temp2=new ObjectFile<T>( ”c:\\poo-file\\temp2’’, ObjectFile.Modo.SCRITTURA );
| -5 0 2 7 15 16 17 18 19 J [ -2 1 3 |
232 233
Capitolo 12 Flussi e file

public void risolviO throws IOException{ private void copiaSegmento( ObjectFile<T> da, ObjectFile<T> a ) throws IOException{
int numSeg=0; boolean fineSegmento=false;
do{ do{
distribuisci(); fineSegmento=copiaElemento( da, a );
tempi.close(); temp2.close(); f.close(); }while( IfineSegmento );
f.open( ObjectFile.Modo.SCRITTURA ); }//copiaSegmento
tempi.open( ObjectFile.Modo.LETTURA ); private void fondi2Segmenti() throws lOExceptionj
temp2.open( ObjectFile.Modo.LETTURA ); boolean fine1Seg=false, fine2Seg=false;
numSeg=ricombina(); while( IfinelSeg && !fine2Seg ){
tempi.close(); temp2.close(); f.close(); T x1=temp1.peek();
f.open( ObjectFile.Modo.LETTURA ); T x2=temp2.peek();
tempi ,open( ObjectFile.Modo.SCRITTURA ); if( x1.compareTo(x2)<0 ) fine1Seg=copiaElemento( tempi, f );
temp2.open( ObjectFile.Modo.SCRITTURA ); else fine2Seg=copiaElemento( temp2, f );
}while( numSeg>1 ); }
//rimozione file temporanei //gestione residui
tempi.close(); while( IfinelSeg ){
temp2.close(); fine1Seg=copiaElemento( tempi, f );
File tf=new File("c:\\poo-file\\temp1'); }
tf.delete(); while( !fine2Seg ){
tf=new File(“c:\\poo-file\\temp2"); fine2Seg=copiaElemento( temp2, f );
tf.delete(); }
}//risolvi }//fondi2Segmenti
private void distribuisci() throws lOExceptionj
while( !f.eof() ){ private boolean copiaElemento( ObjectFile<T> sorg, ObjectFile<T> dest ) throws IOException{
copiaSegmentof f, tempi ); boolean fineSeg=false;
if( !f.eof() ) T prec=sorg.peek();
copiaSegmento( f, temp2 ); dest.put( prec );
} sorg.get();
^/distribuisci if( !sorg.eof() ) fineSeg=sorg.peek().compareTo(prec)<0;
else fineSeg=true;
private int ricombina() throws IOException{ return fineSeg;
int numSeg=0; }//copiaElemento
while( !temp1.eof() && !temp2.eof() ){
fondi2Segmenti(); public static void main( Stringf] args ) throws lOExceptionj
numSeg++; System.out.printlnfOrdinamento esterno di un file di interi per fusione naturale-);
} Scanner sc=new Scannerj System.in );
//gestione residui System.out.printfNome file: ");
while( !temp1.eof() ){ String nomeFile=sc.nextLine();
copiaSegmento( tempi, f ); ObjectFile<lnteger> of=new ObjectFile<lnteger>( nomeFile, ObjectFile.Modo.LETTURA );
numSeg++; System.out.println(-Contenuto iniziale di “+nomeFile);
} System.out.printlnj of );
while( !temp2.eof() ){ of.close();
copiaSegmento( temp2, f ); new NaturalMergeSort<lnteger>(nomeFile).risolvi();
numSeg++; of.openj ObjectFile.Modo.LETTURA );
} System.out.println(-Contenuto finale di -+nomeFile);
return numSeg; System.out.printlnj of );
}//ricombina }//main

}//NaturalMergeSort

234 235
C ap ito lo 12 Flussi e file

La classe NaturalMergeSort è generica nel tipo T delle componenti del file, supposto sia serializzabile sia 10. Come 9. ma utilizzando il metodo selection sort o il metodo insertion sort.
provvisto del confronto naturale. L’utilizzo delle variabili buffer interne agli oggetti ObjectFile consente nel Nel caso selection sort non basarsi sul conteggio preliminare del numero delle componenti del file, ma
metodo copiaElemento di copiare la componente corrente del file sorg e di “sbirciare” sulla prossima utilizzare file temporanei come opportuno. Siano tempi e temp2 due file di appoggio. Si scandisce f alla
(comandando un'operazione sorg.getQ) per capire se con la componente attuale termina il segmento o no. Gli ricerca del minimo. Ogni altro elemento non minore del minimo si copia su temp2. Alla fine di una fase di
altri dettagli sono lasciati allo studio del lettore. ricerca del minimo, il minimo trovato si aggiunge alla fine di tempi. A questo punto, si scambiano i ruoli tra f e
temp2: temp2 diventa file primario, f il secondo file temporaneo. L’algoritmo termina quando il file primario è
Esercizi vuoto. Alla fine, con la mediazione di oggetti File, si può cancellare il file primario originario, ridenominare
1. Modificare il programma Crea in modo da utilizzare flussi tipati bufferizzati. primario il file temporaneo tem pi, e rimuovere il file temporaneo temp2.
2. Due file f1 (testuale) ed f2 tipato contengono numeri interi positivi. Su f1, ciascun intero risiede su una linea Insertion sort esterno può essere implementato come segue. Si usano sempre tempi e temp2. Di momento in
separata. Si deve scrivere un programma che genera, a partire da f1 ed f2, un terzo file tipato di interi f3, in cui momento la porzione ordinata è mantenuta su un file temporaneo. Si scandisce una sola volta f. Per ogni
ogni intero è la giustapposizione dei valori di due interi corrispondenti in f1 ed f2. Ad esempio se f1 contiene componente c di f, si confronta c con la porzione ordinata ad esempio esistente su tempi. Gli elementi di
tempi trovati minori di c, si copiano su temp2. Non appena si incontra un elemento non minore di c su tempi,
12 si scrive c su temp2 e quindi si ricopia il residuo di tempi su temp2. A questo punto la porzione ordinata
3056 corrente si trova su temp2 e si possono scambiare i ruoli tra tempi e temp2. Quando f termina si può
244 rimuovere il file primario, ridenominare il file temporaneo che al momento contiene la sequenza ordinata col
45 nome del file primario, quindi eliminare l’altro file temporaneo.

ed f2 contiene gli interi [355,4,267], il contenuto di f3 dovrà essere: [12355,30564,244267,45]. Naturalmente, Altre letture ________
quando uno dei due file, termina, si continua a scrivere su f3 i numeri rimanenti sul file non terminato. Il Ulteriori dettagli sul processo di serializzazione/esternalizzazione e più in generale sulle problematiche dei
programma deve leggere preliminarmente i nomi esterni dei file f1, f2 ed f3 e alla fine deve visualizzare il flussi e file di Java, ivi compreso il package java.nio che include classi, tra l’altro, per il mapping di file in
contenuto di f3. memoria RAM al fine di accelerare le operazioni di ingresso/uscita, possono essere trovati, ad es., su:
3. Leggere il nome di un file di tipo testo da tastiera e quindi contare il numero dei caratteri, delle parole (una
parola è una sequenza di caratteri alfanumerici) e delle linee del file, e scrivere tali informazioni su video. C.S. Horstmann, G. Cornell, Core Java, Voi. Il - Advanced Features, 8,h Edition, Prentice-Hall, 2008.
4. Dati due file di tipo testo f1 ed f2, scrivere un programma che verifica se f2 è contenuto in f1.
5. È assegnato un file di tipo testo f1. Costruire a partire da esso un secondo file di tipo testo f2 come segue.
Da una linea di input (o da riga di comando) si leggono le informazioni:

s d n L l nL2

dove s e d denotano due caratteri (sorgente e destinazione), ed nL1 ed nL2 sono due interi (nL1£nL2) che
denotano due numeri di linea di f 1. Su f2 va riportato lo stesso contenuto di f1 salvo che su tutte le linee
comprese tra nL1 e nL2, ogni occorrenza del carattere denotato da s va sostituita con una occorrenza del
carattere denotato da d.
6. A partire dalla classe Crea che costruisce un file di interi letti da tastiera, ottenere una classe
CreaObjectFile che costruisce un ObjectFile<lnteger> attingendo sempre da tastiera.
7. L'algoritmo di fusione ordinata implementato nella classe MergeFile costruisce il terzo file f3 con i contenuti
di f1 ed f2 ma non evita la formazione di duplicati. Modificare MergeFile in modo tale che su f3 non sussistano
duplicati. Si potrebbe utilizzare un HashSet<lnteger> di appoggio per tener traccia degli elementi via via
aggiunti ad f3 e non inserire un nuovo elemento quando esso è presente nell’hash set. Tuttavia, considerato
che gli elementi uguali sono tutti adiacenti (in f1 e in f2) per la proprietà dell'ordinamento, si può evitare il
ricorso ad una collezione di supporto mantenendo semplicemente in una variabile (es. x3) l'ultimo elemento
aggiunto ad f3 e non inserire un nuovo elemento in f3 che sia uguale ad x3. Dopo ogni inserimento occorre
aggiornare x3. Naturalmente esiste un problema di partenza allorquando f3 è vuoto e x3 non ha ricevuto
ancora un valore definito. Ci si può aiutare con una booleana flag che vale true prima del primo inserimento in
f3, false altrimenti.
8. Provare a risolvere il problema della fusione ordinata non utilizzando ObjectFile ma semplici file tipati
supportati da Java. Cercare di mantenere semplicità e leggibilità alla soluzione.
9. Con riferimento ad un file tipato di interi f, si scriva un programma Java che ordini esternamente f
utilizzando il metodo bubble sort. Utilizzare un file temporaneo su cui depositare il risultato di una scansione di
bubble. Dopo ogni scansione, se l’algoritmo va continuato, si scambiano i ruoli tra file primario e file
temporaneo.
236 237
Capitolo 13: ________
Elementi di programmazione dell'interfaccia utente grafica

AWT - A Windowing Toolkit, è stato il primo framework di Java per lo sviluppo di GUI (Graphical User
Interface). AWT delega molto al Sistema Operativo (SO) sottostante (es. MS Windows) per l'ottenimento ed il
tracciamento degli elementi della GUI (finestre, pulsanti, menu, etc.). Swing è nato come esperimento mirato a
minimizzare le dipendenze dal SO. Solo la finestra è chiesta al SO. Il tracciamento e la gestione di ogni altro
elemento di GUI è responsabilità del programmatore che cosi può assicurare un look uniforme alle
applicazioni anche cambiando SO.

Il modello di gestione degli eventi di Swing è comunque quello precedentemente definito da AWT: Event
Delegation Model. AWT e Swing sono esempi di framework. Un framework è una collezione di classi già
pronte per l’uso. Se richiesto, tali classi possono essere specializzate via inheritance prima di essere utilizzate.
Accanto alla collezione di classi, un framework definisce anche un proprio flusso di eventi. È possibile far
partecipare i propri oggetti specializzazione di classi del framework, al flusso degli eventi

Gerarchia base di finestre


Una GUI è una gerarchia di componenti. Si distinguono contenitori e componenti (si veda la figura che segue).
Come caso particolare, un componente innestato in un contenitore può essere a sua volta un contenitore etc.

Due contenitori chiave sono JFrame e JPanel. Componenti elementari sono JLabel, JTextField, JButton,
JRadioButton, etc. Le classi JFrame e JPanel si possono utilizzare direttamente per istanziazione o, molto
spesso, possono venire estese e poi utilizzate. Una JFrame assume normalmente il ruolo di contenitore base
degli elementi della GUI di un’applicazione. Un JPanel è un componente parte di una JFrame. Esso è utile per
la visualizzazione grafica (tracciamento di figure, immagini,...). Un caso particolare di JPanel è JApplet, ossia
le mini applicazioni agganciabili alle pagine web.

Event delegation m o d e [_____________


Quando l’utente interagisce con un’applicazione per mezzo di una GUI, quello che succede è che si generano
eventi (ad es. per il click di mouse su un bottone o per la scelta di un item in un menu etc.). Ogni evento viene
catturato inizialmente dal sistema operativo, quindi passato ad AWT di Java (con argomenti, es. le coordinate
del mouse) e in ultima istanza consegnato al codice predisposto presso la GUI per rispondere all’evento
stesso (si veda la figura).

239
Capitolo 13 Programmazione di GUI

Evento raccolto da Windows

Evento passato a Java AWT


Click di mouse (con parametri)
(tngger di evento)
callback
(l’evento è passato com e param etro)

Codice gestione evento,


parte della GUI. meglio di un ascoltatore dell'evento

GUI

Il codice (metodo) Java presso la GUI che si incarica della gestione dell'evento è spesso detto callback perché
rappresenta codice utente invocato dal sistema (normalmente, nell'interazione con un sistema operativo, è
l’utente che invoca codice di sistema, ad es. attraverso una system-call).

Il cuore della programmazione di una GUI consiste nel predisporre il codice di gestione degli eventi scatenati
sull’interfaccia. Ne deriva un vero e proprio stile di programmazione: si parla di Event Driven Programming
(programmazione pilotata dagli eventi) per riferirsi allo schema di calcolo, basato sullo scambio di messaggi, Interfacce di ascoltatori di eventi (Event Listener)
che caratterizza il funzionamento di una GUI. Le interfacce “event listener” introducono uno o più metodi callback che ricevono l’evento scatenante (munito
di parametri) come argomento:
È importante notare che le invocazioni delle callback definiscono uno schema di esecuzione non
deterministico: non si conosce, infatti, in che ordine verranno generati gli eventi possibili. Il programmatore WindowListener
deve garantire che quale che sia l’ordine, il comportamento è corretto. In realtà, gli eventi vengono dapprima ActionListener
bufferizzati in una coda di eventi (EventQueue) di AWT/Swing e quindi processati a cura di un thread (si veda MouseListener
il cap. 23) fondamentale: Event Dispatch Thread (EDT) (thread controllore degli eventi). MouseMotionListener

Non sempre è vero che dopo aver scatenato un evento esso verrà subito processato. Tutto dipende dal fatto
se esistono o meno altri eventi sulla coda. Ultimamente l'ordine di processamento (elaborazione) dipende Nella tabella che segue si richiamano i metodi callback, i parametri e relativi metodi accessori e le sorgenti
dalla particolare politica adottata da EDT. degli eventi.

Classificazione (parziale) degli eventi*• Interfaccia Metodi callback Parametri/metodi accessori Sorgenti di generazione
Si distinguono: degli eventi
ActionListener actionPerformed ActionEvent AbstractButton
• eventi legati alle finestre (WindowEvent)
-getActionCommand JComboBox
• eventi azione (ActionEvent) -getModifiers JTextField
• eventi di mouse (MouseEvent) Timer
• eventi legati alla keyboard (KeyEvent) Ad|ustmentListener AdjustmentValueChanged AdjustmentEvent JScrollBar
-getAdjustable
-qetAdjustmentType, getValue
Quando nasce un evento, viene creato un oggetto della classe di appartenenza ed inizializzato con dati ItemListener itemStateChanged ItemEvent AbstractButton
(parametri) caratteristici dell’evento. Ad esempio, un evento di click di mouse si accompagna alle coordinate -getltem, getltemSelectable JComboBox
x,y del punto di click, un evento azione si accompagna agli attributi della sua sorgente (il tipo di elemento di -getStateChange
interfaccia su cui è nato l’evento, es. un bottone, un campo di testo, etc.) ed al suo contenuto (si pensi ad un FocusListener focusGained FocusEvent Component
campo di testo, in cui l’utente digita una certa stringa). focusLost -isTemporary
KeyListener keyPressed, keyReleased KeyEvent Component
keyTyped -getKeyChar, getKeyCode
Gerarchia delle classi di eventi di AWT -getKeyModifiersText
Di seguito si mostra un diagramma di classi UML con gli eventi AWT. Si sottolinea che queste classi sono -getKeyText. isActionKey
riconosciute sia in ambito AWT che di Swing. MouseListener mousePressed, mouseExited, MouseEvent Component
mouseReleased, mouseClicked, -getClickCount, getX, getY,
mouseEntered -getPoint, translatePoint

240 241
Capitolo 13 Programmazione di GUI

MouseMotionListener mouseDraqqed, mouseMoved MouseEvent Component


MouseWheelListener mouseWheelMoved Mouse WheelEvent Component
-getWheelRotation
-getScrollAmount
WindowListener windowClosing, windowOpened, WindowEvent Window
windowlconified, windowClosed, •getWindow
windowDeicomfied,
windowActivated.
windowDeactivated
WindowFocusListener windowGainedFocus. WindowEvent Window
windowLostFocus -qetOppositeWindow
WindowStateLislener windowStateChanged WindowEvent Window
-getNewState, getOldState

Adattatori (classi adapter)


Quando un'interfaccia listener introduce diversi metodi callback, per semplificare la vita del programmatore
che potrebbe essere interessato solo ad alcuni metodi callback anziché a tutti, il framework AWT mette a Esempio di JFrame
disposizione anche classi adapter che “implementano” i metodi a vuoto, ossia il corpo è {}. Il programmatore
potrebbe quindi estendere (anche “al volo”) queste classi adapter e occuparsi della ridefinizione dei soli metodi L’esempio costruisce una propria classe Finestra che estende JFrame. Il lavoro preparatorio si realizza nel
di interesse. Un esempio tipico è WindowListener che definisce sette metodi callback: windowClosingQ, costruttore di Finestra. Il metodo setTitle() consente di fissare il titolo che apparirà nella barra della finestra. Il
windowClosed(), windowlconified(), windowDeiconified(), windowOpenedQ, windowActivatedQ, metodo setSize() permette di stabilire larghezza e altezza inziali della finestra (le misure sono in pixel). La
windowDeactivatedQ. Adattatori disponibili: finestra comunque è ridimensionabile poi a piacere mediante il mouse. Il metodo setLocationQ specifica le
coordinate del punto in alto a sinistra della finestra rispetto al video (si veda anche più avanti). Il metodo
WindowAdapter setDefaultCloseOperation() dichiara cosa si deve fare, di default, in seguito all’evento di chiusura della
MouseAdapter finestra. In questo caso si esce immediatamente (non sempre una cosa buona dal momento che potrebbe
MouseMotionAdapter essere necessario salvare dei dati su disco prima di uscire).
KeyAdapter
FocusAdapter La classe col main, FinestraChiudibile, si limita ad istanziare Finestra e a renderla visibile mediante il metodo
setVisible(true). Il tutto si "mantiene in piedi” anche in seguito alla terminazione del main, in virtù deH’ambiente
Progetto di un ascoltatore multi-thread che accompagna automaticamente la GUI.
Nel progettare una classe ascoltatore di eventi, o si implementa una (o più) interfaccia listener (ossia si
implementano turri i metodi dell'interfaccia) o si estende, se esiste, una corrispondente classe adapter. Intercettazione e gestione deH’evento di chiusura:________________________________________________
public Finestra(){//costruttore di Finestra
Dovendo gestire l'evento di chiusura di una trame (l'utente clicca sul pulsante x in alto a destra, o digita ALT- setTitlefFinestra Chiudibile");
F4 o seleziona esci da un menu), che è catturato dalla callback windowClosing(WindowEvent e), è setSize(300,200);
conveniente introdurre una classe (tipicamente inner) che estende WindowAdapter e si limita a ridefinire il setLocation(50,200);
metodo windowClosing(...) anziché implementare l'interfaccia WindowListener. Vediamo i dettagli: setDefaultCloseOperation( JFrame.DO_NOTHING„ON_CLOSE ;
addWindowListener( new WindowAdapter(){
package poo.Swing; public class FinestraChiudibile{ public void windowClosing(WindowEvent e){//callback
import java.awt.*; public static void main( String [jargs ){ System.exit(O); Ilo altra gestione come opportuno
import java.awt.event.*; JFrame f=new Finestra();
import javax.Swing.*; f.setVisible(true);
class Finestra extends JFrame{ }
public Finestra(){ }//FinestraChiudibile
setTitlefFinestra Chiudibile"): In questo caso si fissa l’operazione di default alla chiusura con la costante
setSize( 300,200 ); JFrame.DO„NOTHING_ON^CLOSE che prescrive di non fare nulla. Quindi si installa un window listener
setLocation( 50,200 ); ottenuto estendendo ed instanziando “al volo" l’adattatore WindowAdapter e ridefinendo il solo metodo
setDefaultCloseOperation( windowClosing() che (per ora) esegue banalmente una System.exit(O). Naturalmente, una volta intercettato
JFrame.EXIT ON CLOSE); l’evento di chiusura, è possibile pianificare operazioni di gestione più sofisticate all'interno della callback
) windowClosing().
}//Finestra

242 243
Capitolo 13 Programmazione d[GUI

Sistema di coordinate b) affidata allo Event Dispatch Thread di Swing (soluzione preferibile):
Il metodo setLocation() definisce dove è collocata inizialmente la finestra sul video. Richiede, come già
anticipato, le coordinate <x,y> (in pixel) del punto più in alto a sinistra della finestra. Il sistema di coordinate, in public class FinestraChiudibile{
questo caso, si riferisce all’intero video, con l'origine <0,0> posta nello spigolo più in alto a sinistra del video. public static void main( String []args ){
L’asse x va verso destra. L’asse y è diretto verso il basso del video EventQueue.invokeLater( new Runnable(){
public void run(){
x JFrame f=new Finestra();
f.setVisible(true);

});
}//main
▼ y }//FinestraChiudibile

Più in generale, quando si forniscono le coordinate di tracciamento di un oggetto es. su un pannello, il sistema Uso di JFrame e J P a n e l___ ___
di coordinate da adottare è quello locale, l'origine <0,0> è lo spigolo in alto a sinistra del componente e non del Su una JFrame di norma si colloca una struttura di menu per controllare l’applicazione. Su un JPanel (o
video, asse x diretto verso destra, asse y diretto verso il basso. JComponent) si tracciano o si visualizzano oggetti grafici o si inseriscono elementi di interfacciamento con
l’utente come campi di testo (JTextField), bottoni (JButton) etc. È pratica comune quella di creare più pannelli
Gerarchia di componenti GUI da inserire in una JFrame che realizza la GUI e disporre nei singoli pannelli elementi di interfacciamento
E utile dare un’occhiata d’insieme alle classi dei componenti utilizzabili per organizzare una GUI. Al solito, le diversi da menu. Da una JFrame si può anche creare al volo una differente JFrame per gestire l’interazione
classi che iniziano con J sono pertinenti a Swing, le altre sono classiche di AWT. con l'utente in una particolare situazione della GUI.

Potendo inserire più elementi in un contenitore, si pone in generale il problema della loro disposizione.
Esistono in proposito i layout manager. Su una JFrame è di default il BorderLayout (si veda anche la figura
che segue) che consente di inserire gli elementi (es. pannelli) a NORTH, a SOUTH, a EAST, a WEST o nel
CENTER, es.:

frame.add( component, BorderLayout.SOUTH );

Se si desidera, si può cambiare il default installando un differente layout manager. Su un pannello è di default
il FlowLayout secondo cui gli elementi vengono ad occupare posizioni successive delle “righe" del pannello, a
partire dalla prima, a mano a mano che l’utente effettua gli inserimenti. Nella GUI che segue alcuni bottoni
sono inseriti su un pannello a sua volta aggiunto alla JFrame in posizione centrale:

Inizializzazione di una JFrame


a) affidata al thread del main:
public class FinestraChiudibile{
public static void main( String []args )(
JFrame f=new Finestra();
f.setVisible(true);
}
}//FinestraChiudibile
244
245
Capitolo 13 Programmazione di GUI

Nella successiva GUI si mostra una JFrame sulla quale sono inseriti due pannelli: nel primo (posto a nord) si JButton punto=new JButton(“.");
colloca una JLabel con l’etichetta “Risultato:” ed un JTextField; nel secondo (posto al centrol) si inseriscono JButton uguale=new JButton("=“);
bottoni secondo lo schema di una semplice calcolatrice. In questo caso, nel secondo pannello si fissa l’utilizzo JButton piu=new JButton(V);
di un GridLayout:
p.add(sette): p.add(otto); p.add(nove); p.add(diviso);
p.setLayout( new GridLayout(4,4) ); HA righe e 4 colonne p.add(quattro); p.add(cinque); p.add(sei); p.add(per);
p.add(uno); p.add(due); p.add(tre); p.add(meno);
In questo modo gli elementi inseriti occupano ordinatamente le posizioni della griglia. p.add(zero); p.add(punto); p.add(uguale); p.add(piu);

p.setLayout( new GridLayout(4,4,3,3) ); //utilizzata per l’esempio add( q, BorderLayout.NORTH ); //aggiunta del pannello p alla JFrame
add( p, BorderLayout.CENTER);
gli ultimi due parametri stabiliscono i gap (in pixel) verticale/orizzontale tra i componenti.
r--- :
Una finestra di cambio euro-lire
f i C a lc o la tric e

Risultato: 12 45 1 Cambio Euro-lire - □

7 8 9 / Lire 44534

4 5 6 •
____ I ____ I
1 Euro = 1936.27 Lire
1 2 3
........
0 ♦ Sono presenti due campi di testo (JTextField), ciascuno provvisto di label. La GUI è una JFrame con un
.. .............. IL X Ì pannello interno contenenti i due oggetti JTextField; un altro pannello contiene la scritta del cambio.

Le operazioni che realizzano la GUI (a meno della gestione degli eventi), collocate nel costruttore della classe Codice Java per la finestra di cambio: ____________________
Calcolatrice, sono richiamate di seguito: package poo.Swing;
import java.awt.*;
JPanel q=new JPanelQ; import java.awt.event.*;
JLabel l=new JLabelfRisultato:", JLabel. RIGHT); import javax.Swing.*;
JTextField jtf=new JT extFieldf 12.45", 12); import java.util.*;
q.add(l); public class Cambioj
q.add(jtf); public static void main(String []args){//esempio
FinestraCambio fc=new FinestraCambioQ;
JPanel p=new JPanel(); fc.setVisible(true);
p.setLayout( new GridLayout(4,4,3,3) );
}
JButton sette=new JButton("7“); }//Cambio
JButton otto=new JButtonC8");
JButton nove=new JButton(”9"); class FinestraCambio extends JFrame implements ActionListener{
JButton diviso=new JButton(V); private JTextField euro, lire;
JButton quattro=new JButton(’’4"); private final float EURO_LIRE=1936.27f;
JButton cinque=new JButton("5"); public FinestraCambio(){
JButton sei=new JButton("6"); setTitle(“Cambio Euro-Lire’ );
JButton per=new JButton("‘ ”); setDefaultCloseOperation( JFrame.EXIT ON_CLOSE );
JButton uno=new JButtonCT); JPanel p=new JPanel();
JButton due=new JButton("2"): p.add( new JLabelfEuro’ , JLabel.RIGHI) );
JButton tre=new JButton(’’3"); p.add( euro=new JTextField(’ ",12) );
JButton meno=new JButtonf-"); p.addj new JLabel(BLire", JLabel.RIGHI) );
JButton zero=new JButton("0"); p.add( lire=new JTextField("",12) );
246 247
Capitolo 13 Programmazione di GUI

add(p, BorderLayout.NORTH );
JPanel q=new JPanel(); E! F in e s tra
q.add( new JLabel(“1 Euro = 1936.27 Lire", JLabel.fl/GHT) );
con R e p a in t Q @ IE f
add( q, BorderLayout.SOUTH );
euro.addActionListener( this ); R e painting thè w orld
lire.addActionListener( this );
setSize(450,100);

public void actionPerformed( ActionEvent evt ){//callback


if( evt.getSource()==euro ){
float e=Float.parseFloat( euro.getText() );
euro.setText( String.formaf("%1.2f",e) ); package poo.Swing;
float lit=Math.round(e*EURO_URE); import java.awt.*;
lire.setText( String.formaf("%1.0f",lit) ); import java.awt.event.*;
} import javax.Swing.*;
else if( evt.getSource()==lire ){
float eur=Float.parseFloat{ lire.getText() )/EURO LIRE; class FinestraRepaint extends JFrame{
euro.setText( String./ormaf("%1.2f",eur) ); private Pannello p=null;
} private Font f=new FontfHelvetica", Font.BOLD, 20);
}//actionPerformed private Color col=new Color( /*red*/57, /*green*/128, /*blu*/110 );

}//FinestraCambio public FinestraRepaint(){


setTitlefFinestra con Repaint");
Per semplicità FinestraCambio è anche l’ascoltatore degli eventi azione generati dai JTextField. setSize(400,200);
setLocation(50,200);
L'applicazione è preparata a ricevere o lire o euro (non determinismo) e a rispondere mostrando sull’altro text setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
field il valore corrispondente del cambio. add( p=new Pannello() );
}
Gli oggetti text field sono associati allo action listener rappresentato dalla stessa finestra JFrame che, in
questo esempio, implementa la callback che viene invocata quando si scatena un evento azione su un campo private class Pannello extends JPanelj //esempio di JComponent
di testo. In generale è preferibile programmare a parte la classe-ascoltatore, in modo da separare le istruzioni public void paintComponent( Graphics g ){
di risposta agli eventi da tutto il resto del codice. super.paintComponent( g );
System.out.printlnf'paintComponent chiamata"); //demo
Su un JTextField si genera un evento azione quando si digita una stringa di testo e si conclude (importante) setBackground( Color.white );
con INVIO. g.setFont( f );
g.setColor( col );
Nel metodo actionPerformed( ActionEvent evt ) prima di tutto si individua la sorgente dell’evento azione, if( Math.random()<0.5 )
mediante il metodo getSourceQ. Quindi si realizza il corrispondente lavoro di conversione. I metodi g.drawString( "Repainting thè world", 30, 40 );
getText()/setText() consentono di leggere/scrivere una stringa su/da un text field. else
g.drawString( "Repainting thè world”, 100, 100);
Una finestra con repainting }
Il programma mostra la stringa “Repainting thè world’’ ad ogni occasione in cui è richiesta la ri-visualizzazione. }//Pannello
Randomaticamente si sceglie una tra due posizioni per il drawing. Si utilizzano un font ed un colore particolari.
}//FinestraRepaint
La ri-visualizzazione avviene quando si passa da minimizzazione a massimizzazione, re-sizing etc della
finestra. public class FinestraConRepaint{
public static void main( String [jargs ){
EventQueue.invokeLater( new Runnable(){
public void run(){
JFrame f=new FinestraRepaintQ;
248 249
Capitolo 13 Programmazione di GUI

f.setVisible(true);
}//run private class Pannello extends JPanel{
}); public Pannello(){
}//main addMouseListener( mi );
}
}//FinestraConRepaint public void paintComponent(Graphics g){
super.paintComponent(g);
Il lavoro di predisposizione si effettua nel costruttore della classe FinestraRepaint, al cui interno si programma System.out.printlnCpaintComponent chiamata”); //demo
una classe Pannello che estende JPanel e ridefinisce (questo è il motivo per introdurre una classe pannello ad setBackground( Color.white );
hoc) il metodo paintComponent(). È questo metodo che viene invocato ad ogni richiesta di re-painting. Come g.setFont( f );
prima cosa il metodo invoca il paintComponent() della super classe, in modo da ottenere il comportamento di g.setColor( col );
default previsto da JPanel, specializzato poi dalle azioni della paintComponent() di Pannello. La g.drawString( "Swing”, x, y );
paintComponent() riceve l’oggetto Graphics che è il tramite di ogni operazione di visualizzazione grafica. }
Graphics espone tutta una serie di metodi (consultare le API di Java) per tracciare linee (drawLine), rettangoli }//Pannello
(drawRect), ovali (tra cui cerchi) (drawOval) etc. nonché stringhe di caratteri. In un certo senso, la superfice
del pannello è come la tela del pittore. L’oggetto Graphics fornisce, invece, il toolkit per tracciare oggetti grafici private class MouseList extends MouseAdapter{
sulla “tela". public void mouseClicked(MouseEvent e){
FinestraRepaintMouse.this.setXY( e.getX(),e.getY() );
Nell’esempio, ad ogni re-painting si invoca Math.randomQ e se il numero casuale è minore di 0.5 si scrive la e.getComponent().repaint();
stringa "Repainting thè world” a partire dal punto <30,40> (in pixel) dove 30 è la x, 40 la y relative al sistema }
locale di coordinate; diversamente la scritta è mostrata a partire da <100,100>. La visualizzazione avviene su }//MouseList
sfondo bianco del pannello, imposto dal metodo setBackground( Color.white ) che cosi sostituisce il default di
JPanel. La scrittura utilizza un font helvetica di corpo 20 e in grassetto, ed un colore definiti nella classe }//FinestraRepaintMouse
FinestraRepaint.
public class FinestraConRepaintEMousef
Repainting e mouse:_________________ _________ _____________ public static void main( String []args ){
Si mostra una variazione della classe FinestraRepainting in cui il punto di visualizzazione è definito dal click EventQueue.invokeLater( new Runnable(){
del mouse. La stringa oggetto di ri-visualizzazione è “Swing”. public void run(){
JFrame f=new FinestraRepaintMouse();
package poo.Swing; f.setVisible(true);
import java.awt.*;
import java.awt.event.*; });
import javax.Swing.*; }//main
}//FinestraConRepaintEMouse
class FinestraRepaintMouse extends JFrame{
private Pannello p=null; L'inner class MouseList(ener estende l'adattatore MouseAdapter e ridefinisce il metodo mouseClicked() che
private Font f=new FontfHelvetica", Font.BOLD, 20); consente, ad ogni click, di risalire alle coordinate x,y del punto di click. Tali valori sono copiati nelle variabili di
private Color col=new Color( /*red*/57, /*green7128, /*blu*/110 ); istanza della classe FinestraRepaintMouse mediante il metodo setXYQ. Un'istanza del mouse listener è
private MouseList ml=null; stabilita come ascoltatore dell’istanza esterna della classe FinestraRepaintMouse cosi come di una istanza
private int x, y; della classe Pannello sulla cui superficie sono di norma generati i click del mouse.

private void setXY( int x, int y ){this.x=x; this.y=y;} Un particolare da porre in rilievo sono i momenti di richiesta di repainting. Mentre in precedenza il repainting
avveniva in modo “naturale" allorquando la GUI passava dallo stato minimizzato a quello massimizzato, o
public FinestraRepaintMouse(){ passava da secondo piano a primo piano etc., in questa nuova formulazione il repainting è agganciato al click
setTitlefFinestra con Repaint"); del mouse. A questo scopo. Dall’evento e trasmesso al metodo mouseClicked() si risale al componente su cui
setSize(400,200);setLocation(50,200); l'evento è stato scatenato e di questo componente si richiede esplicitamente il repainting invocando il metodo
setDefaultCloseOperation(JFrame.EXIT^ON_CLOSE); repaint().
add( p=new PannelloQ );
ml=new MouseList(); Java AWT/Swmg non memorizza ipixel di un pannello grafico. Piuttosto, ad ogni rivisualizzazione, i pixel sono
addMouseListener(ml); rigenerati da capo. Risponde a questo problema la necessità di ridefinizione del metodo pamtComponentf).
}
250 251
Capitolo 13 Programmazione di GUI

Altri componenti di GUI ____ ButtonGroup gruppo=new ButtonGroup();


JTextArea JRadioButton alrd=new JRadioButtonf'ArrayList”, false); gruppo.add( alrd );
Generalizza l’uso di JTextField. Mentre un JTextField è normalmente usato per l’input/output di una singola JRadioButtol llrd=new JRadioButtonfLinkedList", true); //selezionato inizialmente
linea di testo, una JTextArea può essere impiegata per ospitare un testo di più linee. Una JTextArea può gruppo.add( llrd );
essere equipaggiata con le barre di scorrimento, procedendo come segue:

JTextArea textArea=new JTextArea( 10, 50 ); //10 righe, 50 colonne -- nominali I radio button di un gruppo possono essere agganciati ad uno stesso ActionListener o differenti action listener
JScrollPane textAreaScrollable=new J9crollPane(textArea); //decorazione possono essere definiti uno per ciascun radio button.

Sia un JTextField che una JTextArea possono essere controllati per fornire solo dati di uscita, generati dal JComboBox
programma, non modificabili dall’utente come segue: Quando il numero di radio button non è piccolo, essi possono occupare troppo spazio sul pannello. In questi
casi è utile un Combo box che se evocato (basta clickare sul componente che lo possiede) mostra una lista di
textField o textArea ,setEditable( false ); scelte tra cui l'utente seleziona quella di interesse. La classe JComboBox consente di costruire oggetti combo
box; il numero delle scelte può essere editabile dinamicamente (anticipando setEditable(true)).
Una porzione di testo (es. una linea provvista di fine linea) può essere aggiunta alla fine di una text area col
metodo: È possibile associare ad combo box un ascoltatore di eventi azione. Quando una scelta è fatta nel combo box,
il metodo actionPerformed() consente di risalire all’oggetto combo box source che ha generato l'evento. Il
textArea.append( String testo ). metodo getSelectedltem() del combo box consente quindi di stabilire la scelta effettuata dall’utente.

Il contenuto di tutta l’area può essere prelevato col metodo: JSlider


La classe JSlider permette di gestire slider che forniscono all’utente un “continuo" di valori selezionabili
String paramString(). spostando lo slider col mouse:

È possibile inserire testo ad una certa posizione (riga) pos col metodo: JSlider slider=new JSIider( min, max, valorejniziale );

insertf String testo, int pos ) • quando il valore cambia, un evento di tipo ChangeEvent è generato che un ChangeListener è in grado di
catturare e gestire. Il metodo getValue() sull'oggetto source slider consente di ottenere il valore. In realtà è
Si può rimpiazzare il testo tra le posizioni start ed end (>=start) col metodo: possibile fissare dei tick di variazione (major e minor tick) sullo slider per cui il valore potrebbe essere
“costretto” al tick più vicino (snap to ticks). Per maggiori dettagli si rimanda alle API di Java.
replaceRangef String str, int start, int end ).
JMenuBar, JMenu, JMenultem, JPopupMenu
I cambiamenti al contenuto di una text area possono essere monitorati mediante un DocumentListener cui Sulle finestre (JFrame) si possono costituire barre di menù (oggetti di classe JMenuBar). Ogni menù (oggetti di
vengono trasmessi DocumentEvent (che recano informazioni tipo la posizione dove il cambiamento è classe JMenu) è poi un classico menù a tendina, in cui le opzioni (oggetti di classe JMenultem) possono
intervenuto etc.). Si rimanda alle API di Java per approfondimenti. essere scelte col mouse.

JCheckBox Un menu item a sua volta può essere un (sotto) menù in modo da creare menù concatenati. È possibile
I check box sono utilizzabili quando un numero limitato di scelte sono a disposizione dell’utente. Esempio: introdurre dei separatori tra sotto insiemi di item di uno stesso menù.

JCheckBox italiano=new JCheckBox("ltaliano"); Un menù item, quando è scelto, genera un evento di tipo ActionEvent. Per il corretto funzionamento della GUI
JCheckBox inglese=new JChecIBoxf'Inglese"); è necessario stabilire un ascoltatore di eventi azione (ActionListener) ed associarlo (metodo
italiano.addActionListener( ascoltatore_eventi_azione ); addActionListener()) ai vari JMenultem. Si può dire che i menù item sono in tutto simili a bottoni che
inglese.addActionListener( ascoltatore_eventi_azione ); compaiono solo quando si apre il corrispondente menù a tendina. Per esempi di strutture a menù si veda più
avanti.
La caratteristica dei check box è che possono essere “spuntati”. La spunta (click di mouse) genera un evento
azione che un opportuno ascoltatore di eventi azione può sentire e processare. Più check box possono essere Mentre un menù è aggiunto ad una menu bar ed occupa una posizione fissa sulla GUI, i pop-up menù (classe
spuntati contemporaneamente. JPopupMenu) non sono agganciati ad una menù bar e possono essere evocati per la scelta ad es. cliccando
col pulsante destro del mouse su una posizione del componente:
JRadioButton
Rappresentano elementi di interfaccia simili ai check box. La differenza è che i radio button vanno costituiti in JPopupMenu popup=new JPopupMenuQ;
gruppi. In un gruppo la selezione di un radio button è esclusiva rispetto agli altri radio button del gruppo: JMenultem itemi =new JMenultem("ltem1");
item1.addActionListener( actListener );
252 253
Capitolo 13 Programmazione di GUI

popup.add( itemi ); // e cosi via per altri item private String titolo=“Agendina GUI";
private String impl=" Map "; //default
component.setComponentPopupMenu( popup ); //aggancia il popup a component
public FinestraGUI(){//costruttore
popup.show( parent_component, x, y ); setTitle(titolo+impl);
setDefaultCloseOperation( JFrame. DONOTHING_ON_CLOSE );
chiede di mostrare il popup sul parent .component alla posizione indicata del parent component. addWindowListener( new WindowAdapterQ {
public void windowClosing(WindowEvent e){
Un caso di studio - La classe AgendinaGUI_______ if( consensoUscita() ) System.exit(O);
Per scopi dimostrativi è stata realizzata una GUI per il programma di gestione di un’agendina telefonica (cap.
8). La GUI consente preliminarmente di selezionare la particolare struttura dati da utilizzare per });
l’implementazione dell’agendina (la scelta è poi richiamata nel titolo della finestra). Dopo questo, si possono AscoltatoreEventiAzione listener=new AscoltatoreEventiAzioneQ;
evocare tutte le possibili operazioni sull'agendina mediante le opzioni del menu Comandi. Ogni comando si //creazione barra dei menu'
accompagna di norma ad una finestra di dialogo per l’introduzione dei relativi parametri. Ad es., l'aggiunta di JMenuBar menuBar=new JMenuBar();
un nominativo comporta l'inserimento del cognome, nome, prefisso e telefono del nominativo. this.setJMenuBar( menuBar );
_ a //creazione file menu
Agendina GUI Map JMenu fileMenu=new JMenu(”File"); menuBar.add(fileMenu);
//creazione voci del menu File
JMenu tipolmpl=new JMenufNuova"); fileMenu.add(tipolmpl);
tipoAL=new JMenultem(“ArrayLisf);
tipoAL.addActionListener(listener); tipolmpl.add(tipoAL);
tipoLL=new JMenultemf'LinkedList");
tipoLL.addActionListener(listener); tipolmpl.add(tipoLL);
Agendina GUI LL tipoSet=new JMenultem("Set“);
File C om andi j Help tipoSet.addActionListener(listener);
Aggiungi nominativo tipolmpl.add(tipoSet);
Rimuovi nom inativo tipoMap=new JMenultem("Map“);
N um ero nom inativi
tipoMap.addActionListener(listener);
Svuota agendina tipolmpl.add(tipoMap);
fileMenu.addSeparator();
Telefono di
apri=new JMenultem("Apri");
Persona di
apri.addActionListener(listener);
Elenco
fileMenu.add(apri);
salva=new JMenultem(“Salva");
salva.addActionListener(listener);
fileMenu.add(salva);
salvaConNome=new JMenultem("Salva con nome");
Di seguito si riporta una parte del costruttore della classe AgendinaGUI che illustra i dettagli di costituzione del salvaConNome.addActionListener(listener);
menu File. Per maggiori informazioni si rimanda al codice fornito a parte. fileMenu.add(salvaConNome);
fileMenu.addSeparator();
package poo.agendina; esci=new JMenultemfEsci");
import java.awt.*; esci.addActionListener(listener);
import java.awt.event.*; fileMenu.add(esci);
import javax.Swing.*; //creazione menu Comandi
import java.io.*;
class FinestraGUI extends JFrame{ //creazione menu Help
private File fileDiSalvataggio=null;
private JMenultem tipoAL, tipoLL, tipoSet, tipoMap, pack();
apri, salva, salvaConNome, esci, about, aggiungiNominativo, setLocation(200,200);
rimuoviNominativo, numeroNominativi, svuota, setSize(500,400);
telefonoDi, personaDi, elenco; }//costruttore
254 255
C ap ito lo 13^

Capitolo 14:
In neretto sono indicate le azioni di “aggancio” ai menu item dell'action listener, qui unico ed implementato Introduzione alle espressioni regolari
mediante una inner class della classe FinestraGUI. Le finestre per il settaggio dei campi dei vari comandi sono
create e rese visibili quando è necessario. Il codice completo dell'applicazione è fornito a parte. Le espressioni regolari definiscono un linguaggio le cui parole (simboli o stringhe) si possono specificare in
modo compatto utilizzando un insieme base di regole. Ad esempio:
La classe A g e n d i n a G U I 2 ____________
In questa variante della GUI per il programma di gestione dell’agentina telefonica, si è fatto uso di un gruppo di ”5[123](1-7]"
pulsanti radio button per la scelta della struttura dati di implementazione. La scelta ArrayList comporta
l’emissione di una finestra di dialogo per l’acquisizione della capacità iniziale dell'array. Si rimanda al codice rappresenta tutte le stringhe di tre cifre che iniziano con il carattere ‘5’, seguito poi da T o ‘2’ o ‘3,’ seguito
completo fornito a parte per tutti i dettagli realizzativi. infine da una cifra tra T e 7 ’ (intervallo).

Programma Agendina Telefonica _ □ Nelle ultime versioni di Java è possibile specificare una tale stringa come pattern (schema) di interesse da
"7 “

verificare, quindi chiedere poi se una certa stringa soddisfa il pattern oppure no.

String schema=”5[123][1-7]";
jScelta Tipo Implementazione dell Agendina String s;
- D
Agendina GUI AL
if( s.matches(schema) ) .. .//s soddisfa il pattern
Com andi Help
else ...//s non soddisfa il pattern
% A rrayL ist O LinkedList O Set Q M ap Stringa testo=...;
| Salva
Stringa testoModificato=testo.replaceAII( schema, );
Salva con nom e
Capacita* Array List - ° l
Esci che costruisce e ritorna una nuova stringa a partire da testo, nella quale tutte le occorrenze di schema sono
C apacita' 50 OK
sostituite da matches() e replaceAII() sono metodi della classe String.

Esempi di regex
[013] singola dira 0,1 o 3
[0-9][0-9] coppia di cifre da 00 a 99
A[0-4]b[05] stringa che inizia con A, è seguita una cifra tra 0 a 4, è seguita da b e terminata con 0o 5
[0-9&&[A4567]] singola cifra tra 0 e 9, escluse le cifre 4, 5, 6, 7. Dunque: 0, 1,2, 3, 8, 9
Esercizi _______________ ____________________________ [a-z][A-Z] coppia di lettere, la prima minuscola, la seconda maiuscola.
1. Sviluppare completamente l’applicazione munita di GUI relativa alla calcolatrice aritmetica suggerita in
questo capitolo. Come semplificazione, si suggerisce di ritenere tutti gli operatori equi-prioritari cosi da La notazione [Aabc] significa un solo carattere qualsiasi, tranne (negazione A) le lettere a, b o c.
valutare un’espressione strettamente da sinistra a destra. Prevedere un bottone “C” per annullare
l’espressione, “CE” per cancellare l’ultimo operando inserito. Di momento in momento, l’espressione immessa [a-z&&[Aaeiou]] specifica una consonante.
va mostrata nel text field Risultato. A fine valutazione (quando si clicca sul bottone “=’’) l’espressione nel text
field va sostituita con il suo risultato. Il simbolo * indica ripetizione 0,1 o più volte. Il simbolo + indica ripetizione 1 o più volte.La scrittura:
2. Realizzare una differente GUI per il programma di gestione di un’agendina telefonica che utilizzi pop up
menu per la scelta dei comandi. [a-zA-Z_][a-zA-Z0-9_$]‘

Altre letture ________________________ specifica la formazione di un identificatore Java (inizia con una lettera o un carattere di sottolineatura epuò
I concetti della programmazione in Java di GUI e più in generale di applicazioni grafiche possono essere continuare con un numero arbitrario di lettere, cifre, o '$’). Data una stringa, ad esempio estrattacon un
approfonditi, ad es., su: tokenizzatore, si può dunque verificare se essa costituisce un valido identificatore Java o meno.

C.S. Horstmann, G. Cornell, Core Java, Volumi I e II, 8,h Edition, Prentice-Hall, 2008. La scrittura:

S. Mazzanti, V. Milanese, Programmazione di applicazioni grafiche in Java, Apogeo, 2006. [\\-\\4][0-9][0-9]* o più semplicemente: [\\-\\+][0-9]+

descrive una costante intera con segno. Si noti che essendo i simboli *-* e V già dotati di significato, per
“forzare” il loro significato standard di caratteri segno si è usata la costruzione ’Wcar’ tipica delle sequenze di
escape all’interno di letterali stringa.
256 257
Capitolo 14 Espressioni regolari

Dal punto di vista Java, un letterale (costante) int in realtà non deve mai iniziare con il segno Se positivo, il String REALE=“\\-?([0-9]+l((0-9]+)?\\.[0-9]+)([Ee][\\-\\+]?[0-9]{1,3})?(DdFf]?-;
segno è omesso. Pertanto, una regex per un intero di Java si può esprimere come segue (si veda più sotto
per il significato del meta-simbolo ?): Il carattere punto V___________________ ____________________________________________________
Il carattere (metasimbolo) in un pattern stà per qualsiasi carattere diverso dal terminatore di linea ~(‘\n’ o V ).
"\\-?[0-9]+" Utilizzando il carattere punto assieme al carattere * si possono esprimere situazioni di pattern matching come
la seguente:
La scrittura
X{N} indica che il pattern in X si intende ripetuto esattamente N volte String documento=...;
(N è una costante intera) if( documento.matches(“.*programmazione in Java.*”) )
X{N,} indica che la ripetizione è almeno N volte //documento contiene la frase “programmazione in Java" strettamente all'Interno di linee
X{N,M) denota che la ripetizione è almeno N volte ed al più M volte
X{0,1} si può abbreviare come X? dove ? denota 0 o 1 ripetizioni
del simbolo a sinistra X. Il significato di default del carattere 7 può essere cambiato, se richiesto, mediante azioni esplicite (si veda più
avanti la classe Matcher).
Un numero di telefono del tipo: prefisso massimo a 4 cifre, seguito da quindi da un numero di telefono che
al minimo è costituito da tre cifre e al massimo 7 cifre si può schematizzare come: Abbreviazioni _______
La scrittura \d indica una cifra tra 0 e 9, \w indica un carattere di parola (word), ossia una lettera minuscola o
[0-9]{2,4}-[0-9]{3,7) cioè: "[0-9]{2,4}\\-[0-9]{3,7}" maiuscola, una cifra o il carattere di sottolineatura _, \s indica un carattere spazio (includendo ' ', tab (\t), fine
linea (\n) etc.). \D indica un carattere non cifra, \W un carattere non di word, \S un carattere non spazio.
Per una targa automobilistica italiana si ha:
Naturalmente, quando tali abbreviazioni sono utilizzate in una stringa, occorre raddoppiare il backslash \ per
[A-Z]{2}[0-9J{3][A-Z]{2} recuperare il significato della costruzione. Ad esempio, interi e reali Java si sono esprimere anche con i
pattern:
Gruppi __________ ______
Per situazioni più complesse è possibile utilizzare oltre alle parentesi quadre, le tonde. Mentre le quadre String INTERO='\\-?[\\d]+“
indicano alternative di singoli caratteri, le tonde sono utili per esprimere, con la barra verticale T, alternative di
gruppi di caratteri. String REALE="\\-?([\\d]+l([\\d]+)?\\.[\\d]+)([Ee][\\-\\+]?[\\d){ 1,3})?“;

(maximin) imum Le classi Pattern e Matcher del package java.util.regex


La verifica che una stringa obbedisca ad un cerio pattern è detta pattern-matchmg. Per poterla eseguire, viene
(AZICAICO)[0-9]{4) prima costruita una rappresentazione in memoria (struttura dati) del pattern e su questa lavora l’algoritmo di
pattern matching.
specifica la coppia (gruppo) AZ o CA o CO, seguita da quattro cifre.
Quando la verifica di un certo pattern va ripetuta più volte in un programma, si possono velocizzare le
Espressione regolare di un numero reale Java operazioni creando una sola volta la struttura dati e ri-utilizzandola per le verifiche. Rispondono a queste
Una costante reale può iniziare col segno se negativo, può ammettere una parte intera, può avere una esigenze le classi Pattern e Matcher che possono essere utilizzate in sostituzione ai metodi matches() e
parte frazionaria, può avere un fattore di scala annunciato dalla lettera E o e , seguita da un esponente che replaceAIIQ della classe String.
può avere segno V o nulla (positivo di default), lungo al massimo tre cifre, e può avere il suffisso ‘D’ o ‘d’
per doublé, ’F' o T per float. Esempi di letterali reali sono: Pattern pattern=Pattern.compHe( regex );
regex è una stringa regular expression, ossia un pattern
-54 numero intero/reale
35E2 significa: 35‘ 102=3500, numero reale floating point (il fattore di scala Matcher matcher-pattern.matcher( str );
consente di recuperare la posizione del punto decimale) str è la stringa da verificare su regex
.321 parte fazionaria pura (fixed point)
0.476 parte frazionaria pura matcher. matchesf);
23.451 numero reale fixed point esegue il pattern matching e ritorna true se str segue il pattern regex, false altrimenti
-13.47E-2 numero floating point, equivalente a: -0.1347
OD 0 doublé matcher. replaceAli( replacement );
ritorna una nuova stringa in cui con tutte le occorrenze di regex in str sono sostituite dalla stringa
Tutte queste possibilità sono catturabili in una regex come segue: replacement.
258 259
Capitolo 14 Espressioni regolari

while( matcher.find() ) {
Il metodo find() di Matcher è utile per trovare tutte le occorrenze di pattern matching. Ad ogni singolo match cont++;
(riscontro) si può conoscere dove il match comincia (metodo start() di Matcher, che ritorna la posizione del System.out.println(“Trovato match ***"+documento.substring( matcher.start(), matcher.end())+......+
carattere in cui inizia il match) e dove finisce (metodo end() di Matcher, che ritorna la posizione del primo “ in posizione n+matcher.start() );
carattere subito dopo il match). }
System.out.println( "Numero di match: "+cont );
Opzioni utili del metodo compile() d[ Pattern^__ System.out.println( “Documento dopo replaceAII di java con JAVA" );
Pattern.compile(“Java”, Pattern. CASE_ INSENSITIVE); documento=matcher.replaceAII( “JAVA" );
chiede di ignorare il caso delle lettere durante il pattern matching System.out.println( documento );
}//main
Pattern.compilef regex, Pattern.DOTALI ); }//TestRegex
generalizza il comportamento di 7 in modo da includere anche i fine linea.
Esercizi
Caso di studio:jin programma di pattern matching 1. Mostrare un'espressione regolare in cui un numero pari di ‘a’ è seguito da un numero dispari di b’ seguite
Si legge da tastiera il nome di un file di tipo testo, e si ricercano in esso tutte le occorrenze di “java” da un numero arbitrario di ‘c’.
indipendentemente dal caso delle lettere. Infine si mostra come cambia il contenuto del file quando tutti gli usi 2. Definire un’espressione regolare per il riconoscimento (condizione necessaria) di un codice fiscale.
di “java” sono sostituiti dalla stringa “JAVA". 3. Definire un’espressione regolare per il riconoscimento di espressioni aritmetiche nelle quali gli operandi
sono interi senza segno e gli operatori sono i caratteri +, -, *, /. Il primo simbolo dev’essere un operando, cui
package poo.regex; possono seguire i simboli operatore operando un numero arbitrario di volte.
import java.util.*; 4. Leggere una linea di un testo fornita da input e classificare e visualizzare i simboli che denotano
import java.io.*; identificatori Java e quelli che rappresentano numeri interi o numeri reali. L’output potrebbe essere
import java.util.regex.*; organizzato come segue:

public class TestRegex{ 125 intero


public static void main( String [jargs ) throws lOExceptionf aitai identificatore
Scanner sc=new Scanner( System.in ); 3.56e-3 reale
String nomefile=null;
boolean okLettura=false; 5. Leggere il contenuto di una matrice a 5x5 di interi a partire da linee di input ciascuna delle quali contiene un
do{ singolo elemento della matrice, specificato secondo il formato:
System.out.printfFornisci il nome di un file testo: “);
nomefile=sc.nextLine(); i=riga#j=colonna#v=valore
File f=new File( nomefile );
okLettura=f.exists(); I tre campi i=, j=, v= si possono presentare in realtà in un qualunque ordine. Ad es. si può fornire prima l’indice
if( lokLettura ) System.out.printlnfFile inesistente!"); di colonna (j=) poi il valore (v=) e poi l’indice di riga (i=) etc. Non sono ammessi spazi tra un campo ed il
}while( lokLettura ); successivo. Definire un'espressione regolare per verificare la corretta formazione di ciascuna linea di input e
quindi assegnare il valore all’elemento a[i][j].
BufferedReader br=new BufferedReader( new FileReader( nomefile ) );
//ottieni stringa dal contenuto del file Altre letture
StringBuilder sb=new StringBuilder(IOOO); Maggiori informazioni sul supporto delle espressioni regolari in Java, possono essere reperite su:
for(;;){
String linea=br.readLine(); C.S. Horstmann, G. Cornei, Core Java, Voi. Il-Advanced Features, 8"’ Edition, Prentice Hall, 2008.
if( linea==null ) break;
sb.append( linea ); sb.append(\n'); M. Habibi, Java Regular Expressions: Taming thè java.util.regex Engine, Apress, 2004.
}
br.close();
String documento=sb.toString();
Pattern pattern=Pattern.compile( “java", Pattern.CASE .INSENSITIVE );
Matcher matcher=pattern.matcher( documento );
int cont=0;

260 261
Capitolo 15:_____
Lista concatenata

Per evitare gli appesantimenti legati agli spostamenti di elementi durante le operazioni di inserimento e
rimozione su un array (si riveda l’implementazione della classe ArrayVector nel cap. 8) è utile il concetto di
lista concatenata o lista a puntatori. Si tratta di un’organizzazione alternativa all’array per la memorizzazione di
una sequenza di dati. La novità consiste nel fatto che ogni elemento di una lista concatenata è incapsulato in
un nodo che contiene, oltre airinformazione, anche il riferimento esplicito (puntatore) al nodo successivo.
Elementi consecutivi della sequenza non risiedono più necessariamente in locazioni contigue di memoria
come accade per l’array. Lo schema concatenato introduce flessibilità ma anche limitazioni:
• non occorre più dimensionare la lista, ciò che facilita la sua espansione/contrazione durante l'esecuzione
del programma
• si possono portare a termine le operazioni di inserimento e rimozione attraverso poche modifiche di
puntatori, ossia ridefinendo la nozione di elemento successivo ad alcuni nodi
• non è più naturale l’algoritmo della ricerca binaria non essendo possibile “calcolare" una posizione mediana
e collocarsi direttamente su di essa.

Un esempio di lista concatenata ordinata di interi è mostrato di seguito:

nodo capolista elemento di coda


o testa della lista della lista

Dovendo aggiungere l'elemento x=6 a tale lista, occorre preliminarmente verificare che esso va collocato tra il
nodo contenente il 2 ed il nodo contenente il 9, quindi è sufficiente: a) creare un nuovo nodo nel cui campo
informazione si pone 6; b) “aggiustare" i puntatori come mostrato nella prossima figura:

nuli

Anche un’operazione di rimozione può essere eseguita modificando semplicemente dei campi puntatore. Si
consideri l’eliminazione dell'elemento 9 dalla lista precedente. Occorre prima individuare il nodo con il 9, quindi
cambiare la relazione "prossimo nodo” come mostrato di seguito:

Gestione di una lista in Java


Per introdurre e gestire una lista concatenata in Java occorre preliminarmente caratterizzare il generico nodo-
elemento come classe e quindi utilizzare la nozione di riferimento (o puntatore) già disponibile nel linguaggio.
Ad es. per una lista di interi si può introdurre la classe:

263
Capitolo 15 Lista concatenata

Il metodo contiene() può essere facilmente “ottimizzato" in modo da sfruttare l’ordinamento degli elementi.
class Nodo{ Quando si incontra, infatti, un elemento non minore di quello cercato x, si può interrompere il ciclo di ricerca:
int info;
Nodo next; public boolean contiene( int x ){
}//Nodo Nodo cor=inizio;
while( cor!=null && cor.info<x )
in cui per semplicità i due campi info e next sono stati resi accessibili in modalità package e sono stati omessi cor=cor.next;
metodi e costruttori. La classe Nodo non richiede di essere visibile all’esterno del file in cui è dichiarata. Si if( cor!=null && cor.info==x )
tratta di una classe di supporto ad una classe che implementa la lista concatenata, es. ListaDilnteri: return true;
return false;
package poo.lista; }//contiene
public class ListaDilnterif
private static class Nodo{//inner class “sganciata" dal this di ListaDilnteri Si noti che si può uscire dal ciclo di while o perchè cor è nuli (lista vuota o si è oltrepassato l’ultimo elemento)
int info; o perché nel nodo corrente si trova l'informazione x o tale informazione è non minore di x. Fuori ciclo si tratta
Nodo next; di eseguire un ulteriore test (si noti l’uso del corto circuito) per concludere la ricerca. Seguono i metodi
}//Nodo toString() ed equals() che realizzano sempre delle scansioni del contenuto della lista:
private Nodo inizio=null; //puntatore di testa
public ListaDilnteri( ListaDilnteri l){...} //costruttore di copia public String toString(){
public int cardinalita(){...} StringBuilder sb=new StringBuilder ();
public boolean contiene( int x ){...} sb.append('[‘);
public void inserisci int x ){...} Nodo cor=inizio;
public void rimuovi( int x ){...} while( cor!=null ){
public String toString(){...} sb.append(cor.info);
public boolean equals( Object o ){...} cor=cor.next; //avanza
}//ListaDilnteri if( cor!=null )
sb.appendf, “); //aggiungi separatore
Nell’ipotesi di disporre di una lista già costruita, ossia popolata di elementi, quello che segue è un raffinamento }
del metodo cardinalita (size) che ritorna il numero di elementi presenti nella lista: sb.append('l');
return sb.toString();
public int cardinalita(){ }//toString
int card=0;
Nodo cursore=inizio; public boolean equals( Object o )(
while( cursore!=null ){ if( !(o instanceof ListaDilnteri) ) return false;
card++; //conta questo nodo if( o==this ) return true;
cursore=cursore.next; //avanza ListaDilnteri l=(ListaDilnteri)o;
} if( this.cardinalita() != I.cardinalita() ) return false;
return card; Nodo cor1=inizio, cor2=l.inizio;
}//cardinalita while( cori !=null )(
if( cor1.info!=cor2.info )
Il metodo utilizza un cursore (puntatore) inizializzato a puntare alla testa della lista. Quindi realizza un ciclo dal return false;
quale si esce quando il cursore vale nuli, ossia è stata raggiunta la fine della lista. Ad ogni iterazione, si cor1=cor1.next;
incrementa la cardinalità e si assegna a cursore il campo next del nodo corrente. Segue una prima versione cor2=cor2.next;
del metodo contiene() che ritorna true se un elemento x appartiene alla lista. Si tratta di una ricerca lineare: }
return true;
public boolean contiene( int x )( }//equals
Nodo cor=inizio;
while( cor!=null && cor.info!=x ) cor=cor.next; Il metodo toString() raccoglie tra una coppia di '[* e ’]’ gli elementi della lista, separandoli con “, “ (virgola e
return cor!=null; spazio). Due liste di interi sono ritenute uguali se hanno la stessa cardinalità e le coppie di elementi nelle
}// contiene stesse posizioni sono uguali.

264 265
Capitolo 15 Lista concatenata

Il metodo inserisci()
Il metodo distingue i 4 possibili casi:
1. inserimento in lista vuota
2. inserimento prima del primo elemento (inserimento in testa)
3. inserimento in un punto intermedio
4. inserimento dopo l’ultimo elemento (inserimento in coda).

In realtà i 4 casi sono riconducibili a due dal momento che 1. e 2. costituiscono sempre un inserimento in testa
e 3. e 4. sono comunque assimilabili aH’inserimento in un punto intermedio.

public void inserisci int x ){


inserimento di x=5 in un punto intermedio
if( inizio==null II inizio.info>=x ){
//casi 1. e 2.: inserimento in testa
Nodo nuovo=new Nodo(); L'inserimento in coda ricade nella casistica discussa tenendo presente che un’informazione maggiore di quella
nuovo.into=x; nuovo.next=inizio;inizio=nuovo; dell’ultimo nodo determinerà una ricerca lineare al cui termine cor sarà fuori della lista (cor==null) e pre
} punterà all’ultimo nodo:
else{
//casi 3. e 4.: inserimento in un punto intermedio o
//in coda, ossia dopo l'ultimo l’elemento
Nodo cor=inizio, pre=null;
while( cor!=null && cor.info<x ){
pre=cor; cor=cor.next;
}
//inserisci x tra pre e cor
Nodo nuovo=new Nodo();
nuovo.info=x; nuovo.next=cor;
pre.next=nuovo;
}
}//inserisci
Poiché quando ricorre un caso tra 3. e 4. la lista certamente non è vuota e l’elemento va posto sicuramente
I casi 1. e 2. sono identificati dal test inizio==null (lista vuota) oppure inizio.info>=x (lista non vuota e nel primo dopo il primo, si possono inizializzare le variabili pre e cor anche come segue:
elemento si trova un’informazione non minore di x). È sufficiente creare un nuovo nodo, porre in esso il valore
x, e fare in modo che esso diventi il nuovo capolista, ossia definire come suo successore il precedente (se Nodo cor=inizio.next, pre=inizio;
esiste) primo elemento della lista e dire che la lista parte da nuovo. Queste operazioni sono descritte
graficamente rispettivamente nelle figure che seguono, in cui il simbolo di “messa a terra" rappresenta il nuli. cosi evitando una girata (inutile) del ciclo di while. Il metodo inserisci può essere riscritto equivalentemente
come segue. La nuova formulazione realizza in ogni caso il ciclo di ricerca e individua i vari casi all’uscita dal
ciclo:

public void inserisci int x ){


Nodo cor=inizio, pre=null;
while( cor!=null && cor.info<x ){
pre=cor; cor=cor.next;
in se rim e n to di x - 6 in se rim e nto di x = 2 in una lista in cui
in lista vu o ta II capo lista contien e 4 }
//inserisci il nuovo nodo tra pre e cor
Per i casi 3. e 4. è necessario identificare prima la posizione di inserimento attraverso un'operazione di Nodo nuovo=new Nodo(); nuovo.info=x; nuovo.next=cor;
ricerca. Si utilizzano due puntatori di scansione, cor e pre, che rispettivamente puntano al nodo corrente ed al if( cor==inizio ) inizio=nuovo;
suo precedente. La necessità di due cursori anziché uno è giustificata dal fatto che per portare a termine else pre.next=nuovo;
l'inserimento occorre conoscere prima di quale nodo va inserita la nuova informazione (nodo puntato da cor) e }//inserisci
a quale nodo esso deve seguire (nodo puntato da pre).
Si nota che, se all’uscita dal while cor==inizio, allora ci si è fermati sulla testa della lista o la lista è vuota. In
entrambe le situazioni occorre fissare l’inizio della lista sul nuovo nodo. Se cor non è nuli allora esiste
266 267
Capitolo 15 Lista concatenata

certamente pre che punta al nodo precedente cor e dunque si deve collegare il nuovo nodo come successore cor=inizio; pre=null;
di pre (pre.next=nuovo). while( cor!=null ) { pre=cor; cor=cor.next;} //trova posizione di coda
if( cor==inizio ) inizio=nuovo;
Il metodo rimuovi!) else pre.next=nuovo;
L’operazione di rimozione di un elemento richiede preliminarmente la ricerca della posizione (nodo) in cui p=p.next; //avanza su /
risiede l’obiettivo da cancellare. Anche qui sono necessari due puntatori di scansione in quanto, in generale,
per portare a termine la rimozione occorre ridefinire la relazione di successore sul nodo precedente a quello
da eliminare.
Si può evitare il ciclo che individua la coda della lista this dopo cui va inserito il nuovo elemento proveniente
public void rimuovi( int x ){ dalla lista /, mantenendosi esplicitamente il puntatore di coda come segue:
Nodo cor=inizio, pre=null;
while( cor!=null && cor.info<x )( public ListaDilnteri( ListaDilnteri I )(
pre=cor; cor=cor.next; Nodo coda=null, p=l.inizio;
} while( p!=null ){
if( cor!=null && cor.info==x ){ Nodo nuovo=new Nodo();
if( cor==inizio) //caso 1.: rimozione dalla testa nuovo.info=p.info; nuovo.next=null;
inizio=cor.next; //inserimento di nuovo in coda
else //caso 2.: rimozione da un punto intermedio o dalla coda if( inizio==null ) inizio=nuovo;
pre.next=cor.next; else coda.next=nuovo;
coda=nuovo; //nuova coda
}//rimuovi p=p.next; //avanza su I
}
Casi della rimozione:
Sono tre e sono sintetizzati nelle figure che seguono:
Un metodo main()
Il programma legge una successione di interi da tastiera, terminata un numero negativo, e si inseriscono i
numeri in ordine in un oggetto lista. Successivamente si realizzano alcune operazioni di interrogazione sulla
lista creata:

public static void main( String[] args ){//inserito nella classe ListaDilnteri
Scanner sc=new Scanner( System.in );
ListaDilnteri lista=new ListaDilnteri();
for(;;){ //legge una successione di interi sino al primo <0
int x=sc.nextlnt();
if( x<0 ) break;
2 da un punto intermedio lista.inserisci( x );
}
System.out.println("cardinalita="+lista.cardinalita());
System.out.println( lista );
if( cerca(2) ) lista.rimuovi(2);
System.out.println("cardinalita="+lista.cardinalita());
3. dalla coda System.out.println( lista );
}//main
Costruttore di copia
NulIPointerException
public ListaDilnteri( ListaDilnteri /){ Lavorando su una lista concatenata, l’eccezione in cui ci si può imbattere è la NulIPointerException (erede di
Nodo cor=null, pre=null, p=/.inizio; RuntimeException), che è analoga alla IndexOutOfBoundsException degli array. Essa, per altro, è generale
while( p!=null ){ dereferenziando un oggetto a partire da nuli.
Nodo nuovo=new Nodo();
nuovo.info=p.info; nuovo.next=null; NulIPointerException insorge quando si cerca di accedere ad un nodo ma il riferimento che si possiede è nuli.
//inserimento di nuovo in coda Se si rivedono i metodi della classe ListaDilnteri, dopo aver fatto ad es. un ciclo di ricerca con un cursore cor,
268 269
Lista concatenata
C ap ito lo 15

all’uscita dal ciclo ci si cautela verificando che cor!=null prima di controllare di aver trovato effettivamente public boolean contains( T elem ){
l’elemento: cor.info==x. Una condizione del tipo: for( T e: this ){
if( e.equals(elem) ) return true;
if( cor!=null && cor.info==x )... if( e.compareTo(elem)>0 ) return false;
}
consente appunto di evitare il NulIPointerException se al momento del test cor effettivamente è nuli (x non return false;
esiste sulla lista). In modo analogo, si dovrebbe prestare attenzione all’uso del puntatore pre che in certe }//contains
situazioni è sicuramente nuli e dunque usarlo per operazioni tipo: pre.next=nuovo, darebbe luogo
all'eccezione. Ad es., se una rimozione riguarda l’elemento di testa della lista, cor punta alla testa e pre è nuli. public boolean isEmpty(){
È un errore usare pre in queste circostanze. return !iterator().hasNext();
}//isEmpty
Caso di studio: progetto di una collezione ordinata
La classe ListaDilnteri può essere agevolmente generalizzata nel tipo degli elementi attraverso l ’introduzione public boolean isFull(){ return false; }//isFull
di un’interfaccia (ADT) come quella che segue. Il significato delle varie operazioni dovrebbe essere auto­
esplicativo. Il metodo get() riceve un elemento (per scopi di ricerca) e ritorna il primo elemento della lista public T get( T elem ){
uguale (nel senso di equals()) al parametro. for( T x: this ){
if( x.equals(elem) ) return x;
public interface CollezioneOrdinata<T extends Comparable<? super T » extends lterable<T> { if( x.compareTo(elem)>0 ) return nuli;
int size(); }
boolean contains( T elem ); return nuli;
T get( T elem ); }//get
boolean isEmpty();
boolean isFull(); public void remove( T elem ){
void clear(); lterator<T> it=this.iterator();
void add( T elem ); while( it.hasNext() ){
void remove( T elem ); T e=it.next();
}//CollezioneOrdinata if( e.equals( elem ) ) { it.remove(); break;}
}
Segue una classe astratta che implementa Collezioneordinata e concretizza quanti più metodi è possibile, }//remove

public abstract class @SuppressWarnings("unchecked")


CollezioneOrdinataAstrattacT extends Comparable<? super T » implements CollezioneOrdinata<T>{ public boolean equals( Object o ){
if( !(o instanceof Collezioneordinata) ) return false;
public int size(){ if( o==this ) return true;
int c=0; CollezioneOrdinata<T>s=(CollezioneOrdinata)o; //warning di conversione soppresso
for(T e: this) c++; if( s.size()!=this.size() ) return false;
return c; lterator<T> it1=this.iterator(), it2=s.iterator();
}//size while( it1.hasNext() ){
T e1 = it1.next(); T e2 = it2.next();
public void clear(){ if( !e1 ,equals(e2) ) return false;
lterator<T> i=this.iterator(); }//while
while( i.haslslextQ ) { return true;
i.next(); }//equals
i.remove();
} public int hashCode(){
}//clear int p=43, h=0;
for( T e: this ) h=h*p+e.hashCode();
return h;
}//hashCode

270 271
Capitolo 15 Lista concatenata

public String toString(){ else{//inserimento dopo il primo e prima dell'ultimo


StringBuilder sb=new StringBuilder(200); Nodo<T> cor=first.next, pre=first;
sb.append('['); while( cor!=null && cor.info.compareTo(elem)<0 ){
lterator<T> it=iterator(); pre=cor; cor=cor.next;
while( it.hasNext() ){ }
sb.append( it.next() ); //inserimento di nuovo tra pre e cor
if( it.hasNext() ) sb.append(','); nuovo.next=cor;
} pre.next=nuovo;
sb.append(']'); }
return sb.toString(); size++; //conta questa aggiunta
}//toString }//add

}//CollezioneOrdinataAstratta public void remove( T elem ){


Nodo<T> cor=first, pre=null;
È utile riflettere che le implementazioni di metodi della classe astratta CollezioneOrdinataAstratta possono while( cor!=null && cor.info.compareTo(elem)<0 ){
essere riviste o meno da una sottoclasse concreta. In particolare, un erede concreto potrebbe ridefinire pre=cor; cor=cor.next;
(overriding) un metodo implementato dalla classe astratta ad es. per ragioni di efficienza. Classico è il metodo }
size(). Nella super classe astratta tale metodo è costruito sulla base dell’iteratore. Se una classe erede if( cor!=null && cor.info.equals(elem) ){//elem è trovato nel nodo cor
introduce e mantiene un campo size, allora la ridefinizione di size() potrebbe essere conveniente in quanto si jf( cor==first ){
riduce a restituire il valore dell'attributo size e dunque evita un’attraversamento della collezione. Ad ogni modo, first=first.next;
in sede di prima stesura (prototipazione) di una classe erede, si potrebbero evitare talune ridefinizioni al fine di if( first==null ) last=null;
ottenere “rapidamente" una versione eseguibile. Successivamente, si possono ridefinire specifici metodi. }
else if( cor==last ){//ultimo nodo
Una classe ListaOrdinataConcatenata<T>:_____________________ last=pre;
Si mantengono testa (tirsi) e coda (lasf) della lista per velocizzare alcune operazioni. Ci si limita last.next=null;
essenzialmente a realizzare i metodi astratti. }
else{
public class pre.next=cor.next;
ListaOrdinataConcatenatacT extends Comparable<? super T»extends CollezioneOrdinataAstratta<T> { }
size--; //conta questa rimozione
private static class Nodo<E>{ //classe inner estatica per efficienza }
E info; }//remove
Nodo<E> next;
}//Nodo public lterator<T> iterator(){ return new lteratore(); }//iterator

private Nodo<T> first=null, last=null; private class Iteratore implements lterator<T>{


private int size=0; private Nodo<T> cor=null, pre=null;
public int size(){ return size;} //cor o è nuli o punta all'ultimo nodo già visitato
public void add( T elem ){
Nodo<T> nuovo=new Nodo<T>(); public boolean hasNext(){
nuovo.info=elem; if( cor==null ) return first!=null;
if( first==null II first.info.compareTo(elem)>=0 ){//inserimento in testa return cor.next!=null;
nuovo.next=first; }//hasNext
if( first==null ) last=nuovo;
first=nuovo; public T next(){
} if( !hasNext() ) throw new NoSuchElementException();
else if( last.info.compareTo(elem)<=0 ){//inserimento in coda if( cor==null ) cor=first;
nuovo.next=null; else{ pre=cor; cor=cor.next;}
last.next=nuovo; return cor.info;
last=nuovo; }//next

272 273
Capitolo 15
Lista concatenata

public void remove(){ public abstract class StackAstratto<T> implements Stack<T>{


if( pre==cor ) throw new IHegalStateException(); public int size(){
if( cor==first ){ int c=0;
first=first.next; for( lterator<T> it=iterator(); it.hasNext(); it.next(), c++ );
if( first==null ) last=null; return c;
} }//size
else if( cor==last ) { last=pre; last.next=null;}
else{ pre.next=cor.next;} public void clear(){
cor=pre; //arretra cor while( !this.isEmpty() ) this.pop();
size--; }//clear
}//remove
public T top(){
}//lteratore lterator<T> it=iterator();
if( !it.hasNext() )
}//ListaOrdinataConcatenata throw new RuntimeException(“Stack vuoto!");
T elem=it.next();
La struttura di iterazione di ListaOrdinataConcatenata è affidata ad una inner class Iteratore che utilizza due return elem;
cursori cor e pre. Le assunzioni sono che cor può valere nuli (cor si trova prima del primo elemento o la lista è }//top
vuota) o diverso da nuli nel qualcaso punta ad un nodo già “consumato” (restituito). Come il nome suggerisce,
cor riferisce il nodo corrente che è influenzato da un'eventuale operazione di rimozione. Dopo una rimozione public T pop(){
cor è arretrato al valore pre. Tutto ciò consente di scoprire il tentativo di eseguire due remove consecutive lterator<T> it=iterator();
(cor==pre) e sollevare l’eccezione HlegalStateException. if( !it.hasNext() )
throw new RuntimeExceptionfStack vuoto!");
Stack ADT e gerarchia di classi T elem=it.next();
È noto che lo stack è una collezione di elementi in cui le operazioni avvengono ad uno stesso estremo detto it.remove();
cima (o top) dello stack (gestione LIFO - Last Input First Output), come segue: return elem;
}//pop
• push( elem ) pone elem in cima allo stack, solleva un'eccezione se lo stack ha dimensione limitata ed è
pieno al tempo della push public boolean isEmpty(){
• T pop() rimuove e restituisce l’elemento in cima, solleva un'eccezione se lo stack è vuoto return !iterator().hasNext();
• T top() ritorna l’elemento in cima allo stack senza rimuoverlo, solleva un'eccezione se lo stack è vuoto. }//isEmpty

Una nozione generale di stack, iterabile, è definita dalla seguente interfaccia: public boolean isFull(){ return false;}

package poo.util; public String toString(){


public interface Stack<T> extends lterable<T>{ StringBuffer sb=new StringBuffer(IOO);
int size(); sb.append(’C);
void clearQ; lterator<T> it=iterator();
void push(T elem); while( it.hasNextQ ){
T pop(); sb.append( it.next() );
Ttop(); if( it.hasNext() ) sb.append(',');
boolean isEmpty(); }
boolean isFullQ; sb.append(']');
}//Stack return sb.toString();
}//toString
Una classe StackAstratto<T>
package poo.util; @SuppressWamingsfunchecked")
import java.util.*; public boolean equals( Object o ){
if( !(o instanceof Stack) ) return false;
if( o==this ) return true;
Stack<T> s=(Stack)o;
274 275
C apito lo 15 Lista concatenata

if( s.size()!=this.size() ) return false; return e;


lterator<T> it1=this.iterator(); }//pop
lterator<T> it2=s.iterator();
while( it1.hasNext() ){ ©Override
T e1 = it1.next();T e2 = it2.next(); public int size(){ return size;}
if( !e1.equals(e2) ) return false;
}//while public lterator<T> iterator()( return new Stacklterator();}
return true;
}//equals private class Stacklterator implements lterator<T>{
private Nodo<T> cor=null, pre=null;
public int hashCode(){ public boolean hasNext(){
int p=43, h=0; if( cor==null ) return testa!=null;
for( T e: this ) return cor.next!=null;
h=h*p+e.hashCode(); }//hasNext
return h; public T next(){
}//hashCode if( !hasNext() )
throw new NoSuchElementException();
}//StackAstratto if( cor==null ) contesta;
else{ pre=cor; cor=cor.next;}
Nel metodo size(), l'uso dell’iteratore esplicito e l’incremento della variabile c nella sezione “passo” del ciclo di return cor.info;
for, fa sì che il corpo del for si riduca aH’islruzione nulla (denotata dal solo Il "passo” consiste di due }//next
istruzioni separate da 7. public void remove(){
if( cor==pre ) throw new HlegalStateExceptionQ;
Una classe StackConcatenatocT>:____________ if( cor==testa ) testa=testa.next;
package poo.util; else pre.next=cor.next;
import java.util.*; cor=pre; size-;
}//remove
public class StackConcatenato<T> extends StackAstratto<T>{ }//Stacklterator
private static class Nodo<E>{
E info; }//StackConcatenato
Nodo<E> next;
}//Nodo Uno stack concatenato costituisce un caso particolare della lista concatenata in cui le operazioni di gestione
private Nodo<T> testa=null; avvengono sempre in testa (top dello stack). Si nota che tramite l’iteratore è possibile ispezionare e rimuovere
private int size; un qualsiasi elemento, indipendentemente dalla logica UFO. Di seguito si riporta una classe StackArray che
memorizza lo stack su un array non scalabile di elementi. Il primo buco libero è indicato da size, che funziona
public void clear(){ come "cima" dello stack.
testa=null; size=0;
}//clear Una classe StackArrayeTx___________________________________________________________________
package poo.util;
@Override import java.util.*;
public void push( T x )(
Nodo<T> n=new Nodo<T>(); public class StackArray<T> extends StackAstratto<T>{
n.info=x; n.next=testa; private int size=0;
testa=n; size++; private T[] array;
}//push
©SuppressWarningsfunchecked")
public T pop(){ public StackArray( int n ){
if( testa==null ) if( n<=0 ) throw new IHegalArgumentException();
throw new RuntimeException("Stack empty!"); array=(T[]) new Object[n);
T e=testa.info;testa=testa.next;
size--;
276 277
Capitolo 15 Lista concatenata

public void clear(){ All'atto della rimozione di un elemento, oltre a provvedere a spostare di un posto a sinistra gli elementi che
for( int i=0; i<size; ++i ) array[i]=null; seguono quello rimosso, si decrementa size e si pone a nuli la cella dell’elemento spostato per facilitarne il
size=0; rilascio al garbage collector. In Stacklterator, il cursore è rappresentato dalla variabile cor che o vale size (il
}//clear cursore si trova prima del primo elemento in posizione size-1) o indica una cella il cui valore è già stato
restituito.
©Override
public void push( T x ){ Un'applicazione di test per lo stack:
if( size==n ) throw new RuntimeException(“Stack full!"); Si legge una stringa dei tipo siringa 1$stringa2e si vuole verificare se stringa2 è uguale ed opposta a stringai,
array[size]=x; ossia se stringai seguita da stringa2 formano una successione palindroma.
size++;
}//push Es. assoSossa è palindroma, assoSasso non è palindroma.

public T pop(){ Una possibile soluzione consiste nel caricare i caratteri di stringai su uno stack di caratteri, sino aH'arrivo della
if( isEmpty() ) throw new RuntimeExceptionfStack empty!"); marca $ (che non va sullo stack). Da questo momento in poi, ogni carattere di stringa2 che arriva dev’essere
size—; uguale a quello affiorante sullo stack. Se cosi è, si elimina l’elemento in cima allo stack e si prosegue sino a
T e= array[size]; array[size]=null; che non termina stringa2. A questo punto, se lo stack è vuoto, l’input è palindromo.
return e;
}//pop Naturalmente, l’input è malformato se non soddisfa il pattern stringai$stringa2. A questo proposito, il
programma che segue fa uso di una semplice espressione regolare per rigettare immediatamente un input
©Override malformato (attenzione che il carattere ’$’ è già dotato di significato nelle regex -indica un fine linea- per cui
public int size(){ return size; }//size esso è introdotto come \\$).

©Override package poo.esempi;


public boolean isFull(){ return size==array.length;}//isFull import java.util.Scanner;
import poo.util.*;
©Override public class VerificaPalindromaf
public lterator<T> iterator(){ return new Stacklterator();} public static void main( String(] args ){
String FORMATO_INPUT="[\\w]+\\$[\\w]+";
private class Stacklterator implements lterator<T>{ Stack<Character> pila=new StackConcatenato<Character>();
private int cor=size; Scanner sc=new Scanner( System.in );
private boolean rimuovibile=false; System.out.printlnfFornisci una linea del tipo: stringa 1$stringa2“);
public boolean hasNext(){ String input=sc.nextLine();
return cor>0;
}//hasNext if( !input.matches(FORMATO.INPUT) ){
public T next(){ System. out.printlnfStringa malformata");
if( !hasNext() ) throw new NoSuchElementException(); System.exit(-I);
cor--; }
rimuovibile=true;
return array[cor]; int pos=0;
}//next for(;;){
public void remove(){ char c=input.charAt(pos); pos++;
if( Irimuovibile ) throw new IHegalStateException(); if( c!='$' ) pila.push( c );
rimuovibile=false; else break;
for( int i=cor; i<size-1; ++i ) }//for
array[i]=array[i+1];
size-; array[sizej=null; boolean flag=true;
}//remove for(;;){
}//Stacklterator if( pos==input.length() Il pila.isEmpty() ) break;
char c=input.charAt(pos);
}//StackArray char x=pila.pop();
if( c!=x ){flag=false; break;}
278 279
C ap ito lo 15^ Lista concatenata

pos++; public void clear(){


}//for while( lisEmptyO ) this.get();
}//clear
if( flag && pila.isEmpty() && pos==input.length() )
System.out.println(input+" e' palindromo"); public T peek(){
else if( isEmpty() )
System.out.println(input+" non e’ palindromo"); throw new RuntimeException("Coda vuota!");
return iterator().next();
}//main }//peek
}//VerificaPalindroma
public T get(){
La variabile boolean flag diventa false non appena si scopre che la stringa non è palindroma (due caratteri if( isEmpty() )
corrispondenti differiscono). A fine ciclo di for(;;), per concludere che la stringa è palindroma occorre verificare throw new FìuntimeExceptionf'Coda vuota!");
che flag sia true, che l’indice di scansione pos sia arrivato alla length() della stringa e che la pila sia vuota. lterator<T> it=iterator();
T e=it.next();it.remove();
Coda ADT e gerarchia di classi return e;
È noto che una coda (queue) è una collezione di elementi in cui gli inserimenti avvengono ad uno estremo }//get
(fine della struttura) e le rimozioni avvengono all’altro estremo (testa della struttura) (gestione FIFO - First
Input First Output), come segue: public boolean isEmpty(){
return !iterator().hasNext();
• put( elem ) aggiunge elem alla fine della coda, solleva un’eccezione se la coda è limitata e se al momento }//isEmpty
della put la coda è piena
• T get() estrae (e rimuove) l’elemento in testa alla coda e lo restituisce, solleva un’eccezione se la coda è public boolean isFull(){
vuota return false;
• T peek(), come get() ma senza rimuovere l’elemento }//isFull

I nomi delle operazioni put/get non sono standardizzati. Altre volte si parla rispettivamente di append/estract, public String toString()(
offer/poll etc. Il concetto di coda può essere espresso da un'interfaccia come quella che segue: StringBuffer sb=new StringBuffer(IOO);
sb.append('[');
package poo.util; lterator<T> it=iterator();
public interface Coda<T> extends lterable<T>{ while( it.hasNext() ){
int size(); sb.append( it.next() );
void clear(); if( it.hasNext() ) sb.append(',');
void put( T elem ); }
Tget(); sb.append(']');
T peek(); return sb.toString();
boolean isEmpty(); }//toString
boolean isFull();
}//Coda public int hashCode(){
int p=43, h=0;
Una classe CodaAstratta<T>:_____________ for( T e: this )
package poo.util; h=h*p+e.hashCode();
import java.util.*; return h;
)//hashCode
public abstract class CodaAstratta<T> implements Coda<T>{
public int size(){ oublic boolean equals( Object o ){
int c=0; if( !(o instanceof Coda) ) return false;
for( lterator<T> it=iterator(); it.hasNext(); it.next(), c++ ); if( o==this ) return true;
return c; Coda<T> s=(Coda)o;
}//size if( s.size()!=this.size() ) return false;
lterator<T> it1=this.iterator();
280 281
Capitolo 15 Lista concatenata

lterator<T> it2=s.iterator(); public boolean hasNext(){


while( it1.hasNext() ){ if( cor==null ) return inizio!=null;
T e1 = it1.next(), e2 = it2.next(); return cor.next!=null;
if( !e1.equals(e2) ) return false; }//hasNext
}//while public T next(){
return true; if( !hasNext() ) throw new NoSuchElementException();
}//equals if( cor==null ) cor=inizio;
else{ pre=cor; cor=cor.next;}
}//CodaAstratta return cor.info;
}//next
Una classe CodaConcatenata<Tx_______ _________________ public void remove(){
package poo.util; if( cor==pre ) throw new HlegalStateExceptionQ;
import java.util.*; if( cor==inizio ){
inizio=inizio.next;
public class CodaConcatenata<T> extends CodaAstratta<T>{ if( inizio==null ) fine=null;
private static class Nodo<E>{
E info; else if( cor==fine ){
Nodo<E> next; fine=pre; fine.next=null;
}//Nodo }
else{
private Nodo<T> inizio=null, fine=null; pre.next=cor.next;
private int size=0; }
cor=pre;
public int size(){ return size;} size-;
}//remove
public void clear(){ }//lterator
inizio=null; fine=null;
}//clear }//CodaConcatenata

public void put( T e ){ Una classe BufferLimitato<T>:


Nodo<T> nuovo=new Nodo<T>(); In molte applicazioni pratiche non è ammesso che la dimensione della coda possa essere arbitraria, come
nuovo.info=e; consentito da CodaConcatenata. È opportuno quindi realizzare la coda su un array nativo, diciamolo buffer. Al
nuovo.next=null; fine di sfruttare al massimo le slot (posizioni) libere del buffer in presenza della logica FIFO, è utile introdurre 3
if( fine==null ) inizio=nuovo; indici di gestione (si veda anche la figura che segue): in - indice del primo slot libero (individua l’estremo
else fine.next=nuovo; coda), out - indice del primo slot occupato (individua l’estremo di estrazione o di testa della coda), size -
fine=nuovo; size++; memorizza il numero effettivo di elementi in coda. Inizialmente in=out=size=0.
}//put

public T get(){ buffer limitato


if( isEmpty() ) throw new RuntimeException("Coda vuota!"); I l di capacità 8
A ' ^
T e=inizio.info;
inizio=inizio.next; out jn In questa istantanea: in=5, out=1, size=4
if( inizio==null ) fine=null;
size--;
return e; È chiaro che dinamicamente, a seguito deH'avvicendamento delle operazioni put/get (arrivi/partenze), la zona
}//get occupata nel buffer si sposta e come caso parcolare si può svuotare o può occupare tutti gli slot disponibili. È
facile verificare che quando in==out, il buffer o è pieno o è vuoto. Per sciogliere il dubbio si dovrebbe
public lterator<T> iterator(){ return new lteratore();}//iterator esaminare l’ultima operazione effettuata: se essa è stata una put allora il buffer è pieno, se è stata una get è
vuoto. In realtà, nei casi in cui in==out è sufficiente interrogare il valore di size per sapere se il buffer è pieno
private class Iteratore implements lterator<T>{ (size==buffer.length) o è vuoto (size==0). Per consentire il massimo sfruttamento dell’array buffer, è
private Nodo<T> pre=null, cor=null;
282 283
Capitolo 15 Lista concatenata

necessario che gli indici in ed out siano gestiti in veste circolare: allorquando attingono l’ultima posizione public lterator<T> iterator(){
(buffer.length-1), devono ripartire da 0. Tutto ciò si ottiene con l'incremento modulare, es. return new lteratore();
}//iterator
in=(in+1 )%buffer.length e similmente per out
private class Iteratore implements lterator<T>{
in questo modo sino a che in<buffer.length-1, l’incremento è quello usuale, quando in==buffer.length-1, private int cursor=-1;
(in+1 )%buffer.length si valuta a 0. Si può verificare che il decremento modulare, ad es., di in è ottenibile come private boolean rimuovibile=false;
segue: (in-1 +buffer.length)%buffer.length. public boolean hasNext(){
if( cursor==-1 ) return size>0;
package poo.util; return (cursore 1)%buffer.length != in;
import java.util.*; }//hasNext
public class BufferLimitato<T> extends CodaAstratta<T>{ public T next(){
private T[] buffer; if( !hasNext() ) throw new NoSuchElementException();
private int in, out, size; if( cursor==-1 ) cursor=out;
//in e’ la posizione di inserimento else cursor=(cursor+1 )%buffer.length;
//out e' la posizione di estrazione rimuovibile=true;
//size e' il numero effettivo di elementi nel buffer return buffer[cursor];
}//next
@SuppressWarnings(,,unchecked',) public void remove(){
public BufferLimitato(int n){ if( Irimuovibile )
if( n<=0 ) throw new HlegalArgumentException(); throw new HlegalStateExceptionQ;
buffer=(T[]) new Objectfn]; int j=(cursor+1)%buffer.length; //indice elemento successivo a cursor
in=0; out=0; size=0; while( j!=in ){
} buffer[(j-1+buffer.length)%buffer.length]=buffer0l;
j=(j+1)%buffer.length;
public void clear(){ }
for( int i=out,j=0; j<size; i=(i+1)%buffer.length,++j ) rimuovibile=false;
buffer[i]=null; size--;
in=0; out=0; size=0; in=(in-1+buffer.length)%buffer.length; //arretra indice in
}//clear buffer[in]=null; //svuota nuovo slot di indice in
cursor=(cursor-1+buffer.length)%buffer.length; //arretra cursor
public int size(){ return size; }//size }//remove
}//lterator
public boolean isFull(){ return size==buffer.length;}//isFull
}//BufferLimitato
public void put( T e ){
if( size==buffer.length ) throw new RuntimeException(''Buffer full!"); Si nota che nel metodo remove() dell’lteratore, sia lo scorrimento sinistro degli elementi nel buffer che
buffer[in]=e; seguono quello corrente denotato da cursor, che l'arretramento dell'indice in seguono l’aritmetica modulare.
in=(in+1)%buffer.length;
size++; Un'applicazione di test per la coda:
}//put L’obiettivo è seguire l'andamento di una fila di persone davanti ad una cassa di un supermercato, o davanti ad
uno sportello di un ufficio postale etc. La coda è supposta di capacità limitata. L'applicazione è guidata
public T get(){ interattivamente mediante tre tipi di comandi (ogni comando è evocato da una singola lettera ‘A’ o ‘P’ o 'Q'):
if( size==0 )
throw new RuntimeException(KBuffer empty!"); A)rrivo string INVIO
T e=buffer[out]; buffer[out]=null; Partenza INVIO
out=(out+1)%buffer.length; Q(uit INVIO
size--;
return e; Un comando di arrivo specifica una stringa alfanumerica che denota il cliente che si accoda. Un comando di
}//get partenza specifica che il cliente più vecchio nella coda se ne va. Il comando Q(uit termina l'applicazione. Per

284 285
C ap ito lo ^15 Lista concatenata

scopi dimostrativi, il programma visualizza il contenuto della coda dopo ogni operazione effettuata. La corretta public static void main( Stringi] args ){
formazione di una linea comando è verificata mediante pattern matching con un’espressione regolare. sc=new Scanner(System.in);
coda=new BufferLimitato<String>(50); //esempio
package poo.esempi; boolean uscita=false;
import java.util.*; comandi();
import poo.util.*; do{
uscita=run();
public class TestCoda { }while( luscita );
static Scanner sc=null; }//main
static Coda<String> coda=null;
static String LINEA. COMANDO=“([Aa][\\sJ+[a-zA-ZO-9]+l[Pp]l[Qq])“; }//TestCoda

static void comandi(){ Una classe ListaDoppia


System.out.printlnfComandi ammessi: '); La classe LinkedList di java.util implementa una lista a doppio puntatore: ogni nodo mantiene il riferimento al
System.out.println(''A)rrivo string INVIO"); nodo successivo e al nodo precedente. In questo modo diventa possibile la navigazione nei due sensi
System.out.println("P)artenza INVIO"); (dall’inizio alla fine e viceversa). Al fine di dare un'idea di come si possa realizzare una tale lista, si presenta
System.out.println("Q)uit INVIO"); una classe ListaDoppia generica nel tipo degli elementi (supposti dotati dell’operazione di confronto naturale)
}//comandi ed iterabile, e con i soli metodi add(x) e remove(x).

static boolean run(){ La struttura di iterazione è quella semplice di Iterator. Si dovrebbe notare come in diverse situazioni, è
System.out.print("»"); sufficiente disporre del solo riferimento al nodo corrente per completare un'operazione. Tutto ciò è legato al
String linea=sc.nextLine(), s=null; fatto che i nodi hanno appunto due puntatori L’implementazione di una classe con caratteristiche simili a
if( !linea.matches( LINEA COMANDO ) ){ LinkedList (e con la struttura di iterazione Listlterator) costituisce un progetto di sviluppo proposto più avanti.
System.out.println("Linea comando errata!"); comandi(); return false;
} package poo.listadoppia;
char c=Character,toLowerCase(linea.charAt(0)); import java.util.*;
switch(c){ public class ListaDoppia<T extends Comparable<? super T » implements lterable<T>{
case 'a': private static class Nodo<E>{
int i=linea.lastlndexOff '); E info;
s=linea.substring(i+1); Nodo<E> next, prior;
try{ }
coda.put( s); private Nodo<T> testa=null;
System.out.println("*'+s+"‘ entra in coda");
System.out.printlnf'Situazione attuale: "+coda); public void add( T elem ){//aggiunta in ordine
}catch( RuntimeException e ){ System.out.println(”Coda piena!");} Nodo<T> nuovo=new Nodo<T>(); nuovo.info=elem;
break; if( testa==null II testa.info.compareTo(elem)>=0 ){//aggiunta in testa
case 'p‘: nuovo.next=testa; nuovo.prior=null;
try{ if( testa!=null ) testa.prior=nuovo;
s=coda.get(); testa=nuovo;
System.out.println("*"+s+"‘ esce dalla coda"); }
System.out.println("Situazione attuale: "+coda); else{ //aggiunta dopo il primo elemento
}catch( RuntimeException e ){ System.out.printlnfCoda vuota!”) ;} Nodo<T> contesta.next, pretesta;
break; while( cor!=null && cor.info.compareTo(elem)<0 )(
case ’q': pre=cor; cor=cor.next;
System.out.println(“Situazione coda residua: ”+coda); }
return true; nuovo.next=cor; -
}//switch if( cor!=null ) cor.prior=nuovo;
return false; nuovo.prior=pre;
}//run pre.next=nuovo;

}//add
286 287
Capitolo 15 Lista concatenata

public void remove( T elem ){ public void remove(){


Nodo<T> cor=testa; if( Irimuovibile ) throw new HlegalStateExceptionQ;
while( cor!=null && cor.info.compareTo(elem)<0 ) rimuovibile=false;
cor=cor.next; if( cor==testa ){
if( cor!=null && cor.info.equals(elem) ){ testa=testa.next;
Ile lem trovato if( testa!=null ) testa.prior=null;
if( cor==testa ){ }
testa=testa.next; else{
if( testa!=null ) testa.prior=null; cor.prior.next=cor.next;
} if( cor.next!=null ) cor.next.prior=cor.next;
else{ }
//esiste certamente il precedente di cor cor=cor.prior; //arretra cor
cor.prior.next=cor.next; }//remove
if( cor.next!=null )
cor.next.prior=cor.prior; }//lteratore

public static void main( Stringi] args ){//demo


}//remove ListaDoppia<String> ld=new ListaDoppia<String>();
for( String s: args ) Id.add(s);
public String toString(){ System.out.println(ld);
StringBuilder sb=new StringBuilder(IOO); ld.remove("zaino");
Nodo<T> cor=testa; System.out.println(ld);
sb.append('[‘); for( String x: Id ) System.out.print(x+“ ");
while( cor!=null ){ System.out.println();
sb.append( cor.info ); lterator<String> it=ld.iterator();
if( cor.next!=null ) sb.appendf, "); while( it.hasNext() ){
cor=cor.next; it.next();
} it.remove();
sb.append(']'); }
return sb.toString(); System.out.println(ld);
}//toString }//main

public lterator<T> iterator(){ return new lteratore(); }//iterator }//ListaDoppia

private class Iteratore implements lterator<T>{ Esercizi


//cor o è nuli o punta ad un nodo già visitato 1. Implementare nella classe ListaDilnteri i seguenti metodi aggiuntivi:
private Nodo<T> cor=null; public int cardinalita(int x) che ritorna il numero di ripetizioni di x nella lista this
private boolean rimuovibile=false; public void inseriscif ListaDilnteri I ) che inserisce nella lista this tutti gli elementi della lista I
public boolean hasNext(){ public void rimuovi( ListaDilnteri I ) che rimuove dalla lista this tutti gli elementi della lista I
if( cor==null ) return testa!=null; public void rimuoviTutti(int x) che cancella dalla lista this tutte le occorrenze di x
return cor.next!=null; public int hashCode() che calcola e ritorna l'hash code a partire dal contenuto della lista
}//hasNext 2. Sviluppare classi eredi di ListaOrdinataAstratta che memorizzano gli elementi rispettivamente: (a) su un
ArrayList di java.util (b) su una LinkedList di java.util, facendo "buon uso” delle strutture dati.
public T next(){ 3. Sviluppare una classe concreta VectorConcatenato<T> che estende AbstractVector<T> e memorizza gli
if( !hasNext() ) throw new NoSuchElementException(); elementi su una lista concatenata semplice a puntatori espliciti.
rimuovibile=true; 4. Si indica con doppio stack o doppia coda (Deque) una collezione in cui gli inserimenti e le rimozioni
if( cor==null ) contesta; possono avvenire ai due estremi della struttura. Data l’interfaccia (ADT) minima che segue, in cui il significato
else cor=cor.next; dei metodi dovrebbe essere intuitivo:
return cor.info;
}//next
288 289
Capitolo 15 Lista concatenata

public interface Deque<T> extends lterable<T>{ successivo (next) e al precedente (prior)) in modo da essere “navigabile" indifferentemente “in avanti" o
int size(); “indietro”.
T getFirst(); Rilevante nella classe ListaConcatenata<T> è l’implementazione della sola struttura di iterazione
T getLast(); Listlterator<T>. A questo proposito si suggerisce di introdurre una inner class in cui il cursore (iteratore) può
void addFirst( Te) ; essere nuli o se non nuli punta ad un nodo non ancora processato (situazione più vicina alla “visione astratta"
void addLast( Te) ; di un iteratore). Il cursore può essere nuli in due casi (da distinguere): prima del primo elemento o dopo
T removeFirst(); l'ultimo elemento (quest’ultima circostanza si verifica anche quando la lista è vuota).
T removeLast(); Il metodo sort() riceve un oggetto Comparator e ordina, ad esempio con l’algoritmo bubble sort, la lista.
}//Deque Attenzione: quando uno scambio è richiesto tra i contenuti di due nodi, vanno scambiate solo le informazioni.
Non modificare i puntatori durante gli scambi.
sviluppare una classe concreta DequeConcatenata<T> che memorizza la deque su una lista concatenata Successivamente realizzare una GUI che esponga una struttura a menù capace di evocare tutte le operazioni
semplice a puntatori espliciti (un solo puntatore per nodo). di Lista (comprese cioè quelle del Listlterator), e che dunque possa consentire all’utente di lavorare su una
5. Sulla riga di comando di un programma Java si fornisce un’espressione costituita solo da parentesi tonde, lista (es. di interi o di stringhe) e mostrare su una text area presente al centro della GUI lo stato corrente della
graffe e quadre, aperte e chiuse. Sapendo che tra le parentesi non sussistono spazi, si vuole scrivere un lista, es. come [12, 4, 5,15). Quando si chiede di inserire un elemento (o rimuoverlo etc.) subito dopo occorre
programma che verifica la corretta formazione dell’espressione. Ad es. visualizzare sulla text area il nuovo contenuto della lista. Quando si accende un iteratore, si mostra la
posizione (A) dove si trova l’iteratore; dopo una next si mostra la nuova posizione mentre in un campo di testo
([{]}) non è ben formata Corrente si mostra l’elemento corrente o ? se esso non è definito. Si nota che scegliendo il comando iterator,
[]({[]}) è ben formata la freccia si trova inizialmente prima del primo elemento, e risultano abilitati i soli comandi hasNext, next,
remove. Se invece si accende un Listlterator, si può specificare la posizione iniziale del list iterator come
Si nota che un’espressione di parentesi è ben formata se al tempo di arrivo di una parentesi chiusa, essa è la quella di default (identica a iterator) o si può fornire un numero che esprime dove piazzare la freccia (es.
chiusa dell’ultima parentesi aperta non ancora chiusa. Si suggerisce di utilizzare uno stack di caratteri su cui specificando size(), la freccia va posta dopo l’ultimo elemento, specificando size()-1, la freccia va posta tra
mantenere le parentesi aperte non ancora chiuse. Alla fine il programma deve scrivere se l’espressione è ben penultimo ed ultimo elemento etc.).
formata o meno. È possibile scartare immediatamente un input malformato, contenente cioè caratteri non Si suggerisce di introdurre una menu bar con il menu File (con gli item New, Apri, Salva, Salva con nome,
parentesi, utilizzando il supporto di un’espressione regolare. Exit), il menu Command (con gli item corrispondenti alle operazioni possibili sulla lista), qualche item potrebbe
orire un menu di secondo livello (es. iterator, listlterator etc), etc.
Progetto
É assegnata la seguente interfaccia, che si ispira liberamente al concetto di LinkedList di java.util:

package poo.util;
import java.util.*;
public interface Lista<T> extends lterable<T>{
int size();
void clear();
void addFirst( T elem );
void addLast( T elem );
T getFirst();
T getLast();
T removeFirst();
T removeLast();
boolean isEmpty();
boolean isFull();
void sort( Comparator<? super T> c );
Listlterator<T> listlterator();
Listlterator<T> listlterator( int start ); //0<=start<=size()
}//Lista

Progettare e sviluppare:
• una classe astratta ListaAstratta<T> che implementa l’interfaccia Lista<T> e concretizza quanti più metodi
è possibile, e sicuramente equals(), toStringQ e hashCode()
• una classe concreta ListaConcatenata<T> erede di ListaAstratta<T> e basata su puntatori espliciti. In
particolare, la classe deve basarsi su una lista doppiamente concatenata (ogni nodo ha il puntatore al
290 291
Capitolo 16:______________________________________________ ______________________
Sviluppo di un’applicazione: aritmetica di polinomi

Si vogliono sviluppare alcune classi per il supporto deH’aritmetica di polinomi P(x) con indeterminata x di tipo
doublé, coefficienti interi relativi e gradi interi non negativi. I polinomi devono ammettere (almeno) le
operazioni di addizione e moltiplicazione. Un polinomio è ordinato come in matematica, con la sequenza dei
monomi che si sviluppa per gradi decrescenti. Ad esempio:

P1(x)=2xA3-3xA2-2
P2(x)=-4xA5-2xA3+3x
P1(x)+P2(x)=-4xA5-3xA2+3x-2
P1(x)* P2(x)=-8xA8+12xA7-4xA6+14xA5+6xA4-5xA3-6x

L’esponenziazione è espressa col carattere ‘A’. In questa veste i polinomi possono essere visualizzati su
output o anche acquisiti da input.

Un polinomio è una successione (collezione ordinata) di monomi. Come primo passo bisogna definire una
classe Monomio.

Una classe Monomio________________________________________________________________________


package poo.polinomi;
public class Monomio implements Comparable<Mononio>{
private final int coeff, grado;
public Monomio( int coeff, int grado ){
if( grado<0 ) throw new RuntimeExceptionfGrado negativo!");
this.coeff=coeff; this.grado=grado;
}//Monomio

public Monomio( Monomio m ){//costruttore di copia


coeff=m.coeff; grado=m.grado;
}//Monomio

public int getCoeff(){ return coeff;}


public int getGrado(){ return grado;}

public Monomio add( Monomio m ){


if( !this.equals(m) ) throw new RuntimeException("Monomi non simili!”);
return new Monomio( coeff+m.coeff, grado );
}//add

public Monomio mul( Monomio m ){


return new Monomio( coeff'm.coeff, grado+m.grado );
}//mul

public Monomio mul( int s ){


return new Monomio( coeff's, grado );
}//mul

public int compareTo( Monomio m ){


return m.grado-this.grado;
}//compareTo
293
Capitolo 16 Aritmetica di polinomi

Un diagramma di classi UML:


public boolean equals( Object o ){
if( !( o instanceof Monomio ) ) return false;
if( o==this ) return true; lterable<Monomio> « im p le m e n ts » « a b s tra c t»
Monomio m=(Monomio)o; « in t e r f a c e » & -----------------^ P o lin o m io A stra tto
return grado==m.grado; P olin om io
}//equals
« e x te n d s »
public int hashCode(){ return grado;}

public String toString(){


StringBuilder sb=new StringBuilder(50);
if( coeff<0 ) sb.append('-'); P o lin o m io C o n ca te n a to P o lin o m io M a p
if( Math.abs(coeff)!=1 II grado==0 ) sb.append( Math.abs(coeff) );
if( coetf!=0 && grado>0 ) sb.append('x'); package poo.polinomi;
if( coeff!=0 && grado 1 ){ sb.append(w ); sb.append(grado);} import java.util.*;
return sb.toStringQ; public abstract class PolinomioAstratto implements Polinomio{
}//toString
public int size(){
}//Monomio int c=0;
for( lterator<Monomio> it=this.iterator(); it.hasNext(); it.next(), c++ );
L’uguaglianza tra due monomi è interpretata come similitudine della matematica: due monomi sono simili se return c;
hanno la stessa parte letterale, ossia lo stesso grado. In modo congruente, il metodo hashCode() si limita a }//size
restituire l'intero che esprime il grado (due oggetti uguali devono avere lo stesso hash code).
protected abstract Polinomio create(); //metodo factory
Il metodo toString() restituisce una stringa “minima” rappresentante il monomio.
public Polinomio add( Polinomio p ){
Le eccezioni per grado negativo o per addizione di monomi non simili sono runtime exception (unchecked) e Polinomio somma=create();
dunque sono evitabili con un test dell’utente. for( Monomio m: this ) somma.add( m );
for( Monomio m: p ) somma.add( m );
La classe Monomio rappresenta un ulteriore esempio di classe di oggetti immutabili (come i razionali, le return somma;
stringhe etc). }//add

Un’interfaccia Polinomio:______________________________________________________________________ public Polinomio mul( Polinomio p ){


Un polinomio è assunto come una collezione iterabile di monomi, definito dalla seguente interfaccia (ÀÒT): Polinomio prodotto=create();
for( Monomio m: this ) prodotto=prodotto.add( p.mul( m ) );
package poo.polinomi; return prodotto;
public interface Polinomio extends lterable<Monomio>{ }//mul
int size();
void add( Monomio m ); public Polinomio mul( Monomio m ){
Polinomio add( Polinomio p ); Polinomio prodotto=create();
Polinomio mul( Polinomio p ); for( Monomio m1 : this )
Polinomio mul( Monomio m ); prodotto.add( m1.mul( m ) );
Polinomio derivata(); return prodotto;
doublé valore( doublé x ); }//mul
}//Polinomio
public Polinomio derivata(){
È conveniente introdurre, al solito, una classe PolinomioAstratto che implementa l’interfaccia Polinomio, e //TODO come esercizio
fornisce una concretizzazione di tutti quei metodi che possono basarsi l’iteratore. Un metodo astratto return nuli; //per ora
(protected) utile è createQ (metodo factory) che è in grado di costruire un'istanza di polinomio dello stesso tipo }//derivata
dell’oggetto this.
294 295
Capitolo 16 __ Aritmeticajli polinomi

public doublé valore( doublé x ){ Dei due metodi mul() il primo implementato moltiplica il polinomio this per un monomio ricevuto
//TODO come esercizio parametricamente e ritorna il polinomio prodotto ottenuto. Si spazzola il polinomio this (con un for-each poiché
return 0; //per ora un polinomio è iterabile), ottenendo i monomi uno alla volta, si fa il prodotto tra ogni monomio e quello ricevuto
}//valore per parametro e quindi si aggiunge il monomio prodotto al polinomio prodotto.

public String toString(){ Il metodo mul() tra polinomi si basa sul metodo mul() di un polinomio per un monomio. Si crea il polinomio
StringBuilder sb=new StringBuilder(); prodotto, quindi si itera con un for-each sui monomi del polinomio this; per ogni monomio ottenuto si
lterator<Monomio> it=this.iterator(); costruisce il polinomio prodotto-parziale del monomio per il secondo polinomio. Il polinomio prodotto-parziale
boolean flag=true; viene quindi sommato al polinomio prodotto complessivo.
while( it.hasNext() ){
Monomio m=it.next(); Il metodo toString si fonda sul fatto che la successione dei monomi è per grado decrescente. Una particolarità
if( m.getCoeff()>0 && Iflag ) riguarda la gestione del segno del coefficiente dei monomi. Un coefficiente positivo non si accompagna al
sb.append(V); segno V , uno negativo sì per default. Pertanto il provvedimento è stato adottato di far apparire il segno V
sb.append( m ); sulla stringa per i monomi positivi, tranne, ovviamente, per il primo monomio. A questo scopo si usa una
if( flag ) flag=!flag; boolean flag inizializzata a true che diventa false non appena è stato emesso il primo monomio. Per il resto si
} sfrutta il toStringO della classe Monomio.
return sb.toString();
}//toString La classe PolinomioLL: _________
Memorizza la collezione dei monomi su una LinkedList di java.util.
public boolean equals( Object o ){
if( !(o instanceof Polinomio) ) return false; package poo.polinomi;
if( o==this ) return true; import java.util.*;
Polinomio p=(Polinomio)o; public class PolinomioLL extends PolinomioAstratto{
if( this.size()!=p.size() ) return false; private LinkedList<Monomio> lista=new LinkedList<Monomio>();
lterator<Monomio> it=this.iterator();
for( Monomio m; p){ protected PolinomioLL create(){ //covarianza del tipo di ritorno
Monomio q=it.next(); return new PolinomioLLQ;
if( m.getCoeff()!=q.getCoeff() Il }//create
m.getGrado()!=q.getGrado() ) return false;
} public lterator<Monomio> iterator(){ return lista.iterator(); }//iterator
return true; public int size(){ return lista.sizeQ;}
}//equals public void add( Monomio m ){
//si mantiene la lista ordinata per gradi decrescenti
public int hashCode(){ if( m.getCoeff()==0 ) return;
int p=17, hash=0; Listlterator<Monomio> lit=lista.listlterator();
for( Monomio m: this ){ boolean flag=false; //true quando m è sistemato
int hc=(String.valueOf(m.getCoeff())+ while( lit.hasNext() && Iflag ){
String.valueOf(m.getGrado())).hashCode(); Monomio m1=lit.next();
hash=hash*p+hc; if( m.equals(ml) ){//monomi simili
} Monomio m2=m.add(m1);
return hash; if( m2.getCoeff()!=0 ) lit.set( m2 );
}//hashCode else lit.remove();
flag=true;
}//PolinomioAstratto }
else if( m1.compareTo(m)>0 ){
Il metodo factory create() è la chiave per scrivere nella classe astratta metodi come add(), mul() etc. che per lit.previous();lit.add(m);flag=true;
progetto devono costruire un polinomio e ritornarlo. Il metodo add( p ) crea il polinomio somma, quindi }
aggiunge ad esso i monomi del polinomio this e del polinomio p, mediante il metodo (necessariamente }//while
astratto) add( Monomio ) che aggiunge un monomio a un polinomio e si fa carico dei problemi di similitudine, if( Iflag ) lit.add( m );
per cui due monomi simili vengono "fusi” insieme e laddove il coefficiente si annulli per somma algebrica, }//add
elimina il monomio risultante dal polinomio somma. }//PolinomioLL
296 297
Capitolo 16 Aritmetica di polinomi

Come si vede la classe PolinomioLL è molto sintetica. L'iterator lo fornisce già LinkedList. Si nota che la protected PolinomioConcatenato create(){
ridefinizione del metodo factory che dovrebbe attenersi alla intestazione (signature) return new PolinomioConcatenato();
}//create
Polinomio create(){...}
public void add( Monomio m ){
è stata programmata equivalentemente (per covarianza del tipo di ritorno) come segue: if( m.getCoeff()==0 ) return;
Nodo cor=testa, pre=null;
PolinomioLL create(){...} while( cor!=null && cor.info.compareTo(m)<0 ){
pre=cor; cor=cor.next;
Il metodo add( Monomio m ) sfrutta il Listlterator. Quando un monomio del polinomio this è trovato simile al }//while
parametro m, si crea tentativamente il monomio somma m2. Se m2 ha un coefficiente diverso da zero allora si if( cor!=null && cor.info.equals(m) ){
rimpiazza il monomio corrente col metodo set() del list iterator. Se il coefficiente di m2 è zero, si rimuove il //monomi simili
monomio corrente dal polinomio this. In caso di monomi non simili, si inserisce il monomio m rispettando cor.info=cor.info.add(m);
l’ordine decrescente dei gradi. Si tratta di una proprietà importante, garantita dal metodo add() ad ogni if( cor.info.getCoeff()==0 ){
inserimento di un nuovo monomio. //rimuovi nodo cor
if( cor==testa ) testa=cor.next;
La classe PolinomioConcatenato: else pre.next=cor.next;
Utilizza una lista concatenata semplice a puntatori espliciti.

package poo.polinomi;
import java.util.*; //aggiunta di m tra cor e pre
public class PolinomioConcatenato extends PolinomioAstratto{ Nodo nuovo=new Nodo();
private static class Nodo{ nuovo.info=m; nuovo.next=cor;
Monomio info: if( cor==testa ) testa=nuovo;
Nodo next; else pre.next=nuovo;
}//Nodo
}//add
private class Iteratore implements lterator<Monomio>{ }//PolinomioConcatenato
Nodo pre=null, cor=null;
public boolean hasNext(){ Un programma di test: ________
if( cor==null ) return testa!=null; package poo.polinomi;
return cor.next!=null;
}//hasNext public class TPOLG{
public Monomio next(){ public static void main( String Qargs ){
if( !hasNext() ) throw new NoSuchElementException(); Polinomio p1=new PolinomioLL();
if( cor==null ) contesta; p1 .add( new Monomio( 2, 3) ) ;
else{ pre=cor; cor=cor.next;} p1.add( new Monomioj 0,1 ) );
return cor.info; p1 .add( new Monomio( -3,2 ) );
}//next p1 .add( new Monomio( -2, 0 ) );
public void remove(){ Polinomio p2=new PolinomioMapQ;
if( cor==pre ) throw new HlegalStateException(); p2.add( new Monomio( -2, 3 ) );
//rimuovi nodo cor p2.add( new Monomioj 3,1 ) );
if( cor==testa ) testa=testa.next; p2.add( new Monomio( -4, 5 ) );
else pre.next=cor.next; System.out.printlnfpl ="+p1 );
cor=pre; //arretra cor System.out.println("p2="+p2);
}//remove System.out.println("p1 +p2='+p1 ,add(p2));
}//lteratore Polinomio pm=p1.mul(p2);
System.out.printlnfpl *p2="+pm );
private Nodo testa=null; System.out.printlnQ;
public lterator<Monomio> iterator()( return new lteratore();} //iterator }//main
}//TPOLG
298 299
Capitolo 16 Elementi di complessità degli algoritmi

Capitolo 17:__________________________________________ _________ ________ _______


Esercizi
Concetti di complessità degli algoritmi
1. Implementare i metodi derivata() e valore() dell’interfaccia Polinomio.
2. Scrivere e testare tre ulteriori classi concrete eredi di PolinomioAstratto basate rispettivamente su ArrayList, La complessità (o efficienza) di un algoritmo può essere complessità temporale, che mira a caratterizzare il
Set e Map. tempo di esecuzione dell’algoritmo al variare della dimensione dei dati, o complessità spaziale, che valuta
3. Modificare l’applicazione sui polinomi in modo da ammettere come input di un polinomio un'espressione del l’ingombro di memoria richiesto dai dati dell'algoritmo, sempre al variare della numerosità dei dati. In quanto
tipo 3xA4-5xA2+7, ossia lo stesso formato generato dal metodo toString() della classe PolinomioAstratto. segue, l’attenzione è posta sulla complessità temporale.
Definire un’espressione regolare per verificare la corretta formazione di una stringa/polinomio, quindi estrarre i
monomi ed inserirli in un oggetto polinomio. Piuttosto che tentare di “misurare" il tempo di esecuzione richiesto da un algoritmo su una certa piattaforma (la
misura, infatti, è sensibile al tipo di compilatore adoperato, al sistema operativo sottostante, all’hardware della
macchina etc.), l’obiettivo è estrarre il comportamento intrinseco dell’algoritmo al variare della dimensione
dell'input. Queste informazioni sono utili per confrontare algoritmi diversi che risolvono uno stesso problema.

Ad esempio, volendo caratterizzare la complessità di un algoritmo di ordinamento di un array con n elementi,


di interesse non è tanto conoscere “esattamente" il tempo impiegato dall’algoritmo, quanto la dipendenza del
comportamento temporale dell'algoritmo al crescere di n (comportamento asintotico). Per chiarire le idee, di
seguito si considera l’algoritmo di selection sort su un array di interi a di n elementi (n potrebbe essere
a.length).

Complessità “esatta” di selection sort*S i


Per comodità si riproduce di seguito l'algoritmo selection sort {si suppone l’array a già inizializzato):

for( int j=n-1; j>0; j--){


int iMax=0;
for( int i=1; i<=j; i++ ) //for-interno
if( a[i]>a[iMax] ) iMax=i;
int tmp=a[j];
a[j]=a[iMax];
a[iMax]=tmp;
}//for-esterno

Si definisce “complessità esatta" di un algoritmo il conteggio delle operazioni eseguite per realizzare l’obiettivo,
in questo caso portare a termine l’ordinamento. A questo proposito si assume, per semplicità, che tutte le
operazioni elementari (un confronto, un'assegnazione etc.) abbiamo un costo temporale unitario. Sia Tss(n)
la funzione complessità esatta di selection sort, che dipende dalla dimensione dell’input n. Si ha:

Tss ( n ) = I + unni _ operazioni _ far _ esterno

dove l’1 si riferisce alla inizializzazione del for esterno. Contando le operazioni eseguite dal for-esterno si ha:

Tss(n) = I + 7 * ( n - I) + 4 *|(w - 1) + (/i - 2) + ... + 3 + 2 + l|

Ad ogni girata del ciclo esterno, si eseguono sempre 7 operazioni: l'inizializzazione di iMax, l’inizializzazione
del for interno, le tre assegnazioni per lo scambio di a[iMax] con a[j], la condizione del for esterno, il passo del
for esterno. Siccome il ciclo esterno è ripetuto (n-1) si ha il primo contributo 7*(n-1). Manca ancora il computo
delle operazioni eseguite dal for interno. Quando j vale (n-1), il ciclo interno è ripetuto (n-1) volte, quando j vale
(n-2), il for interno è ripetuto (n-2) volte etc. Considerando che ad ogni iterazione del for interno, nel caso
peggiore (in generale l’algoritmo dovrebbe essere studiato nei tre casi: peggiore, migliore e medio dei dati) si
compiono 4 operazioni (verifica condizione di continuazione, confronto ed assegnazione di un nuovo valore ad
iMax, ed il passo del for (i++)), ne deriva che la sommatoria delle varie iterazioni del ciclo interno vanno
300 301
Capitolo 17 Elementi d(complessità degli algoritmi

moltiplicate per 4. Ricordando l’equivalenza di Gauss (cap. 1) sulla somma dei primi n numeri naturali
(1 +2+3+.. .+(n_1)+n):

si riconosce subito che entro le parentesi quadre c’è la somma dei primi (n-1) naturali, dunque:

Pertanto:

r vs(/»)= I + 7 * (// - I ) + 2 * (// - I ) * // = 2// ’ + 5 / / - 6

Notazione big 0 (ordine di) e comportamento asintotico per n-»oo


Si dice che una (unzione T(n) è dell’ordine di una funzione f(n) e si scrive: T(n)=0(f(n)), se si possono trovare
due costanti positive a ed notali che: T(n)<a‘f(n), t?n>n0. Nel caso di selection sort si ha subito: TSs(n)=0(rt).
Infatti, scegliendo ad es. a=3, già per no=1 si ha: Tss(n)<3n*, Vn>1. Si nota che i valori delle costanti a ed n0
potrebbero essere scelti più grandi e la relazione Tss(n)<a‘f(n), \/n>n0, varrebbe a maggior ragione.

Si osserva ancora che, in base alla definizione dell’operatore 0, risulta pure: 7\s (n) = ( ) ( n ' ) ed anche Si vede che per n>1, 2n2+5n-6 è compresa tra n2 e 3n2. Dunque Tssfn^&Xn2). All’atto pratico si preferisce
7’v.v(n) = ( H n k ), k > 2. utilizzare la notazione “big 0" e scrivere 0(f(n)) ma col significato dell’operatore 0 grande, ossia con
riferimento alla “più piccola" funzione dominante f(n).
I risultati che precedono dicono che per stimare l’ordine di grandezza (0) del tempo di esecuzione di un
algoritmo è sufficiente ignorare i coefficienti e termini di grado inferiore nell’espressione della complessità Alcune complessità ____ _________________
esatta. Inoltre, dire che Tssfn^Ofn2) significa dire che il tempo “vero” di esecuzione dell’algoritmo su qualsiasi Ricerca in una tabella hash: Thash(n)=Ó(1) ossia l’operazione richiede un tempo (quasi) costante.
calcolatore, cresce col quadrato della dimensione dell’input, ossia è proporzionale ad r f a meno di qualche
costante. Ricerca lineare: TRi(n)=0(n).
Si nota, infine, che T(n)=0(f(n)) significa che la funzione T(n) è dominata dalla f(n) non appena la dimensione Ricerca Binaria: TRB(n)=0(log2 n)=0(log n).
dell’input supera una certa soglia minima n0.
Per tutti i metodi di ordinamento elementari: ToE(n)=0(n2), in cui OE può essere Selection Sort, Bubble Sort,
Operatore grande etc.
Si dice che una funzione T(n) = f2(f(n)) se si possono trovare due costanti positive a ed notali che:
Metodi avanzati di ordinamento: ToA(n)=0(n*log n), dove OA può essere Merge Sort, Heap Sort, Quick Sort
T(n) > o * / (//),V« > //„ etc.

ossia la funzione T(n) domina la f(n) appena n supera una soglia no. Per il problema dell’ordinamento, basato su confronti e scambi, sussiste il risultato: T(n)= Q(n*log n) ossia
nessun algoritmo esiste con una complessità inferiore a 0(n‘ log n).
Operatore 0 grandeS i
Si dice che una funzione T(n) = &2(f(n)) se si possono trovare tre costanti positive ai, a? ed n0 tale che: Si dicono algoritmi polinomiali, tutti gli (usuali) algoritmi che hanno complessità del tipo 0(nk) per qualche k.
Gran parte dei problemi sono (per nostra fortuna) risolvibili in tempi polinomiali. Tuttavia esistono algoritmi con
complessità esponenziale, es. T(n)=0(2n). Tali algoritmi ed i relativi problemi sono spesso detti intrattabili, nel
a, * f ( n ) < T ( n ) < a, V/j > //„
senso che all’atto pratico, per n non troppo piccolo (es. 50), l’algoritmo non termina, quale che sia il calcolatore
utilizzato. In questi casi si ricorre spesso a metodi euristici per ottenere soluzioni approssimate del problema.
In questo caso la T(n) cresce come la f(n) non appena n supera la soglia n0. La figura che segue illustra la
notazione con riferimento alle funzioni n2, 3n? e 2n2+5n-6 per n>0: Di seguito si considera un metodo di ordinamento in cui T(n)=0(n). Si tratta di Bucket Sort, un algoritmo di
ordinamento non basato su confronti e scambi. Si considera una collezione di n interi fornita da input. Si
302 303
Capitolo 17

assume che ogni numero abbia un valore compreso, ad esempio, tra 0 e 100. In presenza di queste ipotesi è Capitolo 18:
possibile preparare un array di int di 101 elementi: nella posizione di indice i si mantiene il contatore del
Tecniche di programmazione ricorsiva
numero di volte che l’intero i compare nella sequenza di input. Si capisce che è possibile leggere i dati in
input, caricare l’array e scrivere alla fine il suo contenuto, dal primo all'ultimo elemento, per ottenere la Un metodo si dice ricorsivo se daH’interno del suo corpo istruzioni chiama (invoca) se stesso (ricorsione diretta
sequenza ordinata. Complessivamente si realizzano 2‘ n operazioni, dunque O(n). Si nota che mentre negli o auto-ricorsione). Più in generale si è in presenza di chiamate ricorsive anche quando un metodo A chiama
altri metodi di ordinamento la fase di caricamento dell’array è trascurata (l'algoritmo opera quando i dati sono un metodo B che chiama un metodo C ... che chiama A (ricorsione indiretta). In sostanza, per essere affetto
pronti), in questo caso la lettura è parte integrante del metodo. da ricorsione, un metodo deve registrare una chiamata a se stesso prima che una precedente chiamata sia
stata completata.
import java.util.*;
public class BucketSort { Per essere ben definito, un metodo ricorsivo deve derivare da una precisa formulazione, es. matematica, della
public static void main( String [] args ){
risoluzione di un (sotto) problema. Una definizione ricorsiva prevede normalmente rimandi alla stessa tecnica
System.out.println("Bucket sort"); risolutiva ma con un insieme di dati “via via più piccolo”. In altre parole, il procedimento ricorsivo può auto-
System.out.printlnf'Fomisci una successione di interi ciascuno compreso tra 0 e 100"); invocarsi più volte ma ogni volta deve applicarsi ad un numero di casi inferiori a quello di partenza.
Scanner sc=new Scanner( System.in ); Diversamente, se la ricorsione riparte dagli stessi dati originari, è equivalente ad un loop infinito, ossia è senza
int []a=new int[101 ]; //inizializzato a tutti 0 per default via di uscita. Le considerazioni che precedono mirano a porre in rilievo la fondamentale esigenza di verificare
//acquisizione dei dati che in una formulazione ricorsiva siano sempre presenti uno o più casi di uscita che bloccano la “ricorsione in
for(;;){ avanti" e attivano “percorsi di ritorno” che possono preludere alla terminazione dell’algoritmo
int x=sc.nextlnt();
if( x<0 II x>100 ) break; Calcolo della potenza an
a[x]++; //conta x
Si considera il calcolo della potenza an con base a intera ed esponente n intero non negativo. É ovvio che
} an= ra *a ‘ a \..*a (n prodotti), ma una differente formulazione esiste ed è la seguente:
//visualizzazione dei dati, 8 al massimo per linea di uscita
int c=0;
1 se n=0
for( int i=0; ka.length; i++ )
for( int j=0; j<a[i); j++ ){
a*an 1 se n>0
System.out.printf("%5d“,i);
c++;
che contempla un calcolo ricorsivo quando n>0. Dalla formulazione ricorsiva matematica si deduce il seguente
if( c%8==0 ) System.out.println();
metodo Java ricorsivo:

int potenza( int a, int n ){


}//Bucket Sort
//pre: n>=0
if( n==0 ) return 1;
Esercizi ____ _______ ______ return a*potenza(a,n-1 ); //dato l’uso di return, l’alternativa else è inutile
1. Per un assegnato problema un algoritmo A ha complessità 0(n2), in particolare TA(n)=0.25n?, mentre un }//potenza
algoritmo B ha complessità 0(n), in particolare TB(n)=10n. Verificare il comportamento asintotico dei due
algoritmi al variare di n, stabilendo il valore di n al di sopra del quale B è più efficiente di A. Analisi dell’esecuzione di potenza(2,3)S i
2. Come 1. ma con TA(n)=0.0001n2, TB(n)=100n. Si sa che ad ogni invocazione di un metodo, si crea in memoria un’area dati (o record di attivazione) costituita
3. Come 1. ma con TA(n)=0.003n2, T[)(n)=243n. dai parametri e dai dati locali del metodo. Nel caso di potenza, l’area dati contiene solo il dato a ed il dato n
4. Per quale valore di n un algoritmo che esegue 100n2 istruzioni diventa più efficiente di uno che realizza
(parametri formali). Ad esempio, all'invocazione (in un main) int x=potenza(2,3) corrisponde la creazione
0.01‘ 2n istruzioni ? dell’area dati che segue. Per scopi di esemplificazione, si indica a sinistra il punto di sospensione
5. Applicare il procedimento seguito per l'analisi di Selection Sort al caso di Insertion Sort (si riveda il cap. 2) e
(sottolineato), in cui è atteso il risultato; a destra il numero della chiamata:
dimostrare che anche in questo caso: Tis(n)=0(n2).

int x=potenza(2.3)
1a invocazione del metodo
3 n

ad ogni invocazione, si applica l’algoritmo del metodo ai dati trasmessi. Nel caso in esame si ha la seguente
discesa ricorsiva:

304 305
Capitolo 18 Tecniche di programmazione ricorsiva

int x=potenza(2,3) Un esempio di metodo tail recursive è illustrato dal calcolo del massimo comun divisore di due numeri interi
r
return 2*potenza(2.2ì 13 invocazione del metodo
positivi con l’algoritmo di Euclide (si rivedano i cap. 1 e 3):

int mcd( int n, int m ){


t ‘ jf( m==0 ) return n;
return 2*potenza(2,1) 28 invocazione del metodo return mcd( m, n%m );
}//mcd
return 2*potenza(2,Q) 3a invocazione del metodo
In questo algoritmo, se l’attivazione corrente di mcd non è in grado di calcolare e restituire il risultato, essa
delega completamente il compito ad una nuova attivazione mcd( m, n%m ). Quando quest’ultima termina e
return 1 4a invocazione del metodo ritorna il controllo al chiamante, nessun'altra azione va effettuata se non propagare il risultato ricevuto ancora
al chiamante etc. In sostanza, se si innesca una cascata di k attivazioni tail recursive, l'ultima definisce il mcd,
le precedenti fanno solo il “passa mano” del risultato al chiamante.
Le aree dati sono create durante la fase di discesa ricorsiva e distrutte durante la catena dei ritorni. Ad
esempio, a seguito del ritorno dalla 4a alla 3a attivazione di potenza, si distrugge la 4a area dati. Uscendo dalla Il riconoscimento della ricorsione in coda potrebbe preludere ad un’ottimizzazione delle aree dati del processo
1a tutte le aree dati saranno state rimosse dalla memoria. Dovrebbe essere chiaro a questo punto come ricorsivo, nel senso che ogni nuova chiamata tail recursive potrebbe “sfruttare" l'area dati del chiamante ed
un’esecuzione ricorsiva comporti un aggravio spazio/tempo connesso con la creazione/distruzione delle aree evitare di crearne una nuova. I normali linguaggi di programmazione, incluso Java, non riconoscono la
dati, che non si ha se l’algoritmo è espresso in veste iterativa (con un ciclo). ricorsione in coda, che quindi è trattata allo stesso modo della normale ricorsione.

Anche se può non essere banale, ma ad una formulazione ricorsiva si può sempre associare un’equivalente Ricorsione e divide-et-impera
formulazione iterativa dell’algoritmo. Nel caso attuale si può avere: Spesso una soluzione ricorsiva si può ottenere dividendo il problema in sotto problemi più o meno della stessa
dimensione, applicando ai sotto problemi la stessa tecnica risolutiva (ricorsione) e poi combinando i risultati
int potenza( int a, int n ){ ottenuti.
int p=1;
for( int i=0; i<n; i++ ) p*=a; Si consideri il calcolo del massimo valore in un array v di n elementi (es. interi). Ovviamente, la determinazione
return p; del massimo si può basare su un semplice ciclo (iterazione). Tuttavia una risoluzione ricorsiva si può
}//potenza impostare dividendo il vettore in due sotto vettori della stessa (o quasi) dimensione, cercando il massimo
separatamente sui due sotto vettori e poi restituendo il maggiore tra i due sub massimi. Segue un esempio
Una formulazione iterativa è di norma molto più efficiente di una ricorsiva. Il fatto è che per alcuni problemi una concreto:
soluzione ricorsiva può risultare più naturale. In questi casi, ottenuta una soluzione ricorsiva, si può poi
lavorare per convertirla in una veste iterativa. int massimo( int v[], int inf, int sup ){
// pre: v.length>0
Esercizio if( inf==sup ) return v[inf];
Scrivere in veste ricorsiva e iterativa un metodo che riceve una stringa s e verifica se essa è palindroma, ossia int med=(inf+sup)/2;
si legge identicamente da sinistra a destra e viceversa (es. “anna", "radar” sono palindrome). Si ha: int m1=massimo( v, inf, med );
int m2=massimo( v, med+1, sup );
boolean palindroma( String s ){//v. iterativa return (m1>m2)? m1 : m2;
boolean palindroma( String s ){//v. ricorsiva int i=0, j=s.length()-1; {//massimo
if( s.length()<=1 ) return true; while( i<=j ){
if( s.charAt(0)!=s.charAt(s.length()-1) return false; if( s.charAt(i)!=s.charAt(j) ) return false; Il metodo riceve il vettore v e due indici inf e sup che delimitano un sotto vettore. Inizialmente si trasmette
return palindroma( s.substring(1,s.length()-1) ); i++; j--; inf=0 e sup=v.length-1 in modo da considerare tutto il vettore. Se il sotto vettore si riduce ad un solo elemento,
}//palindroma } il massimo coincide con questo elemento. Diversamente, si suddivide a metà l’area di ricerca v[inf..sup], si
return true; applica ricorsivamente il metodo ai due sotto vettori e quindi si restituisce il maggiore dei due sub massimi
}// ottenuti. Il metodo può essere fatto partire in un main come segue:

Ricorsione in coda (tail recursion) _ int []a={3,2,15,-1,6,7};


Un metodo si dice che segue la “ricorsione in coda" quando la chiamata ricorsiva è l’ultima istruzione int max=massimo( a,0,a.length-1 );
dell’algoritmo. In altre parole, dopo la chiamata ricorsiva non sussistono altre azioni da effettuare. Il metodo
potenza() non è tail recursive: dopo aver calcolato potenza(a,n-1), il risultato va poi moltiplicato per la base a
prima di essere restituito.
306 307
Capitolo 18 Tecniche di programmazione ricorsiva

Torri di Hanoi public static void main( Stringi] args ){


È un classico esempio di problema risolvibile con la tecnica ricorsiva, che appartiene alla categoria dei giochi. new TorriDiHanoi().muovi( 3, Pin.SX, Pin.CL, Pin.DX );
Si hanno tre paletti (pin) ed un numero N>1 di dischi tutti di dimensione diversa. Inizialmente gli N dischi sono }//main
impilati sul paletto di sinistra. L’obiettivo è spostare gli N dischi sul paletto di destra, usando la mediazione del }//TorriDiHanoi
paletto centrale. Vincoli del gioco:
Il programma è concepito per assistere l’utente suggerendogli le mosse da compiere di volta in volta per
• si può spostare un solo disco per volta arrivare alla soluzione. Il metodo spostaidisco(da,a) risolve il problema banale di spostare un dischetto dal
• un disco può essere collocato sopra un altro più grande ma non viceversa paletto da al paletto a. Esso consiste solo in una stampa. Il metodo muovi() riceve il numero n di dischi da
• in ogni momento ogni disco deve essere impilato su un paletto. spostare e l’indicazione del paletto sorgente, del paletto dest(inazione e di quello aus(iliario, utilizzabile per
spostamenti temporanei. La struttura del metodo muovi() riflette esattamente la strategia suggerita in
Strategia risolutiva: Si spostano i primi N-1 dischi (dal più piccolo) sul paletto centrale. Quindi si sposta il disco precedenza, ed è intrinsecamente ricorsiva.
più grande a destinazione. Si completa il discorso spostando gli N-1 dischi parcheggiati sul paletto centrale sul
paletto destro. Come si vede il problema di spostare N dischi è decomposto in due (sotto) problemi dello Output generato quando n=3:
stesso tipo ma dimensione N-1, più un problema banale che è quello di spostare un solo disco da sorgente a
destinazione. Ma come si spostano N-1 dischi? Semplice: con la stessa tecnica descritta. Segue l’applicazione Sposta un disco da SX a DX
della strategia nella risoluzione manuale del gioco quando N=3 Sposta un disco da SX a CL
Sposta un disco da DX a CL
Torri di Hanoi per N=3 Sposta un disco da SX a DX
Sposta un disco da CL a SX
Sposta un disco da CL a DX
Sposta un disco da SX a DX

Calcolo delle permutazioni __


Dato un array con n elementi distinti, si vuole determinare e mostrare tutte le n! permutazioni degli n elementi.

Si tratta di un altro problema in cui è più agevole trovare una risoluzione ricorsiva che iterativa.

Come strategia risolutiva si può adottare la seguente: si fissa il primo elemento in prima posizione e si
generano (supponendo di saperlo fare) tutte le permutazioni degli n-1 elementi restanti. Ovviamente, cosi
facendo si genera una parte delle permutazioni degli n elementi caratterizzate dall’avere fissato il primo
elemento. Se adesso si pone in prima posizione il secondo elemento dell’insieme e si generano tutte le
permutazioni dei rimanenti n-1 elementi, si accresce il numero delle permutazioni trovate degli n elementi. Per
trovarle tutte, occorre separatamente far trovare in prima posizione tutti gli elementi deH'insieme e con ogni
fine sposta N-1 dischi fine sposta N-1 dischi elemento fissato generare le permutazioni degli n-1 elementi restanti.
sposta 1 disco
Ma come si generano le permutazioni degli n-1 elementi restanti dopo aver fissato il primo ? Semplicemente
Una soluzione Java:____________ con la stessa tecnica. Dunque la strategia è ricorsiva e può essere affidata ad un metodo Java ricorsivo che
package poo.recursion; riceve l’array ed un indice i il cui significato è che sino ad i-1 sono bloccati degli elementi (prefisso costante di
public class TorriDiHanoi{ permutazioni) e l’obiettivo consiste nel generare tutte le possibili permutazioni degli elementi che rimangono
private static enum Pin { SX, CL, DX }; da i sino all’ultima posizione.
private void spostaidisco( int da, int a )(
System.out.println(‘ Sposta un disco da ’,+da+" a "+a); Per avvicendare in posizione i, uno alla volta, tutti gli elementi a disposizione (da i a n-1), si pianifica un ciclo di
}//sposta1 disco for che ad ogni girata scambia l’elemento i-esimo con un altro e lancia (ricorsivamente) il processo di
public void muovi( int n, Pin sorg, Pin aus, Pin dest ){ generazione di tutte le permutazioni restanti. A questo punto dovrebbe essere comprensibile il codice che
if( n==1 ) spostaiDisco( sorg, dest ); segue:
else{
muovi( n-1, sorg, dest, aus ); package poo.recursion;
sposta 1Disco( sorg, dest ); import java.util.Arrays;
muovi( n-1, aus, sorg, dest );
} public class Permutazioni!
}//muovi
308 309
Capitolo 18 Tecniche di programmazione ricorsiva

public static void main( String []args ){//esempio


int a[]={1, 2, 3}; Si riportano di seguito due programmi, entrambi basati sulla tecnica backtracking e sulla ricorsione, che
permutai a, 0 ); trovano tutte le possibili soluzioni al problema delle N regine. Si assumono come punti di scelta le possibili
}//main righe della scacchiera. Le scelte a disposizione, su ogni riga, sono quindi le possibili colonne della scacchiera.
static void permutai int []a, int i ){ L’algoritmo generico tentativoQ è concretizzato dal metodo collocaReginaQ che riceve un numero di riga
if( i==a.length-1 ) System.out.println( Arrays.toString(a) ); (punto di scelta) e cerca di trovare, se esiste, una posizione di colonna (scelta) su questa riga dove collocare
else{ la regina corrente in modo che non risulti sotto attacco con le regine collocate in precedenza. Le azioni
for( int j=i; j<a.length; j++ ){ “assegnabile", “assegna”, "deassegna" e “scrivi soluzione" sono confinate in altrettanti metodi ausiliari privati.
int park=a[i]; a[i]=a[j]; a[j]=park; //scambia a[i] con a[j]
permutai a, i+1 ); Il primo programma rappresenta la scacchiera come matrice di booleani. La verifica delle condizioni di attacco
park=a(i]; a[i]=a[j]; a[j]=park; //perché nuovamente lo scambio? si ottiene navigando opportunamente sulle colonne/diagonali della scacchiera. Il secondo programma non
memorizza esplicitamente la scacchiera ma sfrutta tre array monodimensionali dove conservare le posizioni
} delle regine via via collocate. Si nota che una diagonale nord-est è caratterizzata da punti <i,j> tale che
}//permuta i+j=costante. Una diagonale nord-ovest è caratterizzata da punti <i,j> tale che incostante. Es. sulla diagonale
}//Permutazioni principale i-j=0, su quella secondaria i+j=n-1.

Problema delle N regine Una prima soluzione:_________________________________________________________________________


Si tratta di collocare N regine su una scacchiera NxN in modo che mai due regine risultino sotto aTtacco. Due package poo.recursion;
regine sono sotto attacco se risiedono su una stessa riga, colonna o diagonale. import java.util.*;
class Scacchiera{
È un tipico problema che si presta ad essere risolto per tentativi, utilizzando la tecnica backtracking. Un tale int n, numSol=0;
problema può essere schematizzato come un insieme di punti di scelta (o punti decisionali) e un insieme di boolean [][]board;
scelte che possono essere adottate su ogni punto di scelta. Un assegnamento di scelte a tutti i punti di scelta public Scacchiera( int n ){
che rispetta i vincoli del problema costituisce una soluzione del problema. Il modo di operare del backtracking if( n<=1 ) throw new HlegalArgumentExceptionQ;
è riassunto, in pseudo-codice, dal seguente metodo ricorsivo tentativo(), da adattare al problema specifico: this.n=n;
board=new boolean[n][n];
void tentativo( Punto__di_scelta ps ){ for( int i=0; i<n; i++ )
for( ogni scelta s nell'insieme delle possibili scelte ){ for( int j=0; j<n; j++ ) board[i)[j]=false;
if( s è assegnabile a ps ){ }//costruttore
assegna s a ps:
if( ps è l'ultimo punto di scelta ) scrivi soluzione: public void risolvi(){
else tentativo^ punto_discelta_successivo_a ps ); collocaRegina( 0 ); //innesca il processo
deassegna s da ps: }//risolvi

private void collocaRegina( int rig ){


}//tentativo for( int col=0; cokn; col++ )
if( assegnabile( rig, col ) ){
Cruciale è il ciclo di for che deve poter “provare” una qualsiasi scelta s su ps. Inoltre è importante considerare assegna( rig, col );
che dopo un tentativo fallimentare (un assegnamento di scelta s a ps che successivamente si rivela non in if( rig==n-1 ) scnviSoluzioneQ;
grado di condurre ad una soluzione del problema), occorre ripartire con scelte differenti da quella tentata in else collocaRegina( rig+1 );
precedenza. Assegnamenti “sbagliati" (col “senno di poi”) sui punti di scelta considerati in precedenza, deassegna( rig, col );
possono far si che nessuna scelta valida s esista per il punto di scelta corrente ps. In questo caso, il ciclo di for }
termina e si ritorna (backtrack) nella precedente attivazione di tentativo(), relativa al precedente punto di }//collocaRegina
scelta. Qui occorre deassegnare l’ultima scelta tentata sul precedente punto di scelta, ripartire con il for e se
qualche scelta alternativa esiste, riprendere l’andata in avanti (forward) dell’algoritmo, con una chiamata private boolean assegnabile( int rig, int col )(
ricorsiva di tentativoQ sul prossimo punto di scelta. Il comportamento “scatenato" di tentativoQ consente di //verifica nord
trovare tutte le possibili soluzioni del problema. Come caso particolare l’insieme delle soluzioni può essere for( int i=rig-1; i>=0; i-- )
vuoto. Se necessario, l'algoritmo di tentativo() può essere modificato in modo da arrestarsi al raggiungimento if( board[i][col] ) return false:
di un numero prefissato di soluzioni. Il processo di ricerca delle soluzioni si innesca invocando il metodo //verifica nord-est
tentativoQ sul primo punto di scelta. for( int i=rig-1,j=col+1; i>=0 && j<n; i--,j++ )
if( board[i][j] ) return false;
310 311
Capitolo 18 Tecniche di programmazione ricorsiva

//verifica nord-ovest sino a -(n-1) che si mappa sull’indice n (il primo libero dopo la zona occupata dalle diagonali con indice >=0).
for( int i=rig-1,j=col-1; i>=0 && j>=0; i—,j—) Gli altri dettagli dovrebbero essere auto-esplicativi
if( board[i][j] ) return false;
return true; package poo.recursion;
}//assegnabile import java.util.*;

private void assegna( int rig, int col ){ class Scacchiera{


board[rig][col]=true; int n, numSol=0;
}//assegna int Qc;
boolean []su;
private void deassegna( int rig, int col ){ boolean [jgiu;
board[rig][col]=false; public Scacchiera( int n ){//costruttore
}//deassegna this.n=n;
c=new int(n);
private void scriviSoluzione(){ su=new boolean[2*n-1);
numSol++; giu=new boolean[2'n-1];
System.out.print( numSol+“ " ); for( int i=0; i<2*n-1; i++ ){
for( int i=0; i<n; i++ ) if( i<n ) c[i]=-1;
for( int j=0; j<n; j++ ) su[i]=false; giu[i]=false;
if( board[i][jj ){
System.out.print( "<"+i+","+j+"> " );
break;
} public void risolvi(){
System.out.println(); collocaRegina( 0 );
}//scriviSoluzione {//risolvi

}//Scacchiera private void collocaRegina( int rig ){


lor( int col=0; cokn; col++ )
public class EnneReginef if( assegnabile( rig, col ) ){
public static void main( String []args ){ assegna( rig, col );
System.out.printlnf'Problema delle N regine"); if( rig==n-1 ) scriviSoluzione();
Scanner sc=new Scanner( System.in ); else collocaRegina( rig+1 );
System.out.print("N(>2)=“); deassegna( rig, col );
int n=sc.nextlnt(); }
new Scacchiera(n).risolvi(); }//collocaRegina
System.out.println("Fine elenco soluzioni");
}//main private boolean assegnabile( int rig, int col ){
}//EnneRegine if( c[col]!=-1 ) return false;
if( rig-col<0 && giu[(rig-col)+2*n-1] ) return false;
Una seconda soluzione:_____________________ if( rig-col>=0 && giu(rig-col] ) return false;
La scacchiera si basa ora sui tre vettori: c, su e giu. Il vettore c serve a ricordare l’assegnamento delle regine if( su[rig+col] ) return false;
presso le varie colonne: c[j] memorizza la riga su cui è stata posta la regina sulla colonna j. I vettori su e giu return true;
sono associati rispettivamente a tutte le possibili diagonali nord-est (parallele alla diagonale secondaria) e a {//assegnabile
tutte le possibili diagonali nord-ovest (parallele alla diagonale principale). Entrambi i vettori ammettono
pertanto 2*n-1 elementi. private void assegna( int rig, int col ){
c[col]=rig; su[rig+col]=true;
La verifica di assegnabilità (metodo assegnabile()) di una regina su una certa posizione <riga,colonna> è if( rig-cokO ) giu[(rig-col)+2*n-1]=true;
portata immediatamente a termine consultando c, su e giu. Da notare che per le diagonali giu negative (da -1 else giu[rig-col]=true;
a -(n-1)) si utilizza una trasformazione di coordinate consistente nel sommare alla differenza negativa {//assegna
riga -colonna la quantità 2*n-1che consente di riportare l’indice su una posizione effettiva ed unica dell'array.
In particolare, la diagonale -1 si mappa sulla posizione 2*n-2 (ultima dell'array giu) e a seguire -2 su 2‘ n-3 etc
312 313
Capitolo 18 Tecniche di programmazione ricorsiva

private void deassegna( int rig, int col ){ Considerando che un oggetto viene creato da dentro un metodo e che la new alloca memoria nello heap, si ha
c[col]=-1; su[rig+col]=false; sempre (in Java) che i riferimenti agli oggetti originano nello stack e puntano allo heap. Siccome poi un
if( rig-cokO ) giu[(rig-col)+2‘ n-1]=false; oggetto al suo interno può riferire altri oggetti (es. il campo next di un nodo di una lista concatenata), sono
else giu[rig-col]=false; possibili riferimenti heap-heap.
}//deassegna
Le due aree di memoria gestite a stack e ad heap tipicamente crescono in senso contrapposto. Ovvio che
private void scriviSoluzione(){ sotto certe condizioni (una ricorsione infinita, o un loop infinito che crea continuamente oggetti) si può avere
numSol++; uno stack-heap overflow (collisione) che comporta la terminazione del programma.
System.out.print( numSolf" " );
for( int i=0; i<n; i++ ) La gestione della memoria può anche seguire uno schema leggermente diverso da quello suggerito (classico).
System.out.print( "<“+c[i]+","+i+"> “ ); Ad es. allo stack si potrebbe assegnare un’area di dimensione prefissata e lasciare tutto il resto della memoria
System.out.println(); del programma per l’uso come heap. Anche cosi sono ovviamente possibili i trabocchi dello stack e/o dello
}//scriviSoluzione heap.

}//Scacchiera Conversione ricorsione-iterazione


Alcune volte si consegue intuitivamente. Altre volte si tratta di “imitare” il comportamento dello stack delle aree
public class Regine{ dati, ossia rendere esplicito il meccanismo di gestione delle aree dati sullo stack del programma. Si consideri il
public static void main( String []args ){ caso del programma delle TorriDiHanoi. L’area dati del metodo muovi() è costituita dal valore di n, e dalle
System.out.println("Problema delle N regine"); indicazioni rispettivamente dei paletti sorgente, ausiliario e destinazione:
Scanner sc=new Scanner( System.in );
System.out.print("N(>2)="); public void muovi( int n, Pin sorg, Pin aus, Pin dest )( //versione ricorsiva di muovi
int n=sc.nextlnt(); if( n==1 ) spostaiDisco( sorg, dest );
new Scacchiera(n).risolvi(); else{
}//main muovi( n-1, sorg, dest, aus );
}//Regine spostai Disco( sorg, dest );
muovi( n-1, aus, sorg, dest );
Stack ed Heap }
Le aree dati dei metodi obbediscono naturalmente ad una gestione a stack (o pila). In corrispondenza ad una }//muovi
chiamata ad un metodo (ricorsiva o no), si crea una nuova area dati e la si colloca in cima allo stack che
memorizza le aree dati. Quando termina l’attivazione corrente, l’area dati da distruggere è quella che si trova Si può introdurre una classe ad hoc AreaDati contenente i quattro parametri citati. Quindi usare uno stack (es.
in cima allo stack. una linked list) su cui inizialmente si pone l'area dati della chiamata originale a muovi() che attiva il processo
ricorsivo. A questo punto, un algoritmo iterativo si può agevolmente ottenere ripetendo sino alla condizione di
buco stack vuoto le seguenti azioni:

buco (1 ) prelievo dell’area dati corrente dalla cima dello stack


(2) ispezione del valore di n:
se n==1, si pianifica lo spostamento di 1 disco;
se n>1, si tratta di “schedulare" le tre azioni della parte else sullo stack, ponendole esattamente nell’ordine
inverso a come compaiono nel metodo muovi() e facendo in modo che la chiamata intermedia a
spostaidisco() venga simulata allo stesso modo delle chiamate ricorsive ma con n=1.
cima dello stack
Stack public class TorriDiHanoi{

Un programma Java riceve, a supporto della sua esecuzione, un'area di memoria che è gestita da un parte a public void muovilte( int n, Pin sorg, Pin aus, Pin dest ){//versione iterativa
stack (per far fronte alle chiamate dei metodi, anche in versione ricorsiva), dal lato opposto a heap (o class AreaDati{ //inner class
mucchio). La memoria heap è utilizzata per allocare/deallocare dinamicamente gli oggetti (istanze di classi). int n; Pin sorg, aus, dest;
Essa si accompagna ad una gestione complessa: si pensi ad esempio che dopo aver in sequenza creato 10 AreaDati( int n, Pin sorg, Pin aus, Pin dest ){
oggetti, si può avere che il secondo non serve più e viene deallocato, creando cosi un “buco”. Il gestore della this.n=n; this.sorg=sorg; this.aus=aus; this.dest=dest;
memoria deve mantenere traccia dei “buchi” che si determinano, al fine di sfruttarli. }
}//AreaDati

314 315
Capitolo 18 Tecniche di programmazione ricorsiva

Stack<AreaDati> stack=new StackConcatenato<AreaDati>(); Un metodo ricorsivo per l’ordinamento: Merge Sort


stack.push( new AreaDati( n, sorg, aus, dest ) ); //simula prima chiamata Merge sort è un classico algoritmo per l’ordinamento di un vettore, intrinsecamente ricorsivo e basato sulla
while( stack.size()!=0 ){ strategia divide-et-impera. Dato un segmento di vettore v individuato da due indici inf e sup, l'algoritmo divide
AreaDati ad=stack.pop(); v in due segmenti della stessa (o quasi) dimensione, diciamoli s1 ed s2. Ordina separatamente s1 ed s2.
if( ad.n==1 ) sposta1disco( ad.sorgente, ad.destinazione ); Dopo questo s1 ed s2 sono al loro interno ordinati. Quindi fonde (merge) s1 ed s2 in modo da costruire un
else{ unico segmento ordinato che sostituisce v. Ma come vengono ordinati s1 ed s2 ? Semplice: con la stessa
stack.push( new AreaDati( ad.n-1, ad.aus, ad.sorg, ad.dest) ); tecnica. Di seguito si mostra un’implementazione di merge sort pensata come metodo di utilità della classe
stack.push( new AreaDati( 1, ad.sorg, ad.aus, ad.dest) ); Array del package poo.util. L’implementazione è affidata a due metodi generici nel tipo T degli elementi:
stack.push( new AreaDati( ad.n-1, ad.sorg, ad.dest, ad.aus) );
} public static <T extends Comparable<? super T » void mergeSort( T[] v, int inf, int sup ){...}
} private static <T extends Comparable<? super T » void merge( T[j v, int inf, int med, int sup ){...}
}//muovilte
Il metodo privato merge() è ausiliario a mergeSort() e realizza la fusione ordinata (si veda anche il cap. 12) dei
public static void main( Stringi) args ){ contenuti dei segmenti s1 (individuato dagli indici da inf a med) ed s2 (individuato dagli indici da med+1 a sup).
new TorriDiHanoi().muovilte(3, Pin.SX, Pin.CL, Pin.DX );
}//main public static <T extends Comparable<? super T » void mergeSort( T[] v, int inf, int sup ){
if( inf<sup ){//segmento da ordinare non vuoto e con almeno 2 elementi
}//TorriDiHanoi int med=(inf+sup)/2; //indice mediano
mergeSort( v, inf, med );
Si può obiettare che il peso delle chiamate ricorsive è rimasto nelle invocazioni dei metodi push/pop delle mergeSort( v, med+1, sup );
stack. In realtà è possibile evitare anche queste chiamate a metodi, esplicitando lo stack ed espandendo il merge( v, inf, med, sup );
codice delle operazioni push e pop come mostrato di seguito. }
}//mergeSort
public void muovilterativo( int n, Pin sorg, Pin aus, Pin dest ){
//simulazione area dati @SuppressWamings("unchecked’’)
final int DIM=200; private static <T extends Comparable<? super T » void merge( T[] v, int inf, int med, int sup ){
int N[]=new int[DIM]; T[] aux=(T[])new Comparable[sup-inf+1); //vettore ausiliario
Pin sourceO=new Pin[DIM); int i=inf, j=med+1, k=0;
Pin aux[]=new Pin(DIM); while( i<=med && j<=sup )
Pin destination[]=new Pin[DIM); if( v[i].compareTo(v[j])<=0 ){ aux[k]=v[i); i++; k++;}
int top=0; //indice cima dello stack elsej aux[k]=v[j); j++; k++;}
//prima chiamata //gestione residuo
N[top]=n; source[top]=sorg; aux[top]=aus; destination[top]=dest; top++; //push while( i<=med ){ aux[k]=v[i]; i++; k++;}
while( top!=0 ){ while( j<=sup ){ aux[k]=v[j]; j++; k++;}
top--; //pop area dati corrente //ricopia di aux su v tra inf e sup
n=N[top); sorg=source[top]; aus=aux[top); dest=destination[top); for( k=0; k<aux.length; k++ ) v[inf+k]=aux[k];
if( n==1 ) spostaidisco( sorg, dest ); }//merge
else{
N[top]=n-1; source[top]=aus; aux[top]=sorg; destination[top]=dest; top++; Il metodo merge() utilizza un vettore ausiliario aux in quanto la fusione dei due segmenti ordinati in v su v
N[top]=1; source[top]=sorg; aux[top]=aus; destination[top]=dest; top++; stesso comporterebbe, per sovrascrittura, la perdita di informazioni, aux ha dimensione strettamente
N[top]=n-1; source[top]=sorg; aux[top]=dest; destination[top]=aus; top++; corrispondente ai bisogni (sup-inf+1). merge() realizza il ciclo di fusione mediante due indici / e j che si
muovono separatamente sui due segmenti. Un terzo indice k si muove sul vettore aux. Ad ogni iterazione, il
minimo tra v[i] e v[j] è scritto su aux in posizione k, quindi si avanza l’indice sul segmento da cui proviene il
}//muovilterativo minimo e, ovviamente, si avanza k. Non appena si svuota uno dei due segmenti, si esce dal primo ciclo di
while e, per completare la fusione, si provvede a ricopiare su aux il segmento non vuoto residuo. Il metodo
Il metodo muovilterativo() prefissa una dimensione massima dello stack (DIM=200) e crea quattro array merge termina il suo compito copiando il vettore aux su v, sugli indici da inf a sup.
paralleli corrispondenti ai quattro parametri/attributi dell’area dati. Una variabile locale top mantiene di
momento in momento la cima (indice del primo slot libero) dello stack, ossia il numero di elementi presenti Detto n il numero di elementi in v, è facile verificare che la complessità di merge() è lineare: O(n). Infatti,
nello stack. In questa formulazione, più efficiente, non sussistono chiamate a metodi ausiliari. Per semplicità merge() deve sistemare n elementi, provenienti dai due sotto vettori (da inf a med e da med+1 a sup) e
non si controlla il traboccamento dello stack. dunque compie complessivamente un numero di operazioni proporzionale ad n.

316 317
Capitolo 18 Tecniche di programmazione ricorsiva

Il lavoro di ordinamento si consegue attraverso la decomposizione ricorsiva che si estende sino aH’ottenimento T(n) T(nl 2) ^
di segmenti con singoli elementi. Applicando merge() a questi singoletti si ritrovano le usuali operazioni di n ni 2
confronto e scambio. La figura che segue illustra il processo (ad albero binario) di decomposizione del Tini 2) T(nl 4)
segmenti:
ni 2 ni4
0 12 3 T{ nl 4 ) _ 7 \ r t / 8 )
4 3 2 1 ni 4 n/ S

/ '), 7X4) = 7X2) + i


0 1 2 3 4 2
4 3 2 |1 7X2) = 7X1) + ,

0
A A 1 2 3
2 I 1

Ma T(1)=1 in quanto per n=1 mergeSort() esegue solo il test per concludere che inf non è minore di sup e può
È OD [Il DEI quindi tornare subito al chiamante.

In realtà, il modo di procedere di mergeSort() genera prima il sotto albero sinistro sino alle foglie (singoletti) 4 e Si può osservare che il numero delle uguaglianze esplicitamente ottenute sviluppando la formula di ricorrenza
3. L’applicazione di merge a questi segmenti scambia il 4 con il 3 nel nodo centrale di sinistra. Quindi si è pari al numero massimo dei possibili dimezzamenti di n (suddivisioni di v tra inf e sup in due segmenti e cosi
genera il sotto albero destro corrispondente alla coppia <2,1>, arrivando ai singoletti 2 ed 1 che a seguito della via), ossia è uguale a log? n.
fusione vengono scambiati. A questo punto il processo ha ordinato i due segmenti di dimensione due:
Sommando tutti i primi membri e i secondi membri si ottiene evidentemente un’altra uguaglianza. In più, è
0 12 3 facile previsione che molti termini, trovandosi identicamente (cioè con lo stesso segno) a primo membro e a
4 3 2 1 secondo membro, si cancellano (somma telescopica). Pertanto, l’eguaglianza delle somme dei primi membri e
dei secondi membri porta al risultato:

0
4 \
2 3
3 4 1 2 =7(1)+ log, n
n
La fusione dei due segmenti di dimensione 2 completa l’ordinamento del vettore originale. ossia
0 12 3 T ( n ) = n * 7 (1) + n * l o g , n
1 2 3 4
ed ancora:
Complessità di Merge Sort _
Per determinare la complessità di tutto l'algoritmo di merge sort, si può scrivere la seguente formula di T(n) = n + n * log, n
ricorrenza (2 chiamate ricorsive seguite da una chiamata di merge()):
Applicando l’analisi asintotica, rimuovendo cioè i coefficienti e termini di grado inferiore, si ottiene (il logaritmo
T(n) = 2*T(n/ 2) + TmrrKr(n)
è al solito in base 2):

dove Tmerge(n) è il tempo del metodo merge(). A meno di qualche costante, si può scrivere anche: /'(/i) = ( ) ( n * log n )

T(n) = 2 *T(n / 2) + n Il risultato è teorico. Per sfruttare effettivamente l'efficienza di merge sort l’algoritmo andrebbe riscritto in veste
iterativa (un buon esercizio).
dal momento che mergeQ ha complessità lineare. Dividendo per n i due membri si ha:

T(n) T(nl 2) ,
-------= ----------- - + I
n ni 2

Sviluppando tale formula di ricorrenza, si ha la seguente catena di uguaglianze:

318 319
Capitolo 18 Tecniche di programmazione ricorsiva

Il metodo di ordinamento QuickSort __ conseguente possibilità di accesso fuori dai limiti dall'array v. Per evitare questo problema conviene riscrivere i
Anche questo metodo suddivide il vettore iniziale in due segmenti s1 ed s2 ma lo fa con un criterio diverso. due cicli come segue:
Infatti s1 ed s2 vengono ora determinati in modo tale che tutti gli elementi di s1 risultino ordinati rispetto agli
elementi di s2. Cioè s1[i]^s2[j] per tutte le coppie valide i e j. Al loro interno s1 ed s2 sono in generale ancora while( v.get(i).compareTo(x)<0 ) i++;
disordinati per cui il metodo prosegue ordinando separatamente si ed s2t utilizzando ricorsivamente la stessa while( v.get(j).compareTo(x)>0 ) j--;
tecnica. Per le proprietà di s1 ed s2 non è evidentemente richiesta alcuna fase finale di ricombinazione dei
loro valori. L’operazione che suddivide il vettore in due sottovettori con le proprietà menzionate si chiama e fermarsi anche su valori uguali al perno x. Tali valori vengono egualmente (ma inutilmente) scambiati.
partizionamento. Per generalità, l'algoritmo quick sort verrà descritto con riferimento ad un vettore di elementi Tuttavia il fenomeno non è grave a patto che nel vettore non ricorrano molti valori uguali. A fine ciclo di
(oggetti) confrontabili, mediante due metodi generici, uno pubblico l’altro privato, nella classe di utilità Array del partizionamento (do-while), i valori di i e j non sono necessariamente adiacenti. In questi casi, tutti gli elementi
package poo.util. Il vettore è assunto una java.util.List. In prima approssimazione si ha. in v da j+1 a i-1 sono uguali tra loro e uguali al perno x. È chiaro che questi valori possono non essere più
considerati nel prosieguo del metodo. Pertanto è lecito assumere j come estremo superiore (sup1) del primo
public static <T extends Comparable<? super T » void quickSort( List<T> v ){ segmento, ed i come estremo inferiore (inf2) del secondo segmento. Segue la formulazione completa del
quickSort( v, 0, v.size()-1 ); metodo generico privato quickSort:
}//quickSort
private static <T extends Comparable<? super T » void quickSort( List<T> v, int inf, int sup ){
private static <T extends Comparable<? super T » void quickSort( List<T> v, int inf, int sup )( if( inksup ){
if( inksup ){ T x=v.get((inf+sup)/2); //perno
partiziona v in due segmenti v[inf..sup1] e v[inf2..sup) int i=inf, j=sup;
quickSort( v, inf, sup1 ); do{
quickSort( v, inf2, sup ); while( v.get(i).compareTo(x)<0 ) i++;
} while( v.get(j).compareTo(x)>0 ) j--;
}//quicksort if( k=j ){
//scambia
L’operazione di partizionamento può essere realizzata come segue. Sia x un valore particolare del vettore T park=v.get(i); v.set(i,v.get(j));
(perno o pivot). Partendo da due segmenti vuoti, gradualmente si provvede a riempire il primo con i valori non v.set(j,park);
maggiori di x, il secondo con i valori non minori di x. In concreto: si spazzola il vettore v con due indici nelle i++; j--;
due direzioni contrapposte. Ogni qualvolta si incontra un valore nella relazione voluta nei confronti di x, si }
estende banalmente il segmento in questione. I valori che bloccano l’estensione dei segmenti, vengono }while( i<=j );
scambiati immediatamente, dopo di che si riprende il processo che termina allorquando i due indici si quickSort( v, inf, j );
intersecano. L'algoritmo di partizionamento è presentato di seguito. Come scegliere il valore di x? Idealmente quickSorH v, i, sup );
si dovrebbe adoperare il valore mediano tra quelli di v, in grado di dar luogo a due segmenti con la stessa }
dimensione. In pratica si sceglie un valore a caso tipo: }//quicksort

T x = v.get((inf+sup)/2); //perno Per avviare l’algoritmo è sufficiente un’istruzione di chiamata del metodo generico pubblico come segue:

dunque si ha: Array.<lnteger>quickSort( lista );

T x=v.get((inf+sup)/2); //perno Nel caso migliore (ogni scelta di x suddivide il segmento di v in due segmenti della stessa dimensione)
int i=inf, j=sup; l’algoritmo ha un costo del tipo ()( n * log n ). Nel caso peggiore (ogni scelta di x è il massimo o il minimo nel
do{ segmento di v da ordinare) la complessità di quick sort degrada a quella di selection sort: 0 ( n 2 ). Un'analisi
while( v.get(i).compareTo(x)<=0 ) i++; più complessa (si veda ad es. M.A. Weiss: Data structures and algorithm analysis, The Benjamin/Cummings
while( v.get(j).compareTo(x)>=0 ) j--; Pub. C, 1992) dimostra che mediamente la complessità è ()(n * \o g n ), ciò che fa di quick sort uno dei
if( k = j){
migliori metodi di ordinamento.
//scambia
T park=v.get(i); v.set(i,v.get(j)); v.set(j.park);
Per generalità, la classe di utilità Array rende disponibili versioni overloaded dei metodi mergeSort e quickSort
i++; j--;
capaci di operare su array di interi, di doublé, su array generico in T, su Vector<T> etc.
}
}while( i<=j );

La scelta a caso del perno può comportare che venga selezionato il massimo o il minimo tra quelli da ordinare
col rischio che uno dei due cicli di while possa lasciare il suo indice dopo sup o prima di inf, con la
320 321
In d ic e
Capitolo 1;............................................................................................................................................................... 1
Concetti di programmazione procedurale in Java..................................................................................................1
Un primo programma:............... ...............................................................................................................1
Formalo dott'outpul:..................................................................................................................... 3
Tipi di baso.........................................................................................................................................................3
Conversioni di tipo e caslmg............................................................................................................................ 4
Incromonto/decremonto e assegnamento con aritmetica................................................................................. 4
Algebra di Boole.................................................................................................................................................5
ProprlotO dell'algebra di Boole.......................................................................................................................... 5
Operatori booleani e corto circuito.................................................................................................................... 6
La classe Math....................................................................................................................................................7
Classe Scanner (Java 5 o versiono superiore).................................................................................................7
Il metodo punti di System.out {Java 5 o versione superiore)............................................................................8
Espressioni o assegnazione............................................................................................................. 8
Strutture di controllo.......................................................................................................................................... 8
Selezione a due vie (it-else)..................................................................................................................«......... 9
Un esempio di programma:............................................................................................................................fl
Compilaziono/osocuziono del programma .......... 9
Ciclo di while (o a condiziono iniziale).............................................................................................................IO
Ciclo di while a condizione tinaie......................................................................................................................10
ll-implicilo (operatore ?)....................................................................................................................................11
Selezione n-aria {switch) Analisi dei casi possibili.......................................................................................... 11
Istruzione tor..................................................................................................................................................... 11
Un programma por il calcolo dotta potenza a " ................................................................................................12
Un programma por l'equazione di secondo grado......................................................................................... 13
Massimo comun divisore ed algoritmo di Euclide........................................................................................... 13
Somma doi primi N numerinaturali...................................................................................................................14
Equivalenza di Gauss.......................................................................................................................................14
Calcolo del tattonale.........................................................................................................................................14
Calcolo del mem...............................................................................................................................................15
Calcolo del fattoriale affidalo adun metodo...................................................................................................... 15
Un metodo polonzn..........................................................................................................................................16
Un secondo metodo potenza...........................................................................................................................16
Un terzo metodo potenza.................................................................................................................................17
Un metodo che vorilica so un Intero positivo e prim o......................................................................................17
Molodo di Newton per il calcolo dotta radice quadrala di un numero roale................................................... 17
Sviluppo di un programma....................... 18
Programma Lotto:........................................................................................................................................18
Caso di studio: sviluppo di un programma Calendario per passi successivi................................................. 20
Voesiono di massima dot programma- ......... ?0
Programma completo ........................................ 2?
Poifo/ionnmonti:................................................................................................................. ... ...............23
Strutlura/iono in molodi..............................................................................................................................24
Un altro caso di studio: Sottosequenza di dimensione massima.................................................................... 25
Un primo algoritmo:..... 25
Un programma Java ............. 26
Nuova versione del programma.......................................................................... 27
Esercizi.............................................................................................................................................................28
Capitolo 2 :.............................................................................................................................................................29
Strutture dati Array................................................................................................................................................29
Array monodimonsionali o vettori.....................................................................................................................29
Capitolo 18

Esercizi ___ Capitolo 19:


1. Scrivere in veste ricorsiva il calcolo del fattoriale di n>=0. Si ricorda che:
Strutture dati ricorsive e non lineari
n!=n*(n-1)*(n-2)‘ ...*3*2*1 sen>1
n!=1 se n=0 Lista concatenata e ricorsione________ _________________________________________ _____________
Il metodo è o no tail recursive?
La lista concatenata è una struttura dati naturalmente ricorsiva. Tutto ciò risulta intanto dalla struttura del tipo
2. Scrivere in veste ricorsiva un metodo che riceve un intero positivo n e lo scrive su output alla rovescia. Ad
nodo che ammette un campo next di tipo nodo (dichiarazione ricorsiva). Una lista concatenata è suscettibile di
esempio, se riceve n=236, deve scrivere 632.
essere interpretata in forma ricorsiva cosi da essere manipolabile con metodi ricorsivi quando si consideri la
3. Scrivere un metodo ricorsivo somma(...) basato sulla tecnica divide-et-impera che riceve, tra l’altro, un
seguente definizione:
vettore v di interi, calcola la sommatoria degli elementi e la restituisce.
4. Scrivere in veste ricorsiva l'algoritmo di ricerca binaria di un elemento x (di una classe Comparable) in un Una lista o è vuota oppure consiste di un primo nodo (capolista) seguito da una lista (residua)
vettore v di Comparable, nell'ipotesi che il vettore sia ordinato per valori crescenti.
5. Seguendo la tecnica backtracking, scrivere un metodo ricorsivo void disposizioni(int[]a,int i) che genera su Dovendo cercare un elemento in una lista, si può sfruttare la ricorsione come segue, se la lista è vuota allora
un array a di n posizioni tutte le disposizioni (con ripetizioni) di n bit (disposizioni di due oggetti 0 e 1 su n
la ricerca fallisce: altrimenti si verifica se l'elemento è nel nodo capolista: se si, la ricerca termina con
posti). Se n=3, le disposizioni sono:
successo; se no si prosegue la ricerca sulla lista residua, invocando il metodo di ricerca sul next del capolista.
Come esempio dimostrativo, di seguito si considera una lista concatenata generica nel tipo degli elementi, che
000
implementa l’interfaccia poo.util.CollezioneOrdinata del cap. 15. Per ragioni di leggibilità, la classe nodo è
001
convenientemente ridenominata Lista, e la testa è detta semplicemente lista. Si ignora per semplicità
010
l’iteratore (che si programma come mostrato nel cap. 15).
011
100
package poo.recursion;
101
import poo.util.CollezioneOrdinata;
110
111
public class ListaReccT extends Comparable<? super T » implements CollezioneOrdinata<T>{
private static class Lista<E>{
Similmente al caso del problema delle regine, il parametro i indica l’indice in cui collocare il prossimo bit. Prima
E info;
di i è stato già definito un preambolo.
Lista<E> next;
6. Data una matrice quadrata a nxn, si può calcolarne il determinante come segue:
}//Lista
- se n=1, allora la matrice consiste del solo elemento a[0][0] e il valore del determinante coincide con questo
private Lista<T> lista=null;
elemento
- se n>1, allora si può applicare la regola di Laplace, es. rispetto ad una riga qualsiasi i: A questo punto (gran parte de)i metodi di ListaRec si possono sviluppare in veste ricorsiva secondo questo
- det=2^=o.n i) a[i]0]x(-1)<l+i>xdet(minore(a,i,j)) schema: ogni metodo ha una versione pubblica (per i clienti) che si ricollega alla interfaccia
All’atto pratico si può scegliere i=0. Si vede che il calcolo del determinante di a è ricondotto a quello dei Collezioneordinata, ma delega un metodo ricorsivo privato a tornire il corpo di esecuzione (risposta). Vediamo
determinanti dei minori di ordine (n—1)x(n—1) considerati lungo la riga i. Il minore rispetto alla posizione <i,j> è il metodo size():
la (sotto) matrice che si ottiene eliminando la riga i e la colonna j della matrice di partenza. Ottimizzazione: per
“velocizzare” il calcolo, anziché arrivare a matrici di ordine 1x1, si possono arrestare i rimandi ricorsivi quando public int size(){ //versione pubblica
si incontrano matrici di ordine 2 (o 3), di cui si sa calcolare banalmente il determinante. Una versione iterativa return size( lista ); //invocazione metodo privato ricorsivo con la lista (testa)
dell'algoritmo è già nota e si fonda sulla triangolazione di Gauss. }//size
7..È nota la successione di Fibonacci: 1 1 2 3 5 8 13 21 34 ... in cui il primo ed il secondo numero sono uguali
ad 1, ogni altro elemento (dal terzo in poi) è la somma dei due numeri che immediatamente lo precedono. Si private int size( Lista<T> lista ){//versione privata ricorsiva
vuole scrivere un metodo long fibo(int n) della classe di utilità poo.util.Mat che restituisce l’n-esimo (n>=1) if( lista==null ) return 0;
numero della serie di Fibonacci. Esistono diversi algoritmi per calcolare fibo(n). Segue un esempio. Sia A una return 1+size( lista.next );
matrice 2x2 di interi cosi definita: {{1,1},(1,0)}. Si può dimostrare (per induzione) che l’n-esimo numero della }//size
serie di Fibonacci coincide con l’elemento [0][0] della matrice potenza An1. Concretamente, il metodo fibo(n)
può basarsi su un metodo privato ricorsivo per il calcolo della potenza di An 1se n>1. Per velocizzare il calcolo, Il metodo ricorsivo sfrutta direttamente la definizione ricorsiva della lista. Se essa è vuota, allora size è 0; se
si può usare la seguente tecnica logaritmica: A ^A ^x A *2 se k è pari, A ^A ^x A ^x A se k è dispari. A*2 si non è vuota, la size della lista è 1 (conta il capolista) in più della size della lista residua. Il metodo containsQ
calcola poi allo stesso modo. sfrutta la ricorsione e l’ordinamento:
8. Utilizzando i suggerimenti forniti in questo capitolo, riscrivere in veste iterativa l'algoritmo di merge sort.
9. Come l’esercizio 8 ma con riferimento all’algoritmo quick sort. public boolean contains( T elem ){//versione pubblica
10. Provare a scrivere in veste iterativa l'algoritmo backtracking utilizzato per risolvere il problema delle 8 return contains( lista, elem );
regine. }//contains

322 323
Capitolo 19 Strutture dati ricorsive e non lineari

private boolean contains( Lista<T> lista, T elem ){//versione privata ricorsiva public void remove( T elem ){
if( lista==null ) return false; lista=remove( lista, elem );
if( lista.info.equals(elem) ) return true; }//remove
if( lista.info.compareTo(elem)>0 ) return false;
return contains( lista.next, elem ); private Lista<T> remove( Lista<T> lista, T elem ){
}//contains lf( lista==null )
return lista;
public T get( T elem ) { return get( lista, elem ); }//get if( lista.info.compareTo(elem)>0 )
return lista;
private T get( Lista<T> lista, T elem ){ if( lista.info.equals(elem) ){
if( lista==null II lista.info.compareTo(elem)>0 ) return nuli; return lista.next;
if( lista.info.equals(elem) ) return lista.info; }
return get( lista.next, elem ); lista.next=remove( lista.next, elem );
}//get return lista;
}//remove
Più complesso è il metodo add() che aggiunge alla lista un nuovo elemento in ordine.
I metodi isEmpty(), isFull(), clear() si possono realizzare direttamente col solo metodo pubblico:
public void add( T elem ){
lista=add( lista, elem ); public boolean isEmpty(){ return lista==null; }//isEmpty
}//add
public boolean isFull(){ return false; }//isFull
private Lista<T> add( Lista<T> lista, T elem ){
if( lista==null ){ public void clear(){ lista=null; }//clear
lista=new Lista<T>();
lista.info=elem; lista.next=null; Naturalmente la classe ListaRec<T> dovrebbe essere dotata anche dei metodi equals(), toString() e
return lista; hashCode(). Di seguito si mostra solo il toString(). Gli altri due metodi, da realizzare anch’essi in modo
} ricorsivo, sono lasciati come esercizio del lettore.
if( lista.info.compareTo(elem)>=0 ){
Lista<T> nuovo=new Lista<T>(); public String toString(){ //versione pubblica
nuovo.info=elem; nuovo.next=lista; StringBuilder sb=new StringBuilder(200); //sb è passato al metodo ricorsivo
return nuovo; sb.append('[');
} toString( lista, sb );
lista.next=add( lista.next, elem ); sb.append(’)');
return lista; return sb.toStringQ;
}//add }//toString

Siccome il metodo add() modifica la lista in quanto aggiunge un nuovo nodo in una qualunque posizione private void toString( Lista<T> lista, StringBuilder sb )(
(anche in testa), la versione ricorsiva è stata programmatain modo da restituire una lista (la lista modificata) da if( lista==null ) return;
assegnare (nel metodo public) al campo lista di this. Il metodo privato ricorsivo riceve due parametri: una lista sb.append( lista.info );
e l’elemento da inserire.Se la lista ricevuta è vuota, un nuovo nodo è creato ed inizializzato con l’elemento e if( lista.next!=null )
restituito come lista-risultato del metodo. Se la lista non è vuota e l'elemento va posto prima del primo sb.appendf, “);
elemento (inserimento intesta) allora si crea un nuovo nodo e lo si inizializza con l’elemento, si collega il nodo toString( lista.next, sb );
in modo da avere come lista residua la lista ricevuta come parametro; infine si ritorna il nodo come lista- }//toString
risultato. Se la lista non è vuota e l’elemento va posto dopo il capolista, allora non resta cheinvocare
ricorsivamente il metodo sulla lista residua di quella ricevuta, stando attenti che il risultato che la chiamata
ricorsiva restituirà andrà usato come nuova lista residua della lista ricevuta e quest'ultima dovrà essere
ritornata come risultato del metodo.

La scrittura del metodo add() suggerisce la seguente realizzazione del metodo remove() in veste ricorsiva, il
cui studio è lasciato al lettore.

324 325
Capitolo 19 Strutture dati ricorsive e non lineari

Albero binario Ogni nodo possiede l’informazione di un elemento e due puntatori, figlioS e figlioD, rispettivamente riferimenti
Un albero binario rappresenta una struttura dati non lineare in cui gli elementi sono posti secondo una alla radice del sotto albero sinistro e alla radice del sotto albero destro. Un sotto albero vuoto è denotato dal
relazione gerarchica padre-figlio. La struttura è intrinsecamente ricorsiva: un albero binario o è vuoto o valore nuli del relativo puntatore.
contiene un nodo detto radice dell'albero cui sono “attaccati" due (sotto) alberi detti rispettivamente il sotto
albero sinistro ed il sotto albero destro della radice. Tutto ciò vale ricorsivamente su ogni nodo dell’albero. Un ABR ha proprietà simili ad un TreeSet di java.util. La differenza è che in un TreeSet non sono ammessi
duplicati, mentre in un ABR in generale ciò è possibile.
Un albero binario si dice di ricerca (ABR) se l’informazione nel nodo radice è non minore delle informazioni
esistenti nel sotto albero sinistro della radice (nodi predecessori), e non maggiore delle informazioni esistenti Al fine di dimostrare come si possa gestire un albero binario ed in particolare un ABR in Java, si propone una
nel sotto albero destro della radice (nodi successori). Questa definizione è verificata ricorsivamente su ogni classe generica AlberoBinarioDiRicerca che implementa l’interfaccia poo.util.CollezioneOrdinata:
nodo dell’ABR:
radice La classe AlberoBinarioDiRicercacTx___________________________________________________________
package poo.recursion:
import java.util.Iterator;
import poo.util.CollezioneOrdinata;
public class AlberoBinarioDiRicercacT extends Comparable<? super T»implements CollezioneOrdinata<T>{
private static class Albero<E>{
E info;
Albero<E> figliosinistro, figlioDestro;
}//Albero

Un ABR si presta a supportare la ricerca binaria: dovendo cercare un elemento x, se esso si trova nella radice, private Albero<T> radice=null;
la ricerca termina con successo: se non si trova nella radice, la ricerca prosegue o sul sotto albero sinistro (x è
minore della radice) o nel sotto albero destro (x è maggiore della radice). Quando la ricerca interessa un //metodi pubblici di interfaccia - alcuni rimandano a metodi privati ricorsivi
albero vuoto, allora essa termina con fallimento. La ricerca delineata approssima tanto più la ricerca binaria public int size(){ return size(radice); }//size
quanto più l’albero è bilanciato, vale a dire che la numerosità degli elementi nel sotto albero sinistro è la public boolean contains( T elem )( return contains(radice.elem); }//contains
“stessa” di quella del sotto albero destro e ricorsivamente ciò è verificato su ogni nodo dell’albero. Si può public T get( T elem ) { return get( radice, elem ); }//get
assumere che un albero binario sia bilanciato se preso un qualsiasi nodo, la cardinalità del sotto albero public void clear(){ radice=null; }//clear
sinistro è uguale o al più differisce di 1 da quella del sotto albero destro. L’albero di esempio non è bilanciato. public boolean isEmpty(){ return radice==null; }//isEmpty
public boolean isFull(){ return false; }//isFull
Nodi come 10, 23 e 28 nell’albero di figura sono detti terminali o foglie dell’albero. 25 è un nodo intermedio o public void add( T elem ) { radice=add( radice, elem ); }//add
non terminale. public void remove( T elem )( radice=remove( radice, elem ); }//remove

Si chiama cammino in un albero ogni percorso che dalla radice conduca ad una foglia. @SuppressWarnings(“unchecked")
public boolean equals( Object x ){
Si dice altezza di un albero binario la lunghezza massima dei cammini. Nel caso dell’esempio, l'altezza è 2 if( !(x instanceof AlberoBinarioDiRicerca) ) return false;
(misurata in numero di archi). Detta h l’altezza ed n il numero dei nodi dell’albero si ha che: 2h£n<2tu1. if( x==this ) return true;
return equals( this.radice, ((AlberoBinarioDiRicerca)x).radice );
I nodi di un albero binario sono disposti su livelli. La radice è sul livello 0. I figli della radice sono sul livello 1 }//equals
etc. Un livello / ha al massimo 2' nodi. Se liv è il numero dei livelli di un albero, il numero complessivo dei nodi
n risulta: n<2llv. public String toString(){
StringBuilder sb=new StringBuilder(200);
Un albero binario può essere rappresentato in memoria come mostrato di seguito: sb.append(‘[’); toString( radice, sb );
*9 ** ftglloO if( sb.length()>0 ){ sb.setLength( sb.length()-2 );} //rimuove ultimo separatore ", "
7118 [7 sb.appendf]');
return sb.toStringQ;
}//toString
■. h o " . 7 |25[ v
public int hashCode(){ return hashCode(radice); }//hashCode
\ .1

public void visitaSimmetrica(){ visitaSimmetrica(radice); }//visitaSimmetrica


public void visitaSimmetrica( List<T> l){ visitaSimmetrica(radice.l); }//visitaSimmetrica
326 327
r
Capitolo 19 Strutture dati ricorsive e non lineari

//metodi privati ricorsivi


private int size( Albero<T> radice ){ casi a) e b) possono essere agevolmente realizzati, come mostrato nelle due figure che seguono:
if( radice==null ) return 0;
return 1+size( radice.figlioSinistro )+size( radice.figlioDestro ); 1
}//size 5
/ 5
-V Y-- N
private boolean contains( Albero<T> radice, T elem ){ 2 40 (40 )
if( radice==null ) ZJ
> \
return false;
if( radice.info.equals(elem) ) « i (25 52
return true; s\
\ ;sv
if( radice.info.compareTo(elem)>0 ) 18 36 55 18 36 '55
return contains( radice.figlioSinistro, elem );
return contains( radice.figlioDestro,elem );
}//contains 33 37 .33 37

private T get( Albero<T> radice, T elem ){ (3 8 31 1 (3 8 )


if( radice==null ) 31
return nuli; . •
32 i 32
if( radice.info.equals(elem) )
return radice.info;
if( radice.info.compareTo(elem)>0 ) Nodo da eliminare 32, caso a) Nodo da eliminare 31, caso b)
return get(radice.figlioSinistro, elem);
return get(radice.figlioDestro.elem); 1
}//get

private Albero<T> add( Albero<T> radice, T elem ){


if( radice==null ){
radice=new Albero<T>();
radice.info=elem;
radice.figlioSinistro=null;
radice.figlioDestro=null;
return radice;
}
if( radice.info.compareTo(elem)>=0 ){
radice.figlioSinistro=add( radice.figlioSinistro,elem );
return radice;
}
radice.figlioDestro=add( radice.figlioDestro,elem );
return radice;
}//add
Nodo da eliminare 25, caso c2), vittima 31
Il metodo add() segue la stessa logica vista sulla lista concatenata ricorsiva, adattata al caso dell'albero
binario di ricerca. Il metodo ritorna un albero per tener conto della modifica che la struttura subisce a causa Nel caso c) le difficoltà sono legate al fatto che non si può rimpiazzare un puntatore (quello che riferisce il
deH’inserimento di un nuovo elemento. nodo da eliminare) con due puntatori (i riferimenti figliosinistro e figlioDestro che escono dal nodo obiettivo).
Pertanto, si segue una logica differente. Si cerca un nodo “vittima’’ nel sotto albero destro 0 in quello sinistro
Più complessa è l'operazione di rimozione di un elemento data la caratteristica gerarchica dell’albero. Si del nodo da rimuovere, in modo che il nodo vittima rientri in uno dei casi a) 0 b). Trovata la vittima, la si
possono individuare tre casi principali a seconda che la rimozione riguardi: "promuove” ossia si scrive la sua informazione nel nodo candidato alla rimozione, quindi si rimuove la vittima.
a) una foglia dell'ABR;
b) un nodo con un solo figlio; Come esempio concreto, si decide di eleggere come vittima il nodo più a sinistra nel sotto albero destro del
c) un nodo che ammette entrambi i figli. nodo da cancellare, ossia il minimo nel sotto albero destro. Anche in questa prospettiva, il caso c) si
328 329
Capitolo 19 Strutture dati ricorsive e non lineari

specializza in due sottocasi (si vedano le figure precedenti): c1) il minimo del sotto albero destro coincide con Si nota che due alberi sono uguali se sono entrambi vuoti o, essendo entrambi non vuoti, sono ordinatamente
la radice del sotto albero destro: c2) il minimo del sotto albero destro (massima generalità) è il nodo più a uguali le radici e i sotto alberi corrispondenti (ricorsione).
sinistra del sottoalbero sinistro della radice del sotto albero destro del nodo da rimuovere.
Il metodo visitaSimmetrica() (o in ordine) scrive su output la successione ordinata degli elementi dell'albero.
private Albero<T> remove( Albero<T> radice, T elem )( Essa realizza una visita in profondità (depth-first) secondo la regola: visita prima il sotto albero sinistro, quindi
if( radice==null ) return radice: la radice, quindi il sotto albero destro. Ma come si visitano il sotto albero sinistro e quello destro ? Semplice:
if( radice.info.compareTo(elem)>0 ){ con la stessa regola. Dunque si tratta di un metodo intrinsecamente ricorsivo. Si può verificare che il metodo
radice.figlioSinistro=remove( radice.figlioSinistro, elem ); return radice: toString() segue anch’esso il criterio della visita simmetrica.
}
if( radice.info.compareTo(elem)<0 ){ In generale si possono definire altri due metodi di visita per un albero binario: visita anticipata e visita
radice.figlioDestro=remove( radice.figlioDestro, elem ); return radice; posticipata. La visita anticipata visita prima la radice, poi il sotto albero sinistro, poi quello destro. La visita
} posticipata visita prima il sotto albero sinistro, poi quello destro, poi la radice. Tuttavia per un ABR ha senso di
//trovato nodo radice con elem norma solo la visita simmetrica . Di seguito si illustrano i tre metodi di visita:
if( radice.figlioSinistro==null && radice.figlioDestro==null ) /‘caso a)*/ return nuli;
if( radice.figlioSinistro==null )//caso b)
return radice.figlioDestro; 18 Visita simmetrica: 10 18 23 25 28
if( radice.figlioDestro==null )//caso b) - . Visita anticipata: 18 10 25 23 28
return radice.figlioSinistro; 10 25 Visita posticipata: 10 23 28 25 18
//Albero radice con entrambi i figli
if( radice.figlioDestro.figlioSinistro==null ){ 23 28
//casocl)
radice.info=radice.figlioDestro.info; //promozione vittima La visita simmetrica può essere facilmente adattata in modo da restituire la sequenza ordinata dal massimo al
//eliminazione vittima minimo: è sufficiente visitare prima il sotto albero destro, poi la radice, poi il sotto albero sinistro. La seguente
radice.figlioDestro=radice.figlioDestro.figlioDestro; return radice; variante di visitaSimmetrica() restituisce la sequenza ordinata su uno oggetto List ricevuto come parametro,
} anziché scriverla su output:
//caso c2)
Albero<T> padre=radice.figlioDestro, figlio=radice.figlioDestro.figlioSinistro; private void visitaSimmetrica( Albero<T> radice, List<T> I ){
while( figlio.figlioSinistro!=null ){ padre=figlio; figlio=figlio.figlioSinistro;} if( radice!=null ){
radice.info=figlio.info; //promozione vittima visitaSimmetrica( radice.figlioSinistro, I );
padre.figlioSinistro=figlio.figlioDestro; //eliminazione vittima l.add( radice.info );
return radice; visitaSimmetrica( radice.figlioDestro, I );
}//remove }
}//visitaSimmetrica
private void toString( Albero<T> radice, StringBuilder sb ){
if( radice==null ) return; }//AlberoBinarioDiRicerca
toString( radice.figlioSinistro, sb );
sb.append( radice.info ); sb.appendf, “); Albero binario degli operatori di un’espressione aritmetica
toString( radice.figlioDestro, sb ); L'albero binario può essere utilizzato anche per rappresentare in memoria un’espressione aritmetica, in
}//toString presenza delle usuali precedenze tra gli operatori. Ad es. l’espressione (a+b*c)*(d/e-f) dà luogo all’albero degli
private boolean equals( Albero<T> a l, Albero<T> a2 ){...} //lasciato come esercizio operatori mostrato nella figura che segue. Si nota che gli operatori occupano nodi non terminali, mentre le
private int hashCode( Albero<T> radice ){...} //lasciato come esercizio foglie rappresentano gli operandi.
private void visitaSimmetrica( Albero<T> radice ){
if( radice!=null ){ Un albero di espressione può essere utilizzato per valutare l’espressione (posto che siano noti i valori degli
visitaSimmetrica( radice.figlioSinistro ); operandi). Mentre normalmente un’espressione utilizza la convenzione di porre l’operatore tra gli operandi sui
System.out.print( radice.info+" " ); quali si applica (notazione infissa), altre formulazioni sono possibili nelle quali l’operatore precede o segue i
visitaSimmetrica( radice.figlioDestro ); suoi operandi. Si parla rispettivamente di notazione prefissa e postfissa. Entrambe queste notazioni sono
} interessanti in quanto evitano l’uso di parentesi.
}//visitaSimmetrica

330 331
C ap ito lo 19 Strutture dati ricorsive e non lineari

public class AlberoEspressione {


*
//struttura dei nodi
private static class Nodo{
Nodo figlioS, figlioD;
}//Nodo
private static class NodoOperando extends Nodo{
a * } int info;
public String toString(){ return '"’+info;}
}//NodoOperando
b c d ( e private static class NodoOperatore extends Nodo{
char op;
Albero dell'espressione (a+b*c)*(d/e-f) public String toString(){ return “"+op;}
}//NodoOperatore
Per l'espressione di esempio si ha:
private Nodo radice=null;
forma prefissa: *+a *bc -/ de f
forma postfissa: abc *+d e/ f - * public void inOrder(){ inOrder(radice);}
public void preOrder(){ preOrder(radice);}
Per ottenere queste formulazioni è sufficiente visitare l'albero dell’espressione rispettivamente in modo public void postOrder(){ postOrder(radice);}
anticipato (preOrder) e posticipato (postOrder). public int valore(){ return valore(radice);}
public void build( String expr ){
La visita simmetrica non restituisce in generale l'espressione infissa originaria in quanto possono non essere //costruisce l'albero a partire da un'espressione infissa contenuta in expr
rispettate le precedenze degli operatori. Ad es., la visita simmetrica (inOrder) dell'albero precedente fornisce: StringTokenizer st=new StringTokenizer( expr,"+-*/%()■,true );
radice=buildEspressione( st );
forma “piatta" simmetrica: a + b * c ' d / e - f }//build

che non è equivalente all’espressione originaria. Decidendo di avviluppare ogni operatore in una coppia di private Nodo buildEspressione( StringTokenizer st ){
parentesi '(' e ')’ si ottiene un'espressione equivalente a quella di partenza: Nodo radice=buildOperando(st);
while( st.hasMoreTokensQ ){
forma infissa parentetica: ((a+(b*c))*((d/e)-f)) char op=st.nextT oken().charAt(0);
if( op==')' ) return radice;
Caso di studio NodoOperatore operatore=new NodoOperatore();
Si considera il problema di leggere da input un’espressione aritmetica in cui sono ammessi gli usuali operatori operatore.op=op; operatore.figlioS=radice;
e gli operandi sono costanti intere senza segno. I simboli non sono separati da spazi. Per semplicità Nodo opnd=buildOperando(st);
gli operatori sono assunti equiprioritari. Per recuperare le precedenze della matematica si usano le parentesi, operatore.figlioD=opnd;
una sotto espressione contenuta tra ( e ) è processata prioritariamente. radice=operatore;
}
L'espressione (2+3‘ 4)'(17/2-5) dovrà essere fornita come (2+(3*4))*((17/2)-5) in cui si forza ad es. la return radice;
valutazione di 3*4 impedendo la somma 2+3 ed il risultato moltiplicato per 4 (ordinamento non rispettoso delle }//buildEspressione
precedenze degli operatori).
private Nodo buildOperando( StringTokenizer st ){
Data un’espressione in input, si vuole costruire il corrispondente albero binario e quindi provvedere alla String opnd=st.nextToken();
valutazione dell’espressione e alla visualizzazione della forma infissa, prefissa e postfissa dell'espressione. if( opnd.charAt(0)=='(' ) { return buildEspressione( st );}
L’applicazione è guidata da un main interattivo che consente di inserire un’espressione e verificarne subito il else{
valore, dopo di che è possibile visualizzare l’ultima l’espressione con i comandi: in per inOrder, pre per NodoOperando numero=new NodoOperando();
preOrder, post per postOrder). Il 7 fa uscire dal programma. numero.info=lnteger.parselnt( opnd );
numero.figlioS=null; numero.figlioD=null; return numero;
package poo.recursion; }
import java.util.*; }//buildOperando
import poo.util.*;
332 333
Capitolo 19 Strutture dati ricorsive e non lineari

private int valore( Nodo radice ){ System.out.printlnfpost visualizza la versione postfissa.");


if( radice==null ) throw new RuntimeException(); System.out.printlnfin visualizza la versione infissa parentetica.");
if( radice instanceof NodoOperando ) return ((NodoOperando)radice).into; System.out.printlnf. chiude il programma.");
else{ String expr=null;
int val1=valore( radice.figlioS ); String EXPR="[\\+\\-\\7%0-9\\(\\)]+";
int val2=valore( radice.figlioD ); AlberoEspressione ae=new AlberoEspressione();
switch( ((NodoOperatore)radice).op ){ for(;;){
caseV: return vali +val2; System.out.print("«");
case'-': return vali-val2; expr=sc.nextLine();
case'*': return vali*val2; if( expr.equals(".") ) break;
case'/’: return vai 1/val2; if( expr.equalslgnoreCase("pre") ) { ae.preOrder(); System.out.println();}
case '%*: return vali%val2; else if( expr.equalsIgnoreCasef'post") ){ ae.postOrder(); System.out.println();}
default: throw new RuntimeExceptionfOperatore “+ else if( expr.equalsIgnoreCaseC'in") ){ ae.inOrder(); System.out.println();}
((NodoOperatore)radice).op+" non consentito"); else{
}//switch try{
} if( !expr.matches(EXPR) ) throw new RuntimeException();
}//valore //matches: solo condizione necessaria
ae.build(expr);
private void inOrder( Nodo radice ){ System.out.println(M»"+ae.valore());
if( radice!=null ){ }catch(Exception e){System.out.println("Espressione malformata!");}
if( radice instanceof NodoOperatore ) System.out.print('C);
inOrder( radice.figlioS );
System.out.print( radice ); System.out.printlnf'Bye.");
inOrder( radice.figlioD ); }//main
if( radice instanceof NodoOperatore ) System.out.printC)1);
} }//AlberoEspressione
}//inOrder
Esempio di sessione:______________________________________
private void preOrder( Nodo radice ){ Valutatore di espressioni aritmetiche intere.
if( radice!=null ){ Forma infissa con operatori + - * / % assunti equiprioritari.
System.out.print( radice+" " ); Non sono ammessi gli spazi bianchi.
preOrder( radice.figlioS ); E' possibile avviluppare un'espressione in ().
preOrder( radice.figlioD ); pre visualizza la versione prefissa,
} post visualizza la versione postfissa,
}//preOrder in visualizza la versione infissa parentetica.
. chiude il programma.
private void postOrder( Nodo radice ){ «(2+(3*4))*((17/2)-5)
if( radice!=null ){ »42
postOrder( radice.figlioS ); « in
postOrder( radice.figlioD ); ((2+(3*4))*((17/2)-5))
System.out.print( radice+" " ); « p re
} • + 2 * 3 4 - / 172 5
}//postOrder «post
234 * + 172/5-*
public static void main( String Qargs ){ «.
Scanner sc=new Scanner(System.in); Bye.
System.out.printlnfValutatore di espressioni aritmetiche intere.");
System.out.println("Forma infissa con operatori + - * / % assunti equiprioritari."); In neretto si indica l’input che va chiuso immediatamente da INVIO.
System.out.printlnfNon sono ammessi gli spazi bianchi.");
System.out.printlnfE1possibile avviluppare un'espressione in ().");
System.out.println(“pre visualizza la versione prefissa.");
334 335
C ap ito lo 19 Strutture dati ricorsive e non lineari

PostOrder iterativo Grafi ______


Si introduce nella classe AlberoEspressione l’enumerazione: I grafi sono strutture dati molto importanti per le applicazioni. Gli alberi sono un caso particolare dei grafi. Un
grafo esprime una rete di relazioni tra oggetti. Più esattamente, un grafo formalmente è una coppia G=(N,A}
private static enum Op{ VISITA, SCRIVI} dove N è un insieme di nodi (o vertici) ed A un insieme di archi, ossia coppie <ni,nj> con ni,nj e N. Un arco
collega due nodi in relazione tra loro. Dato l’arco <ni,nj> si dice che nj è adiacente a ni (ossia nj è raggiungibile
Seguendo le indicazioni fornite nel cap. 18 sulla conversione ricorsione-iterazione si ha: da ni in un “sol passo”).

private void postOrder_ite( Nodo radice ){ L'insieme degli archi costituisce matematicamente una relazione, ossia è un sotto insieme delle possibili
class Pair{ //inner class del metodo - simula area dati coppie <ni,nj> del prodotto cartesiano NxN.
Nodo radice;
Op op; Un grafo si dice orientato (o diretto) se un arco <ni,nj> specifica che da ni si può andare ad nj ma non
public Pair( Nodo radice, Op op ) { this.radice=radice; this.op=op;} viceversa. Graficamente, un arco di un grafo orientato è una freccia che fuoriesce dal nodo ni e punta al nodo
public Nodo getRadice(){ return this.radice;} adiacente nj. Se è richiesto che da nj si possa ritornare ad ni, occorre che esista anche l'altro arco <nj,ni>.
public Op getOp(){ return this.op;}
}//Pair Un grafo si dice non orientato se un arco ha significato bidirezionale, ossia è doppio: <ni,nj> indica che da ni si
può andare su nj e viceversa. Ogni nodo partecipante all'arco è adiacente all’altro nodo dell'arco. Le figure che
poo.util.Stack<Pair> pila=new StackConcatenato<Pair>(); seguono mostrano un grafo orientato ed uno non orientato. Entrambi gli esempi utilizzano nodi etichettati con
//simula prima chiamata numeri interi. Tuttavia si possono utilizzare anche altri tipi di etichette (es. stringhe): si pensi ad un grafo che
pila.push( new Pair(radice,Op.VISITA) ); coinvolge città, i cui archi esprimono collegamenti stradali tra città. Un nome potrebbe essere la sigla di una
while( !pila.isEmpty() ){ provincia (CS, RC, TO, etc).
Pair p=pila.pop();
if( p.getOp()==Op.SCRIVI ){ Entrambi i grafi di esempio hanno come insieme di nodi N={ 1,2,3.4,5,6,7}. Il grafo orientato ha come insieme
Nodo rad=p.getRadice(); di archi: A={<1,2>,<1,3>,<2,4>,<3,5>,<4,5>,<5,2>,<5,3>,<4.6>,<7,6>}. In modo analogo si possono
System.out.print( rad+" " ); enumerare gli archi (stavolta bidirezionali) del secondo esempio di grafo non orientato.
}
else{ //simula chiamate ricorsive - in ordine inverso
if( p.getRadice()!=null ){
pila.push( new Pair(p.getRadice(),Op.SCRIVI) );
pila.push( new Pair(p.getRadice().figlioD, Op.VISITA) );
pila.push( new Pair(p.getRadice().figlioS, Op.VISITA) );

}
}//while
}//postOrder ite

Alberi n-ari
Costituiscono la specie più generale di albero, nella quale un nodo può avere 0, uno o più figli:
Grafo orientato Grafo non orientato
: x1 ) xl
Un grafo può essere utilizzato:
• per esprimere una mappa di città e relativi collegamenti stradali;
x2 * <x3 ) x4 x5 x2 y t x3 x4 ) h x5
• per esprimere i comuni di una provincia e le relazioni di confinanza: i nodi sono i comuni, gli archi
A
riflettono le confinanze;
x6 x7 x8 x9 x10 x6 * x7 x8 » H x9 -x lO • per esprimere la relazione di precedenza (propedeuticità) tra corsi universitari: ogni corso è un nodo, una
freccia dal corso ci al corso cj specifica che ci è precondizione per sostenere cj;
Qui ci si limita ad osservare che un albero n-ario (figura a sinistra) viene spesso impiegato nelle applicazioni • per esprimere le relazioni di parentela tra persone. Si nota che un albero binario (o n-ario) è un caso
mappandolo su un corrispondente albero binario (figura a destra). I figli di uno stesso nodo vengono collegati particolare di grafo orientato.
in una lista concatenata la cui testa è mantenuta nel campo figliosinistro del nodo padre: i nodi fratelli sono
linkati con il campo figlioDestro.

336 337
Capitolo 19 Strutture dati ricorsive e non lineari

Altri concetti e definizioni:_____________________________________________________________________ Liste di adiacenze


LJn cammino (o percorso) in un grafo è una successione di archi contigui, ossia ogni nuovo arco inizia col In questo caso si usano liste concatenate per rappresentare i nodi e gli archi. In modo naturale la
nodo dove finisce il precedente: {<n1,n2>,<n2,n3>......<nk-1,nk>). Il numero di archi coinvolti in un cammino rappresentazione è “lista di liste". I nodi, come caso particolare, potrebbero essere memorizzati in un array
costituisce la sua lunghezza. cosi da avere un “array di liste concatenate”. Al posto dell’array si potrebbe utilizzare equivalentemente una
mappa che associa ad un nodo (chiave) la lista corrispondente dei nodi adiacenti (valore).
Un nodo nj è raggiungibile dal nodo ni se esiste un percorso che parte da ni e finisce su nj. Un percorso si dice
ciclo se il nodo finale coincide col nodo iniziale: nk==n1. Un particolare ciclo è l’auto-anello che potrebbe Per lo stesso grafo orientato presentato in precedenza, una rappresentazione con liste di adiacenze è riportata
collegare un nodo con sé stesso. Esempio di ciclo: {<2,4>,<4,5>,<5,2>} di lunghezza 3. di seguito. La lista verticale è il grafo. In ogni nodo del grafo c’è la testa della lista di adiacenze del nodo.

Un grafo si dice connesso se comunque si scelga un nodo esso è raggiungibile a partire da un qualsiasi altro Se n=INI ed m=IAI ossia se n sono i nodi ed m sono gli archi del grafo, allora l’ingombro spaziale di una lista di
nodo. adiacenze è 0(n+m). Se il grafo è pesato, allora in una lista di adiacenze si possono memorizzare
direttamente oggetti archi che contengono, tra l’altro, anche l’informazione di peso o costo dell'arco.
Un grafo si dice completo se per ogni coppia di nodi esiste un arco che li collega.
grafo ^
Un grafo si dice pesato se ad ogni arco è associata un'informazione numerica di peso o costo (es. la distanza <D
1i
in km tra due città). *'*) ♦ <£>
1
In un grafo non orientato, si dice grado di un nodo, il numero di archi (entranti/uscenti) che coinvolgono il ■5
nodo. Nell’esempio di grafo non orientato mostrato in precedenza, grado(4)=3 etc. ;
( 4) <7 )
In un grafo orientato, si chiama grado di entrata di un nodo, il numero di archi entranti sul nodo, grado di uscita 7
1
il numero di archi uscenti. Per l’esempio di grafo orientato precedente, gradoEntrata(4)=1, gradoUscita(4)=2 (5 > •©
etc.
<o
Rappresentazionejn memoria d[un grafo
I
Un grafo può essere rappresentato mediante alcune strutture dati canoniche: matrice di adiacenza e liste di (1 ; < *)
adiacenza. Tuttavia altre particolarizzazioni possono essere utilizzate dal progettista. Una matrice di
adiacenza è di dimensione INIxINI dove INI è la cardinalità deH’insieme dei nodi. Se il grafo è non pesato, la Esempio di liste di adiacenze
matrice può contenere booleani: nella cella [i,j] si pone true se esiste l’arco <i,j>. La matrice di adiacenza del
grafo orientato precedente è mostrata di seguito: Mentre una struttura ad albero è “radicata", ossia la struttura è caratterizzata dal suo nodo radice (nodo di
partenza per le elaborazioni, es. ricerca etc.), i grafi non sono radicati. In molti algoritmi sui grafi, occorre
2 3 4 5 6 7 specificare il nodo da assumere come partenza per l'elaborazione.
1
Il tipo astratto Grafo che segue specifica solo i meccanismi di base per costruire o modificare un grafo. Gli
2 algoritmi possono essere sviluppati al di fuori di Grafo in termini delle sue operazioni. Di seguito si specificano,
3 a titolo di esempio, alcune operazioni importanti sui grafi: quelle di visita e quella concernente la raggiungibilità
4 dei nodi.
5
Il grafo come abstract data type: un esempio
6
7 package poo.grafo;
import java.util.*;
public interface Grafo<N> extends lterable<N>{
int numNodi();
La matrice di adiacenze si specializza in una matrice numerica se il grafo è pesato. In questo caso, int numArchiQ;
all’intersezione tra la riga ni e la colonna nj si pone il peso dell’arco (se esiste) <ni,nj>. Per esprimere la non boolean esisteNodo( N u );
esistenza di un arco si può utilizzare il simbolo di infinito <». Una matrice di adiacenze comporta una boolean esisteArco( Arco<N> a );
complessità spaziale (ingombro di memoria) del tipo 0(n2) se n è il numero dei nodi. Si nota un certo spreco di boolean esisteArco( N u, N v );
memoria corrispondente alla rappresentazione di tutte le possibili coppie di nodi <ni,nj>. void insNodo( N u );
void insArco( Arco<N> a );
void insArco( N u, N v );
338 339
Capitolo 19^ Strutture dati ricorsive e non lineari

void rimuoviNodo( N u ); Operazioni di visita


void rimuoviArco( Arco<N> a ); Si possono considerare due tipi di visita: la visita in ampiezza (o breADTh-first visi!), detta spesso anche visita
void rimuoviArco( N u, N v ); a ventaglio, e la visita in profondità (depth-first visif) detta spesso anche visita a scandaglio.
lterator<? extends Arco<N» adiacenti( N u );
void clear(); La visita in ampiezza corrisponde alla visita per livelli di un albero binario. Si assume un nodo come partenza.
Grafo<N> copia(); Si visita tale nodo. Quindi si esaminano i nodi adiacenti di questo nodo. Tutti questi nodi vanno visitati prima
}//Grafo che un qualsiasi altro nodo adiacente successivo possa essere visitato etc. Si prosegue sino a che non ci
sono altri nodi da visitare. Poiché gli archi potrebbero far si che si ritorni su un nodo già visitato, occorre
N è il tipo generico parametrico delle etichette dei nodi. Quando si rimuove un nodo, occorre rimuovere tutti gli marcare i nodi visitati in modo da considerarli una sola volta. Una visita in ampiezza si può portare a termine
archi che lo riguardano etc. con la mediazione di una coda su cui si memorizzano i nodi adiacenti del nodo corrente. È importante visitare
tutti i nodi adiacenti prima di procedere con gli adiacenti del livello successivo. Si suppone che la coda dei
Dato un nodo u, il metodo adiacenti) restituisce un iteratore sugli archi ai nodi adiacenti del nodo u. La classe pending contenga nodi già visitati.
base Arco<N> generica nel tipo N delle etichette, mantiene le identità dei nodi origine e destinazione dell’arco
e alcuni metodi di gestione. Una possibilità è mostrata di seguito: Visita in ampiezza:

package poo.grafo; sia u il nodo di partenza


public class Arco<N>{ sia coda una coda di nodi pending già visitati
private N origine, destinazione; coda<-u
public Arco( N origine, N destinazione ){ visita u e marcalo visitato
this.origine=origine; while( coda non è vuota ){
this.destinazione=destinazione; x<- estrai da coda il primo nodo
} per tutti i nodi adiacenti v di x{
if( v non è visitato ){
public N getOrigine(){ return origine; }//getOrigine visita v e marcalo visitato
public N getDestinazione(){ return destinazione; }//getDestinazione
coda <—v
©SuppressWarningsCunchecked")
public boolean equals( Object o ){
if( !(o instanceof Arco ) ) return false;
if( o==this ) return true;
Con riferimento all’esempio di grafo orientato fornito in precedenza, se il nodo di partenza è 1, la sequenza dei
Arco<N> a=(Arco<N>)o;
nodi “toccati” dalla visita in ampiezza è: [1,2, 3, 4, 5, 6]. Se il nodo di partenza è 7: [7,6].
return this.origine.equals( a.getOrigine() ) &&
this.destinazione.equals( a.getDestinazione() );
Visita in profondità:
}//equals
Si considera un nodo di partenza. Si visita questo nodo. Se esso ammette nodi adiacenti, allora si attiva
public int hashCode(){ ricorsivamente la visita in profondità a partire da ciascun nodo adiacente.
final int numeroj)rim o=811;
La visita in profondità di un grafo può essere messa in relazione con le visite ricorsive (inOrder(), preOrder(),
return origine.hashCode()*numero primo+destinazione.hashCode();
postOrder()) di un albero binario: l'obiettivo è scendere sino ad una foglia e poi risalire e continuare con il figlio
}//hashCode
destro etc. La visita cioè arriva sino in fondo e poi risale etc. Ovviamente la successione (come anche per la
public String toString(){ visita in ampiezza) dipende ultimamente dall’ordine come vengono esaminati i nodi adiacenti, ad es., con liste
di adiacenza dall’ordine di successione degli archi sulle liste. In pseudo-code, la visita in profondità si può
return V+origine+V+destinazione+V;
}//toString esprimere come segue:

}//Arco visita-in-profondita(g,u){ //g è il grafo, u il nodo assunto come partenza


visita e marca u come visitato
per tutti i nodi v adiacenti ad u {
La classe Arco può poi essere specializzata per gestire il peso etc.
if( v non è visitato ) visita-in-profondita(g,v);
}
}//visita-in-profondita

340 341
Capitolo 19 Strutture dati ricorsive e non lineari

Con riferimento ancora al grafo orientato mostrato in precedenza, se il nodo di partenza è 1 la visita in Tratteggiati sono gli extra archi aggiunti a GR enumerando sistematicamente tutti i percorsi tra coppie di nodi.
profondità “tocca” in successione i nodi: [1,2, 4, 5, 3, 6]. Se il nodo di partenza è 2: [2, 4, 5, 3, 6]. L’algoritmo che costruisce GR ha complessità 0(n4). Esistono algoritmi più efficienti, ma non è questa la sede
per esaminarli.
Raggiungibilità ____________ __
Risponde ad un’esigenza fondamentale: sapere se un nodo nj è raggiungibile partendo da un dato nodo ni. La Esercizi
raggiungibilità può essere formalizzata attraverso la chiusura transitiva (Fh) della relazione di adiacenza R 1. Su una classe ListaRicorsiva<T> sono definiti i seguenti metodi, molti dei quali vanno realizzati in veste
stabilita dall’insieme degli archi. Formalmente: ricorsiva:

ni Fh nj se 1) ni R nj oppure: 3 nk tale che: public int size(); //ritorna la cardinalità della lista
2) ni FF nk && nk R nj public int contains( Te ) ; //ritorna il numero di ripetizioni di e nella lista
public void clear(); //svuota la lista
In altre parole nj è raggiungibile da ni o perché esiste un arco che collega ni ad nj o perché esiste un nodo nk, public void add( Te) ; //aggiunge e alla fine della lista
raggiungibile da ni, ed nj è adiacente ad nk. public void removeAII( Te) ; //rimuove tutte le occorrenze di e dalla lista
public void sort( Comparato^? super T> c ); //ordina la lista usando c per i confronti
Per “mettere su" le informazioni richieste dalla relazione di raggiungibilità occorre generare tutti i possibili public void reverse();
percorsi per ogni coppia di nodi <ni,nj>. I percorsi hanno lunghezza k, 1<=k<=n-1. La lunghezza k=1 si riferisce
ai percorsi/archi che già esistono. I percorsi da generare sono quelli da 2 a n-1, se n sono i nodi del grafo. Si Il metodo reverse() (mutatore) modifica la lista in modo da invertirne il contenuto (ossia i puntatori). Dopo
nota che al più un percorso è lungo n-1 quando, ovviamente, si passa una sola volta per ogni nodo l'operazione, quello che prima era il nodo di coda, ora è il capolista, il penultimo nodo è il secondo etc. quello
(ripassando più volte si allunga inutilmente la lunghezza di un percorso). Per rispondere ai quesiti sulla che inizialmente era il capolista ora è il nuovo elemento di coda. Attenzione: non è possibile creare nuovi nodi.
raggiungibilità, si può creare un nuovo grafo GR (Grafo Raggiungibilità), da inizializzare come copia del grafo Fornire una implementazione basata su un metodo privato ricorsivo. Ottenere la realizzazione sfruttando in
di partenza (in modo da ereditarne i nodi e gli archi). Sul grafo GR si aggiunge un arco tra ni ed nj (prima non modo naturale la definizione ricorsiva della lista. Successivamente, fornire anche una implementazione basata
esisteva) se esiste un percorso di lunghezza k (k>=2 e k<=n-1) che congiunge ni ad nj. Costruito GR, per su un metodo privato iterativo.
conoscere se ni è raggiungibile da nj, basta vedere se nj è adiacente ad ni su GR. In pseudo-codice si ha: 2. Scrivere in veste iterativa il metodo add() della classe AlberoBinarioDiRicerca. Procedere in modo intuitivo.
3. Nell’ipotesi di fornire in input un’espressione aritmetica in veste prefissa, progettare e testare un metodo
crea GR come copia di G buildPre( String expr ), da aggiungere alla classe AlberoEspressione, che riceve l’espressione e costruisce
for( int k=2; k<g.numNodi(); k++ ){ l’albero corrispondente degli operatori.
for( tutti i nodi i di G ) 4. Come il precedente ma quando l'espressione in input è fornita in veste postfissa. Il metodo si chiami
for( tutti i nodi j di G ) buildPost( String expr ).
for( tutti i nodi m di G ) 5. Si consideri un albero binario di caratteri. Scrivere una classe AlberoChar che consenta di: costruire l’albero
if( GR.esisteArco(i,m) && G.esisteArco(m.j) ) a partire da una sua forma linearizzata, e visualizzi il suo contenuto in accordo ai tre metodi di visita. Come
GR.insArco(i.j); forma linearizzata si utilizzi la seguente: si specifica prima il nodo radice quindi i due sotto alberi e cosi via
ricorsivamente. Quando un sotto albero è vuoto, si scrive Ad esempio, la scrittura A.BC..D.. corrisponde
ritorna GR all’albero:

Esempio di grafo di raggiungibilità:

6. Scrivere un metodo di una classe di albero binario che verifica se esso è bilanciato oppure no. Un albero è
da considerare bilanciato quando la cardinalità del sotto albero sinistro è uguale alla cardinalità del sotto
albero destro, o i due valori differiscono di 1. Tale proprietà si intende soddisfatta ricorsivamente a partire da
ogni nodo.
7. Scrivere un metodo di una classe di albero binario che restituisce l’altezza dell’albero.
8. Scrivere un metodo di una classe di albero binario che visualizza il contenuto dell’albero per livelli, ogni
livello essendo attraversato da sinistra a destra.
9. Sviluppare la struttura di iterazione della classe AlberoBinarioDiRicerca. Si nota che è possibile in un caso
ottenere l’iteratore mediante una inner class che all’atto dell’instanziazione ottiene il contenuto della visita
simmetrica dell’albero ad es. su una LinkedList e quindi si basa sull'iteratore della lista. Ovviamente, ad ogni
richiesta di rimozione dall’iteratore, occorre realizzare la rimozione sia dalla lista che dall'albero. In alternativa
342 343
Capitolo 19

(soluzione preferibile per ragioni spaziali), si può basare la struttura di iterazione su uno stack di alberi che di Capitolo 20:__ ___________________________________________________________________
momento in momento contiene la successione dei nodi dal nodo radice sino al nodo più a sinistra. Il nodo in Struttura dati Heap e HeapSort
cima allo stack è il nodo corrente. Dopo averlo prelevato a seguito di una operazione next(), se lo stack non è
vuoto, in cima allo stack si trova il nodo genitore di quello corrente. Se x è il nodo corrente, si procede
Heap - definizione e proprietà
copiando sullo stack i nodi del percorso dalla radice del sotto albero destro di x sino al nodo più a sinistra etc.
Un heap (letteralmente “mucchio") è una struttura dati del tipo collezione parzialmente ordinata. Essa è
Progetto*Si definita naturalmente su un albero binario. Gode delle seguenti due proprietà:
Si considerano espressioni aritmetiche intere con gli operatori A (A denota l’esponenziazione) e le
1) un heap è un albero binario nel quale ogni livello è completo dei suoi nodi tranne (eventualmente) l’ultimo
usuali priorità della matematica: II(A)>II(*,/,%)>II(+,-). A parità di priorità, si assume l’associatività a sinistra. livello, dove possono mancare nodi nella parte destra;
Eventualmente si possono usare le parentesi per alterare le priorità intrinseche: un’espressione in parentesi () 2) ogni nodo contiene un valore che (ad es.) è minore di ogni suo discendente, ossia il sotto albero sinistro e
viene valutata prima. Segue l’algoritmo suggerito per costruire un albero binario di espressione. quello destro contengono nodi con valori maggiori o uguali alla radice (e cosi via ricorsivamente). La figura che
segue mostra un heap. La radice dell’albero contiene 20 che è minore di tutti i suoi discendenti e cosi via
Si usano due stack: il primo è uno stack di alberi operandi, il secondo è uno stack di caratteri operatori. ricorsivamente per ogni nodo.
Quando arriva un operando, si costruisce un albero con solo l’operando e lo si inserisce in cima allo stack di
alberi.

Quando arriva un operatore, sia esso opc (operatore corrente), si procede come segue:
A) Se opc è più prioritario dell'operatore affiorante dallo stack di operatori o tale stack è vuoto, si inserisce
opc in cima allo stack operatori.
B) Se opc non è più prioritario rispetto alla cima dello stack operatori, si preleva l’operatore al top dello stack
operatori, quindi si prelevano due operandi a2 (top) e al (top-1) dallo stack di operandi (in caso di
eccezioni, l’espressione è malformata). Si crea un nodo operatore con l’operatore prelevato, e gli si legano
rispettivamente a l come figlio sinistro e a2 come figlio destro. Il nuovo albero è inserito in cima allo stack
operandi. Si continua ad eseguire il passo B) se opc risulta ancora non più prioritario dell’operatore
affiorante dallo stack operatori. Dopo questo, o perché opc è più prioritario dell’operatore in cima allo stack
operatori o perché lo stack è vuoto, si applica il caso A. Le differenze dall'albero binario di ricerca sono evidenti, sia nel riempimento dei livelli sia, soprattutto, nella
Quando l’espressione è terminata, se lo stack operatori non è vuoto, si estraggono uno alla volta gli operatori disposizione dei valori nei nodi.
dallo stack operatori e si costruiscono alberi con i rispettivi due operandi prelevati dallo stack operandi,
secondo le stesse modalità (operando sinistro e destro) spiegate sopra, e si inseriscono tali alberi sullo stack A causa delle sue proprietà, un heap ammette operazioni di inserimento e rimozione efficienti. È importante
operandi. Si nota che certamente vengono considerati prima gli operatori più prioritari. riflettere che di momento in momento, la radice dell’albero contiene il minimo e che un nuovo inserimento
Quando lo stack operatori è vuoto, allora lo stack operandi dovrebbe contenere un solo elemento che è potrà avvenire riempiendo il primo buco libero sull’ultimo livello.
l’albero dell'espressione. Ogni altra situazione (stack operandi vuoto o con più di un elemento) denota una
espressione malformata. Aggiunta di un elemento
Cosa succede se ci sono parentesi ?
Volendo aggiungere 60 all’heap precedente, si ha:
Quando si incontra una parentesi aperta ’(’ si invoca ricorsivamente la procedura di costruzione (es.
buildEspressione()). Quando si incontra una parentesi chiusa ’)', si ritorna l’albero in cima allo stack operandi
(un solo elemento o l’espressione è malformata).
Materialmente, il metodo buildEspressione() potrebbe introdurre i due stack come variabili locali. Quando
termina, ritorna l’unico elemento (o la sotto espressione è malformata) in cima allo stack operandi locale. Il
metodo buildEspressone() potrebbe ricevere come parametro uno string tokenizer inizialmente aperto sulla
stringa espressione. Il processo di costruzione parte con il metodo build() che riceve una stringa espressione e
quindi sub-appalta il lavoro a buildEspressione() che restituisce l’albero dell’espressione.

Utilizzare un’espressione regolare per scoprire subito che un’espressione aritmetica non è malformata
(condizione necessaria).

L’applicazione dovrebbe essere munita di GUI al fine di consentire la scelta delle operazioni via menu.
L’effetto di una qualsiasi operazione potrebbe essere visualizzato graficamente (eventualmente si può Si crea un nodo con 60 e lo si attacca (nel caso dell’esempio) come figlio destro di 90. Ovviamente, un
rimpiazzare la grafica con una JTextArea usata come console). inserimento per essere accettabile deve mantenere le proprietà dell’heap. Tuttavia a causa dell’inserimento di

344 345
Capitolo 20 Struttura dati heap e Heap sort

60, l’heap non è verificato localmente al sotto albero di radice 90, in quanto 90 non è minore di ogni suo
discendente. Per riaggiustare l’heap si scambiano 90 e 60: Il riaggiusto downward dell’heap si può ottenere confrontando la radice con entrambi i nodi figli immediati. È
sufficiente trovare il minimo tra i due figli della radice. Occorre procedere ad uno scambio se la radice è
maggiore di questo minimo. Nel caso di cui sopra, il minimo tra 60 e 43 è 43. Siccome 90 (la nuova radice) è
maggiore di 43, si scambiano i nodi con 90 e 43 determinando la nuova situazione mostrata di seguito:

Si vede che nonostante lo scambio di 90 (padre originario di 60) con 60, l'heap è ancora violato. Infatti il padre
di 60 (cioè 75) non è minore di tutti i suoi discendenti. Dunque si prosegue scambiando il 75 col 60.

Come si vede, l’heap è a posto dal punto di vista della radice dell’albero, ma non dal punto di vista del nodo
radice del sotto albero destro. Infatti 90 non è minore di tutti i suoi discendenti. Si trova il minimo tra 57 e 71
(cioè 57) e si scambiano il 90 col 57:

A questo punto l'heap è ricostituito. Per riassumere: quando si aggiunge un elemento, si riempie il primo buco
libero sull’ultimo livello e si riaggiusta l’heap upward (dalla foglia verso la radice) confrontandosi col nodo
padre e scambiando subito se la proprietà di heap non è verificata. Si continua cosi sino a che il nodo corrente
è non minore del suo nodo radice.

Rimozione del minimo _______ A questo punto tutto l’heap è ricomposto e nella radice esiste il nuovo minimo (43) che una successiva
In un heap l’elemento che viene rimosso è di norma la radice, ossia il minimo in tutto l’albero. Si provvede a operazione di estrazione prowederà a restituire etc.
colmare la lacuna promuovendo l’ultimo nodo dell'ultimo livello al posto della radice, ed eliminando nel
contempo l'ultimo nodo. Nell’albero risultante di cui sopra, l’operazione di rimozione restituisce x=20. Quindi si Efficienza delle operazioni di inserimento/rimozione
promuove 90 al posto della vecchia radice e si distrugge il vecchio nodo 90. Naturalmente l’operazione fa Dall’analisi della dinamica delle operazioni che si devono compiere per ricomporre l’heap dopo un'aggiunta o
perdere tipicamente la proprietà dell’heap per cui occorre riaggiustarlo ma stavolta procedendo downward una rimozione, risulta che esse interessano solo un percorso (es. quello che dalla foglia-ultimo nodo porta sino
(dalla radice verso le foglie). alla radice nel caso di inserimento, o quello che dalla radice porta ad una foglia nel caso di rimozione).
^ x=20 é nmosso Pertanto, il numero di operazioni effettuate, nel caso peggiore, è pari alla lunghezza di un percorso cioè
all’altezza dell’albero binario (massima lunghezza di un percorso dalla radice ad un nodo foglia). Se h è
l’altezza dell’albero ed ri il numero dei nodi dell’albero, risulta che:

2h < n < 2h'' e dunque: h < lo g , n < h + \.

In altre parole, le operazioni di inserimento e rimozione, costando O(h) costano in realtà 0(log n) dunque sono
efficienti. Il prezzo da pagare per questa efficienza è quello di accettare una struttura dati parzialmente
ordinata, con la garanzia che la radice è il minimo nell’albero.

346 347
C ap ito lo 20 Struttura dati heap e Heap soli

Come sfruttare l’efficienza dell’heap ? ___ _ Un secondo uso della struttura dati Heap è come supporto all’ordinamento di un array, ciò che è noto come
Una struttura dati heap si può agevolmente mappare su un array ed essere manipolata direttamente sull’array, algoritmo di ordinamento HeapSort, richiamato di seguito (si fa riferimento ad un metodo static ad esempio
come segue. Rinunciando ad utilizzare (per semplicità) il primo elemento (indice 0) si possono collocare posto nella classe poo.util.Array):
sull'array gli elementi dell'albero come segue (ci si riferisce all’ultimo albero heap mostrato in precedenza):
public static <T extends Comparable<? super T»void heapSort( T[] v ){
Array heap: Heap<T> h=new Heap<T>( v.length );
1 | 43 1 60 | 57 | 84 1 75 | 90 | 71 | 93 | 91 1 96 | ] 1 | H //prima fase: riempimento heap
0 1 2 3 4 _ 5 ___6__ 7_ 8 9 10 11 12 13 ... for( T e: v ) h.add( e );
| liv-0 | liv-1 l liv-2 l liv-3 | //seconda fase: svuotamento heap
for( int i=0; icv.length; i++ ) v[i]=h.remove();
}//heapSort
Il nodo radice è posto in posizione 1. I suoi figli sono collocati in posizione 2 e 3. In generale, un nodo
collocato in posizione j, avrà i suoi figli posti negli indici 2j e 2j+1. In altre parole, il contenuto dell'albero è Il metodo di utilità heapSort() consiste di due fasi: nella prima si riempie un heap di appoggio con gli elementi
copiato ordinatamente sull'array “per livelli”. Il livello 3, l’ultimo, è incompleto. Si nota come il primo buco libero dell’array. Nella seconda si provvede ripetutamente a rimuovere la testa dell’heap e a collocarla nella prima
sull’albero viene a coincidere col primo buco libero sull’array (in questo caso la posizione 11). posizione libera dell’array.

È utile riesprimere le proprietà dell'heap sull'array. Complessità di Heap Sort

1. La radice (cioè il minimo) è in posizione 1. THeapson(n)=T(prima-fase)+T(seconda-fase)


2. La proprietà heap interpretata downward (cioè, dalla radice in giù) si esprime dicendo:
dove n è il numero di elementi dell’array da ordinare. È pressocchè evidente che i due contributi al tempo di
heap | / 1< heap \ 2* i \ dfc & heap\i \ < heap\ 2 * / + 11, V» e | l ,/i / 2 1 esecuzione, cioè della prima parte e della seconda sono uguali. Di seguito si valuta il tempo della prima fase.
Nel caso peggiore, ogni nuovo elemento aggiunto in posizione j dell’array, migra in tutti i modi possibili,
a patto che esista l’elemento heap\ 2 * i + 11. finendo nella radice (posizione 1). Ossia le operazioni sono log j (log in base 2). Quindi:

3. La proprietà heap interpretata upward (cioè, da una foglia in su nell’albero) si esprime dicendo: T(prima-fase)=log 1 + log 2 + log 3 + ... + log (n-1) + log n = log (1*2*3*...(n-1)*n)=log n!

heap | / 1> heap | / / 2 1, V/ da n o 2. Utilizzando l’approssimazione di n! (per n in crescita asintotica) fornita dalla formula di Stirling:

Direttamente fondata su queste osservazioni è la classe Heap appartenente al package poo.heap e sviluppata / j!~ (—)" dove e è il numero di Nepero (e=2.718)
più avanti in questo capitolo.

Osservazioni: mentre le operazioni di inserimento e rimozione costano 0(log n) se n è il numero degli elementi si ha (prendendo i log in base 2):
dell’heap, la ricerca di un elemento costa O(n). Inoltre, la rimozione di un elemento, dal secondo in poi, costa il
tempo necessario a ricomporre l’heap considerando la necessità di dover ri-collocare nell’heap tutti gli log n!=n*log(n/e)+1/2‘ [log(27i)+log n] ^n log n,
elementi che seguono quello rimosso.
e complessivamente:
Possibili usi di una^struttura dati heap
Un heap può essere direttamente sfruttato per ottenere una coda a priorità, cioè una coda nella quale gli THeapsori(n)=2T(prima-fase) «2 n log n =0(n log n).
elementi che arrivano non escono secondo l’ordine di arrivo (comportamento FIFO delle normali code) ma in
base ad un criterio di urgenza (o priorità). Si pensi ad un pronto soccorso in cui le persone che giungono Caso di studio: implementazione di una classe Heap
possono non essere servite secondo l’ordine d’arrivo ma piuttosto secondo l’urgenza o gravità dei singoli casi. Di seguito si mostra una classe Heap fondata direttamente sui concetti discussi in precedenza.

L’arrivo in coda (operazione add(x)) aggiunge x all'heap preservando la proprietà dell'heap. Un’estrazione package poo.heap;
(operazione remove()) toglie e restituisce il minimo dell'heap, ossia la sua radice in posizione 1, dopo di che si import java.util.*;
ricostituisce l’heap. public class HeapcT extends Comparable<? super T » {
private T[] heap;
L’implementazione della classe Heap può essere banalmente letta come implementazione di una coda a private int n, size;
priorità. //size punta all'ultimo occupato: 1<=size<=n

348 349
Capitolo 20 Struttura dati heap e Heap sort

@SuppressWarnings(“unchecked“)
public Heap( int n ){ public void clear(){
if( n<=0 ) throw new HlegalArgumentException(); for( int i=1; i<=size; i++ )
this.n=n; size=0; heap[i]=null;
heap=(T[]) new Comparable[n+1]; size=0;
}//Heap }//clear

public int size(){ return size; }//size public String toString(){


StringBuilder sb=new StringBuilder(200);
public boolean contains( T elem ){ sb.append('[');
for( int i=1; i<=size; i++ ) for( int i=1; i<=size; i++ ){
if( heap[i].equals(elem) ) return true; sb.append( heap[i] );
return false; if( i<size ) sb.append(',');
}//contains }
sb.append(']');
public void add( T elem ){ return sb.toString();
if( size==n ){//espandi }//toString
heap=Arrays.copyOf( heap, 2**n+1 ); n=2*n;
} }//Heap
size++;
heap[size]=elem; //aggiunge elem in ultima posizione La classe PriorityQueue<E> di ja v a .u tH ___ _________ __________________
//aggiusta heap "upward” Oltre che usare un proprio heap, es. un’istanza della classe Heap di poo.heap, è possibile equivalentemente
int i=size; sfruttare la classe generica e parametrica PriorityQueue<E> di java.util. Il parametro tipo E si suppone dotato
while( i>1 ){ del confronto naturale (implementazione dell’interfaccia Comparable). In alternativa si può usare un costruttore
if( heap[i].compareTo(heap[i/2])<0 ){//scambia heap[i] e heap[i/2] di PriorityQueue che accetta un oggetto comparatore deputato a fare i confronti.
T park=heap[i]; heap[i]=heap[i/2]; heap[i/2]=park; i=i/2;
} Costruttori: _______________ ________________ ______________________________________
else break; PriorityQueue() capacità di default; 11
PriorityQueue( int capacita )
}//add PriorityQueuej Collection<? extends E> c ) inizializza la priority queue con gli elementi provenienti da c
PriorityQueue( int capacita, Comparator<? super E> oggetto-comparatore )
public T remove(){ //rimuove il minimo e lo restituisce
if( size==0 ) throw new RuntimeException("Heap empty!"); Metodi: _
T min=heap[1|; boolean add( É e ), o offer( E e ) aggiungono l’elemento e alla coda
heap[1]=heap[size]; //promozione ultimo elemento void clear()
heap[size]=null; size--; boolean contains( Object x )
//riaggiusto heap E peek() ritorna la testa della coda, senza rimuoverla
int i=1; E poll() ritorna la testa della coda, rimuovendola
while( i<=size/2 ){ int size()
int j=2*i, k=j+1; boolean remove( Object x ) rimuove x dalla coda
//trova min tra heap[j] e heap[k], sia z l'indice del min lterator<E> iteratorQ
int z=j; //ipotesi Object[] toArray()
if( k<=size && heap[k].compareTo(heap[z])<0 ) z=k;
if( heap[i].compareTo(heap[z])>0 ){ Esercizi _____ __
//scambia heap(i) con heap[z] 1. Aggiungere alla classe Heap un costruttore che riceve, tra l’altro, un oggetto Comparator da utilizzare per i
T park=heap[i]; heap[i]=heap[z]; heap[z]=park; i=z; confronti. In assenza di Comparator, la classe deve basarsi sul confronto naturale degli elementi.
} 2. Modificare la classe Heap
else break;
• aggiungendo un metodo void remove( T elem ) che rimuove, se esiste, la prima occorrenza di elem
} • prevedendo la struttura di iterazione (rendere Heap iterabile).
return min;
Attenzione che l'eliminazione di un elemento scardina la proprietà heap che, ovviamente, va mantenuta.
}//remove
350 351
Capitolo 20

3. Leggere una sequenza di interi da tastiera, terminata dal primo non positivo, inserire i numeri in un heap ed Capitolo 21j____________________
estrarre e scrivere su video i numeri dal più grande al più piccolo. Si suggerisce di introdurre una classe
Elemento che ingloba un intero e fornisce l’ordinamento desiderato. In alternativa, se è presente il costruttore
Sviluppo di programmi ad oggetti
di cui all’esercizio 1, definire ed istanziare “al volo" un comparatore per stabilire il criterio di confronto.
In questo capitolo si riportano alcuni casi di studio relativi allo sviluppo di applicazioni ad oggetti. Il punto
4. Come 3. ma utilizzando una PriorityQueue<lnteger> di java.util.
cruciale è la definizione di una opportuna architettura complessiva del programma, basata su un insieme
“affiatato" di classi che interagiscono tra loro. Quali siano le classi di volta in volta più naturali rispetto al
problema in gioco è una materia non formalizzabile. L’intuito e l’esperienza sono fattori importanti.
Suggerimenti del tipo: “trova i nomi/pronomi nella descrizione di un problema" ed avrai le classi, e “osserva i
verbi implicati nel problema" e ti diranno i metodi, lasciano il tempo che trovano. Non esiste alcuna “bacchetta
magica” per individuare le classi. L’approccio è spesso trial-and-error (prova e correggi). Per rappresentare le
classi che si identificano quali costituenti una soluzione del problema, può essere utile un diagramma UML
(Unified Modelling Language - linguaggio unificato di modellazione) che mostra le entità (classi, interfacce,
classi astratte etc.) e le loro relazioni. Lo sforzo di mettere sotto forma di diagramma e dunque in veste grafica
le idee di un progetto, può aiutare in generale a raffinare la soluzione “costringendo" a pensare più in astratto
e non subito in termini di linee di codice Java.

Sommario della notazione UML sulle classi


Si è visto in più di un’occasione che una classe è rappresentata in UML mediante un rettangolo con il nome
aH'interno ed eventualmente uno stereotipo tipo « a b s tra c t» o « in te rfa c e » posto sopra il nome dell’entità
per specificare meglio che cosa rappresenta. UML consente al progettista di specificare informazioni
addizionali su una classe come ad es. i campi (variabili di istanza o attributi) e i metodi (intestazioni o
signature).
UnaClasse unOoaettoUnaClasse
Compartimento
attributi
Compartimento UnaClasse
UnaClasse metodi

D iverse n o ta z io n i p e r c la s s i e og g e tti

Indicatori di visibilità
A ud io C lipM an agcr + public
# protected
-instancc:A udioC lipM anaaer
private
-prevC’lip : A u d io C lip
package
« c o n s tr u c t o r »
-A udioC lioM anager( )

+izetlnstance( ):AudioC’linM anaj»er — ^


+ play(:A ud ioC lip):void Metodo statico o di classe
+1oop( :A udi oCl i p ) :voi d
+stop():void

« in t e r f a c e » « a b s tra c t»
Indirizzo / ’ro J o llo
Prodotto
get Address( )
o p e ra zio n e 1 o p e r a zio n e 1
set Address( )
gctC ityt ) operazioni?2 o p e ra zio n e2
sctCityr )
gctCAPt )
Due notazioni per una classe astratta
sctCAP( )
352 353
C ap ito lo 21 Sviluppo di programmi ad oggetti

Relazioni (associazioni) tra classi e navigabilità: Se una classe usa al suo interno (es. variabile locale in un metodo, parametro, una classe eccezione etc.)
un’altra classe si parla genericamente di dipendenza semplice (relazione tratteggiata con freccia aperta):
In A c è un campo di tipo B da B ad A non è navigabile

Associazioni tutto/parti:_______________________________________________________________________
Descrivono situazioni in cui un oggetto composto (il “tutto”) è posto in relazione agli oggetti che lo
In B c'è un campo di tipo A reciproca) compongono (le “parti”). Si distinguono due tipi di associazioni tutto/parte: la composizione e l’aggregazione.
a) navigabilità unidirezionale b) navigabilità bidirezionale c) non navigabilità In una composizione il tutto non può esistere senza le le sue parti. Se si distrugge il tutto, si distruggono anche
le parti. In un’aggregazione le parti possono sussistere indipendentemente dal tutto. In più le parti aggregate
senza frecce si assume potrebbero anche non esistere o una parte potrebbe appartenere a più aggregati. Un’aggregazione tende ad
la navigabilità bidirezionale essere omogenea nelle parti che la costituiscono. Malgrado la differenziazione, molto spesso si preterisce
usare la relazione di aggregazione in tutti i casi.
Relazioni e molteplicità:

uno uno

uno zero o piu

uno uno o più

uno quattro

uno sei a dieci


Indice dei riferimenti incrociati in^un testo __ _________
L’obiettivo è sviluppare un’applicazione che acquisisce il nome di un file di testo da input e genera il
uno due. quattro o sei
corrispondente indice dei riferimenti incrociati. Il file si suppone contenga una relazione, un libro etc. Si tratta in
concreto di generare l’elenco delle parole distinte, una parola per riga di output, ordinate per lunghezza
u n o o p iù zero o più crescente ed a parità di lunghezza in senso alfabetico, e per ogni parola, a partire dalla prossima linea,
l’elenco dei numeri di linea, ordinati per valori crescenti, del testo sulle quali la parola compare.
Se manca, la molteplicità è 1
Una parola è una sequenza di caratteri alfabetici.
In un oggetto di classe C esiste un attributo che contiene 0, 1 o più elementi di classe D (l’attributo è
tipicamente un oggetto collezione), etc. Di seguito si richiamano le relazioni di ereditarietà (extends) e di Per facilitare l’interpretazione dell’indice dei riferimenti incrociati, il programma deve generare preliminarmente
implementazione (realizes) di interfaccia. su output una copia (listing) del file testo sorgente in cui ogni linea è preceduta dal suo numero di linea seguito
da

La descrizione del problema parla di indice, di parole, di elenco di parole, di numeri di linea, di elenco di
numeri di linea etc. Non c’è dubbio che questi concetti possono essere utilizzati per identificare un certo
numero di classi con le quali comporre una soluzione. Di seguito si mostra l’organizzazione di massima di una
soluzione attraverso un diagramma di classi UML.

Crosslndex è l’applicazione (contiene il main che si ipotizza faccia uso di un indice realizzato mediante un
albero binario). GestoreTesto si interfaccia col file e restituisce a Crosslndex le parole del testo una alla volta,
o l’indicazione di fine file. Indice è un’interfaccia che descrive le operazioni previste sull’indice. Parola cattura
una singola parola e memorizza la relativa successione delle occorrenze di linea. Il diagramma, a titolo
d’esempio, riporta alcune concretizzazione del concetto di indice.
354 355
Capitolo 21 Sviluppo di programmici oggetti

public class GestoreTesto {


public enum Simbolo{ WORD, EO F}
private boolean EOF=false;
private String linea=null;
private Scanner input, scan;
private int numeroLineaCorrente=0;
private String word;
public GestoreTesto( File f ) throws IOException{ input=new Scanner( f ); }//costruttore

private void avanza(){


try{
if( linea==null II !scan.hasNext() ){
linea=input.nextLine(); numeroLineaCorrente++; //conta questa linea
System.out.println( numeroLineaCorrente+’’: "+linea ); //fai echo di linea
scan=new Scanner( linea );
scan.useDelimiterC[Aa-zA-ZJ+M);
}
}catch( Exception ioe ){ EOF=true; input.close();}
}//avanza
Un diagramma di classi per il problema dei riferimenti incrociati
public Simbolo prossimoSimbolo() throws IOException{
GestoreTesto definisce un tipo enumerato Simbolo i cui valori sono WORD e EOF, ed un metodo do{
prossimoSimboloQ che ad ogni invocazione ritorna o il simbolo WORD (parola) o quello EOF (fine file). In avanza();
questo modo si semplifica la scrittura del programma in quanto tutte le azioni che occorre compiere per }while( !EOF && !scan.hasNext() );
ottenere la prossima parola (es. avanzare alla prossima linea non bianca del testo, saltare i caratteri spuri sino if( EOF ) return Simbolo.EOF;
alla prossima parola etc.) sono rimosse dal resto del programma e confinate all’interno di GestoreTesto. Tali word = scan.next();
azioni potrebbero essere riviste o re-implementate senza influenzare il programma, a patto che l’interfaccia return Simbolo. WORD;
della classe non subisca modifiche. }//prossimoSimbolo
È chiaro che se il prossimo simbolo è una WORD, è poi interesse del programma ottenere la stringa public String getString(){
(ortografia) della parola ed il numero di linea su cui la parola compare. Si suppone che la classe GestoreTesto return word;
esporti due metodi: String getString() e int getNumeroLinea() che invocati subito dopo che prossimoSimbolo() }//getString
ritorna WORD restituiscono l'ortografia della parola ed il suo numero di linea. È responsabilità della classe
GestoreTesto mantenere aggiornato il contatore dei numeri di linea mano a mano che si avanza sul testo. Un
public int getNumeroLinea(){
altro sotto compito della classe GestoreTesto è quello di fare l’eco delle linee di testo lette dal file testo su
return numeroLineaCorrente;
standard output.
}//numeroLinea
L’impostazione adottata per GestoreTesto è utile in generale per programmare un analizzatore lessicale
}//GestoreTesto
(lexer) nell’ambito del progetto di un compilatore. Ovviamente, in questi casi i simboli in gioco sono più di due.
Per un esempio concreto si rimanda più avanti al caso di studio sull’implementazione della macchina RASP (si Si nota che per tokenizzare una linea si è fatto uso di uno scanner (scan) aperto sulla stringa linea e si sono
veda anche l’appendice B).
definiti come delimitatori tutti i caratteri non alfabetici. Lo scanner input, invece, è “attaccato" al file ricevuto dal
costruttore (oggetto di tipo File).
La classe GestoreTesto:_______________________
package poo.indice; Il metodo privato avanza() è responsabile di avanzare alla prossima linea non bianca del file testo, se esiste,
altrimenti mette a true la booleana EOF che fa ritornare il simbolo EOF. Dopo ogni lettura di una linea, anche
import java.io.*; bianca, si conta la linea, ossia si incrementa il contatore numeroLineaCorrente ed inoltre si fa l’eco della linea
import java.util.*;
su standard output (video).

Il fine file sullo scanner input è catturato mediante try-catch. Quando nextLineQ solleva un'eccezione, allora
questa è interpretata come situazione di end-of-file e la boolean EOF è posta a true, dopo aver anche chiuso il
file di testo.
356 357
Capitolo 21 Sviluppo di programmi ad oggetti

Il passo successivo è progettare il concetto di indice, ossia una tabella che mantiene le parole distinte del file. public int hashCode(){
L’elenco dei numeri di linea può essere benissimo parte della parola e dunque non essere direttamente visibile finalint MOLT=41;
al livello di indice. Ovviamente, l’indice presuppone il progetto di una classe Parola che modella una singola return ortografia.hashCode()‘ MOLT+elenco.hashCode();
parola distinta del testo. }//hashCode

Si propone la seguente interfaccia Indice, iterabile e ridotta airosso”, ossia col numero minimo di metodi utili }//Parola
al programma. Il metodo size() ritorna il numero di parole nell’indice; il metodo occorrenze( parola ) ritorna il
numero di occorrenze della parola ricevuta come parametro; il metodo add( parola, numero-linea ) chiede che La classe Parola memorizza l’elenco ordinato dei numeri di linea mediante un TreeSet<lnteger>. L’uso di un
venga memorizzata l'informazione che la parola ricevuta compare sulla linea il cui numero è specificato dal set semplifica le azioni, dal momento che non occorre verificare se un numero di linea sia già presente o
parametro. meno. Gli altri dettagli dovrebbero essere auto-esplicativi.
Di seguito si propone una classe astratta IndiceAstratto che concretizza, al solito, quanti più metodi è
Tipo astratto Indice:__________________________________________________________________________ possibile. Seguono poi alcuni esempi di classi concrete.
package poo.indice;
public interface Indice extends lterable<Parola>{ La classe IndiceAstratto:____________________________________ __________
int size(); package poo.indice;
int occorrenze( String ortografia ); import java.util.Iterator;
void add( String ortografia, int numeroRiga );
}//lndice public abstract class IndiceAstratto implements Indice {
public int size(){
La classe Parola: ___ _ __ int c=0;
package poo.indice; for( lterator<Parola> it=this.iterator(); it.hasNext(); it.next(), c++ );
import java.util.*; return c;
public class Parola implements Comparable<Parola>{ }//size
private String ortografia;
private Set<lnteger> elenco=new TreeSet<lnteger>(); public int occorrenze( String ortografia ){
public Parola( String ortografia ) { this.ortografia=ortografia;} Parola orto=new Parola( ortografia );
public void add( int nr ){ elenco.add( nr ); }//add for( Parola p: this ){
public int size(){ return elenco.size();} if( p.equals(orto) ) return p.size();
public String getOrtografia(){ return ortografia;} if( p.compareTo(orto)>0 ) return 0;
}
public boolean equals( Object o ){ return 0;
if( !(o instanceof Parola) ) return false; }//occorrenze
if( o==this ) return true;
Parola p=(Parola)o; public String toString(){
return ortografia.equals( p.ortografia ); StringBuilder sb=new StringBuilder( 400 );
}//equals for( Parola p: this ) sb.append(p);
return sb.toString();
public int compareTo( Parola p ){ }//toString
if( ortografia.length()<p.ortografia.length() Il
ortografia.length()==p.ortografia.length() && public boolean equals( Object o ){
ortografia.compareTo(p.ortografia)<0 ) return -1; if( !(o instanceof Indice) ) return false;
if( this.equals(p) ) return 0; if( o==this ) return true;
return +1; Indice ix=(lndice)o;
}//compareTo if( this.size()!=ix.size() ) return false;
lterator<Parola> i1=this.iterator(), i2=ix.iterator();
public String toString(){ while( i1.hasNext() ){
String s=ortografia+"\n"; Parola p1=i1.next(), p2=i2.next();
lterator<lnteger> i=elenco.iterator(); if( !p1.equals(p2) ) return false;
while( i.hasNext() ){s+=i.next()+" ";} }
s+="\n"; return s; return true;
}//toString }//equals
358 359
Capitolo 21 Sviluppo di programmi ad oggetti

public int occorrenze( String ortografia ){


public int hashCode(){ Parola p = indice.get( ortografia );
finalint MOLT=43; if( p == nuli ) return 0;
int h=0; return p.size();
for( Parola p: this ) h=h*MOLT+p.hashCode(); }//occorrenze
return h;
}//hashCode public void add( String ortografia, int nr ){
Parola p=indice.get( ortografia );
}//lndiceAstratto if( p==null ){
p=new Parola( ortografia ); p.add( nr ); indice.put( ortografia, p );
La classe IndiceLinkato: }
package poo.indice; else p.add( nr );
import java.util.*; }//add
public class IndiceLinkato extends lndiceAstratto{
private LinkedList<Parola> indice=new LinkedList<Parola>(); }//lndiceMappato
public lterator<Parola> iterator(){ return indice.iterator();}
Una concretizzazione custom di IndiceAstratto:*S
i
public int size(){ return indice.size();} package poo.indice;
import poo.util.*;
public void add( String ortografia, int nr ){ import java.util.*;
Parola p = new Parola( ortografia ); public class IndiceSuAlbero extends IndiceAstrattoj
p.add( nr ); //provvisorio private AlberoBinarioDiRicerca<Parola> indice=new AlberoBinarioDiRicerca<Parola>();
Listlterator<Parola> li = indice.listlterator();
boolean flag = false; public int occorrenze! String ortografia ){
while( li.hasNext() ){ Parola p = new Parola! ortografia );
Parola q = li.next(); p = indice.getj p );
if( q.equals(p) ){//se p c’e’ già' gli si aggiunge solo il num di linea if( p==null ) return 0;
q.add( nr ); return; return p.sizej);
} }//occorrenze
if( q.compareTo(p)>0 ){
li.previous(); li.add( p ); flag=true; break; public void add( String ortografia, int nr ){
Parola p = new Parola! ortografia );
if( !indice.contains( p ) ){ p.add( nr ); indice.addj p );}
if( Iflag ) li.add( p ); else{ p = indice.getj p ); p.add( nr );}
}//add }//add

}//lndiceLinkato public lterator<Parola> iterator(){ return indice.iterator(); }//iterator

IndiceLinkato non ridefinisce il metodo occorrenzeQ in quanto la lista non consente di “far meglio” della ricerca }//lndiceSuAlbero
lineare presente nella versione nella classe IndiceAstratto.
Si utilizza un albero binario di ricerca, disponibile nel package poo.util, provvisto della struttura di iterazione.
La classe IndiceMappato:
package poo.indice; L'applicazione Crosslndex:_____________
import java.util.*; package poo.indice;
public class IndiceMappato extends IndiceAstratto! import java.io.*;
private Map<String,Parola> indice=new TreeMap<String,Parola>(); import java.util.*;
public lterator<Parola> iterator(){ return indice.values().iterator();}
public class Crosslndexj
public int size(){ return indice.size();} public static void main( String []args ) throws lOExceptionj
System.out.printlnflndice dei riferimenti incrociati");
Scanner s=new Scanner! System.in );
360 361
Capitolo 21 Sviluppo di programmi ad oggetti

String nomeFile=null; finalizzate appunto al riempimento della tabella dei simboli. Un’altra tabella è destinata a memorizzare il
File f=null; codice oggetto di un programma.
do{ L’assemblatore della macchina RASP è a due passate. Durante la prima passata si considerano solo le
System.out.printfNome file testo = "); etichette delle istruzioni simboliche. Di ogni etichetta si memorizza nella tabella dei simboli se essa è di dati
nomeFile = s.nextLine(); (es. il nome di un array introdotto con una RES(erve) o di istruzione (target di un salto). Nella seconda passata
f = new File(nomeFile); si considera il campo operando delle istruzioni. Ogni simbolo-operando non presente nella tabella dei simboli
if( !f.exists() ) System.out.println("File inesistente. Ridarlo!"); viene aggiunto (dichiarazione implicita). Si verifica inoltre la compatibilità tra modo, operando e codice
}while( !f.exists() ); operativo. Per l’attribuzione degli indirizzi alle etichette, l'assemblatore utilizza due contatori: il PLC (Program
Location Counter) per le istruzioni, e il DLC (Data Location Counter) per i dati.
GestoreTesto gt = new GestoreTesto( f );
Indice indice = new lndiceSuAlbero(); //esempio DLC viene inizializzato con l'indirizzo di partenza di un’area di memoria destinata a contenere tutte le celle
String word = nuli; dati. Il programma oggetto, per convenzione, è caricato a partire dall’indirizzo 0 di memoria, dunque PLC è
int numLinea = 0; inizializzato a 0. L'incremento di PLC tiene conto della lunghezza dell’istruzione che al massimo può essere 3:
GestoreTesto.Simbolo simbolo = nuli; codice operativo, modo, operando. Anziché impaccare in un’unica cella un’intera istruzione, i vari pezzi sono
posti in celle consecutive di memoria. L'indirizzo di una etichetta coincide col valore corrente del PLC.
for(;;){
NeH’implementazione proposta l'area delle celle dati è posta subito dopo l’area del programma oggetto. DLC si
simbolo = gt.prossimoSimbolo(); incrementa ad ogni dato, di 1 per una variabile semplice, di N per un blocco (array) di N interi. Il valore di DLC
if( simbolo==GestoreTesto.Simbolo.EOF ) break; corrente è usato per definire l’indirizzo di un simbolo di dato.
word = gt.getString().toUpperCase(); Dopo la seconda passata, la tabella dei simboli è completa e può partire la generazione di codice. Il file
numLinea = gt.getNumeroLinea(); sorgente viene scandito nuovamente e per ogni effettiva istruzione RASP (le RES a questo punto sono
indice.add( word, numLinea ); ignorate) si genera la corrispondente istruzione di macchina con l'ausilio delle tabelle dei codici operativi, dei
simboli e dei modi.

System.out.println(); Un programma RASP tradotto è già caricato in memoria, pronto per essere eseguito.
System.out.println(“Contenuto deirindice');
System.out.println( indice ); Organizzazione del programma: ____ _____
}//main La figura che segue illustra schematicamente le classi nelle quali il problema è stato decomposto.
L’assemblatore è programmato nella classe Assembler e si avvale di un Lexer (analizzatore lessicale) per
}//Crosslndex estrarre dal testo sorgente, uno alla volta e su richiesta, tutti i simboli presenti. ObjectModule supporta la
generazione di codice macchina.
Il programma acquisisce il nome del file di tipo testo da elaborare, lo passa ad un’istanza di GestoreTesto,
istanzia un oggetto Indice, quindi ottiene in ciclo una alla volta i simboli delle parole distinte del file e aggiunge
ogni parola, unitamente al suo numero di linea, all’indice. Alla fine, il contenuto dell’indice è visualizzato su
standard output. La sinteticità del programma ed il suo carattere astratto (ottenimento dei simboli, gestione
dell’indice) è conseguenza delle classi in cui è stata organizzata l’applicazione.

Un’implementazione della macchina RASP__________ __________ ______ _____


Si presenta un'implementazione della macchina RASP {si veda l’Appendice B) che consiste di un
assemblatore ed un interprete. L’assemblatore traduce un programma sorgente assembler RASP in un
corrispondente programma oggetto (numerico), nel quale le etichette di istruzioni e dati sono rimpiazzate da
indirizzi, ed anche i codici operativi sono trasformati in codici numerici. L’interprete esegue un programma
oggetto caricato in memoria. Esso realizza essenzialmente il ciclo-istruzione della CPU che è iterato sino ad
un’istruzione HALT o al primo errore.

L’assemblatore a due passate:__________________ ___________________


Per effettuare la traduzione, un assemblatore provvede, in generale, a scandire una o più volte il programma
sorgente memorizzato in un file di tipo testo, e a consultare una o più tabelle di supporto. Ad es. la tabella dei Un diagramma di classi per la macchina RASP
codici operativi contiene la corrispondenza tra codice operativo simbolico e codice numerico. Fondamentale è
la tabella dei simboli costruita durante l'analisi del programma sorgente, che conserva per ogni etichetta JRVM è la Java Rasp Virtual Machine, ossia il componente che emula la macchina RASP, i registri, la
(simbolo) del programma, l'indirizzo di memoria cui il simbolo è legato. Le scansioni del file sorgente sono memoria etc. In JRVM è presente l’interprete che simula il comportamento della CPU. L’assemblatore genera

362 363
C ap ito lo 21 Sviluppo di programmi ad O f fa *

il file listing sul quale registra gli eventuali errori, il codice macchina generato, la mappa di memoria delle protected void finalyze() throws IOException{ br.closeQ; pw.close(); }//finalize
variabili etc. Segue il codice delle varie classi, da studiare come esercizio.
public void error( String err )( pw.println( err ); pw.flushQ; System.exit(-1); }//error
Analizzatore lessicale (classe Lexer): ________________________ ________________________ public void toListing( String s ){ pw.println(); pw.println(s); pw.flush(); }//toListing
package poo.rasp;
import java.util.*; public void setEnabledEcho( boolean value )( enabledEcho=value; }//setEnabledEcho
import java.io.*;
public class Lexer { public Sim prossimoSimbolo() throws IOException{
public enum Sim{ IDENT, NUMBER, END LABEL, MODE, SP, UNKNOWN, EOF } if( linea==null )( return Sim.EOF;}
private int num; if( !st.hasMoreTokens() ){
private String str, nomeFileSorgente, nomeFileListing, tk, linea; nextLine();
private StringTokenizer st; if( linea==null ) { return Sim.EOF;}
private BufferedReader br; }
private PrintWriter pw; tk=st.nextToken();
private int lineaCorrente=0; if( tk.charAt(0)==' ' Il tk.charAt(0)==‘\t' ) { str=" “; return Sim.SP; }
private boolean enabledEcho=true; if( tk.matches(ID) ) { str=tk; return Sim.IDENT; }
private String ID="[a-zA-Z][a-zA-Z0-9^$]**; if( tk.matches(INT) ){ num=lnteger.parse/nf(?/c);return Sim .NUMBER;}
private String INT="-?[0-9]+"; if( tk.charAt(0)==';' ){ str=tk; return Sim.EA/D LABEL;)
public Lexer( String nomeFileSorgente, String nomeFileListing ) throws IOException{ if( tk.charAt(0)=='#' Il tk.charAt(0)=='@' ){ str=tk; return Sim .MODE; }
this.nomeFileSorgente=nomeFileSorgente; if( tk.charAt(0)==';' ){//un commento è equiparato ad uno spazio
this.nomeFileListing=nomeFileListing; //skip comment
File f=new File(nomeFileSorgente); nextLine();
if( !f.exists() ) throw new RuntimeExceptionfFile "+nomeFileSorgente+“ non esistente!"); str=" ";
br=new BufferedReader( new FileReader(f) ); return Sim.SP;
pw=new PrintWriter( new FileWriter(nomeFileListing) ); }
nextLine(); return S\m. UNKNOWN;
} }//prossimoSimbolo

private void nextLine() throws IOException{ }//Lexer


do{
linea=br.readLine(); L'Assemblatore (classe Assembler):
if( linea!=null ){ package poo.rasp;
st=new StringTokenizer(linea," :#@;\t',true); import java.io.*;
lineaCorrente++; import java.util.*;
if( enabledEcho ){
pw.println(lineaCorrente+": ’ +linea); public class Assembler {
} //Assemblatore RASP case-sensitive sugli identificatori
} //I codici operativi sono riservati e vanno scritti in maiuscolo
}while( linea!=null && !st.hasMoreTokens() ); private TabellaSimboli tab=new TabellaSimboli();
}//nextLine public static Map<lnteger,String>opCodeString=
new HashMap<lnteger,String>();
public void rewind() throws IOException{ private Map<String,lnteger> opCode=
br.close(); new HashMap<String,lnteger>();
br=new BufferedReader( new FileReader(nomeFileSorgente) ); private Map<Character,lnteger> modi=
nextLine(); new HashMap<Character,lnteger>();
}//rewind; private Lexer lex=null;
private String RES="RES";
public String getStr(){ return s tr;} private String OPCODE=
"(ADDISUBIMULIDIVIHALTIJZIJNZIJGZIJLZIJLEZIJGEZIJUMPILOADISTOREIREADIWRITE)";
public int getNum(){ return num ;} private String JUMP=
"(JZIJNZIJGZIJLZIJLEZIJGEZIJUMP)";
364 365
Capitolo 21^ Sviluppo di programmi ad oggetti

private boolean declaration=true; //processa etichetta istruzione


private Lexer.Sim simbolocorrente; etichetta=lex.getStr();
private String etichetta, opc; tab.add( new Simbolo(etichetta) );
private char modo; avanza();
private int numero; //check :
private int pie; //program location counter if( simboloCorrente!=Lexer.Sim.END LABEL ){
private int die; //data location counter lex.errorf Atteso :”);
private ObjectModule codice=null;
}
private boolean esisteHALT=false; avanzai);
}
public Assemblei String nomeFileSorgente, String nomeFileListing ) //check OpCode
throws IOException{ if( simboloCorrente!=Lexer.Sim./DE/\/7){
lex=new Lexer(nomeFileSorgente, nomeFileListing); lex.error(''Atteso Codice Operativo");
opCode.put("LOADMO); opCodeSfring.put(10,"LOAD"); }
opCode.putfSTOREM 1); opCodeString.pu\(] 1,‘ STORE"); opc=lex.getStr();
opCode.put("READ",12); opCodeString.put(12,"READ"); if( opc.equals(RES) ){
opCode.put("WRITEM3); opCodeSfr/ng.put(13,“WRITE"); if( Ideclaration ) lex.error("RES inattesa");
opCode.put(“ADD",14); opCodeString.put^A'ADD'y //fissa tipo dell’etichetta di RES
opCode.put(“SUB“,15); opCoc/eS/nnp.put(15,"SUB"); avanzai);
opCode.put("MUL",16); opCodeSfr/n0.put(16,“MUL"); if( simboloCorrente!=Lexer.Sim./VL//W6ER ){
opCode.put("DIV“,17); opCodeString.put(17,"DIV“); lex.error("Atteso numero");
opCode.put("JZ",18); opCodeString.put(18,"JZ‘); }
opCode.put(“JNZ“,19); opCodeSfr/np.put(19,"JNZB); numero=lex.getNum();
opCode. put("JLZ",20); opCodeString.put(20, "JLZ“); if( numero<0 ) lex.errorfNumero negativo");
opCode.put(”JLEZ",21); opCodeString.put(21,"JLEZ"); Simbolo s=tab.find(etichetta);
opCode.put("JGZ“,22); opCodeString.pu\(22'JGZ"y s.setSize(numero);
opCode.put(\JGEZ",23); opCodeString.pu\(23,"JGEZ‘ y s.setTipo( Simbolo.Tipo.DA70);
opCode.put(“JUMP",24); opCodeS/r/ng.put(24)”JUMP"); avanzai);
opCode.put('HALT‘ , 25); opCodeString.put(25,"HALT"); }
modi.putC 0);//diretto else if( !opc.matches(OPCODE) ){
modi.putC#', 1);//immediato lex.error("Codice operativo illegale");
modi.putC 2);//indiretto }
}//Assembler else{//codice operativo ok
declaration=false;
private void avanza() throws IOException{ if( lex.getStr().equals("HALT") ){
do{ esisteHALT=true;
simboloCorrente=lex.prossimoSimbolo(); avanzai);
}while( simboloCorrente==Lexer.Sim.SP ); continue;
}//avanza
}
avanzai);
public void compile() throws IOException{ //check modo
//1 passata - costruzione della tabella dei simboli modo=' ';
System. ou/.println("Prima passata..."); if( simboloCorrente==Lexer.Sim.MODE ){
simboloCorrente=lex.prossimoSimbolo(); modo=lex,getStr().charAt(0);
while( simboloCorrente!=Lexer.Sim. EOF ){ if( modo=='#' ){
//salta spazi if( opc.equals(“READ")llopc.equalsi',STORE")ll
while( simboloCorrente==Lexer.Sim.SP ) opc.matches(JUMP) )
simboloCorrente=lex.prossimoSimbolo(); lex.errorfModo incompatibile col codice operativo");
//check primo simbolo }
if( simboloCorrente==Lexer.Sim./DE/\/7&& avanzai);
!lex.getStr().matches(OPCODE)){ }
366 367
Capitolo 21^ Sviluppo di programmi ad oggetti

//check operando if( opc.equalsfHALT") ){


if( simboloCorrente!=Lexer.Sim. IDENT&& plc++;
simboloCorrente!=Lexer.Sim./V(J/WBEfl){ avanzai);
lex.error("Atteso operando"); continue;
} }
if( simboloCorrente==Lexer.Sim./DEA/7){ if( !opc.equals("RES") ) plc=plc+3;
if( !opc.matches(JUMP) ){ //arriva a operando
//dichiarazione implicita di variabile ? do{
String opnd=lex.getStr(); simboloCorrente=lex.prossimoSimbolo();
if( tab.find(opnd)==null ){//si }while( simboloCorrente!=Lexer.Sim.IDENT&&
Simbolo so=new Simbolo( opnd ); simboloCorrente!=Lexer.Sim./VL//WBER);
so.setTipo( Simbolo.Tipo.DATO)', if( simboloCorrente==Lexer.Sim.IDENT){
tab.add( so ); //verifica etichette istruzioni
if( opc.matches(JUMP) &&
tab.find(lex.getStr())==null )
avanza(); lex.error("Etichetta istruzione non definita");
} }
else{//simboloCorrente==Lexer.Sim.NUMBER avanzai);
avanza(); }
} //assegna ora gli indirizzi ai dati
} dlc=plc; //primo indirizzo utile per i dati
} for( Simbolo s: tab ){
if( s.getTipo()==Simbolo.Tipo.DATO ){
if( lesisteHALT ) s.setlndirizzo(dlc);
lex.error("Manca istruzione HALT’ ); dlc=dlc+s.getSize();

//2 passata }
//assegna indirizzi logici lex.toListing(tab.toStringO);
//verifica etichette istruzioni 113 passata - code generation
System.ou/.println(“Seconda passata System.ou/.printlnfGenerazione di codice ...");
iex.setEnabledEcho(false); codice=new ObjectModule();
lex.rewind(); lex.rewind();
simboloCorrente=lex.prossimoSimbolo(); simboloCorrente=lex.prossimoSimbolo();
plc=0; while( true ){
while( simboloCorrente!=Lexer.Sim.EOF ){ //arriva ad un opcode - salta le RES e le etichette
//salta spazi while( simboloCorrente!=Lexer.Sim.EOF&& !lex.getStr().matches(OPCODE) ){
while( simboloCorrente==Lexer.Sim.SP ) simboloCorrente=lex.prossimoSimbolo();
simboloCorrente=lex.prossimoSimbolo(); }
//check primo simbolo if( simboloCorrente==Lexer.Sim.EOF) break;
if( simboloCorrente==Lexer.Sim./DE/\/7&& opc=lex.getStr();
!lex.getStr().matches(OPCODE)){ avanzai);
//processa etichetta istruzione if( opc.equals("HALT") ){
etichetta=lex.getStr(); codice.addlnstruction( opCode.getCHALT") ); continue;
Simbolo s=tab.find(etichetta); }
if( s.getTipo()==Simbolo.Tipo./S7fì ){ modo=' ’;
s.setlndirizzo(plc); if( simboloCorrente==Lexer.Sim. MODE ){
} modo=lex ,getStr() .charAt(O);
avanza(); //salta : avanzai);
avanza(); //arriva ad opcode }
} int indirizzoOperando=0;
opc=lex.getStr(); if ( simboloCorrente==Lexer.Sim. IDENT ){
368 369
Capitolo 21 Sviluppo di programmi ad oggetti

indirizzoOperando=
tab.find(lex.getStr()).getlndirizzo(); public void addlnstruction( int opCode ) { image.add(opCode); }//addlnstruction
}
else public void addData( int size ){
indirizzoOperando=lex.getNum(); if( size<=0 ) throw new IHegalArgumentException();
codice.addlnstruction( opCode.get(opc), for( int i=0; i<size; i++ ) image.add(O);
modi.get(modo), }//addData
indirizzooperando );
avanza(); public int size(){ return image.size();}
}
public String toString(){
//aggiunta celle dati a codice int p=0;
lor( Simbolo s: tab ){ StringBuilder sb=new StringBuilder(500);
if( s.getTipo()==Simbolo.Tipo.DA7"0 ){ for( int x: image ){
codice.addData( s.getSize() ); sb.append(p); sb.appende ');
} sb.appendi x ); sb.append('\n');
} P++;
}
lex.toListing("Tabella codici operativi'); return sb.toString();
lex.toListing(opCode.toString()); }//toString

lex.toListingC'Tabella dei modi"); public lterator<lnteger> iterator(){


lex.toListing(modi.toStringO); return image.iterator();
}//iterator
lex.toListingC'Codice macchina generato");
lex.toListing(codice.toString()); }//ObjectModule

}//compile La classe Simbolo: ______________________________


package poo.rasp;
public ObjectModule getObjectModule(){ public class Simbolo implements Comparable<Simbolo>{
return codice; public enum Tipo{ ISTR, DATO}
private String nome;
private int indirizzo=-1;
public static void main( Stringo args ) throws IOException{//test private int size=1 ; //default
String nomeFileSorgente="c:\\poo-file\\inversione.rasp"; private Tipo tipo=Tipo.ISTR;//default
String nomeFileListing="c:\\poo-file\\inversione.listing"; public Simbolo( String nome ){ this.nome=nome; }//Simbolo
Assembler ass=new Assembler(nomeFileSorgente, nomeFileListing);
ass.compile(); public String getNome(){ return nome;}
public int getlndirizzo(){ return indirizzo;}

}//Assembler public int getSize(){ return size;}


public Tipo getTipo(){ return tipo;}
La classe ObjectModule:___________________________________
package poo.rasp; public void setlndirizzo( int indirizzo ) { this.indirizzo=indirizzo;}//setlndirizzo
import java.util.*;
public class ObjectModule implements lterable<lnteger>{ public void setSize( int size ){
private List<lnteger> image=new ArrayList<lnteger>(); if( size<=0 ) throw new HlegalArgumentException();
this.size=size;
public void addlnstruction( int opCode, int mode, int operand ){ }//setSize
image.add(opCode); image.add(mode); image.add(operand);
}//add Instruction public void setTipo( Tipo tipo ) { this.tipo=tipo; }//setTipo
370 371
Capitolo 21 Sviluppo di programmi ad oggattl

public class JRVM{


public int hashCode(){ return nome.hashCode(); }//hashCode
private int[] mem;
public boolean equals( Object x ){ private int acc, ip, opcode, modo, operando;
if( !(x instanceof Simbolo) ) return false;
if( x==this ) return true; public void loader( ObjectModule om ){
Simbolo s=(Simbolo)x; mem=new int[om.size());
return this.nome.equals(s.nome); int i=0;
for( int cod: om ){ mem[i]=cod; i++;}
}//loader
public int compareTo( Simbolo s ) { return this.nome.compareTo(s.nome); }//compareTo
public void interpreter() throws IOException{
public String toString(){ return nome+" @ “+indirizzo+" size: "+size+" tipo: “+tipo; }//toString Scanner ni=new Scanner( System.in );
}//Simbolo ip=0;
while( true ){
La classe TabellaSimboli: String opc=null;
package poo.rasp; try{
import java.util.*; opcode=mem[ip|;
public class TabellaSimboli implements lterable<Simbolo>{ opc=Assembler.opCodeString.get(opcode);
private Map<Simbolo,Simbolo> tabella=new TreeMap<Simbolo,Simbolo>(); if( opc==null ) throw new RuntimeException();
}catch( Exception e ){
public lterator<Simbolo> iterator(){ return tabella.keySet().iterator(); }//iterator System.out.printlnf'Codice operativo illegale all'indirizzo "+ip);
System.exit(-I);
public void add( Simbolo s ) { tabella.put(s,s);}//add }
public void add( String s ){ add( new Simbolo(s) );}//add if( opc.equals("HALT") ){ break;}
modo=mem[ip+1);
public void remove( Simbolo s ) { tabella.remove(s); }//remove operando=mem[ip+2];
public void remove( String s ){remove( new Simbolo(s) ); }//remove ip=ip+3; //default increment
if( opc.equalsfLOAD") ){
public Simbolo find( Simbolo s ) { return tabella.get( s ); }//find if( modo==0 ) acc=mem[operando];
public Simbolo find( String s ) { return find( new Simbolo(s) );}//find else if( modo==1 /*#*/ ) acc=operando;
else/* @7 acc=mem[mem[operando]];
public void clear(){ tabella.clear(); }//clear }
else if( opc.equals(''STORE“) ){
public String toString(){ if( modo==0 ) mem[operando]=acc;
StringBuilder sb=new StringBuilder(500); else if( modo==2 ) mem[mem[operando]]=acc;
sb.append(“Contenuto Tabella dei Simboli\n"); }
for( Simbolo sim: this) sb.append(sim+”\n"); else if( opc.equals("READ") ){
return sb.toString(); int dato=0;
}//toString do{
System.out.print("int>");
}//TabellaSimboli try{
dato=ni.nextlnt();
La classe JRVM: }catch( Exception e ){
package poo.rasp; System.out.println("?”);
import java.io.*; if( e instanceof NoSuchElementException ){
import java.util.*; ni=new Scanner(System.in);
}
else{
ni.nextLine();
}
372 373
C ap ito lo 21 Sviluppo di programmi ad oggetti

continue; ip=operando;
} else if( modo==2 )
break; ip=mem[operando];
}while(true);
}
if( modo==0 ) mem[operando]=dato;
}
else if( modo==2 ) mem[mem[operando]]=dato; else if( opc.equals("JLZ") ){
} if( acc<0 ){
else if( opc.equals("WRITE“) ){ if( modo==0 )
if( modo==0 ) System.out.println(mem[operando]); ip=operando;
else if( modo==1 ) System.out.println(operando); else if( modo==2 )ip=mem[operando];
else System.out.println(mem[mem[operando]]);
}
else if( opc.equalsfADD") ){ else if( opc.equals(“JLEZ") ){
int dato=0; if( acc<=0 ){
it( modo==0 ) dato=mem[operando]; if( modo==0 )
else if( modo==1 ) dato=operando; ip=operando;
else dato=mem[mem[operando]]; else if( modo==2 )
acc=acc+dato; ip=mem[operando];
}
else if( opc.equalsCSUB") ){
int dato=0; else if( opc.equalsf'JGZ") ){
if( modo==0 ) dato=mem[operando]; if( acc>0 ){
else if( modo==1 ) dato=operando; if( modo==0 )
else dato=mem[mem[operando]]; ip=operando;
acc=acc-dato; else if( modo==2 )
} ip=mem[operando];
else if( opc.equalsfMUL") ){
int dato=0;
if( modo==0 ) dato=mem[operando]; else if( opc.equalsfJGEZ") ){
else if( modo==1 ) dato=operando; if( acc>=0 ){
else dato=mem[mem[operando]]; if( modo==0 )
acc=acc*dato; ip=operando;
} else if( modo==2 )
else if( opc.equalsfDIV") ){ ip=mem[operando];
int dato=0;
if( modo==0 ) dato=mem[operando];
else if( modo==1 ) dato=operando; else if( opc.equals(\JUMP") ){
else dato=mem[mem[operando]]; if( modo==0 )
acc=acc/dato; ip=operando;
} else if( modo==2 )
else if( opc.equalsfJZ") ){ ip=mem[operando];
if( acc==0 ){
}
if( modo==0 ) else{
ip=operando; System.out.printlnflnternal error“); System.exit(-1);
else if( modo==2 )
ip=mem[operando];
}//interpreter

else if( opc.equalsfJNZ") ){


if( acc!=0 ){
if( modo==0 )
374 375
Capitolo 21 Sviluppo di programmi ad oggetti

public static void main( String []args ) throws IOException{ package poo.grafo;
Scanner sc=new Scanner(System.in); import java.util.*;
String nomeFileSorgente=null, nomeFileListing=null;
File f=null; public interface Grafo<N> extends lterable<N>{
boolean ok; int numNodiQ;
do{ int numArchi();
ok=true; boolean esisteNodo( N u );
System.out.print(“Nome file sorgente: "); boolean esisteArco( Arco<N> a );
nomeFileSorgente=sc.nextLine(); boolean esisteArco( N u, N v );
f=new File( nomeFileSorgente ); void insNodo( N u );
if( !f.exists() ){ void insArco( Arco<N> a );
System.out.printlnf'File non esistente. Ridarlo"); void insArco( N u, N v );
ok=false; void rimuoviNodo( N u );
} void rimuoviArco( Arco<N> a );
if( ok ){ void rimuoviArco( N u, N v );
int i=nomeFileSorgente.lastlndexOf(7); Iteratone? extends Arco<N» adiacente N u );
if( i==-1 ){ void clear();
System.out.printlnf'll file deve essere .rasp. Ridarlo"); Grafo<N> copia();
ok=false; }//Grafo
}
if( ok ){
String estensione=nomeFileSorgente.substring(i+1 );
if( lestensione.equalsIgnoreCasefrasp") ){
System.out.println("ll file deve essere .rasp. Ridarlo");
ok=false;
}

}while( !ok );
int i=nomeFileSorgente.lastlndexOf(7);
String nomeFile=nomeFileSorgente.substring( 0, i );
nomeFileListing=nomeFile+".listing";
Assembler ass=new Assemblei nomeFileSorgente, nomeFileListing );
ass.compile();
ObjectModule om=ass.getObjectModule();
JRVM jrvm=new JRVM();
jrvm.loader( om ); jrvm.interpreter();
}//main

J//JRVM

Una gerarchia di classi per la gestione dei grafi


Di seguito si riporta lo sviluppo di una gerarchia di classi e interfacce (package poo.grafo), utile per lavorare
sui grafi (si riveda il cap. 19 per i concetti sui grafi). La gerarchia è sintetizzata nel diagramma di classi UML
che segue.

Il grafo come abstract data type:________________________________________________________________ Un diagramma di classi per i grafi


L'interfaccia fondamentale Grafo<N>, con N il tipo generico delle etichette dei nodi, riportata nel cap. 19 e qui
riprodotta per comodità, descrive un grafo come ADT:

376 377
Capitolo 21 Sviluppo di programmi ad oggetti

La classe Arco<N>: public ArcoPesato( N origine, N destinazione ){


Modella un arco generico. Incapsula i due nodi origine e destinazione. Offre il costruttore Arco( N origine, N super( origine, destinazione );
destinazione ), e i metodi accessori N getOrigine(), N getDestinazione() insieme con gli usuali metodi equals(), }
hashCode() e toString().
public Peso getPeso(){ return peso; }//getPeso
package poo.grafo;
public class Arco<N>{ public void setPeso( Peso peso ) { this.peso=peso; }//setPeso
private N origine, destinazione;
public Arco( N origine, N destinazione ){ public String toString(){
this.origine=origine; return V+super.toString()+7+peso+V;
this.destinazione=destinazione; }//toString
}
public N getOrigine(){ }//ArcoPesato
return origine;
}//getOrigine La classe GrafoAstratto<Nx_____________________________________ _____________ _______________
implementa l’interfaccia Grafo<N> e concretizza quanti più metodi è possibile. Introduce il metodo astratto
public N getDestinazione(){ factory() che una classe erede concreta deve implementare:
return destinazione;
}//getDestinazione public abstract Grafo<N> factory()

@SuppressWamings(''unchecked") Tramite factory() si riesce a concretizzare il metodo copia(). I metodi di Grafo<N> non concretizzati né elencati
public boolean equals( Object o ){ in GrafoAstratto<N> (es. iteratori, i metodi di inserimento etc.) sono ovviamente abstract implicitamente.
if( !(o instanceof Arco ) ) return false;
if( o==this ) return true; package poo.grafo;
Arco<N> a=(Arco<N>)o; import java.util.*;
return this.origine.equals(a.getOrigine()) &&
this.destinazione.equals(a.getDestinazione()); public abstract class GrafoAstratto<N> implements Grafo<N>{
}//equals
public boolean esisteNodo( N u ){
public int hashCode(){ for( N v: this ) if( v.equals(u) ) return true;
int numeroj)rimo=811; return false;
return origine.hashCode()*numero_primo+destinazione.hashCode(); }//esisteNodo
}//hashCode
public boolean esisteArcof Arco<N> a ){
public String toString(){ N u=a.getOrigine();
return -<"+origine+",“+destinazione+“>"; if( esisteNodo(u) ){
}//toString lterator<? extends Arco<N» it=this.adiacenti(u);
while( it.hasNext() ) {
}//Arco Arco<N> ar=it.next();
if( ar.equals(a) ) { return true;}
La classe ArcoPesato<N>:
Estende Arco<N> ed introduce il concetto di peso che per generalità è un’istanza della classe Peso (si veda }
piu avanti per i dettagli). ArcoPesato<N> espone metodi per interrogare ed eventualmente cambiare il peso return false;
dell'arco. }//esisteArco

package poo.grafo; public boolean esisteArco( N u, N v ){


public class ArcoPesato<N> extends Arco<N>{ return esisteArco( new Arco<N>(u,v) );
private Peso peso; }//esisteArco
public ArcoPesato( N origine, N destinazione, Peso peso )(
super( origine, destinazione ); this.peso=peso;

378 379
C ap ito lo 21 Sviluppo di programmi ad oggetti

public int numNodi(){


public void clear(){
int n=0; lterator<N> it=this.iterator();
for( N u: this ) n++; while( it.hasNext() ){
return n;
it.next();
}//numNodi it.remove();
public int numArchi(){ }
}//clear
int na=0;
for( N u: this ){ public Grafo<N> copia(){
Iteratone? extends Arco<N» it=this.adiacenti(u); Grafo<N> copia=factory();
while( it.hasNext() ){ for( N u: this ){
it.next(); copia.insNodo(u);
na++;
}
for( N u: this ){
Iteratone? extends Arco<N» it=this.adiacenti(u);
return na; while( it.hasNext() ){
}//numArchi Arco<N> ac=it.next();
copia.insArco( new Arco<N>(ut ac.getDestinazioneQ) );
public void insArco( N u, N v ){
insArco( new Arco<N>(u,v) );
}//insArco return copia;
}//copia
public void rimuoviNodo( N u ){
lterator<N> it=this.iterator(); private boolean equals( GrafoAstratto<N> g l, GrafoAstratto<N> g2, N u ){
while( it.hasNext() ){ for( N v: gl ){
N v=it.next(); Arco<N> a = new Arco<N>(u,v);
if( v.equals(u) ){ if( gl.esisteArco(a) && !g2.esisteArco(a) ) return false;
it.remove(); if( g2.esisteArco(a) && !g1 .esisteArco(a) ) return false;
break;
}
} return true;
} }//equals
}//rimuoviNodo
@SuppressWarnings(”unchecked")
public void rimuoviArco( Arco<N> a ){ public boolean equals( Object o ){
N u=a.getOrigine(); if( !(o instanceof GrafoAstratto) ) return false;
if( esisteNodo(u) ){ if( o==this ) return true;
lterator<? extends Arco<N» it=this.adiacenti(u); GrafoAstratto<N> g=(GrafoAstratto)o;
while( it.hasNext() ) { if( this.numNodi()!=g.numNodi() Il
Arco<N> ar=it.next(); this.numArchi()!=g.numArchi() ) return false;
if( ar.equals(a) ){it.remove();break;} for( N u: this ){
if( Ig.esisteNodo(u) ) return false;
if( !equals(this,g,u) ) return false;
}//rimuoviArco
}
return true;
public void rimuoviArco( N u, N v ){ }//equals
rimuoviArco( new Arco<N>(u,v) );
}//rimuoviArco

public abstract Grafo<N> factory();

380 381

Capitolo 21 Sviluppo di programmi ad oggetti

public int hashCode(){ La classe GrafoNonOrientatolmpkNx___________________________________________________________


int p=41, h=0; Estende GrafoNonOrientatoAstratto<N> ed è una classe concreta. Il grafo è rappresentato mediante una
for( N u: this ){ HashMap in cui la chiave è un’etichetta N ed il valore una LinkedList di archi. Si tratta di una memorizzazione
h=h*p+u.hashCode(); del tipo liste di adiacenze, in cui l’uso della mappa velocizza l’identificazione della lista delle adiacenze di un
for( N v: this ){ assegnato nodo.
Arco<N> a = new Arco<N>(u,v);
if( this.esisteArco(a) ){h=h*p+a.hashCode();} package poo.grafo;
import java.util.*;
public class GrafoNonOrientatolmpl<N> extends GrafoNonOrientatoAstratto<N>{
return h; private Map<N,LinkedList<Arco<N»> grafo=new HashMap<N,LinkedList<Arco<N»>();
}//hashCode
private class IteratoreGrafo implements lterator<N>{
public String toString(){ private lterator<N> it=grafo.keySet().iterator();
StringBuilder sb=new StringBuilder(500); private N u=null;
for( N u: this ){ public boolean hasNext))) return it.hasNext));}
sb.append(u); sb.append(':'); sb.appende '); public N next(){ return u=it.next();}
lterator<? extends Arco<N» it=this.adiacenti(u); public void remove(){
while( it.hasNext() ) { sb.append( it.next() );} it.remove)); //toglie il nodo corrente e le sue adiacenze
sb.append('\n'); //occorre anche togliere tutti gli archi in cui il nodo corrente
} //è destinazione
return sb.toString(); Set<N> chiavi=grafo.keySet();
}//toString lterator<N> it=chiavi.iterator();
while) it.hasNext)) ){
}//G ratoAstratto N v=it.next();
Iteratone? extends Arco<N» adiacenti=
Il metodo equals() si appoggia ad un metodo privato equals() che riceve due grafi gl e g2 ed un nodo u del grafo.get(v).iterator));
primo grafo e verifica se gl e g2 sono uguali a partire da u. while) adiacenti.hasNext)) ) {
Arco<N> a=adiacenti.next();
L’interfaccia GrafoNonOrientato<N>:_________________________________________ _________________ _ if( a.getDestinazione().equals(u) ){
Estende Grafo<N> e introduce in più il solo metodo int grado) N u ) che ritorna il numero di archi in entrata sul adiacenti.remove)); break;
ed in uscita dal nodo. Ogni arco è navigabile nei due sensi, ossia è doppio.

package poo.grafo; }
public interface GrafoNonOrientato<N> extends Grafo<N>{ public int grado) N u ); }//GrafoNonOrientato }
}//lteratoreGrafo
La classe GrafoNonOrientatoAstratto<N>:_______________________________________________________
Estende GrafoAstratto<N> ed implementa GrafoNonOrientato<N>. private class IteratoreAdiacenti implements lterator<Arco<N»{
private lterator<? extends Arco<N» it;
package poo.grafo; private Arco<N> a=null;
import java.util.*; public IteratoreAdiacenti) N u ){
public abstract class GrafoNonOrientatoAstratto<N> extends GrafoAstratto<N> it=grafo.get(u).iterator();
implements GrafoNonOrientato<N>{ }
public int grado) N u ){ public boolean hasNext))) return it.hasNext)); }//hasNext
int g=0; public Arco<N> next(){
if( esisteNodo(u) ){ if( lit.hasNext)) ) throw new NoSuchElementException));
Iteratone? extends Arco<N» it=adiacenti(u); a=it.next();
while) it.hasNext)) ){it.next();g++;} return a;
} }//next
return g;
}//grado
}//GrafoNonOrientatoAstratto
382 383
Capitolo 21 Sviluppo di programmi ad oggetti

public void remove(){ while( adiacenti.hasNext() ) {


it.remove(); Arco<N> a=adiacenti.next();
//rimuovi adesso l'arco inverso if( a.getDestinazione().equals(u) ){
Arco<N> ai=new Arco<N>(a.getDestinazione(),a.getOrigine()); adiacenti.removeQ; break;
LinkedList<Arco<N» ad=grafo.get(ai.getOrigine());
ad.remove(ai);
}//remove }
}//lteratoreAdiacenti }//rimuoviNodo

public l(era(or<N> iterator(){ return new lteratoreGrafo(); }//iterator public void rimuoviArco( Arco<N> a ){
//rimuove entrambi gli archi <u,v> e <v,u>
public lterator<? extends Arco<N» adiacenti( N u ){ N u=a.getOrigine(), v=a.getDestinazione();
if( Igrafo.containsKey(u) ) throw new IHegalArgumentException(); if( grafo.containsKey(u) ){
return new IteratoreAdiacenti(u); LinkedList<Arco<N» ad=grafo.get(u);
}//adiacenti lterator<? extends Arco<N» adiacenti=ad.iterator();
while( adiacenti.hasNext() ){
public boolean esisteNodo( N u ) { return grafo.containsKey(u); }//esisteNodo Arco<N> ar=adiacenti.next();
if( ar.equals(a) ){
public int numNodi(){ return grafo.size(); }//numNodi adiacenti.removeQ; break;

public void insNodo( N u ){


if( esisteNodo(u) ) throw new RuntimeExceptionfNodo già' presente durante insNodo");
grafo.put(u,new LinkedList<Arco<N»()); if( grafo.containsKey(v) ){
}//insNodo Arco<N> duale=new Arco<N>( a.getDestinazione(), a.getOrigine() );
LinkedList<Arco<N» ad=grafo.get(v);
public void insArco( Arco<N> a ){ lterator<? extends Arco<N» adiacenti=
if( !grafo.containsKey(a.getOrigine()) Il ad.iterator();
!grafo.containsKey(a.getDestinazione()) ){ while( adiacenti.hasNext() ){
throw new RuntimeException("Nodo(i) non esistente(i) durante insArco"); Arco<N> ar=adiacenti.next();
} if( ar.equals(duale) ){
//aggiunge entrambi gli archi <u,v> e <v,u> adiacenti.remove(); break;
LinkedList<Arco<N» ad=grafo.get(a.getOrigine());
ad.remove(a); //per preservare l'unicità' dell'arco }
ad.add(a); }
if( !grafo.containsKey(a.getDestinazione()) ){ }//rimuoviArco
grafo.put( a.getDestinazione(), new LinkedList<Arco<N»() );
} public Grafo<N> factory(){ return new GrafoNonOrientatolmpl<N>();}//factory
ad=grafo.get(a.getDestinazione());
Arco<N> inverso=new Arco<N>(a.getDestinazione(),a.getOrigine()); public void clear(){ grafo.clear(); }//clear
ad.remove( inverso );
ad.add( inverso ); }//GrafoNonOrientatolmpl
}//insArco
L'interfaccia GrafoPesato<N>; ____________________________________
public void rimuoviNodo( N u ){ Estende Grafo<N> ed introduce in più alcuni metodi legati alla gestione di archi pesati.
grafo.remove(u);
Set<N> chiavi=grafo.keySet(); package poo.grafo;
lterator<N> it=chiavi.iterator(); import java.util.*;
while( it.hasNext() ){ public interface GrafoPesato<N> extends Grafo<N>{
N v=it.next(); public void insArco( ArcoPesato<N> ap );
lterator<? extends Arco<N» adiacenti=grafo.get(v).iterator(); public void insArcof Arco<N> a, Peso peso );
public void insArco( N u, N v, Peso peso );
384 385
Capitolo 21 Sviluppo di programmi ad oggetti

public void modArco( ArcoPesato<N> a, Peso peso ); //modifica il peso di un arco public abstract lterator<ArcoPesato<N» adiacente N u );
public lterator<ArcoPesato<N» adiacente N u );
public Peso peso( N u, N v ); public Peso peso( N u, N v ){
}//GrafoPesato lterator<ArcoPesato<N» iap=adiacenti(u);
while( iap.hasNext() ){
L’interfaccia GrafoNonOrientatoPesato<N>: ArcoPesato<N> ap=iap.next();
Estende GrafoNonOrientato<N> e GrafoPesato<N>. Non introduce nuovi metodi, if( ap.getOrigine().equals(u) &&ap.getDestinazione().equals(v) )return ap.getPeso();
}
package poo.grafo; return nuli;
public interface GrafoNonOrientatoPesato<N> extends GrafoNonOrientato<N>,GrafoPesato<N> { }//peso
}//GrafoNonOrientatoPesato
public abstract GrafoPesato<N> factory();
La classe GrafoNonOrientatoPesatoAstratto<N>:
Estende GrafoNonOrientatoAstratto<N> ed implementa GrafoNonOrientatoPesato<N>. public Grafo<N> copia(){
GrafoPesato<N> copia=factory();
package poo.grafo; for( N u: this ){copia.insNodo(u);}
import java.util.lterator; for( N u: this ){
public abstract class GrafoNonOrientatoPesatoAstratto<N> extends GrafoNonOrientatoAstratto<N> lterator<ArcoPesato<N» it=this.adiacenti(u);
implements GrafoPesato<N>{ while( it.hasNext() ){
ArcoPesato<N> ac=it.next();
public void insArco( Arco<N> a, Peso peso ){ copia.insArco( new ArcoPesato<N>(u, ac.getDestinazione(), new Peso(ac.getPeso() )) );
insArco( new ArcoPesato<N>( a.getOrigine(),a.getDestinazione(),peso));
}//insArco
return copia;
public void insArco( N u, N v, Peso peso ){insArco( new ArcoPesato<N>(u,v,peso));}//insArco }//copia

public void insArco( Arco<N> a ){ private boolean equals( GrafoNonOrientatoPesatoAstratfo<N> g 1,


insArco( new ArcoPesato<N>(a.getOrigine(),a.getDestinazione())); GrafoNonOrientatoPesatoAstratto<N> g2, N u ){
}//insArco lterator<ArcoPesato<N» it1=g1 .adiacenti(u);
while( it1.hasNext() ){
public void modArco( ArcoPesato<N> a, Peso peso ){ ArcoPesato<N> al = it1.next();
if( !esisteNodo(a.getOrigine() ) H!esisteNodo(a.getDestinazione()) ) return; if( !g2.esisteArco(a1) ) return false;
lterator<ArcoPesato<N» it=adiacenti(a.getOrigine()); lterator<ArcoPesato<N» it2=g2.adiacenti(u);
while( it.hasNext() ) { while( it2.hasNext() ){
ArcoPesato<N> ap=it.next(); ArcoPesato<N> a2=it2.next();
if( ap.equals(a) ){ if( a1.equals(a2) && !a1.getPeso().equals(a2.getPeso()) ) return false;
ap.setPeso(peso);
//modifica anche arco inverso }
ArcoPesato<N> ai=new ArcoPesato<N>( a.getDestinazione(), a.getOrigine(), a.getPeso() ); lterator<ArcoPesato<N» it2=g2.adiacenti(u);
it=adiacenti(ai.getOrigine()); while( it2.hasNext() ){
while( it.hasNext() ){ ArcoPesato<N> a2 = it2.next();
ap=it.next(); if( !g1.esisteArco(a2) ) return false;
if( ap.equals(ai) ){ iti =g1 adiacenti(u);
ap.setPeso(peso); return; while( itl.hasNextj) ){
ArcoPesato<N> a1=it1.next();
if( a1.equals(a2) && !a1.getPeso().equals(a2.getPeso()) ) return false;

}
insArco( new ArcoPesato<N>(a.getOrigine(),a.getDestinazione(),peso) ); return true;
}//modArco }//equals

386 387
Capitolo 21 Sviluppo di programmi ad oggetti

@SuppressWarnings("unchecked") }
public boolean equals( Object o ){ }
if( !(o instanceof GrafoNonOrientatoPesatoAstratto) ) return false; }//lteratoreGrafo
if( o==this ) return true;
GrafoNonOrientatoPesatoAstratto<N> g=(GrafoNonOrientatoPesatoAstratto<N>)o; private class IteratoreAdiacenti implements lterator<ArcoPesato<N»{
if( this.numNodi()!=g.numNodi() Il private lterator<ArcoPesato<N» it;
this.numArchi()!=g.numArchi() ) return false; private ArcoPesato<N> a=null;
for( N u: this ){ public lteratoreAdiacenti( N u ){
if( Ig.esisteNodo(u) ) return false; it=grafo.get(u).iterator();
if( !equals(this,g,u) ) return false; }
} public boolean hasNext(){
return true; return it.hasNext();
}//equals }//hasNext
public ArcoPesato<N> next(){
}//GrafoNonOrientatoPesatoAstratto if( !it.hasNext() )
throw new NoSuchElementException();
GrafoNonOrientatoPesatoAstratto ridefinisce il metodo factory(). Il metodo copia() ora ritorna in effetti un grafo a=it.next();
pesato. return a;
}//next
La classe GrafoNonOrientatoPesatolmpl<N>: ________________________ _____________________ _ public void remove(){
Estende GrafoNonOrientatoPesatoAstratto<N>. É una classe concreta. it.remove();
//rimuovi adesso l’arco inverso
package poo.grafo; ArcoPesato<N> ai=
import java.util.*; new ArcoPesato<N>( a.getDestinazione(),
a.getOrigine(),a.getPeso());
public class GrafoNonOrientatoPesatolmpl<N> extends GrafoNonOrientatoPesatoAstratto<N>{ LinkedList<ArcoPesato<N» ad=grafo.get(ai.getOrigine());
ad.remove(ai);
private Map<N,LinkedList<ArcoPesato<N»> grafo= }//remove
new HashMap<N,LinkedList<ArcoPesato<N»>(); }//lteratoreAdiacenti

private class IteratoreGrafo implements lterator<N>{ public lterator<N> iterator(){ return new lteratoreGrafo(); }//iterator
private lterator<N> it=grafo.keySet().iterator();
private N u=null; public lterator<ArcoPesato<N» adiacenti( N u ){
public boolean hasNext(){ return it.hasNext();} if( Igrafo.containsKey(u) ) throw new UlegalArgumentException();
public N next(){ return u=it.next();} return new IteratoreAdiacenti(u);
public void remove(){ }//adiacenti
it.remove(); //toglie il nodo corrente e le sue adiacenze
//occorre anche togliere tutti gli archi di cui il nodo corrente
//è destinazione public boolean esisteNodo( N u ){
Set<N> chiavi=grafo.keySet(); return grafo.containsKey(u);
lterator<N> it=chiavi.iterator(); }//esisteNodo
while( it.hasNext() ){
N v=it.next(); public int numNodi(){
lterator<? extends Arco<N» adiacenti= return grafo.size();
grafo.get(v).iterator(); }//numNodi
whilef adiacenti.hasNext() ) {
Arco<N> a=adiacenti.next(); public void insNodo( N u ){
if( a.getDestinazione().equals(u) ){ if( esisteNodo(u) )
adiacenti.remove(); break; throw new RuntimeExceptionf'Nodo già' presente durante insNodo");
} grafo.put( u, new LinkedList<ArcoPesato<N»() );
}//insNodo
388 389
Capitolo 21 Sviluppo di programmi ad oggetti

if( ar.equals(a) ){
public void insArco( ArcoPesato<N> ap ){ adiacenti.remove(); break;
if( !grafo.containsKey(ap.getOrigine()) Il
!grafo.containsKey(ap.getDestinazione()) ){
throw new RuntimeException("Nodo(i) non esistente(i) durante insArco");
} //rimuovi ora arco inverso
LinkedList<ArcoPesato<N» ad=grafo.get(ap.getOrigine()); ArcoPesato<N> ai=
ad.add(ap); new ArcoPesato<N>(a.getDestinazione(),a.getOrigine(),a.getPeso());
//inserisci ora arco inverso u=ai.getOrigine();
ArcoPesato<N> ai= if( grafo.containsKey(u) ){
new ArcoPesato<N>(ap.getDestinazione(),ap.getOrigine(),ap.getPeso()); LinkedList<ArcoPesato<N» ad=grafo.get(u);
if( !grafo.containsKey(ai.getOrigine()) ){ lterator<ArcoPesato<N» adiacenti=ad.iterator();
grafo.put( ai.getOrigine(), while( adiacenti.hasNext() ){
new LinkedList<ArcoPesato<N»() ) ; ArcoPesato<N> ar=adiacenti.next();
} if( ar.equals(ai) ){
ad=grafo.get(ai.getOrigine()); adiacenti.remove(); break;
ad.add(ai);
}//insArco

public void rimuoviNodo( N u ){ }//rimuoviArco


grafo.remove(u);
Set<N> chiavi=grafo.keySet(); public GratoPesato<N> tactory(){
lterator<N> it=chiavi.iteratoci; return new GrafoOrientatoPesatolmpl<N>();
while( it.hasNext() ){ }//factory
N v=it.next();
lterator<ArcoPesato<N» adiacenti= public void clear(){
grafo.get(v).iterator(); grafo.clear();
while( adiacenti.hasNext() ) { }//clear
ArcoPesato<N> a=adiacenti.next();
if( a.getDestinazione().equals(u) ){ }//GrafoNonOrientatoPesatolmpl
adiacenti.remove(); break;
L’interfaccia GrafoOrientato<N>:
Estende Grafo<N> ed aggiunge i metodi che ritornano rispettivamente il grado di entrata/uscita di un nodo,

}//rimuoviNodo package poo.grafo;


public interface GrafoOrientato<N> extends Grafo<N>{
@SuppressWarnings("unchecked") public int gradoEntrata( N u );
public void rimuoviArco( Arco<N> a ){ public int gradoUscita( N u );
if( !(a instanceof ArcoPesato) ) }//GrafoOrientato
throw new lllegalArgumentException();
rimuoviArco( (ArcoPesato<N>)a ); La classe astratta GrafoOrientatoAstratto<N>:_____________________________________________________
}//rimuoviArco Estende GrafoAstratto<N> ed implementa GrafoOrientato<N>. Concretizza i due metodi gradoEntrataf) e
gradoUscita().
public void rimuoviArco( ArcoPesato<N> a ){
N u=a.getOrigine(); package poo.grafo;
if( grafo.containsKey(u) ){ import java.util.*;
LinkedList<ArcoPesato<N» ad=grafo.get(u); public abstract class GrafoOrientatoAstratto<N> extends GrafoAstratto<N> implements GrafoOrientato<N>{
lterator<ArcoPesato<N» adiacenti= public int gradoEntrata( N u ){
ad.iterator(); int gE=0;
while( adiacenti.hasNext() ){ if( esisteNodo(u) ){
ArcoPesato<N> ar=adiacenti.next(); for( N v: this ){
390 391
C ap ito lo 21^ Sviluppo di programmi ad oggetti

lterator<? extends Arco<N» it=adiacenti(v); }


while( it.hasNext() ) { }
Arco<N> a=it.next(); }
if( a.getDestinazione().equals(u) ) { gE++; break;} }//lteratoreGrafo

public lterator<N> iterator(){


return new lteratoreGrafo();
return gE; }//iterator
}//gradoEntrata
public lterator<? extends Arco<N» adiacenti( N u ){
public int gradollscita( N u ){ if( Igrafo.containsKey(u) )
int gU=0; throw new IHegalArgumentException();
if( esisteNodo(u) ){ return grafo.get(u).iterator();
lterator<? extends Arco<N» it=adiacenti(u); }//adiacenti
while( it.hasNext() ) {it.next();gU++;}
} public boolean esisteNodo( N u ) { return grafo.containsKey(u); }//esisteNodo
return gl);
}//gradoUscita public int numNodi(){ return grafo.size(); }//numNodi

}//GrafoOrientatoAstratto public void insNodo( N u ){


if( esisteNodo(u) ) throw new RuntimeExceptionf'Nodo già' presente durante insNodo");
La classe GrafoOrientatolmpl<N>: grafo.put(u,new LinkedList<Arco<N»());
Estende GrafoOrientatoAstratto<N>. É una classe concreta. Memorizza il grafo come mappa li liste di }//insNodo
adiacenze.
public void insArco( Arco<N> a ){
package poo.grafo; if( !grafo.containsKey(a.getOrigine()) Il !grafo.containsKey(a.getDestinazione()) ){
import java.util.*; throw new RuntimeException("Nodo(i) non esistente(i) durante insArco");
public class GrafoOrientatolmpl<N> extends GrafoOrientatoAstratto<N>{ }
LinkedList<Arco<N» ad=grafo.get(a.getOrigine());
private Map<N,LinkedList<Arco<N»> grafo=new HashMap<N,LinkedList<Arco<N»>(); ad.remove(a);
ad.add(a);
private class IteratoreGrafo implements lterator<N>{ }//insArco
private lterator<N> it=grafo.keySet().iterator();
private N u=null; public void rimuoviNodo( N u ){
public boolean hasNext(){ return it.hasNext();} grafo, remove(u);
public N next(){ return u=it.next();} Set<N> chiavi=grafo.keySet();
public void remove(){ lterator<N> it=chiavi.iterator();
it.remove(); //toglie il nodo corrente e le sue adiacenze while( it.hasNext() ){
//occorre anche togliere tutti gli archi in cui il nodo corrente N v=it.next();
Uè destinazione lterator<? extends Arco<N» adiacenti=
Set<N> chiavi=grafo.keySet(); grafo.get(v).iterator();
lterator<N> it=chiavi.iterator(); while( adiacenti.hasNext() ) {
while( it.hasNext() ){ Arco<N> a=adiacenti.next();
N v=it.next(); if( a.getDestinazione().equals(u) ){
lterator<? extends Arco<N» adiacenti= adiacenti.remove(); break;
grafo.get(v).iterator();
while( adiacenti.hasNext() ) { }
Arco<N> a=adiacenti.next(); }
if( a.getDestinazione().equals(u) ){ }//rimuoviNodo
adiacenti.removeQ; break;
}
392 393
C ap ito lo 2M Sviluppo di programm[ad oggetti

public void rimuoviArco( Arco<N> a ){ while( it.hasNext() ) {


N u=a.getOrigine(); ArcoPesato<N> ap=it.next();
if( grafo.containsKey(u) ){ if( ap.equals(a) ){
LinkedList<Arco<N» ad=grafo.get(u); ap.setPeso(peso); return;
lterator<? extends Arco<N » adiacenti=
ad.iterator(); }
while( adiacenti.hasNext() ){ insArco( new ArcoPesato<N>(a.getOrigine(),a.getDestinazione(),peso) );
Arco<N> ar=adiacenti.next(); }//modArco
if( ar.equals(a) ){
adiacenti.remove(); break; public Peso peso( N u, N v ){
lterator<ArcoPesato<N» iap=adiacenti(u);
while( iap.hasNext() ){
} ArcoPesato<N> ap=iap.next();
}//rimuoviArco if( ap.getOrigine().equals(u) &&ap.getDestinazione().equals(v) )return ap.getPeso();
}
public Grafo<N> factory(){ return new GrafoOrientatolmpl<N>(); }//factory return nuli;
}//peso
public void clear(){ grafo.clear(); }//clear
public abstract GrafoPesato<N> factory();
}//GrafoOrientatolmpl
public Grafo<N> copia(){
L’interfaccia GrafoOrientatoPesato<N>:__________________________________________ GrafoPesato<N> copia=factory();
Estende GrafoOrientato<N> e GrafoPesato<N>. Non introduce nuovi metodi. for( N u: this ){copia.insNodo(u);}
for( N u: this ){
package poo.grafo; lterator<ArcoPesato<N» it=this.adiacenti(u);
public interface GrafoOrientatoPesato<N> extends GrafoOrientato<N>, GrafoPesato<N>{ while( it.hasNext() ){
}//GrafoOrientatoPesato ArcoPesato<N> ac=it.next();
copia.insArcof new ArcoPesato<N>(u, ac.getDestinazione(), new Peso(ac.getPeso() )) );
La classe astratta GrafoOrientatoPesatoAstratto<N>:_______________________________
Estende GrafoOrientatoAstratto<N> ed implementa GrafoOrientatoPesato<N>.
return copia;
package poo.grafo; }//copia
import java.util.lterator;
public abstract class GrafoOrientatoPesatoAstratto<N> extends GrafoOrientatoAstratto<N> private boolean equals( GrafoOrientatoPesatoAstratto<N> g l,
implements GrafoOrientatoPesato<N>{ GrafoOrientatoPesatoAstratto<N> g2, N u ){
lterator<ArcoPesato<N» iti =g1 .adiacenti(u);
public void insArco( Arco<N> a, Peso peso ){ while( it1.hasNext() ){
insArco( new ArcoPesato<N>( a.getOrigine(),a.getDestinazione(),peso)); ArcoPesato<N> al = it1.next();
}//insArco if( !g2.esisteArco(a1) ) return false;
lterator<ArcoPesato<N» it2=g2.adiacenti(u);
public void insArco( N u, N v, Peso peso ){ while( it2.hasNext() ){
insArco( new ArcoPesato<N>(u,v,peso)); ArcoPesato<N> a2=it2.next();
}//insArco if( a1.equals(a2) && !a1.getPeso().equals(a2.getPeso()) ) return false;

public void insArco( Arco<N> a ){


insArco( new ArcoPesato<N>(a.getOrigine(),a.getDestinazione()) ); lterator<ArcoPesato<N» it2=g2.adiacenti(u);
}//insArco while( it2.hasNext() ){
ArcoPesato<N> a2 = it2.next();
public void modArco( ArcoPesato<N> a, Peso peso ){ if( !g1.esisteArco(a2) ) return false;
if( !esisteNodo(a.getOrigine() ) ll!esisteNodo(a.getDestinazione()) ) return; iti =g1 .adiacenti(u);
lterator<ArcoPesato<N» it=adiacenti(a.getOrigine());
394 395
Capitolo 21 Sviluppo di programmi ad oggetti

while( it1.hasNext() ){ while( adiacenti.hasNext() ) {


ArcoPesato<N> a1=it1.next(); Arco<N> a=adiacenti.next();
if( a1.equals(a2) && !a1.getPeso().equals(a2.getPeso()) ) return false; if( a.getDestinazione().equals(u) ){
} adiacenti.remove(); break;

return true;
}//equals

@SuppressWamingsfunchecked") }//lteratoreGrafo
public boolean equals( Object o ){
if( !(o instanceof GrafoOrientatoPesatoAstratto) ) return false; public lterator<N> iterator(){
if( o==this ) return true; return new lteratoreGrafo();
GrafoOrientatoPesatoAstratto<N> g=(GrafoOrientatoPesatoAstratto<N>)o; }//iterator
if( this.numNodi()!=g.numNodi() Il
this.numArchi()!=g.numArchi() ) return false; public lterator<ArcoPesato<N» adiacenti( N u ){
for( N u: this ){ if( Igrafo.containsKey(u) ) throw new IHegalArgumentException();
if( !g.esisteNodo(u) ) return false; return grafo.get(u).iterator();
if( !equals(this,g,u) ) return false; }//adiacenti
}
return true; public boolean esisteNodo( N u ){
}//equals return grafo.containsKey(u);
}//esisteNodo
}//GrafoOrientatoPesatoAstratto
public int numNodi(){
Si nota che le classi GrafoNonOrientatoPesatoAstratto e GrafoOrientatoPesatoAstratto ridefiniscono il metodo return grafo.size();
equals() di GrafoAstratto in modo da considerare anche i pesi degli archi. }//numNodi

La classe GrafoOrientatoPesatolmpkNx public void insNodo( N u ){


Estende GrafoOrientatoPesatoAstratto<N>. È una classe concreta. if( esisteNodo(u) )
throw new RuntimeExceptionfNodo già' presente durante insNodo”);
package poo.grafo; qrafo.putf u, new LinkedList<ArcoPesato<N»() );
import java.util.*; }//insNodo
public class GrafoOrientatoPesatolmpl<N> extends GrafoOrientatoPesatoAstratto<N>{
public void insArco( Arco<N> a ){
private Map<N,LinkedList<ArcoPesato<N»> grafo=new HashMap<N,LinkedList<ArcoPesato<N»>(); insArco( new ArcoPesato<N>( a.getOrigine(), a.getDestinazioneQ ));
}//insArco
private class IteratoreGrafo implements lterator<N>{
private lterator<N> it=grafo.keySet().iterator();
private N u=null; public void insArco( Arco<N> a, Peso peso ){
public boolean hasNext(){ return it.hasNext();} insArco( new ArcoPesato<N>( a.getOrigine(), a.getDestinazioneQ, peso ));
public N next(){ return u=it.next();} }//insArco
public void remove(){
it.remove(); //toglie il nodo corrente e le sue adiacenze
//occorre anche togliere tutti gli archi in cui il nodo corrente public void insArco( ArcoPesato<N> ap ){
//è destinazione if( !grafo.containsKey(ap.getOrigine()) Il !grafo.containsKey(ap.getDestinazione()) ){
Set<N> chiavi=grafo.keySet(); throw new RuntimeException("Nodo(i) non esistente(i) durante insArco");
lterator<N> it=chiavi.iterator(); }
while( it.hasNext() ){ LinkedList<ArcoPesato<N» ad=grafo.get(ap.getOrigine());
N v=it.next(); ad.add(ap);
lterator<? extends Arco<N» adiacenti= }//insArco
grafo.get(v).iterator();
396 397
Capitolo 21 Sviluppo di programmi ad oggetti

public void rimuoviNodo( N u ){ package poo.grafo;


gralo.remove(u); import poo.util.*;
Set<N> chiavi=grafo.keySet(); public class Peso implements Comparable<Peso>{
lterator<N> it=chiavi.iterator(); public static final doublé INFINITO=Double.POSITIVEJNFINITY;;
while( it.hasNext() ){ private doublé vai;
N v=it.next(); public Peso(){ this(O); }
lterator<ArcoPesato<N» adiacenti= public Peso( doublé vai ){this.val=val;}
grafo.get(v).iterator(); public Peso( Peso p ){this.val=p.val;}
while( adiacenti.hasNext() ) {
ArcoPesato<N> a=adiacenti.next(); public doublé val(){ return vai;}
if( a.getDestinazione().equals(u) ){
adiacenti.remove(); break; public void setVal( doublé vai ) { this.val=val;}

public Peso piu( Peso p ){


Peso somma=new Peso();
}//rimuoviNodo if( this.val==INFINITO II p.val()==INFINITO )(
somma.setVal( INFINITO );
@SuppressWamings(”unchecked”) }
public void rimuoviArco( Arco<N> a ){ else
if( !(a instanceof ArcoPesato) ) throw new lllegalArgumentException(); somma.setVal( val+p.val() );
rimuoviArco( (ArcoPesato<N>)a ); return somma;
}//rimuoviArco }//piu

public void rimuoviArco( ArcoPesato<N> a ){ public String toString(){


N u=a.getOrigine(); String s="’ ;
if( grafo.containsKey(u) ){ if( val==INFINITO ) s+=“oo";
LinkedList<ArcoPesato<N» ad=grafo.get(u); else s+=val;
lterator<ArcoPesato<N» adiacenti= return s;
ad.iterator(); }//toString
while( adiacenti.hasNext() ){
ArcoPesato<N> ar=adiacenti.next(); public boolean equals( Object o ){
if( ar.equals(a) ){adiacenti.remove(); break;} if( !(o instanceof Peso) ) return false;
if( o==this ) return true;
Peso p=(Peso)o;
}//rimuoviArco return Mat.sufficientementeProssimi( this.val, p.val );
}//equals
public GrafoPesato<N> factory(){
return new GrafoOrientatoPesatolmpl<N>(); public int hashCode(){ return new Double(val).hashCode();}
}//factory
public int compareTo( Peso p ){
public void clear(){ grafo.clear(); }//clear if( this.val==p.val() ) return 0;
if( this.val!=INFINITO && p.val()==INFINITO ) return -1;
}//GrafoOrientatoPesatolmpl if( this.val==INFINITO && p.val()!=INFINITO ) return 1;
if( Mat.sufficientementeProssimi( this.val, p.val ) ) return 0;
La classe Peso:_____________________________________________________________________________ if( this.vakp.val ) return -1;
E un esempio di classe di oggetti mutabili utile per associare un peso agli archi di grafi pesati. Implementa return 1;
Comparable<Peso> ed incapsula un valore (peso) che per generalità è un doublé. Esporta la costante statica }//compareTo
INFINITO che coincide con Double.POSITIVEJNFINITY. Il costruttore di default inizializza il peso a zero. Il
metodo il confronto naturale int compareTo( Peso p ) tiene conto che uno dei due pesi può essere infinito. Il }//Peso
metodo Peso piu( Peso p ) restituisce il peso somma tra this e p.

398 399
Capitolo 21 Sviluppo di programm[ad oggetti

La classe Grafi del package poo.util:________________________________________________ _ while( it.hasNext() ){


Le classi presentate sono utili per elaborare grafi. A titolo di esempio si riporta una classe di utilità Grafi, posta N nodoAdiacente=it.next().getDestinazione();
nel package poo.util, che contiene alcuni algoritmi ricorrenti sui grafi, es. quelli relativi alle operazioni di visita, if( Ivisitato.contains(nodoAdiacente) )
alla raggiungibilità etc. Altri metodi di servizio più specifici e/o più efficienti possono essere aggiunti. Rispetto visitalnProfondita(g,nodoAdiacente,visitato,lista);
agli algoritmi discussi nel cap. 19, si propongono due ulteriori metodi: il calcolo del numero delle componenti }
connesse e la verifica di aciclicità di un grafo orientato. }//visitalnProfondita

package poo.util; public static <N> Grafo<N> raggiungibilita( Grafo<N> g ){


import java.util.*; Grafo<N> grafoR=g.copia();
import poo. grafo.*; //genera tutti i percorsi di lunghezza k;
//k=1 indica i percorsi unitari, cioè' gli archi,
public final class Grafi{ //già' presenti in grafoCompleto per costruzione
private Grafi(){} for( int k=2; k<g.numNodi(); k++ ){
for( N i: g )
public static <N> void visitalnAmpiezza( Grafo<N> g, N u, LinkedList<N> lista ){ for( N j: g )
if( g==null II !g.esisteNodo(u) ) throw new HlegalArgumentException(); for( N m: g )
Set<N> visitato=new HashSet<N>(); if( grafoR.esisteArco(i.m) && g.esisteArco(m,j) )
visitalnAmpiezza(g,u,visitato,lista); grafoR.insArco(i.j);
}//visitalnAmpiezza
return grafoR;
private static <N> void visitalnAmpiezza( Grafo<N> g, N u, Set<N> visitato, LinkedList<N> lista ){ }//raggiungibilita
//breADTh-first visit
LinkedList<N> coda=new LinkedList<N>(); //coda di nodi pending già' visitati public static <N> int componentiConnesse( GrafoNonOrientato<N> g ){
coda.addLast(u); //Si basa sulla visita in ampiezza. Per ogni nodo u del grafo g, si
lista.addLast(u); //"visita" u //fa partire la visita in ampiezza da u. Tutti i nodi raggiunti sono marcati visitati.
visitato.add(u); //"marca" u come visitato //Ad ogni nuova invocazione della visita in ampiezza, si incrementa il contatore
while( !coda.isEmpty() ){ //delle componenti connesse. L'algoritmo termina quando non esistono più nodi
N x=coda.removeFirst(); //non visitati. Alla fine il contatore vale 1 se il grafo è connesso; >1 se esistono
lterator<? extends Arco<N» it=g.adiacenti(x); //"isole” di connessione.
while( it.hasNext() ){ Set<N> visitato=new HashSet<N>();
N nodoAdiacente=it.next().getDestinazione(); LinkedList<N> fittizia=new LinkedList<N>();
if( Ivisitato.contains(nodoAdiacente) ){ int ncc=0;
coda.addLast( nodoAdiacente ); for( N u: g ){
lista.addLast( nodoAdiacente ); if( Ivisitato.contains(u) ){
visitato.add(nodoAdiacente); ncc++;
visitalnAmpiezza( g, u, visitato, fittizia );

}//visitalnAmpiezza return ncc;


}//componentiConnesse
public static <N> void visitalnProfondita( Grafo<N> g, N u, LinkedList<N> lista ){
if( g==null II Ig.esisteNodo(u) ) throw new HlegalArgumentException(); public static <N> boolean aciclico( GrafoOrientato<N> g ){
Set<N> visitato=new HashSet<N>(); //Verifica se il grafo e' riducibile. Se si, esso è aciciclo.
visitalnProfondita(g,u,visitato,lista); //Si parte dai nodi con grado di entrata 0 e si mettono in una coda dei pending.
}//visitalnProfondita //Ripetutamente, si estrae un nodo dalla coda. Lo si "elimina" logicamente,
//quindi si eliminano gli archi ai relativi nodi adiacenti e nel contempo
private static <N> void visitalnProfondita( Grafo<N> g, N u, Set<N> visitato, LinkedList<N> lista ){ //si decrementa il grado di entrata diogni nodo adiacente. Ogni nuovo nodo con grado di entrata 0
//depth-first visit //viene inserito nella coda. Si ripete sino a che la coda dei pending
lista.addLast(u); //si svuota. A fine algoritmo, se tutti i nodi del grafo sono stati rimossi
visitato.add(u); //il grafo è aciciclo, altrimenti è ciclico.
lterator<? extends Arco<N» it=g.adiacenti(u); Set<N> rimossi=new HashSet<N>(); //collezione dei nodi "rimossi"
400 401
Capitolo 21 Sviluppo di programmi ad oggetti

Map<N,lnteger> gradoEntrata=new HashMap<N,lnleger>(); visitalnProfondita( g, 1, lis );


LinkedList<N> daRimuovere=new LinkedList<N>(); //coda “pending" System.out.printlnfVisita in profondità': "+lis);
for( N u: g ){ System.out.printlnQ;
int gE=g.gradoEntrata(u); System.out.printlnfGrafo aciclico ? "+aciclico(g));
gradoEntrata.put( u, gE ); Grafo<lnteger> gR=raggiungibilita(g);
if( gE==0 ) daRimuovere.addLast(u); System.out.println('Grafo di raggiungibilita'");
} System.out.println(gR);
//la coda daRimuovere contiene nodi con grado di entrata 0 System.out.println("Rimuovo l'arco <2,4>“);
while( IdaRimuovere.isEmptyO ){ g.rimuoviArco(2,4);
N nodo=daRimuovere.removeFirst(); System.out.println(g);
rimossi.add(nodo); //rimozione logica di nodo gR.clear();
lterator<? extends Arco<N» it=g.adiacenti(nodo); gR=raggiungibilita(g);
//si decrementa il grado di entrata di ogni nodo adiacente ad n System.out.printlnfNuovo grafo di raggiungibilita");
while( it.hasNextQ ){ System.out.println(gR);
N destinazione=it.next().getDestinazione();
gradoEntrata.put(destinazione,gradoEntrata.get(destinazione)-1); System.out.println();
if( gradoEntrata,get(destinazione)==0 ) g.insArco(2,4); //ricomposizione grafo iniziale
daRimuovere.addLast( destinazione ); System.out.println(g);
System.out.println('Test rimuoviNodo 5");
g.rimuoviNodo(5);
//si ritorna true se rimossi contiene tutti i nodi System.out.println(g);
for( N u: g ) System.out.printlnfGrafo ricomposto");
if( !rimossi.contains(u) ) return false; g.insNodo(5); g.insArco(5,2); g.insArco(5,3);
return true; g.insArco(4,5); g.insArco(3,5);
}//aciclico System.out.println(g);
System.out.printlnfTest rimozione nodo 5 con iteratore");
lterator<lnteger> it=g.iterator();
while( it.hasNext() ){
public static void main( Stringi] args ){//Demo int x=it.next();
GrafoOrientato<lnteger> g=new GrafoOrientatolmpklnteger>(); if(x==5){
//GrafoNonOrientato<lnteger> g=new Gra<oNonOrientatolmpklnteger>(); it.remove();
//nel caso di grafo non orientato, commentare la verifica di aciclico break;
g.insNodo(1);g.insNodo(2);g.insNodo(3); g.insNodo(4);g.insNodo(5); }
g.insNodo(6);g.inslslodo(7);.insArco(1,2); g.insArco(1,3);g.insArco(2,4); }
g.insArco(3,5);g.insArco(4,5);g.insArco(5,2);g.insArco(5,3);g.insArco(4,6); System.out.println(“Grafo dopo la rimozione del nodo 5“);
g.insArco(7,6); System.out.println(g);
}//main
GrafoOrientato<lnteger> ge=new GrafoOrientatolmpklnteger>();
ge.insNodo(5);ge.insNodo(6);ge.insNodo(7);ge.insNodo(1);ge.insNodo(2); }//Grafi
ge.insNodo(3);ge.insNodo(4);ge.insArco(1,3);ge.insArco(1,2);ge.insArco(4,6);
ge.insArco(2,4);ge.insArco(3,5);ge.insArco(4,5);ge.insArco(5,3);ge.insArco(5,2); Esercizi ___________ ______ ___
ge.insArco(7,6); T. È assegnato un file di tipo testo f, il cui nome esterno va letto da tastiera. Si deve sviluppare un'applicazione
che determina, per ogni parola (una parola è una sequenza alfanumerica di caratteri) p che compare almeno
System.out.println("g==ge ? "+ g.equals(ge)); una volta nel testo, le frequenze f(p) e f(p,q). f(p) esprime il numero di volte che p compare nel file, diviso il
System.out.println("g.hashCode()="+g.hashCode()+" ge.hashCode()="+ge.hashCode()); numero totale di parole che compaiono nel testo. f(p,q) esprime il numero di volte che la parola p è seguita
dalla parola q nel testo, diviso il numero totale di volte che p compare nel testo. Il programma deve quindi
System.out.println(g); calcolare, data una particolare parola campione pc, la parola q che “più verosimilmente" segue la parola pc nel
LinkedList<lnteger> lis=new LinkedList<lnteger>(); testo (“più verosimilmente” significa che è massimo il valore di f(pc,g)) e quella q che "meno verosimilmente"
visitalnAmpiezza( g, 1, lis ); segue p nel testo (minimo f(pc,g)). Si suggerisce di basare lo sviluppo del programma utilizzando un grafo
System.out.println(“Visita in ampiezza: ’ +lis); orientato e pesato su cui raccogliere le informazioni di occorrenza delle parole. I nodi sono associati alle
lis.clear(); parole distinte del testo. Ogni nodo parola si accompagna al contatore delle sue occorrenze. I nodi adiacenti
402 403
Capitolo 21 Sviluppo di programmi ad oggetti

ad un nodo p, sono le parole che immediatamente seguono p nel testo. I pesi degli archi <p,q> sono i valori isita in ampiezza/profondità del grafo a partire da un nodo selezionato
f (P.Q)- enerazione di un ordinamento topologico (a questo proposito si potrebbe seguire lo schema grafico di cui
opra o semplicemente fornire la sequenza dei nodi che definiscono l’ordinamento).
2. (Problema dei 4 colori) Scrivere un metodo di servizio della classe poo.util.Grafi che riceva un grafo
(orientato o non) e provveda a “colorarlo" utilizzando al massimo 4 colori (rosso, verde, giallo, blu) in modo i della generazione dell’ordinamento topologico, considerare l’algoritmo di riduzione utilizzato nel metodo
che mai due nodi adiacenti ricevano lo stesso colore. È stato dimostrato che per grafi “planari”, ossia tracciabili erifica l’esistenza di cicli nella classe poo.util.Grafi.
su un piano in modo che mai gli archi si intersecano se non nei vertici, 4 colori sono sufficienti. Il metodo deve
visualizzare su output tutte le possibili soluzioni. Si suggerisce di utilizzare la tecnica backtracking.

Progetto
E assegnato un insieme finito di S di oggetti sui quali è definita una relazione d’ordine parziale < (precede),
cioè un ordinamento che vige tra alcune coppie di oggetti di S ma non su tutte. La relazione d’ordine può
essere espressa mediante un grafo orientato come quello che segue:

La relazione precede soddisfa le seguenti proprietà, comunque si considerino tre elementi distinti di S:
1. x<y and y<z implica x<z (transitività)
2. x<y non implica y<x (asimmetria)
3. x not< x (non riflessività)

Il problema dell'ordinamento topologico consiste nell'ottenere un ordinamento lineare degli elementi, ossia una
distribuzione dei vertici del grafo su una riga in modo che considerato un elemento x di S, tutti i suoi
predecessori lo precedono sulla riga. In altri termini, nell’ordinamento lineare cercato, gli archi orientati
“puntano sempre a destra". Nel caso dell’esempio, un possibile ordinamento lineare è il seguente:

Si deve progettare e realizzare un’applicazione Java dotata di GUI che consenta di inserire nodi e archi
orientati di un grafo e permetta poi l’effettuazione di (almeno) le seguenti operazioni:
• caricamento/salvataggio di un grafo
• verifica di esistenza o meno di cicli in un grafo
404 405
Capitolo 22:__________
Concetti di unit testing
Il progetto di un programma ad oggetti, in Java, C++ o qualunque altro linguaggio, non può ritenersi concluso
senza una verifica della sua correttezza. Malgrado la disponibilità di strumenti verificatori, non è possibile
dimostrare formalmente la correttezza di sistemi software di dimensioni medio grandi (si pensi ad un
programma costituito da milioni di istruzioni). La strada percorribile rimane quella del testing, ossia compiere
sul programma una serie di esperimenti di esecuzione, variando i dati di input, al fine di “accertare" il suo
“buon” funzionamento nei vari casi possibili. Il testing costituisce comunque una tecnica limitata. Si dice che “il
testing può solo provare la presenza di errori ma non può predicare sulla loro assenza”. Ovviamente, la
correttezza di un intero programma è il risultato della correttezza dei singoli moduli che lo compongono.
Ingegneristicamente è opportuno occuparsi, il più presto possibile, della verifica di correttezza delle singole
classi e non attendere la loro intergrazione a formare un sotto sistema allorquando molteplici possono essere
le cause di malfunzionamento.

Il punto di vista di questo testo è che la correttezza di un programma deriva innanzi tutto dal suo progetto,
ossia dalla sua decomposizione in classi. Testando accuratamente le singole classi, magari servendosi di
classi stub (o fittizie) per quanto attiene a quelle classi utilizzate ma non ancora sviluppate, è possibile
incrementalmente “assicurare" la correttezza dell’intero progetto.

La correttezza di una classe dipende poi dalla correttezza dei suoi metodi, i quali vanno accuratamente
collaudati. Un metodo riceve dei parametri e fornisce un risultato. Occorre testare che, attraverso una
opportuna scelta dei casi di tesi (combinazioni di valori dei parametri), il comportamento del metodo resta
sempre prevedibile, sia nel caso di comportamento positivo (il metodo si conclude fornendo il risultato atteso)
sia nel caso di comportamento negativo (il metodo si conclude sollevando un’eccezione prevista). La scelta
dei casi di test può non essere semplice. Ogni metodo, infatti, introduce di norma nel suo corpo istruzioni di
selezione (if, switch), ripetizione (loop) eventualmente innestati, etc. che sfidano le operazioni di testing. I casi
di scelta dovrebbero essere selezionati in modo da “garantire" che tutte le vie del controllo vengono
interessate (copertura) durante il testing ed il comportamento resta quello atteso.

Mentre per un approfondimento di questi concetti si rimanda ai corsi di Ingegneria del Software, qui si nota
che il progetto di una classe dovrebbe acconpagnarsi ad un pacchetto di metodi “affiatati” o “coesi". Solo i
metodi strettamente richiesti dal tipo astratto della classe dovrebbero essere presenti. Inoltre il progetto della
classe dovrebbe garantire il soddisfacimento del suo invariante, ossia una proprietà intrinseca della classe che
dovrebbe essere vera dopo l’esecuzione dei costruttori, e subito prima e subito dopo l’esecuzione di ogni
metodo pubblico. Ad es. la classe Razionale di cui al cap. 3 ha come invariante il fatto che numeratore e
denominatore risultano sempre primi fra loro. Similmente, in un conto bancario, la somma delle operazioni di
deposito meno la somma delle operazioni di prelevamento deve essere sempre uguale al bilancio del conto,
etc. Inoltre ogni metodo dovrebbe essere progettato secondo una logica "contrattuale”.

Il Client che invoca il metodo dovrebbe garantire il soddisfacimento della precondizione (cosa deve essere
vero prima di invocare il metodo, ad es. prima di chiamare un metodo come la radice quadrata sqrt(x), occorre
assicurare che il parametro x sia non negativo). Il progettista della classe, posto che la precondizione sia
soddisfatta, dovrebbe invece garantire che il metodo si conclude soddisfacendo la sua postcondizione (cosa
deve essere vero alla fine dell’esecuzione del metodo, ad es. circa l’accuratezza del risultato prodotto). Se un
metodo è invocato con la sua precondizione falsa, allora la sua esecuzione può concludersi in un uno dei
seguenti modi: entrando in loop, restituendo un risultato insensato, sollevando un’eccezione etc. La logica
contrattuale dunque distribuisce responsabilità e benefici ai diversi partner in gioco (Client e progettista della
classe).

407
Capitolo 22 Concetti di unit testing

L'istruzione assert switch( i ){


A partire dalla versione 4, Java mette a disposizione un'istruzione assert utilizzabile in uno dei seguenti modi: case 0:...; break;
case 1:...; break;
assert espressione-boolearr, case 2:...; break;
assert espressione-boolean : espressione; case 3:...; break;
default:
La prima forma stabilisce che una certa espressione booleana, supposta senza effetti collaterali (side effects), assert false : i;
dovrebbe essere vera in un punto fissato del programma. Se ciò non è, si solleva un AssertionError che }
tipicamente arresta l’esecuzione del programma. La seconda forma è simile alla prima, solo che per maggior*
informazione, all'AssertionError viene passato il risultato della espressione che segue i Qui l’assert potrebbe aiutare a scoprire il perché la variabile i prende un valore diverso da quelli attesi.

Le istruzioni assert possono essere distribuite in un programma in modo intuitivo, ad es. possonc Caso di studio
rappresentare un valido sostituto delle ben note istruzioni System.out.println( msg ) che il programmatore puc La classe che segue implementa uno stack di interi di dimensione limitata, non scalabile dinamicamente.
aver bisogno di inserire ma poi commentare per non essere “sopraffatto" dalle numerose stampe durante une
fase di testing. Le istruzioni assert hanno il vantaggio che possono essere abilitate e disabilitate a riga d public class Stackf
comando, con le opzioni -ea (enable assertion) o -da (disable assertion) (in Eclipse basta cliccare su Rur protected int []stack;
As->Run Configurations... e specificare le opzioni come argomenti per la Virtual machine (VM)). In queste protected int cima=0, n;
modo, pur essendo presenti (il programmatore può lasciarle nel programma anche nella sua veste finale) I* public Stack( int n ) { this.n=n; stack=new intjn]; }
assert possono essere completamente ininfluenti (non vengono valutate e dunque non costano se public boolean empty(){ return cima==0;}
disabilitate). public boolean full(){ return cima==n;}
public int top(){ return stack[cima-1];}
Le istruzione assert potrebbero essere introdotte in un programma a oggetti secondo la logica contrattuale public void push( int x ){ stack[cima]=x; cima-n-;}
richiamata di sopra. Il cliente potrebbe prevedere una assert prima di invocare un metodo. Il progettista delle public int pop(){ int x=stack[cima-1]; cima-; return x ;}
classe potrebbe inserire delle assert alla fine di un metodo pubblico per la verifica della postcondizione de }//Stack
metodo e per la verifica dell'Invariante di classe.
Da una semplice analisi testuale (code inspection), risulta che la classe è scritta in modo corretto. Inoltre, un
Esempi:___________________________________________________________________________________ uso “accorto" della struttura dati può evitare ogni insorgere di eccezioni. Tuttavia, sono possibili banalmente
Si consideri il seguente frammento di codice: situazioni di errore quando al tempo di una pop() o top() lo stack è vuoto, o quando al tempo di una push() lo
stack è pieno.
if( i%3==0 ){...}
else if( i%3==1 ){...} Per scopi dimostrativi, si propone ora una "decorazione" della classe Stack inspirata alla logica contrattuale sul
else{ //sicuramente i%3==2... problema della correttezza.

public class Stack{

Nella seconda else, si potrebbe aver commesso l’errore di “assumere troppo". Per aiutare in fase di testing class Old{
l'individuazione di queste situazioni errate è conveniente scrivere: int [jstack;
int cima;
if( i%3==0 ){...} public Old( int []stack, int cima ){
else if( i%3==1 ){...} this.stack=new int[stack.length);
else{ System.arraycopy(stack, 0, this.stack, 0, stack.length);
assert i%3==2 : i; this.cima=cima;

}//Old

Quale altro esempio, si considera un’istruzione switch su un intero i che dovrebbe assumere "sicuramente” un protected Old old=null;
valore tra 0 e 3: protected int [jstack;
protected int cima=0, n;

408 409
Capitolo 22 Concetti di unii tetttng

public Stack( ini n ) { Si nota che per poter procedere ad una corretta valutazione delle postcondizioni dei metodi, è stata introdotta
try{ una inner class denominata Old che all’ingresso di ogni metodo mutatore viene istanziata con una copia dello
stack=new int[n]; this.n=n; stato dell’oggetto Stack. La generazione della copia è vincolata all’abilitazione delle asserzioni mediante
}ca(ch(Exception e){ throw new NegativeCapacityException();} un'istruzione del tipo:
asseti INV():cima;
}//costruttore assert ( old=new Old( stack, cima ) ) != nuli;

public boolean empty(){ return cima==0;} Si nota che una tale istruzione fallisce solo in presenza di un fallimento della new Old(...) (improbabile). La
public boolean full(){ return cima==n;} postcondizione di pushQ, ad esempio, verifica che l’indice cima (che punta sempre al primo slot libero
dell’array) sia incrementato di 1 rispetto al valore che aveva in ingresso al metodo, e che in posizione cima-1
public int top() { si trovi esattamente l’elemento inserito. Similmente per pop(). I metodi push(), pop() e top() sollevano
asserì ( old=new Old( stack, cima ) ) != nuli; un'eccezione rispettivamente di tipo StackFullException e StackEmptyException (supposte eredi di
int x=-1; //fittizio RuntimeException) quando invocati con la precondizione falsa.
try{
x=stack[cima-1]; L'invariante della classe stack è stato catturato in un metodo protected (esportato solo agli eventuali eredi) che
}catch( Exception e ) { throw new StackEmptyExceptionQ;} verifica sempre che cima sia compreso tra 0 ed n (capacità dello stack), e che quando cima==0 e cima==n
assert Arrays.equals(stack, old.stack) && cima==old.cima && x==stack[cima-1] : "top inconsistente"; rispettivamente lo stack è effettivamente vuoto e pieno. L’invariante è verificato la prima volta alla fine del
asserì INV():cima; costruttore. Il costruttore può sollevare l'eccezione unchecked NegativeCapacityException.
return x;
}//top Una classe decorata come Stack si presta agevolmente ad essere testata rispetto ai casi positivi e negativi. Si
nota tuttavia che gran parte del software ad oggetti sviluppato attualmente non segue di fatto la logica
public void push( int x ) { contrattuale. Al più un “saggio" sviluppatore tiene conto della logica contrattuale nella definizione dei commenti
assert ( old=new Old( stack, cima ) ) != nuli; speciali a corredo della documentazione delle classi che javadoc è in grado di trasformare in codice HTML.
try{
stack[cima]=x; cima++; Test d| unità e J U n i t ___ _____ ______________________________
}catch( Exception e ) { throw new StackFullException();} Il test di unità rimane una questione fondamentale di sviluppo del software, indipendentemente se legato alla
assert (cima==old.cima+1) && (stack[old.cima]==x) : "push inconsistente"; logica contrattuale o meno. Per attuarlo, si può ricorrere vantaggiosamente a framework come XUnit,
assert INV():cima; disponibili in vari linguaggi (Java, C++, Smalltalk, etc.). In Java, JUnit è in grado di sfruttare la riflessione
}//push computazionale (introspezione) per rendere il suo utilizzo più intuitivo da parte dell’utente. JUnit è piuttosto
semplice e risulta spesso usato. Un vantaggio legato a JUnit è che si possono progettare i casi di test da
public int pop() { utilizzare con una assegnata classe e riutilizzarli sistematicamente (test di regressione) per verificare se una
assert ( old=new Old( stack, cima ) ) != nuli; modifica non abbia intaccato le funzionalità della classe. L’uso di JUnit si caratterizza per l’assenza nel codice
int x=-1; //fittizio di un progetto di istruzioni esplicite di testing e per la sua “virtù” di evitare l’utilizzo di debugger.
try{
x=stack[cima-1]; cima--; JUnit 4.x __________________ ___________ _____________
}catch( Exception e ) { throw new StackEmptyException();} Può essere scaricato liberamente dal sito www.junit.org e può essere usato a riga di comando o (meglio)
assert cima==old.cima-1 && x==old.stack[old.cima-1] : "pop inconsistente"; come plug-in di Eclipse. Le versioni recenti richiedono Java 5 o superiore, e sfruttano le annotazioni
assert INV():cima; (annunciate dai tag del tipo @tag, il cui significato prescinde dai commenti di javadoc), l'importazione statica di
return x; metodi etc.ll risultato è una maggior compattezza e semplicità delle classi di test, rispetto alla versione 3.8.1.
}//pop
Di seguito si mostra una classe StackTest, disegnata per funzionare con JUnit 4.X. Dopo aver riportato il
codice, si forniscono brevemente dei commenti sugli aspetti più importanti concernenti la formulazione dei
protected boolean INV(){ test. Si rimanda alla documentazione di JUnit per maggiori dettagli.
if( !(cima>=0 && cima<=n) ) return false;
if( cima==0 && !empty() ) return false; package poo. stack;
jf( cima==n && !full() ) return false; import org.junit.Test;
return true; import org.junit.Before;
J//INV import org.junit.After;
}//Stack import org.junit.BeforeClass;
import org.junit.AfterClass;
import static org.junit.Assert.*;
410 411
Capitolo 22 Concetti di unittesting

import static org.hamcrest.CoreMatchers.*; ©Test (expected=StackFullException.class)


import junit.framework.*; public void fullNegativo(){
s.push(10);
public class StackTest{ s.push(20);
private Stack s; s.push(30);
}//fullNegativo
©BeforeClass public static void runOnceBeforeAIITests(){
System. orv/.println(''@BeforeClass"); //demo }//StackTest
}//runOnceBeforeAIITests
1 meccanismi utilizzati vanno importati da diversi package come mostrato. Un metodo di test è annuciato dal
@AfterClass public static void runOnceAfterAIITests(){ tag ©Test. I metodi con le annotazioni ©Before e ©After specificano le azioni che vanno eseguite
System.ouf.printlnf ©AfterClass"); //demo rispettivamente prima di eseguire un qualsiasi metodo di test e subito dopo. Eventualmente, piu metodi
}//runOnceAfterAIITests ©Before o ©After possono essere introdotti. I metodi static con tag ©BeforeClass e ©AfterClass, se
presenti, specificano azioni da eseguire, una sola volta, rispettivamente prima di tutti i test, e dopo la fine di
@Before public void runBeforeEachTest(){ tutti i test. All'interno di questi metodi potrebbero esserci azioni globali quali ad es. l’apertura di una
System.ou/.printlnf© Before''); //demo connessione di rete con un server e poi la sua chiusura, che interessano appunto tutto l’insieme dei metodi di
s=new Stack(2); test.
}//runBeforeEachTest
Nella classe StackTest, prima di eseguire un qualunque metodo di test si crea un oggetto stack di dimensione
@After public void runAfterEachTest(){ 2 ed ovviamente vuoto. L'oggetto è poi ‘'dimenticato” alla fine del metodo di test.
System.ouf.printlnf© After");//demo
s=null; I test negativi che dovrebbero concludersi con il lancio di una eccezione, possono essere etichettati, subito
}//run AfterEachTest dopo il tag ©Test con la clausola: (expected=nome_classe eccezione). Un caso particolare di annotazione è
quello ©Test (timeout=10) che consente di specificare un metodo di test che fallisce se non termina entro 10
©Test (expected=NegativeCapacityException.class) millisecondi (es. per loop infinito o per un’operazione troppo lenta).
public void constructor(){//esempio di test negativo
new Stack(-1); II metodo assertThat() di JUnit consente di verificare che una variabile possegga un valore specificato, es:
}//constructor
assertThat( x,is(3) );
©Test public void empty(){ assertThat( lista, hasltem(12) );
assertThat{ s.empty(), is(true) ); assertThat( stnngaRisposta, either(containsString("Y'')).or(containsString(“y“)) );
}//empty
Poiché la classe di test è preparata separatamente dalla classe applicativa da testare, è possibile ri-eseguire i
© Test (expected=StackEmptyException.class) test in presenza di qualche cambiamento nei metodi della classe applicativa (es. per ragioni di efficienza) al
public void popNegativo(){ fine di verificare il buon comportamento della nuova implementazione.
s.pop();
}//popNegativo Esercizi____ __________ ____ ____
1. Verificare la correttezza della classe ArrayVector<T> riportata nel capitolo 8, una volta con le istruzioni
@Test public void popPositivo(){ assert e la logica contrattuale, una seconda volta con JUnit.
s.push( 10); 2. Verificare la correttezza della classe BufferLimitato<T> proposta nel capitolo 15, separatamente con la
assertThat( s.pop(), is( 10) ); logica contrattuale e utilizzando JUnit.
assertThat{ s.empty(), is(not(false)) );
}//testPopPositivo Altre letture ______
I concetti di sviluppo di sistemi software ad oggetti secondo la logica contrattuale possono essere approfonditi
©Test public void fullPositivo(){
sul seguente testo di riferimento, basato sul linguaggio Eiffel:
s.push(10);
s.push(20);
B. Meyer, Object oriented software construction, 2nd Edition, Prentice Hall, 2000.
assertThat[ s.full(), /'s(true) );
}//fullPositivo

412 413
Capitolo 22

Il sistema delle annotazioni può essere esplorato su: Capitolo 23:___________ _____________ _______
C.S. Horstmann, G. Cornei, Core Java, Voi. Il-Advanced Features, 8^ Edition, Prentice Hall, 2008. Introduzione alla programmazione multi-thread

J. Bloch, Effective Java, 2^ Edition, Addison-Wesly, 2008. I moderni sistemi operativi come Microsoft Windows, Linux etc. supportano il multi-tasking (task è sinonimo di
applicazione o processo, ossia un programma in esecuzione). Ad un certo istante di tempo più task possono
Maggior dettagli su JUnit si possono trovare sul sito http://w w w .iunit.org/. risiedere contemporaneamente in memoria ed eseguire in concorrenza sulla cpu (processore). Se il sistema
dispone effettivamente di un singolo processore, allora i vari task occupano a turno la cpu per un certo quanto
di tempo (time slice), trascorso il quale il task è tolto dalla cpu, il suo stato (contesto) è salvato in memoria, un
nuovo task, se esiste, è scelto, il suo stato caricato sulla cpu e la sua esecuzione ripresa esattamente
dall'ultimo punto dove era stata interrotta precedentemente (o dall’inizio se il task non ha eseguito in
precedenza).

Si capisce che se il time slice è sufficientemente piccolo (all’atto pratico qualche decina di millisecondi), questo
schema di operazioni determina l’esecuzione (quasi) contemporanea dei vari task. In realtà, la concorrenza è
percepibile dal punto di vista umano, dal momento che in un secondo più processi hanno eseguito il (o parte
del) loro compito elaborativo. A basso livello, ciò che accade è l’avvicendamento continuo (interleaving) dei
task sulla cpu, che eseguono “un pò l’uno, un pò l’altro".

Le moderne architetture multi-core, che realizzano più cpu all'Interno dello stesso processore, danno la
possibilità a più task di eseguire in parallelo (concorrenza fisica) se occupano core distinti. Tuttavia, siccome il
numero dei core di norma è più piccolo del numero dei task, un avvicendamento comunque si verifica nei
confronti di ogni singolo core.

I task o processi rappresentano le unità di assegnazione delle risorse da parte del sistema operativo. Ad ogni
task si fornisce uno spazio di memoria, una tabella di file, la possibilità di eseguire su una cpu etc.

Tuttavia i task sono noti come "processi pesantf-. ad ogni avvicendamento (context-switch) su una cpu di un
task con un altro, può essere sensibile il numero di operazioni di gestione da compiere: salvataggio dello stato
del processo interrotto, ripristino dello stato del nuovo processo prescelto per l’esecuzione.

Al giorno d’oggi i meccanismi della concorrenza, oltre che essere utilizzabili al livello del sistema operativo,
risultano sempre più spesso disponibili anche all’intemo di un linguaggio di programmazione ad alto livello
come Java. Diventa cosi possibile strutturare un'applicazione in veste multi-thread. Il termine thread indica
un'unità di programma concorrente che vive all’interno di un processo e condivide, con gli altri thread del
processo, le risorse assegnate complessivamente all’applicazione. Un context-switch tra thread è
un’operazione più leggera del context-switch tra processi, in quanto può limitarsi a salvare solo lo stato (stack)
di esecuzione del thread e non tutte le informazioni del processo. Per queste ragioni, i thread sono spesso
detti “processi leggerT e sono disponibili anche al livello di sistema operativo.

I core di un sistema multi-core possono in un caso essere utilizzati per eseguire in parallelo i thread di uno
stesso processo.

Le considerazioni che precedono, oggetto di approfondimenti nei corsi sui sistemi operativi, lasciano intendere
il grande interesse nei confronti della programmazione concorrente che, potenzialmente, può recare il
beneficio dell’abbassamento dei tempi di esecuzione (aumento delle prestazioni) di un’applicazione.

Tuttavia lo sviluppo di sistemi software concorrenti risulta più difficile rispetto a quello dei normali sistemi
sequenziali (mono thread) per la necessità di dover coordinare (sincronizzare) i thread e garantire cosi
un'evoluzione predicibile e consistente con quella di un corrispondente programma sequenziale.

414
41 5
Capitolo 23 Introduzione alla programmazione multi-thread

Il comportamento di un sistema concorrente è governato dal non determinismo: in un qualunque momento un Il programmaStarter crea due istanze di Generatore, la prima (gl) ha id 1 e seed 0 (il generatore inizia a
processo o un thread può essere estromesso dalla cpu (si dice pre-emptato ad es. perché è scaduto il quanto produrre da 0), la seconda (g2) ha id 2 e seed 1000000. In questo modo è possibile distinguere i "prodotti"
di tempo o perché si è svegliato un processo o un thread più prioritario etc.) e dunque occorre controllare generati dai due thread. I thread sono posti in esecuzione invocando su di essi il metodo start(). A questo
accuratamente che non si determinino inconsistenze sui dati utilizzati. Se ad esempio un thread è pre-emptato punto il main termina, ma l’applicazione no (notare che un thread generatore esegue in un ciclo infinito).
nel mezzo di una manipolazione di una struttura dati condivisa (es. una collezione) con altri thread, è possibile Un’applicazione Java termina solo quando in essa "restano in piedi" thread demoni (il garbage collector è
che un dato sia stato aggiunto/rimosso alla/dalla collezione, ma non tutte le informazioni della struttura dati deamon). I processi demoni forniscono servizi agli altri processi. Se un’applicazione consiste solo di thread
sono aggiornate (es. la size non riflette l'aggiunta o la rimozione). demoni allora la JVM la fa terminare. Naturalmente, un’applicazione può sempre essere terminata dall'utente
(in Eclipse si clicca sul pulsante rosso di attività).
Sviluppare un sistema concorrente corretto è pertanto il risultato della “sapiente" armonizzazione dei thread
coinvolti. In qualche caso bisogna impedire ad un thread di compiere operazioni su dati condivisi, se un altro L’applicazione multi-thread risultante è composta da tre thread: il main (thread creato implicitamente) e i due
thread non ha completato le sue operazioni. generatori gl e g2. L’output è costituito da un blocco di stampe consecutive di un generatore, seguito da un
blocco di stampe consecutive dell'altro generatore, quindi di nuovo un blocco di stampe del primo generatore
D’altra parte l'"eccessiva" sincronizzazione, “frenando troppo” i thread, può degradare sensibilmente le seguito da un blocco del secondo generatore etc. L’alternanza è l’effetto deH'ambiente time-slicing sottostante.
prestazioni del sistema software. Errori nella sincronizzazione possono poi portare a situazioni “spiacevoli"
come deadlock e starvation. Il deadlock (o blocco critico fatale) rappresenta il fatto che un gruppo di processi o Un generatore interrompibile
thread si attendono reciprocamente: nessuno di essi è più in grado di riprendere ad eseguire. La starvation Di seguito si mostra una nuova versione della classe Generatore che è interrompibile.
(letteralmente “morire di fame”), spesso detta “blocco individuale”, è la situazione per cui un processo in attesa
di riprendere ad eseguire, accusa tempi di attesa non limitati. package poo.thread.generatore;
public class Generatorelnterrompibile extends Thread{
I thread di un programma Java sono mappati (dalla JVM) su thread propri del sistema operativo sottostante. private int seed, id;
public Generatorelnterrompibile( int id, int seed ){
Una prima applicazione multi-thread this.id=id; this.seed=seed;
Per verificare l’ambiente multi-thread supportato da Java, di seguito si considera un thread generatore che }
genera in successione numeri, a partire da un seme iniziale (seed). Una classe di thread si può programmare public void run(){
estendendo la classe base Thread (di java.lang) o implementando l’interfaccia Runnable (di java.lang). while( !islnterrupted() /*&& esiste altro da fare'/ ){
L'algoritmo del thread è implementato nel metodo public run(). System.out.println(MGeneratore#"+id+" produce "+seed);
seed++;
package poo.thread.generatore;
public class Generatore extends Thread{ }//run
private int seed, id; }// Generatorelnterrompibile
public Generatore( int id, int seed ) { this.id=id; this.seed=seed;}
public void run(){ Il metodo run() ora contiene un ciclo che è iterato a patto che il thread non sia stato interrotto e, in generale, ci
while( true ){ sia dell’altro lavoro da fare. L’interruzione si può richiedere come mostrato di seguito.
System.out.println(“Generatore#',+id+" produce ”+seed);
seed++; package poo.thread.generatore;
public class Starterlnterrompente {
}//run public static void main( Stringo args ){
}//Generatore Generatorelnterrompibile g1=new Generatorelnterrompibile( 1,0);
Generatorelnterrompibile g2=new Generatorelnterrompibile( 2,1000000 );
Più thread generatori possono essere avviati (spawning) mediante un main: g1.start();
g2.start();
package poo.thread.generatore; //pausa di 10 sec per il main thread
public class Starter ( try{
public static void main( Stringo args ){ Thread.sleep( 10000 ); //il tempo e’ in millisecondi
Generatore g1=new Generatore( 1,0); }catch( InterruptedException e ){}
Generatore g2=new Generatore! 2,1000000 ); g1.interrupt();
g1.start(); g2.start(); g2.interrupt();
}//main }//main
}//Starter }//Starterl nterrompente

416 417
Capitolo 23 Introduzione alia programmazione multi-thread

Lo starter ora avvia i thread generatori, li lascia lavorare per (circa) 10 sec (lower bound)quindi invia a Merge sort multi-thread
ciascuno di loro una richiesta di interruzione ( metodo interruptQ ). Si presenta una versione di merge sort (si riveda il cap. 18) in cui ogni sotto compito di ordinamento è svolto
da un thread worker (lavoratore) con funzioni di sorter (ordinatore). Dividendo il vettore in due sotto vettori, si
Metodi della classe Thread fanno partire due worker concorrenti che ordinano contemporaneamente i due spezzoni. Solo dopo che
Costruttori: entrambi i worker hanno terminato il loro lavoro, si procede con la fusione ordinata dei due segmenti. Ogni
Thread(), Thread( String nome ), Thread( Runnable r ), Thread( Runnable r, String nome ) worker divide a sua volta il suo segmento ed attiva due sotto worker e cosi via, determinando un albero di
void start() worker master-slave.
rende il thread this "pronto" per eseguire
void yield() package poo.thread.mergesort:
cede volontariamente il controllo della cpu ad un altro thread (termina anticipatamente il time slice). In import java.util.*;
qualche implementazione Java, l'effetto di yield() potrebbe ridursi ad una no-operation public class MergeSortMultiThread <T extends Comparable<? super T » {
void interrupt() private T[] a;
invia un segnale di interruzione al thread.Normalmente la richiesta viene registrata in un flag booleano del private int inf, sup;
thread target, che può essere interrogato dal metodo islnterrupted() o dal metodo static interruptedQ; se il public MergeSortMultiThread( T[] a, int inf, int sup ) throws InterruptedException!
thread ricevente è addormentato in una join(), sleep(), wait() (si veda più avanti) etc. esso riceve una this.a=a; this.inf=inf; this.sup=sup;
InterruptedException e si sveglia, ossia diventa nuovamente pronto per eseguire }//MergeSortMultiThread
booiean islnterrupted()
ritorna true se esiste una richiesta pendente di interruzione su questo thread public void start() throws InterruptedException!
static booiean interruptedQ Sorter primo=new Sorter( a, inf, sup );
ritorna true se esiste una richiesta pendente di interruzione sul thread corrente, e se sì pone a false il flag primo.start();
diriaiiesta try{
static Thread currentThreadO primo.join();
ritorna il riferimento al thread corrente }catch( InterruptedException e ) { throw e ;}
String getNamef) }//start
ritorna il nome del thread, come impostato da un costruttore
void setName( String nome ) private class Sorter extends Thread{
consente di cambiare il nome di questo thread private T[]a, aux;
void setPriorityf int nuova_priorita ) private int inf, sup;
cambia la priorità del thread this; i valori ammissibili di priorità sono quelli deH’intervallo public Sorter( T[]a, int inf, int sup ){
[Thread.MIN_PRIORITY, Thread.MAX _PRIORITY] this.a=a; this.inf=inf; this.sup=sup;
int getPriority() }//Sorter
ritorna il valore corrente della priorità del thread this (che per default coincide con la priorità del thread che public void run(){
ha creato questo thread). La priorità di default di un thread è Thread.NORM ^PRIORITY. Tra più thread if( inf<sup ){
pronti per eseguire, il (o un) thread più prioritario è scelto ed esegue per primo sulla cpu. È buona norma int med=(inf+sup)/2;
non fare eccessivo affidamento sul meccanismo delle priorità dei thread. Suggerimento pratico: Sorter s1=new Sortela, inf, med);
non utilizzare le priorità Sorter s2=new Sortela,med+1,sup);
void join() s1.start(); s2.start();
es. t.join() blocca il thread corrente sino a che t non termina (es. l’esecuzione del metodo run() oltrepassa try{
la } di chiusura del corpo). Può sollevare l’eccezione checked InterruptedException s1.join(); s2.join(); //attesa terminazione di s1 e s2
void join( long millisecondi ) }catch( InterruptedException e ){
blocca il thread corrente per al più il numero di millisecondi specificati, perché il thread target termini Il in caso di interruzione si fa terminare il thread
void setDeamon() return;
marca questo thread come demone }
booiean isDeamonf) merge(a,inf,med,sup);
ritorna true se il thread è demone
static void sleepl long millisecondi ) }//run
pone a dormire il thread corrente sino a che non sia trascorso il numero di millisecondi specificati. Può @SuppressWarnings("unchecked'')
sollevare l’eccezione checked InterruptedException private void merge( T[]v, int inf, int med, int sup ){
booiean isAliveQ aux=(T[]) new Comparable[sup-inf+1 ];
ritorna true se il thread interrogato è vivo (magari succede che subito aver risposto che esso è vivo, int i=inf, j=med+1, k=0;
il thread termina; morale: non conviene dipendere “troppo” dal valore restituito da questo metodo).
418 419
Capitolo 23 Introduzione alla programmazione multi-thread

while( i<=med && j<=sup ){ private Stazione s;


if( v[i].compareTo(vO])<0 ){ private int id;
aux[k]=v[i]; i++; k++; public Sensore( int id, long periodo, Stazione s ){
} if( periodocO ) throw new IHegalArgumentExceptionQ;
else{ aux[k]=v[j]; j++; k++;} this.id=id; this.periodo=periodo; this.s=s;
} }
while( i<=med ){
aux[k]=v[i]; i++; k++; private void pausa() throws InterruptedExceptionf
} try{
while( j<=sup ){ Thread.sleepf periodo );
aux[k]=v[j]; j++; k++; }catch(lnterruptedException e){ throw e ;}
} }//pausa
for( k=0; k<aux.length; k++ )
v[k+inf]=aux[k]; public void run(){
}//merge while( !islnterrupted() ){
boolean passaVeicolo=(Math.random()<0.5)? true : false;
}//Sorter if( passaVeicolo ){
System.out.println("Sensore#"+id+" segnala passaggio veicolo");
public static void main( String []args ) throws lnterruptedException{ s.segnaleVeicolo();
Integerf] a={ 10,9,8,7,6.5,4,3,2,1}; }
System.out.println("Vettore iniziale: "+Arrays.toString(a)); try{ pausa(); }catch( InterruptedException e ){ break;}
MergeSortMultiThread<lnteger> msmt=new MergeSortMultiThread<lnteger>( a, 0, a.length-1 );
msmt.start(); }//run
System.out.println("Vettore ordinato: "+Arrays.toString(a));
}//main }//Sensor

}//MergeSortMultiThread package poo.thread.stazione;


import java.io.*;
Stazione di monitoraggio import java.util.*;
Si considera una stazione per il monitoraggio del traffico su una certa strada. Ai lati di ciascuna carreggiata c’è public class Trasmettitore implements Runnable{
un sensore che ad ogni passaggio di un veicolo invia un segnale alla stazione cosi che il veicolo è contato. private PrintWriter pw;
Periodicamente, il contatore dei veicoli viene rilevato da un processo trasmettitore e archiviato su un file di log. private Stazione s;
Ogni volta che avviene la rilevazione, il contatore viene posto a zero. La stazione di monitoraggio è descritta private long periodo;
dalla seguente interfaccia: public Trasmettitore( Stazione s, long periodo, String fileLog ) throws IOException{
if( period<0 ) throw new IHegalArgumentException();
package poo.thread.stazione; this.s=s; this.periodo=periodo; pw=new PrintWriter( new FileWriter(fileLog) );
public interface Stazione { }
void segnaleVeicolo();
int rilevazione(); private void pausa() throws InterruptedException!
}//Stazione try{
Thread.sleep( periodo );
Il metodo segnaleVeicolo() è invocato da un sensore. Il metodo rilevazione() è chiamato dal trasmettitore. Sia i }catch(lnterruptedException e){ throw e ;}
sensori che il trasmettitore sono thread con un comportamento periodico. Per scopi dimostrativi, Sensore }//pausa
estende Thread e Trasmettitore implementa Runnable. In quest'ultimo caso un oggetto trasmettitore è
Runnable e per essere fatto partire come Thread si procede come segue: public void run(){
while( !Thread.currentThread().islnterrupted() ){
Trasmettitore t=new Trasmettitore(...); Thread tt=new Thread( t ); t.startQ; try{
pausa();
package poo.thread.stazione; }catch(lnterruptedException e ) { break;}
public class Sensore extends Thread{ int dato=s.rilevazione();
private long periodo; Date ora=new Date(); pw.println( ora+" veicoli: ”+dato ); pw.flush();
420 421
Capitolo 23 Introduzione alla programmazione multi-thread

System.out.println( ora+“ veicoli: "+dato ); quanto si riferiscono a dati condivisi da più thread. Le sezioni critiche dovrebbero essere eseguite in mutua
} esclusione: se un thread perde il controllo mentre si trova in una sezione critica, un altro thread non dovrebbe
pw.close(); essere in grado di iniziare un’altra sezione critica, piuttosto dovrebbe essere bloccato.
}//run
La mutua esclusione si può ottenere come segue. Si supponga che esista un lucchetto associato alla
}//Tasmettitore stazione. Se il lucchetto è aperto, un thread può entrare in sezione critica e simultaneamente chiudere il
lucchetto. A questo punto, se il thread perde il controllo (es. per time-slicing) esso viene estromesso dalla cpu
Una stazione “naif"_________ ma il lucchetto rimane chiuso! Se un altro thread tenta di entrare in sezione critica, esso troverà il lucchetto
Un’implementazione piuttosto “ingenua” dell’interfaccia Stazione è mostrata di seguito: chiuso e dovrà necessariamente bloccarsi. Solo il thread che detiene il lucchetto, riprendendo la sua
esecuzione, sarà in grado di completare la sua sezione critica e quindi aprire nuovamente il lucchetto. Tra i
package poo.thread.stazione; thread bloccati in attesa del lucchetto, uno solo potrà appropriarsi del lucchetto ed eseguire una sezione critica
public class StazioneUnSafe implements Stazione) etc
private int veicoli=0;
Risponde a queste problematiche il meccanismo dei blocchi synchronized di Java. La classe Object introduce
public void segnaleVeicolo(){ un lucchetto, automaticamente disponibile in ogni oggetto-istanza di una classe. Etichettando i metodi
veicoli++; segnaleVeicolo() e rilevazione() con il modificatore synchronized essi vengono dichiarati sezioni critiche di
}//campionamento codice da eseguire in mutua esclusione. Un thread che invochi un tale metodo, si blocca se il lucchetto
associato a this è chiuso, diversamente chiude il lucchetto ed entra nel metodo. Tra i thread bloccati sullo
public int rilevazione(){ stesso lucchetto, uno è scelto e svegliato, non appena il lucchetto sta per essere riaperto. In realtà il lucchetto
int dato=veicoli; rimane chiuso e passato al thread svegliato.
veicoli=0;
return dato; Una classe sicura rispetto all'accesso concorrente di più thread è detta thread-safe.
}//rilevazione
Una stazione thread-safe
}//StazioneUnSafe package poo.thread.stazione;
public class StazioneThreadSafe implements Stazione!
StazioneUnSafe non controlla in alcun modo gli accessi concorrenti da parte dei thread sensori e private int veicoli=0;
trasmettitore. Sono possibili malfunzionamenti. Considerato che un’istruzione come veicoli++ è scomposta al
livello di macchina (si veda la macchina RASP in appendice B) in un blocco di istruzioni primitive del tipo: public synchronized void segnaleVeicolo(){
veicoli++;
LOAD veicoli (1) }//campionamento
ADD# 1 (2)
STORE veicoli (3) public synchronized int rilevazione(){
int dato=veicoli;
e che le istruzioni di macchina sono atomiche (o indivisibili), potrebbe succedere che un sensore abbia veicoli=0;
invocato segnaleVeicolo() ed abbia eseguito l’operazione (1) prima di essere pre-emptato (context-switch). A return dato;
questo punto se il processo trasmettitore fa una rilevazione, ottiene il valore corrente del contatore, lo scrive }//rilevazione
sul file di log quindi azzera il contatore. Quando riprende il sensore, esso esegue il passo (2) ed incrementa il }// StazioneThreadSafe
valore del contatore veicoli che esisteva al tempo del passo (1), dunque non tiene conto deH'azzeramento del
trasmettitore. La conseguenza è che il traffico “monitorato” risulta “virtualmente” più elevato del dovuto. Ora ogni esecuzione di segnaleVeicolo() esclude che possa aver luogo simultaneamente una rilevazione() e
viceversa. Piuttosto, una richiesta del trasmettitore viene bloccata in attesa che finisca la segnalazione di
Un altro malfunzionamento potrebbe succedere se è il trasmettitore ad aver iniziato il metodo rilevazione(), ad veicolo e viceversa.Di seguito si mostra un main che configura e fa partire l’applicazione di monitoraggio.
aver copiato il valore di veicoli nella variabile locale dato e quindi perde il controllo (pre-emption). Magari un
sensore segnala un veicolo ed incrementa il contatore di veicoli. Quando riprende il trasmettitore, esso azzera La classe Monitoraggio:______________________________________________________________________
il contatore. Qui l’effetto è che si perde una segnalazione di veicolo. package poo.thread.stazione;
import java.io.*;
I problemi delineati sono sintomatici di una situazione generale: quando più thread accedono public class Monitoraggio {
concorrentemente a dei dati condivisi (i dati della stazione, cioè il contatore veicoli) ogni accesso public static void main( Stringi) args ) throws IOException{
(un’esecuzione del metodo segnaleVeicolo() o di quello rilevazione()) dovrebbe avvenire in modo indivisibile o Stazione s=new StazioneThreadSafe();
atomico in modo da impedire ad altri thread di modificare i dati se un precedente thread non ha ancora finito le Trasmettitore t=new Trasmettitore( s, 5000, ’ c:\\poo-file\\log.txt1' );
sue operazioni. Metodi come segna!eVeicolo() e rilevazione() rappresentano sezioni critiche di codice, in Thread tt=new Thread(t);
422 423
Capitolo 23 Introduzione alla programmazione multi-thread

Sensore s1=new Sensore(1,500,s); while( condizione-per-dormire-è-true )


Sensore s2=new Sensore(2,500,s); try{ waitQ; }catch( InterruptedException e )(}
tt.start(); s1.start(); s2.start();
try{ L’uso del ciclo di while è indispensabile per imporre ad un thread svegliato di ri-verificare se le condizioni che
Thread.sleep(20000); //esempio di periodo di osservazione prescrivono di dormire sono ancora vere al tempo del risveglio.
}catch(lnterruptedException e){}
s1 .interrupt(); s2.interrupt(); tt.interrupt(); Si intuisce quindi che la semantica della waitQ corrisponda ad un'operazione in tre fasi: 1) apertura del
}//main lucchetto (cosi da consentire a qualche altro thread di entrare e modificare la struttura dati), 2)
}//Monitoraggio addormentamento sul wait-set, 3) (al tempo di un risveglio) riacquisizione del lucchetto ed uscita dalla waitQ.
In base allo schema di sopra, un thread svegliato è costretto a rivedere le condizioni della struttura dati e cosi
Mutua esclusione e sospensione decidere se tornare a dormire (il risveglio è stato un falso allarme) o se uscire dal while con il lucchetto chiuso
In realtà esistono due problemi: la mutua esclusione, che garantisce che l’esecuzione di sezioni critiche e quindi continuare le operazioni della sezione critica.
avvenga in modo sequenziale, e la sospensione (sincronizzazione) che richiede che un thread che abbia
ottenuto il lucchetto ma che poi trovi la struttura dati in uno stato che non gli permette di continuare, deve porsi Cenni al Java Memory Model
in attesa che questo stato cambi. L'implementazione di Java sfrutta le caratteristiche dei moderni processori: il caching, il multi-core etc. Ad es. il
compilatore ottimizza spesso il codice mappando variabili su registri del processore. Pertanto è importante
La classe Object rende disponibili tre metodi utili per la sincronizzazione: wait(), notifyO e notifyAII() che capire le implicazioni di queste ottimizzazioni sui programmi. Un thread che gira su un diverso core ed accede
possono essere utilizzati esclusivamente aH’interno di una sezione critica, ossia dentro un blocco ad una variabile condivisa potrebbe “vedere" un valore non aggiornato, nel senso che il valore vero è in un
synchronized. registro che non è stato ancora “riversato" in memoria.

Questi metodi agiscono sul lucchetto fornito da Object. Il metodo wait() pone il thread che lo invoca a dormire L’uso dei lucchetti ha un impatto che va oltre la mutua esclusione di cui si è parlato in precedenza. La
sul wait-set (dormitorio) associato al lucchetto dell’oggetto. Il metodo notifyO sveglia un thread, se esiste, che sincronizzazione indotta da un lucchetto, infatti, incide sulla visibilità dei valori delle variabili.
dorme sul wait-set. Non è garantito che il thread che si sveglia sia quello più vecchio, ossia che aspetta da più
tempo. In altre parole il wait-set non è necessariamente una coda. Il metodo notifyAII() sveglia tutti i thread che Il Java Memory Model (JMM) stabilisce che quando un thread esce da una sezione critica e dunque apre il
dormono sul wait-set. lucchetto, certamente i valori delle variabili utilizzate durante la sezione critica sono aggiornati in memoria cosi
che un nuovo thread che acquisisca il lucchetto vede questi ultimi valori e non valori vecchi.
Blocchi synchronized, waitQ, notify()/notifyAII() costituiscono il m onitor nativo di Java. Il monitor è rientrante:
se un thread già possiede il lucchetto, e chiama un metodo synchronized, virtualmente lascia e si riappropria Si tratta di una proprietà importante che rende il comportamento del programma predicibile ed indipendente
immediatamente del lucchetto. Tutto ciò rende possibile che un metodo synchronized possa anche essere dalle ottimizzazioni e dalle caratteristiche della macchina sottostante, al prezzo degli oneri computazionali
ricorsivo. della sincronizzazione (gestione dei lucchetti).

Anche quando un thread tenta di entrare in una sezione critica e trova il lucchetto chiuso, il thread è posto a Le conseguenze di JMM sono “sentite" anche al livello di programmazione (si veda più avanti).
dormire sul wait-set. Tuttavia Java distingue i thread che sono entrati sul wait-set per causa di wait() dai thread
che vi si trovano a causa del lucchetto chiuso. Questi ultimi sono svegliati automaticamente ad ogni tentativo Produttore/Consumatore e BufferLimitato
di riapertura del lucchetto. I metodi notify()/notifyAII() hanno effetto, invece, solo sui thread che dormono per È una classica applicazione di programmazione concorrente. Nel caso più semplice un singolo produttore
ragioni di waitQ. Se non ci sono thread che dormono per ragioni di wait(), le operazioni di notifica sono no- genera messaggi verso un singolo consumatore.
operation.
Al fine di esaltare l’indipendenza tra produttore e consumatore (si rifletta che al tempo di un messaggio, il
Occorre riflettere che un thread svegliato da un dormitorio è uno che è già dentro una sezione critica. Esso consumatore potrebbe non essere pronto a riceverlo e processarlo e viceversa, quando il consumatore fosse
ridiventa “pronto per eseguire” (ready-to-run). Per il resto non ha nessun privilegio rispetto ai thread che disposto a ricevere un messaggio, magari il produttore non è pronto a spedirne uno) è opportuno interporre tra
vogliano entrare dall’esterno in una sezione critica guardata dallo stesso lucchetto. In altre parole, un thread di essi una mailbox, ossia un buffer di dimensione limitata. In questo modo, al tempo di una produzione, se c'è
svegliato deve competere per riacquisire il lucchetto come tutti gli altri (tutto ciò è nascosto nel codice della spazio nel buffer il produttore vi inserisce il messaggio senza attendere che il consumatore lo riceva, e
wait()). Un fatto fondamentale a questo punto è capire che tra il momento della notifica e la reale condizione di disponendosi subito a generare un nuovo messaggio etc. Similmente, se il consumatore desidera un
esecuzione (che presuppone, lo ripetiamo, il riacquisto del lucchetto) può passare un lasso di tempo durante il messaggio, e almeno uno è disponibile nel buffer, lo preleva dal buffer senza alcuna comunicazione col
quale le condizioni che avevano suggerito il risveglio, possono non essere più vere nel momento in cui il produttore.
thread svegliato esegue sulla cpu.
Naturalmente può succedere che al tempo di una produzione il buffer sia pieno o al tempo di un consumo il
Da quanto precede scaturisce lo schema programmativo generale da seguire per una corretta buffer sia vuoto. Nel primo caso il produttore deve attendere che il consumatore estragga qualche messaggio
sospensione/riattivazione in una sezione critica: dal buffer. Nel secondo caso il consumatore deve attendere che il produttore inserisca qualche nuovo
messaggio nel buffer.

424 425
Capitolo 23 Introduzione alla programmazione multi-thread

Quando più messaggi sono disponibili nel buffer, in generale può essere opportuno che il loro prelevamento public Produttore( int id, BufferLimitato<String> b, int delayMax, int delayMin ){
avvenga in ordine FIFO, ossia secondo l’ordine di arrivo. this.id=id; this.b=b; this.delayMax=delayMax; this.delayMin=delayMin;
}
Si capisce che la mailbox possa può essere realizzata mediante la classe generica BufferLimitato presente in
poo.util (si riveda il cap. 15). Tale classe, tuttavia, non è thread-safe. Si può progettare una classe private void delay(){
BufferLimitatoMJ (basata sul monitor di Java) erede di BufferLimitato in modo da rendere le operazioni get/put try{
sezioni critiche. Thread.sleep( (int)(Math.random()*(delayMax-delayMin)+delayMin) );
}catch( InterruptedException e ){}
package poo.thread.buffer; }//delay
import poo.util.BufferLimitato;
public class BufferLimitatoMJ> extends BufferLimitato<T>{ public void run(){
public BufferLimitatoMJ( int n ) { super(n);} while( true )(
public synchronized void put( T msg ){ delay();
while( super.isFullQ ) msg^'PfT+id+V+i;
try{ wait(); }catch( InterruptedException e ){} System.out.println("Produttore#"+id+" genera messaggio "+msg);
super.put(msg); i++;
notify(); //al piu' un consumatore sta aspettando b.put( msg );
}//put
public synchronized T get(){ }//run
while( super.isEmpty() )
try{ wait(); }catch( InterruptedException e )(} }//Produttore
T msg=super.get();
notify(); //al piu' un produttore sta aspettando package poo.thread.buffer;
return msg; import poo.util.*;
}//get public class Consumatore extends Thread{
public synchronized int size(){ return super.sizeQ;} //Si ignorano le eccezioni InterruptedException dal momento che un thread può
public synchronized void clear(){ super.clear();} //trovarsi in wait() non interrompibile.
public synchronized boolean isEmpty(){ return super.isEmpty();} private int id;
public synchronized boolean isFull(){ return super.isFull();} private BufferLimitato<String> b;
}//BufferLimitatoMJ private int delayMax, delayMin;
private String msg;
Tutti i metodi di BufferLimitato<T> sono stati sincronizzati. Si nota esplicitamente che anche i metodi predicati public Consumatore( int id, BufferLimitato<String> b, int delayMax, int delayMin ){
di stato come size(), isFullQ, isEmpty() vanno sincronizzati per le ragioni legate al JMM. I metodi get() e put() this.id=id; this.b=b; this.delayMax=delayMax; this.delayMin=delayMin;
pongono rispettivamente a dormire il consumatore o il produttore se trovano il buffer rispettivamente vuoto o }
pieno.
private void delay(){
Dopo che una produzione (rispettivamente un consumo) va a buon fine, si esegue una notify() al fine di try{
svegliare l'unico thread partner eventualmente in attesa sul wait-set. Quando più processi possono essere Thread.sleep( (int)(Math.random()‘ (delayMax-delayMin)+delayMin) );
presenti sul wait-set, è sempre conveniente utilizzare la notifyAHQ in quanto la notifyO non è detto che svegli il }catch( InterruptedException e ){}
processo “giusto” in grado di riprendere. Seguono le classi Produttore e Consumatore. }//delay

package poo.thread.buffer; public void run(){


import poo.util.*; while( true )(
public class Produttore extends Thread{ msg=b.get();
//Si ignorano le eccezioni InterruptedException dal momento che un thread può trovarsi System.out.println("Consumatore#''+id+“ consuma messaggio "+msg);
//in wait() non interrompibile. delay();
private int id; }
private BufferLimitato<String> b; }//run
private int delayMax, delayMin, i=0;
private String msg; }//Consumatore

426 427
Capitolo 23 Introduzione alla programmazione multi-thread

package poo.thread.buffer; asincronismo tra produttori e consumatori sia per garantire che l'ordine di ricezione dei messaggi corrisponda
import poo.util.*; all'ordine di produzione.
public class ProdiConsl {//classe pilota La classe BufferLimitatoMJ può essere agevolmente modificata in accordo allo scenario più generale. In
public static void main( String 0 args ){ pratica è opportuno sostituire la notify() con la notifyAII() dal momento che nelle nuove condizioni non si può
BufferLimitato<String> b=new BufferLimitatoMJ<String>( 5 ); //capacità 5 msg escludere, in dipendenza anche della capacità della mailbox (che potrebbe essere anche pari ad 1 - buffer
Produttore p1=new Produttore( 1, b, 10000, 2000 ); unitario), che entrambi i tipi di processi possano trovarsi a dormire per ragioni di wait sul wait-set della mailbox.
Consumatore c1=new Consumatore( 1, b, 10000, 5000 ); La notifyAHQ sveglia tutti i processi in wait. A seguito di questo, ciascun processo ha la responsabilità
p1.start();c1.start(); (attraverso il ciclo di while) di verificare se il risveglio è effettivamente possibile o occorre tornare a dormire.
}//main
}//Prod1 Consl Anche nel caso in cui, ad un certo momento, fossero in wait solo produttori (o solo consumatori) la notify()
non garantirebbe il risveglio del processo che aspetta da più tempo.
L'intervallo di tempo tra due produzioni (o consumi) consecutive (i) è distribuito uniformemente neH’intervallo
[delayMin, delayMax] specificato al tempo di costruzione di un thread produttore o consumatore. Si presenta una classe Mailbox nella quale non solo i messaggi sono consegnati ai consumatori in modo
FIFO, ma anche i risvegli dei processi, aH'interno di ciascuna categoria, avvengono in modo FIFO.
Esempio di output:______________________________________________________ __ _
Produttore# 1 genera messaggio P#1_0 Mailbox con risvegli FIFO
Consumatore#1 consuma messaggio P#1_0 package poo.thread.buffer;
Produttore# 1 genera messaggio P#1_1 import poo.util.BufferLimitato;
Consumatore# 1 consuma messaggio P#1 _1 import java.util.*;
Produttore# 1 genera messaggio P#1_2 public class Mailbox<T> extends BufferLimitato<T>{
Produttore# 1 genera messaggio P#1_3 private LinkedList<Thread> listaProd=new LinkedList<Thread>();
Consumatore# 1 consuma messaggio P#1_2 private LinkedList<Thread> listaCons=new LinkedList<Thread>();
Produttore# 1 genera messaggio P#1_4 public Mailbox( int n ) { super(n);}
Consumatore# 1 consuma messaggio P#1_3
Produttore# 1 genera messaggio P#1_5 private boolean produttoreDeveDormire(){
Consumatore# 1 consuma messaggio P#1_4 if( super.isFull() Il listaProd.getFirst()!=Thread.currentThread() )
Produttore# 1 genera messaggio P#1_6 return true;
Produttore# 1 genera messaggio P#1_7 return false;
Consumatore# 1 consuma messaggio P#1_5 }//produttoreDeveDormire
Produttore#1 genera messaggio P#1_8
Consumatore# 1 consuma messaggio P#1_6 private boolean consumatoreDeveDormire(){
Produttore# 1 genera messaggio P#1_9 if( super.isEmpty() Il listaCons.getFirst()!=Thread.currentThread() )
Produttore#1 genera messaggio P#1_10 return true;
Consumatore# 1 consuma messaggio P#1_7 return false;
Produttore# 1 genera messaggio P#1_11 }//consumatoreDeveDormire
Consumatore# 1 consuma messaggio P#1_8
Produttore# 1 genera messaggio P#1_12 public synchronized void put( T msg ){
Consumatore#1 consuma messaggio P#1_9 listaProd.addLast( Thread.currentThreadQ );
Produttore# 1 genera messaggio P#1_13 while( produttoreDeveDormireO )
Produttore# 1 genera messaggio P#1_14 try{ wait(); }catch( InterruptedException e ){}
Produttore# 1 genera messaggio P#1_15 listaProd.removeFirst();
Consumatore#1 consuma messaggio P#1 _10 super.put(msg);
Consumatore#! consuma messaggio P#1_11 notifyAHQ;
}//put

Si dovrebbe notare come i consumi avvengano in modo FIFO rispetto alle produzioni. public synchronized T get(){
listaCons.addLast( Thread.currentThreadQ );
N Produttori M Consumatori while( consumatoreDeveDormireQ )
In un scenario più generale un gruppo di produttori P0, Pi, Pn 1, genera dati verso un gruppo di consumatori try{ wait(); }catch( InterruptedException e ){}
Co, Ci, .... Cm-i . Il prodotto (messaggio) di un generico produttore può essere ricevuto da un qualsiasi listaCons.removeFirstQ;
consumatore. Anche in questo caso la mediazione della mailbox è fondamentale sia per assicurare T msg=super.get();
428 429
Capitolo 23 Introduzione alla programmazione multi-thread

notifyAHQ; private boolean produttoreDeveDormire(){


return msg; if( size==buffer.length II listaProd.peek().thread!=Thread.currentThread() )
}//get return true;
return false;
public synchronized int size(){ return super.size();} }//produttoreDeveDormire
public synchronized void clear(){ super.clear();}
public synchronized boolean isEmpty(){ return super.isEmpty();} private boolean consumatoreDeveDormire(){
public synchronized boolean isFull(){ return super.isFull();} if( size==0 II listaCons.peek().thread!=Thread.currentThread() )
return true;
}//Mailbox return false;
}//consumatoreDeveDormire
Per tenere traccia dell’ordine di arrivo dei processi, sono state introdotte due linked list di Thread: listaProd e
listaCons. Un produttore che esegua put, prima si pone in coda alla lista dei produttori (pessimismo). Quindi public synchronized void put( int id, T msg ){
entra nel ciclo di attesa condizionato dal fatto che il produttore debba dormire. A questo scopo è stato listaProd.add( new Processo( Thread.currentThread(), id ) );
introdotto un metodo privato che ritorna true se il produttore deve effettivamente dormire. Ciò accade quando while( produttoreDeveDormireQ )
o il buffer è pieno oppure, pur non essendo pieno, il produttore non è il primo della lista listaProd. In modo try{ wait(); }catch( InterruptedException e ){}
speculare si comportano i consumatori. listaProd.pollQ;
buffer[in]=msg; in=(in+1)%buffer.length; size++;
Quando un processo esce dal ciclo di while deve, come prima cosa, eliminare il riferimento a sè stesso notifyAHQ;
presente in testa alla relativa lista. È evidente l’importanza della notifyAII() in questo contesto, non essendo }//put
possibile svegliare selettivamente un ben preciso processo.
public synchronized T get( int id ){
Lo schema programmativo risultante è generale. Ciò che cambia di caso in caso è la condizione per cui un listaCons.add( new Processo( Thread.currentThread(), id ) );
processo deve dormire e questa si può catturare in un metodo privato della classe. while( consumatoreDeveDormireQ )
try{ wait(); }catch( InterruptedException e ){}
Di seguito si mostra una ri-organizzazione della classe mailbox allorquando i risvegli devono avvenire per listaCons.poll();
priorità: minore è l'indice (id) del processo, maggiore è la sua priorità aH’interno della sua categoria. T msg=buffer[out]; out=(out+1)%buffer.length; size—;
notifyAllQ;
Mailbox con risvegli prioritari return msg;
package poo.thread.buffer.priorita; }//get
import java.util.*;
public class MailboxPrioritaria<T> { }//MailboxPrioritaria

private static class Processo implements Comparable<Processo>{ Per realizzare i risvegli prioritari si è fatto uso di priority queue di java.util. Il criterio di ordine è catturato nella
Thread thread; inner class Processo che memorizza l’id del thread e il riferimento al thread, ed implementa Comparable.
int id;
public Processo( Thread t, int id ) { this.thread=t; this.id=id;} Per semplicità i file sono replicati nel package poo.thread.buffer.priorita, e la classe MailboxPrioritaria è stata
public int compareTo( Processo p ) { return p.id-this.id;} resa classe base e non più erede di BufferLimitato. Ciò in quanto i metodi get() e put() ammettono ora un
}//Processo parametro in più che è l’identificatore unico (id) del thread invocante.

private PriorityQueue<Processo> listaProd=new PriorityQueue<Processo>(); Segue un esempio di main.


private PriorityQueue<Processo> listaCons=new PriorityQueue<Processo>();
private T[] buffer; package poo.thread.buffer.priorita;
private int in=0, out=0, size=0; import poo.util.*;
public class ProdCons {
©SuppressWamingsf'unchecked'') public static void main( String [] args ){
public MailboxPrioritaria( int n ){ MailboxPrioritaria<String> b=new MailboxPrioritaria<String>( 5 );
if( n<=0 ) throw new IHegalArgumentException(); Produttore p1=new Produttore( 1, b, 10000,2000 );
buffer=(T0) new Object[n]; Produttore p2=new Produttorej 2, b, 5000,1000 );
} Produttore p3=new Produttorej 3, b, 20000, 5000 );
Consumatore c1=new Consumatore( 1, b, 10000, 5000 );
430 431
Capitolo 23 Introduzione alla programmazione multi-thread

Consumatore c2=new Consumatore( 2, b, 8000,1000 ); public synchronized void rilasciaForchette( int id ){


p1.start(); p2.start(); p3.start(); if( id<0 II id>=forchetta.length )
c1.start(); c2.start(); throw new IHegalArgumentExceptionQ;
}//main forchetta[id]=true;
}//ProdCons forchetta[(id+1)%forchetta.length]=true;
notifyAHQ;
Il problema de| cinque filosofi }//rilasciaForchette
E un altro classico problema di programmazione concorrente dovuto a E. Djikstra.
}//Tavolo
Cinque filosofi (in generale n>1 filosofi) passano la loro vita alternando l’attività di riflessione a quella di
alimentazione. Vivono in una casa con una tavola perennemente apparecchiata. Al centro della tavola c’è un package poo.thread.filosofi;
grosso piatto di spaghetti (di quantità illimitata). Ogni filosofo ha un posto a sedere identificato dal suo indice i, public class Filosofo extends Thread{
da 0 a 4 (in generale da 0 a n-1). La dotazione di ogni filosofo consiste della sedia, del suo piatto e di una private final int id;
forchetta alla sua sinistra. Siccome gli spaghetti sono molto intricati, per servirsi da mangiare occorre usare private Tavolo t;
due forchette: la propria e quella del filosofo a destra, posto che questi al momento non stia mangiando. Un private final long MIN, MAX;
filosofo trattiene le forchette per tutto un pasto e le rilascia quando ha finito di mangiare, nel qualcaso torna public Filosofo( final int id, final Tavolo t, final long MIN, final long MAX ){
nuovamente a pensare. this.id=id; this.t=t; this.MIN=MIN; this.MAX=MAX;
}
Numerando i filosofi da 0 a n-1 in senso antiorario, la forchetta alla sinistra del filosofo / è identificata dal
numero t quella alla sua destra ha numero (A-1)%n. private void pausa(){
try{
I filosofi sono modellabili come thread. Le forchette sono risorse condivise. Occorre risolvere il problema in Thread.sleep( (long)(Math.random()*(MAX-MIN)+MIN) );
modo da consentire ai filosofi di pensare e mangiare per sempre (si fa per dire). }catch( InterruptedException ie ){}
}//pausa
II problema può essere schematizzato con una classe Tavolo (risorsa passiva), una classe Filosofo e una
classe Problema che configura e lancia l’esecuzione. Il tavolo contiene le risorse forchette modellate con un public void run(){
array di boolean: forchettafi] è true se la forchetta i-esima è disponibile, false se è già in uso. Il tavolo deve while( true ){
essere thread-safe. System.out.printlnfFilosofo "+id+" pensa...");
pausa();
Il problema è un classico in quanto può indurre deadlock (blocco critico). Si consideri infatti la soluzione “naif” System.out.println("Filosofo "+id+“ richiede forchette...");
che segue: t.ottieniForchette(id);
System.out.printlnfFilosofo "+id+“ mangia...");
package poo.thread.filosofi; pausaQ;
public class Tavolo! //versione che può creare deadlock System.out.printlnfFilosofo “+id+" rilascia forchette...");
private boolean forchetta!]; t.rilasciaForchette(id);
public Tavolo( int n ){ }
if( n<=1 ) throw new HlegalArgumentException(); }//run
forchetta=new boolean[n];
for( int i=0; kforchetta.length; i++ ) forchetta[i]=true; }//Filosofo
}
package poo.thread.filosofi;
public synchronized void ottieniForchette( int id ){ public class Problema {
if( id<0 II id>=forchetta.length ) throw new lllegalArgumentException(); public static void main( Stringi] args ){
while( !forchetta[id] ) Tavolo t=new Tavolo(5);
try{ wait(); }catch( InterruptedException ie ){} for( int i=0; i<5; i++ )
forchetta[id]=false; new Filosofo(i,t,0,0).start();
while( !forchetta[(id+1)%forchetta.length] ) }//main
try{ wait(); }catch( InterruptedException ie ){} }//Problema
forchetta[(id+1)%forchetta.length]=false;
}//ottieniForchette Ogni filosofo riceve due costanti MIN e MAX che esprimono il minimo ed il massimo tempo per pensare o per
mangiare. L’attesa, casualmente generata in modo che sia uniformemente distribuita tra MIN e MAX, è
432 433
Capitolo 23 Introduzione alla programmazione multi-thread

programmata nel metodo privato pausa() mediante sleep(). Al fine di consentire "massima competizione", public synchronized void ottieniForchette( int id ){
questi tempi possono essere posti a 0. D'altra parte occorre sempre tener presente che if( id<0 II id>=forchetta.length )
throw new IHegalArgumentException();
la correttezza di un programma concorrente non deve dipendere dal tempo. while( !forchetta[id] Il ! forchetta[(id+1)%forchetta.length] )
try{ wait(); }catch( InterruptedException ie ){}
Dunque, per studiare una soluzione concorrente è opportuno che non ci siano sleep() che possono falsare il forchetta[id]=false;
comportamento. Nel main di cui sopra, essendo i tempi impostati a zero, la pausa non ha efficacia. Mandando forchetta[(id+1)%forchetta.length]=false;
in esecuzione il programma, dopo un certo tempo si registra l’output che segue. }//ottieniForchette

public synchronized void rilasciaForchette( int id ){


Filosofo 4 pensa... if( id<0 II id>=forchetta.length )
Filosofo 4 richiede forchette... throw new IHegalArgumentException();
Filosofo 3 mangia... forchetta[id]=true;
Filosofo 3 rilascia forchette... forchetta[(id+1)%forchetta.length]=true;
Filosofo 3 pensa... notifyAII();
Filosofo 3 richiede forchette... }//rilasciaForchette
Filosofo 2 mangia...
Filosofo 2 rilascia forchette... }//Tavolo
Filosofo 2 pensa...
Filosofo 2 richiede forchette... Con la nuova versione di Tavolo non si genera più deadlock, anche quando i tempi MIN e MAX sono impostati
Filosofo 1 mangia... a 0. L'analisi dell'output generato conferma, inoltre, che mai succede che mangiano contemporaneamente due
Filosofo 1 rilascia forchette... filosofi adiacenti (situazione assurda). L’uso di tempi maggiori di 0 rende solo più lenta la dinamica del
Filosofo 1 pensa... sistema. Si ribadisce comunque il principio già enunciato: un sistema concorrente che si comporti
Filosofo 0 mangia... (apparentemente) “bene" solo perché ci sono delle sleep() qui e là, è scorretto per definizione.
Filosofo 1 richiede forchette...
Filosofo 0 rilascia forchette... Malgrado l’assenza di deadlock, la soluzione non garantisce l'assenza di starvation (blocco individuale)
Filosofo 0 pensa... essendo possibile che il sistema conceda le forchette sempre ad altri e mai ad un certo filosofo che le sta
Filosofo 0 richiede forchette... aspettando. Per il controllo della starvation si veda ad es. l’esercizio 1 in fondo al capitolo.

Il sistema dei 5 filosofi è entrato in deadlock! Ogni filosofo richiede le forchette ma l’operazione induce un Uso di blocchi synchronized ______
blocco fatale! I filosofi si aspettano l’un l’altro ciclicamente e senza via d’uscita. Il problema è dovuto al fatto Java rende possibile controllare l’estensione di una sezione critica mediante l’uso del blocco synchronized che
che il metodo richiedeForchette() si impossessa delle due forchette una alla volta: cominciando con quella si può utilizzare in alternativa al metodo synchronized che rende tutto il metodo sezione critica. Si scrive:
propria del filosofo (alla sua sinistra). Se tutti e 5 i filosofi riescono ad prendere la propria forchetta, poi si
bloccano in attesa dell’altra che appartiene al filosofo destro e che, per ipotesi, non è disponibile. Da qui il synchronized( this ){//se si utilizza il lucchetto di this
deadlock. azioni della sezione critica
)
Per evitare il deadlock si può adottare una differente politica di acquisizione delle risorse: un filosofo o
acquisisce tutte e due le forchette in un solo colpo o nessuna. In sostanza, se una sola forchetta è disponibile, e si rimuove synchronized dall'intestazione del metodo.
Il filosofo non la tocca: aspetta che sia pronta anche l’altra.
A prescindere da questioni di estensione della sezione critica, l’uso dei blocchi synchronized espliciti può
package poo.thread.filosofi; contribuire alla invulnerabilità di una classe thread-safe in quanto è possibile basare la mutua esclusione
public class Tavolo {//versione senza deadlock anziché sull’oggetto this (soluzione di default) su un oggetto diverso e privato introdotto nella classe. Per
private boolean forchetta!]; chiarire le idee si presenta di seguito una versione “più robusta" del tavolo per i filosofi.
public Tavolo( int n ){
if( n<=1 ) throw new IHegalArgumentException(); In questa nuova impostazione un oggetto (un’istanza di qualsiasi classe va bene, qui si usa direttamente
forchetta=new boolean[n]; Object) lock, incapsulato, viene usato per ottenere la mutua esclusione e come dormitorio.
for( int i=0; kforchetta.length; i++ )
forchetta[i]=true; I metodi wait(), notify() e notifyAII() fanno esplicitamente riferimento all’oggetto lock di cui si sfrutta il lucchetto.

434 435
Capitolo 23 Introduzione alla programmazione multi-thread

package poo.thread.filosofi; Ogni processo è in grado di inviare, sincronamente, un messaggio all’altro processo e contemporaneamente
public class Tavolo{ ricevere un messaggio dal partner (si invia nuli quando non si ha nulla da trasmettere).
private boolean forchetta!];
private Object lock=new Object(); //oggetto lucchetto privato Lo scambio è sincrono: chi prima arriva aspetta l’altro processo. Di seguito si mostra, a titolo di esempio,
public Tavolo! int n ){ un'implementazione del meccanismo Exchanger utilizzando il monitor nativo di Java in una classe
if( n<=1 ) throw new HlegalArgumentException(); ExchangerMJ che implementa l’interfaccia Exchanger che segue:
forchetta=new boolean[n];
for( int i=0; i<forchetta.length; i++ ) forchetta[i]=true; package poo.thread.scambiatore;
} public interface Exchanger<T> {
public T exchange( T msg );
public void ottieniForchette( int id ){ }//Exchanger
if( id<0 II id>=forchetta.length ) throw new HlegalArgumentException();
synchronized( lock ){ package poo.thread.scambiatore;
while( !forchetta[id] Il ! forchetta[(id+1)%forchetta.length] ) public class ExchangerMJ<T> implements Exchanger<T>{
private T dato;
try{ private boolean partner = false, rilascio = false;
lock.wait(); private Object lock = new Object();
}catch( InterruptedException ie ){} public T exchanae( T msg ){
forchetta[id]=false; synchronizea( lock t{
while( rilascio )//protezione per “rientro veloce"
forchetta[(id+1)%forchetta.length]=false; try{ lock.wait();} catch(lnterruptedException e){)
} T x=null;
}//ottieniForchette if( Ipartner ){
dato = msg; partner = true;
while( partner ) //attesa arrivo partner
public void rilasciaForchette( int id ){ try{ lock.wait(); }catch( InterruptedException e ){}
if( id<0 II id>=forchetta.length ) throw new HlegalArgumentException(); x = dato; rilascio = false;
synchronized( lock ){ lock.notifyO;
forchetta[id]=true;
else{
forchetta[(id+1)%forchetta.length]=true; x = dato; dato = msg; partner = false; rilascio = true;
lock.notifyAII(); lock.notifyO;
}
}//rilasciaForchette return x;
}//synchronized
}//exchange
}//Tavolo }//ExchangerMJ
Scambiatore sincrono Una fase di sincronizzazione (comunicazione) è sempre chiusa dal processo che arriva per primo. Per evitare
Quando processi produttori e consumatori interagiscono indirettamente mediante un buffer limitato, il sistema malfunzionamenti (si veda anche l’esercizio 2 a fine capitolo) dovuti al rientro “veloce" del processo che arriva
di comunicazione è di tipo asincrono: un mittente deposita il messaggio nel buffer e riprende subito a fare secondo, si è introdotto un ciclo di attesa sulla variabile boolean rilascio che vale true appena si realizza la
altro, senza aspettare che un consumatore prelevi il messaggio. sincronizzazione, e viene posta a false al termine della comunicazione.

Esistono, tuttavia, casi in cui si desidera che produttore e consumatore siano entrambi pronti a comunicare per Classi Produttore e Consumatore ____ ___
scambiarsi un messaggio, e quindi l’interazione dev’essere diretta e non più mediata da un buffer. Si parla in package poo.thread.scambiatore;
questi casi di sistema di comunicazione sincrono o a rendezvous (“stretta di mano") a significare che il primo public class Produttore extends Thread{
processo che arriva “aH’appuntamento" aspetta il partner e quando tutti e due sono pronti, avviene il private Exchanger<String> exch;
trasferimento del messaggio dal mittente al ricevente e subito dopo i due processi riprendono ad eseguire in private int id, delayMax, delayMin, i=0;
concorrenza. private String msg;
Risponde a queste problematiche di comunicazione sincrona il meccanismo Exchanger<T> disponibile, a public Produttore! int id, Exchanger<String> exch, int delayMax, int delayMin ){
partire dalla versione 5 di Java, nel package java.util.concurrent. Un oggetto exchanger va usato tra una this.id=id; this.exch=exch; this.delayMax=delayMax; this.delayMin=delayMin;
singola coppia di processi, es. un produttore e un consumatore. La classe Exchanger ha un solo metodo: }
private void delay(){
T exchange( T msg ) try{ Thread.sleep( (int)(Math.random()'(delayMax-delayMin)+delayMin) );
}catch( InterruptedException e ){}
}//delay
436 43 7
Capitolo 23 Introduzione alla programmazione multi-thread

public void run(){ Consumatore#2 riceve messaggio P#1_1


while( true ){ Produttore# 1 genera messaggio P#1_2
delay(); Consumatore#2 riceve messaggio P#1_2
msg="P#'‘+id+\_“+i; Produttore# 1 genera messaggio P#1_3
System.out.printlnf'Produttore#"+id+’’ genera messaggio ”+msg); Consumatore#2 riceve messaggio P# 1_3
i++; Produttore# 1 genera messaggio P#1_4
exch.exchange( msg ); //trasmissione sincrona di msg Consumatore#2 riceve messaggio P#1_4
} Produttore# 1 genera messaggio P#1_5
}//run Consumatore#2 riceve messaggio P#1_5
Produttore# 1 genera messaggio P#1_6
}//Produttore Consumatore#2 riceve messaggio P#1_6

package poo.thread.scambiatore;
public class Consumatore extends Threadf Si nota come ogni comunicazione (rendezvous) preceda la generazione del prossimo messaggio da parte del
private int id; produttore.
private Exchanger<String> exch;
private int delayMax, delayMin; Thread e sistema delle eccezioni ___
private String msg; Nel metodo run() di un thread possono, naturalmente, essere riportate eccezioni checked/unchecked la cui
public Consumatore! int id, Exchanger<String> exch, int delayMax, int delayMin ){ cattura e gestione può essere affidata al solito a blocchi try-catch. Tuttavia, data la loro particolare natura, i
this.id=id; this.exch=exch; this.delayMax=delayMax; this.delayMin=delayMin; metodi run() non possono sollevare eccezioni checked; inoltre, un’eccezione unchecked riportata in un thread
} ma non catturata e gestita esplicitamente a cura del programmatore, determina la terminazione del thread. È
possibile, tuttavia, prima che il thread muoia, che l’eccezione sia passata ad un gestore (handler) di eccezioni
private void delay(){ che ad es. può emettere qualche messaggio diagnostico e fornire informazioni dettagliate sull’eccezione.
try{ Thread.sleep! (int)(Math.random()‘ (delayMax-delayMin)+delayMin) ); Esiste in proposito l’interfaccia Thread. UncaughlExceptionHandler con il solo metodo:
}catch( InterruptedException e ){}
}//delay void uncaughtExceptionf Thread t, Throwable e ),

public void run(){ che può essere implementata da una classe gestore personalizzato. A partire dalla versione 5 di Java è data
while( true ){ la possibilità di installare un gestore presso un thread particolare col metodo:
msg=exch.exchange(null);//riceve msg e trasmette nuli setUncaughtExceptionHandler(handler), o fissare per tutti i thread un gestore di default con il metodo static
System.out.println(”Consumatore#’,+id+" consuma messaggio "+msg); della classe Thread: setDefaultUncaughtExceptionHandler( handler ). Di seguito si mostra un esempio di
delay(); programmazione di un custom handler e la sua installazione nel thread di un main al cui interno si solleva
un’eccezione runtime di divisione per zero. L’handler fornisce una segnalazione diagnostica mediante un
}//run message dialog di Java Swing (si veda il cap. 11).

}//Consumatore import javax.Swing.*;


public class HandlerDump {
package poo.thread.scambiatore; static class CustomHandler implements Thread.UncaughtExceptionHandler{
public class Scambio { public void uncaughtException( Thread t, Throwable e){
public static void main( Stringo args ){//esempio di main JOptionPane.showMessageDialog! nuli, “Eccezione ’’+e+" nel thread ”+t.getName() );
Exchanger<String> exc = new ExchangerMJ<String>(); }
Produttore p = new Produttore! 1, exc, 1000, 500 ); }//CustomHandler
Consumatore c = new Consumatore! 2, exc, 1000, 500 ); public static void main(String args[]) throws Exception {
p.startQ; c.start(); Thread.currentThread().setUncaughtExceptionHandler( new CustomHandler!) );
}//main System.out.println(1/0);
}//Scambio }//main
}//HandlerDump
Esempio di output; ______
Produttore# 1 genera messaggio P#1_0
Consumatore#2 riceve messaggio P#1_0
Produttore# 1 genera messaggio P#1_1
43 8 439
Capitolo 23 Introduzione alla programmazione multi-thread

Output prodotto. public static void main( String []args ){


l=Collections.synchronizedList( new LinkedList<lnteger>() );
l.add(3); l.add(18); l.add(5); l.add(15);
Thread t=new Thread( new BrokerQ );
t.startQ;
lterator<lnteger> it=/.iterator();
while( it.hasNext() )(
System.out.println( it.next() );
try{ Thread.s/eep(100);}
Concorrenza e collection framework catch(lnterruptedException e){);
Una classe thread-safe introduce gli oneri spazio/tempo legati, ad ogni invocazione di un metodo }
sincronizzatola) aH’ottenimento del lucchetto prima di accedere ai/modificare i dati interni della classe, (b) al }//main
rilascio del lucchetto all’uscita dal metodo, (c) alla gestione in generale del wait-set. Tali oneri sono di norma
inaccettabili, per ragioni di degradazione delle prestazioni di esecuzione, quando la classe è utilizzata in un }//TestConcurrentModification
programma sequenziale, ossia un programma contenente il solo thread associato implicitamente al main.
D’altra parte, un principio ovvio di ingegneria del software è che un cliente utilizzatore non dovrebbe pagare i In questo esempio, l’eccezione può essere evitata innestando, presso il Client, il ciclo di iterazione aH’intemo di
costi d’uso di un meccanismo quando questo meccanismo non è richiesto. un blocco synchronized come segue:

Le considerazioni che precedono giustificano il perché gran parte delle classi del collection framework di Java public static void main( String [jargs ){
siano state progettate e sviluppate in forma non thread-safe. Dunque un oggetto ArrayList, LinkedList etc. non l=Collections.synchronizedList( new LinkedList<lnteger>() );
fornisce al cliente alcuna protezione rispetto agli usi multi-thread. Tuttavia, alcuni servizi sono messi a l.add(3); l.add(18); l.add(5); l.add( 15);
disposizione per consentire di ottenere una versione thread-safe di una classe collezione, ad es.: Thread t=new Thread( new BrokerQ );
t.startQ;
l\s{<\n\eger>h=Co\\ec\\ons.synchronizedUst( new LinkedList<lnteger>() ); lterator<lnteger> it=l.iterator();
synchronized(/){
In questo caso, l’oggetto restituito da new LinkedList<lnteger>() è incorporato (wrapped) in un nuovo oggetto while( it.hasNextQ ){
List i cui metodi sono sincronizzati. Da questo momento in poi, ogni invocazione di un metodo di / si System.out.println( it.nextQ );
accompagna ai costi della sincronizzazione. In modo analogo, si possono utilizzare i metodi di Collections try{ Thread.s/eep(100);}
synchronizedSetQ, synchronizedMap() per sincronizzare un oggetto set o map, o synchronizedCollection per catch(lnterruptedException e)Q;
sincronizzare in generale un oggetto Collection.

L’uso di una collezione sincronizzata, tuttavia, non risolve tutti i problemi che si possono presentare. Ad es., è }//main
sincronizzato l'ottenimento di un iteratore sulla collezione, ma non un’iterazione in quanto tale. Un errore
comune è legato allo scenario di una struttura dati iterabile che viene modificata (ad es. da un altro thread) L’uso del blocco synchronized rende tutta l’iterazione una sezione critica, dunque atomica. Non è più possibile
durante un ciclo di iterazione. In questi casi viene sollevata l’eccezione unchecked per il thread broker effettuare una modifica sulla lista mentre l’iterazione è in corso, in quanto il lucchetto
ConcurrentModificationException. Il seguente programma si interrompe generando appunto una associato ad / è indisponibile. Si nota che la soluzione proposta dipende dal fatto che la sincronizzazione in /
ConcurrentModificationException. Un thread broker può, infatti, modificare la lista mentre è in corso sia basata su this e non su un lucchetto privato. Si osserva inoltre che “sincronizzare troppo” non è mai un
un’iterazione nel main: provvedimento opportuno: la struttura dati / resta bloccata durante tutta l’iterazione, per quanto lunga essa
possa essere (si pensi ad una lista con un numero elevato di elementi, e ad un thread che vorrebbe
import java.util.*; interrogarne la sizeQ durante l’iterazione).
public class TestConcurrentModification {
private static List<lnteqer>/; A ben riflettere non è necessario che sia presente un ambiente multi-thread per il potenziale sollevarsi di una
private static class Broker implements Runnable{
public void run(){ ConcurrentModificationException. Ad esempio, il codice sequenziale che segue (o uno equivalente nel quale
while( true )( un’iterazione, fatta partire, viene temporaneamente interrotta, è seguita da una modifica della collezione,
if( Math.random()<0.5 ){ seguita infine da una ripresa dell’iterazione) genera banalmente una ConcurrentModificationException:
if( l.size()>0 ) l.remove(O); I/O è l'indice del primo elemento
else l.aad( (int)(Math.random()*15) );

}//Broker

440 441
Capitolo 23 Introduzione alla programmazione multi-thread

public static void main( String []args ){ Le collezioni di Java possono generare una vista immutabile del loro contenuto ricorrendo ai servizi della
fcnew LinkedList<lnteger>(); classe di utilità Collections. Ad es. se Is è una lista di interi, l’operazione:
/.add(3); /.add(18); /.add(5); /.add( 15);
lterator<lnteger> it=/.iterator(); List<lnteger> ils=Co\\ec\\ons.unmodifiableList( Is );
while( it.hasNext() ){
System.out.println( it.next() ); trasforma Is in un nuovo oggetto lista ils con lo stesso contenuto di Is, ma read-only, ossia non modificabile.
l.remove(O); Ogni tentativo di chiamata di un metodo su ils (anche attraverso un iteratore) che potrebbe cambiarne lo stato,
solleva un’eccezione di tipo UnsupportedModificationException. Collections espone anche i metodi
}//main unmodifiableSetO, unmodifiableMapO, unmodifiableCollectionO per bloccare le modifiche su un set, una map
o una collection in generale. Ovviamente, una collezione read-only come ils è anche thread-safe.
Si capisce che il progetto di una struttura di iterazione, per essere generale, dovrebbe prendersi cura di
scoprire le situazioni di “modifica concorrente" della collezione mentre è in atto un’iterazione. Tutti gli esempi Variabili volatili ___ _____ __ ___
di iteratori mostrati nei capitoli precedenti assumono l’esistenza di un ambiente sequenziale (mono thread) e Si consideri il problema di due thread t1 e t2 illustrato di seguito. Si vuole garantire che il blocco di azioni A11
l’assenza di modifiche durante un'iterazione (un’ipotesi molto spesso verificata). di t1 preceda quello A22 di t2. Si potrebbe usare un lucchetto ma una soluzione più semplice ed efficiente
esiste e si basa sull’uso di una variabile volatile boolean sync inizializzata a false e condivisa tra t1 e t2.
La classe java.util.Vector<T> Quando t1 ha finito le azioni A11, esso pone a true synch. t2 non passa ad eseguire A22 sino a che
Nel package java.util è presente, sin dalle prime versioni di Java, la classe Vector<T> che, a partire dalla synch=false.
versione 5, implementa anche le interfacce List<T> e lterable<T>. Vector è una classica classe thread-safe di
Java che amministra un array sotto stante scalabile dinamicamente. Si sottolinea che non è una buona norma volatile boolean synch=false;
usare un java.util.Vector<T> in tutti quei casi (applicazioni mono-thread) in cui è utilizzabile senza problemi un t1: t2:
semplice ArrayList<T>. L’uso di Vector<T> non è immune dal problema della A11 A21
ConcurrentModificationException. I metodi tradizionali per elaborare un oggetto Vector<T> sono: synch=true; while( Isynch );
A12 A22
void addElement( elem )
aggiunge elem alla fine del vector Se synch fosse una normale variabile, la soluzione prospettata potrebbe non funzionare. Infatti, il compilatore
T elementAt( indice ) Java, non avendo informazioni sul fatto che synch è utilizzata da t2 ma modificata da t1, potrebbe (dal punto di
ritorna l’elemento alla posizione indice visto di t2) mappare synch su un registro della cpu e siccome il valore iniziale è false, t2 potrebbe entrare in un
Enumeration<T> elementsQ loop infinito, malgrado t1 modifichi synch a true.
ritorna un'enumerazione del contenuto del vector
void removeElementAt( indice ) Dichiarando synch come variabile volatile, si dice al compilatore che essa è affetta asincronamente da
rimuove l’elemento alla posizione indice modifiche da parte di altri thread. In altre parole, il compilatore non può più fare l’ipotesi che il valore di synch
void setElementAt( elem, indice ) visto da t2 resti fissato a false. Ancora: la variabile synch non può essere mappata su un registro: ogni suo
cambia l’elemento alla posizione indice con elem uso deve necessariamente riferirsi alla sua locazione in memoria centrale, dove è contenuto il suo valore
T lastElementf) effettivo.
Ritorna l’ultimo elemento del vector
L’assegnazione ed il test della variabile volatile synch, costituiscono azioni atomiche, pur non essendo
Una Enumeration<T> è simile ad un lterator<T>. I metodi per scorrere un’enumerazione sono: “guardate" da un lucchetto.
boolean hasMoreElements() Un e s e m p i o : ___________________________ ___________________________________________
T nextElemento
Un thread Java di norma non può essere “ucciso" (ossia costretto a terminare) dall’esterno. Esso infatti
potrebbe possedere uno o più lucchetti su strutture dati, per cui è opportuno che questi accessi terminino
Va da sé che attualmente è preferibile, usando un java.util.Vector<T>, fare riferimento ai metodi ben noti normalmente prima di obbligare il thread a finire la sua esecuzione. Per stoppare un thread la soluzione
dell’interfaccia List<T> e a quelli di lterator<T> (si nota che l’interfaccia Enumeration non prevede l’operazione raccomandata consiste nel comunicare al thread, mediante un metodo, che esso dovrebbe uscire “prima
remove()).
possibile” ma lasciare al thread stesso, sulla base del suo stato interno, l’individuazione del momento esatto di
terminazione. Segue un esempio della soluzione, basato sull’uso di una variabile volatile:
Concorrenza e oggetti immutabili
Classi di oggetti immutabili come Razionale (si veda il cap. 3), Monomio (si veda il cap. 16), String etc. hanno public class MioThread extends Thread{
un rapporto favorevole con la concorrenza. Infatti, un oggetto con stato immutabile è automaticamente thread- private volatile boolean richiestaUscita=false;
safe, senza ricorrere ad altri meccanismi (es. metodi/blocchi sincronizzati). Per questa ragione, è sempre
opportuno valutare, durante la fase di progetto di una classe, se essa può assumere il carattere di classe di public void richiestaTerminazione(){ richiestaUscita =true; }//richiestaTerminazione
oggetti immutabili.
442 443
Capitolo23 Introduzione alla programmazione multi-thread

public void run(){ int getAndSetf int vai )


while( IrichiestaUscita ){ atomicamente assegna vai a this e ritorna il valore precedente
azioni void set( int vai )
} atomicamente assegna vai a this
}//ru n String toStringf)
}//MioThread
Si osserva che il metodo get() ha gli stessi effetti di memoria della lettura di una variabile volatile. Il metodo
Nota: vecchi metodi della classe Thread, mantenuti nelle nuove versioni di Java per compatibilità all'indietro, quali stop(), suspendQ set() ha gli stessi effetti di memoria di assegnamento (scrittura) su una variabile volatile. Il metodo
e resume(), sono attualmente deprecati, ossia se ne sconsiglia fortemente l’uso. Il primo fa terminare in modo forzoso un thread. Il compareAndSet() ha gli stessi effetti di memoria di lettura e scrittura su una variabile volatile. Esso ritorna true
secondo sospende l'esecuzione di un thread (quale che sia il suo stato interno). Il terzo fa riprendere un thread sospeso. In realtà il
metodo stop(), a differenza di suspend(), prima libera eventuali lucchetti posseduti dal thread e poi ne forza la terminazione. Ma
se ha successo, ossia il valore trovato è quello atteso. Per altre informazioni si rimanda alle API di Java.
anche cosi facendo possono aversi problemi gravi di inconsistenza sui dati.
Di seguito si mostra una semplice classe Contatore i cui oggetti possono essere acceduti da più thread
A partire dalla versione 5 di Java, le variabili volatili hanno accresciuto il loro significato legandosi al Java contemporaneamente. In alternativa all’uso di metodi o blocchi sincronizzati, siccome un contatore è una
Memory Model. In particolare, gli accessi ad un volatile hanno conseguenze sulla visibilità. Tutte le modifiche semplice variabile intera, la soluzione fa uso di un intero atomico:
a variabili, non solo a synch (si veda il blocco A11 sopra), che precedono l’assegnazione di valore a synch,
sono visibili (cioè sono riflesse in memoria) ad un thread che interroghi il volatile (ad es. in A22 di t2). import java.util.concurrent.atomic.*;
public class Contatore!
L'assegnamento di valore ad un volatile corrisponde all'uscita da un blocco sincronizzato. Similmente, il test di private Atomiclnteger cont=new Atomiclnteger(O);
un volatile, corrisponde all'ingresso in un blocco sincronizzato.
public void incr(){ cont.incrementAndGet();}
Pur essendo variabili con accessi atomici, sussistono dei limiti nell’uso dei volatili. Ad es. se una variabile public void decr(){ cont.decrementAndGet();}
volatile v è numerica, l’assegnamento v=expr non è garantito essere atomico, proprio perché la definizione del public int val(){ return cont.get();}
valore da assegnare è parte della valutazione di un'espressione. In questi casi non si possono utilizzare i }//Contatore
volatili ma si deve ricorrere o a soluzioni classiche basate sulla lock-synchronization o (in situazione semplici)
a oggetti atomici. Nel package java.util.concurrent, Java 5 e le versioni superiori mettono a disposizione una famiglia di classi
(sincronizzatori e collezioni per l’uso multi-thread) per la programmazione concorrente. Ad es., oltre che poter
A partire da Java 5, nel sotto package di java.util.concurrent.atomic sono disponibili diverse classi di oggetti usare il monitor nativo di Java (oggetti lucchetti e metodi/blocchi synchronized con le operazioni
atomici: Atomiclnteger, AtomicBoolean,... AtomicReference, che rappresentano classi thread-safe e lock-free, wait()/notify[AII]()), il programmatore può in alternativa fare uso di semafori (classe Semaphore), o una
ossia non basate sull’uso di lucchetti. Piuttosto, l’implementazione di tali classi si fonda direttamente su versione leggermente più comoda del monitor nativo: la struttura Lock/Condition costituita da oggetti lucchetti
istruzioni macchina presenti nei moderni processori, tipo CAS: compare-and-swap. Ricordando che le espliciti ed associate condition (“dormitori" separati per i thread). Un thread può mettersi in wait su una
istruzioni di macchina (si veda anche l’appendice B) sono atomiche, una tale istruzione consente di interrogare specifica condition con il metodo await(). Il metodo signal() sveglia un thread in wait su una condition, ma non
il valore di una variabile e se questo è trovato uguale ad un valore atteso, assegnare (swap) alla variabile un necessariamente quello che aspetta da più tempo (i risvegli non sono FIFO). Per questa ragione, esiste anche
nuovo valore. Il tutto in modo indivisibile o atomico. la versione signalAII() che risveglia tutti i thread in wait su una condition, lasciando ad essi la responsabilità di
determinare se il risveglio si deve completare o piuttosto essi devono tornare a dormire sulla condition. A titolo
Metodi di Atomiclnteger di esempio, la classe che segue mostra un’implementazione del buffer limitato mediante look e condition, ed
int addAndGetfint vai) associato stile di programmazione (notare che i metodi get()/put() ora non sono più etichettati synchronized).
atomicamente somma vai al valore di this e ritorna il risultato
int getAndAdd(int vai) Buffer limitato con risveg][FIFO, basato su Lock/Condition
atomicamente somma vai al valore di this e ritorna il valore precedente package poo.thread.buffer;
boolean compareAndSetf int expected, int update ) import poo.util.*;
se il valore di this è uguale a expected, esso è cambiato atomicamente a update, e si ritorna true; si ritorna import java.util.*;
false altrimenti import java.util.concurrent.locks.*;
int decrementAndGet()
atomicamente decrementa il valore di this e ritorna il nuovo valore public class BufferLimitatoLC<T> extends BufferLimitato<T>{
int incrementAndGetf) private LinkedList<Thread> codaProduttori=new LinkedList<Thread>();
usa la tua fantasia private LinkedList<Thread> codaConsumatori=new LinkedList<Thread>();
int intValuef), int get() private Lock lucchetto=new ReentrantLock();
ritornano il valore di this private Condition attesaProd=lucchetto.newCondition();
int getAndDecrement() private Condition attesaCons=lucchetto.newCondition();
atomicamente decrementa il valore di this e ritorna il valore precedente
public BufferLimitatoLC( int n ){ super(n); }//BufferLimitatoLC
444 445
Capitolo 23 Introduzione alla programmazione multi-thread

private boolean produttoreDeveDormire(){


if( isFull() Il codaProduttori.getFirst()!=Thread.currentThread() ) return true; public boolean isFull(){
return false; lucchetto.lock();
}//produttoreDeveDormire try{
return super.isFull();
private boolean consumatoreDeveDormire(){ }finally{ lucchetto.unlock();}
if( isEmpty() Il codaConsumatori.getFirst()!=Thread.currentThread() ) return true; }//isFull
return false;
}//consumatoreDeveDormire public int size(){
lucchetto.lock();
public void put( T elem ){ try{
lucchetto.lock(); return super.size();
try{ }finally{ lucchetto.unlock();}
codaProduttori.add( Thread.currentThread() ); }//size
while( produttoreDeveDormire() ){
try{ public void clear(){
attesaProd.await(); lucchetto.lock();
}catch( InterruptedException e ){} try{
} super.clear();
codaProduttori.removeFirst(); }finally{ lucchetto.unlock();}
super.put(elem); }//clear
attesaCons.signalAII();
}finally{ }//Bufferl_imitatoLC
lucchetto.unlock();
Classi collezioni direttamente utilizzabili per la sincronizzazione sono ArrayBlockingQueue<T> e
}//put SynchronousQueue<T>, che implementano l'interfaccia BlockingQueue<T>. ArrayBlockingQueue<T> è uno
standard bounded buffer. SynchronousQueue<T> si comporta come un canale sincrono “rendezvous", simile
public T get(){ alla classe Exchanger<T>. Una classe collezione di rilievo è ConcurrentHashMap che utilizza al suo interno
lucchetto.lock(); una batteria di lucchetti al fine di consentire accessi simultanei a più thread lettori ed ad un numero fissato di
try{ thread scrittori (es. 16).
codaConsumatori.add( Thread.currentThread() );
while( consumatoreDeveDormire() ){ Alcune classi si caratterizzano per il supporto di stili di programmazione concorrente ad un alto livello di
try{ astrazione (es. l'executor framework), in modo da offrire all’utente soluzioni di semplice utilizzo e dotate di
attesaCons.await(); buone prestazioni e trasferire, per quanto possibile, gli oneri della sincronizzazione dall’utente alle classi della
}catch( InterruptedException e ){} libreria.
}
codaConsumatori.removeFirst(); Esercizi ___________
T x=super.get(); 1. Modificare il programma degli N filosofi in modo da evitare anche la starvation. Si suggerisce di introdurre
attesaProd.signalAII(); nella classe Tavolo un array di contatori, con tanti elementi quanti sono i filosofi. Ogni elemento dell'array
return x; conta il numero di volte che un filosofo mangia. Nell’attribuire le forchette (metodo richiediForchette()) si
}finally{ controlla se un filosofo abbia mangiato più di un certo numero di volte (es. 2) rispetto ai due vicini. Se è cosi,
lucchetto.unlockQ; pur essendo disponibili le forchette, il filosofo non le prende per fairness. Si nota che se i è il filosofo corrente,
e la numerazione è al solito quella antioraria, il filosofo alla sua sinistra ha numero (i-1+n)%n, quello alla sua
}//put destra ha numero (i+1)%n, se n sono i filosofi.
2. Individuare i malfunzionamenti della classe ExchangerMJ<T> indotti dalla eliminazione della variabile
public boolean isEmpty(){ rilascio e del ciclo di attesa a protezione di un rientro veloce.
lucchetto.lock(); 3. Perché una classe thread-safe basata sul lucchetto di this è più vulnerabile rispetto ad una classe che
try{ utilizza come lucchetto un oggetto interno incapsulato? Fornire un esempio esplicativo.
return super.isEmpty(); 4. Un conto corrente bancario è descritto dall’interfaccia:
}finally{ lucchetto.unlockj);}
}//isEmpty
446 447
Capitolo 23

public interface ContoBancario{ Appendice A: _________ ____


void deposito( doublé quanto );
void prelievo( doublé quanto );
Rappresentazione in bit delle informazioni
}//ContoBancario
Qualsiasi tipo di informazione, numerica o simbolica, per essere elaborata da un calcolatore richiede di essere
preliminarmente espressa in bit. In questa appendice si esaminano alcune tecniche per rappresentare le
Si desidera implementare una classe ContoBancarioMJ che realizza l’interfaccia di cui sopra, utilizzando il informazioni in bit. L’attenzione è rivolta prevalentemente alle informazioni di tipo numerico.
monitor nativo di Java e obbedendo alle seguenti specifiche. Un conto può essere acceduto
contemporaneamente da più processi. Un processo depositante aggiunge un ammontare al bilancio e può
Sistemi di numerazione p o s i z i o n a l i ______________
eventualmente svegliare qualche prelevante in attesa. Un processo prelevante mira a prelevare un certo
Il problema di rappresentare i numeri in bit rientra in quello più generale della rappresentazione di informazioni
ammontare dal bilancio del conto. Si desidera che i prelevanti vengano serviti in ordine rigorosamente FIFO:
numeriche in un Sistema di Numerazione Posizionale (SNP). Un SNP è caratterizzato dai seguenti elementi:
un prelevante deve sospendersi se l'ammontare richiesto non è disponibile (il bilancio non può andare in
rosso) o, pur essendo disponibile, esiste già qualche altro prelevante in attesa. Occorre evitare di svegliare un
prelevante se non si è sicuri che esso possa riprendere (il suo ammontare è disponibile sul bilancio del conto). • una base B (intero >1);
Programmare la classe ContoBancarioMJ, quindi un generico thread prelevante e uno depositante ed infine • un alfabeto delle cifre AC={0, bi, b2.....be 1} costituito da B simboli cifre denotanti i naturali tra 0 e B-1.
un main che istanzi uno scenario e lo mandi in esecuzione.
5. Risolvere l’esercizio precendente utilizzando la struttura Lock/Condition. Un qualunque numero (intero o reale) può essere rappresentato giustapponendo un certo numero di cifre
6. Mantenendo l’ipotesi di ambiente sequenziale, modificare la struttura di iterazione ad es. della classe prese dall'alfabeto del SNP. Ad esempio, per il numero naturale M si ha:
poo.util.ListaOrdinataConcatenata<T> in modo da sollevare, quando è richiesto, l’eccezione
ConcurrentModificationException. Mb - CnCn 1...C i Cq
7. Considerando che lo stato interno di una classe stazione di monitoraggio si riduce al contatore intero dei
veicoli, sviluppare una classe concreta StazioneAtomica che implementa l’interfaccia Stazione e realizza il dove ogni cifra è dotata di un peso che è una potenza della base B che dipende dal posto occupato dalla cifra
contatore veicoli con un Atomiclnteger di java.util.concurrent.atomic. nel numero. La cifra Ci, che occupa il posto i-esimo, i posti essendo contati da 0 dalla destra del numero, è
provvista di un peso pari a B1. Il valore di Mb è dato da:
Altre letture
Mb = Cn*Bn + Cn i*Bn ’ + ... + CTB1 +...+Co*B°
Lo studio sistematico e approfondito delle classi di java.util.concurrent esula dagli scopi di questo testo. Oltre
che sulle API di Java, maggiori dettagli si possono trovare ad es. su:
Più in generale, un allineamento illimitato del tipo:
C. S. Horstmann, G. Cornei: Core Java, Voi. I-Fundamentals, 8,h Edition, Prentice Hall, 2008.
CnCniCn2-.C 2C 1C0.C 1C 2...
B. Goetz, J. Bloch, J. Bowbeer, D. Lea, D. Holmes, T. Peierls: Java concurrency in practice, Addison Wesley,
rappresenta il numero reale espresso in base 10 dato da:
2006.
R,0=Cn‘ Bn+Cn i ‘ Bn 1+...+C,B’+C0B°+C ,*B ’+C ?‘ B 2+...

Esempi:
135,o= 1**102+3*10+5
2378= 2*82+3*8+7= 159,0
1011,o= 1*103+1*10+1
10112 = 1*23+1*2+1 =23+2+1 = 11,o

Sistemi ricorren t i : ______________________ ___ ________


1) B=10 Sistema Decimale: AC={0,1,2,3,4,5,6,7,8,9}
2) B=2 Sistema Binario: AC={0,1}
3) B=8 Sistema Ottale: AC={0,1,2,3,4,5,6,7}
4) B=16 Sistema Esadecimale: AC={0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F} //A rappresenta 10, B 11 etc.

Conversioni di base di numeri naturali ___________________________

1) Da base B/10 a base 10


Un numero espresso in una base B/10 si può convertire in base 10 utilizzando direttamente lo sviluppo
polinomiale:
448 449
Appendice A Rappresentazione in bit delle informazioni

Quoziente 16 Resto
3 A 5 i 6 = 3 * 1 6 2 + A * 1 6 + 5 = 3 * 1 6 2+ 1 0 * 1 6 + 5 = 3 * 2 5 6 + 1 6 0 + 5 = 9 3 3 , 0 172 C
10 A
1 1 2 3 4 = 1 * 4 3+ 1 * 4 2+ 2 * 4 + 3 = 9 1 , 0 0

1 1 0 1 1 2 = 1 * 2 4+ 1 * 2 3+ 1 * 2 + 1 = 2 4+ 2 3+ 2 + 1 = 2 7 ,o dunque 17 2 i 0= A C ,6 - Si lascia come esercizio del lettore la conversione di altri numeri in base 10 nelle basi 2,
4, 8 e 16.
Si nota che nel caso di un numero espresso in bit è sufficiente sommare le potenze di 2 corrispondenti agli 1
presenti. Conversione di una frazione decimale in una base B*10
Nella base d'arrivo B la frazione sarà ancora tale ma va prefissato il numero di cifre frazionarie desiderate. Se
2) Da base 10 a base B / 10 questo numero è s si ha, approssimativamente:
È opportuno esaminare separatamente i numeri naturali e le frazioni pure. Successivamente si ha l'algoritmo
per convertire un numero reale. Sia N 10 un numero naturale e sia B / 1 0 la base destinazione. Quello che si sa F = C 1C 2 C 3 ...C $
è che il numero sarà espresso da una giustapposizione di cifre, da determinare, come segue:
ossia F = C ,‘ B '+C ?*B 2+...+C S*B s
N ,o = C kC k iC k - 2 - ..C iC o tale che N = C k * B k+ C k , * B k '+ . . . + C , * B 1+ C o * B ° .
È facile verificare che se si moltiplica la frazione per la base B si ottiene:
Al fine di calcolare le cifre C,, conviene mettere in evidenza B nell'espressione polinomiale. Si ha:
F*B = C., + C.2*B ’ + C.3*B? + ... + C s* B<s 1>
N = ( C k* B k ’ + C k , * B k 2 + . . .+ C i) * B + C o
Dunque, C , è la parte intera del prodotto F*B. Sostituendo ad F la frazione residua e continuando nei prodotti
Interpretando l'ultima identità si vede subito che la quantità entro parentesi (...) è il quoziente della divisione per B e prendendo di volta in volta le parti intere, si trovano nell'ordine C 2, C 3 ... e cosi via.
intera di N per la base B, mentre C o è il resto di questadivisione. Pertanto, eseguendo una prima divisione
intera tra N e Be prendendo il resto si ottiene la cifra meno significativa C o. A questo punto il procedimento si Per convertire una frazione decimale F in una base B? 10 ad s cifre, si applica il metodo delle moltiplicazioni
può iterare considerando l'espressione tra parentesi e mettendo nuovamente in evidenza la base B. Si ha: ripetute per B. La parte intera di ogni prodotto fornisce una cifra, dalla più significativa in poi. La frazione
residua va considerata ai fini della prosecuzione dell'algoritmo che termina allorquando sono state generate
Q = (C k * B k 2+ C k i* B k 3 + . . .+ C 2 ) * B + C i tutte le s cifre desiderate.

da cui si vede che C 1 è uguale al resto della divisione intera tra il quoziente ottenuto al passo precedente e la Come esempio si consideri la trasformazione di 0.3,0 in base 7 a 5 cifre. Ci si può organizzare come segue:
base B. Emerge chiaramente l'algoritmo di conversione:

Per convertire un numero Nto in una base destinazione B/10, è sufficiente applicare il metodo delle divisioni Frazione *7 Parte Intera
ripetute per B. Ad ogni passo il resto della divisione denota una cifra. Il quoziente rappresenta la quantità da 03 2
dividere per la base al passo successivo. Al primo passo il quoziente è proprio il numero N. Il metodo va 0.1 0
iterato sino all'ottenimento di quoziente nullo. I vari resti sono le cifre della rappresentazione cercata, dalla 07 4
cifra meno significativa a quella più significativa. 0.9 6
0.3 2
Si consideri la conversione in bit del numero 1 3 io . Ci si può organizzare come segue: 0.1
pertanto
Quoziente 2 Resto 0.3io= 0.20462?. Si nota che il periodo di 0.3 in base 10 è 0, mentre in base 7 è 2046. Volendo trasformare
13 1 0.52,o in base 2 a 6 cifre, si ha:
6 0
3 1 Frazione *2 Parte Intera
1 1 0.52 1
0.04 0
0
0.08 0
0.16 0
prendendo i resti dall'ultimo verso il primo si ha: 13i0 = 11012. Come altro esempio, si converte 172io in base 0.32 0
16. 0.64 1
0.28

Il lettore è invitato ad auto-esercitarsi.


450 451
Appendice A Rappresentazione in bit delle informazioni

Ogni cifra ottale richiede tre bit per essere rappresentata. Si consideri ora un numero N espresso in ottale (se
Codifica indiretta in bit_________ espresso in decimale, occorre prima convertirlo in ottale col metodo diretto). Sia N=23 io=278. La codifica
Il metodo visto in precedenza si dice metodo di codifica diretta in bit, o metodo di codifica naturale. Esso si diretta in bit di N e la sua codifica indiretta sono
basa sui concetti dei sistemi di numerazione posizionali. In realtà esiste una maniera alternativa di procedere,
che non considera pesi e sviluppo polinomiale. Per capire di cosa si tratta, si consideri il sistema decimale. 23i0 = 10 1112 codifica diretta
Evidentemente è possibile codificare in modo diretto in bit le singole cifre. Occorrono in generale Iog2 l 0 = 3.35 27e = 0 1 0 1 1 1 2 codifica indiretta
bit per esprimere una cifra decimale. In sostanza 4 bit. Infatti, con quattro bit si hanno 24=16 combinazioni
distinte di 4 bit di cui 10 assegnabili per codificare le cifre decimali. Le due rappresentazioni sono perfettamente coincidenti. Questa è una proprietà delle basi potenze di 2. In
altre parole:
Assegnato un numero in base 10, si può derivare una sua codifica indiretta in bit sostituendo ogni cifra ad es.
con il quartetto di bit della sua codifica in binario puro. codificare un numero in una base potenza di 2 (2,4,8,16) è equivalente a codificarlo direttamente in bit.
Cifra Codice Naturale Viceversa, data una codifica in bit, è possibile scriverla in forma compatta utilizzando una base potenza di 2
Decimale
0 0000
superiore (tipicamente 8 0 16) raggruppando dalla destra i bit a k a k (rispettivamente k=3 e k=4) e sostituendo
1 0001 al gruppetto la cifra ottale od esadecimale corrispondente:
2 0010
3 0011 10 110 111 111 1012 = 267758
4 0100
5 0101
6
10 1101 1111 11012 = 2DFDi 6
0110
7 0111
8 1000 Altri codici BCD
9 1001 Le cifre decimali possono essere codificate mediante stringhe di lunghezza (minima) 4 bit. Esistono numerosi
codici BCD (Binary Coded Decimals) utilizzabili per effettuare la codifica indiretta in bit partendo dalla
235i0—> 0010 0011 01012 rappresentazione decimale. Il più diffuso è quello detto 8-4-2-1 che rappresenta le cifre decimali secondo il
codice binario puro. Spesso utilizzato è il codice Eccesso-3. Altri codici BCD possono basarsi su più di 4 bit.
Si nota subito che malgrado la "codifica", ogni cifra originaria mantiene la sua individualità nella Essi aggiungono dei bit di ridondanza per la protezione dagli errori che si possono verificare durante il
rappresentazione finale: il primo quartetto è 2, il secondo è 3 ed il terzo è 5. Si può dire che la codifica indiretta trasferimento dell'informazione, ad es. lungo una linea di trasmissione. La tabella che segue riassume alcuni
in bit mantiene la codifica decimale. Sulla codifica indiretta non è possibile applicare lo sviluppo polinomiale: codici BCD. Il codice Gray è caratterizzato dalla proprietà che stringhe di bit di cifre consecutive differiscono in
per passare alla rappresentazione in cifre decimali, si raggruppano i bit a quattro a quattro e si utilizza la una sola posizione, Il codice Eccesso-3 aggiunge s stematicamente 3 al codice naturale della cifra
tabella di conversione. considerata.
Cifra 84 -2-1 Eccesso-3 Gray
Dato uno numero N, la sua codifica diretta in bit ed una sua codifica indiretta sono rappresentazioni distinte e 0 0000 0011 0000
scorrelate. 1 0001 0100 0001
2 0010 0101 0011
Tutto ciò ha ripercussioni sui circuiti aritmetici della CPU. Se la rappresentazione in bit è quella naturale 3 0011 0110 0010
(normalmente utilizzata dalle macchine) allora i circuiti lavorano in aritmetica binaria. Se, invece, la macchina 4 0100 0111 0110
adotta la rappresentazione indiretta, di fatto i circuiti aritmetici funzionano in decimale. 5 0101 1000 0111
6 0110 1001 0101
È interessante adesso esaminare la codifica indiretta in bit di un numero espresso in una base potenza di 2 (2, 7 0111 1010 0100
4,8,16 ...). Per semplicità si consideri il sistema ottale: 8 1000 1011 1100
9 1001 1100 1101
Cifra Ottale Codice Naturale
0 000
Alcuni codici BCD
1 001
2 010
3 011
Stringhe di bit di lunghezza n
4 100 È utile osservare che una stringa di n bit può codificare in generale 2n oggetti distinti. Infatti, 2n sono le
5 101 configurazioni che si possono formare con n bit (disposizioni con ripetizioni di 2 oggetti, 0 e 1, su n posti).
6 110 Identificando ogni configurazione con il relativo intero si ha:
7 111

452 453
A p p en d ic e A Rappresentazione in bit delle informazioni

0 0000...0 Rappresentazione dei numeri negativi


1 0000 ... 1 Si è visto che con n bit sono rappresentabili i numeri naturali tra 0 e 2n-1. Metà dei naturali possono essere
utilizzati, secondo vari metodi, per codificare numeri negativi.
2n-1 1111...1
Rappresentazione per segno e modulo
Le 2n configurazione (da tutti 0 a tutti 1) corrispondono ai numeri naturali tra 0 e 2n-1. Associando ciascuna Il primo bit è dedicato al segno, con la convenzione che 0 indica + e 1 indica - . I restanti n-1 bit sono a
configurazione ad un diverso oggetto in uno spazio di al più 2n oggetti si ottiene un codice. disposizione per codificare il valore assoluto di un numero naturale compreso tra 0 e 2n1-1. L'intervallo
ammesso per gli interi risulta essere dunque:
Aritmetica binaria
Le regole per l'effettuazione delle operazioni aritmetiche in base 2 sono quelle stesse della base 10, qui [-(2"M ), 2n1-1]
particolarizzate al caso binario. Di seguito si riportano le tabelle dell'addizione, sottrazione e moltiplicazione tra
due bit. Se n=4, l’intervallo di definizione è [-7, +7].

0 1 Tale metodo di rappresentazione, scarsamente utilizzato, risulta utile per l'effettuazione di operazioni tipo e
addizione a
0 00 01 in quanto è facile applicare la regola dei segni per determinare il segno del risultato. Come inconveniente si
1 01 10 nota l'esistenza di una doppia rappresentazione per lo zero.
a+b
a+b=xy, x bit di riporto, y bit somma Rappresentazione per complementi diminuiti
I numeri positivi si lasciano inalterati. Quelli negativi si rimpiazzano con i corrispondenti complementi diminuiti.
b Dato un numero i<0, dicesi complemento diminuito (o ad 1) di i la quantità:
0 1
sottrazione a 0 00 01 c1(i)=2n-lil-1=(2n-1)-lil
1 11 00
b-a Ad esempio, se n=4, c1(-5)=(24-1)-5=10 -» 1010
b-a=xy x bit di prestito, y bit differenza
Tenendo conto che quale che sia n, la quantità 2n-1=111 ...1, si ha che 24-1 =1111, per cui:
b
0 1 c1(-5)= 1111-101 = 1010
moltiplicazione a 0 0 0
1 0 1 Regola mnemonica: si considera la rappresentazione ad n bit di lil e si invertono i vari bit (0 diventa 1 ed 1
a*b diventa 0):

5 = 0101 -> c1 (-5) = 1010


La somma tra due bit 1 dà 2, ossia 0 col riporto di 1. Similmente 0-1 comporta il prestito di una unità dalla cifra
a sinistra. Tale unità vale 2 sulla cifra attuale e dunque il bit differenza è 1 e sussiste prestito sulla cifra La funzione c1 è ben definita nel dominio seguente:
seguente. Sulla scorta di queste tabelle è agevole effettuare operazioni aritmetiche nel sistema binario.
Esempi: [-(2"M ), 2 "1*5-1]

Se n=4, l'intervallo di definizione di c1 è (-7, +7]. Per le proprietà della rappresentazione per complementi
11 <r- l ipi>r I I diminuiti, risulta che:
01101 + <=> 13 +
110 = 6
10011 19 • il primo bit conserva il significato di bit segno
• per cambiare segno ad un numero occorre invertire tutti i bit della rappresentazione
1 1 ■<- 1 1r c .s f /' t i • esiste ancora una doppia rappresentazione per lo zero.
110 0 - <=> 1 2 -
001 1 = 3 Rappresentazione per complementi alla base
1001 9 I positivi si lasciano inalterati. I negativi si rimpiazzano con i corrispondenti complementi alla base. Dato un
numero i<0, dicesi complemento alla base (o a 2) di i la quantità:

c2(i)=2n-lil

454 455
Appendice A Rappresentazione in bit delle informazioni

Ad esempio, se n=4,
R. per seqno e modulo R. per c1 R. per c2
c2(-5)=24-5=11= 1011.
+7 0111 0111 0111
Il dominio della funzione c2 è il seguente: +6 0110 0110 0110
+5 0101 0101 0101
[-2 « \2 "M ] +4 0100 0100 0100
+3 0011 0011 0011
Se n=4, l'intervallo di definizione di c2 è [-8,+7], Se n=16, l'intervallo è [-32768, +32767] etc.
+2 0010 0010 0010
Per costruire il complemento a 2 di un numero negativo, si può derivare quello diminuito e sommare 1 a +1 0001 0001 0001
quest'ultimo: +o 0000 0000 0000

c2(-3) = c1(-3)+1; 3=0011 => c1(-3)=1100 -0 1000 1111


-1 1001 1110 1111
c2(-3) = 1100+1= 1101 -2 1010 1101 1110
-3 1011 1100 1101
È agevole verificare la seguente regola mnemonica per il calcolo del complemento a 2 di i: si considera la
rappresentazione ad n bit di lil. Di essa si lasciano inalterati gli ultimi bit zero meno significativi compreso il -4 1100 1011 1100
primo 1, si complementano tutti gli altri. Ad esempio: -5 1101 1010 1011
-6 1110 1001 1010
3=0011 -» c2(-3)= 1101 -7 1111 1000 1001
-8 1000
in quanto non esistono zeri meno significativi e quindi si lascia inalterato solo l'ultimo 1.
Rappresentazioni a confronto per n=4
c2(-4) -> 4=0100 —> 1100 e cosi via.
Un metodo per rivelare i bit di un intero:__________________________________________
Risulta che: Si mostra un metodo che ritorna la rappresentazione per complementi a 2 di un intero Java.

• il primo bit conserva il significato di bit segno public static int[] int2bit( int x ){
• esiste una sola rappresentazione per lo zero int[] bit=new int[32|;
• la rappresentazione è sbilanciata: esiste un negativo in più per cui un cambiamento di segno può for( int j=0; j<bit.length; j++ ) bit[j]=0;
generare traboccamento (overfloW). int i=bit.length-1, q=Math.abs(xj;
do{ //converti in bit il valore assoluto di x
La rappresentazione per complementi a 2 è quella normalmente adottata all'atto pratico. Essa, tra l'altro, bit[i]=q%2;
consente di giustificare l'intervallo di definizione dei tipi interi di un linguaggio ad alto livello come Java. q=q/2; i~;
}while( q!=0 );
La rappresentazione per complementi a 2 semplifica i circuiti aritmetici di una CPU. Per gli scopi attuali è il( x<0 ){//ricostruisci il complemento a 2
sufficiente osservare che l'operazione di sottrazione si può ricondurre alla somma del minuendo più il c2 del int j=bit.length-1;
sottraendo. In concreto, sempre con n=4, si consideri l'operazione 5-2. Si ha: while( j>0 && bit[j]==0 ) j- ; //trova posizione j primo 1 meno significativo
//commuta tutti i bit a sinistra di j
for( int k=j-1 ; k>=0; k - ) bit[k]=(bit[k]==0)?1:0; (*)
5-2=5+c2(-2) => 0101 +
1110 =
return bit;
1 0011=3 }//int2bit

Più rapidamente si potrebbe utilizzare il metodo static String toBinaryString( int ) di Integer. Utile è anche
in cui il bit di trabocco si può ignorare. In un registro di macchina ad n bit resta comunque il risultato corretto
lnteger.toHexString( in t) che ritorna la stringa esadecimale dell’intero ricevuto parametricamente.
della sottrazione. Tutto ciò è vero in generale. Se n1 ed n2 sono due numeri già codificati in c2,
n1-n2=n1+c2(n2). Nella tabella che segue si riassumono i tre tipi di rappresentazione dei numeri interi relativi
per n=4.
456 457
Appendice A Rappresentazione in bit delle informazion[

Operatori su interi a livello di bit


L’operazione sulla linea (*) del metodo int2bit(), che commuta i bit da 0 ad 1 e viceversa, potrebbe essere x = x I (1 « n );
anche espressa come: bit[k]=(bit[k]+1)%2.Infatti, se un bit è 1, sommandogli 1 diventa 2 e quindi il resto della
divisione intera per 2 è 0. Se invece il bit è 0, sommando 1 si ottiene 1 ed il resto della divisione intera per 2 è Si vuole testare se l’n-esimo bit di x è 1:
1.
if( ((x & (1 « n ))» n )==1 )...
Esiste un’altra possibilità per esprimere il bit toggling-. fare l'or esclusivo con 1. L’or esclusivo (XOR) in Java è
denotato dal simbolo A. Esso è definito come segue: Si nota che eseguire x « n equivale a moltiplicare x per 2n. Similmente, x » n equivale a dividere x per 2n.
x « 3 5 equivale a x « 3 (il numero di spostamenti è calcolato modulo 32 per int, modulo 64 per long).
0*0=0,0*1=1,1*0=1,1*1=0
Sviluppo polinomiale e formula di Horner
ossia vale 1 quando i due bit sono diversi, 0 se sono uguali. Dunque si può scrivere nel ciclo di for (*): Si è visto che per convertire un numero naturale N da base B^10 a base 10, è sufficiente procedere con lo
bit[k]=bit[k]*1. sviluppo polinomiale: ckBk+Ck iBk 1+...+CiB+c0 All’atto pratico, per calcolare il valore di questo polinomio non è
necessario calcolare separatamente le potenze Bk. Infatti, mettendo ripetutamente in evidenza la base B lo
In realtà sia bit[k] che la costante 1 sono interi a 32 bit di cui solo l’ultimo bit è significativo, tutti gli altri sono 0. sviluppo polinomiale può essere scritto equivalentemente come segue:
L’operazione bit[k]*1 calcola un int (dunque 32 bit) i cui bit sono il risultato dell’or esclusivo bit-a-bit tra bit[k] e
1. Si potrebbe dichiarare l’array ritornato dal metodo int2bit() come byte[] per ridurre lo spazio di memoria. ((....(0‘ B+Ck)‘ B+Cki)‘ B+Ck2)*B+...)*B+Ci)*B+Co
Meglio ancora è un array di boolean. Si nota che oltre agli operatori &&, Il e !, sui boolean è anche disponibile
che è la formula di Horner. Nell’ipotesi di aver generato una stringa di bit di un numero intero positivo in un
array, il metodo che segue realizza appunto la conversione del numero da base B=2 a base 10:
Se x ed y sono due interi, allora x & y restituisce un nuovo int i cui bit sono ottenuti facendo la AND bit-a-bit tra
x ed y: 0&0=0, 0& 1=0, 1&0=0, 1&1=1. Similmente, x I y restituisce un intero i cui bit sono ottenuti facendo la public static int decimale( int[] bit ){
OR bit-a-bit tra x ey: 010=0, 011=1,110=1,111=1. int val=0, B=2;
for( int i=0; kbit.length; i++ ) vai = vai * B + bit[i);
Esempio: return vai;
}//decimale
00110110.. ..10110011 & 00110110....10110011 I
01010001. . ..00110011 = 01010001....00110011 = Formato Floating Point IEEE 754 _____
A) Singola precisione (float di Java, circa 6-7 cifre decimali di precisione), 32 bit.
00010000 . ...00110011 01110111 ...... 10110011
n T I TTt I i i t i ii i r i i i i i i i i i i i i i i
00110110....10110011 * 0 1 2 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 25 27 28 29 30 31

01010001....00110011 = s E F

01100111....10000011 Questo standard non usa i concetti di rappresentazione per complementi a due né per l'esponente né per la
mantissa o parte frazionaria.
Operatori di shift
Esistono tre operatori di shift sulla configurazione di bit di un intero Java: « , » , » > . Ogni operatore si Il primo bit è il bit segno (S) di tutto il numero reale. I bit da 1 ad 8 (byte) codificano l’esponente in forma
accompagna alla specifica del numero n di scorrimenti: x shift n. polarizzata. La costante di polarizzazione è 127. Si intende che all’esponente effettivo occorre sommare 127.
Il numero cosi ottenuto (positivo o nullo) si conserva nel campo E. Quando poi si deve usare il valore, si
x « n scorre a sinistra il contenuto di x di n posti, entra 0 ad ogni shift nella posizione meno significativa, e ricostituisce l’esponente effettivo, sottraendo 127 al campo E. I bit da 9 a 31 codificano la parte frazionaria o
ritorna la stringa di bit risultante. mantissa del numero reale, in versione normalizzata. La normalizzazione significa che il numero reale va
espresso (in bit) come segue:
x » n scorre a destra il contenuto di x, n volte, propagando il bit segno di x, e ritorna la stringa di bit
risultante. l.abcdefg...

x » > n scorre a destra x n volte, entra 0 nella posizione più significativa di x, e ritorna la stringa di bit prima del punto decimale deve esserci un 1, abcdefg... sono cifre binarie dopo la virgola. Per consentire
risultante. maggiore precisione, N intero prima del punto decimale non viene memorizzato. Inoltre non viene
memorizzato nemmeno il carattere punto decimale. Dunque nei bit da 9 a 31 (campo F) si riportano
Esempio: dato l'intero x si vuole porre 1 nel suo n-esimo bit contato da destra (il bit meno significativo conta (eventualmente sfruttando la periodicità della frazione decimale) i bit dopo il punto decimale.
0):
458 459
Appendice A n i p p r v l t n u u M i f m Off ( W » m f o r i m i w v

Vediamo un esempio. Si vuole trasformare nel formato FP-IEEE 754 singola precisione, il numero positivo ri denormalizzati ___
23.85. iato denormalizzato viene usato quando il numero reale è troppo piccolo per essere normalizzato. Si
di numeri reali inferiori a 1.0x2126. In formato denormalizzato verrà rappresentato ad esempio il numero
Il bit segno è S=0 (positivo). (2 129. Tale numero sarebbe già normalizzato come frazione, ma l’esponente polarizzato darebbe
+127=—2 ossia un numero negativo, inaccettabile. Si procede cosi: si riduce la frazione in modo da
Si trasforma in bit la parte intera in modo unsigned: 23io=101 H 2. Per quanto riguarda la frazione 0.85io si ha: re ad un esponente pari a -127: 0.0101x2127*a questo punto l’esponente polarizzato diviene 0 e la
ne memorizza 010100000...0. Il risultato è un numero denormalizzato.
0.85io=0.1101100110...=0.110110 dove 0110 è il periodo in base 2 della frazione (in base 10 il periodo è 0).
:izi___________________________________________________________________________________
Mettendo insieme parte intera e parte frazionaria si ha: 10111.110110 e normalizzando: to n=9 bit, specificare l’intervallo di rappresentazione dei numeri interi relativi (a) per segno e modulo, (b)
omplementi diminuiti, (c) per complementi alla base (c2). Per i numeri i1=-247i0, i2=179io, dire se essi
1.0111110110 x 24 rappresentabili o meno per complementi alla base a n=9 bit. Se si fornire il c2 di il e di i2. Quindi
jire in aritmetica binaria, sui numeri in c2, le operazioni i1-i2, -i1+i2, Ì1+Ì2, specificando e motivando ogni
L'esponente da memorizzare è dunque: 127+4=131 io=01111111+100=1000001 12 uale problema di trabocco del risultato, adottando il punto di vista della macchina,
me 1. quando n=13 bit, i1=3785io, i2=-2857io.
In definitiva, la rappresentazione FP-IEEE754 di 23.85 è la seguente: ineralizzare il metodo decimale() in modo da trattare con una stringa di bit contenente il complemento a 2
intero qualsiasi.
0 1 | 3 | 0 | 0 | 0 | C | 1 |1
y p N r r " T F T -
1 | 0 | 0 | . |1 | 0 | c |1
r r n o n i 1 i 3 1 1 jrnire la rappresentazione floating point IEEE 754 a singola precisione dei seguenti numeri reali:
0 1 2 3 4 5 6 7 3 9 iQ 11 12 13 14 15 16 17 18 19 20 21 22 23 2< 25 25 27 28 29 30 31

C E F
175.39io, r2=379.73io, r3=—153. 157io- Riportare la rappresentazione sia in formato 32 bit che in codice
ecimale equivalente.
Si nota che il periodo 0110 è riutilizzato ripetutamente al fine di riempire i bit sino alla posizione 31. Per quanto
riguarda l’ultimo bit, esso dovrebbe essere 0 in quanto è il primo bit del gruppo periodico. Siccome però si
taglia il periodo in un bit che è seguito da un 1, per ragioni di precisione si somma 1 alla mantissa (e si
propaga il riporto) dopo di che l’ultimo bit diventa, in questo caso, 1 e null’altro cambia.

In esadecimale, tutto il numero float è equivalente a: 41BECCCDi6 Se il numero fosse stato -23.85, allora
sarebbe cambiato solo il bit segno che anziché essere 0 sarebbe stato 1.

B) Doppia precisione (doublé di Java, circa 14-15 cifre decimali di precisione), 64bit.

In modo analogo si può costruire la rappresentazione floating point di 23.85 in doppia precisione:

bit 0: Segno S (0 positivo, 1 negativo)


bit 1-11: Esponente E ad 11 bit, polarizzato, con costante di polarizzazione 1023
bit 12-63: Frazione F a 52 bit, normalizzata

Si nota che la costante di polarizzazione per un esponente a k bit è data da: 2k M .

Casi particolari
Riferiamoci al formato in singola precisione.

E=0 e F=0 si specifica lo zero. Attenzione che esistono +0 e -0


E=0 e F!=0 si specifica che il numero reale è denormalizzato (si vedapiù avanti)
E=FF e F=0 si specifica l’infinito (00). Attenzione che esistono +°o e
E=FF e F!=0 si specifica un numero indefinito denotato come NaN (Not a Number).

Casi in cui si genera un NaN includono: sqrì( numero negativo), °o~ etc. Il carattere NaN di un risultato si può
interrogare cosi: if( Double.isNaN( Math.sqrt(-2) ) ... Le classi Float e Doublé esportano le costanti static
POSITIVEJNFINITY e NEGATIVEJNFINITY.

460 461
Appendice B:_____
La macchina RASP
Si propone una macchina didattica basata sul modello di Von Neumann con l'obiettivo di chiarire
l’organizzazione interna ed il funzionamento di un tipico calcolatore. Un calcolatore può essere visto come un
sistema capace di:

• ricevere
• trasmettere
• memorizzare
• elaborare
informazioni.

Per realizzare questi compiti risulta dotato di unità di ingresso, d'uscita, di memorizzazione e di elaborazione.
La macchina RASP (Random Access Stored Program) costituisce un'astrazione di un calcolatore reale. Il suo
schema funzionale è mostrato nella figura che segue. Essa prende dati da un nastro d'ingresso; emette dati su
un nastro di uscita. Esiste un unico organo di memoria interna M. I nastri di ingresso/uscita e la memoria sono
supposti di lunghezza infinita ed organizzati come sequenze di celle.
Nastro d'ingresso

Architettura della macchina RASP

Ogni cella (o registro) può memorizzare un numero intero. I dati ammessi sono esclusivamente interi. Mentre i
nastri d'ingresso/uscita sono acceduti in forma sequenziale (dopo una lettura/scrittura la testina avanza di una
cella sul nastro di ingresso/uscita), la memoria interna M è acceduta in forma casuale, ossia in ogni momento
si può accedere ad una sua qualunque cella. Ad ogni cella di memoria è associato un numero d'ordine, a
partire da 0, che ne denota univocamente la posizione.Tale numero costituisce [indirizzo della cella e va usato
ogni volta che si intende accedere in lettura o scrittura alla cella. L'accesso in lettura ritorna l'intero contenuto
nella cella. L’accesso in scrittura sostituisce l'informazione precedente con un nuovo intero. La scrittura è
distruttiva. Il tempo di accesso ad una qualunque cella è costante e si parla anche di memoria RAM (Random
Access Memory).

L'unità di elaborazione (detta unità centrale o CPU o processore) è quella che presiede al funzionamento
complessivo della macchina. Essa è capace di eseguire algoritmi formulati in termini di istruzioni di macchina
(operazioni primitive della macchina RASP), che rispondono al formato illustrato di seguito:

463
A p p en d ic e B La macchina RASP

Istruzioni di ingresso/uscita
Codice Operativo Modo Operando

Mnemonico Significato
Formato Istruzione RASP
READ x M[x]< dato prossima cella in ingresso
Il campo Codice Operativo denota un'operazione da compiere; il campo Operando indica un dato coinvolto
READ @ x M [M [x]]<-dato prossima cella in ingresso
nell'operazione. Il campo Modo specifica la modalità da utilizzare per il reperimento dell’operando. Molto
spesso un operando è contenuto o va depositato in una cella di memoria M. Si tratta della modalità di accesso
A) Lettura di un dato dal nastro di ingresso
diretto alla memoria.
Mnemonico Significato
Un esempio di istruzione RASP è riportato di seguito:
WRITE # x prossima cella in uscita«-x
READ 10 WRITE x prossima cella in uscita* M(x]

READ comanda la lettura del contenuto della prossima cella del nastro di ingresso e la memorizzazione di tale WRITE @ x prossima cella in uscita* M[M[x]]
valore nella cella di memoria di indirizzo 10. In generale, un'operazione si esplica su due operandi e definisce B) Scrittura di un dato sul nastro di uscita
un risultato. Tipico esempio è un’istruzione aritmetica esprimibile in linea di principio come segue:
Istruzioni di spostamento
ADD 10 20 30 A) Caricamento dell’accumulatore
che richiede di sommare i due operandi contenuti rispettivamente nelle celle di memoria agli indirizzi 10 e 20 e Mnemonico Significato
di conservare il risultato nella cella di indirizzo 30. All’atto pratico si preferisce adeguare le istruzioni ad un
unico formato come quello presentato in precedenza. Pertanto l'effetto del comando “ADD 10 20 30" viene LOAD # x ACC<—x
ottenuto utilizzando più istruzioni, ciascuna delle quali riferisce un solo operando. Esiste a questo scopo un LOAD x ACC<—M[x]
meccanismo implicito di specificazione di un operando rappresentato dalla cella accumulatore. L’istruzione
LOAD @ x ACC<—M(M[x]]
"ADD 10 20 30" viene ottenuta in RASP in tre passi come segue:

LOAD 10 B) Memorizzazione del contenuto dell’accumulatore


ADD 20
Mnemonico Mnemonico
STORE 30
STORE x M[x]<—ACC
LOAD carica inizialmente l’operando nella cella 10 nell’accumulatore. ADD somma al dato contenuto STORE @ x M[M[x]]<—ACC
nell’accumulatore il dato della cella 20 e lascia il risultato nell'accumulatore. STORE, infine, salva il risultato
nella cella 30.
Operazioni aritmetiche
L'accumulatore è sorgente e/o destinazione di molte operazioni. Svolge pertanto un ruolo chiave
nell'esecuzione delle istruzioni di macchina. Nei calcolatori reali sono di norma presenti uno o più registri
accumulatori. Nell'unità di elaborazione è presente una (sotto)unità aritmetica (ALU o Arithmetic Logic Unit) Mnemonico Significato
che è responsabile dell'effettuazione concreta dell'operazione evocata da una istruzione. Nei primi esempi O p A rU x ACC <-ACC o p a r x
discussi, il campo operando dell'istruzione coincide con un indirizzo di memoria (indirizzamento diretto alla
memoria). In altri casi l'operando può essere immediato, ossia l'istruzione contiene direttamente l'operando. In O pArx A CC <-ACC o p a r M[x]
altri ancora, il campo operando contiene l'indirizzo di una cella di memoria dentro cui è contenuto l'indirizzo O pAr @ x ACC <-ACC o p a r M[M[x]]
finale dell'operando. Si tratta del cosiddetto indirizzamento indiretto. Queste tre modalità di selezione
dell'operando (metodi di indirizzamento) sono specificate dal campo Modo (di indirizzamento). dove l'operatore aritmetico O p A r può
valere;
Di seguito si descrivono le istruzioni della macchina RASP, organizzate per categoria. La notazione M[x] ADD (addizione)
indica il contenuto della cella di memoria di indirizzo x. I simboli # e riportati immediatamente dopo il codice SUB (sottrazione)
operativo, specificano rispettivamente l'operando immediato e l'indirizzamento indiretto. L’indirizzamento
MUL (moltiplicazione)
diretto è implicito. La notazione dest<-dato esprime la scrittura di dato nella cella destinazione).
DIV (divisione)

464 465
Appendice B La macchina RASP

di altre istruzioni nel programma. Per favorire la leggibilità si usano liberamente dei commenti sulle linee
Controllo del flusso di esecuzione istruzioni; il introduce un commento che finisce alla fine della linea.

Mnemonico Significato Legge due interi e scrive il maggiore:____________________________________________________________


JUMP x IP<- X
"READ--------------- 1 ------------------ ; MI1] <— primo dato
A) Salto incondizionato READ ~T~ ; M[2] <- secondo dato
LOAD ; ACC <- M[1]
TUE ~T~ ; ACC <—ACC - M(2]
Mnemonico Significato JGZ PRIMO ; M[1] > M[2] ?
"WRITE ~2 ; no! scrive M[2]
J condizione x If ACC soddisfa la condizione Then
IP< X TJUMP FINE""
dove condizione può essere:
PRIMO: WRITE ~T~ ; si! scrive M[1]
Z (zero, ACC=0) Else
GZ (maggiore di zero, ACC>0) IP < -IP + 1 T IR E HALT ; termina l’algoritmo
GEZ (maggiore o uguale a zero, ACC>=0) Endlf
LZ (minore di zero, ACC<0) Si è fatto uso di etichette simboliche (PRIMO, FINE) per specificare la destinazione di istruzioni di salto.
LEZ (minore o uguale a zero, ACC<=0)
NZ (non zero, ACC<>0) Legge 10 interi e ne calcola e scrive somma:________________
B) Salto condizionato dal valore dell’accumulatore
Si leggono uno alla volta i dati in ingresso. Ogni numero letto viene sommato ad una somma parziale che alla
fine conterrà la somma totale. Il programma ammette un gruppo di istruzioni ripetute 10 volte (ciclo). Si tratta
Mnem onico Significato delle istruzioni da quella etichettata CICLO: JZ FINE al JUMP CICLO. La cella 1 contiene un contatore
inizializzato a 10 e decrementato dopo ogni lettura. La cella 2 è utilizzata per accumulare la somma. Essa è
HALT termina l ’ esecuzione dell'algoritm o posta a 0 fuori ciclo e ad ogni iterazione è aggiornata con la somma al suo contenuto del nuovo numero letto.
C) Terminazione
T M 5 1 ------------ "5--------------------
Una certa attenzione dovrebbe essere prestata alle operazioni aritmetiche. Ad esempio, essendo l’aritmetica STORE 2 ; M[2] <- 0
intera, l’operazione di divisione (DIV) calcola il quoziente intero. Inoltre, la non commutatività di SUB e DIV LOAD# 10
obbliga a predisporre rispettivamente il minuendo ed il dividendo nell’accumulatore. L'istruzione HALT consiste STORE 1 ; M[1j <— 10
del solo codice operativo. CICLO: JZ FINE ;M[1) = 0?
READ 3 ; no legge numero in M[3)
Nelle istruzioni di salto, x denota l'indirizzo di una istruzione nel programma. Un registro usato al lato sinistro di LOAD 2
< - denota la destinazione di un dato; al lato destro di <- denota il suo contenuto. ADD 3
STORE 2 ; M[2l <- M(2] + M|3)
La macchina RASP, come tutti i calcolatori alla Von Neumann, lavora a programma memorizzato. All'inizio LOAD 1
essa va dotata di un algoritmo (o programma) e dei relativi dati, i quali vanno memorizzati (non importa come SUB# 1
in questo momento) nella memoria interna. Un programma è una sequenza di istruzioni di macchina. STORE 1 ; M[ 1] <— M[ 1] -1
JUMP CICLO
Esempi di algoritmi RASP FINE: WRITE 2 ; scrive M[2]
Esaminando le istruzioni della macchina RASP ci si rende conto che esse sono in numero limitato ed hanno HALT
un contenuto operativo elementare. Tuttavia sussiste il seguente risultato generale:
Rappresentazionejn memoria dhun programma
un qualunque problema risolubile mediante un algoritmo, può essere risolto automaticamente da una
macchina tipo RASP dotandola di un corrispondente programma. La macchina RASP lavora a programma memorizzato. Tuttavia le celle di memoria possono contenere solo
numeri interi, mentre gli esempi di algoritmi presentati ammettono una prevalenza di simboli mnemonici,
Di seguito si considerano alcuni semplici problemi ed un possibile algoritmo RASP. L'obiettivo è familiarizzare utilizzati tanto per le etichette delle istruzioni di salto quanto per i codici operativi. In realtà un vero programma
con l'uso delle istruzioni di macchina. Riguardo al posizionamento del programma in memoria, esso è oggetto in linguaggio macchina è interamente numerico. L’uso di simboli mnemonici è stato preferito in quanto facilita
di una decisione non specificata. Ciò ha ripercussioni sulle istruzioni di salto che necessitano di riferire indirizzi la leggibilità dei programmi. Al fine di chiarire una possibile rappresentazione numerica dei programmi RASP
ci si può riferire indicativamente alle tabelle che seguono.

466 467
Appendice B La macchina RASP

Codice Codice Programma in linguaggio macchina numerico:


Operativo Operativo
Simbolico Numerico
Indirizzo Codice
READ 10 Macchina
WRITE 11
100 1200
LÓAD 12
101 1312
STORE 13
102 12010
ADD 14
103 1311
SUB 15
104 181113
MUL 16
105 1013
DIV 17
106 1212
JZ 18
JNZ 19 107 1413
JLZ 20 108 1312
JLEZ 21 109 1211
Modo Simbolico Modo Numerico
JGZ 22 110 1501
im m ediato (#) 0
JGEZ 23 111 1311
diretto 1
JUMP 24 112 241104
indiretto (@) 2
HALT 25 113 1112
114 25
Codici operativi numerici Codici dei modi di indirizzamento

Un codice operativo è supposto codificato con un intero positivo a due cifre. Un modo è invece espresso con Per comodità si è fatto uso della base 10 e non della base 2 per esprimere le quantità numeriche.
una sola cifra numerica. Un'istruzione RASP è immagazzinabile in una cella di memoria giustapponendo i
numeri che esprimono il codice operativo (2 cifre) il modo di indirizzamento (1 cifra) e l'operando (un Nomi simbolici e notazione Assembler __ ___
qualunque intero). Osservando la tabella del codice macchina numerico si può meglio apprezzare l’utilità della notazione
simbolica nella scrittura di programmi al livello di macchina. Un programma numerico risulta illeggibile e la
Con l’ausilio delle tabelle dei codici operativi e dei modi è possibile convertire un programma RASP in veste probabilità di commettere errori provando a scrivere direttamente in veste numerica è alta (le cose peggiorano
totalmente numerica purché si stabilisca a priori l’indirizzo di partenza (origine) del programma. In quanto se si utilizzano bit e non cifre decimali).
segue si considera nuovamente il programma relativo alla sommatoria dei primi 10 numeri letti da input,
nell’ipotesi che l’indirizzo origine sia 100. La collocazione in memoria del programma consente subito di Si chiama livello assembler il linguaggio macchina utilizzato simbolicamente.
“risolvere" le etichette: esse coincidono con gli indirizzi di memoria delle relative istruzioni etichettate: CICLO
corrisponde all'indirizzo 104, FINE si identifica con l'indirizzo 113. Il “vero" programma in linguaggio macchina Mentre l’uso di simboli per le etichette ed i codici operativi è stato già esemplificato, il problema che resta è la
è riportato subito dopo. specificazione simbolica degli operandi.

Programma somma di 10 interi collocato in memoria a partire dall’Indirizzo 100; Si può intanto osservare che è una forzatura collocare a priori i dati in celle di indirizzi prefissati. In realtà ciò
che occorre è, ad esempio, riservare una cella per la somma, un’altra per il contatore e cosi via,
Indirizzo Etichetta Codice Operativo Operando indipendentemente dal valore preciso degli indirizzi. D'altra parte, allocare “a mano” i dati in celle prescelte
100 LOAD # può comportare il rischio di sovrapposizioni se un certo indirizzo, per errore, viene utilizzato per più scopi
101 STORE 2 (esempio, collisione tra zona istruzioni e celle dati).
102 LOAD # 10
103 STORE 1 Si comprende pertanto che esistono vantaggi a denotare i dati con nomi simbolici, con ripercussioni positive
104 CICLO: JZ FINE sulla leggibilità del programma, lasciando ad un momento successivo la definizione degli indirizzi. Scegliendo
105 READ 3
di indicare con SOMMA, NUMERO e CONTATORE tre celle di memoria dedicate a contenere i dati del
106 LOAD IT -
problema sulla somma dei numeri, si può riscrivere il programma in assembler RASP come riportato di
107 ADD 3
seguito. Evidentemente esiste una corrispondenza uno-ad-uno tra istruzioni assembler e istruzioni di
108 STORE [2
macchina. I soli operandi numerici che restano in assembler RASP sono gli operandi immediati. Il
109 LOAD 1
110 SU B# 1
procedimento di sostituzione di nomi simbolici (di codici operativi, di indirizzi e di operandi) con valori numerici
111 STORE 1
può essere svolto automaticamente da un programma traduttore (detto assemblatore) che utilizzando le
112 JUMP CICLO
tabelle dei codici operativi e dei modi in veste numerica, s'incarica di fissare l'identità delle celle per i dati
113 FINE WRITE 2 simbolici e stabilisce l’indirizzo origine del programma. Assegnando tali compiti all’assemblatore si evitano i
114 HALT rischi di sovrapposizione di indirizzi cui si è accennato sopra.

468 469
Appendice B La macchina RASP

Programma assemb er per la somma di 10 interi: qualsiasi programma ripetendo sino all'istruzione HALT il ciclo interpretazione dell'istruzione riassunto di
seguito.
LOAD# 0
STORE SOMMA ;SOMMA<-0 Ciclo istruzione della CPU
LOAD# 10
STORE CONTATORE ;CONTATORE<- 10 1. CIR<-M[IP]; preleva istruzione corrente (Fetch)
CICLO: JZ FINE 2. IP<-IP+1 ;avanza a prossima istruzione
READ NUMERO 3. Decodifica CIR ;se richiesto, ottiene operando in accordo al modo di
LOAD SOMMA indirizzamento
ADD NUMERO 4. Esegui Codice Operativo ;esegue istruzione corrente (Execute)
STORE SOMMA ;SOMMA<—SOMMA+NUMERO 5. Ritorna al passo 1.
LOAD CONTATORE
SUB# 1 Fondamentalmente la CPU itera due fasi: Fetch (prelievo istruzione corrente) ed Execute (esecuzione
STORE CONTATORE :CONTATORE<-CONTATORE-1 istruzione). La fase di Fetch copia la prossima istruzione del programma, puntata da IP, nel registro CIR. A
JUMP CICLO partire da CIR la CPU è poi in grado di “spacchettare" i vari campi componenti l'istruzione. Ad esempio, in
FINE: WRITE SOMMA accordo alle tabelle dei codici operativi e dei modi, le prime due cifre di CIR forniscono il codice operativo, la
HALT terza cifra il modo se il codice operativo lo prevede. A questo punto, come parte della decodifica del contenuto
di CIR, la CPU preleva l’operando eventualmente coinvolto nell’istruzione applicando il metodo di
indirizzamento specificato dal campo modo. Ottenuto l’operando, si passa all’esecuzione vera e propria
A questo punto è utile riflettere sul fatto che l’adozione del livello assembler introduce qualche vincolo sulla attualizzando il codice operativo. Si nota che l'incremento di IP al passo 2. consente di farlo puntare
programmazione. Un assemblatore RASP può richiedere all’utente di dimensionare una sequenza ad esempio anticipatamente alla prossima istruzione, in accordo ad un’esecuzione strettamente sequenziale.
di 100 elementi, in modo che solo al più 100 elementi potranno essere elaborati e non una quantità di dati Occasionalmente l'ordine sequenziale può essere abbandonato, sulla base del verificarsi di una certa
arbitraria limitata solo dalle dimensioni della memoria M, come sembrerebbe plausibile accedendo alle celle condizione o incondizionatamente, mediante un'istruzione di salto. Per esemplificare, si consideri la situazione
con gli indirizzi fisici o numerici. seguente, relativa ad un fetch dall’indirizzo 100:

A questo scopo è utile la pseudo-istruzione RES(erve che consente di riservare un ammontare contiguo di IP 101
celle di memoria. Ad esempio, JG Z 500
CIR
A: RES 10 ACC 15

istruisce l'assemblatore a riservare per A un blocco (array) di 10 celle consecutive. A è l’indirizzo base di Fetch istruzione dall’indirizzo 100 che contiene JGZ 500.
questo blocco, coincidente con l'indirizzo del primo elemento. Gli indirizzi delle singole celle possono essere
costruiti aggiungendo ad A degli spiazzamenti come segue: IP punta già a 101, dunque è posizionato per prelevare, al termine dell’elaborazione dell'istruzione corrente,
l’istruzione nella cella 101. L'istruzione corrente in CIR è un salto all’Indirizzo 500 condizionato da un valore
indirizzo Ai=A + i, V i e [0..9] positivo nell’accumulatore. Siccome ACC contiene 15, l’esecuzione dell’istruzione JGZ comporta che l’indirizzo
500 (operando di JGZ) sia forzato in IP. Come conseguenza, la prossima istruzione verrà prelevata non da
L’istruzione: 101 ma dalla cella 500. Naturalmente, nel caso il contenuto dell’accumulatore non fosse stato positivo,
l’istruzione di salto non avrebbe alterato il valore di IP.
LOAD# A
Altri registri di calcolo interni alla CPU, non visibili dall'utente, possono esistere per usi temporanei nel corso
carica nell’accumulatore l’indirizzo A, non il contenuto del primo elemento del blocco. Si tenga presente che in dell’effettuazione di operazioni aritmetiche etc. Il ciclo istruzione è ancora un algoritmo. Esso è cablato nella
una macchina come RASP dati e indirizzi sono indistinguibili. Si tratta sempre e comunque di numeri interi. macchina, ossia nei suoi blocchi costituenti, ed eseguito automaticamente.

Interpretazione di un programma in linguaggio macchina Come ultima osservazione, non di poca importanza, si nota che l'esecuzione di un'istruzione di macchina
La CPU della macchina RASP è in grado di eseguire automaticamente un programma caricato in memoria. costituisce un'attività atomica: una volta iniziata, essa viene completata prima che la cpu possa occuparsi di
Fondamentali sono al riguardo le due celle di supporto (si riveda l’architettura della macchina RASP) IP altro (es. il programma corrente possa essere interrotto).
(Instruction Pointer, o puntatore alla prossima istruzione, altre volte riferito come Program Counter o contatore
di programma) e CIR (Current Instruction Register, o registro istruzione corrente) dell'unità centrale.
Inizialmente in IP viene posto l'indirizzo della prima istruzione. Successivamente esso viene aggiornato in
modo da contenere sempre l'indirizzo della prossima istruzione da processare. La macchina RASP esegue un
470 471
Appendice B La macchina RASP

Strumenti Esempi di programmi in Assembler RASP:


È stato implementato in Java (si veda il cap. 21) un traduttore (assemblatore) per il linguaggio Assembler 1) ;Legge due numeri e calcola e scrive la loro somma
RASP che converte un programma simbolico in una versione numerica di macchina. Un programma tradotto è READ A
eseguito da un interprete che realizza, in software, la macchina RASP. READ B
LOAD A
Sintassi EBNF di Assembler RASP ADD B
STORE C
<programma> ::= <dichiarazionixistruzioni> WRITE C
<dichiarazioni> ::= {<dichiarazione>} HALT
<dichiarazione> ::= <etichetta> RES <interosenzasegno><finelinea>l<commento><finelinea>
<istruzioni> ::= (<istruzione>) 2);Legge un vettore di 5 elementi, somma gli elementi e stampa la somma
<istruzione>::= (<etichetta>]<codiceoperativo> [<modo>] A: RES 5
[<operando>] [<commento>] <finelinea> I <commentoxfinelinea> LOAD# A
<codiceoperativo> ::= STORE IND
LOAD I STORE I READ I WRITE I ADD I SUB I MUL I LOAD# 0
DIV I JZ I JNZ I JGZ I JGEZ I JLZ I JLEZ I JUMP I HALT STORE I
<modo> ::= # I @ CICLOA:
<finelinea> ::= \n LOAD I ;ciclo di lettura del vettore A
<etichetta> ::= <identificatore> : SUB# 5
<operando> ::= <intero> I <identificatore> JZ FINEA
<commento> ::= ; (<carattere>) READ@ IND
<interosenzasegno> ::= <cifra> (<cifra>) LOAD 1
<intero> ::= [<segno>] <interosenzasegno> ADD# 1
<segno> ::= + I - STORE I
<identificatore> ::= <lettera> {<lettera>kcifra>l_l$} LOAD IND
<lettera> ::= alblcldlelflglhliljlklllmlnlolplqlrlsltlulvlxlylwlzl ADD# 1
AIBICIDIEIFIGIHIIIJIKIUMINIOIPIQIRISITIUIVIXIYIWIZ STORE IND
<cifra> ::= 01112I3I4I5I6I7I8I9 JUMP CICLOA
<carattere> ::= come da alfabeto ASCII FINEA:
LOAD# A
I simboli non-terminali sono racchiusi tra < e >. I simboli terminali sono indicati normalmente. Il meta-simbolo STORE IND
::= si legge: “è definito come”. Le quantità racchiuse entro ( e } si intendono ripetibili 0, 1 o più volte. Le LOAD# 0
quantità racchiuse entro ( e ) si intendono opzionali: possono esserci o mancare. Il meta-simbolo I separa STORE 1
simboli alternativi. STORE SOMMA
CICLO:
II linguaggio è case-sensitive e i codici operativi vanno scritti in maiuscolo. Si ammette una definizione di LOAD 1
identificatore prossima a quella di Java. Le pseudo-istruzioni RES(erve consentono di riservare blocchi SUB# 5
contigui di celle di memoria (array) con un’assegnata dimensione. Le normali variabili sono dichiarate JZ FINE
implicitamente in seguito al primo uso. Si intende che l’assemblatore riserva ad ogni variabile una cella di LOAD SOMMA
memoria il cui indirizzo è sconosciuto al programmatore. Naturalmente, anche se inutilmente, si potrebbero ADD@ IND
adoperare delle RES anche per variabili singole. STORE SOMMA
LOAD 1
L’assemblatore controlla la sintassi e la semantica. Un controllo semantico ha a che fare con l'uso del modo di ADD# 1
indirizzamento. Ad esempio, alcuni codici operativi non possono applicarsi ad operandi immediati (es. READ o STORE I
STORE). LOAD IND
ADD# 1
Per migliorare la leggibilità dei programmi assembler sono utili i caratteri tabulatori per separare un elemento STORE IND
sintattico dal successivo (es. dopo una etichetta o dopo un codice operativo etc). JUMP CICLO
FINE:
WRITE SOMMA
HALT
472 473
Appendice B La macchina RASP

T( blocco ) =
3) ;Legge un vettore sino al primo negativo, e scrive il contenuto inverso del vettore T(H)
A: RES 10 ;dimensione massima ammissibile T(I2)
LOAD# A
STORE AIND T(ln)
LOAD# 0
STORE N In altre parole, la traduzione di un blocco consiste nella successione ordinata delle traduzioni delle singole
LETTURA: istruzioni componenti.
READ DATO ;ciclo di lettura vettore A
LOAD DATO A s s e g n a z io n e :________________________________________________________________________
JLZ INVERSIONE
LOAD N T (v= e xpr)=
SUB# 10 T(expr) ; si suppone che la valutazione di expr lasci il risultato in ACC
JZ LETTURA STORE v
LOAD DATO
STORE® AIND Istruzione if:
LOAD AIND
ADD# 1 T( if( cond ) blocco; ) =
STORE AIND T( cond ) ; si suppone che la valutazione di cond lasci il risultato in ACC
LOAD N 3 false FINEIF
ADD# 1 T( blocco )
STORE N FINEIF:
JUMP LETTURA
INVERSIONE: in cui J false indica una istruzione di salto che fa saltare se la condizione dell’if è falsa.
LOAD N
JZ FINE Istruzione if-else:
LOAD AIND
SUB# 1 T( if( cond ) blocco 1; else blocco2; ) =
STORE AIND T(cond )
WRITE® AIND J false ELSE
LOAD N T( blocco 1 )
SUB# 1 JUMP FINEIF
STORE N ELSE:
JUMP INVERSIONE T( blocco2 )
FINE: FINEIF:
HALT
Come nel caso precedente, si valuta prima la condizione, quindi se essa è falsa si salta alla parte else
Traduzione di un algoritmo Java in Assembler RASP (blocco2) dopo di che si esce dall’if. Se, invece, la condizione è vera, si esegue il blocco 1 e quindi si esce
Per facilitare la scrittura di programmi assembler RASP, può essere conveniente progettare l'algoritmo ad alto (ovviamente occorre saltare, in questo caso, la parte else).
livello in Java (parte creativa) e quindi procedere manualmente a convertire l’algoritmo in RASP sulla base di
alcune regole di traduzione, richiamate di seguito. Diciamo T() una funzione che traduce costrutti Java (in Istruzione while: ______
pratica un sotto insieme delle istruzioni Java) in costrutti Assembler RASP.
T( while( cond ) blocco; ) =
Blocco: WHILE: T(cond)
Un blocco è una sequenza di istruzioni Java racchiuse in una coppia di parentesi graffe: {11, I2, I3, .... In}. J false FINEWHILE
Come caso particolare, un blocco può anche ridursi ad una sola istruzione, non necessariamente avviluppata T( blocco )
entro { e }. JUMP WHILE
FINEWHILE:

474 475
Appendice B La macchina RASP

Istruzione do-while:_________ L'algoritmo conta per ogni elemento a[i) la sua frequenza (variabile f). Quindi confronta la frequenza ottenuta
con la frequenza massima corrente e se maggiore aggiorna la moda e la sua frequenza.
T( do{ blocco Jwhile(cond); ) =
DO: T( blocco ) .legge un vettore di voti e trova la moda
T( cond ) ;int[] v=new int[5]; //esempio
J false FINEDO ;for(int i=0; kv.length; i++){
JUMP DO ; read voto
FINEDO: ; if( votoci8 II voto>30 ) System.exit(-1);
; v[i]=voto;
Istruzione for: ;}
;int moda=0, fmax=0;
T( for( iniz; cond; passo ) blocco; ) = ;for( int i=0; kv.length; i++){
T( iniz ) ; if( v[i)!=moda )(
FOR: T( cond ) ; int f=0; //conta frequenza di v[i]
J false FINEFOR ; for( int i=i; jcv.lenqth; i++ )
T( blocco ) if( vli]==v[i] ) f++;
T( passo ) ; if( f>fmax ){
JUMP FOR ; fmax=f; moda=v(i];
FINEFOR: ; if( fmax>v.length/2 ) break;
; }
Si nota che le azioni di inizializzazione sono compiute una volta per tutte prima di eseguire il ciclo. Ad ogni
iterazione, similmente ad un ciclo di while, se la condizione è falsa si salta alla fine del for. Diversamente, si
esegue il corpo del for (blocco), quindi il passo e infine si procede con una nuova iterazione. scrivi moda
V: RES 5
Array di interi e accesso agli elementi: LOAD# 5
STORE VLENGTH
int[] a = new int[10]; LOAD# 0
for( int i=0; i<10; i++ ) leggi il valore di a[i|; STORE I
LETTURA:
si può tradurre come segue: LOAD I
SUB VLENGTH
A: RES 10; riserva un blocco di 10 JZ FINELETTURA
LOAD# 0 READ VOTO
STORE I ; inizializzazione del for LOAD VOTO
FOR: SUB# 18
LOAD I JGEZ AVANTI
SUB# 10 salutazione condizione JUMP FINE
JZ FINEFOR AVANTI:
LOAD# A LOAD VOTO
ADD I SUB# 30
STORE Al ; a[i] JLEZ MEMORIZZA
READ® Al ;fine corpo del for JUMP FINE
LOAD I MEMORIZZA:
ADD# 1 LOAD# V
STORE I ; fine passo ADD I
JUMP FOR STORE VI
FINEFOR: LOAD VOTO
STORE® VI
Caso di studio LOAD I
ADD# 1
Si legge un campione di voti universitari, quindi si desidera trovare e scrivere la moda, ossia il voto più ripetuto
STORE I
neH’insieme. Si riporta un possibile algoritmo risolutivo in Java e quindi la sua traduzione in Assembler RASP.
476 477
A p p en d ic e B La macchina RASP

JUMP LETTURA JLEZ CONTINUA


FINELETTURA: JUMP FINEFOR1
LOAD# 0 CONTINUA:
STORE MODA LOAD I
STORE FMAX ADD# 1
STORE 1 STORE I
LOAD VLENGTH JUMP FORI
DIV# 2 FINEFOR1:
STORE META WRITE MODA
FORI: FINE: HALT
LOAD 1
SUB VLENGTH Si lascia come esercizio il verificare la correttezza della traduzione e l'esecuzione del programma assembler
JZ FINEFOR1 RASP risultante.
LOAD# V
ADD I Istruzione di chiamata a metodo:
STORE VI T(m (p1,p2,p3, ...))=
LOAD® VI pus/?# CONTINUA ; etichetta/indirizzo di ritorno
SUB MODA push p1
JZ CONTINUA push p2
LOAD# 0 push p3
STORE F
LOAD 1 JUMP M
STORE J CONTINUA:
FOR2:
LOAD J Corpo di un metodo:
SUB VLENGTH T( m )=
JZ FINEFOR2 M:
LOAD# V T( blocco-corpo-di-m );
ADD J
STORE VJ Istruzione di ritorno da un metodo:
LOAD® VI T( return )=
SUB® VJ pop params
JNZ FINEIF pop indirizzo di ritorno in RETADD
LOAD F JUMP@ RETADD
ADD# 1
STORE F Per supportare le chiamate a metodi è conveniente introdurre uno stack in cui memorizzare l’indirizzo di
FINEIF: ritorno e i parametri. In pratica, sullo stack si può realizzare l’area dati del metodo. Durante l’esecuzione del
LOAD J metodo i parametri possono essere mantenuti e acceduti direttamente sullo stack. Al tempo di ritorno dal
ADD# 1 metodo, si elimina l’area dati, ossia si eliminano le celle dei parametri, quindi si copia in una variabile locale,
STORE J es. RETADD, l’indirizzo di ritorno, togliendolo dallo stack; si inserisce (se esiste) in cima allo stack il valore
JUMP FOR2 restituito dal metodo, infine si comanda con modalità di indirizzamento indiretto il salto tramite RETADD.
FINEFOR2:
LOAD F Indirizzo di ritorno
SUB FMAX parami
JLEZ CONTINUA
LOAD F paramN
STORE FMAX
Struttura area dati di un metodo
LOAD® VI
STORE MODA
È necessario introdurre un’etichetta all’istruzione successiva alla chiamata a metodo, da utilizzare come
LOAD FMAX
indirizzo di ritorno. Il nome del metodo può corrispondere naturalmente ad una etichetta alla prima istruzione
SUB META
del corpo del metodo.
478 479
Appendice B La macchina RASP

Lo stack si può dichiarare come un array dimensionato "generosamente”. Occorre mantenere l’indicazione ADD# 2
dell’estremo libero dello stack, ad es. mediante una variabile tipo stack size (SS), che memorizza il numero di STORE SS ; aggiorna SS
elementi dello stack. JUMP FATT
FINEFATT:
Traduzione in assembler di un metodo ricorsivo LOAD# STACK
;void main(){ ADD SS
; int num; SUB# 1
; read num STORE TOP
; if( numcO ) exit(-1); LOAD® TOP ; POP risultato
; int ris=fatt( num ); STORE RIS
; scrivi ris; LOAD SS
;}//main SUB# 1
;int fatt( int n ){ STORE SS ; fine POP
; //pre: n>=0 SCRIVI:
; if( n<=1 ) return 1; WRITE RIS
; return n'fatt( n-1 ); HALT
;}//fatt
;FATT(N) ricorsivo
STACK è un array gestito a stack. Memorizza le aree dati delle chiamate ricorsive. FATT:
SS è la stack size (inizializzata a 0). In ogni momento ind(STACK)+(SS-1) è il top element (ultimo parametro). LOAD# STACK
Invocazione metodo: sullo stack si pone prima l'indirizzo di ritorno quindi i parametri. ADD SS
Corpo del metodo: lavora sui parametri che restano sullo stack, e su variabili “locali". SUB# 1
Terminazione metodo: si preleva l'indirizzo di ritorno dallo stack (ad SS-(n+1) rispetto a ind(STACK)) STORE TOP
e lo si salva in una variabile locale es. RETADD LOAD® TOP
si pone il risultato (se esiste) al posto dell'indirizzo di ritorno sullo stack STORE N
si decrementa SS di n (in modo da eliminare i parametri) e si comanda la restituzione del controllo. SUB# 1
Il traboccamento dello stack comporta la scrittura di -1 e la terminazione del programma. JGZ RICORSANE
STACK: RES 200 ;N<=1
LOAD# 200 LOAD# STACK
STORE DIM ADD SS
LOAD# 0 SUB# 2 ; seleziona cella indirizzo di ritorno
STORE SS ; Stack Size STORE RA
READ NUM LOAD@ RA
LOAD NUM STORE RETADD ; salva indirizzo di ritorno
JLZ NEGATIVO LOAD SS
;chiama FATT SUB# 2
LOAD SS STORE SS ; fine POP indirizzo di ritorno
ADD# 2 ; dimensione area dati LOAD# STACK
SUB DIM ADD SS
JGZ STACKOVERFLOW STORE TOP
LOAD# STACK LOAD# 1
ADD SS STORE@ TOP ; PUSH 1 sullo stack al posto dell'indirizzo di ritorno
STORE TOP LOAD SS
LOAD# FINEFATT ADD# 1
STORE® TOP ; PUSH indirizzo di ritorno STORE SS ; fine PUSH risultato
LOAD TOP JUMP® RETADD
ADD# 1 RICORSIONE:
STORE TOP LOAD SS
LOAD NUM ADD# 2 ; dimensione area dati
STORE® TOP ; PUSH NUM SUB DIM
LOAD SS JGZ STACKOVERFLOW
480 481
Appendice B La macchina RASP

LOAD# STACK Traduzione in assembler di un metodo tail recursive


ADD SS Di seguito si considera il calcolo del massimo comun divisore con l'algoritmo di Euclide, come esempio di un
STORE TOP metodo tail recursive (si riveda il cap. 18). La traduzione proposta implementa direttamente la tail recursion nel
LOAD# FINERICORSIONE senso che tutte le chiamate ricorsive, dalla seconda in poi, riutilizzano l’area dati della prima chiamata. Solo i
STORE® TOP; PUSH indirizzo ritorno parametri sono ricaricati nell'area dati mentre l’indirizzo di ritorno rimane quello originale per il ritorno
LOAD TOP immediato al main. È facile verificare che l’efficienza dello sfruttamento della tail recursion ne avvicina la
ADD# 1 complessità spazio/tempo a quella di una corrispondente versione iterativa del metodo. Per semplicità, si
STORE TOP dimensiona lo stack in modo da contenere una sola area dati di MCD e non si controllano le situazioni di
LOAD N traboccamento.
SUB# 1
STORE® TOP ; PUSH di N-1 void main(){
LOAD SS int x, y;
ADD# 2 read x;
STORE SS ; fine PUSH read y;
JUMP FATT if( x<=0 II y<=0 ) exit(-1);
FINERICORSIONE: int ris=mcd(x,y);
LOAD# STACK scrivi ris;
ADD SS }
SUB# 1 Algoritmo di Euclide per mcd
STORE TOP int mcd( int n, int m ){
LOAD® TOP if( m==0 ) return n;
STORE RIS ; POP risultato ricevuto return mcd( m, n%m );
LOAD SS }
SUB# 1
STORE SS ; fine POP STACK:: RES 3 ; dimensione d
LOAD# STACK LOAD# 0
ADD SS STORE SS ; Stack Size
SUB# 1 ; preleva parametro N READ X
STORE TOP READ Y
LOAD® TOP LOAD X
MUL RIS JLEZ ERROR
STORE RIS LOAD Y
EXIT: JLEZ ERROR
LOAD# STACK MCD
ADD SS LOAD# STACK
SUB# 2 ADD SS
STORE RA STORE TOP
LOAD® RA LOAD# FINEMCD
STORE RETADD STORE® TOP
LOAD RIS LOAD TOP
STORE® RA ; sostituisci l'indirizzo di ritorno con il RIS ADD# 1
LOAD SS STORE TOP
SUB# 1 LOAD X
STORE SS STORE® TOP
JUMP® RETADD LOAD TOP
NEGATIVO: ADD# 1
HALT STORE TOP
STACKOVERFLOW: LOAD Y
WRITE# -1 STORE® TOP
HALT LOAD SS
ADD# 3
482 483
La macchina RASP
A p p en d ic e B

STORE SS MUL M
JUMP MCD ; prima chiamata STORE TMP
FINEMCD: LOAD N
LOAD# STACK SUB TMP
ADD SS STORE @ TOP ; pone N%M come nuovo M - SS non cambia
SUB# 1 JUMP MCD
STORE TOP ERROR:
LOAD® TOP HALT
STORE RIS
WRITE RIS Esercizi
HALT 1. Leggere un array di 10 interi, ordinare l'array col metodo insertion sort e scrivere infine l’array ordinato su
output. Impostare la soluzione in Java e poi tradurla sistematicamente in Assembler RASP.
;MCD(N,M) 2. Leggere un array di 8 interi v, supposto ordinato per valori crescenti. Leggere quindi un intero x. Cercare x
MCD: in v con l'algoritmo della ricerca binaria e scrivere la posizione di x in v o -1 se x non è in v. Scrivere in Java
LOAD# STACK una soluzione e poi convertirla in Assembler RASP.
ADD SS 3. Considerato che un array bidimensionale (matrice) M viene comunque conservato in memoria come vettore
SUB# 1 monodimensionale, ad esempio seguendo lo “schema per righe” secondo cui si memorizza la prima riga,
STORE TOP quindi la seconda riga e cosi via, detta <i,j> una coppia di indici validi, l’indirizzo della cella di memoria
LOAD® TOP corrispondente all’elemento M[i][j] si può calcolare come segue (M denota l’indirizzo di partenza del blocco di
STORE M memoria associato alla matrice): M+i*M[0].length+j, nell’ipotesi che tutte le righe abbiano la stessa lunghezza.
JNZ TAILRECURSION La quantità M+i*M[0].length esprime l’indirizzo base del vettore riga M[i], Scrivere un programma RASP che
; occorre ritornare N legga (per righe) il contenuto di una matrice quadrata 5x5 di interi e scriva 1 in uscita se la matrice è
LOAD TOP simmetrica, 0 se la matrice non è simmetrica. Il programma deve ovviamente riservare un blocco contiguo di
SUB# 1 25 celle.
STORE TOP 4. Come 2. ma utilizzando un metodo tail recursive per la ricerca binaria.
LOAD® TOP
STORE N
LOAD TOP
SUB# 1
STORE TOP
LOAD® TOP
STORE RA
LOAD N
STORE® TOP ; pone N al posto dell’indirizzo di ritorno
LOAD SS
SUB# 2
STORE SS
JUMP® RA ; esegue return
TAILRECURSION:
LOAD TOP
SUB# 1
STORE TOP
LOAD® TOP
STORE N
LOAD M
STORE® TOP ; pone M come nuovo N
LOAD TOP
ADD# 1
STORE TOP
LOAD N
. DIV M
484 485

Potrebbero piacerti anche