Publié par

Il y a 12 ans -

Temps de lecture 9 minutes

Des closures en Java

Le débat autour de l’introduction des closures dans le langage Java fait rage – avec toute la mesure et l’absence de pédanterie dont sait faire preuve notre profession sur ce type de sujet. Selon toute vraisemblance, les closures seront l’une des fonctionnalités phare de Java 7. Reste à savoir sous quelle forme.
Deux écoles ont émergé sur le sujet : la première, désignée BGGA (du nom de ses auteurs et promoteurs, Gilad Bracha, Neal Gafter, James Gosling et Peter von der Ahe), propose une extension syntaxique relativement complexe mais permettant d’introduire dans le langage tous les idiomes nécessaires à un support des closures similaire à celui disponible dans Ruby ou Smalltalk : function types, free variables, blocks, etc. ; la seconde désignée par l’acronyme CICE (pour Concise Inner Class Expressions) et soutenue par Joshua Bloch, Doug Lea et « Crazy Bob » Lee, propose plus modestement une simplification de la syntaxe java dans le but de désinhiber l’usage des Inner Class en lieu et place des closures.
L’occasion de faire ici un point sur ces deux approches et d’apporter, sinon une pierre à l’ouvrage, du moins des éclaircissements sur les termes du débat.

La définition la plus répandue des closures est la suivante (Neal Gafter fournit un historique des closures dans son blog [en]) :

A closure is a function that captures the bindings of free variables in its lexical context.

En substance, une closure est un bloc de code référençable, manipulant optionnellement des variables dites « libres » – libres en ce sens qu’elles sont définies non dans le bloc de code, ni de façon globale, mais par le contexte dans lequel le bloc est exécuté.

Un peu de code valant souvent mieux qu’un long discours, voici un exemple très simple d’utilisation des closures en Ruby (Ruby fait un usage intensif des closures, ce qui est probablement à l’origine de l’affection que lui porte ses utilisateurs). Cet exemple est tiré de l’article de Martin Fowler sur le sujet [en].

Imaginons que vous souhaitiez extraire d’une liste d’employés une sous-liste comprenant uniquement ceux qui sont cadres. Une implémentation possible en java est la suivante :

public static List managers(List employees) {
  List result = new ArrayList();
  for (Employee e : employees)
    if (e.isManager) result.add(e);
  return result;
}

En Ruby, la même fonction serait codée de la sorte :

def managers(employees)
  return employees.select {|e| e.isManager}
end

La méthode select est définie dans la classe Collection de Ruby. Elle prend en paramètre un bloc de code – la fameuse closure – , défini entre accolades. Si le bloc de code prend des arguments, ces derniers sont déclarés entre deux barres verticales (ce sont les variables « libres », dont la portée est définie par le contexte). La méthode select encapsule l’algorithmique : itération sur la collection, exécution du bloc de code pour chaque élément et construction d’une sous-liste contenant les éléments pour lesquels le bloc de code s’évalue à true.

Il existe bien sûr en Java un mécanisme assez similaire, autorisé par l’usage de classes anonymes imbriquées (Anonymous Inner Class, ou AIC).
Supposons que la classe List possède, symétriquement à sa contre-partie Ruby, une méthode select, prenant en paramètre une interface définie comme suit :

interface Filter(){
  public boolean accept(T e);
}

Alors le code java pourrait s’écrire comme suit :

public static List managers(List employees) {
  return employees.select(
    new Filter {
      public boolean accept(Employee e){
        return e.isManager();
      }
    }
  );
}

Les AIC sont l’avatar traditionnel des closures en java. Les API du JDK en font largement usage (pensez aux interfaces Runnable, Comparable, Callable ou TimerTask, qui fournissent des closures aux classes Thread ou Executor, ou encore permettent de configurer les collections). Les Design Patterns callbacks, factories, predicates ou stategies sont des candidats naturels à ce type de construction. Spring, en particulier dans son framework de templates, les exploite à très bon escient pour masquer le caractère fastidieux de certaines API, JDBC en particulier.

Pour autant, la programmation par closures, très répandue chez les rubyistes, est quasiment inexistante chez les programmeurs java. Le listing ci-dessus fournit une première explication : la syntaxe des AIC, verbeuse à souhait, est propre à décourager les meilleures intentions. Ensuite, le support des AIC dans Java comme mécanisme de closures souffre de sévères limitations : une fois instanciée, une AIC est un objet à part entière, doté d’une portée qui lui est propre, et partiellement aveugle au contexte qui l’a créé. En conséquence, les variables ou méthodes sont résolues dans le portée de ce nouvel objet.

Examinons, pour illustrer ces quelques lignes de Ruby :

i = 1;
1.upto(100) { |num| i *= num; }
puts i;

