logo le blog invivoo blanc

Les nouveautés de Java 19

16 janvier 2023 | Java | 0 comments

Java 19 est disponible depuis le 20 septembre 2022. C’est une version non LTS (Long-term support). Cette release contient une prévisualisation du projet LOOM tant attendu, qui introduit un nouveau modèle de concurrence légère à java.

Vous pouvez télécharger OpenJDK JDK 19.0.1 ici.

Le contenu de cette release :

JEP 427 : Pattern Matching pour le switch (3éme prévisualisation)

JEP405 : Record Patterns (prévisualisation)

JEP 422 : Linux/RISC-V Port

JEP 424 : API de mémoire et de fonctions étrangères (prévisualisation)

JEP 426 : API vectorielle (4éme incubation)

JEP 425 : Les threads virtuels (prévisualisation)

JEP 428 : La concurrence structurée (incubation)

Toutes les JEP de cette nouvelle version sont soit en prévisualisation soit en incubation. Cela signifie que vous devez ajouter les options suivantes dans le fichier pom.xml de votre projet, afin de pouvoir manipuler les nouveautés de java 19.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <source>19</source>
        <target>19</target>
        <compilerArgs>
            <arg>--enable-preview</arg>
            <arg>--add-modules=jdk.incubator.concurrent</arg>
            <arg>--add-modules=jdk.incubator.vector</arg>
        </compilerArgs>
        <compilerVersion>19</compilerVersion>
        <source>19</source>
        <target>19</target>
    </configuration>
</plugin>

Pattern Matching pour le switch (3ème prévisualisation)

La JEP 406 introduit  le « Pattern Matching » pour le switch,pour la première fois, dans le jdk17. Grâce à cette JEP on a la possibilité de faire un switch sur le type d’une variable et même de traiter le cas où l’objet est « null ». Il y a eu également l’introduction d’un nouveau pattern dit « guarded pattern » qui permet d’améliorer la lisibilité du pattern switch. Le « guarded pattern » est de la forme « p && e », où « p » est un pattern et « e » est une expression booléenne.

Ci-dessous un exemple de code avec java 17 sans utiliser le « guarded pattern » :

switch (myObject) {
    case null -> System.out.println("null!");
    case Trade trade -> {
        if (trade.legalEntity != null) {
            System.out.println("Trade with LegalEntity");
        } else {
            System.out.println("Trade without LegalEntity");
        }
    }
    default -> System.out.println("Default ...");
}

Et voici le même exemple en utilisant le « guarded pattern »

switch (myObject) {
    case null -> System.out.println("null!");
    case Trade trade && trade.legalEntity != null -> System.out.println("Trade with LegalEntity");
    case Trade trade -> System.out.println("Trade without LegalEntity");
    default -> System.out.println("Default ...");
}

Dans notre exemple, le « guarded pattern » est représenté par : « Trade trade && trade.legalEntity != null ».

Java 19, quant à lui, modifie la syntaxe de ce « guarded pattern » et introduit le mot clé when. La nouvelle syntaxe s’écrit alors de la forme suivante : « p when e » où « p » est un pattern et « e » est une expression booléenne.

Avec java 19, notre « guarded pattern » s’écrit alors de la manière suivante « Trade trade when trade.legalEntity != null », et notre exemple devient :

switch (myObject) {
    case null -> System.out.println("null!");
    case Trade trade when trade.legalEntity != null -> System.out.println("Tread with LegalEntity");
    case Trade trade -> System.out.println("Tread without LegalEntity");
    default -> System.out.println("Default ...");
}

A noter que le mot clé « when » est un « contextual keyword ». Si vous avez une variable « when » dans votre « case », cela ne posera aucun problème. Votre code fonctionnera ! On pourra donc écrire :

case Trade when when when.legalEntity != null

Record Patterns (1ére prévisualisation)

Les records font partie du standard java depuis la version 16 du JDK (JEP 395). Un record est une classe immutable qui ne nécessite que les types et les noms des champs. Pour en savoir plus à ce sujet, nous en parlons ici.

L’exemple ci-dessous présente deux records que nous utiliserons au cours des exemples suivants.

record LegalEntity(int id, String code, String asset) {}
record Trade(int contractId, String portfolio, LegalEntity legalEntity) {}

Supposons que notre code teste l’instance d’un objet avant d’appeler une méthode :

1. Sans le record patterns : on écrit notre code de la manière suivante :

if (myObject instanceof Trade trade) {
    process(trade.contractId, trade.portfolio, trade.legalEntity.asset);
}

2. Avec le record patterns :  on gagne en lisibilité et nous pouvons écrire notre code de la manière suivante :

if (myObject instanceof Trade(int contractId, String portfolio, LegalEntity legalEntity)) {
    process(contractId, portfolio, legalEntity.asset);
}

