Avvia il
Transcript
Avvia il
Analisi di un flusso di input con Scanner In questa sezione verranno studiati flussi di dati originati da sorgenti di input e diretti al PC. Gli strumenti che il linguaggio Java mette a disposizione per l’analisi dei contenuti dei flussi sono sufficientemente astratti da consentire di affrontare il problema in modo generale, concentrando l’attenzione sui dati, piuttosto che sulla natura specifica delle sorgenti che posson oessere molto diverse: tastiera, file, connessioni internet, eccetera. In base alla modialità con cui vengono interpretati i dati del flusso questo viene detto binario o di caratteri. In questa sezione si affronterà solo il flusso di caratteri, che può essere ben descritto, ricorrendo ad una semplificazione, come la lettura di un documento testuale, carattere dopo carattere, dall’inizio alla fine. Il più agevole strumento messo a disposizione da Java per l’analisi dei flussi di caratteri in input è lo Scanner. Questo oggetto, attraverso una serie di metodi, consente di leggere una sorgente che invia dati formattati traducendoli nei loro equivalenti binari. Per comprendere il funzionamento dello Scanner e chiarire in seguito cosa s’intenda per rappresentazione binaria di un dato bisogna innazitutto introdurre alcune definizioni. Il primo concetto è quello di delimitazione, questa non è altro che un particolare carattere o sequenza di caratteri che permette di isolare all’interno di un testo dei segmenti detti token. Per esemplificare, se viene definito come delimitatore il carattere spazio " ", allora il testo “Usando un pessimo calco dall’inglese, gli informatici direbbero che questa frase sta per essere tokenizzata” potrà essere frazionato nei seguenti 15 token: “Usando”, “un”, “pessimo”, “calco”, “dall’inglese,”, “gli, “informatici”, “direbbero”, “che”, “questa”, “frase”, “sta”, “per”, “essere”, “tokenizzata” Indipendentemente dalla sorgente di dati a cui viene collegato uno Scanner, i delimitatori che definiscono i token possono essere ampiamente personalizzati, ma il set di default è costituito dai caratteri speciali: line-feed o new-line (’∖n’), carriage-return (’∖r’), tabulation (’∖t’) e dal carattere spazio (’ ’). Una volta che un token del flusso viene isolato, questo può essere processato da Scanner che, attraverso opportuni metodi, può tradurlo in un suo equivalente binario. Ad esempio, a seconda del particolare metodo adoperato, il token “123” potrà essere tradotto nell’equivalente stringa "123" oppure nel numero 123 di tipo int o ancora nel numero 123 di tipo double. Il formato numerico con cui vengono rappresentati date, numeri, valute varia da paese a paese, e la localizzazione del JRE tiene conto di questo aspetto. Per fare un esempio, nella localizzazione italiana dal JRE il token “123.5” non è riconosciuto come numero float o double. Infatti, il nostro formato dei numeri a virgola mobile prevede come separatore decimale la virgola invece del punto, che è utilizzato in altri paesi tra cui gli USA1 . Sia chiaro che quanto detto non ha nulla a che fare con i letterali numerici del codice sorgente, il cui formato è indipendente 1 È comunque possibile sia cambiare la localizzazione della JRE sia personalizzare ampiamente i vari aspetti della formattazione di numeri, date, eccetera. 1 dalle particolari localizzazioni del JRE ed è definito in accordo alla grammatica del linguaggio Java. Lo Scanner è un particolare oggetto di Java che può essere collegato ad una certa sorgente di dati. Attraverso i suoi metodi, Scanner può accedere in modo sequenziale al flusso dei dati generati dalla sorgente. La modalità con cui i dati vengono elaborati è la seguente: i caratteri, man mano che vengono ricevuti, vengono accodati in una coda di input e quando i ntesta alla coda si forma un token Scanner può processarlo e tradurlo in un suo equivalente binario. L’elaborazione dei dati termina necessariamente quando il flusso di input viene chiuso. Nel caso in cui la sorgente sia una stringa o un file, la chiusura del flusso di input avviene dopo la rimozione dalla coda di input dell’ultimo token. Se invece la sorgente è la tastiera (detta anche standart input), una connessione internet o una qualunque altra sorgente che invia dati con continuità, il flusso dei dati, in linea di principio, potrebbe non interrompersi mai e in questo caso l’interruzione dell’elaborazione della coda di input deve essere gestita esplicitamente dal codice (si veda l’esempio di input da tastiera). L’elaborazione del flusso avviene attravero due tipi di metodi offerti da Scanner. I primi hanno il compito di verificare la presenza di token processabili e convertibili in unpo specifico tipo di dato binario, i secondi si occupano della rimozione del token dalla coda di input e della sua conversione in dato binario. Il caso più semplice di processamento di un flusso di input è quello in cui si vuole convertire ciascun token nella sua corrispondente stringa. I metodi offerti da Scanner per questa operazione sono i seguenti: hasNext() - il segmento di caratteri accodati in input contiene almeno un token: restituisce il valore true , altrimenti attende sinché giungano nuovi dati a completare un token e poi restituisce true. - il flusso viene chiuso e non vi sono ulteriori caratteri da processare: il metodo restituisce il valore false next() - il segmento di caratteri accodati in input contiene almeno un token: processa il primo token della coda, rimuove il token dalla coda di input e restituisce la sua equivalente oggetto String - non vi sono in coda token da processare: JRE lancia un errore di runtime (NoSuchElementException) . Per ciascun tipo primitivo, è definita una coppia di metodi analogi a hasNext() e next(), ma differenti in un aspetto fondamentale: mentre un token è sempre convertibile in una stringa, non sempre lo è in un tipo numerico, si ricordi a questo proposito l’esempio precedentemente citato relativo al token “123.5”. 2 Per il tipo int, ad esempio, i metodi sono: hasNextInt() - il segmento di caratteri accodato contiene almeno un token: restituisce il valore true se il token rappresenta un numero intero, altrimenti attende sino al completamento del token e poi restituisce true. - il flusso viene chiuso e non vi sono caratteri da processare: il metodo restituisce il valore false nextInt() - il segmento di caratteri da processare contiene almeno un token: processa il primo token disponibile, se questo rappresenta un numero intero restituisce il valore corrispondente nel tipo int dopo aver rimosso il token dalla coda, altrimenti JRE lancia un errore di runtime (InputMismatchException). - non vi sono token da processare: JRE lancia un errore di runtime (NoSuchElementException). Per elaborare un flusso di input dal quale si vuole estrarre i dati di un certo tipo type, il tipico approccio è il seguente: partenza attende nuovo token o segnale di fine flusso fine fine flusso? si no rimuove il token (e lo traduce in String) token di tipo type? no si rimuove il token e lo traduce in type 3 Esempio di input da stringa Il primo esempio proposto, è un programma che elabora i dati contenuti in una stringa per estrarre tutti i numeri interi e farne la somma: import java . util . Scanner ; class TrovaInteri { public static void main ( String [] args ) { String str = " 123 centoventitre 014 140 12.2 3/4 0 ,1 " ; // crea uno Scanner collegato all ’ oggetto stringa str Scanner sc = new Scanner ( str ); int sum = 0; // avvia il ciclo di elaborazione dell ’ input // rimanendo in attesa di un token da elaborare while ( sc . hasNext ()) { // verifica se il token è convertibile ad intero if ( sc . hasNextInt ()) { // in caso affermativo ... rimuove il token , // e restituisce il valore intero corr ispond ente double value = sc . nextInt (); // quindi somma il valore a sum sum += value ; } else { // altrimenti ... rimuove il token e // il valore restituito non viene utilizzato sc . next (); } } // l ’ uscita dal ciclo avviene quando sc . hasNext () restituisce // false , ovvero quando tutti i token della stringa str // sono stati romossi dalla coda di input System . out . println ( " La somma degli interi è " + sum ); } } Il programma restituisce: La somma degli interi è 277 4 Esempio di input da tastiera Un analogo programma che elabora i dati inviati in input da tastiera è il seguente: import java . util . Scanner ; class Tr ovaInteriBis { public static void main ( String [] args ) { // crea uno Scanner collegato alla tastiera Scanner sc = new Scanner ( System . in ); int sum =0; // avvia il ciclo di elaborazione dell ’ input // rimanendo in attesa di un token da elaborare while ( sc . hasNext ()) { // verifica se il token è convertibile ad intero if ( sc . hasNextInt ()) { // in caso affermativo ... rimuove il token , // e restituisce il valore intero corr ispond ente double value = sc . nextInt (); // quindi somma il valore a sum sum += value ; } else { // altrimenti ... rimuove il token e // il valore restituito viene assegnato a str String str = sc . next (); // esce dal ciclo se il token è " quit " if ( str . equals ( " quit " )) break ; } } // sc . hasNext () restituisce sempre true , quindi l ’ uscita // dal ciclo avviene grazie al break System . out . println ( " La somma degli interi è " + sum ); } } Si osservi come nel caso di input da tastiera il metodo hasNext non restituisce mai il valore false dato che il flusso dati, almeno potenzialmente, potrebbe non interrompersi mai. Pertanto è necessario ricorrere ad una condizione d’interruzione esplicita, che in questo esempio è realizzata dalla ricezione del token “quit”. Condizioni alternative di uscita dal ciclo potrebbero essere, ad esempio, la ricezione di un token non intero o l’elaborazione di un predeterminato numero di token. 5 Esempio di input da file Un programma che elabora i dati inviati in input da un file testuale è il seguente: import java . util . Scanner ; import java . io . File ; import java . io . IOException ; class Tr o va Int er iT ris { // dovendo accedere ad un file , main può lanciare un ’ eccezione di // Input / Output che deve essere dichiarata con la parola chiave throws // o gestita con il costrutto try - catch public static void main ( String [] args ) throws IOException { // path relativo file di input String pathIn = " testo . txt " ; // crea un oggetto File collegato al file fisico di percorso pathIn File fin = new File ( pathIn ); // crea uno Scanner collegato all ’ oggetto File f Scanner sc = new Scanner ( fin ); int sum = 0; // avvia il ciclo di elaborazione dell ’ input // rimanendo in attesa di un token da elaborare while ( sc . hasNext ()) { // verifica se il token è convertibile ad intero if ( sc . hasNextInt ()) { // in caso affermativo ... rimuove il token , // e restituisce il valore intero corr ispond ente double value = sc . nextInt (); // quindi somma il valore a sum sum += value ; } else { // altrimenti ... rimuove il token e // il valore restituito non viene utilizzato sc . next (); } } // l ’ uscita dal ciclo avviene quando sc . hasNext () restituisce // false , ovvero quando tutti i token della file // sono stati romossi dalla coda di input System . out . println ( " La somma degli interi è " + sum ); } } Si osservi come in questo esempio sia necessario gestire un’eventuale eccezione IOException che può verificarsi, ad esempio, nel caso in cui il file non esista, non sia leggibile o, per qualche motivo, diventi inaccessibile durante la lettura dei dati in input. 6 Flusso di caratteri in output L’oggetto PrintWriter è lo strumento per gestire i flussi di caratteri inviati in output verso lo schermo, un file o un qualunque dispositivo di output. Il flusso dei dati in output è mediato da un buffer, ovvero una zona di memoria riservata da PrintWriter in cui i dati da inviare vengono temporanemaente accodati, analogo alla coda di input che utilizza Scanner. Un oggetto PrintWriter può funzionare in due possibili modalità: ∙ lo svuotamento del buffer e l’invio dei dati in esso contenuti può avvenire indirettamente quando viene accodato un carattere di nuova linea, ∙ solo su un esplicito comando si svuotamento. (flush) Per gestire il flusso di caratteri, PrintWriter definisce quattro metodi il cui effetto, nella modalità svuotamento buffer su nuova linea, è il seguente ∙ print(str) accoda nel buffer la stringa str. Se la stringa contiene dei caratteri carriagereturn, la porzione di stringa fino all’ultimo carattere di line-feed ("∖n") compreso, se presente, viene mandata in output e la parte rimanente viene trattenuta nel buffer; ∙ println(str) accoda al buffer la stringa str e aggiunge alla fine del buffer un carattere carriage-return, il cui effetto è di svuotare il buffer ed inviare tutti i dati che in esso erano contenuti in output; ∙ flush() equivale all’accodamento nel buffer di un line-feed, l’effetto è di svuotarle il buffer e di inviare in output tutti i dati che in esso erano accodati, in questo caso senza bisogno di aggiungere il carriage-return; ∙ close() invia i dati eventualmente contenuti nel buffer e chiude il flusso di output. Nel caso della modalità svuotamento buffer su flush esplicito i caratteri di line-feed inseriti esplicitamente nella stringa o aggiunti da println() vengono accodati nel buffer, che si svuoterà in output solo quando verranno eseguite le istruzioni flush() o close(). Anche se di scarsa utilità nelle applicazioni elementari2 , è possibile associare un PrintWriter allo standard output, cioè alla console su schermo. Per non incorrere in errori, vi sono tuttavia alcune differenze sostanziali nell’uso dei metodi print() e println() dell’oggetto System.out rispetto a quelli forniti da un oggettoPrintWriter collegato a System.out. Infatti, mentre l’istruzione: System.out.print("Ciao Mondo!"); invia in output su schermo la stringa in argomento, il frammento di codice: // frammento 1 // crea un pw su schermo in modalità flush esplicito PrintWriter pw = new PrintWriter ( System . out ); pw . println ( " Ciao Mondo ! " ); 2 Ma fondamentale, ad esempio, per l’internazionalizzazione delle applicazioni. 7 non mandaalcunché in output perché nella modalità di flush esplicito il buffer viene svuotato in output solo ricevendo un’istruzione di svuotamento flush() o close(). Analogamente, il frammento di codice: // frammento 1 // crea un pw su schermo in modalità flush su nuova linea PrintWriter pw = new PrintWriter ( System . out , true ); pw . print ( " Ciao Mondo ! " ); non manda alcunché in output perché, nella modalità flush su new-line, l’istruzione print ha come unico effetto quello di accodare la stringa in argomento nel buffer, ma affinché i dati vengano inviati in output è necessario: - giunga un comando esplicito di svuotamento: flush() o close(), - vengano accodati nel buffer uno o più caratteri di line-feed: print ("....∖n...")) oppure println("...."). Per concludere viene presentato un programma che elabora i dati inviati in input da un file testuale e li invia in output su un file è il seguente: import java . util .*; import java . io .*; class TrovaInteriIO { // dovendo accedere ad un file , main può lanciare un ’ eccezione di // Input / Output che deve essere dichiarata con la parola chiave throws // o gestita con il costrutto try - catch public static void main ( String [] args ) throws IOException { // path relativo file di input String pathIn = " testo . txt " ; // path relativo file di output String pathOut = " out . txt " ; // crea un oggetto File collegato al file fisico di percorso pathIn File fin = new File ( pathIn ); // crea un oggetto File collegato al file fisico // ( eventualmente non esistente ) di percorso pathOut File fout = new File ( pathOut ); // crea uno Scanner collegato all ’ oggetto File fin Scanner sc = new Scanner ( fin ); // crea un PrintWriter collegato all ’ oggetto File fout , // nel caso del flusso su file l ’ unica modalità possibile è // flush su nuova linea == false PrintWriter pw = new PrintWriter ( fout ); int sum = 0; // avvia il ciclo di elaborazione dell ’ input // rimanendo in attesa di un token da elaborare while ( sc . hasNext ()) { // verifica se il token è convertibile ad intero if ( sc . hasNextInt ()) { // in caso affermativo ... rimuove il token , // e restituisce il valore intero corr ispond ente double value = sc . nextInt (); 8 // quindi somma il valore a sum sum += value ; } else { // altrimenti ... rimuove il token e // il valore restituito non viene utilizzato sc . next (); } } // accoda nel bufferla stringa " La somma ... " con un carattere // di nuova riga in coda pw . println ( " La somma degli interi è " + sum ); // svuota il buffer su fout pw . flush (); // chiude il flusso di output ( superluo ) pw . close (); } } In questo esempio, del tutto simile al precedente, l’eventuale eccezione IOException può verificarsi nelle situazioni già menzionate, ma anche nel caso in cui il file collegato all’oggetto fout non risulti creabile/scrivibile. Output formattato Si immagini di voler mandare in output una serie di numeri interi allineati a formare una tabella. Questa operazione prevede la traduzione di un numero in formato binario nel suo equivalente in stringa, pertanto si configura come una sorta di operazione inversa di quella svolta da Scanner. Se, ad esempio, si considera Math.PI, numero a virgola mobile di tipo double, occorrerà decidere, tra i diversi formati disponibili, quale particolare adoperare per rappresentarlo come stringa. Variando alcuni aspetti del formato, si possono ottenere una miriade di rappresentazioni, tra le quali: "3.14159", "3,14159", "3.14", "+3.14", " 3.14", "3.14e+00", "3.14E+00", eccetera. Il separatore decimale, il numero dei caratteri decimali, il numero di caratteri totali, la notazione sceintifica, la presenza obbligatoria del segno + per i numeri positivi, sono solo alcuni degli aspetti del formato che possono essere personalizzati. In Java, l’output formattato può essere gestito in vari modi, ma il più semplice è attraverso il metodo printf() che può essere applicato ad un PrintWriter o, nel caso dello standard output, direttamente a System.out. La scelta del formato avviene attraverso la specificazione di una particolare stringa (pattern) che determina ogni singola caratteristica del formato; si rimanda ad una guida la descrizione dei vari aspetti codificabili nei pattern di formattazione e ci si limita, di seguito, a fornire una serie di esempi per illustrare alvumne modalità di formattazione per i dati numerici. 9 import java . util .*; import java . io .*; class O u tp u tFo rma t ta t o { public static void main ( String [] args ){ // numero da convertire in letterale stringa double number = Math . PI ; // crea un PrintWriter sullo standard output in modalità // flush su nuova linea == true PrintWriter pw = new PrintWriter ( System . out , true ); // dieci caratteri di cui due decimali pw . printf ( " %10.2 f \ n " , number ); // dieci caratteri di cui due decimali pw . printf ( " % -10.2 f \ n " , number ); // dieci caratteri di cui due decimali // in testa e a capo : pw . printf ( " %010.2 f \ n " , number ); // dieci caratteri di cui due decimali // notazione esponenziale e a capo : pw . printf ( " %010.2 e \ n " , number ); // dieci caratteri di cui due decimali // notazione esponenziale e a capo : pw . printf ( " %010.2 e \ n " , number ); giustificato a destra e a capo : giustificato a sinistra e a capo : giustificato a destra con zeri giustificato a destra con zeri in testa giustificato a destra con zeri in testa int parte = ( int ) number ; // intero di dieci caratteri giustificato a destra con zeri in testa con // segno obbligatorio e a capo : pw . printf ( " %0+10 d \ n " , parte ); // intero di otto caratteri giustificato a destra con zeri in testa con // segno obbligatorio circondato da una coppia di pipe (|) e a capo : pw . printf ( " |%0+8 d |\ n " , parte ); } } L’output del precedente programma è 3 ,14 3 ,14 0000003 ,14 003.14 e +00 003.14 e +00 +000000003 |+0000003| 10