Ce code permet d’afficher le factoriel de 100. La variable i est définie en dehors de la closure et modifiée dans son corps. Le code java équivalent ne compilerait pas (à supposer qu’une API similaire soit disponible). En effet, pour qu’une variable externe soit accédée dans le corps d’une AIC, elle doit être déclarée final (la raison sous-jacente est que la variable est copiée dans le contexte de l’AIC). Elle ne peut donc être modifiée. Cela ne pose pas de difficulté si la variable est d’un type mutable (comme une liste ou un wrapper), mais peut se révéler problématique s’il s’agit d’un type immutable ou d’un value type (en particulier les String et les types primaires).

Voici un exemple type des contorsions nécessaires en java pour contourner cette contrainte :

final int[] numCompares = new int[1];
Arrays.sort(a, new Comparator() {
  public int compare(Integer i1, Integer i2) {
    numCompares[0]++;
    return i1.compareTo(i2);
  }
});
System.out.println(numCompares[0]);

C’est sur ce constat primordial que la proposition CICE a vu le jour. Son objet n’est pas d’altérer le langage java mais de rendre plus concise la syntaxe de création des AIC (en fait, un sous-ensemble de ces dernières, appelées single-abstract-method-types ou SAM, qui sont en substance des interfaces ne comprenant qu’une unique méthode).

Avec la syntaxe proposée par CICE, notre exemple initial serait réécrit de la sorte :

public static List managers(List employees) {
  return employees.select(
    Filter { return element.isManager();}
  );
}

Cette syntaxe dite concise est complétée par une altération des règles d’accès aux variables locales du bloc appelant (en particulier, celles déclarées explicitement publiques peuvent être assignées dans le corps de l’AIC).

Cette approche a bien sûr le mérite de la simplicité puisqu’ellle n’introduit aucun nouveau concept et ne nécessite pas la réécriture des API existantes ; elle pourrait de surcroît probablement être implémentée à l’aide d’une modification mineure du compilateur.

Pour certains, cependant, l’approche CICE ne fait qu’effleurer le sujet et ne propose qu’une mise à disposition minimaliste des closures en Java.

En effet, d’une façon générale, les AIC rompent le contexte d’exécution du code appelant et ne permettent pas de conserver la sémantique d’un nombre important de structures syntaxiques (celles que Neal Gafter appelle « lexically scoped language constructs ») :

  • noms des variables
  • noms des méthodes
  • noms des types
  • signification de this
  • noms des labels
  • référent d’une instruction break sans label
  • référent d’une instruction continue sans label
  • checked exceptions déclarées ou interceptées
  • référent d’une instruction return
  • et quelques autres plus exotiques (état d’assignation des variables, reachability, …)

La proposition BGGA est une extension du langage java permettant la mise en oeuvre des closures sans rupture de transparence. La syntaxe proposée par BGGA est loin d’être triviale, ce qui lui vaut de nombreuses critiques – ses contempteurs sont au demeurant souvent les partisans de CICE.

