Classe Scanner di Java
Analizza primitive e stringhe dall'input di testo in Java con la classe Scanner — nextInt, nextLine, useDelimiter.
BufferedReader.readLine() dal capitolo sugli stream bufferizzati è lo strumento giusto quando l'input è orientato alle righe e si vuole ogni riga come String. Scanner è lo strumento giusto quando l'input è un flusso di token — interi, double, parole separate da spazi vuoti o campi separati da una regex a scelta. È il parser incluso nel JDK.
Scanner è anche la classe usata dalla maggior parte dei tutorial Java introduttivi per leggere dalla tastiera. new Scanner(System.in) e si ha un programma interattivo funzionante in due righe. Questa comodità porta con sé un problema noto — la trappola nextInt/nextLine — di cui questo capitolo si occupa principalmente.
Cosa analizza Scanner
I metodi di lettura dei token, abbinati ai predicati hasNext:
boolean hasNext(); String next(); // a whitespace-delimited token
boolean hasNextInt(); int nextInt(); // a token parsed as int
boolean hasNextLong(); long nextLong();
boolean hasNextDouble(); double nextDouble();
boolean hasNextBoolean(); boolean nextBoolean();
boolean hasNextLine(); String nextLine(); // the rest of the current lineIl contratto è identico per tutti i metodi tipizzati: hasNextX() verifica se il prossimo token può essere analizzato come X senza consumarlo; nextX() lo consuma. Una mancata corrispondenza (nextInt() quando il token è "hello") lancia InputMismatchException. La fine dello stream lancia NoSuchElementException.
Un token è, per impostazione predefinita, una sequenza massima di caratteri non spaziatori. Il pattern delimitatore è quello che Pattern.UNICODE_CHARACTER_CLASS considera spazio bianco — spazi, tabulazioni, newline e simili. È possibile modificarlo con useDelimiter(...).
Costruttori
new Scanner(InputStream source); // typical: System.in
new Scanner(InputStream source, Charset charset); // explicit charset (preferred for files)
new Scanner(Path source, Charset charset); // open a file by path
new Scanner(String source); // parse a literal String — great for tests
new Scanner(Readable source); // wrap any Readable (Reader, CharBuffer, ...)Stessa regola del resto di java.io/java.nio: passare sempre un charset esplicito quando si leggono byte. I costruttori senza charset utilizzano la codifica della piattaforma.
try (Scanner s = new Scanner(path, StandardCharsets.UTF_8)) {
while (s.hasNextInt()) {
process(s.nextInt());
}
}Chiudere Scanner chiude lo stream sottostante. Non chiudere un Scanner che avvolge System.in — chiuderlo chiude System.in, e qualsiasi ulteriore lettura nella stessa JVM fallirà.
La trappola nextInt / nextLine
La domanda Java più posta su Stack Overflow.
Scanner s = new Scanner(System.in);
System.out.print("age: "); int age = s.nextInt();
System.out.print("name: "); String name = s.nextLine();Si digita 30, si preme Invio, poi Alice, si preme Invio. Risultato atteso: age=30, name=Alice. Risultato effettivo: age=30, name="".
Il motivo: nextInt() legge le cifre 30 e si ferma. Lascia il \n finale nel buffer di input. Il successivo nextLine() legge tutto fino al prossimo newline — che è lì, immediatamente — e restituisce la stringa vuota prima che l'utente abbia la possibilità di digitare qualcosa.
La soluzione è una delle seguenti:
int age = s.nextInt(); s.nextLine(); // explicit "skip to end of line"
String name = s.nextLine();oppure, in modo più robusto, analizzare l'intera riga direttamente:
int age = Integer.parseInt(s.nextLine().trim()); // always reads the full line
String name = s.nextLine();Il secondo pattern è quello che si usa nel codice reale. Mescolare metodi di lettura per token (nextInt, nextDouble, next) con la lettura per righe (nextLine) è una ricetta per bug off-by-one; scegliere uno e mantenerlo. O si analizza riga per riga con nextLine, o si analizza token per token con next* e si chiama nextLine solo per lo scopo esplicito di "saltare il resto di questa riga".
hasNext come condizione del ciclo
La struttura di ogni ciclo Scanner:
while (s.hasNextInt()) { // predicate, no exception
int n = s.nextInt(); // consume
process(n);
}hasNextInt() restituisce false alla fine dello stream e quando il prossimo token non è un intero — quindi il ciclo termina in modo pulito sull'EOF e su un token non numerico (il che è spesso la cosa giusta, ad esempio quando il footer finale è non numerico). Se si vuole invece fallire in modo esplicito, usare hasNext() e lasciare che nextInt() lanci InputMismatchException in caso di mancata corrispondenza:
while (s.hasNext()) {
int n = s.nextInt(); // throws if the token isn't an int
process(n);
}Stesso controllo di fine stream, comportamento diverso sui token errati.
Delimitatori personalizzati
Il delimitatore predefinito è lo spazio bianco. Per input simili a CSV è possibile cambiarlo:
s.useDelimiter(",|\\R"); // comma or any line break\\R è la regex Java per "qualsiasi sequenza di newline" (\n, \r\n, \r, più i separatori di riga Unicode). Il pattern combinato divide su virgole e newline, quindi 1,2,3\n4,5,6 produce sei token.
Detto ciò: per CSV reale, usare una libreria CSV. Scanner non gestisce campi tra virgolette, virgole con escape o newline incorporati. Per i casi semplici — una lista di numeri, una configurazione delimitata da spazi — è perfetto.
Il problema della locale
nextDouble() analizza con il separatore decimale della locale predefinita. Su una JVM tedesca, 3.14 fallisce (3,14 è la forma tedesca). Su una JVM statunitense, 3,14 fallisce.
Per input leggibili da macchina, forzare la locale del parser:
s.useLocale(Locale.ROOT); // dot as decimal separator, no grouping
double x = s.nextDouble(); // now parses "3.14"Locale.ROOT è la locale "neutrale" — la convenzione per l'analisi di file di dati non destinati agli esseri umani. Dimenticarlo è il motivo più comune per cui un lettore CSV funziona in sviluppo e fallisce in CI: la macchina di sviluppo e quella di CI hanno locale predefinita diversa.
Scanner vs BufferedReader
Scanner | BufferedReader | |
|---|---|---|
| Legge | token (tipizzati) | righe (String) |
| Velocità | lento (regex su ogni token) | veloce |
| Comodità | alta (nextInt ecc.) | bassa (si analizza manualmente) |
| Adatto per | input piccoli, prompt interattivi, test | file grandi, elaborazione log, cicli intensivi |
Regola pratica: se l'input proviene da un utente e si vogliono tipi, usare Scanner. Se l'input è un file e si vogliono righe, usare BufferedReader. Per input di dimensioni da programmazione competitiva (milioni di token), BufferedReader + StringTokenizer è un ordine di grandezza più veloce di Scanner.
Un esempio pratico: parsing di un piccolo formato testuale
Il programma seguente analizza un piccolo file di testo delimitato da spazi con tre record per riga — id name score — usando Scanner. Dimostra il ciclo hasNextInt(), la correzione della locale per nextDouble(), la trappola nextInt/nextLine e la sua risoluzione, e infine useDelimiter per un'alternativa simile a CSV.
Cosa trarre dall'esecuzione:
- La prima lettura ha analizzato tre record di tre tipi diversi in tre righe di codice. L'API basata su token è genuinamente comoda quando l'input è strutturato come token — nessuna regex, nessun
String.split, nessunInteger.parseIntmanuale. Questo è il caso d'uso diScanner. useLocale(Locale.ROOT)è stata la riga che ha reso97.5analizzabile. Senza di essa, il parser usa la locale predefinita della JVM; su una macchina dove questa è tedesca,97.5avrebbe lanciatoInputMismatchException. Per input leggibili da macchina, fissare sempre la locale.- La divisione buggy/fixed per la trappola ha stampato
name=''poiname='Alice'. Il bug era reale —nextInt()ha lasciato il\nnel buffer — e la correzione orientata alle righe (Integer.parseInt(s.nextLine().trim())) è stato il modo più pulito per evitare di mescolare i due stili di lettura. Scegliere uno stile e mantenerlo. - Il blocco
useDelimiter("," + "|" + "\\R")ha analizzato righe separate da virgole con lo stesso codice di lettura token, solo con un delimitatore diverso. Vale la stessa avvertenza della parte teorica: funziona per CSV pulito e si rompe su CSV reale con campi tra virgolette. Usare una vera libreria CSV per qualsiasi cosa proveniente da Excel. - Il footer dell'input misto (
-- end --) ha mostrato perchéhasNextInt()è la condizione corretta del ciclo: ha restituitofalseal primo token non intero e il ciclo è uscito in modo pulito. Passare ahasNext()avrebbe fatto continuare il ciclo finchénextInt()non avesse lanciato un'eccezione — entrambe le forme sono utili, a seconda che un token non intero significhi "abbiamo finito" o "l'input è errato".
Prossimi passi
PrintWriter (il capitolo precedente) e Scanner sono le classi di input/output orientate ai caratteri usate dalla maggior parte del codice Java introduttivo. Il prossimo capitolo, Java PrintStream, tratta il fratello orientato ai byte di PrintWriter — e spiega perché System.out e System.err sono PrintStream invece di PrintWriter.