Qu'est-ce qu'une IOException en Java et comment la gérer ?

java.io.IOException est l'exception de référence pour signaler qu'une opération d'entrée/sortie a échoué en Java. Lecture d'un fichier absent, écriture sur un disque plein, socket fermée par le serveur : tous ces cas remontent une IOException ou l'une de ses sous-classes.

Pourquoi IOException est une exception vérifiée

Elle hérite directement de Exception (pas de RuntimeException), ce qui en fait une checked exception : le compilateur oblige à la capturer ou à la déclarer avec throws.

// Ne compile pas : IOException non gérée
public void read(String path) {
    Files.readAllLines(Paths.get(path)); // ❌
}

// Version correcte : déclarer
public void read(String path) throws IOException {
    Files.readAllLines(Paths.get(path));
}

Cette contrainte force le développeur à réfléchir explicitement aux cas d'échec, qui sont la règle (pas l'exception) en I/O.

Hiérarchie : les sous-classes utiles

  • FileNotFoundException — le fichier n'existe pas.
  • EOFException — fin de fichier atteinte prématurément.
  • InterruptedIOException — opération interrompue par Thread.interrupt().
  • MalformedURLException — URL syntaxiquement invalide.
  • SocketException, SocketTimeoutException — erreurs réseau.
  • CharacterCodingException — problème d'encodage.

Attraper la sous-classe la plus spécifique permet de répondre différemment selon le cas.

Causes fréquentes

  1. Fichier inexistant ou chemin incorrect (attention aux séparateurs / vs \, aux chemins relatifs).
  2. Permissions insuffisantes — lecture d'un fichier système, écriture dans un dossier protégé.
  3. Flux (stream) déjà fermé — utilisation d'un InputStream après close().
  4. Disque plein — écriture impossible, exception au flush().
  5. Réseau coupé — câble débranché, timeout, serveur down.
  6. Encodage — lecture d'un fichier ISO-8859-1 avec un décodeur UTF-8 strict.

Le pattern moderne : try-with-resources

Depuis Java 7, le try-with-resources ferme automatiquement les ressources I/O, même en cas d'exception :

import java.io.*;
import java.nio.file.*;

public void copier(Path source, Path cible) throws IOException {
    try (InputStream in = Files.newInputStream(source);
         OutputStream out = Files.newOutputStream(cible)) {
        in.transferTo(out);
    }
    // in et out sont fermés automatiquement, même si une IOException est lancée
}

Cette syntaxe remplace le vieux finally { if (in != null) in.close(); } fragile et verbeux.

Capturer et réagir proprement

try {
    String contenu = Files.readString(Paths.get("config.json"));
    traiter(contenu);
} catch (NoSuchFileException e) {
    logger.warn("Fichier de config manquant, utilisation des valeurs par défaut");
    utiliserValeursParDefaut();
} catch (AccessDeniedException e) {
    logger.error("Permission refusée pour {}", e.getFile());
    throw new ConfigException("Configuration illisible", e);
} catch (IOException e) {
    // Filet de sécurité pour les autres erreurs I/O
    logger.error("Erreur I/O inattendue", e);
    throw new ConfigException("Impossible de charger la configuration", e);
}

Points clés :

  • Attrapez la sous-classe la plus spécifique en premier.
  • N'ignorez jamais une exception avec un catch vide — vous perdez toute information de débogage.
  • Encapsulez dans une exception métier si votre couche supérieure n'a rien à faire d'une IOException.

Erreurs courantes à éviter

Attraper trop large

// ❌ Avale même les bugs de logique
try {
    ...
} catch (Exception e) {
    e.printStackTrace();
}

Réencapsuler sans la cause

// ❌ La cause réelle est perdue
catch (IOException e) {
    throw new RuntimeException("Oups"); // sans 'e' en second argument
}

// ✅ La cause est préservée
catch (IOException e) {
    throw new RuntimeException("Lecture impossible", e);
}

Ignorer silencieusement

// ❌ Le problème se reproduira demain sans trace
try { ... } catch (IOException ignored) { }

Quand une IOException n'en est pas une

Certaines API modernes comme java.nio.file.Files lèvent des sous-classes plus informatives :

  • NoSuchFileException au lieu de FileNotFoundException
  • AccessDeniedException
  • FileAlreadyExistsException
  • DirectoryNotEmptyException

Elles héritent toutes de IOException, donc un catch (IOException e) les intercepte toutes. Cibler la sous-classe précise rend vos messages d'erreur plus actionnables.

Règle de survie : fermez toujours vos ressources avec try-with-resources, loguez la cause complète, et capturez la sous-classe la plus spécifique quand vous comptez y réagir.