Stack Labs Blog moves to Dev.to | Le Blog Stack Labs déménage sur Dev.to 🚀

7 mai 2018 | Java | Olivier Revial

Java Fullthrottle : aperçu des prochaines versions du JDK

Temps de lecture estimé : 12 minutes

Lors d’une université du DevoxxFR 2018 animée par Rémi Forax et José Paumard, nous avons eu un aperçu détaillé de la roadmap de l’OpenJDK. Cet article a pour objectif de résumer les principales fonctionnalités des futures versions de Java, de la 10 à la 14… et plus 😀

Java accélère

Dès le début de la présentation, l’objectif de Java est clairement annoncé : “Java appuie sur la pédale” 🏎 ! Les speakers nous expliquent alors que le rythme des livraisons va accélerer pour passer à une version majeure tous les 6 mois. Ainsi, les prochaines livraisons sont programmées tous les 6 mois, comme le montre le tableau ci-dessous :

Version Date LTS ?
Java 8 Mars 2014 Oui
Java 9 Septembre 2017 -
Java 10 Mars 2018 -
Java 11 Septembre 2018 Oui
Java 12 Mars 2019 -
Java 13 Septembre 2019 -
Java 16 Mars 2021 -
Java 17 Septembre 2021 Oui

Vous remarquerez quelque chose dans ce tableau : seules les versions de Java 8, 11 et 17 sont des Long-Term Support, ce qui signifie qu’il est en général plus prudent de migrer ses applications vers l’une de ces versions LTS… et que donc vous n’aurez pas besoin de migrer votre code tous les 6 mois. Ouf ! 😌

Autre point important à noter : le contenu exact de chacune des versions n’est pas encore défini, ce qui change considérablement des précédentes versions. En effet, jusqu’à Java 9 les fonctionnalités étaient définies à l’avance, quitte à retarder la sortie de la nouvelle version. Ainsi Java 9 est sortie en juillet 2017 avec près d’un an de retard. A l’inverse, à partir de la version 10 les livraisons devraient être faites en temps et en heure, quitte à ne pas embarquer une fonctionnalité qui ne serait pas prête.

Inférence de type pour les variables locales, ‘var’, constantes dynamiques, classes de données, nouveau switch à base de lambda, interfaces fermées, nouvelles choses du coté des génériques, voilà le menu de ces futures versions. Mais voyons tout cela plus en détail.

Java 10

Inférence de type pour les variables locales avec var

Le mot-clé var, bien connu des développeurs Javascript et apparu plus récemment dans des langages utilisant la JVM (Scala ou Kotlin par exemple) était jusqu’à maintenant absent en Java.

Mais à partir de la version 10 vous devriez pouvoir écrire le code suivant :

var list = List.of("elem1", "elem2");
list.forEach(System.out::println);

Le mot-clé var permettra donc de ne pas spécifier systématiquement le type d’une variable à l’instantiation, laissant ainsi au compilateur la responsabilité de déduire le type de la variable. Le principal intérêt de cet ajout est de permettre une meilleure lisibilité de certains codes, notamment les boucles for, les try-with-resource et les classes anonymes. Par exemple, le code suivant :

Map<String, Path> paths = new HashMap<>();
// Map init
for(Path path : paths.values()) {
    try(Stream<String> lines = Files.lines(path)) {
        ...
    }
}

peut désormais s’écrire comme ceci :

var paths = new HashMap<String, String>();
// Map init
for(var path : paths.values()) {
    try(var lines = Files.lines(path)) {
        ...
    }
}

Attention cependant, l’inférence de type n’est pas possible pour les champs et méthodes de classe ainsi que dans les cas où le type ne peut pas être déterminé avec le contexte.

var a;                  // Fail : aucun type ne peut être déterminé
var a = null;           // Fail : null n'a aucun type
var lambda = x -> x;    // Fail
var a = null;           // Fail
var array = {1, 2, 3}   // Fail

class Foo {
    private var bar;                // Fail
    public int bar(var param) {}    // Fail
    public var baz(int param) {}    // Fail
}

Enfin, Rémi et José nous rappellent l’importance de bien nommer ses variables puisque le fait d’utiliser var enlève du contexte à la variable.

