Java 10 – Guidelines pour l’usage de var

Bonjour à tous ! Voici la suite de l’article Java 10 – le début de l’inférence de type qui se concentre cette fois sur le document Style Guidelines for Local Variable Type Inference in Java publié par Stuart W. Marks pour accompagner la sortie de l’inférence de type des variables locales (LVTI) introduite avec Java SE 10.

Je vous propose donc d’étudier les principes et les guidelines qui sont présentés dans ce document, ainsi que les exemples illustratifs.

Les principes

Stuart W. Marks indique que l’apparition de la LVTI a été quelque peu controversée, certains étant inquiets à propos de la possible perte de lisibilité que pourrait apporter un abus de l’usage de var.

Il pense qu’il n’existe pas de règle universelle concernant l’utilisation ou non de var  dans un morceau de code et que le contexte est important pour décider d’inférer le type ou de l’indiquer explicitement.

Voyons donc dans un premier temps les quatre principes de programmation qu’il énonce. Rappelons tout de même que comme tout principe, ils disposent de leurs exceptions.

P1. Reading code is more important than writing code

Traduction : “Lire le code est plus important que l’écrire”.

En effet, c’est un principe communément admis qu’un morceau de code sera généralement plus souvent lu qu’il ne sera écrit. Lorsqu’on écrit le code, on a souvent une vision d’ensemble du contexte d’utilisation et des dépendances de ce  morceau de code, contrairement aux futurs relecteurs (ce qui inclut bien sûr l’auteur du code lui même).

Ecrire du code concis et court est souvent une bonne chose, mais abuser de la concision peut rendre au final le code plus difficile à lire.

P2. Code should be clear from local reasoning

Traduction : “Le code devrait être compréhensible à partir d’un raisonnement local”.

Ce qu’il faut comprendre ici, c’est que le lecteur devrait être capable de comprendre un morceau de code à partir de ce seul code, sans devoir aller rechercher les définitions des méthodes impliquées par exemple.

P3. Code readability shouldn’t depend on IDEs

Traduction : “La lisibilité du code ne devrait pas dépendre d’un IDE (Environnement de développement intégré)”.

Java est un langage qui est très rarement utilisé sans IDE : auto-complétion, importations automatiques, générateurs de code … sont autant d’outils pratiques et nécessaires au confort de la programmation Java, si bien que certains sont parfois presque indispensables. C’est donc assez drôle de parler de ne pas dépendre des IDEs pour un langage aussi dépendant de ceux-ci (et encore je ne cite pas les outils externes, tels que les créateurs d’UI pour Android, les gestionnaires de ressources ou encore le cas de Java EE).

Cela étant, ici il n’est question que de lisibilité et il faut admettre qu’un morceau de code est assez souvent lu sans IDE, lancer un IDE peut parfois être long et il est fréquent de se contenter d’un simple éditeur de texte pour lire un petit bout de code plutôt que de s’embêter à démarrer IntelliJ, Eclipse ou que sais-je.

L’idée ici est que malgré la facilité qu’a un IDE à donner le type d’une variable inférée (et ce même en cas d’imbrications d’inférences complexes), l’usage de var  devrait être évité lorsque la compréhension du code nécessite de telles fonctionnalités de la part de l’éditeur de texte.

P4. Explicit types are a tradeoff

Traduction : “Les types explicites sont un compromis”.

L’inférence de type vise, entre autre, à éviter la redondance d’informations concernant le type d’une variable. L’usage de type explicit est à réserver aux cas où l’expression d’initialisation et le contexte ne suffisent pas à la compréhension du code.

Les guidelines

À la suite de ces principes généraux, une liste de sept guidelines est présentée concernant l’inférence de type des variables locales.

G1. Choose variable names that provides useful information

Traduction : “Choisissez des noms de variable qui apportent de l’information utile”.

Cette règle générale est d’autant plus importante lorsque le type explicite est remplacé par var . Le premier exemple proposé est le suivant :

Ici, le nom de variable apporte suffisamment d’informations sur le type de la variable.

