Les nouveautés de Java 20

Jean-Michel Doudoux

Senior Tech Lead


Publié le 23/05/2023Temps de lecture : 8 minutes
Description

Les nouveautés de Java 20

JDK 20 est la troisième release publiée depuis le JDK 17. La version GA 20 du JDK a été publiée le 21 mars 2023.

Elle contient sept JEPs que l’on peut regrouper en deux catégories :

  • Des évolutions dans le langage

  • Des évolutions dans les API

Toutes ces JEPs sont en preview ou en incubation. Elles sont issues des travaux de plusieurs projets : Amber, Loom et Panama.

Cinq JEPS concernent des évolutions dans les API (projet Panama et Loom) :

Deux JEPs concernent des évolutions dans la syntaxe du langage Java (projet Amber) :

Ces 7 JEPS sont toutes en preview ou en incubator, dont 6 sont des itérations de mises à jour de fonctionnalités déjà proposées précédemment. Une seule est une nouvelle fonctionnalité (JEP 429 : Scoped Values).

Les spécifications de la version 20 de la plateforme Java SE sont définies dans la JSR 395.

Les fonctionnalités du projet Amber

Le projet Amber propose deux fonctionnalités dans le JDK 20 :

  • Les records patterns

  • Le pattern matching dans l’instruction switch

Les record patterns

La JEP 432 propose une seconde preview de la fonctionnalité concernant les record patterns.

Les changements suivants ont été apportés à cette fonctionnalité par rapport à Java 19 :

  • Ajout de la prise en charge de l’inférence des types d’arguments génériques dans les record patterns

       record Mono<T>(T val) implements Container<T> {}
       Mono<Mono<String>> monoDeMono = new Mono<>(new Mono<>("valeur"));
    
       //    if (monoDeMono instanceof Mono<Mono<String>>(Mono<String>(var s))) {
       if (monoDeMono instanceof Mono(Mono(var s))) {JDK
         System.out.println("mono contient " + s);
       }
  • Ajout de la prise en charge des record patterns dans les boucles for améliorées

       List<Employe> employes = List.of(new Employe("Nom1", "Prenom1"));
       for(Employe(var nom, var prenom) : employes) {
         System.out.println(nom + " " + prenom);
       }

    Si un élément du parcours est null alors une exception de type java.lang.MatchException est levée.

  • La suppression de la prise en charge des record patterns nommés (named record patterns).

Le pattern matching pour l’instruction switch

La JEP 433 propose une quatrième preview de la fonctionnalité concernant le pattern matching pour l’instruction switch.

Les changements suivants ont été apportés à cette fonctionnalité par rapport au JDK 19 :

  • Lorsque l’exhaustivité des cas d’un switch n’est pas satisfaite (sur un type scellé ou une énumération par exemple), une exception de type java.lang.MatchException est maintenant levée. Précédemment, c’était une exception de type java.lang.IncompatibleClassChangeError qui était levée

  • Le support de l’inférence pour les arguments de type pour les records pattern est désormais prise en charge dans les instructions switch

Les fonctionnalités du projet Panama

Le projet Panama propose deux fonctionnalités dans le JDK 20 :

  • Foreign Function & Memory API (Preview)

  • Vector API (Incubator)

L’API Foreign Function and Memory

L’API Foreign Function & Memory (FFM) combine deux API introduites en incubation : l’API Foreign-Memory Access (JEP 370, 383 et 393) et l’API Foreign Linker (JEP 389). L’API FFM a été introduite en incubation dans le JDK 17 via la JEP 412 et dans le JDK 18 via la JEP 419 et pour la première fois en preview dans le JDK 19 via la JEP 424.

La JEP 434 propose une seconde preview de L’API FFM dans le JDK 20. Celle-ci propose quelques évolutions dans l’API qui tiennent compte des retours fournis par la communauté vis-à-vis de la précédente preview. Dans cette version, plusieurs évolutions sont apportées à l’API :

  • Les abstractions MemorySegment et MemoryAddress sont unifiées (les adresses mémoire sont désormais modélisées par des segments de mémoire de longueur zéro)

  • La hiérarchie scellée MemoryLayout est améliorée pour faciliter l’utilisation avec le pattern matching dans les instructions switch

  • L’interface MemorySession est remplacée par les interfaces Arena et SegmentScope pour faciliter le partage des segments à travers les portées

