ProgrammationDéveloppeur Java

Qu'est-ce qu'un final efficace (effectively final) en Java, comment cela est-il lié aux expressions lambda et aux classes internes, et quels détails faut-il connaître ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Historique de la question :

Avant Java 8, pour utiliser des variables d'un contexte externe dans une classe interne ou une classe anonyme, ces variables devaient obligatoirement être déclarées comme final. Dans Java 8, cette exigence a été assouplie : maintenant, une variable peut ne pas être déclarée comme final, à condition qu'en fait elle ne soit pas modifiée (effectively final).

Problème :

Les expressions lambda et les classes internes utilisent des fermetures autour des variables du bloc externe. Cependant, si ces variables changent de valeur, cela provoque de la confusion et un comportement incorrect : il est impossible de comprendre quelle valeur utiliser.

Solution :

Le compilateur autorise l'utilisation d'une variable dans une classe interne ou une lambda uniquement si elle est effectively final — c'est-à-dire jamais modifiée après initialisation, même si elle n'est pas explicitement déclarée comme final.

Exemple de code :

public void demo() { int x = 10; Runnable r = () -> System.out.println(x); // x — effectively final r.run(); }

Si on essaie de modifier x :

public void demo() { int x = 10; x = 20; // maintenant x n'est pas effectively final Runnable r = () -> System.out.println(x); // Erreur de compilation }

Points clés :

  • Le compilateur Java détermine lui-même si une variable est effectively final
  • Violer cette règle entraîne des erreurs de compilation
  • Non seulement les lambdas, mais aussi les classes internes anonymes sont soumises à cette règle

Questions piégeuses.

Peut-on utiliser des objets mutables si la référence elle-même est effectively final ?

Oui, si la référence ne change pas, mais que l'objet référencé change — c'est permis. Par exemple,

List<String> list = new ArrayList<>(); list.add("A"); Runnable r = () -> System.out.println(list.get(0)); // OK list = new ArrayList<>(); // Si c'est le cas, une erreur de compilation se produira

Peut-on déclarer une variable comme final, puis modifier le contenu de l'objet ?

Oui. final se réfère à la référence, pas au contenu de l'objet. Modifier l'état de l'objet via la référence est autorisé. Par exemple,

final List<Integer> nums = new ArrayList<>(); nums.add(5); // OK nums = new ArrayList<>(); // Erreur

Peut-on utiliser des variables-arguments de méthode dans des lambdas ?

Oui, si elles sont également effectively final — c'est-à-dire non modifiées à l'intérieur de la méthode après initialisation.

Erreurs typiques et anti-patterns

  • Les variables utilisées dans une lambda ou une classe interne sont modifiées involontairement, ce qui entraîne des erreurs de compilation.
  • Confusion entre les références final et l'état des objets via ces références.
  • Essayer de contourner la restriction via des tableaux ou des objets holder entraîne des bugs subtils.

Exemple de la vie réelle

Cas négatif

Des variables dans une méthode sont accidentellement réécrites après la création de la lambda, ce qui empêche la compilation et entraîne une perte de temps pour en comprendre la raison.

Avantages :

  • Le code fonctionne comme prévu si les variables sont utilisées correctement.

Inconvénients :

  • Difficultés à analyser les erreurs si l'efficacité de final est violée implicitement.

Cas positif

Le développeur utilise une valeur incrémentable via AtomicInteger (ou un autre objet holder) qui conserve la référence et non la valeur, assurant le bon fonctionnement des lambdas, même si le compteur doit être modifié à l'intérieur de l'expression lambda.

Avantages :

  • Pas d'erreurs de compilation.
  • Il est clair que la valeur change.

Inconvénients :

  • Facile de se tromper lors d'accès multithread si des objets non sécurisés pour les threads sont utilisés.