Comment fonctionne la classe Java Scanner en interne

La classe java.util.Scanner est l'outil le plus courant pour lire une entrée texte en Java. Derrière sa simplicité apparente se cache un moteur de parsing basé sur les expressions régulières qui explique à la fois sa flexibilité et ses performances modestes.

Utilisation basique

import java.util.Scanner;

Scanner sc = new Scanner(System.in);
System.out.print("Votre nom : ");
String nom = sc.nextLine();
System.out.print("Votre âge : ");
int age = sc.nextInt();
System.out.println("Bonjour " + nom + ", " + age + " ans");
sc.close();

Le fonctionnement interne

Scanner est conçu autour de trois éléments clés :

  1. Un Readable source (InputStream, File, String, ReadableByteChannel…). Scanner le convertit en CharBuffer interne pour éviter des appels I/O excessifs.
  2. Un Pattern séparateur (par défaut \\p{javaWhitespace}+). Il définit ce qui sépare les tokens.
  3. Un tampon et un Matcher. À chaque appel de next(), nextInt(), etc., Scanner avance dans le buffer en cherchant le prochain motif correspondant au type demandé.

Le rôle du séparateur

Le séparateur est une expression régulière qu'on peut redéfinir :

Scanner sc = new Scanner("rouge,vert,bleu");
sc.useDelimiter(",");
while (sc.hasNext()) {
    System.out.println(sc.next()); // rouge, puis vert, puis bleu
}

En combinant useDelimiter() et des regex, on peut parser des formats complexes sans tokenisation manuelle.

Les méthodes de lecture

MéthodeLitRegex utilisée (implicite)
next()Un token (défini par le séparateur)—
nextLine()Toute la ligne jusqu'au sautjusqu'Ă  \n
nextInt()Un entier-?\d+
nextDouble()Un décimalmotif numérique localisé
nextBoolean()true ou false(true|false)

Chaque nextXxx() applique le pattern au buffer ; si la correspondance échoue, InputMismatchException est levée, mais le buffer n'est pas consommé — vous pouvez appeler next() pour sauter le token invalide.

Le piège classique : next() puis nextLine()

System.out.print("Âge : ");
int age = sc.nextInt();     // lit 30, laisse \n dans le buffer
System.out.print("Nom : ");
String nom = sc.nextLine(); // lit la ligne vide restée dans le buffer !

Solution : ajouter un sc.nextLine() après le nextInt() pour consommer le saut de ligne, ou parser tout en nextLine() puis convertir.

Dépendance au Locale

Scanner respecte le Locale courant pour les formats numériques. Sur un système français, "3,14" est accepté ; sur un système US, "3.14". Cela peut surprendre en production :

Scanner sc = new Scanner("3.14");
sc.useLocale(java.util.Locale.US);
double pi = sc.nextDouble(); // 3.14

Performance

Scanner est lent comparé à BufferedReader : la compilation et l'application d'un Pattern à chaque token coûte ~10 à 50× plus. Sur des concours de programmation ou du traitement de très gros fichiers, vous verrez souvent :

import java.io.*;

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String ligne;
while ((ligne = br.readLine()) != null) {
    String[] parts = ligne.split("\\s+");
    int a = Integer.parseInt(parts[0]);
    // ...
}

Moins élégant, mais 10 à 50 fois plus rapide. Pour un traitement encore plus fin, StreamTokenizer permet un contrôle bas niveau sans regex.

Ne pas fermer System.in

Quand la source est System.in, ne fermez pas le Scanner — ou fermez-le en fin de programme seulement. Fermer un Scanner ferme sa source sous-jacente, et System.in fermé ne se rouvre pas.

Résumé

  • Scanner = Readable + Pattern sĂ©parateur + Matcher.
  • Excellent pour des lectures simples, des scripts, des TP.
  • Éviter sur les gros volumes : prĂ©fĂ©rer BufferedReader.
  • Attention au mĂ©lange nextInt() / nextLine() et au Locale.
  • Toujours close() pour les Scanner ouverts sur un File, jamais sur System.in en cours de programme.

Connaître le moteur interne vous aide à choisir Scanner quand c'est pertinent, et à vous en passer quand la performance l'exige.