Comment proteger votre code Java contre les modifications inattendues : Guide complet des classes immuables

Protegez votre code Java contre les modifications non intentionnelles ! Apprenez a prevenir les bugs lies aux references mutables, aux copies defensives et aux bonnes pratiques d'immutabilite.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 7 min read
Comment proteger votre code Java contre les modifications inattendues : Guide complet des classes immuables

Introduction

L’immutabilite est l’un des concepts les plus puissants en programmation Java. Une classe immuable garantit que l’etat de ses instances ne peut jamais etre modifie apres leur creation. Ce paradigme est au coeur de nombreuses bibliotheques Java modernes et constitue une pierre angulaire de la programmation fonctionnelle.

Pourquoi l’immutabilite est-elle si importante ? Elle offre plusieurs avantages majeurs :

  • Thread-safety naturelle : Les objets immuables peuvent etre partages entre threads sans synchronisation
  • Simplicite du raisonnement : Vous pouvez etre certain que l’etat d’un objet ne changera jamais
  • Securite : Impossible de corrompre l’etat interne d’un objet par erreur
  • Hashcode stable : Les objets immuables peuvent etre utilises comme cles de HashMap en toute securite
  • Caching facilite : Les resultats de methodes peuvent etre mis en cache sans risque

Cependant, creer une classe veritablement immuable en Java n’est pas aussi simple qu’il y parait. De nombreux developpeurs pensent qu’il suffit de declarer tous les champs final pour obtenir l’immutabilite. Cette approche est insuffisante et peut mener a des bugs subtils et difficiles a debugger.

Dans cet article, nous allons explorer en profondeur les differents pieges qui guettent le developpeur Java lors de la creation de classes immuables, et nous verrons comment les eviter grace a des techniques eprouvees.

Les classes immuables : une présentation

Une classe immuable est une classe dont les instances ne peuvent pas être modifiées après leur création. Cela signifie que toutes les propriétés d’une instance de cette classe sont finalement définies lors de sa construction et qu’aucune modification ne peut y être apportée ensuite.

Pour comprendre pourquoi cela est important, imaginez une situation où vous créez un objet qui représente un événement dans votre application. Vous voulez que cet objet soit immuable afin d’éviter toute modification accidentelle de ses propriétés. Si vous ne le faites pas, vous risquez de perturber l’état global de votre application.

Le problème des références mutables

Un premier problème qui peut se produire est celui des références mutables. Lorsque vous retournez une liste (ou toute autre collection) dans une méthode, vous créez un problème. En effet, la liste que vous retournez est une référence à la liste existante dans votre objet. Si quelqu’un modifie cette liste, cela peut affecter l’état de votre objet.

Par exemple, considérons le code suivant :

public List<String> getNames() {
    return this.names;
}

Ici, nous retournerons la liste names telle qu’elle est. Si quelqu’un modifie cette liste, cela peut affecter l’état de notre objet.

Pour éviter ce problème, nous pouvons créer une copie défensive de la liste :

public List<String> getNames() {
    return new ArrayList<String>(this.names);
}

Cela créera une nouvelle liste qui est une copie de names. Si quelqu’un modifie cette nouvelle liste, cela ne pourra pas affecter l’état de notre objet.

Le problème des injecteurs

Un deuxième problème que nous pouvons rencontrer est celui des injecteurs. Lorsque nous créons un objet avec un constructeur qui prend en argument une collection (ou toute autre objet mutable), nous créez un problème si cette collection est modifiée après la construction de l’objet.

Par exemple, considérons le code suivant :

public final class NewNames {
    private final List<String> names;

    public NewNames(List<String> names) {
        this.names = names;
    }

    public String getName(int index) {
        return names.get(index);
    }

    public int size() {
        return names.size();
    }
}

Ici, nous créons un objet NewNames avec une liste de noms. Cependant, si quelqu’un modifie cette liste après la construction de l’objet, cela pourra affecter l’état de l’objet.

Pour éviter ce problème, nous pouvons créer une copie défensive de la collection lors de la construction de l’objet :