Sans entrer dans le détail (qui peut être consulté sur le site http://www.javac.info/), on retiendra les caractéristiques suivantes :

  • BGGA définit une syntaxe permettant de déclarer des closures littérales ressemblant à ça :
    {int x, int y > x+y}
    
  • BGGA définit également le concept des function type, dans lequel une fonction possède une liste d’arguments, un type de retour et une clause throws ; on peut instancier un function type à l’aide d’une closure littérale compatible
  • la transparence lexicale est intégralement garantie, tant pour l’association de variables que pour la résolution de noms ou les structures de contrôle (break, continue, return) – certaines closures peuvent cependant être marquées restricted et se comporter de façon identiques aux AIC
  • BGGA propose une grammaire simplifiée permettant d’exploiter les closures selon une syntaxe très proche de celle des structures de contrôle natives du langage – avec une telle syntaxe, la modification du langage pour introduire la seconde forme de boucle for aurait été superflue

Avec BGGA, l’algorithme suivant

lock.lock();
try {
  ++counter;
}
finally {
  lock.unlock();
}

deviendrait

withLock(lock, {=>
  ++counter;
});

dans la forme canonique et, dans la forme simplifiée :

withLock(lock) {
  ++counter;
}

Notre exemple initial pourrait être codé de la sorte (pour peu que l’API de Collection java se dote d’une méthode select appropriée) :

public static List managers(List employees) {
  return employees.select({Employee e => e.isManager()});
}

Comme évoqué plus haut, la médaille a un revers. Si côté client l’utilisation des closures BGGA semble relativement simple, la syntaxe risque de considérablement complexifier les API du JDK ou des frameworks désireux d’en tirer parti. Pour se donner une idée, voici le code de la fonction withLock utilisée plus haut :

public static 
T withLock(Lock lock, {=>T throws E} block) throws E {
  lock.lock();
  try {
    return block.invoke();
  } finally {
    lock.unlock();
  }
}

A l’instar des génériques, qui ont rendu presqu’illisible au commun le code source de certaines classes du JDK, une telle évolution syntaxique risque de dresser une barrière supplémentaire à l’apprentissage du langage java, et creuser encore davantage le fossé entre l’utilisateur d’API (Joe Java, comme disent certains) et le concepteur d’API. Le jeu en vaut-il la chandelle ?

Références (toutes en anglais) :
[1] La proposition BGGA, présentée par Neal Gafter aux Google TechTalks
[2] L’approche CICE
[3] Un très intéressant échange sur le site de Crazy Bob, partisan de CICE
[4] Deux articles parus sur developerWorks, le premier sur les closures en général, et le second plus spécifiquement sur le débat BGGA vs CICE.
[5] Un article plus ancien, sur l’approche traditionnelle des closures en java

Publié par

Commentaire

11 réponses pour " Des closures en Java "

  1. Publié par , Il y a 13 ans

    « … des génériques, qui ont rendu presqu’illisible au commun »
    Il ne faut pas exagérer.
    Les génériques sont un gros plus. Ca manquait vraiment.
    En plus ça s’intègre assez simplement avec le code java existant.

  2. Publié par , Il y a 13 ans

    Je suis moi aussi un fan des génériques… Simplement, je ne peux que constater que leur introduction (et malgré leur excellent design et leur très bonne intégration dans les API existantes) a augmenté le coût d’apprentissage du langage. Et a créé une distance entre les utilisateurs d’API, qui peuvent bénéficier très simplement des génériques :

    List<String> l = new ArrayList<String>();

    et les créateurs d’API, qui maîtrisent les subtilités de la syntaxe et des wildcards pour écrire ce type de code (extrait de java.util.Collections) :


    public static <T extends Comparable<? super T>> void sort(List<T> list) {
       ...
    }

  3. Publié par , Il y a 13 ans

    Un gros avantage de Java réside dans sa cohérence pédagogique et sa lisibilité. Rappelez-vous, les pointeurs et autres malloc, efficaces pour gérer la mémoire, mais tellement peu developer-friendly !
    Objectifs ? Courbe d’apprentissage réduite, amélioration de la maintenabilité, …

    Je suis d’accord pour dire que les Generiques manquaient : les (cast) à tour de bras n’étaient pas dans la philosophie de simplicité recherchée par Java. En revanche, le code généré devient de plus en plus illisible… et nuit aux objectifs précités.

    L’introduction des closures me parait une erreur: oui, cela réduit le nombre de lignes de code. Non, cela ne simplifie pas la lisibilité. Non, cela ne favorise pas Java.

    Je pense que les closures ne sont qu’un signe de faiblesse de Java qui succombe au lobbying des supporters de Ruby ou des outils de scripting qui permettent de coder souvent en moins de lignes… mais au détriment de la lisibilité, de la simplicité.

    A moins de ne vouloir chercher l’ajout d’une nouvelle rubrique dans nos documents de normes de développement : « Fonctionnalités Java à ne pas utiliser : closures,… » ?

    Yann

  4. Publié par , Il y a 12 ans

    Guillaume : je pense que le jeu en vaut la chandelle, il y a les utilisateurs de base et les utilisateurs avancés. Il en est de même avec le framework de collections de Sun, je constate que la collection la plus populaire est « List » alors que parfois un « Set » serait plus approprié. Pourquoi ? Je soupçonne la relative complexité de redéfinir « hashCode » et « equals » d’en être à l’origine (au moins en partie, des fois l’origine est plus inquiétante). Finalement Sun aurait pu se contenter des tableaux et de la classe utilitaire « Collections » (consacrée aux tableaux dans ce cas), mais heureusement que Sun a su résister et publier ce framework.

    Yann : le code Java produit par le programmeur n’a pas attendu les génériques pour être illisible, il en sera de même avec les closures ; c’est le programmeur qui rend un code illisible.

    Un « Google Tech Talk » donné par Neal Gafter sur les closures : http://www.youtube.com/watch?v=0zVizaCOhME

  5. Publié par , Il y a 11 ans

    Je ne comprends pas, si on veut introduire les closure dans Java pourquoi ne pas faire tout simplement ce que fait déjà le langage Lisp ?

  6. Publié par , Il y a 11 ans

    Bonsoir Arnault,
    Par ce que les ‘{‘, c’est nettement plus cool que les ‘(‘ :)

    Plaisanterie mise à part, cet article à été rédigé en 2007, à l’heure actuelle les closures ont été descopées de Java 7. (Faute d’avoir trouvé un consensus sur leur implémentation). Elles sont en revanche disponibles dans Groovy.

  7. Publié par , Il y a 7 ans

    Lambda expression allows the compiler to infer the type of the variable based on the context

  8. Publié par , Il y a 6 ans

    Qu’est ce qu’ils sont gavant à chercher à factoriser des bouts de codes en s’inspirant d’autres langages. Non seulement dans la répétition, le gain en temps de saisie s’annule vite pouvant même, en fin de compte, devenir une perte de temps; mais en plus, ça n’améliore pas la lisibilité du code, ça a même tendance à le déstructurer.

Les commentaires sont fermés.

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.