Java 11

La version 11 de Java, prévue pour septembre 2018, est la prochaine LTS. Par conséquent c’est aussi celle qui va “valider” les fonctionnalités apportées par les précédentes versions, mais aussi apporter un plus grand nombre de nouvelles fonctionnalités.

Lambdas utilisant var en paramètre

Une des nouveautés que devrait apporter Java 11 est la possibilité d’utiliser le mot-clé var en tant que paramètre de lambda, ce qui permettra d’alléger considérablement la syntaxe des lambdas. Il sera également d’utiliser des “modifiers” et des annotations au sein des lambdas, comme le montrent les exemples ci-dessous :

Avant :

BinaryOperator<Integer> fun = (Integer x, Integer y) -> x + y; 
BinaryOperator<Integer> fun = (var x, var y) -> x + y;                          // Utilisation de var à la place du type explicite 
BinaryOperator<Integer> fun = (final var x, final var y) -> x + y;              // Utilisation de modifiers 
BinaryOperator<Integer> fun = (@NonNull var x, @NonNull var y) -> x + y;        // Utilisation d'annotations 

Nestmates 🐦

Le problème

Pour comprendre ce que sont les nestmates, tentons une traduction du résumé de la JEP 181 de l’OpenJDK :

La JEP 181 introduit les nests, un contexte de contrôle d’accès qui permet de s’aligner avec la notion de types imbriquées en Java. Les nests permettent aux classes qui font partie d’une même entité logique mais sont compilées dans différents fichiers de classe, d’accéder aux membres privées de l’autre sans avoir besoin que le compilateur insère des méthodes d’accessibilité.

Euh, j’ai rien compris, c’est normal ? 🤔

Prenons cette classe :

public class Foo {
    private int i = 0;

    public class Bar {
        public int i() {
            return i;
        }
    }
}

Que se passe-t’il lorsque Bar essaye d’accéder à i, membre privé de la classe Foo ? Cela fonctionne : 1. Selon les règles du langage Java, une classe imbriquée (Bar ici) a accès à tous les membres de la classe englobante, y compris les membres privées (i dans notre cas). 1. A la compilation, le compilateur va générer deux fichiers de classes distincts : un fichier Foo.class et un fichier Foo$Bar.class. Seulement selon les mêmes règles de langage, deux classes dans deux fichiers distincts n’ont pas accès aux membres privées de l’autre classe. 1. Pour résoudre cela, le compilateur va rajouter un accesseur pour le champ i dans le bytecode de la classe Foo, qui sera nommé access$000()

Ok, mais si ça marche, pourquoi on doit changer quelque chose ? 🤔

La raison est simple : c’est un problème de sécurité potentiel. L’accesseur généré, qui n’a pas été défini par le développeur, peut potentiellement être utilisé par n’importe quelle classe, soit directement soit par réflexion, et même dans une classe non-imbriquée.

La solution

La solution à ce problème consiste à rajouter la notion de nestmates, c’est-à-dire un ensemble de classes qui partagent un mécanisme commun de contrôle d’accès. Pour résumer, les nested class (classes imbriquées) formeront désormais un nest (nid) dans lequel ils auront des droits d’accès spécifiques à leurs membres privés.

Le petit plus…

Comme José et Rémi nous le rappellent, ce bug existe depuis… la version 1.1 de Java !

Améliorations JVM

Lorsqu’on parle de Java, il y a certes les fonctionnalités liées au langage en lui-même, mais il y a aussi la JVM qui rentre en jeu pour exécuter notre code.

Sans rentrer dans trop de détails techniques, précisons simplement que Rémi nous a présenté les “Constant Lambda” et les “Constant Dynamic”, deux mécanismes qui permettront à terme une plus grand expressivité et de meilleures performances pour tous les langages et compilateurs utilisant la JVM.

Raw Strings

