Nelle applicazioni Java di tipo “console”, cioè quelle che girano all’interno di una console (o “terminale”) a caratteri del sistema operativo, è molto tipico dover chiedere dei dati in input all’utente. In questo contesto ci sono diversi modi per gestire l’input:
- La classe
BufferedReader
che incapsula unInputStreamReader
che incapsula ilSystem.in
- La classe
Scanner
connessa alSystem.in
- La classe
Console
In questo articolo descriverò queste tre tecniche mostrando anche degli esempi pratici basilari.
Premessa
In tutti i moderni sistemi operativi esiste la nozione dei “canali” (stream) che permettono ad un programma di gestire l’input/output verso il resto del sistema, principalmente verso dei file. Questo approccio è una caratteristica peculiare dei sistemi operativi Unix e Unix-like ma esiste in modo similare anche in altri sistemi operativi come quelli Windows. Inoltre è un concetto alla base del I/O nei linguaggi C/C++ e altri derivati da questi.
Di questi canali ne esistono tre predefiniti chiamati convenzionalmente standard-input (abbreviato stdin), standard-output (abbreviato stdout) e standard-error (abbreviato stderr). Il canale stdin rappresenta lo stream per la lettura dei dati in input (in una console/terminale questo input arriva normalmente dalla tastiera) mentre i canali stdout/stderr rappresentano gli stream per scrivere qualcosa in output (stderr in modo più specifico serve per gli errori). Questi stream standard possono anche essere eventualmente “redirezionati” da/verso un file o altro dispositivo, nel qual caso ovviamente non hanno più la funzione originale/predefinita.
In Java questi stream vengono rappresentati dagli oggetti referenziati dalle costanti in
, out
e err
della classe java.lang.System
. Nel corso di questo articolo, l’attenzione sarà ovviamente focalizzata su System.in
che è un oggetto di classe java.io.InputStream
.
Uso delle classi BufferedReader e InputStreamReader con System.in
L’utilizzo delle classi di I/O BufferedReader
e InputStreamReader
con System.in
rappresenta il modo più classico e “portabile” per chiedere dell’input all’utente da standard-input, poiché queste due classi esistono praticamente da sempre nel framework della piattaforma Standard Edition di Java.
InputStreamReader
innanzitutto è semplicemente una classe che si comporta da “adattatore” per permettere di utilizzare un java.io.InputStream
(e in questo caso specifico il System.in
) dove è richiesto un java.io.Reader
, che è in grado di leggere “caratteri” invece che solo byte “crudi”.
Al di “sopra” del InputStreamReader
si mette solitamente un BufferedReader
per un motivo molto semplice: ha il metodo readLine()
che consente di leggere facilmente e comodamente una “riga” di testo.
L’esempio di utilizzo basilare di queste classi con System.in
è il seguente:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class ProvaInput1 {
public static void main(String[] args) throws IOException {
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Inserisci il tuo nome: ");
String nome = input.readLine();
System.out.print("Inserisci il tuo cognome: ");
String cognome = input.readLine();
// .......
}
}
Questa soluzione, pur essendo quella più classica e tradizionale, ha però due aspetti negativi che sono un po’ scomodi e noiosi:
-
BufferedReader
è solamente in grado di leggere una riga di testo oppure (ma per la lettura daSystem.in
è più raro) un singolo carattere o un blocco di n caratteri. Qualunque altra logica per estrarre dei token o per convertire l’input in un valore numerico primitivo (es.int
,long
, ecc...) richiede del codice extra da scrivere appositamente. -
BufferedReader
, essendo una classe che fa parte del packagejava.io
per la gestione base del I/O, tratta e lancia in caso di errore l’eccezionejava.io.IOException
, che tra l’altro è una eccezione “checked” e quindi il programmatore non può ignorarla.
Riguardo le eccezioni, c’è comunque da dire che leggendo da System.in
è abbastanza raro/improbabile che avvenga una eccezione, specialmente se l’input arriva direttamente da tastiera. Nell’esempio sopra, per pura semplicità, IOException
è dichiarata con il throws
nel metodo main
e quindi può uscire fuori facendo così terminare l’applicazione. In altri scenari più reali e complessi potrebbe essere necessario fare una gestione più accurata.
Prima di concludere questa sezione, è bene precisare che l’oggetto BufferedReader
connesso (indirettamente) a System.in
dovrebbe essere creato una volta sola all’interno della applicazione. In altre parole, non c’è bisogno di ricreare più volte gli oggetti BufferedReader
e InputStreamReader
collegati a System.in
.
Uso della classe Scanner con System.in
La classe java.util.Scanner
è stata introdotta nella release Java 5 per gestire la tokenizzazione (ovvero la separazione in token) di una sequenza di testo sfruttando in maniera sofisticata le regular expression.
In generale Scanner
legge una sequenza di caratteri da una certa “sorgente” (che può essere un file rappresentato da java.io.File
oppure può essere un java.io.InputStream
, un String
o altro) e separa la sequenza in token utilizzando in maniera predefinita una regular expression che rappresenta “uno o più whitespace”. Un whitespace è uno spazio, un horizontal tab, un line feed, un carriage return e altro (vedere la documentazione javadoc di Character.isWhitespace(int) per i dettagli).
La classe Scanner
quindi può essere facilmente usata anche per leggere l’input da System.in
. Rispetto alla soluzione con BufferedReader
e InputStreamReader
, l’utilizzo di Scanner
risulta differente per diversi motivi. Innanzitutto Scanner
in generale non è una classe di I/O quindi, a parte l’oggetto che riceve come “sorgente”, non è interoperabile con le altre classi del package java.io
poiché non deriva da una di queste classi (infatti estende direttamente Object
). Inoltre i metodi di Scanner
non lanciano la eccezione IOException
ma altre eccezioni “unchecked” tra cui principalmente InputMismatchException
e NoSuchElementException
.
Scanner
però permette di estrarre dei singoli token come stringa con il metodo next()
e anche come valori primitivi con i metodi nextBoolean()
, nextByte()
, nextInt()
, ecc... e inoltre permette di leggere una intera riga di testo con il metodo nextLine()
, praticamente in maniera similare/equivalente al readLine()
di BufferedReader
.
In più offre dei metodi nella forma hasNextXyz()
per verificare a priori se il prossimo token è estraibile secondo un certo tipo.
L’esempio di utilizzo basilare di Scanner
con System.in
è il seguente:
import java.util.Scanner;
public class ProvaInput2 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("Inserisci il tuo nome: ");
String nome = input.nextLine();
System.out.print("Inserisci il tuo cognome: ");
String cognome = input.nextLine();
// .......
}
}
La soluzione con Scanner
è quindi più interessante e pratica rispetto alla soluzione con BufferedReader
e InputStreamReader
, specialmente per non dover gestire eccezioni “checked” e anche per poter sfruttare i metodi nextXyz()
per estrarre i token come valori primitivi.
Con Scanner
però bisogna prestare attenzione ad alcuni aspetti che possono trarre in inganno o peggio creare problemi:
-
il ben noto problema dell’uso combinato di
nextLine()
insieme agli altrinextXyz()
(es.nextInt()
) -
il fatto che l’input dei valori numerici (in modo specifico quelli decimali) è “localizzato”, cioè si basa su un
java.util.Locale
e quindi il formato cambia in base alla lingua/paese impostato -
il fatto che se l’input è “malformato” (es. si inserisce
1x3
ad unnextInt()
) il metodo lanciaInputMismatchException
ma il token non viene immediatamente rimosso perché resta disponibile inScanner
in modo da poter essere estratto e (eventualmente) trattato diversamente.
(nota: una spiegazione dettagliata di questi aspetti richiederebbe un ulteriore intero articolo che spero di poter fare in futuro)
In maniera similare a quanto detto nella precedente sezione, è bene precisare che l’oggetto Scanner
connesso a System.in
dovrebbe essere creato una volta sola all’interno della applicazione. In altre parole, non c’è bisogno di ricreare più volte l’oggetto Scanner
collegato a System.in
.
Uso della classe Console
La classe java.io.Console
è una piccola novità introdotta nella release Java 6. Questa classe, come dice bene il nome, è specifica per la gestione dell’input/output in una “console” (o “terminale”) del sistema.
Console
ha la caratteristica principale di essere un oggetto singleton, infatti non c’è un costruttore pubblico nella classe e l’unica istanza di Console
si può ottenere solamente invocando il metodo statico System.console()
.
Prima di continuare, bisogna chiarire innanzitutto una questione fondamentale. Le due precedenti tecniche descritte si basano sull’utilizzo di System.in
, cioè lo stream di standard-input. Console
invece non si basa su System.in
, in quanto input e output vengono gestiti ad un livello più “basso”. Questo ha una implicazione molto importante: per poter usare la classe Console
è necessario utilizzare una “vera” console, cioè l’applicazione disponibile nel sistema operativo che apre una finestra per operare in modalità “a caratteri”.
Se lo standard-input venisse redirezionato (es. da un file) o se non si sta utilizzando una vera console (ad esempio la vista Console del IDE Eclipse), la classe Console
non può funzionare e il metodo System.console()
restituisce semplicemente un null
.
Tramite Console
si possono fare diversi tipi di operazioni:
- scrivere una stringa “formattata” (metodi
format
eprintf
) - leggere una intera riga (metodi
readLine
) con la possibilità di usare una stringa “formattata” di prompt - leggere una password (metodi
readPassword
) con la possibilità di usare una stringa “formattata” di prompt - ottenere un
Reader
e/o unPrintWriter
(metodireader
ewriter
) per fare operazioni più sofisticate o particolari (ad esempio per incapsulare ilReader
inScanner
)
In particolare Console
offre una funzionalità esclusiva che non è possibile realizzare con le altre tecniche descritte prima: permette di leggere con readPassword
una password in maniera “mascherata”, cioè senza fare l’eco dei caratteri (in pratica non si vede la password che si sta scrivendo). Inoltre la password viene fornita come array char[]
in modo da poter essere successivamente azzerato per questioni di sicurezza.
Riguardo la gestione delle eccezioni, i metodi di lettura readLine
e readPassword
di Console
non lanciano IOException
ma java.io.IOError
(introdotta in Java 6) per indicare l’occorrenza di un errore di I/O “serio”, cioè molto grave (e che in teoria non dovrebbe mai capitare).
L’esempio di utilizzo basilare di Console
è il seguente:
import java.io.Console;
public class ProvaInput3 {
public static void main(String[] args) {
Console console = System.console();
if (console == null) {
System.err.println("Console non disponibile!");
System.exit(0);
}
String nome = console.readLine("Inserisci il tuo nome: ");
String cognome = console.readLine("Inserisci il tuo cognome: ");
// .......
}
}
Come si può vedere dal codice, una buona prassi è verificare innanzitutto se l’oggetto Console
è disponibile. Se non lo fosse, generalmente la soluzione più semplice (ma drastica) è terminare subito l’applicazione (con il metodo System.exit(status)
o facendo un return
per uscire dal main
).
Per concludere, a differenza delle precedenti soluzioni, non ci sono problemi se si dovesse invocare più volte il metodo System.console()
in quanto l’oggetto Console
restituito è sempre lo stesso essendo gestito come singleton.