Exemple :

public class HelloFFM {

  public static void main(String[] args) {
    try {
      System.loadLibrary("user32");
      Optional<MemorySegment> msgBoxFunction = SymbolLookup.loaderLookup().find("MessageBoxA");
      FunctionDescriptor msgBoxFunctionDesc = FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS, JAVA_INT);
      Linker linker = Linker.nativeLinker();
      MethodHandle methodHandle = linker.downcallHandle(msgBoxFunction.get(), msgBoxFunctionDesc

      try (Arena offHeap = Arena.openConfined()) {
        MemorySegment cStringMessage = offHeap.allocateUtf8String("Voulez-vous utiliser Java 20 ?");
        MemorySegment cStringTitre = offHeap.allocateUtf8String("Confirmation");
        int bouton = (int) methodHandle.invoke(NULL, cStringMessage, cStringTitre, 36);
        System.out.println("Bouton selectionne : " + bouton);
      }
    } catch (Throwable t) {
      t.printStackTrace();
    }
  }
}
article Java 20 001
Figure 1. La boîte de dialogue affichée

L’API Vector

L’API Vector a été proposée pour la première fois en incubation via la JEP 338 et intégrée au JDK 16. Une seconde incubation a été proposée par la JEP 414 et intégré au JDK 17. Une troisième incubation a été proposée par la JEP 417 et intégrée au JDK 18. Une quatrième incubation a été proposée par la JEP 426 et intégrée au JDK 19.

La JEP 438 repropose l’API pour une cinquième incubation dans le JDK 20, sans changement dans l’API par rapport au JDK 19. Seules quelques corrections de bugs et d’améliorations des performances sont appliquées.

Exemple :

import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;

public class TestVector {

  static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

  public static float[] calculerVectoriel(float[] a, float[] b) {
    float[] resultat = new float[a.length];
    int i = 0;
    for (; i < SPECIES.loopBound(a.length) ; i += SPECIES.length()) {
      var va = FloatVector.fromArray(SPECIES, a, i);
      var vb = FloatVector.fromArray(SPECIES, b, i);
      var vr = va.mul(va).sub(vb.mul(vb));
      vr.intoArray(resultat, i);
    }
    for (; i < a.length; i++) {
      resultat[i] = a[i] * a[i] - b[i] * b[i];
    }
    return resultat;
  }
}

Les fonctionnalités du projet Loom

Le projet Loom propose trois fonctionnalités dans le JDK 20 :

  • Virtual Threads (Preview)

  • Structured Concurrency (Incubator)

  • Scoped Values (Incubator)

Les threads virtuels

Les threads virtuels ont été introduits pour la première fois en preview via la JEP 425 dans le JDK 19.

La JEP 436 repropose les threads virtuels pour une seconde preview.

Quelques changements dans des API décrits dans la première JEP qui concernent des fonctionnalités qui ne sont pas spécifiques aux threads virtuels et qui ont été finalisés dans le JDK 19 ne sont plus explicitement listés dans la JEP actuelle :

  • Dans la classe java.lang.Thread : les méthodes join(Duration), sleep(Duration) et threadId()

  • Dans l’interface java.util.concurrent.Future : les méthodes resultNow(), exceptionNow() et state()

  • L’interface java.util.concurrent.ExecutorService qui hérite de l’interface java.lang.AutoCloseable

  • Dans la classe java.lang.ThreadGroup : les modifications dans l’implémentation de certaines méthodes pour préparer leur décommissionnement

Structured concurrency

L’API Structured concurrency a été introduite en incubation dans le JDK 19 via la JEP 428.

La JEP 437 repropose l’API Structured concurrency pour une seconde incubation.

