Aller au contenu

Bonnes pratiques Java

Les bonnes pratiques Java ont évolue significativement avec les versions modernes du langage. Java 21 apporte des idiomes expressifs (records, sealed classes, pattern matching) qui remplacent des patterns verbeux historiques. Cette section couvre les conventions, les idiomes modernes, les anti-patterns courants et les fondamentaux de performance JVM.


Conventions de code

Java dispose d'un style officiel bien établi, avec deux références principales :

Convention Éditeur Points clés
Google Java Style Google Indentation 2 espaces, longueur 100 char, imports tries
Oracle Code Style Oracle Indentation 4 espaces, longueur 80 char (historique)

La majorité des projets modernes privilegient Google Java Style.

Règles de nommage

// Packages : tout en minuscules, sans tirets
package com.example.gestioncommandes.service;

// Classes et interfaces : PascalCase
public class CommandeService { }
public interface UtilisateurRepository { }

// Methodes et variables : camelCase
public List<Commande> listerCommandesEnCours() { }
private int nombreTentatives;

// Constantes : SCREAMING_SNAKE_CASE
public static final int TIMEOUT_SECONDES = 30;
public static final String TOPIC_COMMANDES = "commandes.creees";

// Generiques : lettres simples (T, E, K, V) ou noms descriptifs
public class Cache<K, V> { }
public <T extends Comparable<T>> T trouverMin(List<T> elements) { }

Idiomes modernes (Java 16-21)

Records

Les records (Java 16) sont des classes de données immuables avec equals, hashCode, toString et accesseurs générés automatiquement.

// Record Java 16+ — remplace les classes DTO avec Lombok @Value
public record Adresse(
    String rue,
    String ville,
    String codePostal
) {
    // Constructeur compact pour la validation
    public Adresse {
        if (codePostal == null || codePostal.length() != 5) {
            throw new IllegalArgumentException("Code postal invalide : " + codePostal);
        }
        // Les champs sont assignes automatiquement apres le bloc compact
    }

    // Methode supplementaire possible
    public String afficher() {
        return rue + ", " + codePostal + " " + ville;
    }
}

// Utilisation
Adresse adresse = new Adresse("12 rue de la Paix", "Paris", "75001");
System.out.println(adresse.ville());    // "Paris" — accesseur genere
System.out.println(adresse.afficher()); // "12 rue de la Paix, 75001 Paris"

Sealed classes

Les sealed classes (Java 17) limitent l'héritage a un ensemble connu de sous-classes, permettant un pattern matching exhaustif.

// Hierarchie de resultats d'operation — sealed pour l'exhaustivite
public sealed interface ResultatOperation<T>
    permits ResultatOperation.Succes, ResultatOperation.Echec {

    record Succes<T>(T valeur) implements ResultatOperation<T> {}
    record Echec<T>(String message, Exception cause) implements ResultatOperation<T> {}
}

// Pattern matching switch exhaustif — le compilateur verifie tous les cas
public <T> void traiter(ResultatOperation<T> resultat) {
    switch (resultat) {
        case ResultatOperation.Succes<T> s -> System.out.println("Succes : " + s.valeur());
        case ResultatOperation.Echec<T> e -> System.err.println("Erreur : " + e.message());
        // Pas besoin de 'default' — le compilateur verifie l'exhaustivite
    }
}

Pattern matching

// Pattern matching instanceof (Java 16) — plus de cast explicite
Object obj = recupererDonnee();

// Ancien style — verbeux
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Style moderne — binding variable directement
if (obj instanceof String s && s.length() > 10) {
    System.out.println("Chaine longue : " + s);
}

// Pattern matching switch (Java 21)
String description = switch (obj) {
    case Integer i when i < 0 -> "Entier negatif : " + i;
    case Integer i             -> "Entier positif : " + i;
    case String s              -> "Chaine : " + s;
    case null                  -> "Valeur nulle";
    default                    -> "Autre type : " + obj.getClass().getSimpleName();
};

Optional — usage correct

import java.util.Optional;

// Bon usage : representer une valeur potentiellement absente dans les signatures
public Optional<Utilisateur> trouverParEmail(String email) {
    return repository.findByEmail(email); // Optional<Utilisateur>
}

// Exploitation de Optional
Optional<Utilisateur> optUser = trouverParEmail("alice@example.com");

// map + orElse — chaine fonctionnelle
String nom = optUser
    .map(Utilisateur::getNom)
    .orElse("Utilisateur inconnu");

// ifPresent — action si present
optUser.ifPresent(u -> log.info("Connexion : {}", u.getNom()));

// orElseThrow — leve une exception si absent
Utilisateur user = optUser.orElseThrow(() ->
    new UtilisateurNotFoundException("alice@example.com"));

Mauvais usages de Optional

  • Ne jamais utiliser Optional comme parametre de méthode ou de constructeur
  • Ne jamais utiliser Optional comme champ d'entité JPA
  • Ne jamais appeler get() sans vérifier isPresent() — utilisez orElse, orElseThrow, map
  • Optional.of(null) leve NullPointerException — utilisez Optional.ofNullable(valeur)

Anti-patterns courants

Retour de null

// A eviter — force les appelants a checker null ou risque NullPointerException
public Produit trouverProduit(Long id) {
    // ...
    return null; // Si non trouve
}

// Preferer — contrat explicite dans la signature
public Optional<Produit> trouverProduit(Long id) {
    return repository.findById(id);
}

// Ou lever une exception metier si l'absence est une erreur
public Produit trouverProduitOuEchouer(Long id) {
    return repository.findById(id)
        .orElseThrow(() -> new ProduitNotFoundException(id));
}