public NewNames(List<String> names) {
    this.names = new ArrayList<String>(names);
}

Cela créera une nouvelle liste qui est une copie de names. Si quelqu’un modifie cette nouvelle liste, cela ne pourra pas affecter l’état de notre objet.

Les méthodes overridables

Un troisième problème que nous pouvons rencontrer est celui des méthodes overridables. Lorsque nous créons une classe qui hérite d’une autre classe et que nous voulons la rendre immuable, nous devons être attentifs aux méthodes qui peuvent être surchargées par les sous-classes.

Par exemple, considérons le code suivant :

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Ici, nous créons une classe Person avec un nom qui est immuable. Cependant, si quelqu’un crée une sous-classe de Person et surcharge la méthode getName(), cela pourra affecter l’état de l’objet.

Pour éviter ce problème, nous pouvons marquer la classe comme finale afin d’empêcher toute modification :

public final class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Cela empêchera tout sous-classe de modifier la classe Person.

Les accesseurs et modificateurs

Enfin, nous devons être attentifs aux accesseurs et modificateurs que nous utilisons dans nos classes. Lorsque nous créons une propriété qui doit être immuable, nous devons l’initialiser avec une valeur finale afin d’éviter toute modification ultérieure.

Par exemple, considérons le code suivant :

public class Test {
    public int number = 2;
}

Ici, nous créons une classe Test avec une propriété number qui est immuable. Cependant, si quelqu’un modifie cette propriété après la construction de l’objet, cela pourra affecter l’état de l’objet.

Pour éviter ce problème, nous devrions initialiser la propriété avec une valeur finale :

public class Test {
    public final int number = 2;
}

Cela empechera toute modification ulterieure de la propriete number.

Exemple complet : Une classe immuable robuste

Voici un exemple complet qui integre toutes les bonnes pratiques pour creer une classe immuable solide :

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;

public final class ImmutableEmployee {
    private final String name;
    private final int id;
    private final Date dateOfJoining;
    private final List<String> skills;

    public ImmutableEmployee(String name, int id, Date dateOfJoining, List<String> skills) {
        this.name = name;
        this.id = id;
        // Copie defensive pour Date (objet mutable)
        this.dateOfJoining = new Date(dateOfJoining.getTime());
        // Copie defensive pour List (collection mutable)
        this.skills = new ArrayList<>(skills);
    }

    public String getName() {
        return name;
    }

    public int getId() {
        return id;
    }

    public Date getDateOfJoining() {
        // Retourne une copie pour eviter les modifications externes
        return new Date(dateOfJoining.getTime());
    }

    public List<String> getSkills() {
        // Retourne une vue non modifiable
        return Collections.unmodifiableList(skills);
    }

    @Override
    public String toString() {
        return "ImmutableEmployee{name='" + name + "', id=" + id +
               ", dateOfJoining=" + dateOfJoining + ", skills=" + skills + "}";
    }
}

Utilisation avec Java Records (Java 14+)

Depuis Java 14, les Records offrent une syntaxe concise pour creer des classes immuables :

public record Employee(String name, int id, List<String> skills) {
    // Constructeur compact avec validation et copie defensive
    public Employee {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be null or blank");
        }
        if (id <= 0) {
            throw new IllegalArgumentException("ID must be positive");
        }
        // Copie defensive
        skills = List.copyOf(skills);
    }
}

Bonnes Pratiques

Voici les regles d’or pour creer des classes immuables en Java :

1. Declarer la classe comme final

Cela empeche toute sous-classe de compromettre l’immutabilite :

public final class ImmutableConfig {
    // ...
}

2. Rendre tous les champs private et final

private final String value;
private final int count;

3. Ne pas fournir de setters

Aucune methode ne doit permettre de modifier l’etat interne de l’objet.

4. Effectuer des copies defensives

Toujours copier les objets mutables, tant a l’entree (constructeur) qu’a la sortie (getters) :

// Dans le constructeur
public MyClass(Date date) {
    this.date = new Date(date.getTime());
}

// Dans le getter
public Date getDate() {
    return new Date(this.date.getTime());
}