Comme vous pouvez le constater, il est possible d’utiliser les variables locales déclarées dans le constructeur dans votre code.

Record Patterns avec des objets imbriqués

La vraie force du « record pattern » est de pouvoir l’utiliser avec des objets beaucoup plus complexes. La classe « Trade » de notre exemple contient la classe « LegalEntity » comme objet imbriqué. On peut donc écrire ça comme suit :

if (myObject instanceof Trade(int contractId, String portfolio, LegalEntity(int id, String code, String asset))) {
    process(contractId, portfolio, asset);
}

En conclusion, si nous disposons d’objets imbriqués, nous pouvons les déconstruire à l’aide de leurs constructeurs respectifs. Nous pourrons ainsi utiliser dans notre code les variables locales déclarées dans notre « record pattern ».

Linux/RISC-V Port

« RISC-V » est une architecture de jeu d’instructions RISC, libre et open source (instruction set architecture ou ISA). Conçue à l’origine à l’université de Californie, elle est maintenant développée en collaboration sous le parrainage de RISC-V International.

Le port Linux/RISC-V intègre le support de l’interpréteur de template, le compilateur JIT C1 (client), le compilateur JIT C2 (serveur) et tous les GC principaux actuels, y compris ZGC et Shenandoah. Mais il ne prendra en charge pour le moment que la configuration RV64GV de RISC-V, qui est un ISA 64 bits. 

Pour plus de détails, consultez la JEP 422.

API de mémoire et de fonctions étrangères (API FFM) (1ére prévisualisation)

Après deux apparitions en incubation dans java 17 à travers la JEP 412 puis dans java 18  à travers la JEP 419, cette fonctionnalité passe en prévisualisation dans Java 19.

Grâce à l’API FFM, il est plus facile pour les programmes java d’interagir avec du code et des données en dehors de l’environnement d’exécution Java (Java runtime).

En Effet, l’API FFM remplace l’interface native Java (JNI) par un modèle de développement Java pur et assure une performance équivalente ou supérieure. Grâce à cette nouvelle API, on a la possibilité d’opérer sur différents types de mémoires étrangères (la mémoire native, la mémoire persistante, la mémoire de tas …) et sur d’autres plateformes (32 bits x86 …). L’API FFM supporte également les fonctions étrangères écrites dans d’autres langages comme C++.

Cette JEP introduit de nouvelles classes et interfaces sous le package java.lang.foreign du module java.base.
En voici les principales classes :

  • Pour allouer de la mémoire étrangère : MemorySegment, MemoryAddress, SegmentAllocator.
  • Pour manipuler et accéder à la mémoire étrangère structurée : MemoryLayout, VarHandle.
  • Pour contrôler l’allocation et la désallocation de la mémoire étrangère : MemorySession.
  • Pour appeler des fonctions étrangères : Linker, FunctionDescriptor, SymbolLookup.

L’API vectorielle (4éme incubation)

La première apparition en incubation de l’api vectorielle remonte à java 16 à travers la JEP 338, suivi par la JEP 414 et la JEP 417 respectivement dans java 17 et java 18.

L’API vectorielle permet d’effectuer des calculs vectoriels qui se compilent de manière fiable au moment de l’exécution en instructions vectorielles optimales.

Cette quatrième JEP comprend des améliorations de performance et incorpore les nouvelles fonctionnalités de l’API de mémoire et des fonctions étrangères. Ainsi il est possible de charger et de stocker les vecteurs depuis et vers l’interface MemorySegments.

Deux nouvelles opérations vectorielles dites cross-lane ont également été ajoutées : Compress et son inverse expand.

Virtual Threads (1ére prévisualisation)

L’une des fonctionnalités les plus intéressantes dans cette nouvelle version de java est probablement l’introduction des threads virtuels via la JEP 425. En effet, après plusieurs années de travail, le projet LOOM est enfin intégré en prévisualisation dans le jdk19. LOOM introduit à java un nouveau modèle de programmation concurrente légère, à travers l’introduction d’un nouveau type de thread « les threads virtuels ». Nous disposons donc maintenant de deux types de threads:

  1. Thread plateforme : il s’agit des threads système au sens de l’OS, tels que nous les connaissons aujourd’hui dans la programmation concurrente.
  2. Thread virtuel : c’est toujours un java.lang.Thread mais pour s’exécuter, il lui faut un thread plateforme.

Un thread virtuel ne monopolise pas un thread OS pendant toute l’exécution du code comme le fait un thread plateforme. Cela signifie que de nombreux threads virtuels peuvent exécuter leur code Java sur le même thread plateforme (OS).

On note aussi que quelques microsecondes suffisent à la création d’un thread virtuel, contre un temps de création beaucoup plus long, quelques millisecondes (ms) pour un thread de plateforme.