Abus d'exceptions verifiees

// Problematique — exception verifiee qui force les appelants a la gerer partout
public class ServiceFichier {
    public String lire(String chemin) throws IOException { // Checked exception
        return Files.readString(Path.of(chemin));
    }
}

// Preferable — wrapper en exception non-verifiee si non recuperable
public class ServiceFichier {
    public String lire(String chemin) {
        try {
            return Files.readString(Path.of(chemin));
        } catch (IOException e) {
            // L'appelant ne peut pas recuperer de cette erreur — unchecked
            throw new FichierIntrouvableException("Impossible de lire : " + chemin, e);
        }
    }
}

God class

// Anti-pattern : une classe qui fait tout
public class GestionnaireApplication {
    public void creerUtilisateur() { /* ... */ }
    public void envoyerEmail() { /* ... */ }
    public void genererFacture() { /* ... */ }
    public void exporterRapport() { /* ... */ }
    public void traiterPaiement() { /* ... */ }
    // 50 autres methodes...
}

// Correct : responsabilite unique (SRP)
public class UtilisateurService { public void creer() { /* ... */ } }
public class NotificationService { public void envoyerEmail() { /* ... */ } }
public class FacturationService { public void genererFacture() { /* ... */ } }

Singleton mutable (anti-pattern)

// Anti-pattern : singleton mutable — difficile a tester, problemes de concurrence
public class ConfigurationManager {
    private static ConfigurationManager instance;
    private Map<String, String> config = new HashMap<>(); // Mutable !

    public static ConfigurationManager getInstance() {
        if (instance == null) {
            instance = new ConfigurationManager(); // Pas thread-safe
        }
        return instance;
    }
}

// Preferable : injection de dependances Spring
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {
    private final String apiUrl;   // Immuable
    private final int timeout;

    public AppConfig(String apiUrl, int timeout) {
        this.apiUrl = apiUrl;
        this.timeout = timeout;
    }

    public String getApiUrl() { return apiUrl; }
    public int getTimeout() { return timeout; }
}

Gestion des erreurs

try-with-resources

// Avant Java 7 — fermeture manuelle risquee
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("fichier.txt"));
    // traitement...
} finally {
    if (reader != null) {
        try { reader.close(); } catch (IOException e) { /* ignore */ }
    }
}

// Java 7+ try-with-resources — fermeture automatique et sure
try (BufferedReader reader = new BufferedReader(new FileReader("fichier.txt"));
     PrintWriter writer = new PrintWriter("sortie.txt")) {
    String ligne;
    while ((ligne = reader.readLine()) != null) {
        writer.println(ligne.toUpperCase());
    }
} catch (IOException e) {
    log.error("Erreur de lecture/ecriture", e);
}
// reader et writer sont fermes automatiquement, meme en cas d'exception

Checked vs Unchecked exceptions

// Exception verifiee (checked) — l'appelant DOIT la gerer ou la propager
// Utilisee quand l'appelant peut recuperer (ex: fichier absent -> proposer un autre)
public class FichierException extends Exception {
    public FichierException(String message) { super(message); }
}

// Exception non-verifiee (unchecked) — l'appelant peut ignorer
// Utilisee pour les erreurs de programmation ou les cas non-recuperables
public class ProduitNotFoundException extends RuntimeException {
    public ProduitNotFoundException(Long id) {
        super("Produit introuvable : " + id);
    }

    public ProduitNotFoundException(Long id, Throwable cause) {
        super("Produit introuvable : " + id, cause);
    }
}

Performance JVM

Choix du Garbage Collector

# G1GC (default Java 9+) — equilibre latence et debit, adapte aux applications web
java -XX:+UseG1GC -jar app.jar

# ZGC — latence tres faible (<1ms pause), Java 15+ (production depuis Java 21)
# Ideal pour les applications avec contraintes de latence strictes
java -XX:+UseZGC -jar app.jar

# Shenandoah — similaire ZGC, maintenu par Red Hat
java -XX:+UseShenandoahGC -jar app.jar

Tuning JVM de base

# Options recommandees pour conteneurs Docker
java \
    -XX:+UseContainerSupport \          # Detecte les limites memoire Docker
    -XX:MaxRAMPercentage=75.0 \         # Utilise 75% de la RAM container
    -XX:+UseZGC \                       # GC a faible latence
    -XX:+ZGenerational \                # ZGC generationnel (Java 21, meilleur debit)
    -Xlog:gc*:gc.log:time,uptime \      # Logs GC pour analyse
    -jar app.jar

JIT warmup

La JVM compile le bytecode en code natif progressivement. Les premières requêtes sont plus lentes pendant la phase de "warmup" du JIT.

// Strategie : warmup programmatique au demarrage (Spring ApplicationRunner)
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class JvmWarmup implements ApplicationRunner {

    private final ItemService service;

    public JvmWarmup(ItemService service) {
        this.service = service;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Executer quelques appels au demarrage pour chauffer le JIT
        // avant que le trafic reel arrive
        try {
            service.listerTous();
        } catch (Exception e) {
            // Ignorer les erreurs de warmup (ex: BDD pas encore prete)
        }
    }
}

Java Flight Recorder pour le profiling

Java Flight Recorder (JFR) est intégré depuis Java 11 et permet d'enregistrer des métriques JVM en production avec un overhead minimal (\<2%). Analysez les enregistrements avec JDK Mission Control (JMC) pour identifier les hotspots CPU, les allocations mémoire excessives et les problèmes de GC.

# Demarrer un enregistrement de 60 secondes
jcmd <PID> JFR.start duration=60s filename=profil.jfr