Vous êtes sur la page 1sur 43

Java

A scuola con PC Open

1a lezione

ProgrammareJava
per tutti
A
partire da questo numero vi proponiamo un corso di programmazione per principianti assoluti nel quale si far uso del linguaggio Java per tutti gli esempi e gli approfondimenti. La ragione della scelta di tale linguaggio va ricercata nelle sue peculiarit, che lo rendono idoneo per lo scopo e che risulteranno evidenti via via durante il corso. Per il momento ci limiteremo a citare il suo amplissimo campo dimpiego, lestrema facilit di reperire documentazione ed esempi di buon livello, il costo nullo, la possibilit di eseguire uno stesso programma su una vasta gamma di dispositivi.
di Marco Mussini

Inizia da questo mese un nuovo corso dedicato alla programmazione, rivolto ai neofiti. La scelta caduta su Java, linguaggio molto versatile e diffuso e la cui documentazione facile da reperire
Nel corso non si dar per scontata alcuna conoscenza specifica in tema di programmazione, mentre si assumer una normale dimestichezza nellutilizzo generico del PC e in particolare di Windows e delle sue applicazioni. Il corso organizzato per temi principali che saranno affrontati in sequenza, mantenendo un livello di complessit omogeneo, negli articoli mensili. Nel primo articolo introdurremo i concetti di base e la terminologia da conoscere e tratteremo un primo semplice caso pratico seguendo un metodo generale, arrivando a un programma Java funzionante sul vostro sistema. Chi ha gi

IL CALENDARIO DELLE LEZIONI


Lezione 1
Rudimenti di programmazione - Terminologia essenziale - Dallalgoritmo al programma - Preparazione dellambiente Java - Il primo programma - Programmazione a eventi - Gestione di immagini

Lezione 3
File - Salvare i dati - Elaborare i dati

Lezione 2
Elementi di programmazione a oggetti e grafica - Programmazione a oggetti - Creare una GUI
una certa esperienza in questo campo pu saltare le sezioni che affrontano argomenti elementari; tuttavia noi ne consi-

Lezione 4
Networking - Programmi client-server - Interazione con risorse Web
gliamo ugualmente una rapida lettura, magari per far emergere eventuali convinzioni imprecise.

1 Programmare: le basi
Esecutori e algoritmi
Ogniqualvolta si intende far compiere un determinato compito a un esecutore necessario descriverglielo in una forma a lui comprensibile. quindi fondamentale, nel farlo, immedesimarsi nellesecutore e nel suo modo di pensare e di agire. Nel caso particolare in cui lesecutore sia umano, per la descrizione del compito si pu usare il linguaggio naturale, a schemi e a gesti, e fare affidamento entro certi limiti nella capacit di intuizione e di imitazione. Naturalmente sempre preferibile cercare di non abusarne e fare invece in modo di spiegarsi in un modo che risulti: - inequivocabile: non ci devono essere ambiguit riguardo a ci che si deve compiere, quando, o come. - completo: tutte le situazioni che possono presentarsi durante lesecuzione del compito devono essere previste e una opportuna azione (o reazione) deve essere sempre precisata. Non si deve dare per scontata alcuna conoscenza o iniziativa. Se la descrizione del compito fatta rispettando tali vincoli, essa viene tecnicamente chiamata algoritmo. Se le prescrizioni sopra elencate appaiono scontate nel caso di esecutore umano, non stupir certo il fatto che esse rimangano assolutamente valide quando lesecutore una macchina, del tutto priva com di capacit intuitive e di intelligenza, a dispetto della sua potenza di calcolo tipicamente molto elevata (donde la locuzione, spesso usata, idiota veloce). Vi naturalmente un ulteriore requisito non proprio trascurabile, lalgoritmo deve anche essere: - comprensibile: ci si deve esprimere in una lingua o formalismo che lesecutore conosca. A questo riguardo vale la pena di sottolineare che cos come uno stesso algoritmo (per esempio: la procedura per addizionare due numeri interi) pu essere spiegato a esecutori umani di diverse nazionalit nelle loro rispettive lingue naturali, anche per i computer esiste una pluralit di linguaggi nei quali esprimere algoritmi. Un algoritmo espresso (in gergo: implementato) in un determinato linguaggio di programmazione detto programma.

Java
sempre esprimibili come un inI linguaggi di programmazio- sieme ordinato, pi o meno ne sono vecchi quasi quanto i grande, di operazioni elemencomputer e, come questi, han- tari appartenenti a queste cano subito una lunga storia evo- tegorie. lutiva, attraversando un certo importante sottolineare numero di epoche e mode che queste capacit sono cocon mutamenti a volte notevo- muni a tutti i linguaggi di proli nella loro concezione di base. grammazione, anche se espresPer questo spesso si parla, con se in forme spesso molto diuna semplificazione cronolo- verse. Tenendone conto, posgica un po forzata, di lin- siamo esprimere un algoritmo guaggi di prima, seconda, ...., N- in una sorta di linguaggio geesima generazione. Nella se- nerico, scrivendo quello che zione Linguaggi di programma- si chiama pseudo codice, che zione, troverete alcuni ap- risulter poi facile tradurre nel profondimenti. linguaggio particolare che deNellaccingersi a implemen- cideremo poi di adottare per tare un algoritmo, conviene te- limplementazione vera e pronere ben presente che qualun- pria. que sia il linguaggio di programmazione, lhardware, il si- Formulazione dellalgoritmo: stema operativo e la potenza un caso pratico Per vedere un caso pratico, della macchina, le operazioni che un computer classico consideriamo un gioco, indoin grado di eseguire possono vina il numero, per due giocaessere fatte rientrare in poche tori, che descriveremo cos: categorie fondamentali: A1 Prima descrizione 1. Ricordare. Memorizzare qualitativa dati; creare copie di dati (evenIl giocatore A inventa un tualmente con cambiamento di numero a caso; il giocatore formato); sostituire o cancellaB deve scoprirlo andando per re dati memorizzati. tentativi. Per ogni tentativo di 2. Calcolare. Innanzitutto B, A risponde annunciando calcoli aritmetici (le quattro se ha indovinato (nel qual operazioni), logici (algebra di caso la partita finisce e si Boole: And, Or, Not, Xor) e bit a proclama il numero di bit (per esempio: inverti lNtentativi impiegati) o se il esimo bit del K-esimo byte...), numero segreto maggiore ma anche tutti i calcoli pi o minore. complessi basati su questi, incluse, per esempio, funzioni triQuesta descrizione dovrebgonometriche e trascendenti. 3. Decidere. Confrontare da- be risultare perfettamente ti fra loro oppure con valori di comprensibile per qualsiasi riferimento e (importantissi- esecutore umano di lingua itamo) in base al risultato del con- liana che sia dotato di normali fronto fare una cosa oppure capacit intuitive e di iniziativa unaltra. Sono possibili anche autonoma. Sfortunatamente esso risulta incomprensibile decisioni a scelta multipla. 4. Ripetere o saltare. Reite- per un computer: per questo rirare una certa operazione, o chiede varie trasformazioni. gruppo di operazioni, per un Per esempio: certo numero di volte, o fino a Deve essere reso esplicito tutto ci che implicito. Per che non si verifica qualcosa di esempio, si deve precisare prestabilito; oppure saltare un che cosa si sottintenda in certo gruppo di operazioni e espressioni come per ogni passare direttamente a un altro tentativo di B o annunciare punto dellalgoritmo. se ha indovinato. 5. Comunicare. Scambiare dati e informazioni con lope- Si devono eliminare indicazioni inutili come lo scopo ratore umano o con altri comdel gioco (il giocatore B deputer o con periferiche dello ve scoprirlo) o descrizioni stesso computer: per esempio: qualitative del modo di proleggere da tastiera, mostrare a cedere, come andando per video, scrivere e leggere da ditentativi. sco o dalla rete, sentire i mo Si deve precisare come si vimenti del mouse, emettere possa raggiungere uno suoni. scopo: per esempio (si Tutte le operazioni complesproclama il numero di tense e tutti i programmi sono

1a lezione
A2

Linguaggi di programmazione

Algoritmo in formulazione sequenziale


1. Scegli un numero casuale, che non potr cambiare, e ricordalo: chiama X questo numero. 2. Ricevi dallesterno un numero (il tentativo) e ricordalo: chiama T questo numero. 3. Confronta T con X: 4. Se T maggiore di X, allora esegui da 5 a 7, altrimenti passa a 8. 5. scrivi Il numero segreto minore, 6. incrementa il contatore N, 7. torna al passo 2. 8. Se T minore di X, allora esegui da 9 a 11, altrimenti passa a 12. 9. scrivi Il numero segreto maggiore, 10. incrementa il contatore N, 11. torna al passo 2. 12. scrivi Hai indovinato. Numero di tentativi impiegati: 13. scrivi il contatore N, 14. termina tativi impiegati) che implica di aver svolto un conteggio durante la partita. Esso deve cio diventare un algoritmo, prima ancora che si possa tentare di tradurlo in un vero e proprio programma scrivendolo in un particolare linguaggio. Volendo riformulare la descrizione del gioco come un algoritmo e facendo giocare al computer nel ruolo di A, un esempio pi corretto (dal punto di vista di A) riportato nel riquadro A2. Questa nuova formulazione differisce dalla precedente sia per il tono sia per i contenuti. Non si descrivono obiettivi da raggiungere, ma si enunciano operazioni da eseguire. Tutti i casi possibili sono previsti e trattati esplicitamente, in modo esaustivo e senza lasciare spazio a interpretazioni. Si noti anche che tutte le operazioni indicate, che sono veramente elementari, appartengono alle cinque categorie elencate pi sopra. Questo tipo di operazione si chiama inizializzazione ed comunissima negli algoritmi e nei programmi. Ogni calcolo dovrebbe essere fatto su valori ben precisi; non si pu dire incrementa il contatore N tralasciando di stabilire chiaramente da quale valore iniziale si debba partire. Entit come T e N, destinate a contenere dei dati che durante lesecuzione dellalgoritmo potranno cambiare, sono definite variabili. Al contrario, entit come X, in cui vengono ricordati valori fissati una volta per tutte, sono definite costanti. Sia le variabili sia le costanti dovrebbero essere inizializzate prima delluso o comunque in occasione del primo uso. Successive operazioni di attribuzione di un valore ad una variabile non si chiamano pi inizializzazioni, ma assegnamenti. Le costanti, invece (coerentemente con il loro nome...), dopo essere state inizializzate non possono pi ricevere assegnamenti.

Variabili, costanti e inizializzazioni


Per la verit lalgoritmo non ancora perfetto: si parla di un contatore N, che viene ora incrementato, ora scritto, ma non si stabilisce quale debba essere il suo valore iniziale. invece necessario precisare che inizialmente tale contatore deve avere valore 1. Per questo necessario aggiungere allinizio dellalgoritmo un altro punto come questo: 0. Ricorda il contatore N e assegnagli 1 come valore iniziale

Decisioni, ripetizioni e salti


In un paio di punti dellalgoritmo sopra riportato si prescrive di tornare al passo 2. Lo scopo di queste indicazioni evidentemente quello di far ripetere (iterare, reiterare) una porzione dellalgoritmo, che nel nostro esempio quella in cui si riceve un tentativo e lo si confronta con il numero segreto. Istruzioni di questo tipo si chiamano costrutti iterativi. In altri punti compaiono indicazioni del tipo se.... allora esegui da 5 a 7 altrimenti passa a 8. In questo caso quello che

Java
si punta a ottenere eseguire
sentazione per gli algoritmi. Si tratta di una notazione grafica definita diagramma di flusso o flow chart. Questa rappresentazione ha il vantaggio di rendere immediatamente riconoscibile il funzionamento generale dellalgoritmo, la presenza di cicli e decisioni e il modo in cui questi sono organizzati. Sebbene siano previsti simboli standard per molti tipi di operazioni elementari, nella pratica comune possono bastare tre simboli fondamentali: il rettangolo (operazione generica), il rombo (decisione) e la freccia (flusso di esecuzione). A titolo di esempio, con questa notazione il nostro algoritmo pu essere rappresentato come in figura 1. 1

1a lezione

una sezione dellalgoritmo se e solo se una certa condizione vera. Di qui il nome di costrutti condizionali. In un caso cos semplice, con un algoritmo brevissimo ed una sola sezione ripetuta, indicazioni del tipo vai al passo 2 (tecnicamente definite salti) o esegui da 5 a 7 risultano ancora comprensibili e facili da seguire. Tuttavia, qualora lalgoritmo venisse modificato con linserimento di nuovi passi, la numerazione di quelli esistenti ne risulterebbe alterata; per esempio, il passo 2 potrebbe diventare il passo 3 e tutti i punti in cui si fa riferimento ad esso andrebbero modificati di conseguenza. Inoltre, in presenza di algoritmi pi complessi e con un alto numero di sezioni ripetute, magari anche contenute (innestate o nidificate) le une nelle altre, la notazione numerica comincerebbe a diventare scomoda. Per queste ragioni i costrutti iterativi e i costrutti condizionali vengono abitualmente descritti con una notazione pi comprensibile, come mostrato nel riquadro A3. Questo tipo di presentazione dellalgoritmo risulta assai pi leggibile nonostante la numerazione dei passi sia stata rimossa. Un notevole contributo viene dallespediente di far rientrare il margine sinistro del testo per meglio evidenziare i gruppi di istruzioni e il loro apparentamento con istruzioni condizionali (se... allora) o iterative (ripeti.... fino a....): questo accorgimento si chiama comunemente indentazione e i blocchi di righe interessati sono detti indentati. opportuno accennare anche a un altro tipo di rappreA3

Lalgoritmo del riquadro A3 rappresentato come diagramma di flusso

Algoritmi strutturati
La formulazione strutturata dellalgoritmo ne aumenta il grado di leggibilit al punto che tutti i linguaggi di programmazione pi recenti consentono (o addirittura impongono) di scrivere i programmi in questa forma. Sono quindi bandite istruzioni poco espressive e scomode sia da scrivere sia da leggere, come vai al passo 12064, esegui da 9324 a 10022, torna al passo 6750, ed esistono invece costrutti sintattici che descrivono lossatura logica del programma ricalcando esattamente le forme espressive usate nella formulazione strutturata degli algoritmi. Qui di seguito ne riportiamo un elenco: Esecuzione condizionale Selezione alternative Ripetizione semplice a condizione iniziale o a condizione finale Ripetizione con operazioni di conteggio e controllo

Per approfondimenti, il riquadro Strutture di Controllo (ultima pagina di questo articolo) mostra in dettaglio la corrispondenza fra pseudocodice, sintassi in Java e simbologia grafica.

Dallo pseudocodice a Java


A questo punto possiamo finalmente vedere come si presenta il nostro algoritmo tradotto da pseudo codice a codice Java vero e proprio. Riquadro A4. Il programma contiene ancora dettagli sintattici e istruzioni che risulteranno chiari solo nelle prossime lezioni. Tuttavia nella sua struttura risulta immersa, fedelmente riprodotta, la struttura logica dellalgoritmo del riquadro A3. Vi raccomandiamo ora di predisporre lambiente per lavorare in Java sul vostro PC. Fate innanzitutto riferimento alle procedure indicate nella sezione Preparazione dellambiente Java. Se non ne avete gi uno di vostro gradimento, dovrete anche scegliere un text editor per immettere il programma, per esempio fra quelli gratuiti da noi segnalati. Oppure potete optare fin dallinizio per un ambiente integrato di lavoro, che consente di operare in modo pi pratico: per esempio, le istruzioni per installare JCreator LE (gratuito) si trovano nellapposito riquadro. A installazione completata provate a scrivere, compilare ed eseguire il programma del

Riformulazione dellalgoritmo in forma strutturata


Scegli un numero casuale, che non potr cambiare, e ricordalo: chiama X questo numero Ripeti quanto segue fino a diversa indicazione: Ricevi dallesterno un numero (il tentativo) e ricordalo: chiama T questo numero Confronta T con X: Se T maggiore di X, allora: - scrivi Il numero segreto minore, incrementa il contatore N Altrimenti, se T minore di X, allora: - scrivi Il numero segreto maggiore, incrementa il contatore N Altrimenti: scrivi Hai indovinato. Numero di tentativi impiegati: scrivi il contatore N interrompi le ripetizioni

riquadro A4 seguendo le procedure di compilazione esecuzione mostrate per il caso editor + compilazione a linea di comando o per il caso di JCreator LE, a seconda dello strumento per il quale avete optato. Da notare che il listato Java contiene commenti in molte linee: i commenti sono compresi fra // e la fine della linea e possono essere omessi per risparmiare tempo di digitazione. In alternativa, naturalmente, trovate i listati sul CD ROM allegato a PC Open. Se non saranno stati commessi errori di trascrizione del listato, il programma in esecuzione apparir come mostrato nella figura sottostante. Segnaliamo in particolare che alla richiesta di un tentativo, in caso di immissione di un valore non interpretabile come numerico il programma reagir segnalando errore. Come esercizio vi proponiamo di analizzare il programma per cercare di individuare che cosa determina tale comportamento. 2

Ecco come apparir il programma in esecuzione

Java
A4

1a lezione

Lalgoritmo implementato in Java


// Preannunciamo al compilatore che useremo alcune classi di libreria import java.util.Random; // generatore di numeri pseudocasuali import java.io.InputStreamReader; //lettura a caratteri da flusso di input a byte import java.io.BufferedReader; //lettura a righe di testo da flusso di input a caratteri // Classe principale (e unica) del programma public class Indovina { // Metodo di innesco standard del programma e unico metodo della classe public static void main(String [] args) { // predisponi un contatore di tentativi 'n' con valore iniziale pari a 0 int n=0; // Scegli un numero casuale e ricordalo nella variabile 'x' Random r=new java.util.Random(); int x=r.nextInt(100)+1; // (condizione iniziale: numero non ancora indovinato) boolean indovinato=false; //(predisponi flusso in lettura per righe di testo e collegalo alla tastiera) BufferedReader br=new BufferedReader (new InputStreamReader (System.in)); // Ripeti quanto segue fintantoche il numero non viene indovinato // (v. condizione 'while' a fine ciclo) do { // (predisponi la cattura e gestione di eventuali errori verificatisi // nella decodifica del tentativo immesso) try { // Ricevi dall'esterno il tentativo e ricordalo come 't' System.out.println("Tentativo? "); // invita a inserire un valore String tentativo=br.readLine(); // leggi una stringa da tastiera int t=Integer.parseInt(tentativo); //converti stringa in num. int. if(t>x) // Se t e' maggiore di x... { // stampa messaggio... System.out.println("Il numero segreto e' minore"); n=n+1; // incrementa contatore di tentativi } else if(t<x) // Se t e' minore di x... { // stampa messaggio... System.out.println("Il numero segreto e' maggiore"); n=n+1; // incrementa contatore di tentativi } else // Per esclusione: t uguale a x - numero indovinato ! { // stampa messaggio... System.out.println("Hai indovinato!"); // stampa numero di tentativi... System.out.println("Numero di tentativi impiegati:"+n); indovinato=true; // segnala di interrompere le ripetizioni } } catch(Exception e) // Gestione degli errori catturati nel blocco try { System.out.println("Tentativo non valido - ripetere, prego"); } } while(indovinato==false); // Il ciclo termina solo quando 'indovinato' vale true } // fine metodo main } // fine classe Indovina

2 Preparazione dellambiente Java

rima di poter scrivere ed eseguire programmi in Java necessario dotarsi dei ferri del mestiere. Vediamo quindi quali sono e che cosa occorre fare per installare un ambiente di lavoro completo nel sistema.

ticamente alla sua indentazione. Oppure, nel caso in cui il file sia stato scritto in precedenza, pu offrire comandi per indentare o de-indentare gruppi di linee selezionati.

Copie di backup automatiche


Una funzione molto utile nel deprecato caso in cui leditor vada in crash, rischiando di far perdere il lavoro.

Il Text Editor
Si tratta dello strumento di uso quotidiano per il programmatore e la sua scelta non andrebbe mai compiuta distrattamente. Un buon editor facilita il lavoro, aiuta a prevenire gli errori, rende meno stancante lattivit e pu ridurre anche di molto il tempo necessario per scrivere, provare e correggere i programmi. Ricordiamo qui di seguito alcune delle funzioni pi importanti.

Undo potenziato
Rispetto allundo normale che ricorda tuttal pi quello che stato fatto dopo lultimo salvataggio, lundo di un buon editor dovrebbe essere in grado di ricordare e annullare anche modifiche precedenti (unlimited undo/redo capability).

Supporto specifico per il linguaggio usato


Leditor pu conoscere la sintassi del linguaggio adoperato e avvalersi di questa conoscenza per fare un controllo di validit sintattica del codice sorgente mentre lo si scrive. Inoltre, per aumentarne la leggibilit, pu evidenziare con colori diversi le parole chiave del linguaggio, i nomi di variabili e costanti, i valori, i commenti e cos via (syntax highlighting). Alcuni editor, quando riconoscono che lutente sta iniziando a scrivere un costrutto del linguaggio, inseriscono automaticamente la punteggiatura necessaria e le eventuali parentesi (completamento automatico). Molto comoda, specie con linguaggi la cui sintassi ricca di parentesi e di blocchi con indentazione, la funzione che, quando il cursore posizionato su una parentesi, riconosce ed evidenzia in colore la parentesi ad essa associata (bracket matching).

Funzioni per lavorare sui blocchi


I migliori text editor consentono operazioni di taglia e incolla non solo su sezioni di testo contiguo, ma anche su rettangoli di testo e colonne.

Comode funzioni di movimento


essenziale che la diteggiatura del programmatore sulleditor sia aiutata al massimo da una opportuna scelta di tasti (che dovrebbero essere ben disposti sulla tastiera e non troppo diversi da quelli degli altri programmi) e da speciali combinazioni dedicate allo spostamento fra righe, parole e blocchi.

Numerazione delle linee di codice


I compilatori segnalano gli errori dando come riferimento il numero della linea del sorgente in cui essi si trovano. Se leditor non fornisce lindicazione dei numeri di linea, o se non prevede almeno una funzione Go-to per posizionarsi direttamente a una certa linea del sorgente, il lavoro diventa veramente arduo.

Indentazione del codice automatica o assistita.


Durante la digitazione, leditor pu riconoscere che si sta scrivendo un blocco di codice e provvedere automa-

Visualizzazione gerarchica dei costrutti sintattici


Specialmente in file sor-

Java
genti di notevole lunghezza e
con costrutti sintattici profondamente annidati pu succedere di perdersi cercando di seguire su schermo il flusso di esecuzione del programma. Questa funzione consente di collassare o espandere un blocco di cui momentaneamente non interessa vedere il contenuto. ti gli strumenti di base necessari (ad eccezione del text editor, da procurarsi a parte). Al momento in cui scriviamo il JDK arrivato alla versione 1.5.0 Update 1; possibile scaricarlo dal sito http://java.sun.com, oppure potete trovarlo sul CD Guida 2 (CD2, PDF, Corsi, Java). Fra gli strumenti dellambiente Java, quelli di utilizzo quotidiano (a parte, naturalmente, leditor) sono i seguenti (figura 3): javac.exe il compilatore Java, che traduce i sorgenti Java (files con estensione .java) in bytecode (files con estensione .class). Deve essere utilizzato dopo ogni modifica al programma, per aggiornare la versione eseguibile. java.exe linterprete di bytecode, usato per eseguire i programmi Java dopo che sono stati compilati con javac.exe. jar.exe lo strumento per confezionare dei files .jar (Java ARchive), che raccolgono tutte le classi e i file di risorse necessari per il funzionamento di un programma. Risulta quindi possibile distribuire un programma Java, inclusi file di configurazione, help, file grafici e sonori, semplicemente consegnando un singolo file.

1a lezione

Installazione di JCreator LE
Alcuni programmatori sono abituati a scrivere il programma in un editor puro ed eseguire le compilazioni a linea di comando da una finestra DOS, con un pieno controllo su opzioni e processo di generazione. Altri invece preferiscono lavorare in un ambiente integrato (IDE) che offra innanzitutto un facile accesso a tutte le funzioni. Vediamo dunque la procedura da seguire per linstallazione di JCreator LE, un buon IDE freeware specializzato nella gestione di progetti in Java. Da notare che la procedura va eseguita a JDK e relativa documentazione gi correttamente installati.
Una volta lanciato il programma di installazione (lo trovate anche sul CD ROM allegato a PC Open) viene innanzitutto chiesto a quali tipi di file si voglia associare il programma. I files .java sono i sorgenti Java: grazie allassociazione, con un doppio clic verranno aperti direttamente in JCreator. Gli altri due tipi di file sono file di servizio del programma. Accettiamo i default e procediamo.

Integrazione con gli altri strumenti di programmazione


Lattivit del programmatore una continua ripetizione del ciclo scrittura sorgente compilazione test. Risulta comodo poter lanciare la compilazione direttamente dallinterno delleditor, dal momento che tipico che vengano segnalati errori e che questi debbano essere corretti nelleditor prima di ritentare la compilazione. Strumenti che integrano in modo molto spinto tutti i tool di programmazione sono spesso chiamati IDE (Integrated Development Environment). Vi segnaliamo i seguenti editors per Windows completamente gratuiti e adatti alluso con programmi Java: vi consigliamo di provarli e di selezionare quello che preferite. Li trovate sul CD Guida 2 allegato a PC Open. ConTEXT CPad Crimson Editor Source Edit SYN Per chi cercasse un ambiente completo anzich un semplice editor, segnaliamo invece linteressante JCreator LE, un IDE potente, leggero e gratuito. Dal momento che questo tipo di prodotti si integra con il JDK, linstallazione leggermente pi complessa di quella di un semplice editor; chi fosse interessato pu fare riferimento allapposito riquadro Installazione di JCreator.

Installazione
Linstallazione completa del JDK richiede circa 190 MB, mentre linstallazione della ricchissima documentazione (vivamente consigliata) ne richiede circa altri 250. Vediamo la procedura da seguire per linstallazione e configurazione. Iniziamo lanciando il programma di installazione del JDK. Per prima cosa ci viene richiesto di accettare laccordo di licenza (figura 4). Nella seconda fase viene offerta la possibilit di selezionare i componenti da installare. Consigliamo vivamente una installazione completa. (figura 5). Prima di passare alla schermata successiva, consigliamo di cambiare il percorso di in-

Nel secondo step ci viene chiesto di indicare sotto quale directory stato installato il JDK. Questa indicazione necessaria perch lIDE ha la necessit di invocare javac.exe o java.exe quando si selezionano le corrispondenti opzioni nei suoi menu. Nel nostro caso il JDK installato sotto c:\jdk150. Selezioniamo quindi questo percorso e procediamo.

Nellultima fase dobbiamo informare il programma di installazione su dove si trovi la documentazione di Java. Ci permetter allIDE di visualizzarla quando richiesto dallutente. Procediamo dopo questultima fase e linstallazione avr termine.

Il Java Development Kit


Per lo sviluppo di programmi Java disponibile gratuitamente il Java Development Kit (JDK) un kit comprendente tut3

Ora che lIDE installato, verifichiamo il buon funzionamento del sistema provando a immettere un programma di prova. Per prima cosa creare una cartella C:\java. Una volta lanciato JCreator, dal menu File selezionare New. Nel dialog box che appare, scegliere la scheda Files, poi licona Java File. Nella casella Filename immettere Ciao.java e come location indicare il percorso C:\java. Premere OK e il file verr creato e aperto nella finestra di editing.

Gli strumenti dellambiente Java di utilizzo quotidiano

Java
stallazione (usando il pulsante Change). Quello proposto per default, infatti, lungo e scomodo da digitare. Meglio quindi installare in una directory dal nome breve e posizionata direttamente sotto la radice del disco, come c:\jdk150. (figura 6). Pu ora avere inizio linstallazione vera e propria del JDK, che richiede alcuni minuti anche su macchine di buona potenza. (figura 7) Dopo il JDK (Java Development Kit) viene installato il JRE (Java Runtime Environment). Anche in questo caso lasciamo invariate le opzioni. Stavolta non importante che la directory di installazione sia breve, quindi si pu tranquillamente accettare il default (figura 8). Al termine dellinstallazione del JRE viene richiesto di indicare a quali browser dovr essere agganciato il Java Plug-in, che consente lesecuzione degli applet presenti nelle pagine Web. Consigliamo di installarlo in tutti i browser presenti (figura 9). A questo punto linstallazione del JDK completata. Di norma non richiesto un riavvio dopo loperazione, ma nel caso venisse richiesto, lasciamo fare (figura 10). Possiamo verificare che linstallazione sia andata a buon fine aprendo una finestra MSDOS e digitando il comando java version. Dovremmo ottenere un messaggio che riporta la versione di Java installata, come mostrato in figura (figura 11). Dopo il JDK ora il momento di installare la documentazione tecnica e di riferimento. Tutto quello che occorre fare estrarre in una cartella a piacere lintero contenuto del file jdk-1_5_0-doc.zip. Noi abbiamo scelto di estrarlo in c:\jdk150docs. Ricordarsi di selezionare lopzione che mantiene la struttura delle cartelle, altrimenti la documentazione risulter completamente inutilizzabile (figura 12). 4

1a lezione

Nella finestra di editing digitare il seguente programma esattamente come riportato in figura

Si procede ora alla compilazione del programma. Loperazione effettuabile senza nemmeno uscire dallIDE. Dal menu Build selezionare Compile file.