Si vous voulez avoir plus d’information sur les threads virtuels, on en parle dans cet article  « projet loom thread virtuel ».

De nombreux changements ont été apportés à la plateforme Java dans le cadre du projet LOOM.

Voici les plus pertinents :

Les changements dans la classe java.lang.Thread

– Ajout de nouvelles APIs qui permettent la création de thread virtuel ou de plateforme Thread.BuilderThread.ofVirtual(), Thread.ofPlatform(). Il convient de souligner qu’aucun constructeur n’a été ajouté ou modifié et que par conséquent, les constructeurs de la classe Thread permettent de créer uniquement des threads de plateforme.

var platformThread = Thread.ofPlatform()
        .name("P1")
        .start(() -> System.out.println("Start P1"));
var virtualThread = Thread.ofVirtual()
        .name("V1")
        .start(() -> System.out.println("Start V1"));

– Ajout de la méthode Thread.startVirtualThread(Runnable) qui permet de créer et de démarrer un thread virtuel.

– Ajout de la méthode Thread.isVirtual() qui permet de tester si un thread est virtuel ou pas.

– Ajout d’une nouvelle méthode Thread.threadId() qui permet de récupérer l’id d’un thread. Cette méthode remplace l’ancienne méthode Thread.getId()qui est maintenant déclarée deprecated.

var virtualThread = Thread.startVirtualThread(() -> System.out.println("Start V1"));
if(virtualThread.isVirtual()) {
    System.out.println("my virtual thread id : "+  virtualThread.threadId());
}
- Ajout de deux nouvelles surcharges pour les méthodes Thread.join() et Thread.sleep(). Ces deux nouvelles méthodes acceptent une instance de la classe java.time.Duration.

– A noter que la méthode statique Thread.getAllStackTraces() qui retourne une map de tous les threads dans les précédentes versions de java, retourne désormais une map de tous les threads plateformes uniquement.

Les principaux changements dans le package java.util.concurrent

Deux nouvelles méthodes de fabrique (factory) ont été ajoutées dans la classe Executors.

  • Executors.newThreadPerTaskExecutor(ThreadFactory) : qui prend en paramètre la fabrique qui servira à la création des threads virtuels ou de plateforme, et retourne un ExecutorService qui démarre un thread pour chaque tâche.
  • Executors.newVirtualThreadPerTaskExecutor() :qui retourne un ExecutorService qui démarre un thread virtuel pour chaque tâche.

Notez que l’interface ExecutorService étend maintenant l’interface AutoCloseable. Nous pourrons ainsi l’utiliser dans un try catch avec ressource.

private Trade getTradeByContractId(int contractId) throws ExecutionException, InterruptedException {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var futureLe = executor.submit(() -> getLegalEntity(contractId));
        var futurePortfolio = executor.submit(() -> getPortfolio(contractId));
        return new Trade(contractId, futurePortfolio.get(), futureLe.get());
    }
}

Trois nouvelles méthodes default ont également été introduites dans l’interface Future <V> :

  • resultNow() : permet de récupérer le résultat si la tâche est terminée. Dans le cas contraire, si la tâche n’est pas encore terminée, la méthode lève une IllegalStateException
  • exceptionNow() : retourne l’exception levée par la tâche ou génère une IllegalStateException s’il s’agit d’une tâche terminée ou interrompue.
  • state() :retourne l’état de la tâche (RUNNING, SUCCESS, FAILED, CANCELLED).

La prise en charge de la classe ThreadLocal

Tout comme les threads de plateforme, les threads virtuels supportent la classe ThreadLocal et la classe InheritableThreadLocal. Si vous utilisez ces classes dans votre code vous n’avez rien à changer.

Afin de prendre en charge les threads virtuels, de nombreux autres changements ont été apportés à la plateforme java, particulièrement dans les packages suivants : java.net, java.nio, java.io, java.lang.management, java.lang.ThreadGroup, JNI …

Celles-ci ne seront pas abordées par le présent article. Pour plus d’informations, veuillez consulter la JEP 425 qui détaille tous ces changements..

En conclusion voici les principaux caractéristiques d’un thread virtuel :

– Un thread virtuel est toujours un thread démon. En effet, si vous essayez de le modifier avec la méthode Thread.setDaemon(boolean) vous aurez une IllegalArgumentException.

– Un thread virtuel a une priorité fixe à Thread.NORM_PRIORITY qui vaut 5. A noter que la méthode Thread.setPriority(int) n’a aucun effet sur les threads virtuels pour le moment.

– Un thread virtuel ne supporte pas les méthodes suivantes : stop(), suspend () et resume(). En effet, si vous utilisez  ces méthodes vous aurez une UnsupportedOperationException.

La concurrence structurée (1ére incubation)

