Input/output (ed altro) da file di testo con le API di Java

Di seguito sono riportate le risposte (con esempi di codice sorgente) ad alcune delle domande più frequenti riguardanti gli esercizi d’esame. Le risposte sono organizzate secondo il seguente

Argomenti sulla linea di comando

Per argomenti sulla linea di comando si intendono tutte le parole (stringhe massimali non contenenti spazio) che seguono il nome della classe nell’invocazione della JVM. Ad esempio, se avete compilato una classe di nome Soluzione e ne invocate l’esecuzione tramite l’interprete come:

java Soluzione uno          2 tr_e

gli argomenti saranno le tre parole: uno, 2 e tr_e.

La funzione main che ha segnatura:

public static void main( String[] args );

può accedere a tali parole tramite l’array args il cui i-esimo puntatore punta alla stringa corrispondente all’i-esimo argomento (l’argomento di posto 0 è la prima parola).

Osservate che gli argomenti sono stringhe, qualora sia richiesto trattare alcuni di essi come numeri sarà necessario usare una funzione di conversione, come ad esempio parseT delle varie sottoclassi di Number (dove T è uno dei tipi elementari), come ad esempio con il metodo parseInt di Integer.

Si riporta, a titolo di esempio, un programma che, dati per argomenti alcuni numeri interi, ne stampa la somma:

public class SommaArgs {

	public static void main( String[] args ) {

		int somma = 0;
		for ( String arg: args ) {
			somma += Integer.parseInt( arg );
		}
		System.out.println( somma );

	}

}

Scarica il codice sorgente di questo esempio.

Input/Output

Di seguito sono riportati alcuni scampoli di codice Java necessari a gestire l’input in formato testuale che tipicamente è richiesto dalla soluzione degli esercizi di laboratorio e d’esame.

La gestione di tale input può essere organizzata secondo due coppie indipendenti di varianti a seconda

  1. di come viene consumato
  1. come sequenza di linee,
  2. tokenizzato come sequenza di tipi primitivi (int, float, ...) e stringhe;
  1. che provenga
  1. dal flusso standard (System.in),
  2. da un file (indicato tramite il suo path).

Il codice ha in generale la seguente forma:

α  ... in = new ...( ... );
β  while ( /* c'è input */ )
β     /* consuma l'input */
α  in.close();

ed è organizzato in due parti:

  • α istanziazione (e gestione) di un oggetto che rappresenti l’input,
  • β ciclo che consumi (ed elabori) l’input.

Secondo l’organizzazione logica discussa all’inizio, il modo in cui sarà consumato (1.) e l’origine dell’input (2.) daranno luogo a quattro diverse implementazioni della parte α, mentre la modalità in cui l’input sarà consumato (1.) darà luogo a due diverse implementazioni della parte β.

Parte α: istanziare l’oggetto usato per l’input

Per leggere l’input una linea dopo l’altra (1.1.) è sufficiente usare un BufferedReader. Il costruttore di tale classe ha per parametro un Reader, che può essere istanziato (2.1.) come un InputStreamReader che a sua volta avvolga System.in, o (2.2.) come un FileReader.

D’altro canto, per tokenizzare l’input (1.2.) è sufficiente usare uno Scanner. Il costruttore di tale classe ha per parametro un InputStream, che può essere direttamente (2.1.) un System.in, o istanziato (2.2.) come FileInputStream.

Le quattro versioni della parte α del codice sono pertanto:

(1.1., 2.1.)  BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) );
(1.1., 2.2.)  BufferedReader in = new BufferedReader( new FileReader( path ) );
(1.2., 2.1.)  Scanner in = new Scanner( System.in );
(1.2., 2.2.)  Scanner in = new Scanner( new FileInputStream( path ) );

dove si assume che path sia una variabile di tipo stinga che contiene il path del file che contiene l’input.

Parte β: consumare l’input

Per consumare (ed elaborare) l’input, sono sufficienti due solte implementazioni della parte β, dal momento che il tipo dell’oggetto in può essere solo un BufferedReader o uno Scanner, a seconda di (1.), ma indipendentemente da (2.).

Per leggere una sequenza di linee (1.1.) si può utilizzare il metodo readLine; per di più, tale metodo è in grado di segnalare la fine dell’input restituendo il valore speciale null. Il ciclo che consuma l’input, in questo caso, ha quindi la forma seguente:

String var = null;
while ( ( var = in.readLine() ) != null )
   /* consuma l'input */

Tipi elementari

Per leggere una sequenza di tipi elementari (1.2.) si possono utilizzare i metodi nextT (dove T è uno dei tipi elementari), ad esempio, per gli interi, si puà usare il metodo nextInt; per sapere se l’input è finito (o se ci sono ancora a disposizione altri elementi), si può usare il metodo hasNextT (dove T è, come sopra, uno dei tipi elementari), ad esempio, ancora nel caso degli interi hasNextInt. Il ciclo che consuma l’input, sempre nel caso degli interi, ha quindi la forma seguente:

while ( in.hasNextInt() ) {
   int var = in.nextInt();
   /* consuma l'input */
}

Stringhe

Qualora sia necessario leggere delle stringhe (1.2.), ossia delle sequenze massimali di caratteri diversi da whitespace (che sono spazio, segno di tabulazione orizzontale e verticale e a-capo), si possono usare i metodi next e hasNext in modo del tutto analogo al caso precedente:

