Comment fonctionne le mot-clé final en Java ?

Le mot-clé final est l'un des modificateurs les plus mal compris en Java. Contrairement à une idée reçue, il ne rend pas un objet immuable — il empêche simplement la réaffectation d'une référence, la surcharge d'une méthode ou l'héritage d'une classe, selon le contexte où il est placé.

1. final sur une variable

Une variable déclarée final ne peut être affectée qu'une seule fois.

final int age = 30;
age = 31; // ❌ Erreur de compilation

final List<String> liste = new ArrayList<>();
liste = new ArrayList<>();  // ❌ interdit : réaffectation
liste.add("bonjour");       // ✅ autorisé : on mute l'objet

Pour une variable primitive, final équivaut à une constante. Pour une référence d'objet, seule la référence est figée — l'objet pointé reste modifiable. C'est le piège le plus courant : final ne crée pas d'immuabilité.

2. final sur un champ d'instance

Un champ final doit être initialisé soit à la déclaration, soit dans le constructeur. Il est idéal pour modéliser des objets value-based :

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }
}

Tous les champs d'un record (Java 16+) sont implicitement final.

3. final sur une méthode

Empêche une sous-classe de surcharger la méthode. Utile pour figer un algorithme critique ou un invariant de sécurité :

public class Compte {
    public final void verrouiller() {
        // Méthode impossible à redéfinir dans une sous-classe
    }
}

public class CompteEpargne extends Compte {
    @Override
    public void verrouiller() { // ❌ Erreur de compilation
        // ...
    }
}

4. final sur une classe

Empêche toute extension. Les exemples les plus célèbres sont String, Integer et toutes les classes wrapper :

public final class Token {
    private final String valeur;

    public Token(String v) { this.valeur = v; }
}

public class TokenEtendu extends Token { } // ❌ Erreur de compilation

Utile pour garantir qu'une classe respecte toujours son contrat, notamment pour les classes immuables ou les classes utilitaires (Math, Collections).

5. final sur un paramètre de méthode

Indique qu'on ne réaffectera pas le paramètre à l'intérieur de la méthode :

public void traiter(final String entree) {
    entree = entree.trim();      // ❌ interdit
    String propre = entree.trim(); // ✅ ok
}

Purement cosmétique sur le plan comportemental, mais recommandé dans un style strict.

« effectively final » : le cousin discret

Depuis Java 8, une variable locale utilisée dans une lambda ou une classe anonyme n'a pas besoin d'être déclarée final — il suffit qu'elle ne soit jamais réaffectée. On parle alors de variable effectively final :

String prefixe = "[INFO] "; // non marquée final, mais jamais réaffectée

List.of("a", "b", "c").forEach(s -> System.out.println(prefixe + s));
// ✅ compile : prefixe est effectively final

Comment rendre un objet réellement immuable ?

Si votre objectif est l'immuabilité, final seul ne suffit pas. Vous devez :

  1. Déclarer la classe final (pour empêcher l'héritage).
  2. Déclarer tous les champs private final.
  3. Ne fournir aucun setter.
  4. Dans le constructeur, copier défensivement les collections et dates modifiables.
  5. Dans les getters, retourner des copies ou des vues non modifiables.

Ou, beaucoup plus simple, utiliser un record (Java 16+) :

public record Point(int x, int y) { }

Le compilateur génère automatiquement le constructeur, les accesseurs, equals, hashCode et toString, et tous les champs sont final.