Académique Documents
Professionnel Documents
Culture Documents
1 Introduzione
Preparazione: Sbucciate le nocciole nell'acqua calda e asciugatele bene al sole o al fuoco; indi pestatele finissime nel mortaio con lo zucchero versato poco per volta. Mettete il latte al fuoco e quando sar entrato in bollore sminuzzateci dentro i savoiardi e fateli bollire per cinque minuti, aggiungendovi il burro. Passate il composto dallo staccio e rimettetelo al fuoco con le nocciole pestate per sciogliervi dentro lo zucchero. Lasciatelo poi ghiacciare per aggiungervi le uova, prima i rossi, dopo le chiare montate; versatelo in uno stampo unto di burro e spolverizzato di pan grattato, che non venga tutto pieno, cuocetelo in forno o nel fornello e servitelo freddo. Questa dose potr bastare per nove o dieci persone. (Artusi)
Immaginate una cucina, una certa quantit di ingredienti, utensili per cucinare, fornelli, forno, un cuoco (possibilmente umano). Preparare un budino di nocciole un processo che parte dagli ingredienti, viene portato avanti dal cuoco, con l'aiuto di un forno e di altri strumenti e, cosa significativa, in accordo con le istruzioni di una ricetta. Gli ingredienti corrispondono, nella terminologia che useremo nel proseguimento di questo corso, all'input del processo. Il budino , abbastanza ovviamente, l'output. La ricetta l'algoritmo. In altre parole, l'algoritmo prescrive le attivit che costituiscono il processo attraverso cui a partire dall'input (ingredienti) si arriva all'output (budino). La ricetta, o l'algoritmo, se scritta in maniera formale, come all'inizio di questo capitolo, corrisponde a ci che viene chiamato software, o programma, mentre gli utensili, il forno e, in questo caso, lo stesso cuoco, vanno sotto il nome di hardware.
-2Nella prima parte di questo corso ci occuperemo di algoritmi, ossia di come utilizzare gli ingredienti per ottenere un budino. Come vedremo in seguito, l'analogia tra un algoritmo computazionale e una ricetta pu essere spinta solo fino a un certo punto, oltre il quale cessa di essere illuminante e diventa un mero gioco di parole. Per esempio, sia un computer che l'hardware-cucina sono in grado di compiere solo operazioni elementari: il computer, come vedremo pi in dettaglio nel seguito di questo corso, pu operare direttamente solo sui bit, ossia su interruttori che possono essere o accesi o spenti, mentre l'hardware-cucina pu sbucciare, pestare, mescolare, cuocere e misurare quantit, ma non pu, direttamente, creare un budino dal nulla. Un primo (importantissimo) problema che si presenta, sia nel caso del computer che in quello dell'hardware-cucina, quello del livello di dettagli al quale dobbiamo scendere perch la serie di istruzioni che costituiscono l'algoritmo abbia un senso, e ci permetta di arrivare al risultato, sia che si tratti di un budino o del calcolo molto complesso per la costruzione della struttura portante di un ponte. Prendiamo per esempio l'istruzione "pestare le nocciole finissime nel mortaio". Perch l'algoritmo non dice "ridurre le nocciole a particelle grandi al massimo un decimo di millimetro"? Semplicemente perch questo livello di dettaglio eccessivo per lo scopo che ci prefiggiamo, che quello di ottenere una pasta omogenea di nocciole. In altre parole possiamo dire che in questo caso l'hardware gi sa cosa significa "pestare le nocciole finissime", e non ha bisogno di ulteriori dettagli. Consideriamo un altro esempio, pi vicino al resto di quel che studieremo durante il corso: la moltiplicazione tra due numeri interi. Supponiamo che ci chiedano di moltiplicare 528 per 46. Sappiamo esattamente (o almeno lo spero) cosa fare. Moltiplichiamo 6 per 8, che d 48, Scriviamo 8 e riportiamo 4; quindi moltiplichiamo 6 per 2 e aggiungiamo il 4 del riporto, e questo ci d 16. Scriviamo 6 a sinistra dell'8 e riportiamo 1, eccetera. Qui possiamo porci la stessa domanda di prima, relativa al grado di sbriciolamento delle nocciole. Perch moltiplichiamo 6 per 8 e non invece aggiungiamo 8 volte 6 a s stesso? La risposta, abbastanza ovvia, che gi sappiamo come moltiplicare 6 per 8, e non abbiamo bisogno di ricorrere alla definizione elementare di moltiplicazione. Al contrario, ma allo stesso modo, potremmo chiederci perch non moltiplichiamo direttamente 528 per 46, senza ricorrere a un algoritmo. Alcune persone riescono a farlo: queste persone corrispondono a dei cuochi che sanno preparare un perfetto budino di nocciole senza leggere la ricetta. In altre parole, usando l'algoritmo della moltiplicazione stiamo dicendo che l'hardware (in questo caso noi stessi) in grado di compiere certe operazioni elementari (moltiplicare 6 per 8, riportare 4, eccetera) ma non capace di moltiplicare 528 per 46 "al volo". Questo esempio mostra la necessit di mettersi subito d'accordo sulle azioni basilari che un algoritmo deve essere in grado di prescrivere. Senza questa specificazione inutile cercare di stabilire un algoritmo per un qualsiasi dato problema. Naturalmente, problemi diversi sono associati a diversi tipi di azioni basilari. Nel caso della cucina, le azioni basilari sono mescolare, pestare, cuocere, pesare, eccetera. Nel caso della moltiplicazione di due numeri grandi le azioni basilari si riducono a moltiplicazioni di numeri minori di 10, riporti, somme, eccetera. Nel caso degli algoritmi di cui ci occuperemo in seguito, e qui arriviamo al limite dell'analogia con le ricette di cui parlavamo prima, le azioni basilari di cui stiamo parlando dovranno essere specificate con chiarezza e precisione. Non saremo in grado di accettare istruzioni tipo "montare le chiare a neve". L'idea che un certo cuoco ha di chiare montate a neve pu essere decisamente diversa da quella di un altro cuoco. Le istruzioni dovranno essere chiaramente distinte dalle non-istruzioni. "Questa dose potr bastare per nove o dieci persone" non , per esempio, un'istruzione che serve a preparare il budino. Frasi ambigue tipo "che non venga tutto pieno" (met? tre quarti? nove decimi?) non trovano posto in algoritmi che vanno
-3poi realmente eseguiti sui calcolatori. Le ricette, per dirla tutta, rispetto agli algoritmi per calcolatore dnno troppe cose per scontate, la pi notevole delle quali che un essere umano (il cuoco) fa parte dell'hardware. Nel disegnare algoritmi per calcolatori non potremo permetterci questo lusso, e dovremo cercare di essere molto pi stringenti e precisi. Nel seguito ci occuperemo principalmente di problemi per i quali possibile una precisa formalizzazione, quasi sempre matematica: se un dato problema cos chiaramente comprensibile ed esprimibile, sar in generale possibile definire una strategia di soluzione basata sullapplicazione sistematica di ben precise regole operative che consentir di ottenere il risultato atteso a partire dai dati disponibili. In molti casi sar possibile affidare lapplicazione delle regole di soluzione a un esecutore altamente specializzato (un calcolatore), in grado di svolgere il compito con estrema rapidit.
-4-
se X, Y, Q, e R sono numeri interi, con X Y e X = Q * Y + R, allora linsieme intersezione di D(X) e D(Y) uguale allinsieme intersezione di D(Y) e D(R). Quindi il problema di trovare il valore massimo nellinsieme intersezione di D(X) e D(Y) pu essere ridotto al problema pi semplice di trovare il massimo nellinsieme intersezione di D(Y) e D(R). Sulla base di questa propriet possibile scrivere lalgoritmo di Euclide (Algoritmo E) per il MCD: 1. R = MOD(X,Y); [MOD il resto della divisione intera]
2. Se R = 0 allora Y il MCD(X,Y), altrimenti calcola MCD(Y,R). Vediamo questo algoritmo allopera sul nostro esempio favorito. Abbiamo MOD(32,12)=8 (perch 32 = 12 * 2 + 8), quindi calcoliamo MOD(12,8)=4 (perch 12 = 8 * 1 + 4), e infine MOD(8,4)=0 (perch 8 = 4 * 2), quindi MCD(32,12)=4. La parola algoritmo deriva dal nome di un matematico arabo, Mohammed alKhowarizmi, che visse nel nono secolo della nostra era e trov alcuni procedimenti sequenziali per la moltiplicazione e la divisione di numeri interi. Il suo nome fu latinizzato in Algorismus, e da qui ad algoritmo il passo breve.
-5questo numero potr essere piccolo o grande, a seconda delle dimensioni dell'azienda, del numero degli impiegati e della consistenza del loro stipendio.
-6(legale) non funzionano. Come esempio estremo, immaginiamo per il problema dello "stipendio totale" il seguente algoritmo: 1. produci zero come output. Questo algoritmo funziona solo per una ristrettissima classe di aziende. Un altro aspetto importante da non sottovalutare riguarda il tempo di esecuzione, da parte dell'hardware di turno, di ciascuna azione basilare, o operazione, prescritta dall'algoritmo. Risulta infatti necessario, anche se apparentemente ovvio, richiedere che ciascun passo elementare dell'algoritmo possa essere portato a termine in un tempo finito. Nel caso contrario l'algoritmo non terminerebbe mai, risultando quindi di scarsa se non nulla utilit pratica.
Si assume che sia data a priori una descrizione dei passi elementari possibili, o equivalentemente una configurazione hardware e la specifica delle azioni elementari che l'hardware stesso pu eseguire. La soluzione a un problema algoritmico (o computazionale) consiste nell'algoritmo stesso, composto da istruzioni elementari che prescrivono le azioni da compiere, scelte tra le azioni possibili e legali. L'algoritmo, quando viene eseguito in seguito all'immissione di un qualsiasi input legale, risolve il problema, producendo l'output richiesto. Va notato che le regole di un qualsiasi algoritmo sono in genere applicate su rappresentazioni degli oggetti fondamentali che vivono nello spazio in cui lalgoritmo opera. Nel caso del MCD, lalgoritmo di Euclide opera su X, Y eccetera, che sono rappresentazioni di particolari numeri interi (32 e 12, nel nostro caso). Il messaggio fondamentale di questo capitolo riguarda la natura e la definizione di algoritmo e di problema algoritmico: un algoritmo sostanzialmente un insieme di regole che, eseguite ordinatamente, permettono di risolvere un problema a partire dai dati a disposizione (input). Perch questo insieme di regole possa considerarsi un algoritmo a tutti gli effetti deve rispettare alcune propriet: Non ambiguit: le istruzioni devono essere univocamente interpretabili dallesecutore dellalgoritmo, che sia un calcolatore (come pi spesso accade) o un essere umano; Eseguibilit: lesecutore deve essere in grado, con le risorse a disposizione, di eseguire ogni istruzione in un tempo finito; Finitezza: lesecuzione di un algoritmo deve terminare in un tempo finito per ogni insieme di valori in input.
E' importante capire che gli esempi che abbiamo sviluppato in questo capitolo introduttivo (la ricetta e la moltiplicazione di due interi) non rendono giustizia alla considerevole complessit del problema generale di trovare algoritmi soddisfacenti per un dato problema. Non bisogna pensare che le cose siano cos semplici come sono state presentate. E
-7se sono state presentate in maniera cos semplice solo a scopo esemplificativo. I problemi algoritmici di interesse pratico possono risultare incredibilmente complessi, e possono richiedere anni di lavoro da parte di un equipe di specialisti per poter essere risolti in maniera soddisfacente. Addirittura alcuni problemi non possono essere assolutamente risolti in maniera soddisfacente, mentre altri non ammettono nessuna soluzione ( possibile stabilire con un algoritmo quale sar il cambio Euro/Dollaro il primo gennaio del 2100?). E ci che peggio, per certi problemi non sappiamo neppure se possano essere risolti algoritmicamente o meno.
-8-
2 Algoritmi e dati
Sappiamo gi che gli algoritmi contengono istruzioni elementari selezionate con cura che prescrivono le azioni basilari che devono essere eseguite al fine di ottenere un certo risultato in output a partire da un certo input. Non abbiamo parlato del modo in cui queste istruzioni sono arrangiate (???) nellalgoritmo, in modo tale che chi poi si incaricher di eseguire materialmente lalgoritmo (probabilmente un calcolatore) possa immaginare lordine preciso nel quale le azioni elementari devono essere eseguite. Non abbiamo neanche discusso gli oggetti manipolati da queste azioni elementari. Lesecuzione di un algoritmo pu essere pensato come portato avanti da un piccolo robot, o un processore, che chiameremo Corrintorno. Il processore riceve istruzione di correre qui e l facendo questo e quello, dove questo e quello sono proprio le azioni basilari dellalgoritmo. Nellalgoritmo dello stipendio totale del capitolo precedente al piccolo Corrintorno stato ordinato di prendere nota del numero 0 e poi di cominciare a lavorare sulla lista di impiegati, trovando gli stipendi e aggiungendoli, uno a uno, al numero annotato allinizio. Dovrebbe risultare chiaro che lordine in cui le azioni elementari sono eseguite cruciale. E di unimportanza fondamentale non solo che le azioni elementari siano chiare e non ambigue, ma anche che lo stesso criterio di chiarezza e non ambiguit sia applicato al meccanismo che controlla la sequenza in cui le istruzioni elementari sono eseguite. Lalgoritmo deve quindi contenere istruzioni di controllo per spingere il processore (il nostro Corrintorno) in questa o quella direzione, a seconda dei casi, dicendogli chiaramente cosa fare passo per passo.
-9ricetta: inserire le chiare montate a neve dopo aver amalgamato i rossi duovo); Salto condizionale: sono della forma se succede Q allora fai A altrimenti fai B (if Q then do A else do B), o semplicemente se succede Q allora fai A (if Q then do A), in cui Q qualche tipo di condizione. Nella ricetta: sminuzzare i savoiardi se il latte bolle, altrimenti continuare a scaldare il latte).
Queste due strutture di controllo, sequenza diretta e salto, non spiegano come un algoritmo di lunghezza prefissata possa eseguire processi arbitrariamente lunghi, a seconda dellinput. Un algoritmo che contenga solo sequenze dirette e salti condizionali pu solo prescrivere processi di lunghezza prefissata, poich nessuna parte dellalgoritmo pu essere eseguita pi di una volta. Strutture di controllo che permettono allalgoritmo di eseguire processi arbitrariamente lunghi sono nascoste anche nella ricetta del budino, ma sono di gran lunga pi esplicite nellalgoritmo di stipendio totale. Queste strutture sono genericamente chiamate iterazioni, o costrutti di loop (loop significa cappio, ossia una cosa che torna su s stessa come la corda di un cappio), e possono presentarsi in diverse maniere. Qui ne descriviamo due: Iterazioni limitate: sono della forma fai A esattamente N volte (do A exactly N times), in cui N un numero; Iterazioni condizionali: sono della forma fai A fino a che non si verifica la condizione Q (do A until Q), oppure finch la condizione Q vera fai A (while Q do A). Nella ricetta, implicitamente: battere le chiare duovo finch non sono montate a neve).
Quando abbiamo descritto lalgoritmo dello stipendio totale siamo rimasti sul vago relativamente a come la parte principale dellalgoritmo dovesse essere svolta: abbiamo scritto qualcosa del tipo scorri tutta la lista, aggiungendo lo stipendio dellimpiegato corrente al numero annotato. In realt, per descrivere con precisione lalgoritmo (e ogni algoritmo va descritto con estrema precisione) avremmo dovuto utilizzare un costrutto iterativo, specificando esattamente in questo modo a Corrintorno il modo in cui scorrere la lista degli impiegati. Assumiamo che insieme alla lista sia data in input anche la sua lunghezza, ovvero il numero degli impiegati, N. In questo caso possibile utilizzare un costrutto del tipo iterazione limitata, che porta al seguente algoritmo: 1. annota 0; 2. punta al primo stipendio della lista; 3. fai le cose che seguono N-1 volte; 3.1. somma lo stipendio a cui stai puntando al numero annotato; 3.2. punta al prossimo stipendio; 4. somma lo stipendio a cui stai puntando al numero annotato; 5. produci il numero annotato come output. Le cose che seguono al punto 3. si riferiscono naturalmente notato subito il livello di indentazione di questi punti, che sono scritti Quello dellindentazione un trucco che useremo spesso, anche concretamente a scrivere programmi, per connotare i cicli iterativi, o codice. ai punti 3.1. e 3.2. Va pi a destra degli altri. quando ci troveremo in generale i blocchi di
Gli studenti sono incoraggiati a cercare di capire come mai al punto 2. stiamo usando
- 10 N-1 invece di N, e perch stiamo sommando separatamente lultimo stipendio. E da notare che lalgoritmo fallisce se la lista vuota (cio se N = 0) perch in questo caso la seconda parte del punto 1. non ha alcun significato. Se linput non include il numero N di impiegati, dobbiamo ricorrere a uniterazione condizionale, del tipo while (la lista non finita) somma stipendio. In questo caso per dobbiamo specificare qual il segnale che ci dice che la lista finita.
start
un ovale, per rappresentare linizio (start) o la fine (stop) dellalgoritmo;
azione
un rettangolo, per rappresentare unazione vera e propria (per esempio lassegnazione di un valore a una variabile);
- 11 -
?
una losanga, per rappresentare una condizione. In Figura 1 disegnato, a mo di esempio, il diagramma di flusso dellalgoritmo dello stipendio totale, mentre in Figura 2 e 3 sono rappresentati, rispettivamente, i diagrammi di flusso dellalgoritmo del diretto superiore (vedi sezione ???) e dellalgoritmo di Euclide per il Massimo Comun Divisore.
start
annota 0
punta al primo elemento della lista aggiungi lo stipendio corrente al numero annotato s output somma
la lista finita?
no
stop
Notiamo che osservando un diagramma di flusso ci accorgiamo subito della presenza nellalgoritmo di un ciclo: se seguendo le frecce troviamo un percorso che ci riporta in un punto del programma che abbiamo gi attraversato, siamo in presenza di un ciclo.
- 12 calcolatore per implementare un algoritmo, consiste nellintroduzione di un linguaggio di programmazione che chiameremo naturale. Questo linguaggio di programmazione generico (LP) se da un lato , come vedremo presto, molto vicino a un vero e proprio linguaggio di programmazione (almeno per quel che riguarda luso delle variabili e delle strutture di controllo), dallaltro se ne discosta drasticamente per quel che riguarda il livello di dettaglio in cui si scende per descrivere certe operazioni di alto livello, e da questo punto di vista il risultato netto assomiglia pi a un diagramma di flusso che a un programma vero e proprio. Per fare un esempio concreto, loperazione di input non sar descritta, a livello di LP, con lo stesso dettaglio necessario in un linguaggio di programmazione vero e proprio affinch linput avvenga poi concretamente e in maniera corretta, ma sar genericamente descritta dallistruzione: Input( x, y, z ); dove x, y e z sono le variabili che si stanno leggendo in input. Per descrivere in un minimo didettaglio il LP conveniente iniziare con degli esempi. Il primo esempio che vedremo loramai famoso lalgoritmo dello stipendio totale: Input( lista ); somma := 0; while ( not lista.finita ) { p := lista.prossimo_elemento; somma := somma + p.stipendio; } Output( somma ); Notiamo subito le istruzioni Input e Output, che come abbiamo detto restano molto generiche (il LP non ci dice come effettivamente avvenga linserimento dei dati, n come il risultato finale sia mostrato in output allutente). Notiamo anche luso della struttura di controllo while ( condizione ), seguita da un cosiddetto blocco di codice, incluso tra parentesi graffe. In generale possiamo dire che per scrivere un algoritmo in LP possiamo usare i seguenti elementi: variabili, ossia nomi logici associati agli oggetti sui quali il nostro algoritmo deve operare (nellesempio sopra: lista, somma e p); istruzione Input( lista variabili ); istruzione Output( lista variabili ); ciclo condizionale while ( condizione ) { blocco di codice }; listruzione while fa s che il blocco di codice contenuto tra le parentesi graffe sia ripetuto di seguito finch non si avvera la condizione nelle parentesi tonde; ciclo limitato for i = 1 to N { blocco di codice }; questo ciclo viene usato per eseguire il blocco di codice esattamente N volte. A ogni iterazione la variabile
- 13 i viene incrementata di uno; istruzione if ( A ) { blocco 1 } else { blocco 2 }; questa istruzione comporta lesecuzione del blocco 1 se la condizione A vera: altrimenti sar eseguito il blocco 2; istruzione di assegnazione nome_variabile := valore; luso del simbolo := per differenziare lassegnazione dalla condizione di uguaglianza; operatori logici: not, and, or, =, <= >= > metodi, della forma nome_variabile.nome_metodo: nellesempio sopra riportato lista.finita un metodo che ci dice se la lista finita oppure no. Va notato che il LP non specifica in nessun modo il concreto funzionamento di un metodo (ossia come questo metodo viene implementato in pratica). Per esempio, invocando il metodo p.stipendio otteniamo lo stipendio dellimpiegato il cui profilo contenuto nella variabile p, ma non sappiamo come abbiamo fatto per ottenerlo (in altre parole non siamo scesi fino al livello di dettagli necessario per capire come ottenere questo valore). funzioni, della forma nome_funzione( lista parametri ). La funzione esattamente come un metodo, solo che un metodo in qualche senso appartiene a una variabile (o pi propriamente a una classe di variabili) mentre la funzione opera sulla lista dei parametri che le passiamo e restituisce un certo risultato: possiamo pensare a una funzione come a un sottoalgoritmo, di cui al momento della stesura dellalgoritmo vero e proprio non ci interessa conoscere i dettagli di funzionamento. Va senza dire che quando si scriveranno veri e propri programmi occorrer specificare in dettaglio anche le istruzioni che definiscono i metodi e le funzioni.
- 14 metodo p.stipendio, e poi stiamo assegnando alla variabile somma il valore cos ottenuto. Un modo particolarmente furbo di raggruppare variabili quello di metterle in un array, o vettore. Per continuare con la metafora del contenitore, possiamo dire che un array un contenitore di contenitori. Vediamo gli array allopera in una versione modificata dellalgoritmo dello stipendio totale:
Input( lista ); N := lista.lunghezza; somma := 0; for i = 1 to N { p := lista[i]; somma := somma + p.stipendio; } Output( somma ); In questo caso la variabile lista pensata come un vettore (array) ordinato: gli elementi dellarray, che sono le variabili vere e proprie, sono ottenute appendendo il loro numero progressivo, racchiuso da parentesi quadre, al nome dellarray, e in questo modo lista[1] contiene il primo elemento della lista, lista[2] il secondo e cos via, fino allultimo elemento, che sar lista[N].
Vediamo che il diretto superiore sia di Emanuele che di Giorgio Annalisa, ma solo Emanuele guadagna pi di Annalisa. Inoltre Marco guadagna pi di Emanuele, che il suo diretto superiore. Lalgoritmo deve fornire come risposta, in questo caso, 7.000 Euro (cio la
- 15 somma degli stipendi di Emanuele e Marco). In figura 2 abbiamo il flow chart dellalgoritmo che ci serve. Vediamo immediatamente che i cicli annidati si mostrano visivamente come due percorsi chiusi uno dentro laltro.
- 16 -
start somma := 0
q sup. di p? no
outp ut so mma
p ultimo?
no
p := prossimo
stop
- 17 -
dei
Alla domanda che cos un calcolatore? possibile rispondere in molte maniere diverse, ciascuna relativa al diverso punto di vista dal quale si guarda alloggetto in questione. In questo corso il punto di vista prevalente sar quello che consente di rispondere alla domanda in questa maniera: un calcolatore una macchina programmabile, per tramite della quale possibile scrivere ed eseguire programmi. Potremmo anche dire che un calcolatore una macchina che tramite i programmi permette di risolvere problemi algoritmici. Ladozione di questo punto di vista non ci impedir di considerare di tanto in tanto, e soprattutto a scopo esemplificativo, altri punti di vista: per esempio quello di un semplice utente, per il quale un calcolatore una macchina che esegue programmi. Daltronde, dal punto di vista tecnologico, un calcolatore un sistema elettronico molto complicato.
- 18 anche molto diversi tra loro. Per poter risolvere un particolare problema usando un programma, lutente deve essere in grado di fornire al programma stesso le istruzioni dettagliate su come il problema debba essere risolto. In alternativa pu sempre scriversi un programma tutto suo. Dal punto di vista dellutente, con riferimento allesecuzione di un applicazione, le istruzioni che possibile richiedere al calcolatore di eseguire sono quelle corrispondenti alle richieste di esecuzione delle operazioni fornite dallapplicazione. Inoltre ciascun programma pu essere caratterizzato dallinsieme di operazioni che permette di eseguire (e dalle regole per usarle) e dalla tipologia di informazioni che permette di gestire. La capacit di comprendere e usare un programma largamente indipendente dalla comprensione del funzionamento del calcolatore, esattamente come si riesce a guidare la macchina senza sapere nulla sul funzionamento del motore. Ma allo stesso modo in cui consigliabile conoscere almeno le basi del funzionamento del motore per guidare una macchina, cos sarebbe preferibile avere almeno una vaga idea di come funziona un calcolatore, prima di provare a utilizzare o addirittura a scrivere programmi.
- 19 Come vedremo in seguito, il linguaggio di programmazione Java definisce una sua propria Macchina Virtuale (JVM, ossia Java Virtual Machine) che permette allo stesso programma Java di essere eseguito su piattaforme (hardware) diverse.
tastiera
mouse
schermo
BUS
Memoria Centrale
CPU
- 20 -
stato un errore in unoperazione, per esempio); ALU (Arithmetic-Logic Unit, ovvero Unit Aritmetico-Logica): il luogo in cui effettivamente avvengono le operazioni; Register Stack (pila dei registri): i registri contengono i dati su cui lALU opera; MAR (Memory Address Register): contiene lindirizzo di memoria da cui estrarre il prossimo dato su cui si deve operare o in cui salvare il risultato di unoperazione gi effettuata; MDR (Memory Data Register): registro di transito per il valore prelevato dalla memoria, prima che venga scaricato nel Register Stack, o sulla strada contraria (dal Register Stack alla memoria).
MAR
PC IR
Controller PSW
MDR
ALU
- 21 Una CPU dei nostri giorni contiene una quarantina di milioni di transistor. I transistor sono assemblati a gruppi per formare le cosiddette porte logiche, che implementano le operazioni logiche come AND, OR, NOT e cos via. Quel che vogliamo vedere adesso lidea del funzionamento di una porta logica. Non scenderemo in dettaglio nel modo di funzionamento di un transistor, ma ci accontenteremo dellidea di base, e useremo un modello formato da un circuito elettrico, due interruttori e una lampadina, e quello che ci domanderemo : possiamo costruire il circuito in modo che si comporti come un operatore AND, oppure come un operatore OR? Ricordiamo brevemente la definizione degli operatori AND e OR. Abbiamo a che fare con quantit (o proposizioni) che possono avere solo due valori: Vero (V) e Falso (F). Chiamando P e Q due di queste quantit, ci chiediamo quale sia il valore di P AND Q e di P OR Q. La proposizione (P AND Q) sar vera solo se sono vere entrambe le quantit P e Q, mentre la proposizione (P OR Q) sar vera se anche una sola delle due quantit vera. In altre parole otteniamo la seguente tabellina: P V V F F Q V F V F P AND Q V F F F F V V V P OR Q
Vogliamo ora vedere come costruire dei circuiti che implementino le porte logiche AND e OR. Consideriamo il circuito in figura 3. Notiamo che i due interruttori sono in serie: come risultato avremo che la corrente nel circuito scorrer solo se gli interruttori saranno entrambi chiusi. Se interpretiamo (interruttore chiuso) = Vero e (interruttore aperto) = Falso, abbiamo che la lampadina si accende (cio il risultato Vero) in corrispondenza di un AND logico tra gli interruttori.
- 22 -
Nel caso della figura 4, invece, gli interruttori sono in parallelo. Questo significa che per far accendere la lampadina sar necessario chiudere solo uno dei due interruttori, e ci comporta che il circuito implementa la porta logica OR.
Nella CPU di un calcolatore non ci sono circuiti come quelli che abbiamo appena presentato: ci sono per dei circuiti integrati basati sui transistor, che effettuano lo stesso tipo di operazioni. Esistono altri tipi di porte logiche oltre AND e OR: per gli scopi che ci prefiggiamo vogliamo ricordare solo loperatore logico NOT, che applicato a una proposizione ne cambia il valore di verit (ossia NOT Vero = Falso e viceversa), e lOR Esclusivo, o XOR, che a differenza dellOR inclusivo vero solo se le proposizioni P e Q sono una vera e laltra
- 23 falsa. Il motivo per il quale ci stiamo interessando ai circuiti logici che le operazioni che lALU pu compiere sui bit sono implementate con circuiti logici. Pi tardi daremo un semplice esempio di circuito logico che permette di sommare due numeri scritti in notazione binaria. Prima di arrivare a tanto dobbiamo spendere due parole sul modo in cui il calcolatore gestisce i numeri. Noi esseri umani siamo abituati fin dalla tenera infanzia a contare in base 10. I personaggi dei cartoni animati probabilmente usano una base 8, perch hanno generalmente 4 dita per mano. Ma che significa in realt contare in una certa base B? Significa che per rappresentare un numero qualsiasi abbiamo a disposizione esattamente B simboli (cifre), la posizione delle quali allinterno del numero assume un significato particolare (notazione posizionale): una cifra nella k-esima posizione (a partire da destra) significa che nello sviluppo del numero che stiamo considerando quella cifra moltiplica la (k-1)-ma potenza della base B. In base 10 abbiamo a disposizione 10 simboli, che sono: 0, 1, 2, 3, 4, 5, 6, 7, 8 e 9. Se scriviamo il numero 1234, in realt stiamo intendendo:
Nel seguito, per non confonderci, quando scriveremo un numero in una certa base B lo scriveremo in questo modo: numeroB , e quindi 1234 in base 10 lo scriveremo 123410 . Ora, in un calcolatore la quantit minima di informazione chiamata bit, che una contrazione di binary digit, ossia cifra binaria. Un bit una quantit che pu avere solo due valori: spento o acceso, falso o vero, e quindi in definitiva 0 o 1. Avendo a disposizione solo 2 simboli, un calcolatore costretto a contare in base 2. Vediamo come conterebbe un calcolatore, e partiamo da 02. Il prossimo numero 12. E dopo gi sorgono i problemi, perch non abbiamo a disposizione il simbolo 22. Per analogia, cosa succede in base 10 quando arriviamo al limite dei simboli, cio quando arriviamo a 9? Subito dopo abbiamo 10, ossia riportiamo a 0 il contatore delle unit e incrementiamo quello delle decine (ossia passiamo alla potenza di 10 superiore). In base 2 dobbiamo fare esattamente la stessa cosa, quindi
210 = 102
Continuando a contare, dobbiamo incrementare il contatore delle unit, quindi arriviamo a 310 = 112, e poi abbiamo di nuovo esaurito i simboli, e dobbiamo scalare verso sinistra di unulteriore potenza della base, ottenendo 410 = 1002. Rivediamo ancora il tutto con un esempio: abbiamo il numero in base 2
100101112
e vogliamo sapere quant questo numero in base 10. Dobbiamo partire da destra,
- 24 esattamento come abbiamo fatto per il caso 1234 in base 10, e contare le potenze di 2 (ossia di 102):
Occorre stare attenti a considerare che 1 * 1072 in realt significa, in decimale, 1 * 2710. Quindi in definitiva
100101112 = 15110.
Possiamo anche porci la domanda al contrario: dato un numero in base 10, come possiamo esprimerlo in base 2? Vediamolo con un esempio: abbiamo il numero 104, e vogliamo esprimerlo in base 2. Dobbiamo prima di tutto chiederci qual la pi grande potenza di 2 minore o uguale a 104: la risposta ovviamente 64, cio 26. Possiamo quindi scrivere
104 = 64 + 50.
A questo punto operiamo ricorsivamente, e ci chiediamo qual la pi grande potenza di 2 minore o uguale a 50. La risposta 32 (25). Scriviamo quindi
104 = 64 + 32 + 18.
Siamo arrivati al 18, che possiamo scomporre, in potenze di 2, come 16 + 2. In definitiva abbiamo
104 = 64 + 32 + 16 + 2 = 26 + 25 + 24 + 21
Per scrivere il nostro numero in binario partiamo da destra: notiamo che nello sviluppo
- 25 non c la potenza 20, quindi la prima cifra a destra sar 0. Poi c un 1, perch troviamo 21 nello sviluppo: quindi vediamo che mancano sia 22 che 23. In definitiva otteniamo
10410 = 11100102.
Dal punto di vista del calcolatore, unaltra base importante la base 16, o esadecimale. Per contare in base 16 abbiamo bisogno di 16 simboli (ovvero 16 cifre) per costruire un qualsiasi numero: occorre quindi introdurre nuovi simboli rispetto alle consuete cifre da 0 a 9. I simboli necessari sono fornite dalle prime lettere dellalfabeto latino. Abbiamo quindi la seguente corrispondenza:
A16 = 1010 B16 = 1110 C16 = 1210 D16 = 1310 E16 = 1410 F16 = 1510
Perch importante la base 16? Perch il contenuto di un byte (ossia di una serie consecutiva di 8 bit) pu essere scritto come una coppia di numeri esadecimali, nel modo che andiamo adesso a vedere: prendiamo 8 bit (ossia, come abbiamo detto, un byte) e dividiamolo in una parte inferiore (i quattro bit a destra) e in una parte superiore (i quattro bit a sinistra).
Notiamo che il numero pi grande che possiamo inserire in quattro bit 11112, che corrisponde a 1510 e quindi in definitiva a F16. Ossia abbiamo che il contenuto di 4 bit pu essere espresso come una singola cifra esadecimale. Vediamo quindi che possiamo scrivere i quattro bit pi a destra come
Abbiamo quindi che lintero byte pu essere scritto come DB16. A questo punto
- 26 potremmo domandarci se quel che abbiamo fatto pienamente legale, ossia se dal punto di vista del numero completo vero che 1101101116 = DB16. La risposta s, e la dimostrazione lasciata come esercizio. Si noti che tutto questo reso possibile dal fatto che 16 una potenza esatta di 2.
Possiamo ora discutere come lALU riesca per esempio a sommare 2 numeri interi, scritti in forma binaria, utilizzando sostanzialmente solo le porte logiche che operano sui bit che compongono i numeri. Ripetiamo che nella CPU sono presenti microcircuiti che sebbene molto diversi in pratica dai circuiti elettrici con la lampadina che abbiamo presentato prima, operano per con la stessa idea di base. Introduciamo ora i seguenti simboli grafici per rappresentare le porte logiche: Porta AND:
p q
p AND q
Porta OR:
p q
Porta XOR:
p OR q
p q
p XOR q
Vediamo ora tutte le possibilit che ci si presentano quando dobbiamo sommare due bit P e Q: P 0 1 0 1 Q 0 0 1 1 P+Q 0 1 1 10
Notiamo subito che nel quarto caso otteniamo come risultato 2 bit (10), un po come quando sommiamo due numeri a una cifra in base 10 e otteniamo un risultato maggiore di 10. Per motivi di simmetria aggiungiamo uno zero (ininfluente) ai primi tre risultati, ottenendo la tabella
- 27 -
P 0 1 0 1
Q 0 0 1 1
P+Q 00 01 01 10
Ora, possiamo chiamare il bit pi a destra SOMMA e il bit pi a sinistra RIPORTO, ottenendo lulteriore tabella:
P 0 1 0 1
Q 0 0 1 1
SOMMA 0 1 1 0
RIPORTO 0 0 0 1
Notiamo subito che la SOMMA si comporta come se fosse stata ottenuta da una porta XOR, mentre il RIPORTO si comporta come se fosse stato ottenuto da una porta AND. Possiamo quindi disegnare il seguente circuito, che avr come effetto (output) quello di sommare (con riporto) i due bit in ingresso (input):
p q
SOMMA
RIPORTO
A partire dai due elementi XOR e AND, che ottengono rispettivamente la SOMMA e il RIPORTO, possiamo costruire un primo circuito integrato che chiameremo Addizionatore Incompleto (o AI):
AI
- 28 Laddizionatore incompleto perch ci permette s di sommare due bit, ma non ci permette di sommare numeri binari formati da un numero arbitrario di cifre. Infatti, dopo aver sommato le due cifre a destra, e aver ottenuto un eventuale riporto, ci troviamo nella condizione di dover sommare tre cifre binarie: il circuito che permette di sommare tre cifre binarie quello raffigurato in Figura 6: la linea R(in) porta il riporto di una precendente operazione di somma tra due bit. LAI in basso a sinistra somma le due cifre successive del numero binario, p2 e q2. La somma di questa operazione va a sommarsi, nellAI al centro, con R(in), producendo S(out) (la somma dei due bit pi il riporto della somma precedente) e un riporto che dobbiamo ancora sommare col riporto della somma dei secondi due bit. E facile vedere, facendo una tabellina di tutti i casi possibili, che la linea che esce in basso dallAI centrale e la linea che esce in basso dallAI di sinistra non possono essere tutte e due uguali a 1: quindi per sommarle baster farle entrare in un circuito OR, che in questo caso specifico si comporta come uno XOR, cio come un addizionatore.
R(in) p2 q2 AI AI
S(out) R(out)
Dovrebbe essere chiaro a questo punto che assembrando di seguito diversi circuiti come quelli mostrati in figura 6 possibile, tramite solo operazioni logiche sui bit, sommare numeri interi di grandezza qualsiasi.
- 29 -
4 Introduzione a Java
4.2 Java
Uno dei problemi incontrati da chi scrive software quello della portabilit: idealmente si vorrebbe che un determinato programma, scritto in un determinato linguaggio di programmazione, possa girare su qualsiasi piattaforma, ossia su qualsiasi tipo di hardware con qualsiasi sistema operativo. Di fatto questo obiettivo molto difficile da realizzare: occorre tenere presente che un programma scritto, poniamo, in C, prima di poter essere capito e quindi eseguito dalla macchina ha bisogno di essere compilato: ossia occorre trasformare le
- 30 istruzioni ad alto livello (scritte in un linguaggio molto simile allinglese) in istruzioni di basso livello (codice binario, ossia linguaggio macchina) che la CPU possa comprendere ed eseguire. I dettagli della compilazione naturalmente dipendono dal tipo di macchina su cui si sta lavorando, e ogni hardware diverso richiede in generale istruzioni in linguaggio macchina diverse. Inoltre, cosa da non trascurare data limportanza oramai acquisita dalle interfacce user-friendly dei programmi, laspetto visivo del programma dipende dallinterfaccia grafica utilizzata dal sistema operativo. Java risolve il problema della portabilit eliminando, in qualche senso, la compilazione, o almeno riducendola a una semi-compilazione. Un programma scritto in Java, prima di poter essere eseguito, deve venir trasformato in bytecode, ossia in un formato binario che per non viene direttamente compreso dalla CPU del calcolatore su cui si sta lavorando. Il bytecode, per poter essere eseguito, va dato in pasto alla Java Virtual Machine, che lo traduce al volo in istruzioni che la macchina in grado di interpretare. In altre parole Java introduce una nuova interfaccia (ossia una Macchina Virtuale, vedi capitolo precedente) tra il programma scritto ad alto livello e il Linguaggio Macchina.
4.3 Prolegomena
Per arrivare a eseguire un programma in Java prima di tutto necessario che sul calcolatore siano installati il compilatore Java e la Java Runtime Environment (ossia lAmbiente Java di Esecuzione Programmi). Poi, occorrer scrivere il programma in un file. Per far questo ci si pu servire di un qualsiasi editor di testo (tipo il Notepad). E importante sottolineare che un file Java deve necessariamente avere unestensione .java (cos come i file di Word hanno unestensione .doc). Una volta scritto il programma occorre compilarlo: il compilatore legge il file Java inserito e genera un nuovo file per ogni classe presente nel programma: i nuovi file avranno il nome della classe corrispondente e lestensione .class (questi file di tipo class contengono il bytecode). Dopo aver compilato il programma occorre invocare la Macchina Virtuale di Java per eseguirlo. Per esemplificare il tutto, poniamoci lobiettivo di scrivere un programma che stampi sullo schermo un breve messaggio, per esempio Questo e il mio primo programma in Java. Mettiamoci nel caso concreto e tuttaltro che improbabile in cui il calcolatore su cui stiamo lavorando abbia un qualche tipo di Windows (95, 98, 2000, etc) come sistema operativo. Come prima cosa, per essere ordinati, creiamo una Nuova Cartella sul disco C: chiamandola Corso. Apriamo il Notepad e inseriamo le righe seguenti:
class Esempio { public static void main( String args[] ) { System.out.println( Questo e il mio primo programma in Java ); } }
Abbiamo appena scritto un programma Java. Adesso salviamolo con il nome Esempio.java dentro la cartella Corso appena creata. A questo punto occorre aprire una finestra di comandi del DOS, cambiare directory in modo da posizionarsi nella stessa cartella in
javac Esempio.java
Se non compaiono messaggi derrore significa che il nostro programma non presenta errori di sintassi, e quindi il compilatore ha potuto produrre il file Esempio.class. A questo punto possiamo invocare la Macchina Virtuale di Java per eseguire il programma:
java Esempio
e nella riga immediatamente inferiore comparir la scritta Questo e il mio primo programma in Java (vedi figura).
Pu essere utile, per scrivere programmi in Java (ma anche in altri linguaggi di programmazione), usare i cosiddetti IDE, ossia Integrated Development Environment (Ambienti Integrati di Sviluppo), cio delle applicazioni che mettono a disposizione tutti gli strumenti che sono necessari per scrivere il programma stesso (quindi un editor di testi), per compilarlo e per eseguirlo. Nel caso del presente corso abbiamo scelto unapplicazione Freeware (ossia gratuita), Jcreator.
- 32 -
1. class Esempio { 2. public static void main( String args[] ) { 3. System.out.println( Questo e il mio primo programma in Java ); 4. } 5. }
Nella prima riga utilizziamo la parola chiave class per dichiarare che tutto quello che segue, dallapertura delle parentesi graffe fino alla chiusura alla riga 5, la definizione di una nuova classe. Esempio un identificatore, che costituisce il nome della classe. La riga 2 contiene linizio del metodo main. Notiamo che il metodo definito come public static void: tutte queste parole chiave verranno definite in seguito. Per il momento non vogliamo occuparci dei particolari, ma solo della struttura generale, che verr poi indagata pi a fondo. Dentro le parentesi tonde, subito dopo main, sono contenuti i parametri che possiamo passare al programma dalla linea di comando. Questi parametri sono sempre contenuti in un vettore (args[]) di tipo String (una stringa, come vedremo meglio in seguito, una sequenza di caratteri racchiusa da doppi apici). Tutti i programmi Java devono avere almeno una classe che contenga un metodo main: il metodo main il punto di ingresso nel programma, e cio quando eseguiamo un programma Java il sistema operativo passa il controllo al programma, che inizia la sua esecuzione eseguendo, una per una, le istruzioni contenute nel metodo main. Come si pu facilmente notare dal listato del programma, il corpo del metodo main , come il corpo della classe che lo contiene, delimitato da parentesi graffe. In Java un gruppo di linee di codice racchiuse tra parentesi graffe rappresentano un blocco di programma. In realt in questo semplicissimo programma il metodo main costituito da una sola istruzione, quella contenuta nella riga 3. In questa istruzione utilizziamo la classe System, tramite un suo oggetto, System.out, un oggetto predefinito della libreria di sistema di Java che permette di inviare un output allo schermo. In questo caso usiamo il metodo println delloggetto System.out: questo metodo accetta come parametro una stringa (la serie di caratteri racchiusa tra doppi apici), e la sua azione consiste nello stamparla sullo schermo. Notiamo subito due cose importanti: la prima, che per invocare un metodo di un certo oggetto si scrive il metodo delloggetto seguito da un punto e poi dal nome del metodo (System.out . println qui gli spazi sono stati inseriti per chiarezza). La seconda che per terminare unistruzione, in Java come in molti altri linguaggi, si usa il punto e virgola: in altre parole unistruzione pu essere scritta su pi righe diverse, con la convenzione che listruzione stessa deve necessariamente terminare con un simbolo di punto e virgola. In questa sezione, analizzando un semplice programma Java, sono stati introdotti velocemente e alla buona una quantit di concetti la cui spiegazione costituir il succo del seguito del corso.
- 33 -
- 34 analogamente al caso degli interi, per la grandezza dei numeri che possono contenere. Il tipo float utilizza quattro byte e pu contenere numeri fino a circa 10 38, mentre il tipo double utilizza otto byte e il numero massimo che pu contenere equivale a circa 10308. Ovviamente i due tipi si differenziano anche per la precisione dei calcoli: se bisogna affrontare un problema matematico che richiede una grande accuratezza numerica certamente consigliabile usare il tipo double per rappresentare numeri in virgola mobile.
4.6 Variabili
Allinterno di un programma Java, come per qualsiasi linguaggio di programmazione, la variabile lunita base di memorizzazione di un dato. Una variabile definita dalla combinazione di un identificatore (nome della variabile), un tipo e un inizializzatore opzionale. Inoltre tutte le variabili hanno un campo dazione, che ne definisce la visibilit, e una durata.
tipo
identificatore
[= valore];
dove le [] indicano una parte opzionale della dichiarazione. Vediamo degli esempi:
- 35 E possibile dichiarare pi variabili di uno stesso tipo nella stessa riga, separandone i nomi con una virgola:
int a, b=5, c=123; Una variabile pu anche essere definita dinamicamente, come nellesempio che segue:
class Dinamica { public static void main( String args[] ) { double a = 3.0, b = 4.0; double c = Math.sqrt( a*a + b*b ); System.out.println( Ipotenusa = + c ); } }
In questo semplice programma vengono dichiarate tre variabili locali, a, b e c. a e b vengono inizializzate ai valori rispettivamente 3.0 e 4.0 durante la dichiarazione, mentre c inizializzata dinamicamente (Math.sqrt(x) una funzione matematica che ritorna la radice quadrata dellargomento x).
tipo nome-array[];
in cui tipo definisce il tipo base di ciascun elemento dellarray. Per esempio, se vogliamo dichiarare un array di interi che andr a contenere i giorni in ciascun mese, possiamo scrivere
- 36 int giorniNelMese[];
Occorre fare attenzione al fatto che dopo la dichiarazione di un array in realt ancora non esiste nessun array. Per creare un array infatti occorre dire esplicitamente al compilatore quanti elementi vogliamo raggruppare nellarray, con listruzione:
giorniNelMese = new int[12]; che utilizza la parola chiave int. Dopo questa istruzione esister un array che contiene dodici elementi, ma occorre tenere presente che lindice va da 0 (primo elemento dellarray) a 11 (ultimo elemento dellarray). Quindi se vogliamo dire che i giorni nel mese di gennaio sono 31 dobbiamo scrivere
giorniNelMese[0] = 31; mentre se vogliamo inizializzare il valore relativo a giugno occorre scrivere
giorniNelMese[5] = 30; Un metodo alternativo per dichiarare allallocazione di memoria, il seguente: un array, che unisce la dichiarazione
4.7 Stringhe
In Java le stringhe, ossia sequenze di caratteri racchiuse tra doppi apici, non sono tipi semplici, ma sono implementate come una classe. Tuttavia limportanza delle stringhe nella programmazione in generale cos grande che sembra necessario fornire una breve introduzione alle stringhe prima di aver introdotto il concetto di classe. Per dichiarare una variabile di tipo stringa si ricorre allistruzione
- 37 -
4.8.1 Istruzione if
La forma pi semplice dellistruzione if la seguente:
if ( condizione ) { righe di codice; ... ... } In cui condizione unespressione booleana, che pu essere solo vera o falsa. Quando durante lesecuzione del programma si arriva allistruzione if vista sopra, se condizione vera allora verranno eseguite le righe di codice racchiuse tra la parentesi graffe, mentre se falsa lesecuzione continuer dalla riga seguente la chiusura delle parentesi graffe. Vediamo un semplicissimo esempio di utilizzo di istruzione if:
class EsempioIf { public static void main( String args[] ) { int x = 10; int y = 20; if ( x < y ) { System.out.println( x e minore di y ); } if ( x > y ) { System.out.println( x e maggiore di y ); } } }
- 38 -
Poich 10 minore di 20, la prima condizione vera, e la stringa x e minore di y verr stampata sullo schermo, mentre la seconda condizione falsa, e quindi la stringa x e maggiore di y non verr stampata. Una variante dellistruzione if la seguente
if ( condizione1 ) { codice1; ... ... } else if ( condizione2 ) { codice2; ... ... } else if ( condizione3 ) { codice3; ... } ... ... ... } else { codiceAlternativo; ... ... }
Da notare che gli else if possono essere tanti a piacere. Il significato dovrebbe essere chiaro: se la condizione1 vera viene eseguito codice1, altrimenti si passa al prossimo else if, e quindi se la condizione2 vera viene eseguito codice2, e cos via: se nessuna delle condizioni vera verr eseguito il blocco codiceAlternativo relativo a else.
- 39 -
class EsempioFor { public static void main( String args[] ) { int i; int n = 5; for ( i = 0; i < n; i = i + 1 ) { System.out.println( Il valore di i e: + i ); } } }
Se eseguito, questo programma dar il seguente output:
Chiaramente, quando i diventa uguale a 5 la condizione (i < n) non pi vera, e il ciclo si interrompe. Notiamo che listruzione i = i + 1 pu essere scritta in forma pi concisa come i++. Loperatore ++ ha leffetto di incrementare di uno la variabile intera a cui applicato.
- 40 -
class EsempioWhile { public static void main( String args[] ) { int i = 0; int n = 5; while ( i < n ) { System.out.println( Il valore di I e: + I ); i++; } } }
Loutput lo stesso del programma EsempioFor.
4.9 Operatori
Java offre un ambiente molto ricco per quel che riguarda gli operatori. In generale gli operatori possono essere suddivisi in quattro grandi gruppi: aritmetici, binari, relazionali e logici. Nel seguito descriveremo tutti i gruppi elencati a eccezione degli operatori binari.
- 41 -
OPERATORE + * / % ++ += -= *= /= %= --
RISULTATO Addizione Sottrazione Moltiplicazione Divisione Modulo Incremento Assegnazione addizione Assegnazione sottrazione Assegnazione moltiplicazione Assegnazione divisione Assegnazione modulo Decremento
DESCRIZIONE c = a + b; c = a b; c = a * b; c = a / b; c = a % b; // c il resto della divisione tra a e b c++; // c viene incrementato di 1 c += a; // c viene incrementato di a c -= a; // c viene decrementato di a c *= a; // c viene moltiplicato per a c /= a; // c viene diviso per a c %= a; // c = resto di c/a c--; // c viene decrementato di 1
Tutte le operazioni aritmetiche di base si comportano come ci si potrebbe ragionevolmente attendere. Da ricordare che loperatore Divisione tra interi comporta un risultato intero (senza parte frazionaria).
Da notare che tutti i risultati di queste operazioni sono valori di tipo boolean, e come tali possono e devono essere utilizzati nelle espressioni che controllano le istruzioni if e i cicli while. E da notare che loperatore di uguaglianza formato da due segni = posti accanto: questo per distinguerlo dalloperatore di assegnazione (un solo =).
- 42 -
- 43 -
La classe il "nucleo" di Java. il costrutto logico su cui si basa tutto il linguaggio Java perch definisce la forma e la natura di un oggetto. In quanto tale, la classe costituisce la base della programmazione orientata agli oggetti in Java. Qualunque concetto si desideri implementare in un programma Java deve essere incapsulato in una classe.
...
tipo variabile-istanzaN;
- 44 -
tipo nome-metodo1( elenco-parametri ) { Corpo del metodo1; } tipo nome-metodo2( elenco-parametri ) { Corpo del metodo2; } ... tipo nome-metodoN( elenco-parametri ) { Corpo del metodoN; } }
I dati, o le variabili, definiti allinterno di una classe sono chiamate variabili distanza. Il codice vero e proprio, ossia le istruzioni per operare sulle variabili, e quindi sui dati, contenuto allinterno dei metodi. Nel loro insieme, variabili e metodi di una classe sono chiamati membri della classe. Nella maggior parte dei casi, solo i metodi di una certa classe possono agire sui dati della stessa classe, quindi sono i metodi a determinare in che modo possono essere utilizzati i dati di una classe.
class Persona { String nome; String cognome; String telefono; Persona( String unNome, String unCognome, String unTelefono ) { nome = unNome; cognome = unCognome; telefono = unTelefono;
2 Nota Bene: durante le prove pratiche di laboratorio la classe Persona stata chiamata Elemento.
- 45 -
} }
E importante ricordare che una dichiarazione class ha il solo effetto di definire un modello, e non di creare un oggetto. Per creare un oggetto concreto di tipo Persona (ossia, come si dice in gergo java, per instanziare la classe Persona) si dovr scrivere, in un metodo di unaltra classe, listruzione
Persona p = new Persona( n, c, t ); in cui n, c e t sono variabili (o letterali) di tipo stringa che contengono rispettivamete il nome, il cognome e il numero di telefono della Persona i dati della quale vanno immessi nellagenda. Per fare un esempio concreto, potremmo scrivere
In entrambi i casi il risultato netto sar che la variabile p.nome conterr la stringa Mario, p.cognome la stringa Rossi e p.telefono la stringa 06/1234567. Notiamo che nella classe Persona c un metodo che ha lo stesso nome della classe, a cui passiamo tre variabili di tipo stringa: questo metodo si chiama costruttore, e viene usato appunto per costruire loggetto. In altre parole quando scriviamo new Persona( n, c, t ) stiamo chiamando il metodo Persona della classe Persona, ossia il costruttore della classe.
- 46 3. sfogliare lagenda. Nello scrivere la classe Agenda dovremo creare tre metodi che implementino nel mondo di Java le tre azioni che possiamo compiere nel mondo reale.
class Agenda { private Persona lista[]; final int MAX = 20; int n; Agenda() { lista = new Persona[MAX]; n = 0; } public void inserisci( String ilNome, String ilCognome, String ilTelefono ) { if ( n < MAX ) { lista[n] = new Persona( ilNome, ilCognome, ilTelefono ); n++; } else { System.out.println( "*** ERRORE: impossibile aggiungere numeri di telefono." ); System.out.println( " L'Agenda e' piena" ); } } public void stampa() { System.out.println( "\n\nLista dei numeri presenti nell'Agenda:\n\n" ); for ( int i = 0; i < n; i++ ) { System.out.println( "\t" + lista[i].nome + " " + lista[i].cognome + "......." + lista[i].telefono ); } System.out.println( "\n" ); } public void cerca( String ilCognome ) { int i; boolean trovato = false; for ( i = 0; i < n; i++ ) { if ( lista[n].cognome.equals( ilCognome ) ) { System.out.println( "\t" + lista[i].nome + " " + lista[i].cognome + "......." + lista[i].telefono ); trovato = true;
- 47 -
dire che la quantit di numeri di telefono inseriti pari a zero (n = 0); creare il vettore lista. Il metodo inserisci dichiarato public, perch deve poter essere chiamato
dallesterno di questa classe, e void, cio non restituisce nessun oggetto; riceve in ingresso tre variabile di tipo stringa (ilNome, ilCognome, ilTelefono). Prima di tutto questo metodo controlla che lagenda non sia piena: quindi if ( n < MAX ) crea un nuovo oggetto di tipo Persona, usando le tre variabili di tipo stringa ricevute dal metodo, e lo inserisce nella n-esima posizione del vettore lista (la posizione corrente). Dopo queste operazioni incrementa il valore di n (perch abbiamo aggiunto una persona allagenda). Se invece lagenda gi piena (perch abbiamo gi inserito 20 numeri) si limita a segnalare lerrore sullo schermo. Il metodo stampa molto semplice. Si limita a eseguire un ciclo for che ha come effetto quello di stampare sullo schermo i nomi, i cognomi e i numeri di telefono delle persone presenti nellagenda. Il metodo cerca invece il pi complicato dei tre. Notiamo che riceve in ingresso un oggetto di tipo stringa (ilCognome) che rappresenta il cognome da cercare nellagenda. Inizializza una variabile boolean (trovato) al valore false, per indicare che il cognome che cerchiamo non stato ancora trovato. Dopo di che inizia un ciclo for per scorrere tutte le persone presenti nellagenda, e controlla se il cognome delli-esima Persona corrisponde al cognome (ilCognome) che vogliamo cercare. Per svolgere questo controllo utilizza il metodo equals( String s ) della classe String. Notiamo infatti che lista[i].cognome un oggetto di tipo String. A questo oggetto applichiamo il metodo equals( ilCognome ) che restituisce un valore true se lista[i].cognome (la stringa oggetto al quale stiamo applicando il metodo equals) uguale a ilCognome (la stringa che stiamo passando come parametro al metodo equals), e restituisce un valore false altrimenti. Quindi if ( lista[n].cognome.equals( ilCognome ) ) allora scriviamo sullo schermo il nome, il cognome e il numero di telefono della persona trovata e mettiamo uguale a true la variabile
import java.io.*; class Rubrica { public static void main( String args[] ) throws IOException { Agenda a = new Agenda(); Reader r = new Reader(); boolean continua = true; while ( continua ) { char c = menu( r ); if ( c == 'q' ) { continua = false; } else if ( c == 'i' ) { inserisci( r, a ); } else if ( c == 'c' ) { //cerca(); } else if ( c == 's' ) { a.stampa(); } } } public static char menu( Reader lettore ) throws IOException { System.out.println( "\n\n System.out.println( " [i] telefono; " ); System.out.println( " [c] System.out.println( " [s] System.out.println( " [q] return lettore.readChar(); } Fai una scelta:\n\n" ); Inserisci nuovo numero di Cerca un numero di telefono;" ); Stampa l'Agenda;" ); Esci dal programma." );
- 49 -
public static void inserisci( Reader lettore, Agenda lAgenda ) throws IOException { System.out.println( "\n\n" ); System.out.print( "Inserisci il Nome: " ); String nome = lettore.readString(); System.out.print( "Inserisci il Cognome: " ); String cognome = lettore.readString(); System.out.print( "Inserisci il Numero di Telefono: " ); String telefono = lettore.readString(); lAgenda.inserisci( nome, cognome, telefono ); } }
Notiamo subito che il file Rubrica.java inizia con una dichiarazione di import;
import java.io.*;
Poich abbiamo bisogno di leggere un input da tastiera, durante il programma, stiamo importando tutte le librerie di input/output. Notiamo anche che stiamo usando la classe Reader (che abbiamo scritto durante le lezioni in Laboratorio) e che non sar descritta in nessun dettaglio. Ai nostri scopi baster dire che un oggetto di classe Reader, propriamente usato, ci permette di leggere numeri e stringhe che inseriamo con la tastiera durante lo svolgimento del programma. Anche le varie clausole di gestione degli errori (throws IOException) non saranno discusse. La cosa importante da notare in questa classe il fatto che serve come un semplice contenitore del metodo main, che a sua volta utilizza un metodo menu (cio un metodo che permette allutente di scegliere quale operazione compiere, di volta in volta: inserimento, ricerca o stampa.
- 50 Esistono altri metodi per inizializzare una stringa, che qui per non prenderemo in considerazione. Quel che vogliamo ottenere la conoscenza di alcuni metodi che possiamo applicare a un oggetto di tipo String per ottenere certi risultati.
la variabile lunghezza conterr il valore 11 (tanti quanti sono i caratteri di Mario Rossi, spazio incluso ovviamente).
int i = s.indexOf( M );
dar come risultato che la variabile i contiene il valore 0 (le posizioni dei caratteri partono da 0 e arrivano a N-1, se N la lunghezza della stringa), mentre listruzione
int j = s.indexOf( r );
int k = s.indexOf( s );
dar come risultato 8 (cio viene considerata la prima s della stringa) mentre listruzione
int q = s.indexOf( z );
int i = s.lastIndexOf( s );
restituir il valore 9;
char c = s.charAt( 1 );
String cognome = s.substring( 6 ); Se passiamo una sola variabile intera al metodo substring, diciamo k, il metodo restituisce la sottostringa che parte dal carattere nella posizione k-esima. Nel caso dellesempio sopra la variabile cognome conterr la stringa Rossi. Un altro modo di utilizzare substring il seguente:
I due numeri interi rappresentano lindice iniziale e lindice finale della sottostringa da estrarre. In questo caso nome conterr Mario.
String sSpazi =
Mario Rossi
String s = sSpazi.trim();
- 52 -
String minuscolo = s.toLowerCase(); // restituisce mario rossi String maiuscolo = s.toUpperCase(); // restituisce MARIO ROSSI;
String nome = Mario; String cognome = Rossi; String s = nome + + cognome; // restituisce Mario Rossi
boolean b = s.equals( Mario Rossi ); // restituisce true boolean b = s.equals( MARIO ROSSI ); // restituisce false
Se vogliamo comparare due stringhe indipendentemente dalle maiuscole e minuscole dobbiamo usare equalsIgnoreCase:
restituisce un valore > 0 (perch s, cio Mario Rossi, alfabeticamente maggiore di Giuseppe Verdi). Al contrario,
- 53 -