while ( in.hasNext() ) {
   String var = in.next();
   /* consuma l'input */
}

Osservazioni ed esempi

Mettendo assieme gli esempi di codice delle parti α e β è possibile elaborare l’input, linea per linea o numero per numero, sia che provenga dal flusso standard che da un file.

Un dettaglio utile da ricordare è che nella lettura del flusso standard da console (senza redirezione, cioè), la terminazione del flusso va segnalata esplicitamente tramite l’immissione dell’apposito carattere di controllo ^D denominato EOF (end of file), che si ottiene usualmente premendo assieme i tasti ctrl e d (minuscolo).

Altro dettaglio importante è che alcuni dei costruttori e metodi invocati possono sollevare eccezioni di tipo IOException (o sue sottoclassi), che devono essere opportunamente gestite. Nel contesto della prova d’esame, qualora tali metodi fossero invocati all’interno del metodo main, una soluzione plausibile è quella di aggiungere throws IOException alla dichiarazione di tale metodo (come nel codice riportato di seguito).

A titolo di esempio, riportiamo due piccoli programmi. Il primo legge l’input dal flusso standard ed emette ogni riga preceduta dal suo numero progressivo (per provare il suo funzionamento, si può ad esempio il comando java java NumeraLinee < NumeraLinee.java che, facendo uso della redirezione dell’input, numererà le linee del programma stesso).

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class NumeraLinee {
	public static void main( String[] args ) throws IOException {
		BufferedReader in = new BufferedReader( new InputStreamReader( System.in ) );
		int n = 0;
		String var = null;
		while ( ( var = in.readLine() ) != null ) {
			System.out.println( n + "\t" + var );
			n += 1;
		}
		in.close();
	}
}

Scarica il codice sorgente di questo esempio.

Il secondo legge una sequenza di numeri in virgola mobile da un file il cui path è specificato come parametro (all’invocazione della JVM), e ne stampa la somma.

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Scanner;

public class SommaInput {
   public static void main( String[] args ) throws IOException {
      String path = args[ 0 ];
      float somma = 0.0f;
      Scanner in = new Scanner( new FileInputStream( path ) );
      while ( in.hasNextFloat() ) {
         float var = in.nextFloat();
         somma += var;
      }
      in.close();
      System.out.println( somma );
   }
}

Scarica il codice sorgente di questo esempio.

Di nuovo, a titolo di esempio, assumendo che esista un file input.txt che contenga:

1
2.5
3

eseguendo il programma con il comando java SommaInput input.txt, verrà prodotto in output il numero 6.5

Altri approcci

La ricchezza delle API di Java rende possibile risolvere il problema descritto in questa guida in molti altri modi. Questo è certamente una ricchezza, ma produce anche molta confusione in chi si avvicina per la prima volta al linguaggio e alle sue librerie.

Ad esempio, l’input di tipi elementari potrebbe anche essere implementato leggendo l’input per linea, suddividendo poi la linea con uno StringTokenizer, o con il metodo split di String, traducendo in fine le singole parti nei tipi elementari con i metodi parseT delle varie sottoclassi Number (dove T è uno dei tipi elementari), come ad esempio con il metodo parseInt di Integer. Evidentemente, l’uso della classe Scanner appare una soluzione molto più elementare a questo tipo di problema. In ogni modo, una soluzione alternativa, in questo senso, dell’esercizio due potrebbe essere la seguente:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class SommaInputBis {
   public static void main( String[] args ) throws IOException {
      String path = args[ 0 ];
      float somma = 0.0f;
      BufferedReader in = new BufferedReader( new FileReader( path ) );
      String var = null;
      while ( ( var = in.readLine() ) != null ) {
         float v = Float.parseFloat( var );
         somma += v;
      }
      in.close();
      System.out.println( somma );
   }
}

Scarica il codice sorgente di questo esempio.

D’altro canto, a ben guardare, c’è un metodo nextLine tra quelli di Scanner che si comporta sostanzialmente come il metodo readLine di BufferedReader; in line a di principio, quindi, tutta la discussione si potrebbe di gran lunga semplificare limitandosi ad utilizzare la classe Scanner sia per leggere l’input liena per linea che in modo tokenizzato. Ma è altresì vero che l’uso di una classe complessa come Scanner per uno scopo così banale come quello di leggere l’input per linee sembra del tutto sproprorzionato; inoltre, tale classe ha fatto la sua comparsa solo nelle versioni più recenti di Java, ragion per cui è bene conoscere anche alternative che siano praticabili nel caso in cui si abbia a disposizione sono una versione meno recente del linguaggio.

Dati non testuali

Come ultima osservazione, si noti che in questa guida (per brevità e semplicità) si è trattato solo il caso di file in formato, per così dire, testuale. Le API di Java mettono a disposizione anche classi e metodi per il trattamento di dati in formato binario (ad esempio, tramite le interfacce DataInput e DataOutput e relative implementazioni), che meritano una discussione a se stante.

Una interessante aggiunta nelle API delle nuove versioni di Java è la classe Files che mette a disposizione una serie di metodi statici per leggere (e scrivere) con una sola chiamata l’intero contenuto di un file, come ad esempio il metodo readAllBytes che restituisce un array di byte, o il metodo readAllLines che restituisce una List di strighe.