Java Scanner-Klasse
Primitive Typen und Strings aus Texteingaben in Java mit der Scanner-Klasse parsen — nextInt, nextLine, useDelimiter.
BufferedReader.readLine() aus dem Kapitel über gepufferte Streams ist das richtige Werkzeug, wenn die Eingabe zeilenorientiert ist und man jede Zeile als String haben möchte. Scanner ist das richtige Werkzeug, wenn die Eingabe ein Strom von Tokens ist — ganze Zahlen, Doubles, durch Leerzeichen getrennte Wörter oder Felder, die durch einen selbst gewählten regulären Ausdruck getrennt sind. Es ist der Parser, der mit dem JDK mitgeliefert wird.
Scanner ist auch die Klasse, die die meisten einführenden Java-Tutorials zum Einlesen von der Tastatur verwenden. new Scanner(System.in) und man hat in zwei Zeilen ein funktionierendes interaktives Programm. Diese Bequemlichkeit hat eine bekannte Falle — die nextInt/nextLine-Falle — um die es in diesem Kapitel hauptsächlich geht.
Was Scanner parst
Die Token-Lesemethoden, zusammen mit ihren hasNext-Prädikaten:
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 lineDer Vertrag ist bei allen typisierten Methoden identisch: hasNextX() prüft, ob das nächste Token als X geparst werden kann, ohne es zu konsumieren; nextX() konsumiert es. Eine Nichtübereinstimmung (nextInt() wenn das Token "hello" ist) wirft InputMismatchException. Ende des Streams wirft NoSuchElementException.
Ein Token ist standardmäßig eine maximale Folge von Nicht-Leerzeichen. Das Trennzeichenmuster ist das, was Pattern.UNICODE_CHARACTER_CLASS als Leerzeichen betrachtet — Leerzeichen, Tabulatoren, Zeilenumbrüche und ähnliches. Man kann es mit useDelimiter(...) ändern.
Konstruktoren
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, ...)Gleiche Regel wie beim Rest von java.io/java.nio: immer einen expliziten Zeichensatz angeben beim Lesen von Bytes. Die Konstruktoren ohne Zeichensatz verwenden die Plattformkodierung.
try (Scanner s = new Scanner(path, StandardCharsets.UTF_8)) {
while (s.hasNextInt()) {
process(s.nextInt());
}
}Das Schließen des Scanner schließt den darunterliegenden Stream. Nicht einen Scanner schließen, der System.in umschließt — dadurch wird System.in geschlossen, und weitere Lesevorgänge in derselben JVM schlagen fehl.
Die nextInt / nextLine-Falle
Die am häufigsten gestellte Java-Frage auf Stack Overflow.
Scanner s = new Scanner(System.in);
System.out.print("age: "); int age = s.nextInt();
System.out.print("name: "); String name = s.nextLine();30 eingeben, Enter drücken, dann Alice, Enter drücken. Erwartet: age=30, name=Alice. Tatsächlich: age=30, name="".
Der Grund: nextInt() liest die Ziffern 30 und hält an. Es lässt das nachfolgende \n im Eingabepuffer. Das nächste nextLine() liest alles bis zum nächsten Zeilenumbruch — der sofort folgt — und gibt den leeren String zurück, bevor der Benutzer etwas eintippen kann.
Die Lösung ist eine der folgenden:
int age = s.nextInt(); s.nextLine(); // explicit "skip to end of line"
String name = s.nextLine();oder robuster, die gesamte Zeile selbst parsen:
int age = Integer.parseInt(s.nextLine().trim()); // always reads the full line
String name = s.nextLine();Das zweite Muster ist dasjenige, das man in echtem Code verwendet. Das Mischen von Token-Lesemethoden (nextInt, nextDouble, next) mit Zeilen-Lesemethoden (nextLine) ist ein Rezept für Off-by-one-Fehler; man sollte sich für eine entscheiden und dabei bleiben. Entweder zeilenweise mit nextLine parsen, oder tokenweise mit next* und nextLine nur für den expliziten Zweck "Rest dieser Zeile überspringen" aufrufen.
hasNext als Schleifenbedingung
Die Form jeder Scanner-Schleife:
while (s.hasNextInt()) { // predicate, no exception
int n = s.nextInt(); // consume
process(n);
}hasNextInt() gibt false am Ende des Streams zurück und wenn das nächste Token keine Ganzzahl ist — so endet die Schleife sauber bei EOF und bei einem nicht-numerischen Token (was oft das Richtige ist, z. B. wenn der abschließende Footer nicht-numerisch ist). Wenn man stattdessen laut scheitern möchte, verwendet man hasNext() und lässt nextInt() bei Nichtübereinstimmung InputMismatchException werfen:
while (s.hasNext()) {
int n = s.nextInt(); // throws if the token isn't an int
process(n);
}Dieselbe End-of-Stream-Prüfung, aber unterschiedliches Verhalten bei ungültigen Tokens.
Benutzerdefinierte Trennzeichen
Das Standardtrennzeichen ist Leerzeichen. Für CSV-ähnliche Eingaben kann man es ändern:
s.useDelimiter(",|\\R"); // comma or any line break\\R ist der Java-Regex für "beliebige Zeilenumbruchsequenz" (\n, \r\n, \r, plus die Unicode-Zeilentrennzeichen). Das kombinierte Muster trennt bei Kommas und Zeilenumbrüchen, sodass 1,2,3\n4,5,6 sechs Tokens ergibt.
Allerdings: für echtes CSV sollte man eine CSV-Bibliothek verwenden. Scanner behandelt keine angeführten Felder, keine maskierten Kommas oder eingebetteten Zeilenumbrüche. Für einfache Fälle — eine Liste von Zahlen, eine durch Leerzeichen getrennte Konfiguration — ist er perfekt.
Die Locale-Falle
nextDouble() parst mit dem Dezimaltrennzeichen der Standard-Locale. Auf einer deutschen JVM schlägt 3.14 fehl (3,14 ist die deutsche Form). Auf einer US-JVM schlägt 3,14 fehl.
Für maschinenlesbare Eingaben erzwingt man die Parser-Locale:
s.useLocale(Locale.ROOT); // dot as decimal separator, no grouping
double x = s.nextDouble(); // now parses "3.14"Locale.ROOT ist die "neutrale" Locale — die Konvention zum Parsen von Datendateien, die nicht für Menschen bestimmt sind. Dieses Vergessen ist der häufigste Grund, warum ein CSV-Reader in der Entwicklung funktioniert und in der CI fehlschlägt: die Entwicklungsumgebung und die CI-Umgebung haben unterschiedliche Standard-Locales.
Scanner vs. BufferedReader
Scanner | BufferedReader | |
|---|---|---|
| Liest | Tokens (typisiert) | Zeilen (String) |
| Geschwindigkeit | langsam (Regex bei jedem Token) | schnell |
| Komfort | hoch (nextInt usw.) | gering (man parst selbst) |
| Geeignet für | kleine Eingaben, interaktive Eingaben, Tests | große Dateien, Log-Verarbeitung, heiße Schleifen |
Faustregel: Wenn die Eingabe von einem Menschen kommt und man Typen möchte, verwendet man Scanner. Wenn die Eingabe eine Datei ist und man Zeilen möchte, verwendet man BufferedReader. Für Eingaben in Competitive-Programming-Größe (Millionen von Tokens) ist BufferedReader + StringTokenizer um eine Größenordnung schneller als Scanner.
Ein ausgearbeitetes Beispiel: ein kleines Textformat parsen
Das Programm unten parst eine kleine, durch Leerzeichen getrennte Textdatei mit drei Datensätzen pro Zeile — id name score — mit Scanner. Es demonstriert die hasNextInt()-Schleife, den Locale-Fix für nextDouble(), die nextInt/nextLine-Falle und ihre Lösung sowie useDelimiter für eine CSV-ähnliche Alternative.
Was man aus der Ausführung mitnehmen kann:
- Der erste Lesevorgang hat drei Datensätze mit drei verschiedenen Typen in drei Codezeilen geparst. Die tokenbasierte API ist wirklich praktisch, wenn die Eingabe wie Tokens strukturiert ist — kein Regex, kein
String.split, kein manuellesInteger.parseInt. Das ist der Anwendungsfall fürScanner. useLocale(Locale.ROOT)war die Zeile, die97.5parsebar machte. Ohne sie verwendet der Parser die Standard-Locale der JVM; auf einem System, auf dem das Deutsch ist, würde97.5eineInputMismatchExceptionwerfen. Bei maschinenlesbaren Eingaben immer die Locale festlegen.- Die Buggy/Fixed-Ausgabe für die Falle zeigte
name=''und dannname='Alice'. Der Fehler war real —nextInt()hinterließ das\nim Puffer — und der zeilenorientierte Fix (Integer.parseInt(s.nextLine().trim())) war der sauberste Weg, das Mischen der zwei Lesestile zu vermeiden. Einen Stil wählen und dabei bleiben. - Der
useDelimiter("," + "|" + "\\R")-Block hat kommagetrennte Zeilen mit demselben Token-Lesecode geparst, nur mit einem anderen Trennzeichen. Derselbe Vorbehalt gilt wie im Fließtext: das funktioniert für sauberes CSV und bricht bei echtem CSV mit angeführten Feldern. Für alles, was aus Excel stammt, eine echte CSV-Bibliothek verwenden. - Der gemischte Eingabe-Footer (
-- end --) zeigte, warumhasNextInt()die richtige Schleifenbedingung ist: er gabfalsebeim ersten nicht-ganzzahligen Token zurück und die Schleife endete sauber. ZuhasNext()wechseln hätte die Schleife weiterlaufen lassen, bisnextInt()geworfen hätte — beide Formen sind nützlich, je nachdem ob ein nicht-ganzzahliges Token "wir sind fertig" oder "die Eingabe ist fehlerhaft" bedeutet.
Was als nächstes kommt
PrintWriter (das vorherige Kapitel) und Scanner sind die zeichenorientierten Ein-/Ausgabeklassen, die die meisten einführenden Java-Codes verwenden. Das nächste Kapitel, Java PrintStream, behandelt das byteorientierte Geschwister von PrintWriter — und erklärt, warum System.out und System.err PrintStreams statt PrintWriters sind.