Les raw Strings apportent une fonctionnalité intéressante au language : la possiblité de créer des chaînes de caractères qui s’étendent sur plusieurs lignes. La règle est simple : > Une chaîne de caractères multi-lignes commence par n “backticks” (`) et termine par n “backticks”.

Ce nouveau système de création de chaînes améliore grandement la lisibilité, en particulier dans les deux cas suivants :

Une chaîne très longue

Le code suivant :

String s = "Voici une longue chaîne\n" +
                "de caractères\n" +
                "qui s'étend\n" +
                "sur plusieurs\n" +
                "lignes";

devient plus simplement :

String s = `Voici une longue chaîne
                de caractères
                qui s'étend
                sur plusieurs
                lignes`;

Un peu plus lisible non ?

Du code embedded

Qui ne s’est jamais retrouvé à écrire le test d’une API Json avec un code qui ressemble à celui-ci :

assertThat(s).isEqualTo("{\"name\":\"myName\",\"value\":\"myValue\"");

😱😰

Avec les raw String, il est désormais possible d’écrire quelque chose d’un peu plus lisible :

assertThat(s).isEqualTo(``{"name":"myName","value":"myValue"``);

Sympa, non ? 😌

Raw String API

En plus de la nouvelle écriture des raw String il y aura une API pour les manipuler :

Méthodes : * strip() : fait la même chose que trim() mais fonctionne avec les espaces UTF-8 * stripIndent() : enlève tous les espaces d’indentation * stripLeading(), stripTailing() : enlève les espaces de début/fin de ligne * stripMarkers() : enlève tout ce qui est à l’extérieur des marqueurs * lines() : créé un stream avec les différentes lignes de la chaîne multi-ligne

⚠️ Attention ⚠️

La seule manière de déclarer une chaîne de caractères vide est l’ancienne notation à base de guillemets. En effet, deux caractères backticks consécutifs signalent le début d’une raw String et pas une chaîne vide.

Java 12-13

Améliorer la syntaxe du switch

Rémi et José nous expliquent que le switch classique, tel qu’on le connaît depuis Java 7 (c’est-à-dire avec la gestion des expressions) a un nombre important de limites. Prenons ce switch en exemple :

Vehicle vehicle;
switch(text) {
    case "cas1":
    case "cas2":
        vehicle = new Car();
        break;
    case "cas3":
        vehicle = new Bus();
        break;
    default:
        throw new NotPossibleException();
}

On remarque déjà quelques limites : * Il est peu pratique à utiliser * Le “fall-through” rend le code illisible et sujet à de nombreuses régressions * Il ne gère pas le null * Il ne propose pas de syntaxe qui pourrait être plus intéressante

Voici quelques-unes des syntaxes étudiées par Rémi :

Enlever le “fall-through” et autoriser le null

Vehicle vehicle = switch(text) {
    case "cas1", "cas2":
        break new Car();
    case "cas3":
        break new Bus();
    null, default:
        throw new NotPossibleException();
}

Utiliser -> pour raccourcir la syntaxe

Note : Uniquement pour les expressions sur une ligne :

Vehicle vehicle = switch(text) {
    case "cas1", "cas2" -> new Car();
    case "cas3":
        System.out.println("Creating a bus !");
        break new Bus();
    null, default -> throw new NotPossibleException();
}

Attention, il ne s’agit pas de la même syntaxe que -> pour les lambdas, ce qui pourrait prêter à confusion. Une troisième syntaxe est alors envisagée :

Vehicle vehicle = switch(text) {
    case "cas1", "cas2": new Car();
    case "cas3":
        System.out.println("Creating a bus !");
        break new Bus();
    null, default: throw new NotPossibleException();
}

Le vote

Sur les conseils de Brian Goetz, les speakers nous proposent alors de voter pour la syntaxe que nous préférons :

Alors, quel est le résultat du vote ? …la réponse dans Java 12 !

Etendre le switch avec les types

Après nous avoir parlé invokedynamic et performance des implémentations, Rémi nous explique qu’ils aimeraient pouvoir étendre le switch pour qu’il soit capable d’accepter des types, pour aller plus loin que les types primitifs et les expressions String. Voici le type de switch qu’on pourrait peut-être retrouver dans une prochaine version de Java :

int computeWheels(Vehicle v) {
    return switch(v) {
        case Bike: 2; 
        case Car: 4; 
        case Bus: 6; 
        null, default: throw new NotPossibleException();
    }
}

Pour aller encore plus loin, les types pourraient même être nommés pour éviter des casts :

int computeTax(Vehicle v) {
    return switch(v) {
        case Bike: 10; 
        case Car car: 42 + car.passengers() * 10; 
        case Bus bus: bus.weight() * bus.wheels(); 
        null, default: throw new NotPossibleException();
    }
}

Mais tout cela pose de nombreuses questions, notamment en ce qui concerne l’implémentation, les performances, la facilité d’écriture des types eux-mêmes (sachant qu’on a pas toujours le contrôle des types que l’on manipule)… la réponse dans un an ou deux !

Record

S’il y a bien une chose frustrante en Java, c’est d’écrire des POJO ! Alors certes l’IDE génère toutes les méthodes pour vous (constructeur/getters/setters/equals/hashcode/toString), mais c’est pénible d’avoir une classe de 100 lignes qui n’a aucun code métier.

Lombok a été créé dans ce sens là, pour éviter d’avoir à écrire du code “boilerplate” :

@Data public class Player {
   private final String name;
   private int score;
}

Depuis peu, l’on peut également utiliser les Data Class de Kotlin :

data class Player(val name: String, val score: Int)

Mais si vous vous dites que ça serait chouette de pouvoir avoir un tel mécanisme en pur Java, vous serez content d’apprendre que c’est exactement ce à quoi les contributeurs réfléchissent avec la notion de record:

record Player(final String name, final int score)

Qui générera un POJO classique contenant toutes les méthodes !

Il ne sera pas possible de déclarer l’équivalent d’attributs de classe dans un record, mais il sera possible d’y déclarer des méthodes supplémentaires :

record Player(final String name, final int score) {
    public boolean hasPositiveScore() {
        return score > 0;
    }
}

Il sera enfin possible de définir des préconditions avant l’appel à des constructeurs par défaut (ou super), ce qui permettra notamment d’effectuer de la validation :

record Player(final String name, final int score) {
    public Player(String name, int score) {
        Preconditions.requiresPositive(score);
        default(name, score);
    } 
}

Java 14 🔮

⚠️ Bon, soyons clair : là on est sur du plus que conditionnel concernant l’arrivée réelle des fonctionnalités qui vont être décrites ci-dessous. Il s’agit plus de questions qui se posent plutôt que de fonctionnalités prévues dans les prochaines versions de Java.

Switch exhaustif ?

Une fois que les switchs pourront gérer les types, se posera la question des switchs exhaustifs qui permettraient de supprimer le pénible default. En effet, si on sait d’avance qu’une interface n’a que 3 implémentations possibles, on pourrait écrire un switch sans le default, simplement avec les 3 cas correspondant aux 3 implémentations. Pour que cela soit possible, il faut introduire la notion d’interface scellée, qui interdit d’autres implémentations de notre interface :

sealed interface Vehicle {
    record Bike(...) implements Vehicle();
    record Car(...) implements Vehicle();
    record Bus(...) implements Vehicle();
}

Value Type ?

L’idée derrière les value type est de pouvoir écrire une classe qui sera ensuite stockée en mémoire de la même manière qu’un type primitif, c’est-à-dire que la valeur sera écrite en mémoire directement plutôt que de passer par une référence.

Un value type pourrait s’écrire de cette manière :

value class Player {
    final String name;
    final int score;
    
    public static Player <create>() {}
    ...
}

Ce type particulier permettra de disposer de valeurs contigües en mémoire à la manière des types primitifs. Cela pourra améliorer les temps d’écriture et de parcours de manière non négligeable, par exemple pour des tableaux de valeurs de grande taille.

Conclusion

Vous l’aurez compris, il y aura de quoi faire dans les futures versions du langage Java ! Bien sûr, plus les versions sont éloignées dans le temps et moins les fonctionnalités décrites sont assurées d’être effectivement livrées.

Voici cependant un résumé des différentes fonctionnalités que nous avons abordé dans cet article :

Et comme toute bonne conférence Devoxx, les slides de la présentation sont disponibles sur Slidehsare et la vidéo sur Youtube