L’auteur fait un parallèle sur la notation hongroise, qui consiste concrètement à encoder le type d’une variable dans son nom. La notation hongroise a eu son heure de gloire il y a un moment, et si on retrouve aujourd’hui quelques restes, comme le préfixe m_  pour les membres qui est parfois utilisé (et qui rappelons le, n’est pas conseillé en Java) et qu’il y a des cas où celle-ci peut améliorer la lisibilité du code, on préférera utiliser un nommage indiquant plutôt le rôle de la variable plutôt que son type exact.

Le second exemple proposé illustre cela en proposant le renommage de result, qui n’apporte aucune indication ni sur le type, ni sur le rôle de la variable, en customers, précisant ainsi qu’il s’agit d’une liste de consommateurs :

G2. Minimize the scope of local variable

Traduction : “Minimiser la portée des variables locales”.

Encore une fois, une bonne pratique classique. Les variables devraient être déclarées au plus tard, de manière à rapprocher au maximum leur déclaration de leur utilisation et ainsi limiter les risques d’erreurs et la surface de code à étudier lors de modifications.

Le document présente un bout de code utilisant une ArrayList avec l’ajout d’un élément en dernier :

Ici, MUST_BE_PROCESSED_LAST est l’élément qui doit être impérativement traité en dernier. Passer d’une ArrayList à un HashSet (qui n’apporte donc pas la garantie que cet élément soit parcouru en dernier) introduit un bug dans le programme.

Si 100 lignes de code séparent la déclaration d’items, plutôt que 3, repérer le bug sera bien plus difficile. Pour le coup j’ai du mal à saisir le rapport avec var  dans ce cas précis, puisque le problème est strictement le même sans inférence de type. Mais c’est toujours bien de rappeler ce principe de programmation.

G3. Consider var when the Initializer provides sufficient information to the reader

Traduction : “Utilisez var lorsque l’initialiseur (rhs) donne suffisamment d’informations au lecteur”.

Il est assez évident que lorsqu’on initialise à partir de l’opérateur new, le type est déjà clairement indiqué et l’utilisation de var devient alors évidente :

De la même façon, lorsque la variable est initialisée via une factory par exemple, l’usage de l’inférence de type est tout indiqué :

G4. Use var to break up chained or nested expressions with local variables

Traduction : “Utilisez var pour briser des expression chaînées ou imbriquées avec des variables locales”.

Le chaînage de méthode est un mécanisme classique permettant de simplifier l’écriture du code (il s’agit d’ailleurs une forme d’inférence de type puisque le compilateur infère le type de retour de chaque méthode sans qu’on ait à le nommer).

Parfois, lorsque le chaînage devient trop long ou simplement trop difficile à lire, il est possible de séparer l’expression en deux pour améliorer la lisibilité. Stuart Marks conseille donc d’utiliser l’inférence de type pour les variables locales utilisées pour séparer le chaînage :

G5. Don’t worry too much about “programming to the Interface” with local variables

Traduction : “Ne vous inquiétez pas trop de la déduction du type concret pour les variables locales”.

Un des problèmes des var avec les types génériques est que le type concret est inféré par le compilateur :

Cela va à l’encontre du principe “programming to the interface”, qui préconise de choisir l’interface la plus générale possible de manière à éviter de créer une dépendance avec une implémentation particulière si on utilise des méthodes de ce type concret (ou qu’on appelle des méthodes qui requièrent ce type).

Dans ce cas précis, la variable est de type ArrayList, alors qu’on aurait tout à fait pu se contenter de List (voir de Collection), et ça implique donc un risque de dépendance.

Cela étant, var n’est utilisable que pour les variables locales, le problème est donc bien moins important que pour des membres de classes ou des arguments / retours de fonctions. D’autant plus que la guideline G2 demande de réduire au maximum la portée des variables. Le problème est donc présent mais restera facile à corriger si on brise la compatibilité.

G6. Take care when using var with diamond or generic methods

Traduction : “Attention à l’utilisation de var en combinaison avec le diamant ou les méthodes génériques”.

L’opérateur diamant est une autre forme d’inférence de type en Java, il déduit le type générique à partir du type cible. Si il est utilisé en combinaison de var, n’ayant plus de type explicite, il va se contenter de choisir le type le plus spécifique applicable, qui sera le plus souvent Object :