Se la compilazione andata a buon fine, nel pannello inferiore appariranno i messaggi riportati in figura

interessante a questo punto verificare la situazione sul file system. Aprire una finestra di Esplora risorse e portarsi su C:\java. Come si pu notare, accanto al file Ciao.java comparso il file Ciao.class: questo file contiene il bytecode corrispondente al sorgente ed il risultato della compilazione.

A questo punto si pu mandare in esecuzione il programma. Sempre dal menu Build, selezionare Execute File:

Il programma viene lanciato ed il suo output appare in una finestra DOS, come riportato in figura

Collaudo
Ora che linstallazione del JDK completa conviene verificare che tutto sia regolarmente funzionante provando a immettere, compilare ed eseguire un programma minimo. Per prima cosa, utilizzando

Java
9 14

1a lezione
16

17 10 15

18

11
Linstallazione del Java Development Kit (JDK) richiede circa 190 MB. Ecco alcune fasi dellinstallazione e della configurazione

12

13

un editor (per il momento pu bastare anche il semplice Blocco Note di Windows, raggiungibile da Start/Programmi/Accessori), creare un file di testo e digitare il programma di prova esattamente come mostrato in figura (figura 16). Salvare il file nella directory C:\java dandogli il nome Ciao.java. Assicurarsi (con Esplora Risorse) che il file abbia ricevuto il nome Ciao.java e non Ciao.java.txt; se necessario, correggere il nome in modo che sia esattamente Ciao.java. Una premessa: per scrivere programmi Java sono assolutamente indispensabili due caratteri non presenti sulle tastiere italiane: le parentesi graffe aperte e chiuse. Ci sono tre modi per immetterle: Usare le combinazioni SHIFT+ALT GR+[ e SHIFT+ALT GR+], rispettivamente, per ottenere { e }. Attivare il Num Lock sul pad numerico della tastiera, dopodich tenere premuto ALT e premere in successione i tasti 0,1,2,3 del pad numerico. Questo far apparire la

parentesi graffa aperta. La parentesi graffa chiusa si ottiene in modo analogo, ma la sequenza da digitare 0,1,2,5. Dal Pannello di Controllo selezionare Opzioni Internazionali e della lingua, Lingue, Dettagli, Impostazioni, premere Aggiungi, dallelenco Lingua scegliere Inglese (Stati Uniti) e da Layout di tastiera scegliere Stati Uniti . Premere OK , OK , OK . Sulla barra Start, premere il tasto destro del mouse in una zona libera. Nel sottomenu Barre degli Strumenti attivare lopzione Barra della lingua . Apparir unicona sulla barra Start. Questa icona consente di commutare la lingua e il layout di tastiera fra inglese (EN) e italiano (IT). Ci possibile facendo clic sullicona stessa oppure, pi semplicemente, con la combinazione di tasti ALT sinistro + Maiuscole . Nel layout di tastiera inglese, le parentesi quadre si trovano nello stesso punto in cui si trovano sulla tastiera italiana (ma senza premere ALT GR), mentre gli stessi due tasti (+ Maiuscole) danno le pa-

rentesi graffe. Per compilare il programma Ciao.java aprire una finestra MS-DOS (Prompt dei comandi) e immettere i seguenti comandi: cd c:\java C:\jdk150\bin\javac Ciao.java il primo comando serve per posizionarsi nella directory di lavoro; il secondo chiama il compilatore sul programma sorgente Ciao.java, per generare il corrispondente file bytecode, Ciao.class. Se il programma non conteneva errori e la compilazione ha avuto successo, si presenter una situazione come quella riportata in figura (figura 17). Ora si pu mandare in esecuzione il programma. Per fare questo immettere il comando java Ciao Se non sono stati commessi errori il programma entrer in funzione e stamper Ciao! sul terminale, cos: (figura18) A questo punto possiamo considerare verificata la corretta installazione dellambiente Java!

Java

1a lezione

4 Linguaggi di programmazione

l gran numero di linguaggi esistenti pu disorientare chi si accosta per la prima volta alla programmazione. Che differenza c fra Java e Javascript? Perch certi siti Web sono realizzati in PHP e altri in Perl? meglio un linguaggio interpretato o compilato? Che cosa sono i linguaggi di scripting? HTML pu definirsi un linguaggio di programmazione? Che cosa significa programmare a oggetti? Qual il linguaggio migliore, quello che conviene imparare? E cos via. Anche se per questo corso abbiamo deciso di adottare il linguaggio Java, per ragioni che risulteranno sempre pi chiare strada facendo, giusto e utile ricordare brevemente le caratteristiche e le attitudini degli altri linguaggi di programmazione, soprattutto per sapersi meglio orientare nelle proprie scelte. Certo, elencare tutti i linguaggi finora comparsi sulla scena sarebbe lungo e sostanzialmente inutile. Qui ci limiteremo a proporre innanzitutto due temi di riflessione utili come chiave di lettura: - lobiettivo dellintroduzione di linguaggi di programmazione di nuovo tipo tipicamente stato quello di rendere pi facile e veloce la scrittura di programmi di complessit crescente, o per particolari classi di problemi, riducendo per quanto possibile la possibilit di commettere errori. - il semplice fatto che a tuttoggi continuino ad essere usati numerosi linguaggi diversi per classi di applicazioni diverse sembra dimostrare che non esiste (ancora?) il linguaggio di programmazione perfetto, ideale per ogni campo e situazione dimpiego. Daltra parte, persiste una certa tendenza a perseguire il perfezionamento dei linguaggi di programmazione pi attraverso la continua introduzione di nuovi linguaggi che attraverso un processo di affinamento di quelli esistenti. I linguaggi di programmazio-

ne possono inoltre essere classificati secondo le loro caratteristiche. Le principali categorie che opportuno conoscere sono le seguenti.

Linguaggi compilati
Una volta scritti, per poter essere eseguiti i programmi vengono tradotti una volta per tutte in linguaggio macchina, lunica forma direttamente comprensibile dallhardware. Questa traduzione viene effettuata automaticamente da appositi programmi denominati compilatori. Il programma compilato non richiede pi ulteriori elaborazioni e pu essere mandato direttamente in esecuzione tutte le volte che occorre.

esso sar compatibile con molti tipi diversi di computer e potr fra laltro essere venduto su un mercato pi ampio. Tipizzazione. Sono detti tipati o fortemente tipati quei programmi nei quali prima di poter utilizzare, immagazzinare, elaborare, trasferire dati necessario dichiarare esplicitamente di che tipo di dati si tratta. Questo consente generalmente di prevenire un gran numero di possibili errori di esecuzione dei programmi. Se il tipo dei dati sempre noto prima ancora di iniziare lesecuzione, si parla di tipi statici, altrimenti si parla di tipi dinamici.

Linguaggi interpretati
Per lesecuzione del programma viene usato un altro programma denominato interprete che lo esamina ed esegue le operazioni corrispondenti alle istruzioni che lo compongono. Linterprete cos denominato perch da un lato legge un certo linguaggio (per esempio: BASIC) e dallaltro parla allhardware nella lingua che questo in grado di capire: il linguaggio macchina. Per quanto linterprete possa essere ben progettato, i linguaggi eseguiti in questo modo hanno sempre prestazioni inferiori a quelle ottenibili con la compilazione. In compenso un programma interpretato pu essere mandato in esecuzione appena scritto, senza aspettare la compilazione (unoperazione che per programmi di grandi dimensioni pu durare anche parecchi minuti).

Paradigma di programmazione
Linguaggi macchina. Si tratta di rudimentali linguaggi nei quali le istruzioni sono espresse per mezzo di numeri binari. Possono essere compresi ed eseguiti direttamente dallhardware, senza modifiche n traduzioni. Per la loro vicinanza al livello dellhardware e per il basso livello di astrazione che consentono, sono detti spesso linguaggi di basso livello. Per il fatto che questi linguaggi sono stati i primi a comparire sulla scena, in contemporanea con le macchine, a volte sono anche detti linguaggi di I generazione. Linguaggi simbolici assemblatori. Questi linguaggi (a volte detti linguaggi di II generazione) costituiscono un primo tentativo di astrarsi dallo scomodo cifrario dei linguaggi macchina: i programmi sono ancora composti di istruzioni che corrispondono quasi esattamente alle istruzioni macchina, ma questa volta la forma espressiva pi comprensibile, essendo testuale e generalmente simile alla lingua inglese. Per esempio, listruzione di somma viene indicata come ADD anzich come 01011101. Lo stesso programma riscritto in Assembler si presenta cos. Linguaggi procedurali. In questi linguaggi lenfasi della

Caratteristiche generali
Portabilit. Un linguaggio si dice portabile quando i programmi scritti con esso possono essere portati ed eseguiti su molti tipi diversi di computer senza bisogno di adattamenti complessi. I linguaggi pi portabili sono supportati anche su centinaia di architetture diverse. Questa propriet aumenta evidentemente il valore del programma, in quanto

programmazione viene posta primariamente sulla descrizione della procedura da seguire per elaborare i dati. I programmi non sono completamente lineari e sequenziali, ma modulari, in quanto possono essere strutturati come un insieme di unit pi piccole (funzioni e procedure) ognuna delle quali provvede ad eseguire un compito semplice. Gli esempi pi noti sono il C e il Pascal o le pi recenti varianti di BASIC. Linguaggi a oggetti. Linguaggi nei quali al centro dellattivit del programmatore viene posto il progetto dei tipi di dato da trattare, dellaffinit che li lega e delle operazioni possibili su di essi. I dati trattati sono visti come oggetti sui quali agiscono particolari procedure definite metodi. Oggetti dello stesso tipo sono caratterizzati dallappartenenza a una certa classe, che descrive un insieme di propriet e metodi che li accomunano. La presenza di aspetti comuni a pi classi, ove presente, viene descritta dalla relazione di ereditariet e sfruttata per semplificare la scrittura e la manutenzione dei programmi. C++ e Java sono i pi noti e diffusi linguaggi di questo tipo. Linguaggi funzionali. In questi linguaggi i programmi sono visti come funzioni matematiche e lesecuzione dei programmi di conseguenza assimilata al calcolo di queste funzioni. Questa formulazione si rivela particolarmente espressiva per certi particolari tipi di programmi, ma pu risultare assai criptica per molti altri. Lutilizzo della ricorsione, un meccanismo per mezzo del quale una funzione definita in termini di se stessa (per esempio: il fattoriale di un numero N>1 pari a N moltiplicato per il fattoriale di N-1...), fra i costrutti pi spesso usati nei linguaggi funzionali per implementare algoritmi complessi. Il Lisp il pi noto rappresentante di questa categoria. Linguaggi di scripting. La specializzazione originaria di

Java
questa classe di linguaggi quella dellespressione di procedure da usarsi per coordinare e interpolare il lavoro di altri programmi gi scritti. Per rendere lidea, se i mattoni fossero scritti in C, C++ o Java, allora i linguaggi di scripting sarebbero la calce, con un ruolo complementare e non antitetico. Questi linguaggi (quasi sempre interpretati) solitamente eccellono nellelaborazione dei risultati intermedi di altri programmi, nel richiamo e gestione di programmi, nellesecuzione in forma automatizzata e ripetitiva di gruppi (batch) di comandi normalmente immessi manualmente dalloperatore. In generale essi tendono a privilegiare la sinteticit e limmediatezza duso rispetto alle prestazioni, alla correttezza, alla leggibilit e alla manutenibilit dei programmi. Ferma restando questa impronta culturale di base, i pi recenti linguaggi di scripting sono stati dotati di potenzialit ormai equivalenti a quelle dei linguaggi classici. Fra i linguaggi di scripting si annoverano Tcl, Perl, Python, PHP, il linguaggio batch della shell di MS-DOS (files .BAT), il VBScript e cos via. guaggio capito direttamente dallhardware (linguaggio macchina o codice macchina) e per questo definito di basso livello. Concetti ed istruzioni sono in effetti estremamente elementari. Scrivere programmi complessi esprimendosi direttamente in questo linguaggio oggettivamente scomodo e faticoso, ma grazie al totale controllo che esso fornisce sul funzionamento dellhardware possibile, con qualche accortezza, ottenere le massime prestazioni possibili con una determinata macchina. start: mov mov int message db dx,offset message ah,09h 21h 'Hello World!',13,10,'$' Qualunque algoritmo viene espresso come un teorema da dimostrare. Assiomi, teoremi di repertorio e regole di deduzione sono forniti dal programmatore; al resto (trovare la dimostrazione) pensa il computer. Come il LISP, rende elegante ed estremamente sintetica la programmazione di certe classi di applicazioni e farraginosa quella di tutte le altre. Il Prolog praticamente caduto in disuso. ?- write('Hello World'), nl.

1a lezione
guaggio di alto livello come il Pascal. Per un lasso di tempo di quasi 20 anni stato il linguaggio generico pi diffuso. ancora molto usato in ambiente UNIX, meno in ambiente PC dove strumenti pi moderni sono largamente disponibili da tempo. #include <stdio.h> main() { printf("Hello World\n"); }

C++
Arricchisce un linguaggio di impiego generale e di enorme diffusione, il C, con il concetto di programmazione orientata agli oggetti o alle classi (OOP) in precedenza proposta da linguaggi mai realmente usciti dallambiente accademico, come Eiffel, o di nicchia, come Smalltalk. Il tutto senza importante degrado di prestazioni. Queste caratteristiche ne spiegano la rapidissima adozione (curva di apprendimento non ripida per chi proveniva dal C) e la longevit (il paradigma OOP lo ha reso idoneo a trattare applicazioni complesse e a supportare metodologie di progetto moderne). #include <iostream.h> void main() { cout << "Hello World" << endl; }

PASCAL
Un linguaggio procedurale, fortemente tipato, inizialmente proposto con intenti soprattutto didattici, che si successivamente imposto per il suo rigore formale e sintattico e per la sua disponibilit su microcomputer di ampia diffusione. PROGRAM HelloWorld; BEGIN WRITELN('Hello World'); END

FORTRAN
(FORmula TRANslator). Uno dei primi linguaggi di grande successo ad aver introdotto un livello di astrazione pi elevato, specialmente per quanto riguarda le espressioni matematiche, e un adeguato sistema per linput/output di dati formattati su schermo, tastiera, stampante e disco. PROGRAM HelloWorld PRINT *, "Hello World" END PROGRAM HelloWorld

Piccola antologia dei linguaggi


Ecco infine una breve presentazione di alcuni fra i pi noti linguaggi rappresentativi delle varie generazioni o tipologie che si sono succedute. Ometteremo di citare i linguaggi di scripting in quanto parzialmente estranei al focus di questo corso. A titolo di esempio mostrata limplementazione di uno stesso programma (che stampa semplicemente il messaggio Hello, World!) in ognuno dei linguaggi citati.

LISP
(LISt Processing) Il linguaggio funzionale per eccellenza. Qualunque algoritmo viene espresso come una funzione matematica. I programmi tendono a essere eleganti e leggibili per certe classi di algoritmi che ben si prestano a tale notazione, e oltremodo criptici per tutte le altre. Tradizionalmente associato, pi per ragioni di contemporaneit che per oggettiva specificit, ad applicazioni di intelligenza artificiale. Nella sua epoca di massima popolarit furono anche costruite macchine (LISP machines) il cui hardware era in grado di capire direttamente il LISP. Oggi usato come linguaggio di programmazione interno di certe applicazioni. (DEFUN HELLO-WORLD () (PRINT (LIST 'HELLO 'WORLD)))

BASIC (Beginners All-purpose Symbolic Instruction Code) Nato negli anni 60 apparendo come una sorta di Pascal volgarizzato, non senza un certo retrogusto di Assembler, inizialmente adottato come linguaggio di programmazione di base da tutti gli home computers, col passare degli anni ne sono state proposte innumerevoli varianti arricchite e potenziate. Relativamente intuitivo e con una curva di apprendimento morbida, ancora molto usato e conosciuto.
10 print "Hello World!"

Java
Abbastanza simile al C++ per quanto riguarda la sintassi, costituisce un importantissimo passo avanti dal punto di vista della portabilit, della ricchezza delle librerie a corredo, del supporto per il Web, linternazionalizzazione e la programmazione concorrente, della prevenzione degli errori, della facilit duso, della rapidit con cui possono essere scritti programmi complessi. La contropartita un certo degrado di prestazioni rispetto al C++, avvertibile soprattutto con applicazioni grafiche complesse. class HelloWorld { public static void main (String args[]) { System.out.print("Hello World "); } }

C
Nato negli anni 70 agli AT&T Bell Labs, il linguaggio in cui furono scritte vere e proprie pietre miliari dellinformatica, fra cui UNIX, il TCP/IP e X Windows. Assomiglia per certi versi al Pascal, ma meno rigoroso, consente trucchi e scorciatoie discutibili da un punto di vista teorico ma assai gradite ai programmatori di sistema che vogliono un controllo sullhardware simile allassembler senza rinunciare alle comodit e alla disciplina di un lin-

Linguaggio macchina
Un programma che stampa Hello, world! in linguaggio macchina Intel 8086 sotto MS-DOS si presenta cos: BA 0B 01 B4 09 CD 21 B4 4C CD 21 48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 21 13 10 24

Assembler
Si tratta della rappresentazione testuale dellunico lin-

PROLOG
(PROgrammazione LOGica).

Java

1a lezione

5 Strutture di controllo
Esecuzione condizionale (costrutto if)
Pseudocodice Se vero che (condizione logica) allora esegui (blocco di operazioni 1) altrimenti esegui (blocco di operazioni 2) Esempio Java if(a==4) { /* ..operazioni 1.. */ } else { /* ..operazioni 2.. */ } Flow chart

Selezione alternative (costrutto switch)


