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