Le seul changement est une évolution de la classe StructuredTaskScope pour prendre en charge l’héritage des scoped values par les threads virtuels créés et utilisés par l’instance.

Scoped Value

La JEP 429 propose une nouvelle API qui ajoute une fonctionnalité plus sûre et efficace de partage de valeurs immuables au sein d’un thread. Elle permet de stocker une valeur immuable pour une durée limitée afin que seul le thread qui a écrit la valeur puisse la lire. C’est la seule JEP qui concerne une nouvelle fonctionnalité introduite dans le JDK 20.

Historiquement depuis Java 1.2, on utilise une variable de type java.lang.ThreadLocal pour partager des objets dans le code exécuté par un thread sans avoir à les passer en paramètres des méthodes invoquées.

Mais leurs mises en œuvre présentent plusieurs risques :

  • Les données sont mutables : les données peuvent être lues, mais aussi modifiées par n’importe quelle méthode exécutée par le thread

  • Une source de potentiel fuite de mémoire si des valeurs sont ajoutées dans un thread partagé dans un pool sans être retirées

  • La consommation de ressources : les valeurs d’un ThreadLocal sont copiées automatiquement à la création d’un thread fils en utilisant le constructeur par défaut

L’API ScopedValue tente de remédier à ces inconvénients en proposant une alternative aux variables de type ThreadLocal qui propose :

  • De stocker et de partager des données immuables

  • Pour une durée de vie limitée et clairement délimitée à des traitements du thread qui les a écrits

Comme avec les variables ThreadLocal, on crée et utilise une instance généralement statique et publique pour en faciliter l’accès par les traitements.

public final static ScopedValue<String> VALEUR = ScopedValue.newInstance();

Plusieurs méthodes permettent de définir et de lire les valeurs associées au thread courant :

  • where() pour définir une valeur, chainable pour plusieurs valeurs

  • run() et call() pour exécuter une tâche dans le thread courant

    • run() : sous la forme d’une implémentation de Runnable

      ScopedValue.where(VALEUR, "test").run(() -> { afficherValeur(); });
    • call() : sous la forme d’une implémentation de Callable

      String valeur = ScopedValue.where(VALEUR, "test")
          .<String>call(monService::traiter);
  • get() pour obtenir la valeur ou lève une NoSuchElementException

  • isBound() pour savoir si une valeur est associée au thread : conditionner de préférence l’invocation de get() par une invocation de isBound()

    System.out.println((VALEUR.isBound() ? VALEUR.get() : "non definie"));

Les scoped values proposent de limiter la durée de vie d’une variable par thread à la stricte exécution des traitements exécutés dans la portée. Une fois l’exécution de ces traitements terminée, toutes les données partagées initialement via cette variable par thread ne sont plus accessibles.

Les valeurs encapsulées sont immutables mais il est possible de réassocier une autre valeur pour la portée d’un traitement sous-jacent.

    ScopedValue.where(VALEUR, "valeur").run(() -> {
      afficherValeur();
      ScopedValue.where(VALEUR, "autre-valeur").run(monService::traiter);
      afficherValeur();
    });

Une autre fonctionnalité intéressante concerne le partage des valeurs avec les threads virtuels d’une StucturedTaskScope.

    ScopedValue.where(VALEUR, "valeur", () -> {
      try (var scope = new StructuredTaskScope<String>()) {
        afficherValeur();
        scope.fork(monServiceA::traiter);
        scope.fork(monServiceB::traiter);
        scope.joinUntil(Instant.now().plusSeconds(10));
      } catch (InterruptedException | TimeoutException e) {
        e.printStackTrace();
      }
    });

Les autres fonctionnalités

Les principales nouveautés d’un JDK sont définies dans des JEPs, mais une nouvelle version du JDK contient de nombreuses autres évolutions et corrections de bugs.

Concernant la sécurité

Le protocole DTLS 1.0 est désactivé par défaut (JDK-8256660)

Le protocole DTLS 1.0 est désactivé par défaut pour améliorer la sécurité. Ce protocole présente plusieurs faiblesses et n’est plus recommandé comme indiqué dans la RFC 8996.

Il est préférable d’utiliser la version 1.2 du protocole DTLS supportée par le JDK.

Il est cependant possible de réactiver DTLS 1.0, en ayant pesé les risques encourus, en supprimant "DTLSv1.0" de la propriété de sécurité jdk.tls.disabledAlgorithms dans le fichier de configuration java.security.

Les algorithmes TLS_ECDH_* sont désactivés par défaut (JDK-8279164)

Les algorithmes ECDH utilisés avec TLS qui ne l’étaient pas encore, sont maintenant tous désactivés par défaut, car ils ne préservent pas le secret de transmission. Aucun de ces algorithmes ne devrait être utilisé en pratique, mais si vous en avez absolument besoin, vous pouvez les activer à vos risques et périls avec la propriété de sécurité jdk.tls.disabledAlgorithms.

Concernant la performance

Plusieurs améliorations ont été apportées au ramasse-miettes G1 et de nouveaux intrinsics pour certains algorithmes sur architecture x86_64 et aarch64.

G1: Disable Preventive GCs by Default (JDK-8293861)

Dans le JDK 17, des garbage collections « préventives » ont été ajoutées. Il s’agit de garbage collections spéculatives, dont l’objectif est d’éviter les échecs d’évacuation coûteux dus à de nombreuses allocations lorsque le tas est presque plein.

Toutefois, ces collections spéculatives ont pour conséquence une charge de travail supplémentaire pour le ramasse-miettes. En effet, le vieillissement des objets est basé sur le nombre de survies à des collections mineures : les GC supplémentaires entraînant une promotion prématurée dans la old génération. Ceci conduit à plus de données dans la old génération et à plus de travail de garbage collection pour supprimer ces objets. Cette situation est aggravée par le fait que la prédiction actuelle de déclenchement des garbage collections est souvent activée inutilement.

Dans la majorité des cas, cette fonctionnalité est inefficace et parfois dégrade les performances et comme les échecs d’évacuation sont désormais traités plus rapidement, elle a été désactivée par défaut. Elle peut être réactivée par en utilisant les options -XX:+UnlockDiagnosticVMOptions -XX:+G1UsePreventiveGC.

L’amélioration du contrôle des threads de raffinement concurrents de G1 (JDK-8137022)

Le contrôle des threads de raffinement concurrents de G1 a été complètement réécrit. Le nouveau contrôleur requiert généralement moins de threads. Il tend à réduire les pics d’activité des threads de raffinement. Il a également tendance à retarder l’affinage, ce qui permet un filtrage plus important par les barrières d’écriture lorsqu’il y a plusieurs écritures au même endroit ou à des endroits proches, améliorant ainsi l’efficacité de la barrière.

Une refonte majeure de la gestion des threads concurrents de raffinement de G1 devrait réduire les pics d’activité de ces threads et gérer les barrières d’écriture plus efficacement.

Plusieurs options en ligne de commande pouvaient être utilisées pour fournir des valeurs de paramètres. Ces options ne sont plus utiles. Leur utilisation n’a plus d’effet et génère des avertissements et elles seront supprimées dans une version future :

  • -XX:-G1UseAdaptiveConcRefinement

  • -XX:G1ConcRefinementGreenZone=buffer-count

  • -XX:G1ConcRefinementYellowZone=buffer-count

  • -XX:G1ConcRefinementRedZone=buffer-count

  • -XX:G1ConcRefinementThresholdStep=buffer-count

  • -XX:G1ConcRefinementServiceIntervalMillis=msec

Nouveaux intrinsics pour x86_64 et aarch64 (JDK-8247645)( JDK-8297379)( JDK-8296548)

Plusieurs nouveaux intrinsics pour les processeurs x86/64 et aarch64 ont été ajoutés pour différents algorithmes : Chacha20, Poly1305, MD5, …

Concernant les outils

L’arrêt du support de Java 7 par javac (JDK-8173605)

Dans le JDK 19, javac supporte les versions 7 à 19 incluses.