Pseudocodice Considera il valore di (variabile): Nel caso (valore1) esegui (gruppo operazioni 1) Nel caso (valore2) esegui (gruppo operazioni 2) ... Nel caso (valoreN) esegui (gruppo operazioni N) In tutti gli altri casi esegui (gruppo operazioni per gli altri casi) default: /* ..operazioni.. */ break; } Esempio Java switch(a) { case 5: /* ..operazioni.. */ break; case 71: /* ..operazioni.. */ break; Flow chart

Ripetizione semplice variante a condizione iniziale (costrutto while)


Pseudocodice Fintantoch risulta vero che (condizione logica) ripeti (blocco operazioni) Esempio Java while(a < 100) { /* .. operazioni .. */ } Flow chart

Ripetizione semplice variante a condizione finale (costrutto do..while)


Pseudocodice Esegui (blocco operazioni); se risulta vero che (condizione logica) ripeti il tutto Esempio Java do { /* .. operazioni .. */ } while (a < 100); Flow chart

Ripetizione con operazioni di controllo o conteggio (costrutto for)


Pseudocodice Esegui (inizializzazione); dopodich: se vero (condizione logica) allora esegui (blocco istruzioni); poi esegui (aggiornamento valori); quindi ripeti quanto sopra eccetto inizializzazione Esempio Java for(i=1; i<100; i=i+1) { /* .. operazioni .. */ } Flow chart

Java
A scuola con PC Open

2a lezione

ProgrammareJava Gli oggetti


di Marco Mussini

In questo secondo appuntamento andremo ad approfondire i concetti fondamentali del linguaggio per poi entrare nel vivo della programmazione: Java ci aiuter a scoprire oggetti, eventi e interfacce grafiche

ella prima puntata abbiamo affrontato i problemi concettuali di base della programmazione introducendo nel contempo la terminologia essenziale e i costrutti logici e sintattici di uso pi frequente. In parallelo abbiamo visto come allestire ed utilizzare un ambiente di programmazione per Java, arrivando a realizzare un primo semplice programma funzionante, seppure privo di interfaccia grafica o di interazione con rete e file.

L'obiettivo della seconda lezione del corso, che vi proponiamo questo mese, quello di completare il bagaglio di concetti di base. Andremo a illustrare concetti quali le dichiarazioni, gli identificatori, i tipi e gli eventi per poi introdurre le nozioni fondamentali di programmazione orientata agli oggetti e di programmazione a eventi, concludendo con un programma in Java dotato di interfaccia grafica in grado di visualizzare immagini caricate da disco.

IL CALENDARIO DELLE LEZIONI


Lezione 1
Rudimenti di programmazione - Terminologia essenziale - Dallalgoritmo al programma - Preparazione dellambiente Java - Il primo programma - Programmazione a oggetti - Creare una GUI

Lezione 3
File - Salvare i dati - Elaborare i dati

Lezione 2 Elementi di programmazione


a oggetti e grafica - Tipi e identificatori - Astrazione funzionale

Lezione 4
Networking - Programmi client-server - Interazione con risorse Web

1 Dichiarazioni, tipi e identificatori

bbiamo gi avuto modo di sottolineare che dalle cinque categorie principali a cui appartengono le operazioni eseguibili con un computer, tre (ricordare, calcolare, comunicare) riguardano in modo prevalente la manipolazione di dati. Molti linguaggi esigono che il programmatore specifichi esplicitamente, attraverso una dichiarazione, la natura dei dati prima di usarli: questi linguaggi sono detti tipati o fortemente tipati. Loperazione di dichiarazione ha lo scopo di attribuire alla variabile o costante: un nome univoco (identificatore) per potersi riferire ad essa. Un identificatore una parola che obbedisce a ben precise regole. Esistono inoltre delle convenzioni larga-

mente adottate che aiutano a sceglierli in modo tale da migliorare la leggibilit del programma. caldamente raccomandabile seguire queste prassi se si desidera che i propri programmi risultino chiari e ben comprensibili (si veda pi avanti il paragrafo Identificatori: una scelta delicata per tutti i dettagli): un tipo, che implicitamente determina quali saranno le operazioni che potranno esservi effettuate; una quantit appropriata di spazio in memoria per ospitarne il valore. evidente che questa quantit dipende dal tipo; se per un boolean pu bastare 1 bit, per un double ne servono 64. Quando si devono dichiarare pi variabili dello stesso tipo possibile raggrupparle in

un'unica dichiarazione "collettiva", su un'unica linea di codice, in una lista separata da virgole. Ecco alcuni esempi di dichiarazioni semplici e multiple: byte val_piccolo; short contatore; int a,i,j,k; long bigValue; float c, _valore; double totale; boolean stato, decisione; char b12; Altri linguaggi (soprattutto i linguaggi di scripting e, in misura minore, certe varianti di Basic) consentono al programmatore di prendersi la libert di usare i dati senza specificarne in anticipo il tipo: sar il computer a tentare di determinare in che modo vadano in-

terpretati i dati, in base alle circostanze. Per esempio, la stringa di testo 123 potrebbe essere trattata come un numero se sommata al numero 456, oppure come una stringa di testo se si tentasse di concatenarla alla stringa ABC. Nei linguaggi a oggetti esiste infine, come vedremo, unulteriore scenario: i tipi devono obbligatoriamente essere dichiarati nel programma, ma durante lesecuzione possono cambiare. Va sottolineato che il fatto che un linguaggio pretenda che i tipi vengano dichiarati non deve essere considerato un fatto negativo. Si tratta invece di un potente strumento per la prevenzione degli errori e per definire norme rigorose nel progetto software. Infatti, in un linguaggio che effettua acro-

Java
bazie automatiche per adattare i tipi di dato alle circostanze, un errore di progettazione della struttura dati del programma potrebbe restare latente e produrre conseguenze nefaste molto pi tardi. Un linguaggio fortemente tipato, invece, obbliga il programmatore a una maggior attenzione nel momento in cui scrive il codice, ma in compenso in grado di accorgersi di errori e ambiguit e li segnala immediatamente. Per esempio, lespressione 123+456 potrebbe essere intesa come una somma aritmetica il cui primo addendo stato mal espresso, oppure come una operazione di concatenamento fra due stringhe, in cui la seconda stringa stata erroneamente indicata come un numero. Il compilatore in questo caso potrebbe segnalare che si stanno combinando tipi incompatibili e pretendere che il programmatore intervenga esplicitamente con una conversione di tipo per ottenere dalla stringa di testo 123 il numero 123 (se intendeva ottenere una somma), oppure per costruire la stringa 456 a partire dal numero 456 (se puntava invece a un concatenamento fra stringhe). In questo esempio lincompatibilit di tipo evidente perch i valori sono espliciti; se si trattasse di variabili di tipi diversi lo sarebbe molto meno (per esempio: nellespressione x+y chi ci assicura, a prima vista, che x e y siano di tipi compatibili?) variabili, costanti, classi, funzioni, procedure. In Java, come in quasi tutti i linguaggi, un identificatore pu contenere solo caratteri alfabetici o numerici pi il carattere underscore (_), con l'ulteriore restrizione che il primo carattere non pu mai essere un carattere numerico. La distinzione fra minuscole e maiuscole significativa. Identificatori validi sono, per esempio: a, b12 , contatore, listaDati, calcola_valore_numero, _temp , a_71 Ortografia, sintassi e semantica Oltre a queste regole imposte dal linguaggio, che se non rispettate causano il rifiuto del programma da parte del compilatore, ve ne sono altre che sono "solo" raccomandazioni. Il loro scopo quello di differenziare anche a prima vista l'aspetto degli identificatori destinati a impieghi diversi. Vedi tabella sottostante. La questione lunghezza sempre raccomandabile fare in modo che il nome di un metodo (funzione o procedura) dia l'idea di ci di cui quel metodo si occupa. Questo permette di interpretare facilmente il codice: per esempio max=trova_valore_massimo (vettore); un'istruzione comprensibile anche senza conoscere nei dettagli il funzionamento della funzione trova_valore_massimo, mentre a=elabora(b);

2a lezione
dalle librerie standard del linguaggio. Fra l'altro questa convenzione lascia il carattere underscore meno inflazionato e quindi libero per altri scopi (ad esempio, usato in prima posizione pu suggerire che la variabile privata o di interesse locale). Parole chiave riservate L'ultimo importante vincolo che gli identificatori devono rispettare quello di non coincidere con una delle parole chiave riservate del linguaggio, per non creare ambiguit sintattica in fase di compilazione. Ve ne sono circa 50, fra le quali abbiamo gi incontrato for, while, do, if, else, switch, case, class, int, byte, short, long, float, double, char, boolean, void e cos via; l'elenco completo a http://java.sun.com/ docs/books/tutorial/java/nutsandbolts/_keywords.html Generalmente si pu dire che scegliendo identificatori in lingua italiana si riduce il rischio di collisioni (ma attenzione a omonimi come case, super, private, volatile). Sicuramente gli identificatori con almeno una maiuscola o underscore sono al riparo da questi rischi.

talmente vaga da obbligare il lettore, per capire qualcosa, ad esaminare il tipo di a, il tipo di b e il codice della funzione elabora. D'altra parte non bisogna lasciarsi prendere la mano esagerando con i dettagli: la prolissit un boomerang valore_massimo= scandisci_vettore_e_ identifica_valore_massimo (vettore_dati) Si deve sempre cercare un compromesso fra espressivit e leggibilit ed evitare comunque di scegliere identificatori pi lunghi di una ventina di caratteri. Generalmente accettabile usare nomi corti e poco espressivi per variabili di interesse locale, il cui senso pu facilmente essere dedotto esaminando "i dintorni", mentre importante scegliere molto accuratamente i nomi degli identificatori che saranno usati in lungo e in largo nel programma. Al di l della questione lunghezza, per l'ortografia degli identificatori lunghi e discorsivi esistono due alternative classiche: Uso di underscore per separare le parole: costruisci_vettore_dati Uso delle maiuscole per evidenziare l'inizio di ogni parola: costruisciVettoreDati Il primo metodo era prevalente in ambito C e (in misura minore) C++. Il secondo metodo, che oltretutto produce identificatori leggermente pi corti, tende a essere preferito in Java, anche perch adottato Semantica raccomandata entit risultato, entit, concetto

Tipi di dato
I dati che un computer in grado di manipolare possono essere usati per rappresentare informazioni di qualunque tipo. Anche se per lelettronica del computer si tratta sempre e solo di un insieme di bit, il modo di trattarli ed elaborarli ovviamente diverso se questi bit servono per rappresentare numeri, testo, immagini, suoni o altro ancora. Esempi di identificatori PIGRECO, MAX_SIZE i, count, numValori, last_val, impiegato finito, trovato, vuoto, ha_dati, is_empty, must_exit, salta_blocco dati, impiegati, elenco, lista, scartati, trovati calcola, azzera_tutto, ricercaDato, salva_lavoro, chiusura, inizializzazione DatiPersona, Applicazione, GestoreConnessione

Identificatori: una scelta delicata


Gli identificatori sono nomi univoci utilizzati per riferirsi a Entit da denominare Costante Variabile, attributo, parametro formale (scalari e non booleani) Metodo (funzione o procedura) Classe Interfaccia

Prassi ortografica Sintassi raccomandata Scrittura tutto MAIUSCOLO sostantivo singolare sostantivo singolare attributo singolare, predicato nominale, participio passato, imperativo sostantivo plurale o singolare collettivo sostantivo singolare; verbo all'imperativo sostantivo o locuzione

scalari non booleani Iniziale minuscola scalari booleani array, vettori Iniziale minuscola Iniziale minuscola

situazione, stato, condizione; azione compiuta o da compiere risultati, entit, concetti; caratteristica comune azione concetto; ruolo all'interno del programma

Iniziale minuscola

Iniziale MAIUSCOLA

Java

Generalmente presente una distinzione tra i tipi di dato che il linguaggio riconosce e tratta per... capacit innata (tipi base, detti anche primitivi o scalari) e quelli, pi complessi, che lutente definisce per modellare informazioni pi ricche ed articolate (tipi user-defined). il loro codice ASCII, ma in Java stata adottata la codifica Unicode che consente di rappresentare anche caratteri di lingue non occidentali, come il cinese, il giapponese, il coreano e molti altri; la contropartita unoccupazione di memoria esattamente doppia (16 bit per ogni carattere). Valori logici (booleani): vale a dire: true o false. Questi valori servono nei costrutti condizionali e iterativi, nei quali si devono prendere decisioni logiche in base alla verit o falsit di unespressione. Bit e byte, per consentire la manipolazione diretta dei valori ospitati in memoria senza doverli interpretare come i tipi sopra indicati. Come si pu vedere nellapposito riquadro, in Java previsto un insieme quasi completo di tipi primitivi. Sono previsti numeri interi e reali a varie precisioni, oltre a byte, caratteri e booleani. Vi sono per alcune particolarit rispetto ai tipi base offerti da linguaggi molto diffusi come C e C++: i tipi interi rappresentano sempre numeri relativi (non sono cio disponibili tipi interi unsigned); i caratteri, che adottano la codifica Unicode a 16 bit, sono trattati da un tipo specifico (char) che viene tenuto ben distinto dal tipo intero a 8 bit (byte); la dimensione in bit dei tipi interi definita rigidamente nella specifica del linguaggio; non esiste quindi il rischio che, sotto questo aspetto, implementazioni diverse del compilatore facciano scelte diverse (in C, un int potrebbe essere a 16 bit negli ambienti DOS, a 32 bit negli ambienti Windows e UNIX e a 64 bit su altri ambienti)

2a lezione
valori modificabili contenuti in variabili. valori immutabili immagazzinati in costanti. Una costante deve essere inizializzata con il valore che dovr contenere, ma da quel momento in poi nel programma si menzioner solo il nome della costante. Il valore in essa contenuto rimarr nascosto dentro alla costante. valori immutabili espressi letteralmente nel programma in tutti i punti in cui vengono usati (literal). A differenza di quanto avviene per costanti e variabili, quello che compare nel listato la rappresentazione esteriore del valore: per esempio, il valore 10, e non il nome di una costante xyz che contiene il valore 10. Come in tutti i linguaggi, anche in Java esistono regole precise su come esprimere valori literal per ogni tipo di dato. Si veda la tabella " I literal in Java" per maggiori dettagli.

Tipi primitivi
Solitamente la categoria dei tipi base comprende tipi di dato come i seguenti: numeri, distinti essenzialmente fra numeri interi e numeri reali, con numerose varianti dipendenti dallampiezza dellintervallo di valori rappresentabile (identificate da prefissi quali short, long, long long, double, long double) e, per i numeri interi, dalla presenza o assenza di segno (unsigned) caratteri e, in alcuni linguaggi, sequenze (stringhe) di caratteri. Generalmente i caratteri sono internamente rappresentati su 8 bit, mediante

I literal
In un listato i valori su cui il programma esegue calcoli, confronti e decisioni possono esistere essenzialmente in tre forme:

Tipi base disponibili in Java: le loro caratteristiche e alcuni esempi raccomandati di impiego
Parola Descrizione chiave Java byte Intero byte con segno Dimensione in Codifica memoria (bit) 8 complemento a2 Intervallo di valori rappresentabile -128 .... +127 Esempi di dati rappresentabili

short

Intero short con segno Intero normale con segno Intero long con segno

16

complemento a2 complemento a2 complemento a2 IEEE 754

int

32

long

64

float

double

char boolean

Numero reale 32 (approssimato) in virgola mobile a singola precisione Numero reale 64 (approssimato) in virgola mobile a doppia precisione Carattere di testo 16 Valore logico 1

IEEE 754

Unicode -

Contatori per iterazioni Cardinalit di insiemi Indici posizionali in vettori Mese; giorno del mese Percentuali senza parte frazionaria -32.768 .. +32.767 Contatori per iterazioni Cardinalit di insiemi Indici posizionali in vettori; anni Percentuali senza parte frazionaria -2.147.483.648 ... Contatori per iterazioni 2.147.483.647 Cardinalit di insiemi Indici posizionali in vettori; CAP Importi in denaro senza parte frazionaria -9.223.372.036.854.775.808 ... Contatori per iterazioni +9.223.372.036.854.775.807 Cardinalit di insiemi Indici posizionali in vettori Importi in denaro senza parte frazionaria Valori positivi o negativi con valore Importi in denaro con parte frazionaria ass. compreso fra 1.401298*10-45 Percentuali con parte frazionaria Valori per calcoli scientifici singola prec. e 3.402823*10+38 Grandezze fisiche - Numeri molto grandi Valori positivi o negativi con valore Importi in denaro con parte frazionaria ass. compreso fra 4,9406564*10-324 Valori per calcoli scientifici di alta prec. Grandezze fisiche ad alta precisione e 1,7976931*10+308 Numeri estremamente grandi Qualsiasi singolo carattere della tab. Testi Unicode (www.unicode.org/) Codici fiscali True o false Condizioni logiche - Risultati di decisioni

Java
I literal in Java
Tipo di dato, formato e precisione Valori logici Regola esistono solo due valori possibili, vero e falso

2a lezione

Esempi di literal true, false 123, +123, -123 0x1AF, 0xa4c 011, 047 123L, 0xA3FL, 011L 123.45, +123.45, 123.45 123.45E6, -123.45E6, +123.45E-6, 123.45E-6, ... 123.45D -123.45E-6D 'a','7','X','\n' '\'' '\\' '\u0041' "ciao", "abcdef\n123", "apice\"doppio" null

Numeri interi

int long a singola precisione

espressi in notazione le cifre della rappresentazione decimale, decimale eventualmente precedute dal segno espressi in notazione le cifre della rappresentazione esadecimale esadecimale (indifferentemente maiuscole o minuscole) precedute da 0x espressi in notazione ottale le cifre della rappresentazione ottale precedute da 0 Come sopra, ma con una lettera L (anche minuscola) aggiunta in coda. espressi in notazione decimale espressi in notazione decimale esponenziale in base 10 le cifre della rappresentazione decimale, eventualmente precedute dal segno. Il separatore delle cifre decimali il punto Mantissa espressa come sopra, seguita da E (anche minuscola) e dallesponente eventualmente preceduto dal segno

Numeri reali a doppia precisione

Come sopra, ma con una lettera D (anche minuscola) aggiunta in coda Il carattere (o, per caratteri non stampabili, la relativa escape Citati letteralmente (possibile sequence) delimitato con apici singoli solo per caratteri occidentali Nel caso particolare del carattere apice singolo esso e per alcuni caratteri speciali) va preceduto con backslash per evitare ambiguit. Nel caso particolare del carattere backslash esso va preceduto da un altro backslash. Rappresentati dal loro Il codice Unicode espresso in notazione esadecimale a 4 cifre codice Unicode (indifferentemente con lettere minuscole o maiuscole) e preceduto da \u, il tutto fra apici singoli La sequenza di caratteri delimitata da apici doppi. ammessa la coabitazione di caratteri letterali, escape sequences e codici Unicode. Il carattere apice doppio deve essere preceduto da backslash per evitare ambiguit. la parola null scritta in minuscolo, non delimitata da apici

Singoli caratteri

Stringhe di caratteri Il valore null

Esistono alcuni caratteri che hanno un effetto particolare: interruzione di linea, confine di stringa, e cos via. chiaro che se sorge la necessit di citare nel programma proprio questi caratteri essi non possono essere citati letteralmente, in quanto avrebbero appunto ... il loro effetto speciale anzich essere interpretati come "caratteri citati". Per evitare simili ambiguit questi caratteri sono rappresentabili in una particolare forma secondaria (detta, per ragioni storiche, escape sequence). La tabella qui sotto riassume questi casi particolari. Escape Carattere sequence \\ Barra inversa (Backslash) \" Virgoletta doppia (Double quote) \' Virgoletta singola (Single quote) \b Cancella a sinistra (Backspace) \f Avanzamento foglio (Form feed) \n A capo / Avanzamento linea (Newline) \r Ritorno cursore (o carrello stampante) a inizio linea (Carriage return) \t Tabulazione (Horizontal tab)

Tipi user-defined: i costruttori di tipo


I tipi base messi a disposizione dei linguaggi, pur numerosi, sono ancora troppo elementari per rappresentare adeguatamente le informazioni del mondo reale. Questo per due aspetti che caratterizzano invariabilmente tali informazioni: numerosit e affinit. Qualunque programma non banale non si limita a effettuare calcoli su una manciata di valori. invece normale che per effettuare elaborazioni di una qualche utilit pratica risulti necessario manipolare grandi quantit di dati dello stesso tipo (migliaia o milioni). Scrivere un programma che faccia questo sarebbe praticamente impossibile se per ogni dato elementare da gestire si dovesse dichiarare una variabile elementare con un nome specifico. Ogni qualvolta esista la necessit di effettuare elaborazioni su grandi insiemi di oggetti affini emerge lesigenza di poter denotare ognuno di tali insiemi con un nome collettivo e di poter poi accedere in modo parametrico alle informazioni elementari ivi contenute.

larticolazione e la correlazione. Si tratta di due facce della stessa medaglia. Per esempio, in un archivio parco vetture la descrizione di un singolo autoveicolo si articola in diverse informazioni elementari, quali il numero di telaio, il numero di targa, il modello, lanno di immatricolazione, il chilometraggio e cos via. Vedendo la cosa da unaltra angolazione, si pu dire che quelle informazioni elementari sono correlate dal fatto di riferirsi tutte a una stessa entit. Sta di fatto che nel mondo reale i dati non banali appaiono come insiemi di informazioni elementari legate fra loro, piuttosto che come monoliti isolati. Appare di conseguenza comodo poterli gestire come gruppi, costituenti vere e proprie entit strutturate. La soluzione messa a disposizione dei linguaggi consiste nelluso di speciali costruttori di tipo che applicati ai mattoni elementari rappresentati dai tipi base consentono al programmatore di costruire tipi di dato pi articolati, adatti a rappresentare in forma aggregata informazioni pi ricche. I co-

struttori di tipo pi comuni comprendono: vettori (array): consentono di raggruppare vari elementi dello stesso tipo formando un insieme ordinato. Linsieme potr essere maneggiato come se fosse un dato unico. strutture con campi indipendenti (struct; record): consentono di raggruppare elementi di tipi diversi. strutture con campi in alternativa fra loro (union): consentono di ospitare in alternativa dati di tipo diverso in una stessa zona di memoria.

Gli array
Un array un insieme ordinato di dati dello stesso tipo. caratterizzato da un nome e da una dimensione (intesa come ampiezza o cardinalit), oltre che dal tipo degli elementi contenuti. Una dichiarazione potrebbe suonare per esempio cos: costruisci un array di 1000 elementi, tutti di tipo int, e chiamalo archivio. Laccesso ai dati contenuti nellarray si effettua indicando il nome dellarray e la posizione al suo interno (indice) dellelemento desiderato. Ci si riferisce, per esempio, a lele-

Java
mento di posto 267 dellarray
Nella maggior parte dei linguaggi gli array hanno dimensioni che vengono fissate allatto della dichiarazione o dellallocazione. Non generalmente possibile ingrandire o ridurre un array in seguito, specialmente se gi caricato con dati. portante sfumatura di significato. Mentre nelle strutture i dati contenuti nei campi coesistono (in altre parole in memoria la struttura ha dimensioni pari alla somma degli ingombri dei dati che contiene), nelle union i campi, pur chiaramente distinti a livello di nome, possono essere usati solo in alternativa, in quanto lo spazio sufficiente per contenerne uno soltanto. Questo tipo di strutture risulta utile quando economizzare memoria sia una priorit e quando i dati da rappresentare contengano effettivamente degli aspetti che possono ricorrere solo in alternativa. Per esempio, in una struttura dati dati autoveicolo destinata a modellare sia autovetture sia autocarri, potremmo avere sia un campo intero numero assi (significativo per autocarri, superfluo per automobili) sia un campo booleano tetto apribile (significativo per automobili, probabilmente inutile per autocarri). evidente che riservare spa-

2a lezione
zio in memoria per entrambe queste informazioni sarebbe un inutile spreco: non potrebbero essere mai presenti e significative allo stesso tempo. Utilizzando le union possibile evidenziare questa circostanza e sfruttarla per ottimizzare luso della memoria, dichiarando che questi due campi sono alternativi e non simultanei. Le strutture e le union sono disponibili in molti linguaggi, fra cui C e C++, ma non in Java. Come vedremo pi avanti, le classi Java possono essere usate come sostituti delle strutture, mentre le union non sono disponibili.

archivio. Un array come quello usato per questi esempi pu essere immaginato come una schiera di 1000 caselle, ognuna contenente un numero intero, disposte in fila indiana. Topologicamente questo equivale a un segmento: un luogo monodimensionale, descrivibile con una sola coordinata. Per laccesso infatti sufficiente un solo indice. Esistono per anche array multidimensionali, che possono essere immaginati come schiere di informazioni elementari disposte come su una superficie rettangolare (array bidimensionali), oppure in un volume a parallelepipedo (array tridimensionali), e cos via. In generale, un array N-dimensionale richiede, per laccesso alle informazioni, esattamente N indici (che rappresentano le coordinate delle celle dellarray nellN-spazio). In Java gli indici degli array sono a base 0: in un array monodimensionale di 100 elementi, il primo elemento ha indice 0 e lultimo ha indice 99.

Le strutture
Una struttura (o record) un costrutto che provvede allaggregazione di dati di tipo potenzialmente eterogeneo, ma correlati da un qualche nesso logico. La caratterizzano un nome e lelenco dei contenuti (che sono detti campi), per ognuno dei quali vengono specificati un nome univoco e il tipo. A seconda delle esigenze, la struttura pu essere trattata come unentit unica, oppure essere vista come contenitore di dati, laccesso ai quali viene effettuato per nome.

Combinazione di array e strutture


I costruttori di tipo appena introdotti, in particolare array e strutture, supportano rispettivamente lequivalente sui dati delle operazioni di iterazione e di selezione. possibile, anzi frequente, utilizzarli anche insieme per definire strutture dati ancora pi articolate. Per esempio, un array di 100

Le union
Meritano infine un cenno le union. Si tratta di costrutti affini alle strutture ma con una im-

Esempi di applicazioni di array


Caratteristiche dellarray Realt modellata Array bidimensionale di 1000x5 float Il saldo (positivo o negativo) dei 1000 conti correnti di ciascuna delle 5 filiali di una banca float[][] conticorr; // allocazione array // con unica istruzione conticorr= new float[5] [1000]; Esempio di dichiarazione in Java // in alternativa conticorr=new float[5] []; conticorr[0]=new float[1000]; conticorr[1]=new float[1000]; conticorr[2]=new float[1000]; conticorr[3]=new float[1000]; conticorr[4]=new float[1000]; Array tridimensionale di 12x31x5 float Array quadridimensionale di 30x10x100x365 boolean Le temperature rilevate da una rete Lo stato prestato/disponibile dei libri di 5 sensori di una biblioteca disposti in 30 sale con 10 scaffali da 100 libri luna, nei 365 giorni dellanno float[][][] rilevamenti; // allocazione "atomica" rilevamenti=new float[12][31][5]; // allocazione progressiva rilevamenti=new float[12][][]; rilevamenti[0]=new float[31][]; rilevamenti[0][0]=new float[5]; rilevamenti[0][1]=new float[5]; // ecc... rilevamenti[0][30]=new float[5]; rilevamenti[1][0]=new float[5]; rilevamenti[1][1]=new float[5]; // ecc... ecc... rilevamenti[11][30]=new float[5]; verificare se il 3(2) ottobre(9) la quinta(4) centralina ha rilevato una temperatura superiore a 16.4 gradi if(rilevamenti[9][2][4]>16.4) { // sopra la media stagionale! } il 212 giorno dell'anno stato prestato l'88esimo libro del settimo scaffale della terza sala stato_libri[2][6][87][211]=false; addebito 0.5% (una tantum) su conto n.236 della filiale n.1 conticorr[1][236] *= 0.95; boolean[][][][] stato_libri; stato_libri = new boolean [30] [10] [100] [365];

accredito 142.87 euro su conto n.671 della filiale n.3 Esempio duso in Java conticorr[3][671] += 142.87;

Java
Esempi di applicazioni di strutture
Insieme di campi nome (stringa), cognome (stringa), et (intero), sesso (booleano), indirizzo (stringa) Dati anagrafici di una persona parte reale (float), parte immaginaria (float) Numero complesso in coordinate cartesiane class NumComplesso { float reale; float immaginaria; }

2a lezione

cilindrata (intero), numero porte (byte), tipo alimentazione (carattere), targa (stringa) Caratteristiche di unautovettura class Autovettura { int cilindrata; byte numero_porte; char tipo_alim; String targa; } Dichiara una variabile Autovettura x; // (crea e inizializza..) Determina una qualche tariffa in base alla cilindrata float tariffa; if(x.cilindrata > 1600) {tariffa=10f;} else {tariffa=6.5f;}

Applicazione

class DatiPersona { String nome; Esempio di dichiarazione String cognome; in Java int eta; boolean sesso; String indirizzo; } dichiara una variabile per i dati del direttore DatiPersona direttore; crea e inizializza un record per il direttore direttore=new DatiPersona(); direttore.nome="Mario"; direttore.cognome="Rossi"; direttore.eta=52; oggi e' il compleanno del direttore... direttore.eta ++; strutture dati personali (magari con laggiunta di un campo numero_ matricola) potrebbe rappresentare lelenco dei dipendenti di unazienda; oppure, in una struttura dati del contribuente potrebbe figurare un campo di tipo array di stringhe contenente lelenco dei familiari a carico.

dichiara due numeri complessi NumComplesso valore1,valore2; crea e inizializza valore1 valore1=new NumComplesso(); valore1.reale=24.3f; valore1.immaginaria=5.03f; somma valore2 a valore1 valore1.reale += valore2.reale; valore1.immaginaria += valore2.immaginaria;

Esempio duso in Java

Nella figura qui a fianco vediamo un esempio di questo tipo. Supponendo definita una struttura DatiDipendente, se ne dichiara un array di 100 elementi. Il tutto rappresenta un archivio di dati personali accessibile come mostrato.

// dichiarazione archivio // array di 100 strutture DatiDipendente[] archivioDipendenti = new DatiDipendente[100]; // caricamento dati archivioDipendenti[73].nome = Mario archivioDipendenti[73].cognome = Rossi archivioDipendenti[73].numero_matricola = 16353 // ecc..

2 Astrazione funzionale
Procedure e funzioni
Come abbiamo visto nella prima puntata, praticamente tutti i linguaggi di programmazione offrono strutture sintattiche che consentono di esprimere con maggior chiarezza (anche visiva) gli algoritmi. In particolare, i costrutti iterativi catturano e schematizzano il concetto di ripetizione; quelli condizionali fanno altrettanto per il concetto di decisione o selezione. Questi accorgimenti agevolano soprattutto la lettura e la comprensione di algoritmi di complessit medio-bassa, la cui espressione in linguaggio di programmazione di limitata estensione. Per algoritmi di grande complessit, tuttavia, essi non risultano sufficienti per chiarire il senso del programma, che potrebbe essere lungo decine o centinaia di migliaia di linee: una dimensione troppo ampia perch la logica sottostante possa essere colta dal lettore, anche facendo ricorso a indentazione e a strutture iterative e condizionali perfettamente congegnate. Quello che serve un terzo accorgimento che consenta le seguenti strategie di semplificazione interpretativa ed espressiva: fornire dellalgoritmo una prima descrizione per grandi linee, confinando lapprofondimento sui dettagli in separata sede ed eventualmente rimandandone lesatta definizione, che in un primo momento pu essere data per scontata; (approccio top down: dallalto in basso) identificare eventuali operazioni identiche o molto simili che vengono ripetute in vari punti del programma e definirle una sola volta, evitando inutili ridondanze e consentendo di esprimere operazioni sempre pi complesse in termini di operazioni pi semplici definite in precedenza, fino a creare le condizioni per poter esprimere il programma vero e proprio (approccio bottom up: dal basso verso lalto) Laccorgimento che consente entrambe le filosofie progettuali sopra richiamate si chiama astrazione funzionale ed i costrutti sintattici che lo traducono in pratica sono detti funzioni e procedure. Seppure qualcosa di equivalente esistesse gi, da un punto di vista logico, fin dai tempi del linguaggio macchina, stato con i linguaggi strutturati come Algol 68, C e Pascal che lastrazione funzionale divent una vera e propria architrave della metodologia di progetto dei programmi: questa tecnica stata sposata da questi linguaggi in modo cos pervasivo che lapplicazione dei concetti

Java
di procedura e funzione non

2a lezione

confinata ad algoritmi di grande complessit, ma del tutto generalizzata. Tutti i programmi C, per esempio, sono interamente decomposti in funzioni e deve sempre esistere una funzione con uno speciale nome (main) da cui inizia lesecuzione del programma.

Esempi di sottoprogrammi
Esempi di funzioni calcolo dellarea di un quadrato: float area_quadrato(float lato) { return lato * lato; } calcolo (semplificato....) dellimposta IRPEF: float calcola_IRPEF(float reddito_lordo, int n_figli_a_carico, float totale_detrazioni) { float ris = reddito_lordo*0.35 100*n_figli_a_carico totale_detrazioni; if(ris > 0){return ris;} else{return 0;} } Esempi di procedure Salva il lavoro su disco (solo dichiarazione) void salva(String dati_da_salvare, String nome_file) Invia una e-mail (solo dichiarazione) void invia(String email_destinatario, String titolo, String messaggio) che dati, proprio come variabili e costanti: di conseguenza anche per essi necessario specificare chiaramente il tipo. La dichiarazione del tipo (e del significato) di parametri e valori di ritorno di importanza capitale per funzioni e procedure, in quanto essi costituiscono linterfaccia di contatto fra questi sottoprogrammi e il resto del programma principale. Poter contare su una definizione chiara e inequivocabile di questa interfaccia quindi indispensabile per poter riutilizzare facilmente il sottoprogramma in pi occasioni, al limite anche in programmi diversi, senza bisogno di esaminarlo ogni volta per comprenderne il funzionamento. Possiamo paragonare questa esigenza di precisione a quanto avviene in meccanica, per lidentificazione delle caratteristiche di qualunque componente di un macchinario complesso: dal tipo di filettatura di una vite alle dimensioni e allangolo dei denti di un ingranaggio, solo lesistenza di una esatta e schematica classificazione simbolica degli aspetti che determinano linterazione con il resto del sistema consente di progettare in astratto, velocemente e senza errori. Alcuni esempi di funzioni e procedure sono riportati nel riquadro. Nelle funzioni si noti l'uso della parola chiave return per specificare qual il valore di ritorno della funzione. Quando l'espressione return viene raggiunta, l'esecuzione della funzione termina e l'espressione che segue la parola return viene calcolata e ritornata al chiamante come valore della funzione. In realt la parola return pu essere usata anche nelle procedure, ma senza parametri. Le procedure infatti non ritornano valore. In questo caso return ha solo l'effetto di far terminare immediatamente l'esecuzione della procedura, senza attendere il normale decorso della sequenza di istruzioni. In Java le procedure si presentano assai simili alle funzioni. Di fatto l'unica differenza sta nel tipo di ritorno, che per le procedure deve essere dichiarato del tipo fittizio void (il vuoto, il nulla), a significare che il chiamante non deve attendersi alcun valore di ritorno.

Sottoprogrammi
Grazie allastrazione funzionale possibile catturare una parte ben identificata dellalgoritmo complessivo in una porzione del programma chiaramente delimitata (sottoprogramma), che caratterizzata da un nome univoco e interagisce con il resto del programma attraverso uno scambio di dati. I dati in ingresso sono detti parametri e i dati in uscita valori di ritorno. Solitamente un sottoprogramma che accetta parametri, ma non produce valori di ritorno detto procedura. Se sono previsti valori di ritorno si parla invece di funzione. In questo contesto il termine funzione usato in assoluta analogia con il concetto matematico di funzione, intesa come corrispondenza fra valori delle variabili indipendenti (che corrispondono ai parametri presi dalla funzione) e valori della variabile dipendente (che corrisponde al valore di ritorno). Il sottoprogramma definito funzione si occupa precisamente di effettuare i calcoli necessari per determinare il valore di ritorno corrispondente ai valori ricevuti nei parametri. Naturalmente parametri e valori di ritorno non sono altro

3 Elementi di programmazione a oggetti

a scorsa puntata si conclusa con la presentazione di un semplice programma completo scritto in Java: un linguaggio che, come stato ricordato, appartiene alla categoria dei linguaggi a oggetti. Prima di procedere a unanalisi pi approfondita dellesempio presentato introdurremo dunque i concetti fondamentali di programmazione a oggetti. I programmi scritti in linguaggi procedurali strutturati,

come il C e il Pascal, possono considerarsi impostati sullarchitrave logica rappresentata dallalgoritmo. Laspetto maggiormente enfatizzato come agire piuttosto che su quali dati agire. I dati servono per alimentare lalgoritmo (input) e ne costituiscono il risultato (output). Se dovessimo scrivere in uno di questi linguaggi una libreria (compendio tematico di funzioni) per la gestione di fi-

gure geometriche, questa potrebbe essere costituita da tante funzioni separate che effettuano ognuna una certa operazione solitamente identificata dal titolo della funzione stessa lavorando sui parametri ricevuti in ingresso. Per esempio: float area_quadrato(float lato) {return lato*lato;} float area_rettangolo(float base, float altezza) {return base*altezza;}

float area_triangolo(float base, float altezza) {return base*altezza/2;} float risultato; risultato=area_quadrato(120.5, 67.1); come si vede, quello che esiste un repertorio di funzioni che copre i casi da gestire, ma quello che sfugge a questa impostazione il concetto di figura geometrica di cui si vuole calcolare larea. Le funzioni effettuano il calcolo puro e sempli-

Java
ce dellarea, ma esigono che vengano forniti i dati elementari necessari per assolvere a tale compito. Il fatto che si tratti di figure geometriche e del calcolo dellarea messo in evidenza solo dal titolo delle funzioni: in realt le funzioni lavorano su numeri puri e semplici, sui quali fanno solo calcoli aritmetici. Questo ha unaltra implicazione importante. Sta al programmatore ricordarsi di chiamare la funzione giusta (che avr, naturalmente, un suo nome specifico) per ogni circostanza. Per esempio, per calcolare larea di un rettangolo dovr avere cura di chiamare area_rettangolo e non area_triangolo, che ha anchessa due parametri dello stesso tipo e potrebbe essere confusa con la prima. Questa situazione presenta degli inconvenienti. Innanzitutto si chiede al programmatore uno sforzo mnemonico aggiuntivo: nonostante concettualmente si stia sempre facendo la stessa cosa (calcolo di unarea), di fatto il linguaggio richiede di chiamare funzioni diverse a seconda della figura da trattare. Saranno quindi necessarie decisioni del tipo "se un rettangolo chiama la funzione F1, se un quadrato chiama la funzione F2,...", di conseguenza il programma si allungher diventando fra l'altro meno chiaro. Nasce inoltre il rischio che il programmatore commetta un errore nella scelta della funzione da chiamare caso per caso, compromettendo il buon funzionamento del programma. Ma non finita. Questa tecnica non consente di cogliere e sfruttare le affinit esistenti fra le figure geometriche. Per esempio, restando fra i quadrilateri, il rombo e il quadrato hanno una legge uguale per quanto riguarda il calcolo del perimetro, mentre per larea ci vale per il rombo e il rettangolo. Invece di scrivere 3x2=6 funzioni (una per il perimetro e una per larea, per ognuna delle 3 figure trattate) ne potrebbero bastare di meno. Certo si potrebbe impostare la libreria cos: float area_quadrato(float lato){return lato*lato;} float area_rettangolo_oppure_ rombo(float base, float altezza) {return base*altezza;} float perimetro_quadrato_oppure_ rombo(float lato){return lato*4;} float perimetro_rettangolo (float base, float altezza) {return base*2+altezza*2;} ma si intuisce subito che se le entit da trattare non fossero 3, ma 300, e se le affinit non fossero cos poche ed evidenti, ma pi numerose e sottili, questo approccio diventerebbe ingestibile: il numero delle funzioni da scrivere si moltiplicherebbe esponenzialmente. i titoli delle funzioni diventerebbero rapidamente discorsi illeggibili. al programmatore sarebbe richiesto uno sforzo mnemonico inaccettabile per ricordare nomi e campo di applicazione delle funzioni polivalenti. la percezione di quale sia esattamente l'insieme delle figure trattate dal programma si fa piuttosto confusa. l'aggiunta di un'eventuale nuova figura, o qualche modifica alle caratteristiche di quelle esistenti, potrebbe richiedere uno sforzo di ripensamento generale del programma. scrizione dello stato (normalmente questa consiste in un insieme di attributi) e un elenco di operazioni (metodi). Istanze distinte di una stessa classe sono entit indipendenti, ognuna delle quali, in generale, potr avere uno stato diverso (in altre parole, gli attributi avranno un diverso insieme di valori). Normalmente ogni classe definisce uno speciale metodo, detto costruttore, che consente di creare un nuovo oggetto (istanza) appartenente a quella classe. Questo metodo non richiede di dichiarare il tipo di ritorno, n di usare la parola chiave return. Il tipo del suo valore di ritorno implicitamente la classe a cui appartiene e il valore ritornato l'oggetto che stato creato. Come succede per funzioni e procedure, anche il costruttore pu richiedere dei parametri, che di solito sono usati per inizializzare lo stato del "neonato" oggetto. Se dovessimo riscrivere a oggetti la libreria sopra menzionata, potremmo cominciare osservando che le figure da trattare nel programma sono riconducibili a 4 categorie immediatamente riconoscibili: triangolo, quadrato, rombo e rettangolo. Questo suggerisce subito di definirle come classi. C poi la questione di definire che cos' lo stato per ognuna delle classi appena definite. Che cosa caratterizza completamente un quadrato (ed eventualmente lo differenzia da altri quadrati)? Il valore del lato. Nella classe quadrato, quindi, occorrer un solo attributo, lato. Con questunica informazione possibile calcolare area e perimetro. Per un rettangolo e un rombo, per essere completamente descritta la figura richiede due informazioni: base (che per il rombo vale anche come lato) e altezza. Per un triangolo occorre anche conoscere la lunghezza dei tre lati, altrimenti sar impossibile calcolare il perimetro. Poich, qualunque sia la figura, richiesto di poter calcolare area e perimetro, le nostre classi dovranno quindi avere due metodi ognuna: naturalmente li chiameremmo area e perimetro. In totale scriveremmo quindi 4 classi e 8 metodi (listato 1). Questa modellizzazione ha il pregio di separare chiara-

2a lezione

Listato 1
public class Geometria1 { class Quadrato { float lato; public float area() {return lato*lato;} public float perimetro() {return lato*4;} } class Rombo { float base; // = lato float altezza; public float area() {return base*altezza;} public float perimetro() {return base*4;} } class Rettangolo { float base; float altezza; public float area() {return base*altezza;} public float perimetro() {return base*2 + altezza*2;} } class Triangolo { float base; // = lato1 float altezza; float lato2; float lato3; public float area() {return base*altezza/2;} public float perimetro() {return base+lato2+lato3;} } } mente il codice che tratta ogni figura e la definizione dei dati necessari. Tuttavia non pienamente soddisfacente perch: non d conto del fatto che, pur con le differenze esistenti, tutti questi poligoni sono figure geometriche. alcuni metodi e alcuni attributi sono identici in pi classi. L'ovvia parentela esistente, per esempio, tra un quadrato e un rettangolo, o tra un quadrato e un rombo, non viene sfruttata n per risparmiare memoria n per evitare ripetizioni nell'implementazione dei metodi. a questo punto che entra in gioco uno strumento di importanza fondamentale nel progetto dell'applicazione a oggetti: l'ereditariet. Cos come le classi colgono le caratteristi-

Un approccio orientato agli oggetti


Se si utilizza un linguaggio a oggetti il progetto prende piuttosto lavvio dalla modellizzazione delle entit da trattare. Anzitutto si identificano affinit fra di esse; gruppi di entit accomunate da determinate caratteristiche rilevanti ai fini dellelaborazione vengono categorizzati definendo una classe e dichiarandoli appartenenti a quella classe. Gli esemplari di una certa classe (istanze di quella classe) sono poi detti oggetti , ognuno dei quali ha la propria identit. Gli oggetti sono poi caratterizzati da uno stato che descrive la condizione in cui si trovano e (se sicuramente univoco) pu essere anche usato per identificarli. Sugli oggetti inoltre possibile effettuare delle operazioni, il cui effetto primario spesso quello di cambiarne lo stato, ma pu anche essere semplicemente quello di esportare informazioni basate sullo stato. Ne segue che per definire una classe di oggetti necessario definire una forma di de-

Java
che comuni fra gli oggetti, la reche rombo, rettangolo e quadrato sono quadrilateri. Questo permette di scrivere un algoritmo molto generico per il calcolo del perimetro (come somma di 4 lati) implementandolo nella classe Quadrilatero, e condividerlo nelle sottoclassi. Si veda il listato 2. Come si pu notare le definizioni delle classi ricalcano il diagramma mostrato in figura, mentre il metodo main serve per il collaudo: costruisce un oggetto di ciascuna classe e ne richiede il calcolo di area e perimetro. Il risultato visualizzato dal programma il seguente: quadrato 100x100: a=10000.0, p=400.0 rettangolo 10x20: a=200.0, p=60.0 rombo 10, h5: a=50.0, p=40.0 triangolo 30,40,50 h40: a=600.0, p=120.0 Per quanto riguarda il calcolo dell'area, per tutti i quadrilateri considerati, quadrato compreso, pu andar bene la regola "base x altezza", ma se per esempio vi fosse la prospettiva di introdurre la classe Trapezio, per il quale la formula da utilizzare diversa, allora si potrebbe prendere in considerazione uno schema diverso, in cui venga introdotta una... linea dinastica dedicata al Trapezio, per meglio differenziarlo, come in (figura 2) oppure in (figura 3), a seconda della relazione che interessa enfatizzare per il Quadrato. Un Quadrato pi un tipo particolare di Rombo (4 lati uguali) oppure di Rettangolo (4 angoli retti)? Non sempre emerge con evidenza una unica scelta "giusta"; in questo caso verrebbe quasi spontaneo dire che il quadrato una specie di "incrocio" tra un rombo e un rettangolo. Purtroppo Java supporta solo il modello a ereditariet singola: ogni classe pu ereditare da al pi una classe. Altri

2a lezione

lazione di ereditariet fra classi coglie le analogie fra le classi. Cos come il quadrato da 3x3 cm e il quadrato da 92x92 cm appartengono tutti alla classe dei quadrati, possiamo osservare che le classi Quadrato, Rettangolo e Rombo appartengono tutti alla "super-classe" dei Quadrilateri. Il ragionamento si pu reiterare, osservando che le classi Quadrilateri e Triangoli appartengono alla... "super-super-classe" dei Poligoni. Questo tipo di situazione si pu raffigurare in appositi diagrammi come quello mostrato in (fig. 1). Le frecce indicano una relazione di ereditariet e sono orientate dall'erede verso l'antenato. Il codice corrispondente al diagramma riportato nel listato Con riferimento all'esempio, si dice che la classe Quadrilatero "eredita dalla" (o "deriva dalla", o "specializza la", o "estende la") classe Poligono, oppure che la superclasse della classe Quadrilatero la classe Poligono, oppure che la classe Quadrilatero una sottoclasse della classe Poligono. Dire che una classe B eredita da una classe A significa che tutti i metodi ed attributi definiti in A, che non siano stati dichiarati private, risultano disponibili anche in B, come se vi fossero stati trascritti, ma con il vantaggio di eliminare ripetizioni esplicite. Cos, qualora fosse necessaria una modifica o una correzione, basterebbe farla solo nella classe A per averla automaticamente anche in B e in qualunque altra classe erediti da A. Naturalmente B libera di specializzare i metodi che eredita, ridefinendone in tutto o in parte il funzionamento, cos come di fungere da estensione della classe A, introducendo metodi o attributi aggiuntivi. La modellizzazione in fig. 1 riesce a dare conto del fatto

Listato 2
public class Geometria2 { public static void main(String [] args) { Poligono p; p=new Quadrato(100f); System.out.println("quadrato 100x100: a="+p.area()+", p="+p.perimetro()); p=new Rettangolo(10f,20f); System.out.println("rettangolo 10x20: a="+p.area()+", p="+p.perimetro()); p=new Rombo(10f,5f); System.out.println("rombo 10, h5: a="+p.area()+", p="+p.perimetro()); p=new Triangolo(30f,40f,50f,40f); System.out.println("triangolo 30,40,50 h40: a="+p.area()+", p="+p.perimetro()); } } abstract class Poligono { public abstract float area(); public abstract float perimetro(); } class Quadrilatero extends Poligono { float base, altezza, lato1, lato2, lato3, lato4; public float perimetro() {return lato1+lato2+lato3+lato4;} public float area() {return base*altezza;} } class Quadrato extends Quadrilatero { public Quadrato(float lato) {lato1=lato2=lato3=lato4=base=altezza=lato;} public float area() {return base*base;} // specializzato public float perimetro() {return base*4;} // specializzato } class Rombo extends Quadrilatero { public Rombo(float l, float h) {base=lato1=lato2=lato3=lato4=l; altezza=h;} public float perimetro() {return base*4;} // specializzato } class Rettangolo extends Quadrilatero { public Rettangolo(float b, float h) {base=lato1=lato3=b; altezza=lato2=lato4=h;} public float area() {return base*altezza;} public float perimetro() {return base*2 + altezza*2;} } class Triangolo extends Poligono { float base; // = lato1 float altezza; float lato2; float lato3; public Triangolo(float b, float l2, float l3, float h) {base=b; altezza=h; lato2=l2; lato3=l3;} public float area() {return base*altezza/2;} public float perimetro() {return base+lato2+lato3;} }

Java
linguaggi a oggetti, come il C++, consentono invece schemi di ereditariet pi articolati, in cui una classe pu ereditare parte del proprio "patrimonio genetico" da due o pi classi. In un caso semplice come il nostro una simile tecnica appare perfino eccessiva, comunque per completezza riportiamo in fig. 4 l'aspetto che potrebbe avere il class diagram nel caso volessimo sfruttare l'ereditariet multipla. Come esercizio vi proponiamo di provare a progettare le classi necessarie per modellare un parco automezzi composto da Moto, Auto e Camion, evidenziando gli aspetti comuni con l'introduzione di eventuali classi "antenate" e gestendo le differenze mediante opportune "diramazioni" nella linea ereditaria. necessario dare accesso agli attributi, sar bene farlo solo indirettamente, attraverso dei metodi che consentano di leggere o scriverne il valore. Questa impostazione consentir, al limite, di intercettare la chiamata al metodo e ingannare il codice esterno facendogli credere che lo stato sia espresso sempre allo stesso modo, anche se in realt all'interno tutto cambiato. Per esempio, la versione 1.0 di una classe per la gestione dei numeri complessi potrebbe essere implementata in coordinate cartesiane. Se facesse trasparire all'esterno il proprio stato interno, la classe NumeriComplessi "abituerebbe" i programmatori delle altre classi ad interagire con essa in coordinate cartesiane. Programmi che usano questa classe verrebbero sviluppati sulla base di questo assunto. Supponiamo ora che il programmatore della classe NumeriComplessi si pentisse della scelta implementativa iniziale e decidesse di reimplementare stato e algoritmi interni in coordinate polari. Tutto il resto del programma non sarebbe pi compatibile con questa versione della classe e andrebbe modificato per adattarvisi! Anche la scelta dei metodi da esportare deve essere oculata. Dovrebbero essere esportati solo i metodi strettamente necessari. Ogni metodo che viene esportato come un impegno per il futuro: qualcuno potrebbe trovarlo utile e decidere di usarlo, e a quel punto saremmo "costretti" a supportarlo anche in future revisioni della classe, per non creare problemi ad altre parti del programma. Per esempio, se avessimo una classe Stampante progettata inizialmente per gestire una stampante laser, potremmo fare l'errore di esportare il metodo rimescolaToner() insieme al ben pi significativo StampaPagina(). Passando a una stampante a diversa tecnologia, quel metodo potrebbe non avere pi senso, ma se per ipotesi fosse ormai stato usato da altre parti del programma avremmo un problema. Che fare? Implementarlo "per finta"? Eliminarlo e rivedere le parti di programma che lo usavano? Un dilemma che si sarebbe potuto evitare facendo maggior attenzione alla scelta dei metodi da esportare come nostra interfaccia esterna. Il meccanismo che consente di stabilire l'ambito di visibilit (scope) di attributi e metodi di una classe prevede in Java quattro livelli di "riservatezza", che limitano la popolazione abilitata ad accedervi. Public: accesso consentito da codice di qualunque classe. Package: accesso consentito da codice di questa classe o delle altre classi appartenenti al suo stesso package. Questo il default. Attenzione: se tutte le classi dell'applicativo appartengono a un unico package, questo default produce lo stesso effetto di scrivere "public" ovunque! Protected: accesso consentito da codice di questa classe o di sue sottoclassi. Private: accesso consentito solo da codice di questa classe.

2a lezione
2

Lo scoping
Attributi e metodi sono caratteristiche proprie di una classe. Se tale classe fosse completamente autonoma rispetto al resto del programma ed alle altre classi, queste sue propriet potrebbero anche rimanere "segrete" per tutti. In realt classi e oggetti modellano concetti, ruoli ed entit che costituiscono il programma e che sono gli attori del suo funzionamento. quindi normale e necessario un certo grado di interazione fra essi. Deve cio esistere in una classe un certo insieme di caratteristiche (metodi o attributi) che risultino accessibili anche ad altre classi, mentre altre caratteristiche potranno restare "segrete". vitale che le interfacce fra le parti del programma restino il pi possibile stabili. In caso contrario, un cambiamento nell'interfaccia esposta da una classe A, usata dalle classi B e C, richiederebbe di adattare queste ultime. Ora, il modo in cui espresso lo stato interno di una classe un dettaglio implementativo che pu essere necessario cambiare in occasione di una revisione degli algoritmi di quella classe. quindi immediato riconoscere che preferibile che l'interfaccia esposta all'esterno non comprenda attributi, ma soltanto metodi. Qualora fosse

Metodi e attributi static


Il concetto di "stato" che abbiamo introdotto riguardava la situazione in cui si trova ogni singolo oggetto, rappresentata dal valore degli attributi di quell'oggetto. Possiamo per immaginare anche uno stato di una intera categoria di oggetti: un'informazione che ha lo stesso valore e si applica allo stesso modo per ogni singolo oggetto della classe. Per esempio: l'importo o il termine di pagamento in un archivio di abbonati a un servizio; il tasso d'interesse applicato indistintamente a tutti i clienti di una banca; i salari minimi di categoria in un archivio di dipendenti; e cos via. Se una di queste propriet

condivise da tutti gli oggetti dovesse essere cambiata sarebbe comodo poterla modificare con un'azione unica fatta sulla classe, anzich dover agire individualmente su tutti gli oggetti di quella classe. Questo concetto supportato in Java dal modificatore static che si applica ad attributi o metodi. L'accesso ai membri static dall'esterno si pu effettuare dal nome della classe (come in Classe.azione1(...) ) anzich da una istanza particolare (come in oggetto.azione1(...)). Gli attributi dichiarati static vengono ovviamente allocati una sola volta, con notevole risparmio di memoria (un campo di tipo int condiviso da un milione di oggetti "costa" 4 bytes se dichiarato static, 4 megabytes in caso contrario!).

Java

2a lezione

4 Creazione di una GUI in Java

e librerie di classi fornite con Java comprendono un ricco repertorio di strumenti per creare con relativa semplicit un'interfaccia grafica (Graphical User Interface, GUI) di ottimo livello. Come primo passo, in questa puntata introdurremo i concetti fondamentali che necessario conoscere per poter usare tali librerie.

Programmazione a eventi
In Java il funzionamento di un semplice programma dotato di GUI segue generalmente queste fasi: 1. Inizializzazione. Questa fase comprende le seguenti attivit fondamentali: Creazione della finestra principale Definizione della disposizione (layout) dei sotto-oggetti (detti controlli o widget) all'interno della finestra Creazione dei widget occorrenti (pulsanti, liste, tabelle, check box, radio button, eccetera) e loro inserimento nella finestra secondo la struttura impostata; Definizione delle azioni che dovranno essere intraprese per ogni evento saliente (pressione di un Button, selezione in una List, movimento del mouse,...). Le azioni sono implementate come metodi di particolari classi. Associazione di tali azioni ai widget, attraverso la loro "nomina" ad "ascoltatori (Listeners) di eventi" per i vari widget. 2. Fase di attesa eventi. Il programma entra in una condizione di "inattivit vigile" in cui resta in "ascolto" da tastiera e mouse. 3. Reazione all'evento. Quando viene mosso il mouse, premuto un pulsante, digitato qualcosa sulla tastiera, ci rappresenta un evento per il programma. Da sottolineare che il programma reagisce solo a eventi del mouse capitati entro la propria "giurisdizione", rappresentata dal perimetro della finestra. Le azioni del mouse avvenute all'esterno di esso saranno competenza di altre applica-

zioni. Per quanto riguarda la tastiera, questa viene "sentita" se la finestra la finestra attiva, ossia, come si dice tecnicamente, "ha il focus". La finestra attiva riconoscibile perch, oltre a essere visibile in primo piano, evidenziata da Windows con un diverso colore della barra del titolo e con vari altri accorgimenti. 4. Una volta completata la reazione all'evento (reazione che potrebbe anche comportare la modifica dell'interfaccia grafica, con l'aggiunta o rimozione di widget o con la visualizzazione di una nuova finestra con messaggi o altri controlli) si ritorna al punto 2, la fase di attesa eventi. L'unica eccezione a questa regola si ha quando la reazione a un evento (per esempio: la chiusura della finestra) comporta l'uscita dal programma. In questo caso l'algoritmo termina. Questo paradigma di programmazione, divenuto ormai lo standard per lo sviluppo di interfacce grafiche, detto "programmazione a eventi". In esso, come si vede, la logica seguita dall'elaborazione non completamente definita dall'algoritmo: quello che avverr a run time determinato piuttosto dalla combinazione di due fattori: da un lato le azioni esercitate dall'esterno (eventi) e dall'altro le reazioni che sono state definite nel programma per ognuna di esse. come se durante il periodo di funzionamento dell'applicazione venissero lanciati tanti programmini "di reazione", uno al verificarsi di ogni evento. naturalmente importante che ogni reazione sia definita in modo corretto rispetto all'azione scatenante, ma anche che al termine della reazione l'applicazione venga riportata in uno stato stabile e "normale", pronta per ricevere e gestire eventuali altre azioni. I widget che popolano la GUI di un'applicazione trovano nel paradigma a oggetti una modellizzazione estremamente pratica, che si articola in questo modo: Viene definita una classe per

ogni tipo di widget (vedi tabella per alcuni esempi) Ogni particolare widget corrisponde a un'istanza di una di tali classi. Viene sfruttata a fondo l'ereditariet per mettere a fattor comune caratteristiche e comportamenti condivisi da diversi tipi di widget . Per esempio, tutti i widget hanno delle coordinate, delle dimensioni, un colore, una impostazione di visibilit e una di abilitazione, e cos via. Alcuni widget sono poi caratterizzati da una scritta, altri da un'immagine, e cos via. Si veda il riquadro per un esempio reale (una piccola "famiglia" di widget, quella dei pulsanti) tratto dalla libreria di componenti di Java. Esempio di ereditariet fra widget: la "dinastia" dei pulsanti AbstractButton: Il capostipite della "famiglia", modella e gestisce i comportamenti tipici di tutti i tipi di pulsante, in particolare il lancio di una procedura quando il pulsante viene premuto - JButton: implementa un pulsante che si pu premere e rilasciare - JToggleButton: un pulsante dotato di stato, in grado cio di "restare premuto" finch non viene premuto di nuovo - JCheckBox: casellina (di solito quadrata) con segno di spunta che indica lo stato di selezione; funzionamento indipendente da altre caselline - JRadioButton: come sopra, ma con casellina rotonda e un funzionamento in mutua esclusione nell'ambito di un gruppo di caselline - JMenuItem: un pulsante inserito nel contesto di un menu - JCheckBoxMenuItem: un checkbox utilizzato nel contesto di un menu - JMenu: un menuItem che, se premuto, fa apparire un sottomenu associato - JRadioButtonMenuItem: un radio button utilizzato nel contesto di un menu. L'elenco completo, con indice visuale organizzato per categorie, di tutti i widget disponibili nelle librerie Java di-

sponibile on line all'indirizzo http://java.sun.com/docs/book s/tutorial/uiswing/components/components.html Qui ci interessa innanzitutto suddividere le classi della libreria GUI in alcune grandi aree: I component (tutti i widget sono sottoclassi di una classe capostipite Component). Sono disponibili decine e decine di widget diversi, dal semplice pulsante, all'albero, alla tabella, al visualizzatore di files HTML, al dialog box per la scelta dei colori o per la scelta di un file. I container. Come gi accennato, i widget devono essere collocati in una finestra o comunque in un'area adatta a contenerli. Esistono vari tipi di queste aree; tutte sono modellate da classi che derivano dalla classe Container. Alcuni widget sono anche in grado di contenere a loro volta altri widget e quindi sono al tempo stesso dei Component e dei Container. I layout manager. Se i container forniscono l'area in cui disporre i component, i layout manager sono il "piano regolatore" di quest'area. Esistono layout manager che gestiscono la disposizione a griglia, in sovrimpressione, in fila monodirezionale, a bordi, e cos via. Gli eventi. Anche gli eventi sono modellati come oggetti e quindi appartengono a classi. Ci sono classi che modellano eventi del mouse, altre per gli eventi della tastiera, altri ancora per eventi delle finestre, per il focus, .... Classi e interfacce di supporto (colori, listener, adapters). Alcune di queste classi giocano un ruolo molto importante nello sviluppo delle GUI, come vedremo. Per mettere in pratica i concetti appena introdotti, vediamo un semplice esempio di GUI realizzata in Java. Il programma proposto, Displayer, un visualizzatore di immagini GIF o JPG. Per prima cosa digitatelo e compilatelo facendo riferimento alle procedure descritte nella prima lezione del corso. Una volta passata la compilazione, per utilizzare il program-

Java

2a lezione

Listato programma Displayer


// importazione classi da librerie grafiche import java.awt.*; import java.awt.event.*; import javax.swing.*;

ma baster lanciarlo da riga di comando di una finestra DOS (dopo essersi portati nella directory in cui si trova il file Directory.class risultato della compilazione) con la seguente sintassi: java Displayer nome-file-immagine Il parametro nome-file-immagine viene ricevuto dal programma come primo elemento del vettore di stringhe args[]. Il file immagine da caricare insomma specificato dalla stringa contenuta in args[0]. Dopo aver caricato in memoria l'immagine e averla inserita in un'etichetta per la visualizzazione, il programma aprir una finestra dimensionata in modo tale da poter contenere l'immagine pi un pulsante OK da usare per uscire dall'applicazione. Il nome del file immagine verr inoltre visualizzato nella barra del titolo (fig. 5). Per una prima spiegazione del funzionamento del programma si vedano i commenti inseriti nel listato; alcuni aspetti saranno ripresi e approfonditi nelle prossime puntate. In chiusura vi proponiamo, per esercizio, di modificare il programma in modo che il pulsante OK appaia in alto (NORTH) anzich in basso; che la scritta su di esso diventi "Exit Application"; e che, quando premuto, prima di far terminare l'applicazione, stampi un messaggio "Exit Button Pressed" sulla console DOS.

public class Displayer { // entry point del programma public static void main(String [] args) { // protestiamo se non viene fornito esattamente un parametro if(args.length!=1) { System.err.println("Sintassi: java Displayer <nomefile.gif>"); return; } // creazione finestra con titolo JFrame finestra=new JFrame("Displaying " + args[0]); // lettura immagine e sua installazione in memoria ImageIcon immagine=new ImageIcon(args[0]); // creazione di un widget etichetta (label) contenente l'immagine JLabel label=new JLabel(immagine); // inserimento della label nella zona centrale della finestra finestra.getContentPane().add(label); // creazione di un pulsante "OK" per uscire dal programma JButton termina=new JButton("OK");

ERRATA CORRIGE
// inserimento del pulsante nella zona inferiore della finestra finestra.getContentPane().add(termina, BorderLayout.SOUTH); // definizione di una reazione da associare al pulsante ActionListener azioneTermina=new ActionListener(){ public void actionPerformed(ActionEvent e) { // questo codice verra' chiamato quando si verifichera' // l'evento a cui sara' associata questa reazione. // l'effetto sara' quello di far terminare il programma: System.exit(0); } }; // associazione della reazione all'evento Action del pulsante termina.addActionListener(azioneTermina); // ingrandisce la finestra quanto basta per ospitare i widget finestra.pack(); // mostra la finestra ed entra in modo Attesa Eventi finestra.setVisible(true); } } Segnaliamo alcuni errori comparsi nella prima puntata del corso Java. a pag, 95, nel testo in rosso, il valore per l'inizializzazione del contatore non 1 ma 0, conformemente a quanto riportato nel listato di pag. 97. a pag. 96, fig. 1, sostituire N con X nelle espressioni riportate nei due rombi gialli; inoltre la freccia di ritorno deve arrivare a monte e non a valle del blocco "Ricevi il tentativo T". a pag. 103, nella figura che si riferisce al costrutto switch, nell'ultimo rettangolo in basso sostituire "operazioni N" con "eventuali operazioni default". Ci scusiamo con i lettori per l'inconveniente.

Per approfondimenti
Per una presentazione orientata a Java dei concetti base della programmazione a oggetti: http://java.sun.com/docs/book s/tutorial/java/concepts/ Per una introduzione completa allo sviluppo di interfacce grafiche in Java si veda l'apposita sezione del Java Tutorial: http://java.sun.com/docs/book s/tutorial/index.html

Java
A scuola con PC Open

3a lezione

ProgrammareJava Gestione file


di Marco Mussini

Mettiamo finalmente alla prova le competenze acquisite nelle precedenti lezioni, sia per quanto riguarda la grafica (creazione di nuovi widget) sia per quanto riguarda gli oggetti, i tipi e i costruttori di tipo

n questa terza lezione affronteremo la gestione dei dati su file in Java, accompagnando la trattazione della teoria di base con esempi presentati come funzioni tratte da uno stesso programma. Sar finalmente l'occasione per vedere concretamente in azione ed approfondire ulteriormente diverse conoscenze acquisite nelle prime due lezioni. In particolare, per la grafica vedremo la creazione e luso di alcuni nuovi widget, come i combobox, la struttura dei me-

nu, i pannelli per la visualizzazione di testo HTML e i dialog box; in tema di oggetti, tipi e costruttori di tipo vedremo un esempio duso reale di array e vari esempi di creazione di oggetti. Per quanto riguarda le tematiche di programmazione introdurremo invece il concetto di funzione ricorsiva e analizzeremo la questione degli errori presentando varie tecniche di gestione degli errori, fra cui quella basata sul concetto di eccezione.

IL CALENDARIO DELLE LEZIONI


Lezione 1
Rudimenti di programmazione - Terminologia essenziale - Dallalgoritmo al programma - Preparazione di Java - Il primo programma - Creare una GUI

Lezione 3

Lezione 2
Elementi di programmazione a oggetti e grafica - Tipi e identificatori - Astrazione funzionale - Programmazione a oggetti

File - Archiviare e trasferire i dati - Errori e gestione degli errori - La ricorsione - Un esempio

Lezione 4
Networking - Programmi client-server - Interazione con risorse Web

1 Archiviare e trasferire dati

concetti di file e di stream rappresentano il modello logico per il trattamento di archivi e flussi di dati pi largamente adottato nei linguaggi di programmazione, Java compreso. Prima di introdurre questi concetti riteniamo opportuna una premessa sull'hardware usato per la trasmissione e la memorizzazione persistente di dati. Infatti certe particolarit del modello potrebbero sembrare poco giustificate, quasi arbitrarie, se non si tenesse presente, anche in una prospettiva "storica", la natura dei dispositivi fisici usati per condurre queste operazioni. Il funzionamento della memoria centrale tale da consentire l'accesso ai dati in un tempo approssimativamente invariante (e piccolo) qualun-

que sia la locazione in cui si trovano e qualunque sia l'ordine in cui si presentano gli indirizzi da cui richiesta la lettura di dati: in altre parole, anche se la lettura dei dati venisse richiesta in un ordine perfettamente casuale, il tempo per l'accesso resterebbe approssimativamente costante. Per questa fondamentale peculiarit la si definisce di "memoria ad accesso casuale", o Random Access Memor y (RAM). La presenza della cache in realt modifica le prestazioni della RAM, migliorandole in caso di accesso sequenziale o comunque con casualit "prevedibile". Se dunque per la memoria centrale non fa praticamente nessuna differenza il particolare ordine con cui i dati vengano richiesti dal programma, al

contrario, trasmettere dati e memorizzarli in forma persistente sono processi che risultano fondamentalmente orientati all'accesso in ordine sequenziale, come i dispositivi che li supportano.

Sequenzialit nello scambio di dati


La capacit dei canali di comunicazione dati ha conosciuto una storia di incrementi incredibilmente veloci, con un ritmo di crescita (anche un decuplicamento ogni 3 anni) che ultimamente ha addirittura superato quello delle capacit di calcolo dei computer (all'incirca un raddoppio ogni 18 mesi). Nonostante questi fenomenali incrementi di prestazioni, la trasmissione dati intrinsecamente (e probabilmente rimarr) un processo seriale.

Concettualmente i dati vengono inviati sul canale un bit alla volta; le tecnologie si differenziano per la velocit alla quale riescono ad effettuare questa operazione, o per il mezzo trasmissivo adoperato, ma tutto avviene in modo sequenziale. In ambito microinformatico, per un immediato aumento della banda disponibile, si spesso fatto ricorso a canali paralleli (per le periferiche esterne l'interfaccia 8 bit Centronics per la stampante; per quelle interne, il collegamento IDE, EIDE, ATA 16 bit con i dischi; per il bus di sistema, gli standard ISA, MicroChannel, PCI o AGP; per la memoria si passati da 30 pin a 100, 144, 168 pin, per il processore basti pensare al Socket 940 di AMD). L'introduzione del parallelismo per finalizzata sola-

Java
mente alla velocizzazione di un trasferimento di dati che viene comunque come sequenziale. Semplicemente, il traffico, anzich sotto forma di una sequenza di bit, avviene come una sequenza di byte o di word. quindi un parallelismo puramente "tattico" in un quadro che rimane sostanzialmente seriale. Fra l'altro il continuo aumento delle frequenze di lavoro e il perfezionamento dell'elettronica di controllo rende ormai possibile ottenere prestazioni estremamente elevate senza bisogno di fare ricorso a canali paralleli. In parole povere, anzich usare un cavo parallelo a 32 bit e grossi connettori multipolari, se ne pu usare uno seriale, pi semplice ed economico, che trasferisce un bit alla volta, ma lavora a una frequenza 32 volte maggiore. Non a caso si assiste oggi a un "ritorno" delle soluzioni seriali: il caso del Serial ATA per i dischi, o dell'USB e del Firewire che hanno determinato l'uscita di scena della Centronics per il collegamento alla stampante e della SCSI per i dischi esterni (almeno in sistemi PC). Come vedremo, i costrutti logici messi a disposizione da linguaggi e librerie per modellare e gestire lo scambio di dati risentono di questa impostazione seriale. capacit, alta velocit di trasferimento dati e basso costo per byte. La contropartita, naturalmente, un alto tempo medio di accesso alle informazioni (il nastro deve venire avvolto e riavvolto per raggiungere il punto giusto), ma questo ha scarsa rilevanza nei sistemi di backup. Le unit a disco hanno consentito di affrancarsi parzialmente da un modello rigidamente seriale di accesso ai dati: la testina infatti libera di saltare da una traccia all'altra, anche se per farlo richiede un tempo non trascurabile che comporta un decadimento delle prestazioni rispetto all'accesso sequenziale, che rimane la modalit di utilizzo preferita dal disco. Il throughput istantaneo massimo, cache a parte, si ha evidentemente quando la testina rimane ferma su una traccia, mentre il disco (e con esso i dati) le ruota sotto a 7.200, 10.000 o pi giri al minuto. A proposito di sequenzialit nei sistemi a disco, conviene ricordare che i compact disk (nati, ricordiamolo, per rimpiazzare i dischi in vinile in applicazioni audio!) hanno una unica, lunghissima, traccia a spirale e non diverse tracce concentriche come gli hard disk e i floppy. L'accesso casuale nei CD , di fatto, tutto un saltare avanti e indietro nell'ambito di una unica traccia: un fenomeno che in linea di principio sarebbe paragonabile al fast forward o al rewind nelle unit a nastro. Le memorie di massa dal comportamento pi simile a quello della RAM, le uniche ad essere poco o per nulla orientate a un accesso sequenziale, sono le flash memory, che per, per capacit, costo e velocit, non sono ancora in grado di prendere il posto dei dischi magnetici e ottici se non in applicazioni di nicchia (miniaturizzazione, basso consumo, resistenza agli urti, capacit relativamente basse). Da osservare anche che, oltre all'incremento di capacit ed affidabilit, le tecnologie hardware delle memorie di massa hanno perseguito l'ottimizzazione e velocizzazione di due sole operazioni fondamentali: lettura e scrittura. L'organizzazione logica dei dati, la loro categorizzazione, le operazioni di confronto e copiatura e cos via, sono tipicamente a carico del sistema operativo o delle applicazioni in esecuzione sul computer. Con queste premesse non stupisce pi di tanto constatare che ancor oggi l'archiviazione dei dati sia vista come un processo che, nella sua forma pi semplice, modellato da linguaggi e librerie come fondamentalmente sequenziale ed ammette solo due operazioni principali (lettura e scrittura). veramente raro, per esempio, trovare nelle librerie di I/O di base operazioni per confrontare o modificare in modo aggregato dei dati archiviati: per esempio, istruzioni per cercare un testo in un file, istruzioni per confrontare due file o istruzioni per convertire in maiuscolo tutto il testo in un file. Questo tipo di elaborazioni si realizzano sempre in memoria centrale; in generale, quindi, la sequenza di operazioni sar una sequenza di questo tipo: apertura del file; lettura e/o elaborazione e/o scrittura; chiusura del file. Per contro, il limite costituito da un accesso esclusivamente sequenziale agli archivi apparso ben presto eccessivamente restrittivo, ragion per cui sono state introdotte varianti al modello logico di base che consentono di indirizzare le informazioni in modo pi diretto. Con esse, per leggere il millesimo elemento di un archivio non pi necessario arrivarci leggendo prima (solo per scartarli) i 999 elementi che lo precedono, ma diventa possibile leggerlo direttamente, identificandolo semplicemente come l'elemento di posto 1000. Oppure, per leggere una porzione di un testo archiviato, possibile formulare una richiesta del tipo "leggi 35 caratteri a partire dalla posizione 20561". Per questo tipo di accesso si parla di file ad accesso casuale (random access files), in analogia con la memoria RAM, ma, anche se Java li supporta con apposita classe e metodi, non ne parleremo oltre in questo articolo. Si tenga presente che per una visione dei dati pienamente astratta e orientata al loro modello logico piuttosto che a strutture fisiche, senza alcun

3a lezione
"residuo" di sequenzialit e con la possibilit di commissionare all'archivio stesso anche elaborazioni sui dati, ci si dovrebbe piuttosto rivolgere ai database. Questi, pur dovendosi basare su dispositivi di memorizzazione che sono sempre gli stessi, sono progettati per fornire un'astrazione sui dati molto pi ricca e pi "pulita" di quanto non sia possibile fare con i soli file. Vediamo dunque pi da vicino come funziona in Java l'input/output (I/O) sequenziale da e verso file o altro. Si distinguono nettamente due aree: l'I/O "grezzo", universale, gestito come un flusso di byte, e quello orientato specificamente allo scambio di dati di testo. Java offre classi e servizi diversi, specializzati per questi due tipi di flussi di dati.

Modello logico dell'I/O sequenziale a byte: i file e gli stream


In Java la questione dell'ingresso/uscita di dati (input/ output, I/O) trattata da apposite librerie di classi, riunite nel package java.io. Qui troviamo le modellizzazioni Java dei due concetti fondamentali che consentono di rappresentare e trattare la memorizzazione persistente e il trasferimento di dati: rispettivamente, il File e gli Stream. La classe File (java.io.File) in Java fornisce una rappresentazione astratta di un percorso (path o pathname) nel file system. Questo percorso potrebbe riferirsi a un file o anche, come caso particolare di file, a una directory. La classe File non contiene metodi per leggere o scrivere dati sul file; troveremo metodi per queste operazioni nelle classi Stream, come vedremo. I metodi della classe File consentono invece di controllare a che tipo di entit (file o directory) corrisponde il pathname, di controllare i relativi permessi di lettura o scrittura o la data di modifica, di creare un file, un file temporaneo o una directory con tale pathname e in generale di manipolare i pathname, estrarne parti, risalire alla directory superiore e cos via. essenziale sottolineare che creare un oggetto di classe File non implica affatto la creazione di un file nel file system. Per ottenere in pi questo

Sequenzialit nelle memorie di massa


Anche nelle memorie di massa si sono registrati progressi quantitativi veramente considerevoli, soprattutto sul versante della capacit (pi esattamente, del rapporto tra capacit e costo); ultimamente la capacit di dischi e (soprattutto) flash memory raddoppia a parit di prezzo ogni 12-18 mesi, anche in questo caso con ritmi di crescita pari o superiori a quelli delle CPU. Tuttavia, da un punto di vista qualitativo, ancor oggi la maggior parte dei sistemi di memoria di massa sono o sostanzialmente o strettamente sequenziali. I nastri sono l'esempio pi evidente di memorie di massa seriali. Non si pensi che siano solo dei "ruderi", ormai usciti di scena: cartucce a nastro sopravvivono ancor oggi in sistemi dedicati di backup, ove risultano un sistema assai competitivo in grado di offrire alta

Java
secondo effetto occorre chia-

3a lezione

mare appositi metodi. Gli oggetti di classe File, di per s, rappresentano semplicemente dei pathname. Parte dei metodi della classe File lavorano in astratto sul pathname in memoria, mentre altri effettuano operazioni o controlli sul file system in relazione al file o directory identificato dal pathname. Si vedano le note nella tabella a destra. Una volta specificato il path del file da creare o da elaborare creando un'opportuna istanza della classe File, per poter leggere o scrivere su quel file necessario creare uno Stream (flusso) per modellare e gestire lo scambio di dati da e verso il file. Vi una distinzione esplicita fra gli stream che gestiscono flussi di dati in uscita (OutputStream) e quelli che gestiscono flussi di data in entrata (InputStream). Sono disponibili varie classi che pur rientrando in una delle due categorie si caratterizzano per la destinazione o l'origine dei dati, oppure per qualche servizio a valore aggiunto che forniscono contestualmente al transito dei dati. Tutte le informazioni sulle principali classi di queste due categorie sono riportate in Tabella 1 e in Tabella 2.

Principali metodi della classe File


boolean canRead() boolean canWrite() boolean createNewFile() Verificano se, in base ai permessi del file, l'applicazione pu leggere o scrivere su di esso. Ritornano true o false. Crea nel file system un nuovo file corrispondente al pathname specificato dall'oggetto File su cui questo metodo viene chiamato. Ritorna un booleano, che vale false se un file con quel pathname esiste gi e di conseguenza la creazione fallita. Crea un file temporaneo in una directory arbitraria oppure nella directory che il sistema definisce per tale scopo (per esempio potrebbe essere quella indicata dalla variabile d'ambiente TEMP); il nome del temporaneo viene costruito usando il prefisso e il suffisso specificati (per esempio: "MyApp" e ".tmp") pi una parte pseudocasuale, aggiunta dal metodo, che usata per differenziare il file da quelli esistenti. - Ritornano un oggetto File che rappresenta il pathname del file creato sul file system. Rimuove dal file system il file o la directory che corrispondono al pathname specificato dall'istanza di File su cui questo metodo viene invocato. Ritorna un booleano che vale true se la cancellazione riuscita, false se fallita, qualunque sia la ragione. Una sorta di prenotazione di cancellazione automatica di un file. Il file verr eliminato da Java appena il programma terminer l'esecuzione. Utile per garantire che i file temporanei spariscano a fine algoritmo. La prenotazione, una volta fatta, non pu essere annullata. Confronta questo oggetto File con un altro. N.B.: si tratta quindi di un confronto fra due pathname, non di un confronto sul contenuto di due file nel file system! Verifica se sul file system esiste un file con un pathname uguale a quello specificato da questa istanza di classe File. Ritorna true o false. Vari metodi che consentono di estrarre le parti salienti di un pathname segnatamente, il path vero e proprio e il nome del file), di ottenerne la versione (assoluta e quella "canonica" (per esempio: ripulita di eventuali "." e ".." inutili, con la lettera dell'unit disco trasformata in maiuscolo, e cosi' via), di ricavare il path della directory superiore. A seconda dei metodi il risultato viene risultato come String o come un altra istanza di File. Vedere i programmi esempio per una applicazione di getName() e getParent(). Ritorna true se questo File denota un pathname che risulta assoluto. In ambiente DOS e Windows un pathname assoluto se include come prefisso un identificatore di unit disco locale o di rete. In UNIX un pathname assoluto se inizia con il carattere '/'. Questi metodi ritornano true se questo File denota un pathname che corrisponde, nel file system, rispettivamente a una directory o a un semplice file. Vedere i programmi esempio per una applicazione di questi metodi. Ritorna true se questo pathname corrisponde a un file nascosto. Ritorna la data alla quale stato modificato per l'ultima volta il file o directory denotato dal pathname espresso da questo oggetto File. La data espressa come un intero long che misura i millisecondi dalla mezzanotte del 1 gennaio 1970. Per convertirla in un formato pi maneggevole esistono apposite classi nel package java.util. (Date, Calendar) Ritorna la lunghezza del file. Assumendo che questo pathname corrisponda nel file system a una directory e non a un file, ritorna un array di stringhe che rappresentano i nomi di tutti i files contenuti in quella directory. Se il pathname corrisponde a una directory vuota ritorna un array di 0 elementi. Se il pathname non esiste nel file system, o corrisponde a un file, ritorna null. Come sopra ma consente in pi di specificare un filtro che lavora sul nome dei files della directory. possibile usare questa variante per ottenere l'elenco dei soli files .txt presenti in una directory, oppure per ottenere l'elenco di quelli il cui nome soddisfa una determinata espressione regolare o contiene una sottostringa desiderata. Vedere i programmi esempio per una applicazione di questo metodo. Come sopra, ma ritornano gli elenchi sotto forma di array di oggetti File anzich sotto forma di array di stringhe. Come sopra, ma il filtro pu lavorare sulle propriet dei file e non solo sul loro nome. Ad esempio, il filtro potrebbe selezionare solo i file recenti, solo i file e non le directory, solo i file sui quali permessa la scrittura, e cos via. In un PC con pi unit disco questo metodo consente di conoscerne l'elenco completo. Ognuna di esse, che si pu considerare la radice di un file system, viene rappresentata nell'array ritornato come un oggetto di tipo File. Vedere MyExplorer per una applicazione di listRoots(). Metodi per creare una directory. - Il primo, mkdir, va usato se tutti i livelli superiori esistono gi. Il secondo, mkdirs, si pu sempre usare e provvede automaticamente a creare eventuali livelli superiori presenti nel pathname richiesto, ma non ancora esistenti nel file system. Questi metodi ritornano true se la creazione riuscita e false in caso contrario. Effettua il rename di un file; ritorna false in caso di fallimento. Si noti che su alcuni sistemi (ad es. UNIX) il rename pu implicare lo spostamento da un file system all'altro: ci avviene se nell'operazione la radice del pathname viene cambiata. Il metodo imposta sul file system la data di ultima modifica del file al valore desiderato. Revoca i permessi di scrittura sul file rappresentato da questo oggetto File. Trasforma il pathname in String. Vedere i programmi esempio. Ritorna questo pathname rappresentato sotto forma di URL. Vedere i programmi esempio.

static File createTempFile (String prefix, String suffix) static File createTempFile (String prefix, String suffix, File directory) boolean delete()

void deleteOnExit()

boolean equals(Object obj) boolean exists() File getAbsoluteFile() String getAbsolutePath() File getCanonicalFile() String getCanonicalPath() String getName() String getParent() File getParentFile() String getPath() boolean isAbsolute()

boolean isDirectory() boolean isFile() boolean isHidden() long lastModified()

I/O sequenziale di testo: i Reader e i Writer


Se la classe File ci consente di identificare e creare file e directory del file system, di farvi riferimento e di interrogare il sistema sulle loro propriet, e le varie classi della famiglia degli Stream servono per modellare e gestire flussi di dati "grezzi" da e verso file ed aree di memoria, le classi che permettono di gestire con facilit un I/O di testo sono altre. Si tratta delle famiglie dei Reader e dei Writer, i cui pi importanti rappresentanti sono riportati nelle tabelle 3 e 4. I flussi di dati di testo hanno esigenze specifiche aggiuntive rispetto a quelle degli stream a byte. Per esempio, esiste un concetto di righe di testo, inesistente nei file binari, e quindi risultano molto utili funzioni per leggere e scrivere righe di testo (e non semplicemente singoli caratteri) oppure per emettere newline o per saltarli durante la lettura. Inoltre il testo pu essere

long length() String[] list()

String[] list(FilenameFilter filter)

File[] listFiles() File[] listFiles (FilenameFilter filter) File[] listFiles (FileFilter filter) static File[] listRoots()

boolean mkdir() boolean mkdirs()

boolean renameTo(File dest) boolean setLastModified (long time) boolean setReadOnly() String toString() URL toURL()

Java
rappresentato con numerosi

3a lezione
Tab 1 - Le principali classi della famiglia OutputStream, per la gestione in scrittura di flussi di dati a byte. Tutte le classi appartengono al package java.io
Superclasse astratta delle classi della famiglia OutputStream. Rappresenta un gestore di flussi di dati a bytes in uscita. La destinazione e il trattamento dei flussi varia a seconda della particolare sottoclasse. ByteArrayOutputStream Un OutputStream che invia i dati a un array di byte mantenuto internamente. possibile ottenere accesso a questo array per utilizzare i dati immagazzinati. anche disponibile un metodo per trasformare in String i bytes dell'array, secondo una codifica specificata. Vedere i programmi esempio. FileOutputStream Questo OutputStream invia il flusso di byte su un file. Il file verr aperto in scrittura, se necessario creandolo implicitamente. FilterOutputStream Superclasse da usare per implementare varianti di OutputStream che effettuano elaborazioni sui dati in transito e li riversano poi su un altro OutputStream. BufferedOutputStream OutputStream che ottimizza le prestazioni usando un buffer interno e raggruppando le scritture di dati. DataOutputStream Un OutputStream che fornisce supporto di basso livello per la scrittura di dati di tutti i tipi base Java. I dati vengono scritti usando una rappresentazione (diversa da quella testuale) che ne consente la rilettura con un DataInputStream. PrintStream Questa classe supporta la scrittura dei tipi base Java con metodi print e println simili a quelli della classe PrintWriter. I dati escono per con una rappresentazione che pu essere diversa da quella testuale. ObjectOutputStream Questa classe lavora generalmente in tandem con ObjectInputStream e fornisce metodi per serializzare oggetti e grafi di oggetti e trasmetterli sotto forma di un flusso di byte che potr essere inviato a un file, su rete o riletto per mezzo di un ObjectInputStream OutputStream

formalismi (encoding) diversi. Fra questi il pi comune ancora l'ASCII, ma si sta facendo velocemente largo Unicode, che ha il pregio di consentire il trattamento omogeneo di un mix di caratteri occidentali ed orientali da parte di uno stesso programma (per esempio: in una stessa pagina Web). Fra l'altro Unicode l'unica rappresentazione interna usata da Java per i caratteri, mentre nell'I/O, in cui vi la necessit di "piegarsi" alle consuetudini e alle esigenze del sistema circostante, Java supporta anche la trascodifica da e verso gli altri principali encoding, inclusi quelli giapponesi (JIS e SJIS), cinesi, coreani, e cos via. Risulta insomma chiaro che la gestione di file e flussi di dati di testo non pu essere fatta in modo efficace ragionando a byte e con primitive che consentono di "leggere i prossimi 1.000 bytes in un array"; quello che si desidera piuttosto "leggi la prossima riga di testo convertendola da ASCII a Unicode e creando come risultato una stringa Java". proprio a questo tipo di elaborazioni e trasformazioni che sono deputate le classi delle famiglie Reader e Writer. Si vedano le rispettive tabelle per i dettagli sulle operazioni messe a disposizione.

Tab. 2 - Le principali classi della famiglia InputStream, per la gestione in lettura di flussi di dati a byte. Tutte le classi appartengono al package java.io
InputStream Rappresentazione generica di un flusso di dati in lettura. I dati sono visti unicamente come una sequenza di byte, senza particolare significato, e possono essere letti uno alla volta o a gruppi, ma solo sequenzialmente. possibile solo saltare un numero specificato di byte, ma sempre "andando in avanti". Un InputStream che trae i suoi dati da un vettore di byte in memoria. Un InputStream che legge byte da un File. Un InputStream astratto che ne "avvolge" un altro, fornendo operazioni di lettura "a valore aggiunto" comprendenti eventuali trasformazioni ed elaborazioni sui dati in passaggio. Un InputStream che mantiene un'area di memoria tampone (buffer) per ottimizzare le prestazioni in lettura dei dati e per consentire di "tornare indietro" (entro limiti prefissati) nella lettura del file, attenuando la restrizione di una gestione totalmente sequenziale. Un InputStream che offre la possibilit di leggere dallo stream i tipi di dato primitivi di Java (v. lezione II) anzich semplici byte. Una classe che gestisce byte streams che rappresentano testi codificati in ASCII con separatori di linea; fornisce la funzionalit di conteggio delle linee lette. Luso sconsigliato in quanto esistono alternative pi generiche fra le classi della famiglia Reader (v.) Un InputStream che consente di leggere sequenzialmente e poi di... fare retromarcia (sempre sequenzialmente), reinserendo nello stream parte dei bytes letti (o anche dei bytes diversi da quelli letti). Un InputStream in grado di leggere singoli oggetti o grafi di oggetti interpretando opportunamente i byte del flusso di dati. Per poter essere interpretati correttamente, i byte nello stream devono essere rappresentazioni codificate dalla classe gemella ObjectOutputStream. Un InputStream che in combinazione con la classe simmetrica PipedOutputStream pu essere usato per costruire un canale di comunicazione seriale bufferizzato intra-applicazione che appare a ciascuna delle due parti come un normale Stream. Per esempio, in un word processor, la funzione di Stampa potrebbe scrivere comandi grafici per la stampante sul PipedOutputStream mentre un thread Spooler potrebbe leggerli al ritmo corretto dalla parte del PipedInputStream. Un InputStream che consente di modellare l'atto di leggere tutti i dati disponibili da uno stream e di passare poi a leggere quelli di un altro stream come una lettura da un unico stream virtuale contenente la sequenza dei dati dei due stream reali. Per esempio potrebbe essere usato su due FileInputStream per implementare facilmente l'Append di due file. Un InputStream che legge i suoi dati da un oggetto StringBuffer, che rappresenta un array di caratteri arricchito da funzioni di manipolazione. Concetto simile a quello di ByteArrayInputStream che invece lavora su bytes "grezzi" non necessariamente riferiti a testo del testo accumulandolo in memoria anzich scriverlo su file, potremmo combinare un PrintWriter (che fornisce comodi metodi per scrivere vari tipi di dato e gestire la scrittura per righe del testo) con un OutputStreamWriter (che gestisce

ByteArrayInputStream FileInputStream FilterInputStream BufferedInputStream DataInputStream

Costruire combinazioni di Reader, Writer e Stream


Una particolarit davvero molto comoda di questi sistemi di classi sta nel fatto che sono stati progettati per essere componibili, proprio come i pezzi di un gioco di costruzioni. In particolare, i Reader sono pensati per ricevere i dati o da altri Reader o da un qualsiasi tipo di InputStream, sui cui bytes verr effettuata al volo la conversione in caratteri. Simmetricamente, i Writer sono in grado di inviare i loro flussi di dati verso altri Writer oppure, trascodificati in byte, verso degli OutputStream. Per esempio, se volessimo leggere a righe da un file, potremmo combinare un BufferedReader (che supporta la lettura per linee) con un FileReader (che supporta la lettura di testo da file), come in Listato 1. Supponendo di aver preparato in C:\ un file ciao.txt come quello mostrato in figura Buffe-

LineNumberInputStream PushbackInputStream ObjectInputStream

PipedInputStream

SequenceInputStream

StringBufferInputStream

redReader1.bmp, il codice sopra riportato produrrebbe l'effetto mostrato in figura 1. Se invece volessimo scrivere

Java
Listato 1 Import java.io.*; public class FileExamples { public static void main(String [] args) { try { BufferedReader br= new BufferedReader(new FileReader("c:\\ciao.txt")); System.out.println(br.readLine()); } catch(Exception e) { e.printStackTrace(); } } } 1

3a lezione
Tab. 3 - Le principali classi della famiglia Writer, per gestire flussi di testo in uscita. Tutte appartengono al package java.io.
Writer BufferedWriter Superclasse astratta delle classi che forniscono metodi per scrivere flussi di testo a carattere. Classe che supporta metodi per la scrittura ottimizzata di flussi di testo. Le prestazioni migliorano grazie a un meccanismo che immagazzina in un buffer i dati in uscita, raggruppando per quanto possibile le scritture elementari. Fornisce inoltre un metodo per inviare un newline come definito dal sistema operativo. CharArrayWriter Un Writer che invia il flusso di dati di testo a un array di char. FilterWriter Superclasse astratta da usare per implementare dei Writer che effettuano trasformazioni o elaborazioni sui dati in transito. OutputStreamWriter Un Writer in grado di indirizzare il flusso di testo in uscita verso un OutputStream che lavora a bytes; gestisce automaticamente la conversione da caratteri a bytes, secondo la particolare codifica scelta o secondo quella di default. Vedere i programmi esempio. FileWriter La classe Writer che gestisce la scrittura di testo su file. Equivale alla combinazione fra un OutputStreamWriter e un FileOutputStream. Vedere i programmi esempio. PrintWriter Un Writer che fornisce metodi per emettere sotto forma di testo dati di tutti i tipi elementari definiti in Java, con (println) o senza (print) emissione di newline a fine linea. Vedere i programmi esempio. StringWriter Un Writer che immagazzina il flusso di testo in output in uno StringBuffer mantenuto internamente. In qualsiasi momento e' possibile richiedere l'accesso a questo StringBuffer e usarlo direttamente o costruire una String sulla base del suo contenuto.

Tab. 4 - Le principali classi della famiglia Reader, per gestire flussi di testo in entrata. Dal package java.io.
Reader Superclasse astratta delle classi che forniscono il servizio di leggere flussi di caratteri. A seconda delle classi, la fonte dei dati pu essere a sua volta una fonte a caratteri (un altro Reader) oppure una fonte a bytes (v. InputStreamReader) BufferedReader Classe di grande utilit che legge testo con un meccanismo di bufferizzazione interno che oltre a garantire un aumento di efficienza a run time ne consente una pratica gestione a righe. Vedere i programmi esempio. LineNumberReader Classe che fornisce supporto per leggere per righe un file di testo mantenendo un conteggio delle righe lette. CharArrayReader Un Reader che trae i suoi dati da un array di char (in analogia con ByteArrayInputStream che legge da un array di byte) InputStreamReader Importantissimo Reader che supporta la lettura a caratteri di flussi di dati di testo aventi origine da uno Stream funzionante a bytes. Questa classe realizza quindi la decodifica di formato da bytes a Unicode, ASCII o altri formati. FileReader Reader che legge flussi di dati di testo direttamente da file di testo, assumendo che la codifica usata per i caratteri sia quella di default. Vedere i programmi esempio. StringReader Simile a CharArrayReader ma trae i suoi dati da una String. utente e un thread secondario (spooler) che si occupa di gestire la coda di stampa. L'avvio dell'operazione di Stampa potrebbe allora comportare la creazione di un sistema interno di pipe attraverso cui il testo impaginato e pronto da stampare viene inviato dal main allo spooler. I due thread ricevono e trasmettono dati senza nemmeno accorgersi che la controparte si trova nella medesima Virtual Machine. Listato 2 import java.io.*; public class FileExamples { public static void main(String [] args) { try { ByteArrayOutputStream baos=new ByteArrayOutputStream(); PrintWriter pw=new PrintWriter(new OutputStreamWriter(baos)); pw.print("Testo e numeri "); pw.print(10.0f/3.0f); pw.println(" seguiti da newline"); pw.print("e infine da un'ultima stringa"); pw.flush(); //pw.close(); System.out.println("Dal buffer interno:"); System.out.println(baos.toString()); } catch(Exception e) { e.printStackTrace(); } } }

la trascodifica da testo a byte verso un OutputStream) e agganciare quest'ultimo a un ByteArrayOutputStream, che immagazzina in un array di byte i dati che riceve anzich mandarli a una destinazione esterna. Per esempio il listato 2 genera i risultati mostrati in Figura 2. inoltre possibile creare collegamenti fra Reader e Writer oppure fra InputStream e OutputStream, creando vere e proprie "tubazioni" tali per cui tutto il testo che viene scritto su un Writer risulti leggibile dalla parte del Reader, o tutti i byte che vengono inviati a un OutputStream sbuchino fuori dal lato dell'InputStream ad esso collegato. Non a caso, le classi che supportano questo tipo di "interscambio canalizzato" di dati hanno il prefisso Piped (letteralmente intubato). Queste tecniche risultano utili, per esempio, quando un programma progettato in modo tale da essere composto di parti indipendenti, in esecuzione contemporanea (in thread separati), le quali comunicano fra loro con flussi di dati seriali interni all'applicativo. Per esempio, in un word processor potrebbero essere presenti un thread (main) che gestisce l'impaginazione e l'interfaccia

I vantaggi per il riuso del codice


Grazie alla relazione di ereditariet che lega queste classi a poche classi radice (rispettivamente InputStream, OutputStream, Reader e Writer) risulta molto facile riutilizzare in situazioni nuove dei programmi originariamente scritti per interagire con un certo tipo di fonte o destinazione di dati.

Java

Per esempio, un programma in grado di leggere righe di testo da un file e metterle in ordine alfabetico potrebbe essere fatto lavorare invece su stringhe in memoria, semplicemente passandogli uno StringReader anzich il solito FileReader. Tutto quello che occorre che il programma sia scritto per funzionare con un generico Reader; sar allora possibile passargli unistanza di una qualunque sottoclasse di Reader ed esso funzioner con quella, eseguendo il suo normale tipo di elaborazione, senza accorgersi della differenza della sorgente dei dati. Analogamente, un programma abituato a salvare su disco i propri dati potrebbe essere riciclato per lavorare in contesto di rete, ingannandolo in modo tale che mandi i dati a una destinazione remota anzich scriverli su file. Questo possibile

3a lezione
semplicemente passandogli un OutputStream connesso a un Socket anzich un FileOutputStream aperto su un file. Per il programma, se scritto in modo tale da richiedere un generico OutputStream e non necessariamente un FileOutputStream, non far alcuna differenza.

2 Errori e relativa gestione: le eccezioni

na corretta gestione degli errori una questione di importanza primaria per qualsiasi programma non banale o impegnato in attivit importanti, dalle quali magari dipende la sicurezza dei dati, la continuit di un servizio o l'incolumit delle persone. Trattare correttamente gli errori poi indispensabile specialmente in programmi che si occupano pesantemente di I/O ed per questo che tratteremo l'argomento in questa puntata. In programmazione, con il termine generico di errori ci si pu tuttavia riferire fondamentalmente a tre concetti ben diversi.

Categorie di errori nei programmi


Innanzitutto vi sono gli errori rilevabili prima di mandare in esecuzione il programma. Si tratta di: errori puramente ortografici o lessicali: parole scritte male, maiuscole al posto di minuscole, punteggiatura mancante o errata. errori sintattici: espressioni o costrutti mancanti di qualche parte o scritte in un modo o in un ordine difforme da quello previsto dalla specifica del linguaggio. errori semantici rilevabili staticamente analizzando il programma: per esempio, mancanza di dichiarazioni; mancanza di inizializzazioni; incompatibilit statica di tipo in un assegnamento; omonimia fra nomi di classi, variabili e metodi; incompatibilit statica fra tipo e numero dei parametri formali e attuali di una chiamata a metodo; chiamata a un metodo inesistente per un oggetto di una certa classe; istruzioni irraggiungibili (dead code) a causa di ci-

cli infiniti o di istruzioni di ritorno anticipato incondizionato. Tutti questi errori risultano immediatamente evidenti esaminando il codice sorgente e il compilatore in grado di rilevarli e segnalarli con opportuna diagnostica, rifiutando di ultimare la traduzione e di mandare in esecuzione il programma. Perci, durante l'esecuzione (a run time) non dobbiamo preoccuparci di questa categoria di errori: si pu assumere che siano stati tutti scoperti e corretti in precedenza. Di conseguenza non occorrono (e infatti non esistono in alcun linguaggio) espedienti e costrutti sintattici per rilevare e trattare a run time errori di questo genere. Vi sono poi gli errori logici commessi dal programmatore nell'implementare un algoritmo: si tratta di errori che evidentemente il compilatore, in sede di analisi statica del codice sorgente, non ha potuto rilevare e segnalare. Non errori sintattici, quindi, e neppure errori semantici banali, ma errori concettuali. Per esempio: usare una formula sbagliata per un calcolo; effettuare un numero scorretto di iterazioni in un ciclo; prendere una decisione logica in base a una espressione sbagliata o effettuare confronti con valori di riferimento sbagliati; in un calcolo usare una variabile invece di un'altra (seppure di tipo compatibile, cosicch il compilatore non pu segnalare il problema); dimenticare di effettuare una certa elaborazione necessaria sui dati; passare a un metodo un parametro di tipo giusto, ma di valore sbagliato;

...e cos via. Errori di questo tipo si manifestano solo in esecuzione e per essi il compilatore pu dare solo un aiuto consultivo e in una casistica estremamente ristretta. Del resto, anche difficile pensare a costrutti sintattici del linguaggio che siano realmente in grado di prevenire il verificarsi a run time di questi errori o di contenerne le conseguenze. Esistono invece tecniche, supportate anche da Java, che aiutano a segnalare condizioni scorrette durante l'esecuzione, appena si verificano o, in qualche caso, immediatamente prima che si verifichino. Esse richiedono un impegno supplementare da parte dello sviluppatore, il quale, dopo aver scritto il programma, deve anche "disseminarlo" con delle condizioni logiche che devono essere sempre vere (asserzioni: un concetto introdotto da Hoare nel lontano 1968). Durante l'esecuzione Java verifica se le asserzioni sono rispettate o no e in caso contrario blocca l'esecuzione e segnala il problema, precisando quale asserzione stata violata. Contro gli errori logici, insomma, non esistono difese che siano al tempo stesso molto economiche e molto efficaci. Per la loro prevenzione, il mix di misure pi realistico e credibile una progettazione accurata del software, condotta da specialisti esperti che adottino pratiche standard di ispezione collegiale del codice, accompagnate da un uso intelligente e non smodato delle asserzioni e di altri accorgimenti che non abbiamo lo spazio di discutere qui. Esiste per un'altra importantissima categoria di condizioni anomale che si manifestano anch'esse a run time ma

non sono in alcun modo imputabili al programmatore. Si tratta di problemi derivanti dal malfunzionamento o dal raggiungimento di limiti fisici o tecnici del computer, delle sue periferiche o dei canali di comunicazione verso altri computer, oppure dalla violazione di una regola del sistema. Come vedremo, in molti casi sono condizioni d'errore tipiche della gestione dell'I/O, dei file e della rete, e per questo ne parleremo in questa puntata e nella prossima. Per esempio, solo per citare gli esempi pi classici: Memoria esaurita Il collegamento di rete caduto per congestione o guasto, l'host remoto non risponde, la controparte ha chiuso la comunicazione in corso. Un dispositivo con collegamento wireless, in precedenza raggiungibile, non risponde pi Batteria scarica Disco pieno Disco danneggiato o illeggibile Esiste un file con lo stesso nome Non si dispone di permessi sufficienti per scrivere/leggere un certo file o in una certa directory In alcuni di questi casi (non in tutti) sarebbe teoricamente possibile verificare la situazione prima di tentare l'operazione: per esempio, misurare la memoria libera o lo spazio su disco, verificare se un file omonimo esiste gi, interrogare il sistema operativo per controllare di quali permessi si dispone. Tuttavia, molte operazioni possono fallire per diversi possibili motivi, ed effettuare controlli multipli prima di eseguire ognuna di esse aumenterebbe

Java
troppo le dimensioni del programma e lo renderebbe praticamente illeggibile. In altri casi, per quegli eventi il cui verificarsi assolutamente imprevedibile dal punto di vista del computer (ad esempio: disco rovinato e illeggibile; la rete cessa di funzionare; un computer remoto viene spento durante una comunicazione; e cos via) il controllo sarebbe intrinsecamente impossibile. Insomma, i possibili "incidenti" che possono verificarsi o sono talmente numerosi che il loro controllo puntuale sarebbe troppo oneroso se effettuato in via preventiva, o sfuggono al controllo del programma e del programmatore e per questo semplicemente non possono essere prevenuti. Per tutte queste considerazioni si tende a preferire una gestione a posteriori di questa classe di problemi: in sostanza si tenta l'operazione senza verifiche preliminari e poi si verifica "com' andata" esaminando il risultato (exit code, error code, return status) segnalato dalla funzione o procedura. Questo pu assumere una serie di ben precisi valori, a ognuno dei quali corrisponde un diverso tipo di problema o errore verificatosi: per esempio, -1 potrebbe significare "operazione interrotta", -2 potrebbe significare "disco pieno", e cos via. (Secondo una convenzione adottata in molti linguaggi, in genere gli errori sono indicati con valori negativi, lo 0 significa "nessun errore" e valori positivi possono rappresentare altri tipi di risultati validi in assenza di errori). Cos facendo, anzich effettuare numerosi controlli prima di ogni singola operazione, se ne effettua uno solo subito dopo di essa: lo stato che viene segnalato dar conto di tutta la serie di possibili problemi eventualmente verificatisi durante lo svolgimento dell'operazione. Questa tecnica di gestione degli errori garantisce s una riduzione da N (controlli preliminari) a 1 (controllo dell'esito), con una semplificazione notevole per il programmatore, ma non ancora ottimale. Infatti un programma basato sul controllo degli errori a posteriori appare come un'alternanza continua fra "tenta di fare X" e "controlla com' andata": questa circostanza di certo non favorisce la leggibilit del codice. Le istruzioni sono ben pi numerose di quelle che sarebbero realmente necessarie per "fare quello che si deve fare" se non ci fosse da preoccuparsi degli errori dopo ogni operazione. Per esempio, in pseudocodice: risultato=apri file("prova.txt", scrittura) se risultato="disco pieno" allora segnala "spazio insufficiente!" e termina altrimenti se risultato="permessi insufficienti" allora segnala "impossibile creare il file!" e termina altrimenti se risultato= "settore difettoso" allora segnala "disco illeggibile. Vuoi formattarlo?" se risposta=SI allora formatta disco; ripeti tutto altrimenti termina altrimenti ... procedi con il lavoro ... Per snellire i programmi e conferire loro maggiore chiarezza Java e altri linguaggi introducono anche una tecnica diversa per il rilevamento e la gestione degli errori: la gestione per eccezioni, che nelle librerie di Java (non solo in quelle di I/O) sicuramente il meccanismo pi usato (e pu naturalmente essere usato anche dal programmatore per trattare i "suoi" errori applicativi specifici). Con questa visione, come impostazione di base, si preferisce pensare al contesto di esecuzione del programma come a una sorta di "mondo ideale" in cui non si verificano mai errori dovuti a cause esterne imprevedibili. La memoria infinita, la rete funziona sempre e i collegamenti sono stabili e veloci, il disco non finisce mai e non si guasta mai, e cos via. Potendo trascurare (in prima istanza) tanti fastidiosi dettagli, l'algoritmo pu quindi venire espresso in modo estremamente elegante e conciso, badando solo a realizzare l'implementazione pi chiara ed efficiente possibile. E gli errori, allora? Gli errori sono visti solo come eccezioni, che possono s capitare, ma che vengono trattati a parte, in separata sede, per non rovinare la pulizia generale del codice e per poter Listato 3

3a lezione

import java.io.*; public class FileExamples { public static void main(String [] args) { try { BufferedReader br= new BufferedReader(new FileReader(args[0])); PrintWriter pw= new PrintWriter(new FileWriter(args[1])); while(br.ready()) { pw.println(br.readLine().toUpperCase()); } br.close(); pw.close(); } catch(ArrayIndexOutOfBoundsException a) { System.err.println ("Numero di parametri errato!"); } catch(FileNotFoundException f) { System.err.println ("File non trovato: "+f.getMessage()); } catch(Exception e) { System.err.println ("Rilevata eccezione:"); e.printStackTrace(); } } } mettere a fattor comune la gestione di errori dello stesso tipo, bench capitati durante l'esecuzione di istruzioni diverse. La sezione di codice deputata alla gestione delle eccezioni pu cos essere anch'essa molto chiara e ordinata, dovendosi occupare esclusivamente di tentare un recupero delle condizioni di correttezza necessarie perch il programma possa procedere o perch l'operazione possa essere ritentata, o, se questo non proprio possibile, di provvedere a segnalare il problema e magari a far terminare il programma. Il listato 3 riporta un esempio di programma che legge un file di testo e ne produce un altro contenente lo stesso testo del primo, ma convertito tutto in maiuscole. Il programma gestisce i possibili errori di I/O (e quelli sui parametri) come delle eccezioni, usando il costrutto Java try.catch. Il blocco try contiene un arbitrario numero di istruzioni (al limite anche l'intero algoritmo) che vanno scritte come se non si verificassero mai errori. L'algoritmo espresso quindi in forma molto chiara e facilmente leggibile. Al verificarsi di una eccezione durante l'esecuzione di una qualunque delle istruzioni del blocco try, il controllo passa al primo blocco catch incontrato in grado di gestire o l'eccezione rilevata o una sua superclasse. Esiste una ereditariet anche fra eccezioni; l'eccezione pi generica, quella da cui derivano tutte le altre, la classe Exception e in caso di clausole catch multiple converr mettere in ultima posizione quella che la tratta, in modo tale da permettere alle catch precedenti di intercettare eccezioni pi specifiche. Il blocco catch ha cos l'opportunit di stampare diagnostici (come nel nostro esem-

Java
pio), ma potrebbe anche tenta3

3a lezione
fornisce metodi che supportano il trattamento degli errori, come il metodo printStackTrace() che consente di visualizzare su console la catena di chiamate fra metodi che ha portato al verificarsi dell'eccezione, con l'indicazione del numero di linea e modulo ove l'eccezione stata sollevata. L'effetto del trattamento degli errori nel programma di esempio evidente in figura 3: dapprima il programma viene lanciato senza parametri, facendo s che l'array args[] sia vuoto, cosicch le espressioni args[0] e args[1] risultano illegali e la loro esecuzione provoca un'eccezione ArrayIndexOutOfBoundsException successivamente viene specificato un file di input inesistente; ci provoca il verificarsi di un'eccezione FileNotFoundException quando il costruttore della classe FileReader tenta di aprirlo in lettura; viene poi eseguito correttamente il programma, passando un file da leggere esistente e un nome di file da scrivere valido; nessuna eccezione; infine viene specificato un file da scrivere su unit floppy, ma senza che l'unit contenga alcun disco: viene visualizzato un dialog di errore di Windows; rispondendo Annulla, il sistema operativo segnala il problema a Java, che lo riporta al programma come eccezione FileNotFoundException (anche se con "causale" diversa rispetto al caso precedente). Per completezza, sempre in figura, viene riportato il dump del file ciao.txt (in minuscolo) e del file ciao2.txt (in maiuscolo, generato dal programma).

re di riparare la situazione e ripetere automaticamente l'operazione, per esempio grazie a un ciclo esterno. Il costrutto prevede anche una clausola conclusiva opzionale finally, generalmente poco usata, che specifica azioni che devono essere eseguite solo dopo che il blocco try-catch stato attraversato, con o senza eccezioni. Per generare una eccezione (che un oggetto come qualsiasi altro, per appartenente a una classe che deriva da Exception) sufficiente creare un oggetto E di un qualche tipo di eccezione, eventualmente corredandolo con informazioni sull'errore avvenuto, e poi segnalarlo, usando la primitiva throw(E). Un metodo all'interno del quale vi siano generazioni esplicite di eccezioni oppure chiamate a metodi che dichiarano di poter generare eccezioni deve obbligatoriamente soddisfare ad almeno una delle due seguenti condizioni per ognuna delle eccezioni che possono verificarsi nel suo codice: gestire in locale l'eccezione con un opportuno blocco trycatch; dichiarare di poter sollevare a sua volta verso il chiamante (propagare) lo stesso tipo di eccezione. Questo equivale a dichiararsi "incompetenti in materia" e a passare la patata bollente a qualcuno, pi competente o pi generico, in grado di trattarla. Un metodo pu dichiarare di poter sollevare eccezioni apponendo la clausola throws sulla propria intestazione: per esempio, la dichiarazione:

public void combinaGuai(int parametro) throws MiaEccezioneApplicativa, FileNotFoundException segnala che questo metodo pu sollevare le due eccezioni citate e nessun'altra. Chiunque faccia uso del metodo "combinaGuai" deve obbligatoriamente gestire, oppure dichiarare a sua volta in clausola throws, tali due eccezioni. Da sottolineare che i costrutti try-catch sono annidabili: un intero sistema try-catch potrebbe essere contenuto nel try di un sistema try-catch pi grande. Se si verifica un'eccezione e nessun blocco catch risultato idoneo a gestirla, l'eccezione viene propagata all'eventuale struttura tr y-catch esterna, un po' come avviene per la propagazione delle eccezioni fra le chiamate a metodi. Segnaliamo inoltre che per alcune eccezioni non sussiste l'obbligo di essere gestite o dichiarate dai metodi. Si tratta delle eccezioni che derivano da RuntimeException: si tratta o di eccezioni che possono capitare in cos tante occasioni che se dovessero essere sempre gestite il programma diventerebbe illeggibile, oppure,

viceversa, di eccezioni talmente insolite e di scarso interesse da non rendere vantaggioso pretendere che il programmatore perda tempo a gestirle. Si preferisce quindi lasciare facolt di trattarle o di lasciare che accadano. Infine, ricordiamo per completezza che accanto alle Exception esistono classi del tutto simili e trattabili facoltativamente con costrutti try-catch (come le RuntimeException): gli Error. Questi rappresentano situazioni di errore fatale, assolutamente infrequenti, irrimediabili e incontrollabili (per esempio: errore interno della Virtual Machine che sta eseguendo il programma Java, oppure: bytecode Java alterato e inconsistente), in ogni caso previsto anche in questo caso un meccanismo di segnalazione, di modo che il programma, un attimo prima del crollo generale, possa magari tentare qualche azione protettiva, come quella di salvare automaticamente il lavoro in corso. Sia Exception che Error derivano da una classe super-generica, Throwable, che la capostipite di tutte le classi accettate dalla primitiva throws e che

3 La ricorsione

uando si ha a che fare con il file system ci si trova immediatamente alle prese con la struttura gerarchica delle directory. Queste formano un albero avente per radice l'unit disco, per rami le directories stesse e per foglie (nodi terminali) i files oppure le directories vuote. Se volessimo scrivere un algoritmo che visiti il file system e ne elenchi tutto il contenuto,

molto probabilmente arriveremmo a una formulazione di questo tipo (qui a fianco). Come si pu notare dall'ultima riga, questo algoritmo invoca s stesso. Infatti il problema di visualizzare l'albero sotteso a un nodo X si pu esprimere decomponendolo in due sottoproblemi: (1) visitare le foglie F attaccate al nodo e (2) stampare ognuno degli eventuali sottoalberi Y1...Yn sot-

Algoritmo per la visita completa del file system Visualizza directory(dir X) Stampa il nome di X e vai a capo Se X contiene dei files, allora o per ogni file F contenuto direttamente sotto X - Visualizza il nome di F e vai a capo Se X contiene delle directory, allora o per ogni directory Y contenuta direttamente sotto X - Visualizza directory(Y)

Java
tesi al nodo. Quest'ultimo sotl'avversario e riconsiderando altrettante situazioni e in ognuna tutte le mie possibili contro-contro-mosse, e cos via,... fino a una profondit di analisi prefissata o finch la memoria non si esaurisce. E cos via. Tutti questi problemi potrebbero essere trattati con algoritmi iterativi (ossia con cicli), che fra l'altro in diversi casi risulterebbero di gran lunga migliori in termini di velocit di esecuzione e di memoria occupata, per la nostra mente trova spesso spontaneo esprimere la strategia risolutiva in forma ricorsiva, "riduzionista": per risolvere un problema difficile lo si riconduce a uno o pi sottoproblemi che si sanno immediatamente risolvere o che comunque risultano meno difficili da risolvere. Fra l'altro in qualche caso sarebbe veramente scomodo e poco chiaro esprimere un algoritmo iterativo per un problema intrinsecamente ricorsivo; quando cos si preferisce far passare in secondo piano la questione della minor efficienza di fronte al vantaggio della chiarezza di esposizione. Tornando al nostro algoritmo di visita del file system, con un po' di attenzione ci renderemo conto che purtroppo l'output apparirebbe "piatto" e non fornirebbe un'idea immediata della relazione di contenimento esistente fra le directory. Per questo occorre stampa-

3a lezione

toproblema (2) uguale al precedente, in quanto si tratta ancora di visualizzare un albero. Il fatto che l'algoritmo, in uno o pi punti, sia espresso in termini di s stesso, chiamato ricorsione e l'algoritmo con questa caratteristica ricorsivo. Un algoritmo ricorsivo ha sempre le seguenti caratteristiche: tratta il caso generale riconducendolo a una combinazione di casi pi semplici tratta il caso banale (detto "base della ricorsione") in modo immediato, senza pi "rimandarlo". Problemi che implicano il trattamento di strutture e modelli "autosimili", come questo degli alberi, trovano la loro trattazione naturale in forma ricorsiva. Altri esempi sono: Elaborazione di liste. Una lista si pu vedere come l'elemento di testa + un'altra lista (tutta la coda). (caso banale: lista di un solo elemento) Calcolo del fattoriale. Il fattoriale di un N positivo pari a N moltiplicato per il fattoriale di N-1. (caso banale: N=1) Calcolo di potenza intera. N intero positivo elevato a K intero positivo pari a N moltiplicato per N elevato a K-1. (caso banale: K=1) Ricerca di un carattere in una stringa non nulla. Il carattere presente se in prima posizione, oppure se presente nel resto della stringa. (caso banale: stringa di un solo carattere) Casi molto pi complessi, come la valutazione delle conseguenze delle mosse in un programma che gioca a scacchi o a dama: la valutazione della bont di una mossa pu essere effettuata ipotizzando di aver compiuto la mossa e riconsiderando la situazione che si verrebbe a creare, provando una per una tutte le possibili contromosse del-

Listato 4 import java.io.*; public class FileExamples { public static void main(String [] args) { dump(new File("."),0); } public static void dump(File path, int indent) { for(int i=0; i<indent; i++) System.out.print(" "); System.out.println(path.getName()); File[] files=path.listFiles(); for(int j=0; j<files.length; j++) { if(files[j].isDirectory()) { dump(files[j],indent+1); } } } } re il testo con opportuni rientri sul margine sinistro. L'entit del rientro deve naturalmente crescere con la profondit della directory nell'albero e quindi deve essere un'informazione che "portiamo con noi", incrementandola, nelle chiamate ricorsive. Per supportare questo concetto l'algoritmo potr essere modificato come segue (vedi qui a sinistra). Unimplementazione in Java di questo algoritmo, limitata al solo caso delle directories, riportata in Listato 4. Un piccolo estratto del suo output, catturato mentre era in corso la visita dell'albero di installazione della documentazione di Java, invece riportato in Figura 4. 4

Visualizza directory(dir X, rientro R) Visualizza R spazi Visualizza il nome di X Vai a capo Se X contiene dei files, allora o per ogni file F contenuto direttamente sotto X - Visualizza R+1 spazi - Visualizza il nome di F - Vai a capo Se X contiene delle directory, allora o per ogni directory Y contenuta direttamente sotto X - Visualizza directory(Y, R+1)

4 Un esempio riepilogativo

l programma riepilogativo che vi proponiamo in questa puntata dimostra l'applicazione di alcuni concetti sui file e riprende, sviluppandoli ulteriormente, diversi temi discus-

si nella puntata precedente a proposito di interfacce grafiche. Il programma funziona come una sorta di mini File Explorer e consente di muoversi

nell'albero delle directory di tutti i drive disponibili sul sistema. Per alcuni tipi di file (HTML, TXT, JPG/JPEG e GIF) inoltre in grado di funzionare da visualizzatore; per visionare

il file sufficiente selezionarlo con il mouse. Per ragioni di spazio vedremo qui solo una rassegna di brani di codice che implementano alcune delle funzioni del

Java
Riquadro A JFrame jfMain=new JFrame("MyExplorer"); // crea finestra con titolo JMenuBar jmbMenu=new JMenuBar(); // crea toolbar inizialmente vuota jfMain.setJMenuBar(jmbMenu); // aggancia menubar a finestra JMenu jmFile=new JMenu("File"); // crea menu File inizialmente vuoto jmFile.setMnemonic(KeyEvent.VK_F); // imposta mnemonico (ALT)F per menu File jmbMenu.add(jmFile); // aggancia menu File a menu bar JMenuItem jmiOpen=new JMenuItem("Open...", KeyEvent.VK_O); // crea menuItem Open con mnemonico O jmFile.add(jmiOpen); // aggancia menuItem Open a menu File JMenuItem jmiExit=new JMenuItem("Exit", KeyEvent.VK_E); // menuItem Exit, mnemonico E jmFile.add(jmiExit); // aggancia a menu File // imposta dimensione iniziale dell'area principale della finestra // 400x300 pixel per una finestra in rapporto di forma 4:3 alla nascita jfMain.getContentPane().setPreferredSize(new Dimension(400,300)); jfMain.pack(); // consenti adattamento di geometrie e dimensioni jfMain.setVisible(true); // mostra finestra e relativi contenuti Riquadro B // registra listener che termina il programma // quando viene chiusa la finestra jfMain.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent w) { // Uscita dal programma System.exit(0); } }); Riquadro C // registra un listener per gestire l'azione Open. jmiOpen.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { // Crea un File Open dialog JFileChooser chooser = new JFileChooser(); // Visualizza il dialog e ricevi la scelta dell'utente int returnVal = chooser.showOpenDialog(jfMain); // Se e' stato premuto Apri, visualizziamo il file scelto if(returnVal == JFileChooser.APPROVE_OPTION) { // chiamiamo la procedura di visualizzazione // passando come parametro il file selezionato nel dialog displayFile(chooser.getSelectedFile()); } } }); pleto per tutti i dettagli.

3a lezione
quale va indicata la URL del file da aprire. Ecco come si procede: (f il file da mostrare; il JEditorPane verr poi inserito in un dialog box e visualizzato - riquadro D) Il caso di file GIF o JPEG trattato in modo simile a quanto presentato nel programma Displayer citato nella scorsa puntata.

Un elenco di file system


La nostra applicazione deve offrire la possibilit di selezionare il file system su cui si intende operare. Questo permesso da un JComboBox caricato con i nomi di unit disco ritornati dal metodo listRoots della classe File (riquadro E). Il JComboBox dichiarato final (costante) per poterlo referenziare nell'actionListener che gestisce l'evento di cambio selezione: quello che viene fatto quando questo si verifica aggiornare il path corrente e rigenerare gli elenchi di file e directory chiamando aggiornaElenchi (v.) (riquadro F).

Un dialog box per muoversi nel file system


Quando l'utente seleziona File/Open necessario visualizzare un dialog box che gli consenta di selezionare il file che desidera aprire. Per farlo provvediamo a definire un ActionListener che crea un'istanza di JFileChooser, esamina la decisione presa dall'utente e in caso positivo invoca il metodo displayFile che si occupa di visualizzare il file scelto (riquadro C).

Elencare files o directories


Una volta creati due widget JList per ospitare gli elenchi di file e directory il problema quello di caricarli con i dati corretti. Di questo si occupa il metodo aggiornaElenchi(). Per esempio, per l'aggiornamento dell'elenco delle directory sotto un certo path, si provvede anzitutto a una visita

Visualizzare in finestra il contenuto di un file di testo o HTML


Se il file da visualizzare un file di testo o un file HTML, viene impiegato un JEditorPane, uno speciale widget di Java, al

Riquadro D // Costruisci un visualizzatore universale di files di testo JEditorPane jepPanel=new JEditorPane(); // Disabilita la possibilita' di modificare il testo visualizzato jepPanel.setEditable(false);

programma. Il listato comple-

to, MyExplorer.java, presente su CD Guida 2 allegato alla rivista.

Creazione della finestra principale e dei menu


Per prima cosa ovviamente necessario creare la finestra (un JFrame con titolo appropriato). Si passa poi a creare la MenuBar, alla quale viene aggiunto un menu File. In questo menu vengono poi inserite due voci: Open e Exit, entrambe dotate di acceleratori. La MenuBar cos creata viene infine inserita sul JFrame. L'operazione si conclude

con la definizione delle dimensioni iniziali e con la visualizzazione della finestra principale. Le relative istruzioni sono riportate nel riquadro A: Poich desideriamo inoltre che il programma termini chiudendo la finestra, e questo comportamento non automatico, dobbiamo registrare un WindowListener che gestisca tale circostanza. Il codice che se ne occupa nel riquadro B: Per semplicit, questi spezzoni di codice non comprendono la parte che inserisce i due elenchi (directory e file) a sinistra e a destra nel centro del JFrame. Vedere il listato com-

// Inserisci il visualizzatore nell'area centrale del dialog box jdFileViewer.getContentPane().add(new JScrollPane(jepPanel)); // Preparati a gestire un errore di "URL non valida" o un errore // di I/O try { // Configura il visualizzatore per mostrare il file richiesto jepPanel.setPage(f.toURL()); // Scegli una dimensione iniziale per il dialog box initialSize=new Dimension(800,600); } catch(Exception e) { // In caso di errore stampa un report sul tipo di errore // e sul punto in cui si e' verificato e.printStackTrace(); }

Java
del suo contenuto (ottenuta
nuovo valore e rigenerare tutti gli elenchi. Ecco come (riquadro I).

3a lezione

con il metodo list()), visita nella quale si utilizza una classe che nella directory seleziona i soli "figli" che siano a loro volta delle directories (riquadro G): Una volta ottenuto l'elenco delle directory questo, se non vuoto, pu essere caricato (con setListData()) nella JList per farlo apparire su schermo (riquadro H).

Riquadro E // Ottiene dal sistema una lista dei file system disponibili File[] drives=File.listRoots(); // Crea un ComboBox per consentire la selezione del drive final JComboBox jcbDrives=new JComboBox(drives); Riquadro F // Registra un listener per reagire all'azione di selezione // di una voce dell'elenco jcbDrives.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent a) { // Aggiorna il path corrente con la nuova selezione currPath=(File)jcbDrives.getSelectedItem(); // aggiorna gli elenchi in funzione del nuovo path aggiornaElenchi(); } }); Riquadro G // Classe incaricata di fare da filtro sui files. // Rifiuta qualsiasi file che non sia una directory. class AccettaSoloDirectories implements FilenameFilter { public boolean accept(File path, String name) { // costruisce un File con path e nome forniti, // verifica se e' una directory e ritorna l'esito della verifica // (true o false) return new File(path,name).isDirectory(); } } Riquadro H // sono presenti cartelle - tagliamo il prefisso // il metodo getName della classe File e' in grado // di ritornare la sola parte terminale di un path completo for (int i=0; i<folders.length; i++) folders[i]=new File(folders[i]).getName(); // Visualizza il nuovo elenco nella lista delle directories jlFolders.setListData(folders);

MyExplorer in funzione
Per un approfondimento di tutti gli aspetti rimandiamo all'esame del listato completo del programma, dettagliatamente commentato. Le figure mostrano il programma in azione in alcune situazioni tipiche: la navigazione fra directory (immagine 4), l'apertura di un file HTML (immagine 6) e il dialog box di selezione del file da visualizzare (immagine 5). Come esercizio conclusivo vi proponiamo di modificare il programma in modo tale che visualizzi anche altri formati di testo (come INI, INF o BAT) e perch visualizzi, oltre al nome dei files, anche la loro dimensione in bytes.

Gestire la selezione in elenco


Quando l'utente seleziona una delle directory dell'elenco di sinistra o uno dei file dell'elenco di destra dobbiamo reagire opportunamente. Per un file, in particolare, dobbiamo lanciare il visualizzatore invocando displayFile; per una directory dobbiamo invece aggiornare il path con il 4

Cosa ci aspetta
Nella prossima puntata continueremo lo sviluppo di questi concetti, in particolare per quanto riguarda gli Stream, che si riveleranno indispensabili, come vedremo, nello scambio dati via rete.
Un paio di situazioni tipiche durante il funzionamento di MyExplorer: la navigazione tra directory (in alto); il dialog box di selezione del file da visualizzare (qui a destra) e lapertura di un file HTML (qui sotto).

Riquadro I /// registrazione event handler per reagire al cambio di cartella jlFolders.addMouseListener(new MouseAdapter() { // Il metodo verr chiamato quando il tasto del mouse // verra' rilasciato public void mouseReleased(MouseEvent me) { // Esclude il caso di selezione nulla if(jlFolders.getSelectedValue()!=null) { // Costruisci il path completo della nuova directory currPath=new File (currPath,jlFolders.getSelectedValue().toString()); // Aggiorna elenchi in funzione della nuova directory aggiornaElenchi(); } } });

Java
A scuola con PC Open

4a lezione

ProgrammareJava Comunicare in rete


D
opo un'introduzione sui concetti fondamentali della rete TCP/IP vedremo quali sono le classi e i metodi che a livello logico rappresentano in Java le varie entit in gioco. L'esposizione sar accompagnata da esempi di codice che useremo per dimostrare le tecniche presentate. Come sempre sar l'occasione per riprendere anche alcuni dei temi toccati nelle puntate precedenti, come le classi di I/O e il supporto per la grafica, introducendo qualche nuovo concetto anche su tali aree. Il nostro risultato finale sar nientemeno che un piccolo Web browser! una delle soluzioni d'elezione per scrivere applicazioni di rete, hanno avuto tutta una serie di risposte tecnologiche che oggi fanno parte del "patrimonio genetico" del linguaggio e delle sue librerie, offrendo allo sviluppatore un notevole valore aggiunto.

di Marco Mussini

In questa ultima puntata affrontiamo il tema Internet, con particolare enfasi sulla programmazione in rete

IL CALENDARIO DELLE LEZIONI


Lezione 1
Rudimenti di programmazione - Terminologia essenziale - Dallalgoritmo al programma - Preparazione di Java - Il primo programma - Creare una GUI

Lezione 3
File - Archiviare e trasferire i dati - Errori e gestione degli errori - La ricorsione - Un esempio

Lezione 2
Elementi di programmazione a oggetti e grafica - Tipi e identificatori - Astrazione funzionale - Programmazione a oggetti
l'applicazione, non sar necessaria alcuna azione amministrativa per aggiornare il client. Semplicemente, la prossima volta che verr aperta la pagina Web che contiene l'applet, questo verr scaricato di nuovo (stavolta nella versione pi recente) e l'utente noter immediatamente la novit. Altra conseguenza positiva dell'utilizzo del bytecode come "linguaggio universale" e della Virtual Machine come "esecutore universale" la quasi perfetta portabilit di Java. Con questo termine si intende la possibilit di far girare programmi su macchine di tipo diverso senza doverle sottoporre ad adattamenti complicati. Nel caso di Java, il bytecode come una sorta di speciale linguaggio macchina che viene riconosciuto da un programma, la Virtual Machine appunto, che simula una CPU fisica. Il bytecode pu quindi essere eseguito dovunque vi sia una Virtual Machine. E, come abbiamo gi accennato all'inizio di que-

Il bytecode
Fra esse, una delle pi rilevanti l'utilizzo del bytecode come formato "oggetto" per i programmi Java. Il bytecode estremamente compatto, inoltre non porta con s le librerie usate dall'applicazione, in quanto si pu sempre contare di trovarle nell'ambiente runtime. Di conseguenza, a seconda dei casi, il codice applicativo di un programma scritto in Java pu arrivare a essere anche 10100 volte pi piccolo che se lo stesso programma fosse stato sviluppato in un altro linguaggio tradizionale. Applicazioni di piccole dimensioni ben si prestano a essere scaricate via rete, perch richiedono un tempo relativamente breve per essere trasferite sul client per l'esecuzione. Questa propriet stata sfruttata in Java, fin dal 1997, per proporre una forma innovativa di distribuzione in rete di programmi: l'applet. Un applet un'applicazione ("immersa" in una pagina Web) che non risiede stabilmente sulla piattaforma di esecuzione, ma viene scaricata e avviata "al volo" ogni volta che serve. Non serve installazione e non viene occupato spazio sul disco del client. Inoltre, in caso di rilascio di una nuova versione del-

Lezione 4

Networking - Programmare con i socket - Java e il TCP/IP - Web Browser - Un esempio

Java e Internet: unione perfetta


Possiamo dire che il potente supporto per lo sviluppo di applicazioni di rete fra i principali aspetti caratterizzanti della tecnologia Java. Java anzi stato sviluppato pensando ad applicazioni per la rete e nella rete. L'intenzione dei progettisti del linguaggio e delle sue librerie standard sempre stata quella di rendere possibile lo sviluppo di applicazioni che fossero in grado di essere rapidamente scaricate via rete e installate al volo sul client, che potessero girare su qualsiasi hardware e fossero in grado di operare in qualsiasi contesto linguistico, rimanendo facili da scrivere, con una robusta protezione contro gli errori applicativi e contro i tentativi di abuso come virus e trojan horse. Questi requisiti di progetto, tutti finalizzati a fare di Java

sto corso, esistono Virtual Machine Java per dispositivi di ogni genere, dal microchip immerso in una smart card fino al personal computer, dal palmare al controllore interno di un elettrodomestico, dal telefono cellulare ai pi bizzarri gadget tecnologici. In un mondo in cui i dispositivi di tutte queste categorie (elettrodomestici compresi, ormai!) vengono collegati a Internet per amplificarne l'utilit e il ventaglio di applicazioni, la tecnologia Java diventata una sorta di "lingua franca" grazie alla quale una stessa applicazione pu essere riciclata su pi piattaforme e mercati; di conseguenza (aspetto da non sottovalutare) gli stessi sviluppatori Java possono applicare le proprie conoscenze e la propria esperienza a un amplissimo campo applicativo. Nel caso particolare delle applicazioni Internet, la propriet di Java di essere un linguaggio portabile di estrema importanza perch gli utenti di

Java
Internet si servono di terminali di tipi molto diversi: il sistema operativo pu essere una delle tante versioni esistenti di Windows, Macintosh, Linux, Unix; l'hardware pu essere Intel IA32 compatibile (nel caso dei PC), ma anche PowerMac (per i Mac), XScale/ARM/MIPS (per i palmari), IA64/PA-RISC/SPARC (per workstations), e cos via. Un programma tradizionale, compilato in codice macchina, pu girare su una qualunque di queste architetture, ma solo su quella. Per poterlo usare in un altro ambiente deve essere ricompilato apposta. Java d invece la possibilit di scrivere programmi come se tutta questa diversit non esistesse. Le applicazioni vengono scritte una sola volta e risultano poi eseguibili su praticamente tutte le piattaforme client per Internet. Un'altra peculiarit di Internet e delle applicazioni per Internet sta nel fatto che a uno stesso sito Web possono collegarsi utenti di tutto il mondo e di tutte le lingue. Nel mondo occidentale le lingue utilizzano quasi tutte l'alfabeto latino, "condito" da una variet pi o meno ampia di segni diacritici. Tuttavia gran parte della popolazione mondiale utilizza alfabeti diversi: basti pensare al cirillico utilizzato in Russia, all'ebraico, all'arabo, al sanscrito, al coreano, al cinese, al giapponese. Risulta subito chiaro che un linguaggio che come Java si proponga lo scopo di supportare lo sviluppo di applicazioni Internet deve risolvere il problema del trattamento di stringhe e messaggi scritti in tutti questi alfabeti. E infatti anche in questo comparto Java adotta, fin dal suo esordio, la soluzione standard pi potente e completa: il set di caratteri Unicode, al posto dell'"eurocentrico" ASCII. Grazie a questa scelta possibile scrivere un programma una sola volta, lavorando con il proprio set di caratteri abituale, e poi adattarlo ad altri tipi di alfabeto senza cambiare il codice, ma traducendo solo le stringhe e i messaggi: un lavoro che pu fare un interprete, lasciando il programmatore libero di concentrarsi sul suo lavoro. Pu sembrare unovvia separazione dei ruoli, ma prima dell'avvento di Unicode (e di Java che per primo lo ha adottato) non era cos. Per "internazionalizzare" una applicazione pensata per lavorare con caratteri latini, e dotarla di supporto per caratteri asiatici, era sempre necessaria una revisione profonda e meticolosa del codice, per adattare tutti i punti in cui venivano trattati dati di tipo testuale. Questo lavoro di adattamento, nei casi pi complessi, poteva richiedere uno sforzo dello stesso ordine di grandezza di quello speso per lo sviluppo dell'intera applicazione in versione originale. Il vantaggio offerto da Java anche qui evidente. Un'ultima propriet di Java che si rivela di grande importanza per lo sviluppo di applicazioni in rete il suo sofisticato sistema di protezione che gli conferisce una sorta di preziosa "immunit" rispetto ad attacchi e abusi come worm, virus e trojan horses. Tutti sappiamo che questi "nemici", per potersi introdurre nel nostro sistema, devono essere eseguiti. Per questo la maggior parte di essi si annida dentro gli eseguibili delle applicazioni, dove di solito un buon antivirus aggiornato in grado di scovarli. Anche i programmi Java scaricati come applet o sotto altra forma provengono dalla rete e per questo devono sempre essere considerati sospetti fino a prova contraria. Per assicurare la "disinfezione" delle applicazioni Java stata progettata una strategia preventiva. Invece di cercare le "firme" dei virus noti all'interno dell'applicazione, si effettua un'analisi approfondita di tutte

4a lezione
le sue istruzioni, alla ricerca di qualsiasi istruzione considerata pericolosa per la sicurezza del sistema o dei dati: istruzioni non riconosciute o dalla sintassi scorretta; letture e scritture da disco non precedute da esplicita richiesta di autorizzazione; tentativi di collegarsi via rete a siti diversi da quello dal quale l'applicazione stata scaricata; tentativo di cambiare impostazioni che si riflettono su tutto il sistema. In sostanza, un applet Java non pu fare nulla di pericoloso, a meno che non lo abbia apertamente chiesto e sia stato autorizzato dall'utente. Questo approccio in teoria garantisce di scoprire non solo virus e codice pericoloso di tipo gi noto, ma anche quelli nuovi. Il tutto senza mai richiedere di aggiornare un database di "casi noti", come avviene con i normali antivirus. un po' come se Java fosse "vaccinato", all'origine e per sempre, contro il rischio di fungere da veicolo di codice pericoloso. La "tranquillit" che tale approccio garantisce all'utente Internet costituisce un ulteriore, decisivo fattore di vantaggio di Java rispetto ad altre soluzioni per lo sviluppo di applicazioni di rete.

1 Programmazione in rete con i socket

l modello di programmazione messo a disposizione dalle librerie standard di Java per realizzare programmi che comunicano in rete il classico modello a socket universalmente utilizzato per lavorare con il protocollo usato dalle pi importanti applicazioni Internet, il TCP/IP (Transmission Control Protocol/Internet Protocol): importante ricordare sinteticamente di che cosa si tratta, prima ancora di poter presentare il concetto di socket. Quando si parla di TCP/IP ci si riferisce di fatto a due distinti protocolli, cos come due sono gli aspetti affrontati per assicurare la possibilit di comunicare in rete: l'instradamento, di cui si occupa l'IP, e il traspor-

to, che il compito del TCP.

Il protocollo IP
L'Internet Protocol, che si potrebbe a pieno titolo definire la "colla che tiene insieme Internet", si occupa del problema di far arrivare pacchetti di dati da un indirizzo origine a un indirizzo destinazione guidandoli attraverso la rete. Ogni pacchetto contiene un header IP nel quale sono riportate, fra le altre, le informazioni sull'indirizzo IP del mittente e l'indirizzo IP del destinatario. In base a quest'ultima informazione, i centri di smistamento/instradamento (routers) disseminati nella rete possono decidere che strada far fare a un pacchetto per avviarlo alla sua destinazione finale.

Possiamo paragonare il pacchetto di dati IP a un automobilista in viaggio da un luogo A a un luogo B che a ogni incrocio o bivio si ferma per strada e chiede a un passante P "da che parte si va per B?". A seconda di B, il passante (che nell'esempio corrisponde al router) indicher la strada giusta da imboccare per uscire dall'incrocio in direzione corretta. Esistono quattro scenari possibili per la risposta. Supponiamo che l'indirizzo IP di B sia 123.123.123.123. Scenario 1: il passante risponde: " qui, lei arrivato". Questa situazione corrisponde all'arrivo a destinazione del pacchetto: in sostanza P coincide con B, ossia l'host desti-

nazione, non un router intermedio. L'indirizzo IP di P insomma 123.123.123.123. Scenario 2: il passante risponde: "Vada a destra, B il prossimo incrocio". In questo caso P un router che non coincide con la destinazione B, ma sa con precisione e in modo specifico come raggiungerla. Su P stata cio configurata una regola del tipo: "per raggiungere l'indirizzo 123.123.123.123 occorre far uscire i pacchetti per la scheda di rete n. 5". Non detto che P e B siano adiacenti (collegati direttamente); potrebbero esserci altri "incroci" lungo la strada, ma P conosce B in modo specifico e pu quindi fornire un'indicazio ne puntuale.

Java

Scenario 3: la risposta "Non conosco di preciso tutta la strada fino a B, ma sono sicuro che per di qui, a destra". Ovviamente il passante non pu conoscere la posizione precisa di tutte le citt del mondo e tutte le strade del mondo, ma pur senza conoscere la particolare citt B potrebbe essere almeno in grado di dire se, al bivio, essa si trovi "verso destra" o "verso sinistra". Per esempio, se P sa almeno che B una citt di un determinato Paese e sa che per raggiungere quel Paese occorre andare a destra, potr comunque fornire una risposta utile. Questo corrisponde, per un router, a conoscere una regola che si applica a un gruppo di indirizzi; se l'indirizzo di B rientra in quel gruppo di indirizzi, allora il router pu instradare il pacchetto. Per esempio: "per raggiungere tutti gli indirizzi della sottorete 123.123.*.* occorre far uscire i pacchetti per la scheda di rete n. 5". Scenario 4: "Non ho la pi pallida idea di dove si trovi B. Provi ad andare da questa parte e chieda ancora". Se P non ha la minima idea di dove possa trovarsi B, si suppone per che possa almeno dare un suggerimento su dove recarsi per trovare qualcuno in grado di dare indicazioni. il concetto di default gateway: quando mancano regole precise e perfino regole di sottorete, come nei due scenari precedenti, allora deve esistere una "regola di default", per cos dire ...l'ultima spiaggia, che possa comunque dare un'indicazione indirettamente utile per mettere il pacchetto sulla strada giusta. Lo si manda allora verso un host o router, appunto il default gateway, che incaricato di "saperne di pi" e di instradare lui al meglio il pacchetto. buttati via) oppure per guasto o interferenza alle linee di trasmissione o alle schede di rete. Se un pacchetto va perso, necessario che qualcuno abbia modo di accorgersene e se lo faccia rispedire, altrimenti sar compromessa l'integrit del flusso di dati da A a B. Inoltre, a seconda delle condizioni del traffico, i router posizionati presso gli "incroci" potrebbero fornire ai pacchetti indicazioni diverse nel tempo. Di conseguenza, un flusso di pacchetti tutti originati da A e diretti a B potrebbero seguire strade diverse per raggiungere la meta e anche ammettendo che non ne vada perso nessuno potrebbe accadere che le strade abbiano lunghezze diverse, col risultato che pacchetti partiti prima potrebbero arrivare in ritardo rispetto a pacchetti partiti dopo, ma che sono stati messi su una strada pi breve. All'arrivo, quindi, B riceverebbe un flusso disordinato di pacchetti. Se, come quasi sempre vero, l'ordine di arrivo dei dati significativo, risulta necessario garantire che esso non risulti sconvolto da variazioni nelle politiche di routing. Il Transmission Control Protocol (TCP) si occupa proprio di questi problemi (oltre ad altre questioni pi sottili, ma non meno importanti, che non tratteremo in questo articolo) e assicura che i pacchetti arrivino tutti a destinazione e nello stesso ordine con cui sono partiti. In altre parole, si occupa del "trasporto", con garanzia di integrit, dei dati. Se un pacchetto va perso, il Esempio telefonico TCP se ne accorge, grazie a un sistema di numerazione dei pacchetti, e ne richiede la trasmissione. Se invece rileva che l'ordine di arrivo non rispetta la numerazione, accumula i pacchetti arrivati in anticipo finch non vede arrivare quelli che li precedono nella sequenza; pu cos ricostituire l'ordine corretto e lasciar passare i pacchetti in sospeso.

4a lezione
to n.234". Grazie al concetto di porta, un computer dotato di una singola scheda di rete (e quindi di un unico indirizzo IP) pu comunque gestire fino a 65.536 applicazioni distinte, ognuna delle quali utilizza una sua porta per scambiare i dati senza possibilit di ambiguit con le applicazioni "colleghe". Gli indirizzi di origine e destinazione che sono riportati sui pacchetti in viaggio sulla rete in effetti sono costituiti dall'indirizzo IP e da una porta. Alcune porte (per la precisione, quelle il cui numero compreso fra 0 e 1.023) sono riservate per applicazioni o servizi standard: per esempio, per i web server lo standard la porta 80, mentre per FTP la porta 21. Se dalla rete arriva un pacchetto, questo viene esaminato innanzitutto per vedere a quale porta destinato. Cos, se la porta di destinazione la 80 il pacchetto viene mandato al web server; se 21, all'FTP server. Se il numero di porta sconosciuto (nel senso che nessuna applicazione in esecuzione sull'host "abita" a quel numero di porta) il pacchetto viene ignorato e se faceva parte di un tentativo di aprire una connessione TCP, tale tentativo viene respinto.

Indirizzi e porte
Grazie al TCP e al suo lavoro di controllo e "riordino", quindi, la comunicazione attraverso la rete, che nel modello IP appare un poco precaria, diventa affidabile. Da un punto di vista logico come se fra A e B venisse creata una strada apposita, senza incroci n possibilit di strade alternative n rischio di perdere pacchetti; una sorta di "tubo sigillato" all'interno del quale i dati fluiscono da A a B senza disordini e senza perdite. Questo "tubo" detto "connessione TCP" e le sue due estremit, una sull'host A e una sull'host B, si dicono end points. Ognuna di esse identificata da due informazioni: l'indirizzo dell'host e la porta. L'indirizzo ha il familiare aspetto del tipo 123.45.67.89 ed costituito da un numero a 32 bit scomposto, per comodit di rappresentazione, in 4 byte. La porta invece un numero intero positivo a 16 bit, compreso quindi fra 0 e 65.535. Le porte sono paragonabili a sottoindirizzi interni nell'ambito dell'host: se paragoniamo l'indirizzo IP a "Via Verdi, 1", le porte potrebbero corrispondere a "appartamen-

Connessioni
Possiamo paragonare le connessioni alle telefonate. Per poter essere usata, una connessione deve essere innanzitutto creata. Per farlo occorre che una delle due parti provveda ad alzare la cornetta e comporre il numero (l'insieme di prefisso e

Corrispondente concetto TCP/IP in Java

Prefisso internazionale, prefisso teleselettivo Indirizzo IP, rappresentato da una istanza della classe e numero di base del centralino di un'azienda InetAddress Numero dell'interno (Extension) Porta (numero intero) ServerSocket per ricevere le chiamate; Socket per effettuarle Applicazione client o server OutputStream ottenuto dal Socket InputStream ottenuto dal Socket Creare un Socket con l'indirizzo e porta desiderati Eseguire il metodo accept su un ServerSocket Iniziare lo scambio dati sul Socket ritornato dal metodo accept

Il protocollo TCP
Se il protocollo IP, come si visto, un modo per fornire... indicazioni stradali ai pacchetti in viaggio per la rete, che aiutandoli a ogni "incrocio" assicura che non possano "sbagliare strada", va tenuto presente che non vi garanzia che nei tratti di strada fra un incrocio e l'altro un pacchetto non vada "perso". Nelle reti questo pu avvenire per motivi di congestione (se il traffico eccessivo i pacchetti in surplus vengono Presa telefonica a muro (associata a un numero telefonico interno) Apparecchio telefonico connesso alla presa Microfono incorporato nella cornetta Altoparlante incorporato nella cornetta Chiamare un numero Attendere le chiamate davanti a un telefono Rispondere a una chiamata entrante

Java
numero telefonico corrisponde
all'insieme di indirizzo IP e porta IP). Questo ruolo definito client, initiator o caller. L'altra parte, che doveva essere presente e in ascolto, allo squillare del telefono deve alzare il ricevitore: questo ruolo definito server, responder o callee. In seguito la conversazione sar bidirezionale e chiunque dei due potr chiuderla riagganciando il ricevitore. socket. Un socket la rappresentazione logica di una coppia "indirizzo+porta". Pu essere locale, se rappresenta una porta dell'indirizzo locale del computer, oppure remoto, se rappresenta una porta di un altro computer. Una connessione TCP vista da un programma sempre compresa fra due socket: uno sempre locale, l'altro pu essere remoto ( il caso pi comune e corrisponde alla comunicazione fra due computer) oppure essere anch'esso locale (in questo caso la comunicazione fra due diversi processi che girano sullo stesso computer). Rifacendoci all'esempio della telefonata sopra proposto, il socket pu essere di due tipi: client o server. Il client socket quello lato initiator; quello da cui si richiede un servizio (nell'esempio la telefonata; nel caso Internet, l'apertura di una connessione). Il server socket si trova lato responder ed la posizione da cui si offre la disponibilit a erogare un servizio a chi ne faccia richiesta. Non tuttavia il server socket a gestire direttamente la connessione che viene a crearsi: il suo ruolo solo quello di captare la richiesta di apertura di una connessione. Quando il server socket riceve una simile

4a lezione
richiesta mentre si trova nello stato di attesa (accept), la connessione viene accettata e come end point lato server viene installato un Socket fatto nascere apposta. Il server socket si libera cos dell'incombenza di trattare in prima persona il traffico dati e torna in attesa di altre richieste di connessione. Il client socket modellato in Java dalla classe Socket e offre, oltre a molti altri, il metodo connect che corrisponde all'azione dell'initiator di tentare di aprire la connessione. Il server socket invece modellato dalla classe ServerSocket, che offre il gi citato metodo accept.

I socket
Da un punto di vista di programmazione, gli end point delle connessioni TCP sono modellati come delle entit con un ciclo di vita, delle propriet e una semantica ben definite: i

2 Applicazione Java client-server con TCP/IP


il momento di vedere Java alle prese con il TCP/IP. Per farlo scriveremo una prima semplice applicazione con la classica struttura clientserver. In sostanza l'applicazione consiste in una coppia di programmi separati e indipendenti, ma progettati per interagire attraverso uno scambio di dati su una connessione di rete TCP/IP. Si identifica con il termine di server un programma in grado di erogare un determinato servizio a fronte di una richiesta esterna formulata in un modo prestabilito da un altro programma, identificato come client. Un punto fermo che l'interazione tra client e server parte sempre su iniziativa del client. Quest'ultimo apre una connessione di rete verso il server mentre questo in attesa; se l'operazione riesce, il client usa la connessione per trasmettere la richiesta al server. L'arrivo della connessione ha "svegliato" il server dal suo stato di attesa, che reagisce a questo evento ricevendo la richiesta ed eventualmente esaminandola per stabilire esattamente il da farsi. Il passo successivo del server consiste nell'innescare l'elaborazione che porter alla determinazione del risultato. Il processo si conclude quando il server rispedisce al client il risultato dell'elaborazione per mezzo della stessa connessione di rete sul-

la quale il client ha inviato la richiesta. A questo punto, a seconda della logica di funzionamento del server, la connessione pu venire chiusa oppure essere mantenuta aperta, pronta per le successive richieste. Veniamo ora all'esempio, iniziando dall'esame del codice del server (vedi riquadro "Server").

Il server
Il programma molto semplice: vi una sola classe, Server, con un solo metodo, il main. Il primo punto la creazione di un ServerSocket, che rester in ascolto su una porta prestabilita (la 9999) che deve essere nota sia al server sia al client. ServerSocket ss=new ServerSocket(9999); Questa istruzione potrebbe fallire per vari motivi, ragion per cui stata posta in un blocco try..catch. Fra i possibili motivi possiamo citare: assenza della scheda di rete e protocollo TCP/IP non installato, porta 9999 gi occupata, permesso negato per motivi di sicurezza (per esempio dal firewall di Windows XP SP2). A questo proposito occorre ricordare che se sul PC installato il Service Pack 2 di Windows XP, il tentativo del nostro server di riservare a s la porta 9999 e mettersi in ascolto su di essa verr prudenzialmente bloccato e segnalato con un dialog co-

me quello in figura 1. Sar sufficiente rispondere Sblocca per insegnare a Windows a fidarsi di Java tutte le volte successive. Per esaminare ed eventualmente annullare l'effetto dell'impostazione Sblocca sufficiente entrare in Pannello di Controllo e scegliere Centro sicurezza PC, poi Windows Firewall; dovrebbe presentarsi una situazione simile a quella in figura 2 dove presente una riga che segnala che d'ora in poi si far eccezione per Java. Dopo aver creato il ServerSocket il server deve mettersi 2

in attesa che entrino delle connessioni originate dai client. Socket s=ss.accept();

Java
Il metodo accept, applicato all'oggetto ServerSocket, ha l'effetto di attivare l'ascolto sulla porta alla quale stato agganciato il ServerSocket in fase di creazione. L'esecuzione non procede e resta bloccata in attesa su questa istruzione fino a quando non sar rilevata una connessione entrante sulla porta 9999, qualunque sia la provenienza. All'arrivo di tale connessione verr creato automaticamente un Socket (semplice) che avr la funzione di "terminale" locale, lato server, della connessione stessa. Tale Socket viene ritornato come risultato della funzione accept. Una volta ottenuto il socket locale, che da un punto di vista logico rappresenta un'estremit di un tubo che ci collega direttamente al client, dobbiamo organizzarci per poter leggere e scrivere da/su questo Socket. Questo si fa servendosi di due metodi di questa classe: getInputStream(), che ritorna un InputStream da cui potremo leggere i dati in arrivo dal socket, e getOutputStream(), che ritorna un OutputStream che spedir via socket i dati che scriveremo su di esso. Come sappiamo dalla scorsa puntata, gli Stream sono adatti a scambiare dati "grezzi" a byte; poich per nel nostro esempio il client e il server si parlano con messaggi di testo, dovremo usare delle classi Reader e Writer per manipolarli con facilit. Per la ricezione scegliamo di costruire un BufferedReader per poter leggere comodamente per righe i messaggi in arrivo dal client. Ma i costruttori della classe BufferedReader richiedono come parametro un Reader o una sua sottoclasse, non un InputStream ; per poterli comporre, quindi, ci occorre interporre un "adattatore", rappresentato nell'esempio da una istanza di classe InputStreamReader. BufferedReader br=new BufferedReader (new InputStreamReader (s.getInputStream())); Per la trasmissione creiamo un PrintWriter per poter mandare testo per righe. Fortunatamente il PrintWriter pu essere creato direttamente su un OutputStream: PrintWriter pw=new PrintWriter(s.getOutputStream()); Ora che tutto pronto per gestire dati di testo sul socket possiamo partire con la prima operazione: leggere il messaggio (di una riga) in arrivo dal client. String a=br.readLine(); Quando viene incontrata questa istruzione, l'esecuzione si blocca fino a quando non risultano disponibili per la lettura abbastanza dati per riempire una riga di testo (fino al newline). poich il client invia al server la sua richiesta subito dopo aver aperto la connessione che ci ha "risvegliato" dalla accept, l'attesa di questi dati brevissima e in men che non si dica la variabile "a" ricever il valore ricevuto dal client. A questo punto la richiesta del client va esaminata per stabilire il da farsi ed elaborata poi nel modo richiesto. L'esame del messaggio entrante necessario in tutti i casi in cui il server sia in grado di svolgere varie funzioni diverse a seconda della richiesta ricevuta; ma poich il nostro particolare server svolge sempre lo stesso compito, ossia volgere in maiuscolo la stringa ricevuta, non occorre nessuna analisi e si pu procedere senz'altro con l'elaborazione, che si realizza con la funzione toUpperCase della classe String; in sostanza chiamiamo questo metodo direttamente sulla variabile a che contiene la stringa da volgere in maiuscolo. a=a.toUpperCase(); A questo punto il risultato pronto e pu essere spedito al client semplicemente "stampandolo" sul PrintWriter che abbiamo connesso con l'OutputStream che conduce al client: pw.println(a); Questa istruzione pu non garantire che i dati escano subito. Il sistema, per motivi di efficienza, pu avere la politica di raggruppare i dati da spedire in rete e fare un'unica spedizione "grossa" (non appena si raggiunge un livello di "accumulo" sufficiente) anzich molte spedizioni "piccole", una ogni volta che chiediamo di spedire pochi dati. Noi per sappiamo di non avere altri dati da spedire e vogliamo che questi partano subito. La cosa da fare allora forzare lo svuotamento della... "sala d'attesa" utilizzando sul PrintWriter la funzione flush(). pw.flush(); A questo punto il nostro compito terminato e possiamo chiudere sia il PrintWriter in uscita sia il BufferedReader in entrata, prima di terminare il programma. pw.close(); br.close(); Da notare infine che nel blocco catch abbiamo posto una istruzione che, qualora si verifichi un'eccezione, ci dar informazioni su quello che successo visualizzando sulla

4a lezione
console opportuni messaggi: catch(Exception e) { e.printStackTrace(); } vedremo pi avanti alcuni esempi del risultato prodotto da questa istruzione.

Il client
Vediamo ora come si svolgono le cose dal punto di vista del client. Anche qui la struttura del programma semplissima:

Riquadro "Server"
// per le classi Socket e ServerSocket import java.net.*; // per le classi di i/o import java.io.*; // Programma che ascolta sulla porta 9999, // riceve una stringa e risponde con la stessa // stringa trasformata in maiuscolo public class Server { // il punto di ingresso del programma public static void main(String [] args) { // il blocco try-catch cattura eventuali errori try { // Inizializza il socket verso il server // e implicitamente apre la connessione ServerSocket ss=new ServerSocket(9999); // Attende messaggi dal client System.out.println("In attesa di messaggi"); Socket s=ss.accept(); System.out.println("Rilevata connessione entrante"); // Predispone un PrintWriter per poter inviare // agevolmente stringhe sul socket PrintWriter pw=new PrintWriter(s.getOutputStream()); // Costruisce un BufferedReader per leggere // a righe il flusso di testo ricevuto dal server BufferedReader br=new BufferedReader (new InputStreamReader(s.getInputStream())); // Legge la stringa inviata dal client String a=br.readLine(); // Visualizza il messaggio ricevuto System.out.println("Dal client: "+a); // Trasforma la stringa in maiuscolo a=a.toUpperCase(); // Rimanda al client la stringa trasformata pw.println(a); // Forza la trasmissione immediata pw.flush(); System.out.println("Risultato inviato"); // Chiude le connessioni col server pw.close(); br.close(); } catch(Exception e) { e.printStackTrace(); } } }

Java
Se l'apertura del socket (con implicita connessione al server) riuscita, possiamo costruire quello che ci servir per lo scambio di messaggi di testo con il server. Anche in questo caso gli Input/Output Stream messi a disposizione dal socket non ci vanno bene, quindi provvediamo a "racchiuderli" (in gergo si usa il verbo inglese to wrap, quasi stessimo "incartandoli") rispettivamente in un BufferedReader e in un PrintWriter. Come al solito, per il BufferedReader occorre l'interposizione di un InputStreamReader. Le istruzioni che costruiscono le due strutture che ci servono sono queste: PrintWriter pw=new PrintWriter(s.getOutputStream()); BufferedReader br=new BufferedReader (new InputStreamReader (s.getInputStream())); Ora che tutto pronto per la comunicazione con il server (che nel frattempo, per inciso, si risvegliato dalla accept in cui era in attesa) possiamo spedire la nostra richiesta: una stringa di testo che chiediamo di trasformare in maiuscolo. pw.println("Testo da trasformare"); Come al solito, per assicurarci che il messaggio parta subito forziamo la spedizione immediata con una flush(): pw.flush(); L'ultima operazione da fare predisporsi per ricevere il risultato che il server ci spedir. L'esecuzione del client si soffermer su questa istruzione fino a quando dal socket non arriveranno dati sufficienti per riempire una riga di testo (newline compreso). Allora tale stringa verr ricevuta e assegnata alla variabile stringa "a". String a=br.readLine();

4a lezione
pio: c:\java), dopodich diamo il comando java Server Il server entrer in esecuzione e si metter in attesa, come segnalato da un apposito messaggio (figura 3).

Riquadro "Client"
// per la classe Socket import java.net.*; // per le classi di i/o import java.io.*; // programma che si collega alla porta 9999 del // proprio host, invia una stringa, attende una // stringa in risposta e la visualizza. public class Client { // il punto di ingresso del programma public static void main(String [] args) { // il blocco try-catch cattura eventuali errori try { // Inizializza il socket verso il server // e implicitamente apre la connessione Socket s=new Socket("localhost",9999); // Predispone un PrintWriter per poter inviare // agevolmente stringhe sul socket PrintWriter pw=new PrintWriter(s.getOutputStream()); // Costruisce un BufferedReader per leggere // a righe il flusso di testo ricevuto dal server BufferedReader br=new BufferedReader (new InputStreamReader(s.getInputStream())); // Manda al server una stringa da trasformare pw.println("Testo da trasformare"); System.out.println("Richiesta inviata"); // Forza l'emissione immediata del messaggio pw.flush(); // Legge il messaggio di risposta del server String a=br.readLine(); // Visualizza il messaggio ricevuto System.out.println("Risultato dal server: "+a); // Chiude le connessioni col server pw.close(); br.close(); } catch(Exception e) { e.printStackTrace(); } } }

Ora occupiamoci dell'altro lato. Nella seconda finestra, dopo esserci portati nella directory c:\java, lanciamo il client con java Client Questo d il via a tutto il sistema nel suo complesso, in quanto appena partito il client si collega al server, che senza indugio elabora la richiesta appena ricevuta, rispedisce al client il risultato e termina. Anche il client, appena ricevuto il risultato, lo visualizza e termina. In pochi istanti il "duetto" giunge all'epilogo. Possiamo vedere che cosa succede lato client in figura 4 e lato server in figura 5.

una sola classe (Client) e un so-

lo metodo (l'immancabile main). La prima cosa da fare creare un Socket collegato alla porta e indirizzo dove ci aspettiamo che sia in ascolto il Server: porta 9999 e host locale. In TCP/IP il nome universale per riferirsi all'host su cui si trova il programma in esecuzione "localhost", corrispondente allo speciale indirizzo 127.0.0.1, che viene anche chiamato loopback address (ad indicare che le connessioni aperte verso questo indirizzo saranno come tanti "autoanelli" che si richiudono sul client). La creazione del Socket ver-

so il nostro server sar quindi espressa in questo modo: Socket s=new Socket("localhost",9999); Come gi detto per il server, questa ed altre operazioni che hanno a che fare con la rete possono fallire per molti possibili motivi, corrispondenti allo scatenarsi di determinate eccezioni. Anche nel caso del client, quindi, per catturare ed eventualmente trattare queste eccezioni circondiamo il codice "a rischio" con una struttura try-catch, liberandoci cos dal pensiero di dover controllare "come andata" dopo ogni istruzione: ci penseremo nel blocco catch.

Il sistema in funzione
Se entrambi i programmi sono stati gi scritti e compilati correttamente (la compilazione d luogo a due files di bytecode, rispettivamente Client. class e Server.class) possiamo provare a lanciarli. Per fare questo apriamo due finestre Prompt dei comandi(Start/Programmi/Accessori/Prompt dei comandi) e nella prima facciamo partire il server. Usando il comando cd, portiamoci nella directory dove si trovano i class file dell'applicazione Server (per esem-

Sequence diagrams
Anche se i messaggi stampati dalle due applicazioni forniscono indicazioni utili per capire che cosa successo e che cosa "si sono dette", questa tecnica non adatta per programmi di grandi dimensioni che effettuano molteplici scambi di dati, anche "accavallati" tra loro. Infatti, se gli scambi di dati sono numerosi, questo

Java
elenco di messaggi risulta talpresentate da frecce orientate nel senso del traffico di dati. La loro leggera inclinazione verso il basso, nel senso del tempo crescente, sta a rappresentare il fatto che per questi scambi di dati richiesto un tempo che, in caso di congestione o malfunzionamento della rete, potrebbe anche non essere trascurabile: da qualche microsecondo (nel caso di connessioni loopback) a qualche millisecondo (nel caso di reti locali con buone prestazioni) a qualche secondo (per grandi distanze e/o reti affollate). Anche se nel nostro caso ci irrilevante, in programmi pi complessi potrebbe essere significativo, in quanto, in attesa che la comunicazione si completi e magari torni indietro un qualche risultato, un client potrebbe svolgere qualche sua elaborazione interna per sfruttare il tempo d'attesa. Per esempio, mentre un web browser attende che arrivino i dati della pagina da visualizza-

4a lezione
6

mente lungo da diventare veramente difficile e scomodo da seguire. Inoltre, in presenza di pi "colloqui paralleli" su temi diversi, probabile che l'esame dei messaggi, per tentare di seguire uno in particolare di questi "colloqui", risulti molto difficile. Per queste ragioni, per la descrizione dell'interazione fra client e server si fa uso di notazioni alternative. Una particolarmente espressiva e chiara, e per questo molto usata, riferita al nostro caso, mostrata in figura 6. In questa rappresentazione il tempo scorre dall'alto in basso. La colonna di sinistra rappresenta la storia di tutto ci che accade nel client e tutti gli scambi di dati che attraversano i suoi confini in entrata o in uscita. Discorso analogo per la colonna di destra, che narra gli eventi riguardanti il server. Le comunicazioni intercorse tra i due processi sono rap-

re, si occupa di gestire l'animazione che indica attivit di rete (il logo del browser in alto a destra), o aggiorna la progress bar in basso, o altro ancora. Questo tipo di diagramma, da sempre usato come strumento di progetto nel mondo delle telecomunicazioni per dare rappresentazione di ci che

accade in un sistema composto di pi programmi distinti in esecuzione magari su nodi diversi (nel qual caso si parla di "sistema distribuito"), stato ripreso, canonizzato e standardizzato in tempi relativamente recenti con il nome di Sequence Diagram nell'ambito dell'Unified Modeling Language (UML).

3 Un Web browser scritto in Java

ome "summa" di questa lezione e dell'intero ciclo vi proponiamo un esempio di applicazione in cui impiegheremo, tutte insieme, molte delle tecniche viste finora. L'applicazione una implementazione di un semplice web browser dotato di funzione history (e relativo pulsante Back), pulsante home, selezione diretta delle pagine. L'intero programma, scritto

in Java, lungo appena 139 linee: un risultato inimmaginabile fino a solo pochi anni fa, usando linguaggi e librerie pi primitivi. Un'ulteriore riconferma della particolare idoneit di Java per la rapida realizzazione di applicazioni in rete con interfaccia grafica (Figura 7). Vediamo ora il funzionamento delle parti pi importanti del programma. Il listato com7

pleto, WebBrowser.java, si trova sul CD di PC Open.

Le dichiarazioni e il main
La classe principale, WebBrowser, dichiarata come una sottoclasse di JFrame, alla quale aggiunge i propri metodi e attributi di utilizzo applicativo. Creando un'istanza di WebBrowser nascer quindi implicitamente una finestra top level: non sar necessario crearla appositamente. WebBrowser dichiara di implementare due interfacce che sono richieste per poter gestire gli eventi salienti del nostro browser: HyperLinkListener , che serve per poter trattare i clic del mouse sui link presenti sulla pagina Web, e ActionListener, necessaria per ricevere i clic del mouse sui pulsanti Back e Home e l'evento di pressione del tasto Invio (Return) sul campo di immissione della URL. public class WebBrowser extends JFrame implements HyperlinkListener, ActionListener

Le variabili che dichiariamo riguardano i quattro widget impiegati (due pulsanti, un campo di testo e l'area di visualizzazione HTML), la URL da usare come pagina iniziale (a cui torneremo ogni volta che l'utente premer il tasto Home) e soprattutto la struttura dati usata per memorizzare la histor y . Si tratta della classe Stack, contenuta nel package java.util. Si tratta di una leggera variante della classe Vector (che concettualmente simile a un array dotato per della capacit di crescere di dimensioni a run time, quando vengono inseriti nuovi elementi). La classe Stack rappresenta una pila ordinata di valori. La pila caricata e scaricata dall'alto: per questa ragione, l'ultimo entrato il primo a uscire (Last In First Out, LIFO). Per caricare un valore in cima allo stack si usa il metodo push; per estrarlo si usa il metodo pop. Questi due metodi sono aggiunti dalla classe Stack ai metodi gi presenti nella superclasse, Vector. La history di un browser si comporta esattamente come

Java
uno stack. Infatti, ogni volta che seguiamo un link da una pagina web, come se scrivessimo su un foglietto l'indirizzo della pagina appena lasciata e lo depositassimo su una pila di foglietti che rappresentano le pagine gi visitate. Quando premiamo il pulsante Back per tornare all'ultima pagina visitata, concettualmente quello che succede che viene pescato il foglietto in cima alla pila, viene letto l'indirizzo ivi trascritto e viene aperta la relativa pagina Web. Dopodich, per semplificare il concetto, possiamo assumere che il foglietto venga scartato (in realt nei browser reali esiste anche un pulsante Forward, quindi il foglietto viene conservato in un'altra struttura dati che rappresenta una sorta di "history al contrario" e viene usata dal tasto Forward). Premendo ancora Back viene ripetuto lo stesso procedimento, e cos via fino a quando sullo stack non rimane pi alcun "foglietto". Non essendo possibile risalire oltre la history, in questa situazione il pulsante Back va disattivato, come vedremo pi avanti. JButton jbHome; // pulsante ritorno pagina iniziale JButton jbBack; // pulsante ritorno pag. precedente (history) JTextField jtfURL; // campo per immettere URL desiderata JEditorPane jepHTML; // area di visualizzazione della pagina Web String home; // conterr la URL della pagina iniziale Stack history=new Stack(); // contiene la history Il main dell'applicazione ridotto all'osso. Tutto quello che viene fatto creare una istanza della classe WebBrowser. Il lavoro viene fatto tutto dal costruttore di quella classe. Ricordiamo che, essendo WebBrowser sottoclasse di JFrame, automaticamente nascer anche la finestra principale dell'applicazione. Come parametro al costruttore occorre fornire la pagina iniziale desiderata: scegliamo un sito che i nostri lettori probabilmente conoscono molto bene!.. public static void main(String[] args) { new WebBrowser ("http://www.pcopen.it/"); }

4a lezione
WebBrowser: il costruttore
Vediamo solo i punti salienti del costruttore; tralasceremo ovviamente gli aspetti che non presentano novit rispetto a quanto visto finora. super("Web Browser"); La prima istruzione del costruttore la chiamata al costruttore della superclasse (JFrame), al quale viene passato il parametro da esso richiesto (la stringa da visualizzare nella barra del titolo della finestra). La chiamata al costruttore della superclasse, se richiesta, pu essere fatta solo da un costruttore di classe derivata e deve essere sempre la primissima istruzione di questo. this.home = home; Il parametro home ha lo stesso nome di un attributo della classe WebBrowser. Per assegnare il valore del parametro home all'attributo home, per, non possiamo scrivere un'istruzione del tipo "home=home": sarebbe ambigua. Per specificare che a sinistra del simbolo "=" intendiamo riferirci all'attributo home di questa classe, e in particolare di questa istanza, usiamo il qualificatore this, che fornisce un riferimento a "questo" oggetto (siamo in un costruttore quindi si tratta appunto dell'oggetto che stiamo inizializzando) addWindowListener (new WindowAdapter() {public void windowClosing (WindowEvent e){System.exit(0);}}); Questa istruzione imposta un Window Listener che gestir l'evento di "clic sul quadratino di chiusura" facendo terminare l'applicazione. jbBack.setEnabled(false); // inizialmente disabilitato Durante la costruzione dell'interfaccia grafica necessario impostare lo stato iniziale del pulsante Back a "disattivato": non esiste infatti alcuna history all'avvio del browser. Si verr invece a creare in seguito, quando l'utente inizier a seguire i link della pagina. Allora un'apposita istruzione provveder a riattivare (o a ri-disattivare) il pulsante, secondo necessit. jepHTML = new JEditorPane(home); jepHTML.setEditable(false); jepHTML.addHyperlinkListener(this); JScrollPane scrollPane =

Listato WebBrowser.java,
import javax.swing.*; // per widget Swing import javax.swing.event.*; // per eventi Swing import java.awt.*; // per BorderLayout import java.awt.event.*; // per classi gestione eventi import java.net.*; // per la classe URL import java.io.*; // per classi I/O import java.util.*; // per classe Stack public class WebBrowser extends JFrame implements HyperlinkListener, ActionListener { JButton jbHome; // pulsante ritorno pagina iniziale JButton jbBack; // pulsante ritorno pag. precedente (history) JTextField jtfURL; // campo per immettere URL desiderata JEditorPane jepHTML; // area di visualizzazione della pagina Web String home; // conterra' la URL della pagina iniziale Stack history=new Stack(); // contiene la history public static void main(String[] args) { new WebBrowser("http://www.pcopen.it/"); } public WebBrowser(String home) { super("Web Browser"); // inizializza JFrame e imposta titolo this.home = home; // imposta pagina iniziale // gestione chiusura finestra (termina applicazione) addWindowListener(new WindowAdapter() {public void windowClosing(WindowEvent e){System.exit(0);}}); JPanel topPanel = new JPanel(); // area in alto jbBack = new JButton("Back"); // crea pulsante history jbBack.setEnabled(false); // inizialmente disabilitato jbBack.addActionListener(this); // gestiremo noi i suoi eventi jtfURL = new JTextField(50); // crea campo per immiss. URL jtfURL.setText(home); // inizializza campo con URL home page jtfURL.addActionListener(this); // gestiremo noi i suoi eventi jbHome = new JButton("Home"); // crea pulsante Home jbHome.addActionListener(this); // gestiremo noi i suoi eventi topPanel.add(jbBack); // inserisce pulsante History topPanel.add(jtfURL); // inserisce campo per immiss. URL topPanel.add(jbHome); // inserisce pulsante Home // inserisce in alto il pannello con pulsanti e campo URL getContentPane().add(topPanel, BorderLayout.NORTH); try { // crea HTML display inizializzato su pagina home jepHTML = new JEditorPane(home); jepHTML.setEditable(false); // impedisce editing su pagina // gestiremo noi l'evento di mouseclick su un hyperlink jepHTML.addHyperlinkListener(this); // Installa scrollbars per gestire pagine grandi JScrollPane scrollPane = new JScrollPane(jepHTML); // Inserisce HTML display in area principale finestra getContentPane().add(scrollPane, BorderLayout.CENTER); } catch(Exception e) { // segnala inizializzazione HTML display fallita // (unica causa possibile: impossibile aprire URL) JOptionPane.showMessageDialog (this, "Impossibile aprire la pagina iniziale!", "Errore", JOptionPane.ERROR_MESSAGE); } // Calcola dimensione disponibile schermo Dimension schermo = getToolkit().getScreenSize(); // imposta posizione iniziale nell'angolo alto-sinistra segue a pagina successiva

Java
// e dimensione adattata allo schermo setBounds(0,0,schermo.width-50,schermo.height-200); // visualizza finestra principale browser setVisible(true); } // Metodo di gestione eventi public void actionPerformed(ActionEvent event) { String url; if (event.getSource() == jtfURL) // evento=immessa URL { // ricorda pagina attuale nella history prima di cambiare history.push(jepHTML.getPage().toString()); url = jtfURL.getText(); // questa sara' la nuova pagina } else if(event.getSource() == jbBack) // evento=click su Back { // "pesca" dalla history la URL per la nuova pagina url=(String)history.pop(); } else // evento=click su Home { // ricorda pagina attuale nella history prima di cambiare history.push(jepHTML.getPage().toString()); url = home; // la nuova pagina sara' quella iniziale } // imposta nuova pagina try { jepHTML.setPage(new URL(url)); // Imposta nuova URL jtfURL.setText(url); // Aggiorna URL visualizzata in alto } catch(Exception e) { // Errore in apertura URL: segnalazione JOptionPane.showMessageDialog (this, "Impossibile aprire "+url+": "+e, "Errore", JOptionPane.ERROR_MESSAGE); } // Aggiorna lo stato di abilitazione del pulsante Back // in base a presenza e dimensione history jbBack.setEnabled(history.size()>=1); } // Metodo per la gestione di evento click su hyperlink public void hyperlinkUpdate(HyperlinkEvent event) { if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { try { // ricorda pagina attuale nella history prima di cambiare history.push(jepHTML.getPage().toString()); // passa alla nuova pagina jepHTML.setPage(event.getURL()); // aggiorna URL visualizzata in alto jtfURL.setText(event.getURL().toExternalForm()); } catch(Exception e) { // Errore in apertura URL: segnalazione JOptionPane.showMessageDialog (this, "Impossibile aprire "+event.getURL()+": "+e, "Errore", JOptionPane.ERROR_MESSAGE); } } // Aggiorna lo stato di abilitazione del pulsante Back // in base a presenza e dimensione history jbBack.setEnabled(history.size()>=1); } } new JScrollPane(jepHTML); Queste quattro istruzioni sono la parte pi importante del codice di inizializzazione del nostro browser. La prima crea un visualizzatore multifunzionale adatto anche per HTML ( JEditorPane ), preimpostato per mostrare la pagina iniziale la cui URL contenuta nella variabile home: sar il JEditorPane a risolvere l'indirizzo, a connettersi al server, a scaricare testo e immagini, a disporre gli elementi della pagina e a visualizzarla sul video. Tutto in una singola riga di codice, grazie alla notevole potenza delle librerie GUI/Internet di Java. La seconda riga stabilisce che non sia possibile all'utente modificare il testo contenuto nel JEditorPane (questo perch tale classe, come se non bastasse quanto gi citato, fornisce anche un limitato supporto all'editing WYSIWIG dei documenti visualizzati!): in un browser questa funzione non occorre e rischia solo di generare confusione nell'utente. Disattiviamola. La terza riga registra "questa" (this) istanza di WebBrowser come gestore di eventi inerenti l'attivazione di hyperlinks. poich WebBrowser dichiara di implementare l'interfaccia HyperlinkListener, ammissibile la sua "candidatura" a gestore di tali eventi. Al verificarsi di uno di essi, il sistema chiamer il metodo hyperlinkUpdate, che vedremo fra poco. Da ultima, la quarta istruzione imposta delle scrollbar che, apparendo automaticamente solo se necessario, consentiranno di muoversi su documenti pi grandi dello spazio disponibile a video. JOptionPane.showMessageDialog (this, "Impossibile aprire la pagina iniziale!", "Errore", JOptionPane.ERROR_ MESSAGE); Nel caso in cui la URL fornita come pagina iniziale non risulti sintatticamente valida o il sito o la rete abbiano problemi, l'inizializzazione si arresterebbe. L'applicativo avvisa

4a lezione

quindi l'utente con un messaggio mediante la comoda funzione showMessageDialog della classe JOptionPane, mostrata sopra. I parametri sono, nell'ordine, il Frame a cui deve riferirsi il dialog, il messaggio da visualizzare, il titolo del dialog e il tipo di messaggio (quest'ultimo determina l'icona da mostrare a sinistra del dialog). Codice del tutto simile a questo viene usato in altri punti del programma per errori simili ma verificatisi durante la navigazione anzich in fase di inizializzazione; in ogni caso i messaggi che appaiono sono simili a quello di figura 8. Dimension schermo = getToolkit().getScreenSize(); setBounds(0,0,schermo.width50,schermo.height-200); setVisible(true); Queste istruzioni servono per impostare dimensioni e posizione iniziali della finestra di WebBrowser, in modo tale che occupi uno spazio proporzionato allo spazio massimo disponibile sul display. La terza istruzione provvede poi a farla apparire a video.

Il metodo actionPerformed
Come gi detto, questo metodo la sede in cui vengono gestiti gli eventi relativi ai due pulsanti Back e Home e al campo per la URL. Se l'evento stato generato dalla pressione di Return nel campo URL, per prima cosa viene salvata sulla history (con push) la pagina attuale e poi viene salvato nella variabile url l'indirizzo della prossima pagina, preso dal campo di testo: if (event.getSource() == jtfURL) { history.push(jepHTML.getPage(). toString()); url = jtfURL.getText(); } Se invece stato premuto Back, non occorre salvare nulla sulla history, anzi l'indirizzo della prossima pagina deve essere prelevato dalla cima dello stack della history, quindi usando il metodo pop: 8

Java
else if(event.getSource() == jbBack) // evento=click su Back { url=(String)history.pop(); } Se infine stato premuto Home, le azioni sono simili a quelle relative alla immissione di un nuovo indirizzo nella finestrella URL: unica differenza, l'indirizzo per la prossima pagina viene preso dalla variabile interna home, che abbiamo inizializzato nel costruttore. else // evento=click su Home { history.push(jepHTML.getPage(). toString()); url = home; } Una volta stabilito (nella variabile url) quale sar la prossima pagina possiamo passare a impostarla sul JEditorPane. poich questa operazione implica un tentativo di connessione al server che potrebbe anche non riuscire, necessario prepararsi a trattare le relative eccezioni. Per questo motivo il codice racchiuso in un blocco try. Si noti anche l'uso della classe URL: il metodo setPage di JEditorPane non accetta un indirizzo espresso come stringa (la nostra variabile url), ma pretende che tale indirizzo sia rappresentato come un oggetto di tipo URL. Fortunatamente la classe URL dispone di un comodo costruttore che invece accetta un indirizzo scritto come una stringa, per cui... ce la caviamo a buon mercato, creando al volo un oggetto di classe URL e passandolo a setPage: try { jepHTML.setPage(new URL(url)); jtfURL.setText(url); } Dopo aver cambiato pagina dobbiamo chiederci se nella history ci siano o no pagine verso cui tornare, per stabilire se il pulsante Back debba essere abilitato o meno. Facciamo questo con una singola istruzione: jbBack.setEnabled(history.size() >=1); Il metodo setEnabled richiede un parametro di tipo boolean. Java permette di passare, come valore boolean, il risultato di una espressione logica come quella qui mostrata. Il concetto che se la history contiene almeno una pagina, allora la condizione vera, quindi abilitiamo il pulsante Back , altrimenti ( histor y

4a lezione

vuota) lo disabilitiamo. Avremmo potuto ottenere lo stesso effetto precalcolando una variabile intermedia di tipo boolean, ma questa notazione pi compatta ed evita di dichiarare e usare variabili inutili.

HyperlinkEvent.EventType.ACTIVAT ED) Le operazioni che seguono sono molto simili a quelle viste nel metodo actionPerformed: l'unica differenza la fonte da cui viene tratta la URL da usare; non pi dal campo jtfURL o dalla variabile home e nemmeno dalla history, ma direttamente dall'evento, che "porta con s" questa informazione. Possiamo estrarla usando il metodo getURL(): jepHTML.setPage(event.getURL()); Il risultato finale dei nostri sforzi visibile in Figura 9.

Il metodo hyperlinkUpdate
Questo metodo pu trattare vari possibili tipi di eventi inerenti gli hyperlink, ma a noi interessa solo il caso di "clic sul link", ossia attivazione del link. Occorre quindi un test che ci assicuri di trovarci in questo caso: if (event.getEventType() ==

4 Conclusioni

n questo primo ciclo di quattro puntate abbiamo presentato i temi fondamentali della programmazione in generale (dal concetto di algoritmo ai costrutti sintattici, dalla scelta dei nomi per gli identificatori al trattamento degli errori, dai tipi di dato alla ricorsione) per poi passare ad argomenti pi specifici come la programmazione a oggetti, quella a eventi, la realizzazione di una GUI, tecniche e modelli di I/O su files e stream e infine la programmazione di rete. Naturalmente, dato il rapporto tra la vastit e il numero di argomenti e lo spazio disponibile, non stato possibile presentare una trattazione completa e approfondita, ma ci siamo attenuti alla presenta-

zione dei concetti cardine e degli esempi concreti utili a chiarirli. Tuttavia, probabilmente, alcune conclusioni su Java risultano ormai chiare. Come il lettore avr notato dagli esempi Java che hanno accompagnato i vari argomenti, questo linguaggio (e soprattutto le sue librerie) offre potenti mezzi per programmare con relativa facilit applicazioni che in passato avrebbero richiesto uno sforzo veramente notevole. La "trasversalit" di alcuni temi, come la grafica, il trattamento delle eccezioni, l'I/O con stream, che sono presenti in applicazioni di ogni genere, in Java ben recepita dalle classi di libreria, per il modo "integrato" in cui sono

progettate: per riferirsi a un esempio visto in questa puntata, basti pensare al JEditorPane, che affronta e "banalizza" tutti i e quattro i temi citati offrendone al programmatore una sintesi tanto ricca quanto facile da usare. Per di pi le applicazioni realizzate hanno dimensioni molto ridotte, possono girare senza modifiche su moltissimi tipi diversi di macchine, godono di una certa protezione contro le minacce provenienti dalla rete, possono essere dotate di meccanismi di trattamento degli errori, sono "automaticamente" predisposte per essere internazionalizzate e molto altro ancora; e il tutto non ha richiesto il pagamento di alcuna licenza.

Sicuramente quindi si tratta di un ausilio prezioso che ha sicuramente contribuito in modo enorme ad accelerare lo sviluppo di applicazioni potenti e user-friendly, ad abbassare tempi e costi di sviluppo e a rivitalizzare, sempre che ce ne fosse stato bisogno, il mondo dello sviluppo software. Ma largomento ovviamente non si conclude qui. Prossimamente su PC Open continueremo a trattare ed approfondire, magari con temi monografici saldati alle basi presentate in questo corso, l'argomento della programmazione in generale e della programmazione mediante Java in particolare. Aspettiamo ora i vostri riscontri.