5. Utiliser Collections.unmodifiableList() ou List.copyOf()

// Option 1 : Vue non modifiable
public List<String> getItems() {
    return Collections.unmodifiableList(items);
}

// Option 2 : Copie immutable (Java 10+)
public List<String> getItems() {
    return List.copyOf(items);
}

6. Initialiser tous les champs dans le constructeur

public ImmutablePerson(String name, int age) {
    this.name = Objects.requireNonNull(name, "name cannot be null");
    this.age = age;
}

Pieges Courants

Piege 1 : Oublier les objets mutables imbriques

// MAUVAIS : Date est mutable
public final class BadExample {
    private final Date createdAt;

    public BadExample(Date date) {
        this.createdAt = date; // Reference directe !
    }

    public Date getCreatedAt() {
        return createdAt; // Expose l'objet mutable !
    }
}

// Le probleme :
Date date = new Date();
BadExample bad = new BadExample(date);
date.setTime(0); // Modifie l'etat interne de BadExample !

Piege 2 : Utiliser Arrays.asList() sans copie

// MAUVAIS : Arrays.asList retourne une liste backed par le tableau
String[] arr = {"a", "b", "c"};
List<String> list = Arrays.asList(arr);
arr[0] = "modified"; // Modifie aussi la liste !

// BON : Faire une copie
List<String> safelist = new ArrayList<>(Arrays.asList(arr));

Piege 3 : Classes non-final avec heritage

// MAUVAIS : Une sous-classe peut casser l'immutabilite
public class Person {
    private final String name;
    // ...
}

// Une sous-classe malicieuse :
public class MutablePerson extends Person {
    private String mutableName;

    @Override
    public String getName() {
        return mutableName; // Contourne l'immutabilite !
    }
}

Piege 4 : Retourner this dans les builders

// Attention avec le pattern Builder
public class Builder {
    private List<String> items = new ArrayList<>();

    public Builder addItem(String item) {
        items.add(item);
        return this;
    }

    public ImmutableObject build() {
        // IMPORTANT : Passer une copie, pas la reference
        return new ImmutableObject(new ArrayList<>(items));
    }
}

Piege 5 : Reflection et serialization

Meme les classes immuables peuvent etre compromises par la reflection :

ImmutablePerson person = new ImmutablePerson("John");
Field nameField = ImmutablePerson.class.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(person, "Hacked!"); // Modifie un champ final !

Pour se proteger, utilisez un SecurityManager ou des modules Java 9+.

Comparaison des approches

ApprocheAvantagesInconvenients
Classe manuelleControle totalVerbose, erreurs possibles
Java RecordsConcis, moins d’erreursNecessite Java 14+
Lombok @ValueTres concisDependance externe
AutoValueGeneration automatiqueConfiguration necessaire

Conclusion

La creation de classes immuables en Java est un art qui demande de la rigueur et une bonne comprehension des mecanismes du langage. Les principaux points a retenir sont :

  1. Declarez votre classe final pour empecher l’heritage
  2. Utilisez des champs private final pour tous les attributs
  3. Effectuez des copies defensives a l’entree et a la sortie
  4. Ne fournissez jamais de setters ou de methodes modifiant l’etat
  5. Utilisez les outils modernes comme Records (Java 14+) quand c’est possible

L’immutabilite n’est pas qu’une bonne pratique technique : c’est un changement de paradigme qui simplifie le raisonnement sur le code, elimine des categories entieres de bugs, et rend vos applications plus robustes et maintenables.

En maitrisant ces concepts, vous serez en mesure de concevoir des APIs plus sures et plus previsibles, ce qui beneficiera a l’ensemble de votre equipe et a la qualite de vos projets Java.

Pour aller plus loin

  • Effective Java de Joshua Bloch - Item 17 : Minimize mutability
  • Java Concurrency in Practice - Chapitres sur les objets immuables
  • Documentation officielle des Records Java pour les approches modernes
  • Explorez les bibliotheques comme Immutables.io et AutoValue pour la generation automatique
Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles