Ce nouvel article présente les prémices de l’inférence de type proposée avec Java 10, une fonctionnalité qui va grandement apporter à la verbosité du langage (mais également à la maintenabilité).
Qu’est ce que l’inférence de type ?
L’inférence de type est un concept en programmation, permettant au compilateur ou à l’interpréteur de déduire automatiquement le type d’une variable en fonction du contexte.
Le diamond operator, arrivé avec Java 7, était déjà une première forme d’inférence de type appliquée aux paramètres génériques List<Integer> l = new ArrayList<>();.
Dans le cas de Java 10, l’objectif est de fournir la possibilité d’inférer les variables locales à l’aide de l’expression d’initialisation. Cette fonctionnalité est déjà disponible dans beaucoup de langages, avec des syntaxes variables : var en C# ou en Scala, auto en C++, def en Groovy …
L’inférence de type en Java 10
Java 10, avec la JEPS 286, apporte donc l’inférence de type au langage Java. var, introduit spécifiquement pour cet usage, remplace le type pour demander l’inférence du compilateur :
1 2 3 4 |
var i = 28; // int var str = "Salut"; // java.lang.String var list = new ArrayList<String>(); // ArrayList<String> var stream = list.stream(); // Stream<String> |
La syntaxe choisie est donc proche de celle qu’on peut trouver en C#. var n’est pas un mot-clef à proprement parlé mais un reserved type name, c’est à dire que l’identifieur n’est réservé que pour les noms de types, ce qui permet de laisser l’identifieur disponible pour des noms de fonctions, de variables etc… Le code suivant est donc valide :
1 2 3 4 5 6 7 |
package var; public class Var { public static void var() { var var = 25; // var est un int. } } |
Ce qu’il faut comprendre par là c’est que var n’est réservé que de façon contextuelle, ce qui est un bon point en terme de rétrocompatibilité et l’usage général de nos jours : var est également un mot-clef contextuel en C# par exemple et même si auto est un mot-clef classique en C++ (puisqu’il est directement hérité du C), des mots-clef contextuels ont été ajoutés ( override ou final notamment) dans les dernières normes.
De plus, comme l’indique le papier JEPS 286, il a été décidé qu’il n’y aurait pas de nouveau mot-clef introduit pour les types immuables (il faut comprendre ici les types finaux) comme le font Scala ou Kotlin en différenciant var et val , respectivement pour les types mutables et immuables. Le spécificateur final peut donc être ajouté à une déclaration utilisant var comme on peut s’y attendre : final var str = "Hello";.
Notez que l’inférence va toujours choisir le type le plus spécialisé. Dans l’exemple précédent ( var list = new ArrayList<String>();), c’est ArrayList<String> qui est inféré et non pas List<String> , Collection<String> ou même Object . Dans les cas où vous avez besoin de travailler sur une interface précise, spécifiez simplement le type :
1 |
List<String> list = new ArrayList<>(); |
Cette nouvelle inférence de type pour les variables locales est la bienvenue, même si celle-ci souffre de quelques limitations (mais on peut espérer que certaines seront levées par la suite).
Les limitations de Java 10
Le JEPS 286, présente une liste de messages d’erreur liés à l’utilisation non conforme de l’inférence de type que je vous propose d’étudier un petit peu.
1 2 3 4 5 |
Main.java:81: error: cannot infer type for local variable x var x; ^ (cannot use 'val' on variable without initializer) |
Il n’est pas possible d’inférer le type d’une variable sans initialiseur. Ce n’est pas très étonnant, le mieux que le compilateur pourrait faire ici serait d’inférer le type Object , communs à tous les types objet, et de faire de l’autoboxing pour les types primitifs. La décision d’interdire l’inférence de type dans ce cas est tout à fait compréhensible.
Un peu plus discutable, il n’est pas possible d’utiliser var lorsque l’initialiseur est null :
1 2 3 4 5 |
Main.java:83: error: cannot infer type for local variable g var g = null; ^ (variable initializer is 'null') |
Inférer Object ne semble pas une idée dénuée de sens, mais à titre personnel je trouve ça assez cohérent d’avoir besoin d’un initialiseur pour inférer quoi que ce soit.
Ensuite, l’inférence de type n’est pas possible pour les tableaux :
1 2 3 4 |
Main.java:199: error: cannot infer type for local variable k var k = { 1 , 2 }; ^ (array initializer needs an explicit target-type) |
Dans cet exemple, il est facile de voir que le type déduit devrait être int[] . Cela dit, d’autres cas plus complexes tels que var t = {'a', 28.0, 12}; , auraient demandé d’utiliser des règles de conversion implicite ce que Java a toujours fuit comme la peste. Mais bon, ils auraient au moins pu laisser l’inférence fonctionner lorsque tous les éléments du tableau sont du même type, comme ici, et laisser les cas complexes comme des erreurs… C’est un peu décevant mais encore une fois, l’inférence de type en est ici à sa première version et il n’est pas exclu que les choses s’améliorent par la suite.
Voyons un peu la suite :
1 2 3 4 5 |
Main.java:82: error: cannot infer type for local variable f var f = () -> { }; ^ (lambda expression needs an explicit target-type) |
Les lambdas Java n’étant que des implémentations déguisées d’interfaces (et ne sont pas des first class citizens), il n’est pas possible d’inférer un type sans que l’interface en question soit clairement indiquées par le programmeur. Ça fait encore une fois regretter le design des lambdas dans Java, mais l’inférence n’est pas possible sans une refonte du système. Naturellement, il en va de même pour les références de méthodes (method references) :
1 2 3 4 |
Main.java:195: error: cannot infer type for local variable m var m = this::l; ^ (method reference needs an explicit target-type) |
C’est dommage, mais on paie ici les décisions de design prises avec Java 8 et il n’y a donc pas vraiment d’étonnement à ces inférences impossibles.
Le dernier point en revanche m’a fait bondir :
1 2 3 4 5 |
Main.java:84: error: cannot infer type for local variable c var c = l(); ^ (inferred type is non denotable) |
Hein ?! Mais pourquoi ? Ici, l(); semble être un appel de méthode et il n’y a à priori aucune raison valable pour qu’il ne soit pas possible de déduire le type de retour d’une méthode… En plus, l’exemple donné en début du papier montre un cas qui marche bien ( var stream = list.stream(); ).
J’ai donc fait le test et je confirme que Java 10 supporte l’inférence à partir d’un appel de fonction (heureusement). J’avoue ne pas trop comprendre du coup l’idée de nous montrer ce message d’erreur.
Certaines de ces limitations sont compréhensibles et vont certainement rester comme ça ( var i; , var o = null; ), d’autres sont liées à des lacunes héritées des précédentes versions du langage (coucou les lambdas !) et d’autres (comme pour les tableaux) sont très discutables. Cela étant, Java 10 propose bel est bien une inférence de type locale qui fonctionne.
On parle bien d’inférence de type locale ici. Ne vous attendez pas à pouvoir l’utiliser par exemple en retour de fonction, comme on avait pris l’habitude en C++ :
1 2 3 4 |
var positive(int i) // Error : java: 'var' is not allowed here. { return i >= 0; } |
Pour autant, il n’est pas exclu que l’inférence de type soit étendue par la suite.
Voilà donc un petit aperçu de l’inférence de type des variables locales de Java 10. Je vais certainement faire un autre article concernant les modifications qu’apportera Java 11 à cette fonctionnalité et peut-être aussi sur les guidelines qui ont été publiées par Stuart W. Marks dans ce document pour accompagner l’ajout de cette fonctionnalité au langage. A bientôt !
You must log in to post a comment.