Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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.
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
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:
4 5
Capitolo 1 Concetti di programmazione procedurale
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.
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 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
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)
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
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
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
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
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
22 23
Capitolo 1 Concetti di programmazione procedurale
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:
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
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
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
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
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
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
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;
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];
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:
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
//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:
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).
class Punto{
private doublé x, y; //variabili di istanza
//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
La classe Punto si suppone definita in un file Geometria.java contenente anche la seguente classe col main:
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”
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 );
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
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 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:
- 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
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
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
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:
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:
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.
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:
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)
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:
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
• 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
8. Data la classe:
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.
cb.depositai 200 );
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
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:
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
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;
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
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
100 101
Capitolo 4 Ereditarietà dynamic binding polimorfismo
}//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 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
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
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
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).
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
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).
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.
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
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:
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.
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
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
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:
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
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:
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
int sizeQ
ritorna il numero di elementi presenti nel vettore. Gli elementi del vettore, similmente agli array, hanno indici
tra 0 e size()-1
139
Capitolo 8 Tipi di dati astratti
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
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
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)
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 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—
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.
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
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.
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
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
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:
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
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
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:
180 181
Capitolo 9 Collection framework e collezioni custom
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
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
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:
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.
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:
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.
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..." );
Input dialog
Messaggio sé ^ T
1
i
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.*;
■ I■
selezionare una opzione *----------- 9
208 209
Capitolo 11 Ingresso/uscita grafico
|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
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.
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.
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) );
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.
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;
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
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
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.
239
Capitolo 13 Programmazione di GUI
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
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.:
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:
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
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);
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
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.
È 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})?“;
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:
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.
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:
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.
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
270 271
Capitolo 15 Lista concatenata
272 273
Capitolo 15
Lista concatenata
Una nozione generale di stack, iterabile, è definita dalla seguente interfaccia: public boolean isFull(){ return false;}
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 \\$).
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
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 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 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.
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
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:
dove l’1 si riferisce alla inizializzazione del for esterno. Contando le operazioni eseguite dal for-esterno si ha:
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:
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 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):
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:
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
//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 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.
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
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
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
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:
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
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
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
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 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
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
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:
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:
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.
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
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.
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( )
« 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 quattro
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
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
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.
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
//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
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
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
376 377
Capitolo 21 Sviluppo di programmi ad oggetti
@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
378 379
C ap ito lo 21 Sviluppo di programmi ad oggetti
380 381
■
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 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 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
}
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
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
398 399
Capitolo 21 Sviluppo di programm[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
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.
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
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
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
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
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
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.
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
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
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).
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
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
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
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 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):
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
• 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[
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:
Casi particolari
Riferiamoci al formato in singola precisione.
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
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:
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.
466 467
Appendice B La macchina RASP
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
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
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
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