W3docs

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 line

Il 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

ScannerBufferedReader
Leggetoken (tipizzati)righe (String)
Velocitàlento (regex su ogni token)veloce
Comoditàalta (nextInt ecc.)bassa (si analizza manualmente)
Adatto perinput piccoli, prompt interattivi, testfile 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.

java— editable, runs on the server

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, nessun Integer.parseInt manuale. Questo è il caso d'uso di Scanner.
  • useLocale(Locale.ROOT) è stata la riga che ha reso 97.5 analizzabile. Senza di essa, il parser usa la locale predefinita della JVM; su una macchina dove questa è tedesca, 97.5 avrebbe lanciato InputMismatchException. Per input leggibili da macchina, fissare sempre la locale.
  • La divisione buggy/fixed per la trappola ha stampato name='' poi name='Alice'. Il bug era reale — nextInt() ha lasciato il \n nel 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 restituito false al primo token non intero e il ciclo è uscito in modo pulito. Passare a hasNext() 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.

Esercitazione

Pratica
Su una JVM con il tedesco come locale predefinita, si chiama `scanner.nextDouble()` per analizzare '3.14' da un file di configurazione. Cosa succede e qual è la soluzione?
Su una JVM con il tedesco come locale predefinita, si chiama `scanner.nextDouble()` per analizzare '3.14' da un file di configurazione. Cosa succede e qual è la soluzione?
Was this page helpful?