Java 19 : le point sur les nouveautés

Hier est sortie la version 19 de Java. Pas de nouveauté a proprement parlé mais différent travaux en preview et incubation avance ! 🤓

Meilleurs Pattern Matching des Record

Exemple de code regroupant les nouveautés des JEPS 405 et 427.

La première intègre enfin le matching en profondeur sur les Record. Concrètement, si vous avez un Record qui est composé d'un Record vous pouvez directement extraire ses membres en profondeur. C'est très pratique pour un if car on évite de multiplier les lignes, mais ça l'est aussi pour un switch où on évitera d'imbriquer des switchs, on pourra garder un seul switch et valider tous les cas sur un seul niveau. On y gagne donc en lisibilité, même si je regrette toujours le choix d'avoir introduit le when pour définir les valeurs dans ce genre de cas…

record Color(int r, int g, int b){};
record Gradient(Color from, Color to){}


Function<Gradient, String> ifPatternMatching = (g) -> {
     // JEPS 405 enable nested Record Pattern
    if (g instanceof Gradient(Color(int fr, int fg, int fb),Color(int tr, int tg, int tb))) {
        return fr + "," + fg + "," + fb + " => " + tr + "," + tg + "," + tb;
    }
    return "nope";
}

Function<Gradient, String> switchPatternMatching = (g) -> switch (g) {
    case null -> "null"; // JEPS 427 enable null case
    // JEPS 405 enable nested Record Pattern for switch too
    // sorry about the verbosity of the when clause to match the values...
    case Gradient(Color(int fr, int fg, int fb), Color(int tr,int tg,int tb)) when fr == 0 && fg == 0 && fb == 0 -> tr + "," + tg + "," + tb;
    default -> "nope";
}; 

var g1 = new Gradient(new Color(0,0,0), new Color(255,255,255));
var g2 = new Gradient(new Color(255,255,255), new Color(0,0,0));

System.out.println(ifPatternMatching.apply(g1)); // => 0,0,0 => 255,255,255
System.out.println(switchPatternMatching.apply(g1)); // => 255,255,255
System.out.println(switchPatternMatching.apply(g2)); // => nope
System.out.println(switchPatternMatching.apply(null)); // => null

Nouvelle manière de gérer les tâches concurrentes

Je regroupe encore 2 JEPS, les 425 et 428. La première vient proposer un nouveau type de thread (les virtual thread) et la seconde une nouvelle manière de gérer le plusieurs tâches concurrentes en se basant sur les virtual thread.

Du côté des virtual threads, c'est très simple : on vient proposer des threads qui ne génèrent plus directement des threads au niveau CPU. Mais plutôt quelque chose qui se rapproche des goroutines (en Go) et des coroutines (en Kotlin). Des threads très légers géré par la JVM directement et qui vont utiliser des threads au bon vouloir de la JVM. On peut se dire qu'on perd en contrôle mais l'idée c'est plutôt de déléguer à la JVM une tâche qu'elle fait très bien : gérer la partie hardware pour l'exploiter au mieux en fonction de ce dont on a besoin.

Exemple tiré de la JEPS :

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}  // executor.close() is called implicitly, and waits

Comme avec les Executors classiques, on retrouve une factory, on a pas à s'occuper du tout de comment sera exécuté notre code, juste il le sera. Comme par définition les virtual thread se veulent léger, on peut en générer sans en grand nombre.

Qui dit gérer un grand nombre de tâches, dit qu'on va vouloir gérer les réponses des tâches pour rassembler les réponses et ce n'est pas toujours simple à faire. D'où l'idée de proposer une nouvelle API (attention, elle est en incubation donc sujette à de gros changement dans l'avenir).

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String>  user  = scope.fork(() -> findUser());
        Future<Integer> order = scope.fork(() -> fetchOrder());

        scope.join();           // Join both forks
        scope.throwIfFailed();  // ... and propagate errors

        // Here, both forks have succeeded, so compose their results
        return new Response(user.resultNow(), order.resultNow());
    }
}

Plutôt que créer un nouvel Executor comme plus haut, on va créer une instance de StructuredTaskScope, qui va gérer pour nous l'attente des résultats, et nous donner simplement des Future qu'on pourra traiter. L'idée plus que de révolutionner la gestion du multi-threading est de fournir un outil pour facilité son utilisation, en particulier pour ce qui est de gérer le fait qu'une des tâches peut échouer. En effet en cas d'échec on aura une gestion automatique de l'annulation de l'autre tâche.

Conclusion

Côté développeur, on a finalement pas grand-chose à se mettre sous la dent avec cette nouvelle version. La seule JEPS qui est intégrée sans drapeau est le support de l'architecture RISC-V (c'est une bonne nouvelle, mais ça ne va pas changer notre quotidien). On voit encore qu'il y a des gros travaux de fonds qui vont amener de belles choses dans le futur, mais ce n'est pas pour cette version 19.

Sources :