Fiche pratique sur l’inférence de type locale en Java 10 et versions ultérieures !
Stuart Marks
26 avril 2018
0 minutes de lecture
Bienvenue dans le premier numéro d’une nouvelle série de fiches pratiques que nous publierons sur le blog de Snyk. Nous vous proposerons des contenus à imprimer et à afficher pour vous aider à devenir de meilleurs développeurs. Pour cette première édition, publiée dans la foulée de Java 10, nous nous intéressons à l’inférence de type pour les variables locales, qui fait beaucoup parler d’elle. Vous pouvez télécharger la version PDF en cliquant sur l’image ci-dessus ou sur ce lien ! Le principe de l’inférence de type locale est très simple : remplacez le type explicite de la déclaration par le nouveau nom de type réservé « var », et le type sera inféré. On peut donc remplacer :
par :
Le type de outputStream sera alors inféré comme étant ByteArrayOutputStream. Attendez, vous voulez dire que Java autorise désormais le typage dynamique ? Absolument pas ! TOUTE l’inférence de type a lieu à la compilation, et les types explicites sont intégrés au bytecode par le compilateur. À l’exécution, Java reste aussi statique que jamais. Comme son utilisation est très simple, cette fiche pratique se concentre sur l’aspect le plus important de l’inférence de type locale : son utilisation concrète. Elle vous indique quand utiliser le typage explicite et quand envisager l’inférence de type.
Alors que je souhaitais rédiger cette fiche pratique, Stuart Marks, ingénieur JDK chez Oracle, a publié l’article idéal, qui présente des principes de codage et des conseils sur l’utilisation de l’inférence de type. Pour créer cette fiche, je me suis donc adressé directement à Stuart afin de voir si nous pouvions intégrer ses idées et les condenser dans une fiche pratique que les développeurs pourraient afficher et consulter au quotidien ! Je vous recommande vivement de lire l’article complet de Stuart : il en vaut vraiment la peine !
Principes
1. Lire du code > Écrire du code.
Que vous mettiez 10 minutes ou 10 jours à écrire une ligne de code, vous la relirez presque certainement pendant de nombreuses années. Le code ne sera facile à maintenir et à comprendre à l’avenir que s’il est clair, concis et, surtout, qu’il contient toutes les informations nécessaires pour en comprendre l’objectif. Le but est d’en maximiser la lisibilité.
2. Le code doit être clair à la lecture de son seul contexte local.
Intégrez autant d’informations que possible à votre code pour éviter que le lecteur doive parcourir différentes parties de la base de code afin de comprendre ce qui se passe. Vous pouvez notamment y parvenir grâce au nommage des méthodes et des variables.
3. La lisibilité du code ne doit pas dépendre de l’IDE.
Les IDE peuvent être excellents. Vraiment excellents ! Ils peuvent aider les développeurs à être plus productifs ou à coder avec davantage de précision. Le code doit rester lisible et compréhensible sans dépendre d’un IDE. On lit souvent du code en dehors d’un IDE. De plus, les IDE ne fournissent pas tous la même quantité d’informations au lecteur. Le code doit être explicite : il doit se comprendre de lui-même, sans l’aide d’outils.
À vous de choisir.
Choisir de donner un type explicite à une variable ou de laisser le compilateur Java le déduire implique un compromis. D’un côté, vous voulez réduire l’encombrement, le code répétitif et le formalisme. De l’autre, vous ne voulez pas nuire à la compréhension du code. La déclaration du type n’est pas le seul moyen de transmettre des informations au lecteur. Le nom de la variable et l’expression d’initialisation peuvent aussi en fournir. Pour chaque variable, nous devons prendre en compte tous les moyens disponibles avant de décider s’il est possible de ne pas indiquer explicitement son type.
Recommandations
1. Choisissez des noms de variables qui apportent des informations utiles.
C’est une bonne pratique en général, mais elle est d’autant plus importante avec var. Dans une déclaration var, le nom de la variable peut indiquer sa signification et son utilisation. Remplacer un type explicite par var devrait souvent s’accompagner d’une amélioration du nom de la variable. Il peut parfois être utile d’inclure le type de la variable dans son nom. Par exemple :
2. Limitez la portée des variables locales.
La limitation de la portée des variables locales est abordée dans Effective Java (3e édition), point 57. Cette recommandation est encore plus importante avec var. Le problème survient lorsque la portée de la variable est étendue, c’est-à-dire lorsque de nombreuses lignes de code séparent sa déclaration de son utilisation. Au fil de la maintenance du code, des modifications de type, entre autres, peuvent entraîner des comportements différents. Par exemple, passer d’une List à un Set peut sembler correct, mais votre code dépend-il de l’ordre des éléments plus loin dans la même portée ? Même si les types sont toujours définis statiquement, de subtiles différences entre des implémentations utilisant la même interface peuvent vous piéger. Plutôt que d’éviter var dans ces cas, il vaut mieux modifier le code pour réduire la portée des variables locales, puis les déclarer avec var. Considérez le code suivant :
Ce code comporte désormais un bogue, car les ensembles n’ont pas d’ordre d’itération défini. Toutefois, le programmeur devrait corriger ce bogue immédiatement, puisque les utilisations de la variable items se trouvent juste à côté de sa déclaration. Imaginons maintenant que ce code fasse partie d’une méthode volumineuse, où la variable items a une portée tout aussi étendue :
Ce bogue devient alors beaucoup plus difficile à repérer, car la ligne qui tente d’ajouter un élément à la fin de l’ensemble est trop éloignée de la déclaration du type pour que le problème saute aux yeux.
3. Envisagez var lorsque l’initialiseur fournit suffisamment d’informations au lecteur.
Les variables locales sont souvent initialisées à l’aide de constructeurs. Le nom de la classe instanciée est souvent répété en tant que type explicite à gauche de l’affectation. Si le nom du type est long, var permet de raccourcir le code sans perdre d’informations :
Il est également tout à fait raisonnable d’utiliser var lorsque l’initialiseur est un appel de méthode, comme Files.newBufferedReader(…) ou List stringList = List.of("a", "b", "c").
4. Utilisez var pour décomposer les expressions chaînées ou imbriquées à l’aide de variables locales.
Prenons un code qui reçoit une collection de chaînes et trouve celle qui apparaît le plus souvent. Il pourrait ressembler à ceci :
Ce code est correct, mais il serait plus lisible s’il était réparti sur plusieurs instructions. Le problème avec cette séparation en plusieurs instructions, comme illustré ici,
c’est que l’auteur a probablement renoncé à cette approche parce que le typage explicite donne un résultat extrêmement désordonné et détourne l’attention du code important. var permet d’écrire le code de façon plus naturelle, sans devoir payer le prix élevé de la déclaration explicite des types des variables intermédiaires :
On peut tout à fait préférer le premier extrait, avec sa longue chaîne unique d’appels de méthodes. Cependant, il est parfois préférable de décomposer les longues chaînes d’appels.
5. Avec les variables locales, ne vous préoccupez pas trop du principe « programmer contre l’interface ».
En Java, il est courant d’instancier un type concret, puis d’affecter l’objet à une variable de type interface. Par exemple :
Avec var, en revanche, le type concret est inféré à la place de l’interface :
Le code qui utilise la variable list peut désormais dépendre de l’implémentation concrète. Si l’initialiseur de la variable est modifié ultérieurement, le type inféré peut lui aussi changer, ce qui risque d’entraîner des erreurs ou des bogues dans le code qui utilise ensuite cette variable.
Ce problème est moins important si vous suivez la recommandation 2 : lorsque la portée de la variable locale est réduite, les risques que l’implémentation concrète « fuie » et ait des conséquences sur le code qui suit sont limités.
6. Soyez prudent lorsque vous utilisez var avec l’opérateur diamond ou des méthodes génériques.
var et la fonctionnalité « diamond » permettent tous deux d’omettre des informations de type explicites lorsque celles-ci peuvent être déduites d’autres informations présentes. Cependant, utilisés ensemble, ils peuvent omettre toutes les informations utiles dont le compilateur a besoin pour inférer correctement le type souhaité.
Considérez l’exemple suivant :
Les méthodes génériques tirent si bien parti de l’inférence de type qu’il est plutôt rare que les programmeurs fournissent des arguments de type explicites. L’inférence pour les méthodes génériques repose sur le type cible lorsqu’aucun argument de méthode réel ne fournit suffisamment d’informations sur le type. Dans une déclaration var, il n’y a pas de type cible : un problème semblable à celui de l’opérateur diamond peut donc se produire. Par exemple,
Avec l’opérateur diamond comme avec les méthodes génériques, les arguments réels transmis au constructeur ou à la méthode peuvent fournir des informations de type supplémentaires et permettre d’inférer le type attendu. Cela ajoute un niveau d’indirection, mais le résultat reste prévisible. Ainsi,
7. Soyez prudent lorsque vous utilisez var avec des littéraux.
L’utilisation de var avec des littéraux présente probablement peu d’avantages, car les noms de types sont généralement courts. Cependant, var peut parfois être utile, par exemple pour aligner les noms de variables.
Les littéraux booléens, caractères, longs et chaînes ne posent pas de problème. Le type inféré à partir de ces littéraux est précis, et le sens de var ne prête donc pas à confusion. En revanche, soyez particulièrement prudent lorsque l’initialiseur est une valeur numérique, notamment un littéral entier. Avec un type explicite à gauche de l’affectation, la valeur numérique peut être convertie silencieusement vers un type autre que int, avec élargissement ou réduction. Avec var, la valeur sera inférée comme étant un int, ce qui n’est pas toujours souhaité.
Conclusion
L’utilisation de var dans les déclarations peut améliorer le code en réduisant l’encombrement et en faisant ressortir les informations importantes. À l’inverse, utiliser var sans discernement peut dégrader le code. Bien utilisé, var permet d’améliorer un code de qualité en le rendant plus court et plus clair, sans nuire à sa compréhension. Lorsque vous utilisez var, demandez-vous si le code est devenu plus ambigu ou s’il reste clairement compréhensible sans recherches approfondies.
Téléchargez la fiche pratique dès maintenant !
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