Le problème est semblable pour les méthodes génériques, qui utilisent le type cible si les arguments ne fournissent pas assez d’informations sur le type à choisir. Avec var, encore une fois, il n’y a plus d’informations et on risque d’obtenir un type trop général :

L’utilisation combinée de  var avec l’opérateur diamant ou les méthodes génériques requiert donc un peu d’attention pour éviter de se retrouver avec les mauvais types génériques.

G7. Take care when using var with literals

Traduction : “Faites attention avec les littéraux en utilisant var”.

Lors de l’utilisation de var, le choix du type littéral est particulièrement important. Dans cet exemple, le type des variables est inféré à partir des types des littéraux des initializers :

byte  ou short  ne disposant pas de type littéral à part entière, deux solutions sont possibles : préciser le type explicitement ou utiliser un cast explicite :

Le document ne mentionne pas la seconde possibilité, qui a l’avantage d’aligner les noms de types. Peut-être que le cast explicite a un défaut qui m’échappe.

Exemples

En fin de document, Stuart Marks propose deux exemples où selon lui, l’usage de var est approprié.

Le premier correspond à la fonction removeMatches , qui enlève au maximum max éléments respectant le prédicat matches :

Ici, le type des variables iterator et entry  est particulièrement lourd, en plus d’être assez inutile à préciser, l’important étant qu’on a à faire à un itérateur et à une entrée de la map, ce qui est déjà clairement indiqué par les noms de variables.

L’usage de var est donc tout à fait justifié, ce qui pour le coup rend le code beaucoup plus facile à lire :

Le second exemple correspond à la lecture et le retour d’une seule ligne depuis une socket, profitant du try-with-resources :

Ici, le type est répété à chaque déclaration de variable. Dans le premier cas l’initializer est un getter, et dans les deux seconds un appel à new, probablement les deux cas où l’usage de var est le plus indiqué :

Conclusion

Cette liste de guidelines pour l’utilisation de  var offre donc quelques conseils intéressants et des exemples concrets.

Cet article se concentre principalement sur la lisibilité du code mais je pense qu’il est important de rappeler que l’inférence de type est également un atout pour la maintenabilité en profitant de l’escalade d’inférence :

Que se passe-t-il se subitement, on se rends compte que float ou même int était plus approprié ? Dans le premier cas, on se retrouve obligé de modifier chaque déclaration de variable intermédiaire. Avec l’usage de var, tout ce qu’on a à faire c’est modifier le littéral d’initialisation de startValue  : par exemple var startValue = 100.f .

Si  Util.doSomeComputation  et Util.doSomeOtherComputation  sont des méthodes génériques, pas de problème, le paramètre générique sera déduit depuis le type de l’argument et elles retourneront ce même type.

De la même facon Math.abs  possède des surcharges pour les types double, float, int et long, et la déduction fonctionnera donc correctement.

L’usage de var est donc profitable pour la maintenabilité, il n’y a pas de risque d’oublier une  déclaration à modifier puisque tout se fait automatiquement.

Alors bien sûr, tout n’est pas parfait. Si on utilise le type byte , la fonction Math.abs va choisir la surcharge int et retourner un int, le résultat final sera donc un int au lieu d’un byte. Effectivement, la méthode Math.abs a été créée avant l’ajout des generics dans Java SE, de plus, les generics ne sont pas aussi souples que d’autres mécanismes de programmation génériques (comme les templates) et l’inférence de type avec var est réservée aux variables locales. Cela étant sans inférence de type le problème persiste et la version avec var reste plus simple et plus rapide à maintenir le plus souvent.

Ces guidelines sont donc assez éloignées de l’idée du Almost Always Auto qu’avait énoncé Herb Sutter pour l’inférence de type en C++, mais Java n’apporte pas tous les outils nécessaires pour vraiment profiter de ce même type d’inférence (inférence des arguments et des retours de fonction, decltype, les typedef dans la portée d’une classe générique…). Cela étant, Java n’est encore qu’à ses débuts de l’inférence de type avec var, et les objectifs de cet ajout étaient simplement l’inférence de type locale.

%d bloggers like this: