Java 18 – partie 1

Java 18 a été publié le 22 mars 2022.

OpenJDK 18 est l’implémentation de référence de la version 18 de la plateforme Java, telle que spécifiée dans la JSR 393.

Cette nouvelle version inclut neuf JEP :

Certaines de ces JEP sont des contributions des projets Amber, Panama et Loom dont deux sont en incubation et une est en preview.

Cette première partie se concentre sur une revue détaillée de ces JEPs.

 

Les évolutions dans la JVM

Java 18 propose une évolution importante dans la plateforme qui devrait grandement améliorer la portabilité des données entrantes et sortantes d’une JVM en utilisant UTF-8 par défaut.

 

UTF-8 par défaut (JEP 400)

Jusqu’à Java 17, le jeu de caractères par défaut est déterminé au démarrage de la JVM. Lors de l’utilisation d’API, si le charset à utiliser n’est pas précisé, celui utilisé peut donc varier d’un système à l’autre.

C:\java>jshell
|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro

jshell> java.nio.charset.Charset.defaultCharset()
$1 ==> windows-1252

Le jeu de caractères par défaut n’étant pas le même sur tous les systèmes, les API qui l’utilisent présentent de nombreux risques non évidents, même pour les développeurs expérimentés.

Cela peut entraîner un comportement différent lorsqu’une application est développée et testée dans un environnement, puis exécutée dans un autre dans lequel la JVM choisit un jeu de caractères par défaut différent.

De nombreuses méthodes de l’API standard dépendent d’un jeu de caractères par défaut qui peut être configuré de différentes manières. Le charset utilisé par défaut n’est pas non plus cohérent selon les API du JDK : la plupart utilisent le charset par défaut mais certaines API de NIO fonctionnent par défaut en UTF-8. C’est par exemple le cas des méthodes pour la lecture/écriture de données dans la classe java.nio.file.Files.

Le but de la JEP 400 est de définir UTF-8 comme le jeu de caractères (charset) par défaut utilisé par les API du JDK, exception faite des I/O sur la sortie standard.

C:\java>jshell
|  Welcome to JShell -- Version 18
|  For an introduction type: /help intro

jshell> java.nio.charset.Charset.defaultCharset()
$1 ==> UTF-8

Cela permettra un comportement cohérent dans toutes les JVM utilisant le charset par défaut quel que soit le système d’exploitation et sa configuration.

Une nouvelle méthode charset() dans la classe PrintStream permet de connaître le charset utilisé notamment dans la console puisque dans ce cas, le charset par défaut du système est toujours utilisé.

C:\java>jshell
|  Welcome to JShell -- Version 18
|  For an introduction type: /help intro

jshell> System.out.charset()
$1 ==> windows-1252

Attention : cette fonctionnalité peut introduire des incompatibilités notamment sur les systèmes d’exploitation qui n’utilisent pas UTF-8 par défaut, par exemple Windows.

macOS et de nombreuses distributions linux utilisent UTF-8 comme jeu de caractères par défaut souvent avec la locale Posix C.

D’autres systèmes d’exploitation, comme Windows ayant un encodage par défaut propre à Microsoft selon la locale (par exemple windows-1252 ou windows-31j), peuvent présenter un risque important.

En faisant d’UTF-8 le jeu de caractères par défaut, il y a un risque que les programmes ne fonctionnent pas correctement avec les données consommées ou produites lorsque le jeu de caractères par défaut n’est pas spécifié. Ce risque n’est pas nouveau mais pourrait être accentué dans certains cas.

Les développeurs sont vivement encouragés à vérifier les problèmes de charset en démarrant le runtime Java avec -Dfile.encoding=UTF-8 sur leur version actuelle du JDK (Java 8 à 17).

Une option de la JVM de Java 18 permet de revenir au comportement précédent en valorisant la propriété système file.encoding avec la valeur COMPAT.

Attention aussi car cette JEP peut avoir un impact sur le code source. Le compilateur javac présume que les fichiers sont encodés avec le charset par défaut. A partir de Java 18, c’est UTF-8. Il est possible d’utiliser l’option -encoding de javac pour préciser un autre jeu de caractères.

 

Les évolutions dans les API

Java 18 propose deux JEP qui concernent des évolutions dans les API :

  • Reimplement Core Reflection with Method Handles (JEP 416)
  • Internet-Address Resolution SPI (JEP 418)

 

Réimplémenter l’API Reflexion avec les références de méthodes (416)

Les implémentations des classes du package java.lang.reflect utilisent différents mécanismes.

Le but de la JEP 416 est une ré-implémentation des classes Method, Constructor et Field du package java.lang.reflect en utilisant les method handles du package java.lang.invoke afin de réduire les coûts de maintenance de ces API.

Cette JEP est transparente pour les développeurs car elle ne modifie pas la signature des API concernées.

Il est possible, pour des besoins spécifiques, de revenir à l’ancienne implémentation en valorisant la propriété système jdk.reflect.useDirectMethodHandle de la JVM avec la valeur false.

 

Une SPI pour la résolution des adresses Internet (418)

Historiquement, la classe java.net.InetAddress résout les noms d’hôtes en adresses IP et vice versa. L’implémentation historique utilise une combinaison du fichier host et du DNS proposé par le système d’exploitation de manière bloquante. Ceci est problématique avec le projet Loom.

Le but de la JEP 418 est de proposer une SPI pour la résolution des noms d’hôte. Cela permettra d’utiliser d’autres implémentations de fournisseurs tiers pour effectuer cette résolution.

Ces implémentations pourront être non bloquantes et exploiter de nouveaux protocoles comme DNS sur QUIC ou TLS.

 

Les fonctionnalités dépréciées

Une seule JEP concerne la dépréciation d’une fonctionnalité.

 

Le mécanisme finalize est déprécié forRemoval (JEP 421)

Depuis Java 1.0, il est possible de redéfinir la méthode protected void finalize() pour exécuter du code avant la récupération de la mémoire par le ramasse-miette. Le ramasse-miettes planifiera via un thread dédié l’appel de la méthode finalize() d’un objet inaccessible avant de récupérer la mémoire de l’objet.

L’utilisation de la finalisation est fortement non recommandée depuis longtemps car elle peut être à l’origine d’ennuis : latence imprévisible, fuite de ressource, ordre d’exécution arbitraire, … Cela peut induire des problèmes de performance, de sécurité et de maintenabilité.

Le but de la JEP 421 est de préparer les développeurs à l’éventuelle future désactivation par défaut et suppression de la fonctionnalité historique de finalisation.

Plusieurs méthodes dans les API du JDK sont dépréciées forRemoval :

  • Object.finalize()
  • Enum.finalize()
  • Runtime.runFinalization()
  • System.runFinalization()

La finalisation reste activée par défaut mais il est possible de la désactiver en utilisant l’option --finalization=disabled pour tester le futur comportement.

Il est donc important de migrer le code qui utilise la finalisation pour le remplacer par l’utilisation d’un try-with-resource ou l’API Cleaner.

Pour faciliter la détection de l’utilisation de la finalisation, un event jdk.FinalizerStatistics dans JFR a été ajouté (JDK-8266936). Il permet d’identifier les classes instanciées qui définissent une méthode finalize() contenant des traitements. Il est activé par défaut dans les deux profiles proposés par le JDK.

Un événement émis contient : la classe concernée, le nombre de fois que le finaliseur de la classe a été exécuté, et le nombre d’objets encore dans le heap (non encore finalisés).

Si la finalisation est désactivée avec l’option --finalization=disabled alors aucun événement n’est émis.

 

L’outillage

Java 18 propose un serveur web minimaliste et un nouveau tag Javadoc @snippet pour faciliter l’ajout d’extraits de code dans la documentation.

 

Les extraits de code dans la Javadoc (JEP 413)

Le but de la JEP 413 est d’ajouter le tag @snippet dans le Doclet Standard pour faciliter l’ajout de fragments de code statique ou d’extrait de code d’un fichier externe dans la documentation Javadoc.

Ce fragment peut être inclus dans la balise elle-même :

  /**
   * Méthode de test
   * {@snippet :
   *   public static void main(String[] args) {
   *       System.out.println("Hello World");
   *   }
   * }
   */

La documentation générée contient :

Le fragment peut aussi être externe : dans ce cas, il est lu à partir d’un fichier. Il est possible de définir une région particulière du fichier à inclure en utilisant son nom.

	/**
	 * Méthode principale.
	 * Elle invoque le code :
	 * {@snippet file="com/oxiane/java/ExempleDoc.java" region="exemple"}
	 * Ceci est un exemple de code
	 * @param args paramètre de l'application
	 */

Le fichier ExempleDoc.java contient des commentaires de marquage et des tags particuliers pour définir et délimiter la région.

public class ExempleDoc {

  /**
   * Afficher un message à la console
   * @param message le message à afficher
   */
  public static void afficher(String message) {
    // @start region="exemple"
    System.out.println(message);	
    // @end
  }
}

La documentation générée contient :

Les fichiers externes peuvent être placés :

  • dans le sous-répertoire snippet-files du package de la classe documentée
  • dans un sous-répertoire précisé grâce à l’option --snippet-path de l’outil javadoc

Dans un fragment de code en ligne, il est possible d’utiliser des tags de marquage dans des commentaires de marquage pour mettre en évidence ou remplacer des portions de texte ou lier une portion de texte à d’autres éléments de la documentation.

Le tag @highlight permet de mettre en évidence une ou plusieurs portions de texte.

/**
 * Exemple Hello world
 * {@snippet :
 *   public static void main(String[] args) {
 *     // @highlight substring="System.out" type="highlighted" :
 *     System.out.println("Hello World");      
 *   }
 *}
 */

La documentation générée contient :

Le tag @replace permet de remplacer une ou plusieurs portions de texte.

/**
 * Exemple Hello world
 * {@snippet :
 *   public static void main(String[] args) {
 *     // @replace substring='"Hello World"' replacement='"Bonjour"' :
 *     System.out.println("Hello World");      
 *   }
 *}
 */

La documentation générée contient :

Le tag @link permet de lier une portion de texte à d’autres éléments de la documentation.

/**
 * Exemple Hello world
 * {@snippet :
 *   public static void main(String[] args) {
 *     // @link substring="System.out" target="System#out" :
 *     System.out.println("Hello World");      
 *   }
 *}
 */

La documentation générée contient :

Cette nouvelle fonctionnalité devrait permettre d’avoir plus d’exemples de code inclus dans les javadoc dans le futur. D’autant que l’utilisation de fichiers externes permet de les compiler et même de les tester afin de garantir la fiabilité du code inclus.

 

Un serveur web minimaliste (JEP 408)

Le but de cette JEP est de fournir dans le JDK un serveur web minimaliste permettant de renvoyer uniquement via http 1.1 des fichiers statiques contenus dans un répertoire.

Ses fonctionnalités sont basiques :

  • servir uniquement des fichiers statiques (par défaut du répertoire courant),
  • en créant une page index au besoin,
  • ne supporte que les requêtes de type GET et HEAD sur le protocole HTTP 1.1,
  • pas de support de HTTPS,
  • par défaut sur la boucle locale et le port 8000

Malgré cela, ce serveur web sera pratique pour prototyper, tester ou fournir des fichiers pour des TP en formation.

Le JDK propose la commande jwebserver

C:\java>jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving C:\java and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/
127.0.0.1 - - [30/mars/2022:15:58:47 +0200] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [30/mars/2022:15:58:48 +0200] "GET /favicon.ico HTTP/1.1" 404 –

Le serveur écoute par défaut sur la boucle locale.

Elle propose plusieurs options pour configurer le serveur (interface réseau, numéro de port, le répertoire où les fichiers seront servis, afficher des informations lors du traitement des requêtes)

C:\java>jwebserver --help
Usage: jwebserver [-b bind address] [-p port] [-d directory]
                  [-o none|info|verbose] [-h to show options]
                  [-version to show version information]
Options:
-b, --bind-address    - Address to bind to. Default: 127.0.0.1 (loopback).
                        For all interfaces use "-b 0.0.0.0" or "-b ::".
-d, --directory       - Directory to serve. Default: current directory.
-o, --output          - Output format. none|info|verbose. Default: info.
-p, --port            - Port to listen on. Default: 8000.
-h, -?, --help        - Prints this help message and exits.
-version, --version   - Prints version information and exits.
To stop the server, press Ctrl + C.

Par défaut, chaque requête est logguée dans la console. L’option -o permet de ne rien logguer ou d’être plus verbeux (le chemin du fichier servi et les headers des requêtes et réponses sont affichées).

La commande jwebserver exécute la commande java -m jdk.httpserver

C:\java>java -m jdk.httpserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving C:\java and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/

Une API est proposée dans le package com.sun.net.httpserver du module jdk.httpserver.

La classe SimpleFileServer simplifie la création et le démarrage d’un serveur web.

C:\java>jshell
|  Welcome to JShell -- Version 18
|  For an introduction type: /help intro

jshell> import com.sun.net.httpserver.*;

jshell> var serveur = SimpleFileServer.createFileServer(new InetSocketAddress(8000), Path.of("c:/temp"
), SimpleFileServer.OutputLevel.INFO);
serveur ==> sun.net.httpserver.HttpServerImpl@6e8dacdf

jshell> serveur.createContext("/java",
   ...>   SimpleFileServer.createFileHandler(Path.of("c:/java")));
$4 ==> sun.net.httpserver.HttpContextImpl@34ce8af7

jshell> serveur.start();

jshell> 127.0.0.1 - - [30/mars/2022:16:11:47 +0200] "GET / HTTP/1.1" 200 -

jshell> 127.0.0.1 - - [30/mars/2022:16:11:58 +0200] "GET / HTTP/1.1" 200 -

jshell> serveur.stop(0);

 

Les fonctionnalités en incubation et en preview

Java 18 propose deux API en incubation (issues du projet Panama) et une fonctionnalité du langage en preview (issue du projet Amber) :

  • l’API Vector API (Third Incubator) (JEP 417),
  • l’API Foreign Function and Memory API (Second Incubator) (JEP 419),
  • Pattern Matching for Switch (Second Preview) (JEP 420)

Elles permettent à la communauté de tester ces fonctionnalités et de fournir du feedback.

 

L’API Vector, troisième incubateur (JEP 417)

Le but de l’API Vector est de proposer une API permettant d’exploiter les instructions CPU dédiées lors de calcul vectoriel (SIMD : single instruction multiple data). Dans ce cas, les performances sont grandement améliorées.

L’API Vector a été introduite en incubation dans Java 16 via la JEP 338. Une seconde incubation a été proposée via la JEP 414 en Java 17.

La JEP 417 propose dans une troisième incubation d’incorporer des améliorations en réponse aux commentaires de la communauté, des améliorations de performance et d’autres améliorations significatives concernant la mise en œuvre. Elle propose entre autres :

  • le support de la plateforme ARM Scalar Vector Extension (SVE).
  • l’amélioration des performances des opérations vectorielles qui acceptent des masques sur les architectures qui supportent le masquage dans le hardware

 

L’API Foreign Function and Memory (JEP 419)

L’API Foreign Function and Memory permet à une application Java d’interagir avec du code natif de manière efficace et des données en dehors de la JVM de manière fiable (données off-heap). Le but est de remplacer l’historique API JNI introduite en Java 1.1.

L’API Foreign Function and Memory a été introduite en incubation dans Java 17 via la JEP 412.

La JEP 419 propose d’incorporer des améliorations en réponse aux commentaires de la communauté :

  • la prise en charge de boolean et MemoryAddress pour des accès mémoire via des var handles,
  • une API de déréférencement plus générale, disponible dans les interfaces MemorySegment et MemoryAddress,
  • une API plus simple pour obtenir des downcall method handles, où le passage d’un paramètre de type MethodType n’est plus nécessaire,
  • une nouvelle API pour copier des tableaux Java depuis et vers des segments de mémoire

 

Le Pattern Matching for Switch seconde preview (JEP 420)

Le pattern matching for switch a été introduit sous la forme d’une première preview en Java 17 (JEP 406) dans le cadre du projet Amber.

La JEP 420 propose deux évolutions dans cette seconde preview :


  • une vérification de la dominance qui force désormais une étiquette case constante à apparaître avant un guarded pattern du même type afin d’améliorer la lisibilité


    En Java 17, une vérification de la dominance d’un pattern vis à vis d’un autre était déjà effectuée par le compilateur.


    C:\java>jshell --enable-preview
    | Welcome to JShell -- Version 17
    | For an introduction type: /help intro

    jshell> String chaine = "test";
    chaine ==> "test"

    jshell> String libelle = switch(chaine) {
    ...> case String s -> "chaine longue";
    ...> case String s && (s.length() "chaine courte";
    ...> }
    | Error:
    | this case label is dominated by a preceding case label
    | case String s && (s.length() "chaine courte";
    | ^--------------------------^

    Cet exemple ne se compile pas car le premier case couvre plus de cas que le second case.


    Pour régler le problème, il faut intervertir les deux cases pour que le plus précis soit avant le cas plus général.


    jshell> String libelle = switch(chaine) {
    ...> case String s && (s.length() "chaine courte";
    ...> case String s -> "chaine longue";
    ...> }
    libelle ==> "chaine courte"

    jshell>

    Une situation particulière de dominance a évolué : celle qui concerne l’utilisation d’une valeur constante.


    C:\Users\jm>jshell --enable-preview
    | Welcome to JShell -- Version 17
    | For an introduction type: /help intro

    jshell> String chaine = "test";
    chaine ==> "test"

    jshell> String libelle = switch(chaine) {
    ...> case String s && (s.length() >= 4) -> "chaine longue";
    ...> case "test" -> "test";
    ...> default -> "autre";
    ...> }
    libelle ==> "chaine longue"

    En Java 17, le second case n’est jamais évalué car il est dominé par le premier case


    En Java 18, ce cas provoque une erreur de compilation :


    C:\Users\jm>jshell --enable-preview
    | Welcome to JShell -- Version 18
    | For an introduction type: /help intro

    jshell> String chaine = "test";
    chaine ==> "test"

    jshell> String libelle = switch(chaine) {
    ...> case String s && (s.length() >= 4) -> "chaine longue";
    ...> case "test" -> "test";
    ...> default -> "autre";
    ...> }
    | Error:
    | this case label is dominated by a preceding case label
    | case "test" -> "test";
    | ^----^

    jshell>

    De nouveau, pour corriger l’erreur de compilation il faut que le case avec une valeur constante soit avant le case dominant.


    jshell> String libelle = switch(chaine) {
    ...> case "test" -> "test";
    ...> case String s && (s.length() >= 4) -> "chaine longue";
    ...> default -> "autre";
    ...> }
    libelle ==> "test"


  • une vérification de l’exhaustivité plus précise avec les hiérarchies scellées où la sous-classe directe autorisée ne fait qu’étendre une superclasse scellée (générique)


    jshell> sealed interface Conteneur permits ConteneurChaine, ConteneurGenerique {}
    | created interface Conteneur, however, it cannot be referenced until class ConteneurChaine, and class ConteneurGenerique are declared

    jshell> final class ConteneurChaine implements Conteneur {}
    | created class ConteneurChaine, however, it cannot be referenced until class Conteneur is declared

    jshell> final class ConteneurGenerique implements Conteneur {}
    | created class ConteneurGenerique

    jshell> int traiter(Conteneur c) {
    ...> return switch (c) {
    ...> case ConteneurGenerique i -> 123;
    ...> };
    ...> }
    | created method traiter(Conteneur)

    jshell> var cgi = new ConteneurGenerique();
    cgi ==> ConteneurGenerique@73a8dfcc

    jshell> traiter(cgi)
    $14 ==> 123

    jshell>

    Ce code se compile très bien :


    Les seules sous-classes autorisées de l’interface scellée Conteneur sont ConteneurChaine et ConteneurGenerique, mais le compilateur détecte que le switch ne doit couvrir que la classe ConteneurChaine pour être exhaustif puisque l’expression du sélecteur est de type Conteneur.


    Pour se défendre contre une compilation séparée incompatible, le compilateur ajoute automatiquement un case par défaut dont le code lève une exception de type IncompatibleClassChangeError.

 

Conclusion

Java poursuit son évolution en respectant son modèle de releases tous les 6 mois. Cette version 18, ne contient pas beaucoup de nouvelles fonctionnalités mais son utilisation peut permettre d’anticiper des futurs soucis lors de la migration vers la prochaine LTS.

La seconde partie de cet article détaillera les autres fonctionnalités de Java 18 notamment les évolutions dans les ramasse-miettes et la sécurité.