La JEP 428 introduit une nouvelle API pour la concurrence structurée destinée à simplifier la programmation multithread.

Dans la programmation séquentielle, un programme peut avoir plusieurs sous tâches indépendantes.

Revenons à notre exemple, notre Trade est composé d’une LegalEntity et d’un Portfolio :

private Trade getTradeByContractId(int contractId) {
    var legalEntity = getLegalEntity(contractId);
    var portfolio = getPortfolio(contractId);
    return new Trade(contractId, portfolio, legalEntity);
}

Afin de construire notre Trade, nous devons appeler deux méthodes indépendantes pour récupérer la legalEntity et le portfolio correspondants. La méthode getTradeByContractId() ne se termine donc que si les méthodes getLegalEntity() et getPortfolio() se terminent ou si l’une d’elles lève une exception.

La méthode StructuredTaskScope.ShutdownOnFailure()

En ce qui concerne la concurrence structurée, les mêmes règles sont maintenues. Notre code s’écrit alors de la manière suivante :

private Trade getTradeByContractId(int contractId) throws InterruptedException, ExecutionException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<LegalEntity> futureLe = scope.fork(() -> getLegalEntity(contractId));
        Future<String> futurePortfolio = scope.fork(() -> getPortfolio(contractId));
        scope.join();
        scope.throwIfFailed();
        return new Trade(contractId, futurePortfolio.get(), futureLe.get());
    }
}

Maintenant, les deux méthodes getLegalEntity() et getPortfolio() sont exécutées dans deux threads virtuels différents.

La concurrence structurée relie le cycle de vie des threads au bloc de code qui les a créés. Ce lien est établi via la classe principale de l’API StructuredTaskScope. Cette classe permet de structurer une tâche comme une famille de sous-tâches, de les coordonner ensembles et de gérer leurs cycles de vie.

Les sous-tâches sont exécutées dans leurs propres threads individuellement grâce à la méthode fork(),tandis que la méthode join() vous permet d’attendre que l’ensemble des sous-tâches soient terminées.

Dans notre exemple, on a créé notre instance scope de la classe StructuredTaskScope en utilisant la méthode StructuredTaskScope.ShutdownOnFailure(). Cette méthode configure notre scope de manière qu’il arrête l’exécution de toutes les sous-tâches si l’une d’elles génère une exception durant l’exécution.  

Dans le cas où une sous-tâche lève une d’exception

Revenons à notre exemple et supposons que notre méthode getLegalEntity()lève maintenant une RuntimeException :

private LegalEntity getLegalEntity(int contractId) {
    throw new RuntimeException("No legalEntity found");
}

Si la méthode throwIfFailed() n’est pas mentionnée dans notre méthode getTradeByContractId() de l’exemple précédent, nous aurons une IllegalStateException. Or notre méthode getLegalEntity()génère une RuntimeException(). Le même comportement se produit si la méthodethrowIfFailed() se trouve avant la méthode join().

Afin d’éviter toute confusion et récupérer correctement l’exception générée, il faut donc définir la méthode throwIfFailed() après la méthode join().   

La méthode StructuredTaskScope.ShutdownOnSuccess()

La classe StructuredTaskScope nous propose une seconde configuration grâce à la méthode StructuredTaskScope.ShutdownOnSuccess<T>(). Il s’agit de l’inverse du modèle précédent. Cette seconde configuration permet de stopper l’ensemble des sous-tâches dès que l’une d’elles renvoie un résultat.

Ce modèle peut nous être utile, par exemple, lorsque nous disposons de plusieurs serveurs qui renvoient le même résultat et que nous voulons obtenir la réponse la plus rapide.

Dans l’exemple suivant, nous avons deux serveurs qui retournent le même résultat. Peu importe si l’information provient du serveur A ou B, nous nous intéressons au serveur qui répond le plus rapidement.

private LegalEntity getLegalEntity(int contractId) throws InterruptedException, ExecutionException {
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<LegalEntity>()) {
        scope.fork(() -> getLegalEntityFromServerA(contractId));
        scope.fork(() -> getLegalEntityFromServerB(contractId));
        scope.join();
        return scope.result();
    }
}

Conclusion

Le projet LOOM à travers les threads virtuels et la concurrence structurée simplifie d’avantage la programmation concurrente et rend cette version de java particulièrement intéressante.

La prochaine version Java 20 est annoncée pour mars 2023. Elle devrait comporter 3 JEP toutes en prévisualisation : les records patterns en 2éme prévisualisation, le patern matching en 4éme prévisualisation et l’API FFM en 2éme prévisualisation.

Source

JDK 20 : https://openjdk.org/projects/jdk/20/

JDK 19 : https://openjdk.org/projects/jdk/19/

JDK18 : https://openjdk.org/projects/jdk/18/

JDK17 : https://openjdk.org/projects/jdk/17