C:\java>javac -version
javac 19
C:\java>javac --release=7 Hello.java
warning: [options] source value 7 is obsolete and will be removed in a future release
warning: [options] target value 7 is obsolete and will be removed in a future release
warning: [options] To suppress warnings about obsolete options, use -Xlint:-options.
3 warnings

Dans le JDK 20, en application de la politique décrite dans la JEP 182, la prise en charge de la valeur 7 ou 1.7 pour les options -source, -target et --release de javac a été supprimée.

C:\java>javac -version
javac 20
C:\java>javac --release=7 Hello.java
*error: release version 7 not supported*
Usage: javac <options> <source files>
use --help for a list of possible options

Avertissement de javac lors de l’utilisation d’assignations composées avec pertes possibles lors de conversions (JDK-8244681)

Un nouveau lint "lossy-conversions" a été ajouté au compilateur javac pour avertir des casts de type dans les affectations composées avec pertes possibles lors des conversions. Si le type de l’opérande de droite d’une affectation composée n’est pas compatible avec le type de la variable, une conversion de type est implicitement effectuée avec une perte potentielle pouvant survenir.

Fréquemment, les instructions a += b et a = a + b sont considérées comme identiques. Ce n’est cependant pas toujours le cas.

Exemple

public class Addition1 {

  public static void main(String[] args) {
    short a = 1;
    int b = 2;
    a = a + b;
    // a = (short) (a + b);
  }
}

Le code ci-dessus ne compile pas.

C:\java>javac Addition1.java
Addition1.java:6: error: incompatible types: possible lossy conversion from int to short
    a = a + b;
          ^
1 error

Le compilateur émet une erreur, car le résultat de l’addition donne une valeur de type int (4 octets) qui est affectée avec une variable de type short (2 octets). Il y a donc un risque potentiel de perte de données.

Exemple

public class Addition2 {

  public static void main(String[] args) {
    short a = 30_000;
    int b = 50_000;
    a += b;
    System.out.println(a);
  }
}

Le code ci-dessus se compile correctement

C:\java>javac Addition2.java

C:\java>

Il s’exécute sans erreur mais le résultat affiché est faux.

C:\java>java Addition2
14464

C:\java>

Le souci, c’est qu’avec l’utilisation d’un opérateur d’affectation composé, le compilateur ajoute dans le bytecode un cast implicite vers le type int.

C:\java>javap -c Addition2
Compiled from "Addition2.java"
public class Addition2 {
  public Addition2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: sipush        30000
       3: istore_1
       4: ldc           #7                  // int 50000
       6: istore_2
       7: iload_1
       8: iload_2
       9: iadd
      10: i2s
      11: istore_1
      12: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      15: iload_1
      16: invokevirtual #14                 // Method java/io/PrintStream.println:(I)V
      19: return
}

Cela peut engendrer des bugs silencieux comme dans le cas ci-dessus.

Le compilateur du JDK 20 peut émettre un avertissement de type lossy-conversions si ceux-ci sont activés.

C:\java>javac -Xlint Addition2.java
Addition2.java:6: warning: [lossy-conversions] implicit cast from int to short in compound assignment is possibly lossy
    a += b;
         ^
1 warning

C:\java>

Les avertissements peuvent être supprimés en utilisant @SuppressWarnings("lossy-conversions").

La compression des images jmod

L’outil en ligne de commande jmod pour créer des archives JMOD permet maintenant de préciser le niveau de compression des images créées (JDK-8293499).

Une nouvelle option en ligne de commande --compress a été ajoutée à l’outil jmod pour permettre de définir le niveau de compression lors de la création de l’archive JMOD.

Les valeurs possibles sont zip-[0-9], où zip-0 n’applique aucune compression et zip-9 applique la compression la plus forte. La valeur par défaut est zip-6.

Conclusion

Java 20 est la dernière version non-LTS avant la publication de la prochaine version LTS, Java 21, le 19 septembre prochain.

N’hésitez donc pas à télécharger et tester une distribution du JDK 20 auprès d’un fournisseur pour anticiper la release de la prochaine version LTS de Java.