JDK 15 Nouveautés

Contexte

La version 15 du JDK est disponible depuis le 15 septembre 2020. Maintenant, cela devient traditionnelle depuis le JDK 10 d’avoir deux versions par an (en mars et en septembre). A travers ce billet, nous aurons l’occasion de passer en revue l’ensemble des nouveautés.

Nous sommes à un an de la prochaine version LTS (Long Term Support) avec la sortie du JDK 17.

Nouvelles fonctionnalités

378 - Text Blocks

Les blocs de textes permettent d’écrire des chaînes de caractères sur plusieurs lignes de manière simple. La fonctionnalité a été introduite dans le JDK 13 en mode aperçu. Elle est maintenant disponible dans le langage sans option supplémentaire.

C’est à dire que l’ancien code suivant

String query = "SELECT \"EMP_ID\", \"LAST_NAME\" FROM \"EMPLOYEE_TB\"\n"+
               "WHERE \"CITY\" = 'INDIANAPOLIS'\n" +
               "ORDER BY \"EMP_ID\", \"LAST_NAME\";\n";

devient

String query = """
               SELECT "EMP_ID", "LAST_NAME" FROM "EMPLOYEE_TB"
               WHERE "CITY" = 'INDIANAPOLIS'
               ORDER BY "EMP_ID", "LAST_NAME";
               """;

Pour plus de détail, je vous suggère mon article dédié sur blocs de texte


371 - Hidden Classes

Cette fonctionnalité a été proposée par le projet Valhalla. Le but du projet est d’explorer les possibilités et les évolutions au niveau du langage et de la JVM.

A mon avis, sauf si vous travaillez sur des frameworks, vous aurez peu l’occasion d’utiliser cette fonctionnalité.

En effet, les classes cachées sont des classes créées dynamiquement à l’exécution. Voici deux exemples classiques :

  • génération des classes "proxy" afin qu’un framework puisse faire des traitements avant et après l’appel de notre méthode

  • exécution de code qui ne soit pas du java. Par exemple, un moteur javascript peut générer une classe cachée ayant le code binaire correspondant au traitement souhaité.

avertissement Le terme "Classes cachées" est utilisé car les classes ainsi générées ne sont pas visibles par le chargeur de classes (ClassLoader). Cela permet aussi d’avoir un cycle de vie différent non lié à un chargeur de classes.

Dans le volet de sécurisation de la JVM, l’objectif est aussi de limiter, voire supprimer l’usage des API interne et notamment des API de la classe sun.misc.Unsafe. (Cela a été largement abordé lors de la sortie du JDK 9 en faisant couler beaucoup d’encre).

En l’occurrence, le mécanisme des classes cachées permet de déprécier la méthode sun.misc.Unsafe::defineAnonymousClass


373 - Reimplement the Legacy DatagramSocket API

C’est le tour de l’API DatagramSocket d’être réimplémenté dans ce JDK.

Les API concernées sont :

  • java.net.DatagramSocket

  • java.net.MulticastSocket

L’objectif est le même que la précédente JEP (JEP 353 - Reimplement the Legacy Socket API) concernant l’API Socket qui a été livrée dans JDK 13.

Le code précédent provient du JDK 1.0. C’est à dire un mélange de vieux codes Java et C. De plus, l’implémentation du MulticastSocket était particulière car IPv6 étaient en développement à l’époque. Tout cela amène à avoir du code complexe et difficile à maintenir.

Le but est d’avoir un nouveau développement moderne, plus facile à maintenir. De plus, c’est aussi l’occasion d’avoir une implémentation compatible avec les threads virtuelles du Projet Loom

Ainsi, le projet Loom pourra arriver plus rapidement dans un prochain JDK.


339 - Edwards-Curve Digital Signature Algorithm (EdDSA)

L’objectif est d’implémenter des signatures cryptographiques à l’aide de l’algorithme de signature numérique Edwards-Curve (EdDSA). La définition est faite dans la RFC 8032

Ce schéma de signature est optionnel dans TLS 1.3 mais quelques utilisateurs ont des certificats EdDSA. L’intérêt est donc que la JVM supporte ce type de certificat.

L’implémentation est faite en Java, sans usage de code natif (à l’inverse de l’implémentation ECDSA). Cela permet de ne pas avoir d’adhérence avec une librairie tierce.


Nouvelles fonctionnalités (Coté ramasse-miette)

ZGC et Shenandoah passent du mode expérimental en mode production.

astuce En revanche, cela ne change en rien le ramasse-miette par défaut. C’est toujours G1 pour le JDK 15.


377 - ZGC: A Scalable Low-Latency Garbage Collector

ZGC a été introduit dans le JDK 11 en mode expérimental. Au fur et à mesure des versions, des portages ont été réalisés ainsi que des corrections de bug. Les retours de la communauté sont positifs, ce qui a permit de le considérer comme stable pour la production


379 - Shenandoah: A Low-Pause-Time Garbage Collector

Ce ramasse-miette est peut-être plus connu car il a été introduit dans le JDK 8 et JDK 11 fourni par Red Hat. Son intégration au sein du projet OpenJDK a commencé dans le JDK 12.

Une caractéristique forte de ce ramasse-miette est son temps de pause qui reste identique quelque soit la taille du tas ("heap").


Fonctionnalités en mode Aperçu

Les trois premières fonctionnalités concernent la mise en place du "pattern matching". L’ajout de ces items provient du projet Amber.

astuce Le principe et les directions ont été exposés par Gavin Bierman and Brian Goetz dans le billet suivant en septembre 2018 : Pattern Matching for Java (en).


375 - Pattern Matching for instanceof (Second Preview)

La première fonctionnalité de cette série consiste à supprimer un irritant de Java avec l’utilisation de instanceof.

Généralement, nous écrivons le code suivant :

if (obj instanceof String) {
    String s = (String) obj;
    // utilisation de s
}

Nous nous posons toujours la question, pourquoi convertir la variable obj en chaîne de caractère explicitement alors que nous avons vérifié au préalable que la variable obj est une instance d’une chaîne de caractère.

La proposition est d’écrire le code suivant :

if (obj instanceof String s) {
    // utilisation de s en tant que chaine de caractere
    ... s.contains(..);
} else {
    // pas possible d'utiliser la variable s
}

Dans le premier bloc (if), nous pouvons utiliser directement la variable s comme une chaîne de caractère. D’un autre côté, dans le second bloc (else), la variable s n’est pas connue.

Cela est aussi possible dans l’évaluation des conditions. Cela donne une écriture élégante :

if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}

384 - Records (Second Preview)

C’est un second aperçu. C’est à dire que la fonctionnalité a été intégrée la première fois dans le JDK 14. Ils ont rédigé une nouvelle version en prenant en compte les différents retours.

Les objectifs de cette proposition sont :

  • Diminuer la complexité de l’orienté objet en ayant une structure simple pour aggréger les données

  • Faciliter le développement en proposant des structures immuables

Par exemple, nous avons un objet Point ayant deux attributs : x et y. Nous pensons au début à une classe simple ayant la définition suivante :

public class Point {

	int x;
	int y;

}

Sauf qu’il est nécessaire de protéger les attributs avec des accesseurs (et oui, pas de mutateur car la structure est immuable, pour rappel). Nous avons aussi les bonnes pratiques avec la redéfinition de la méthode hashcode(), equals().

Au final, nous écrivons ceci :

public class Point {

	private final int x;
	private final int y;

	public Point(int x, int y) {
		super();
		this.x = x;
		this.y = y;
	}

	public int getX() {
		return x;
	}
	public int getY() {
		return y;
	}
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + x;
		result = prime * result + y;
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Point other = (Point) obj;
		if (x != other.x)
			return false;
		if (y != other.y)
			return false;
		return true;
	}
	@Override
	public String toString() {
		return "Point [x=" + x + ", y=" + y + "]";
	}
}

avertissement Il est à noter que même lors de la création de la classe, si nous écrivons correctement le code pour les méthodes hashcode() et equals(), il y a un risque fort que les méthodes ne soient pas mises à jour si nous rajoutons un attribut.

Avec les records, nous écrivons simplement :

record Point(int x, int y) { }

astuce Vous voyez l’introduction d’un nouveau mot clé : record.

De ce fait, nous avons automatiquement :

  • un accesseur public pour chaque attribut

  • un attribut final privé pour chaque attribut

  • un constructeur contenant chaque attribut

  • une méthode equals()

  • une méthode hashcode()

  • une méthode toString() pour visualiser les valeurs de chaque attribut.

Des contrôles peuvent être faites lors de la construction:

record Range(int lo, int hi) {
    Range {
        if (lo > hi)  // référence implicite aux paramètres du constructeur
            throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
    }
}

astuce Vous remarquerez qu’il n’a pas de paramètre, ni d’affectation au niveau du constructeur comme nous le ferions habituelle avec une classe Java. Cela n’est pas nécessaire car cela est automatique avec les records.

avertissement Comme précisé plus haut, les records sont immuables. Donc elles ne peuvent pas être utilisées pour représenter une entité JPA. En effet, par définition, les entités JPA sont mutables.

En revanche, elle peuvent très bien servir pour l’utilisation de DTO. Dans ce sens, les librairies commencent en prendre en compte, comme Jackson ci-dessous

Support des Records au niveau de Jackson

360 - Sealed Classes (Preview)

Cette fonctionnalité provient aussi du projet Amber.

L’objectif est que l’auteur d’une classe ou d’une interface puisse contrôler qui peut en hériter ou l’implémenter. C’est à dire que l’auteur limite l’ensemble des classes dérivées possibles.

Par exemple, nous avons :

package com.exemple.geometrie;

public abstract sealed class Forme
    permits Cercle, Rectangle, Carre {
	...
}

C’est à dire qu’il y a seulement trois formes possibles : Cercle, Rectangle et Carre.

Les classes dérivées peuvent être dans des packages différents comme dans l’exemple ci-dessous :

package com.exemple.geometrie;

public abstract sealed class Forme
    permits com.example.polar.Cercle,
            com.example.quad.Rectangle,
            com.example.quad.simple.Carre {
	...
}

avertissement Cela ne change rien au niveau du mot clé final indiquant que la classe ne peut pas avoir de classes dérivées.

Cela va aussi dans le sens des correspondance de modèle ("pattern matching") où nous pouvons avoir une analyse complète des modèles.

Sans correspondance de modèle, nous avons le code suivant (peu lisible) :

int getCentre(Forme forme) {
    if (forme instanceof Cercle) {
        return ... ((Cercle)forme).center() ...
    } else if (forme instanceof Rectangle) {
        return ... ((Rectangle)forme).length() ...
    } else if (forme instanceof Carre) {
        return ... ((Carre)forme).side() ...
    }
}

Grace à l’analyse complète du modèle ("pattern-matching"), nous pouvons écrire simplement le code suivant :

int getCentre(Forme forme) {
    return switch (forme) {
        case Cercle c    -> ... c.center() ...
        case Rectangle r -> ... r.length() ...
        case Carre t    -> ... t.side() ...
    };
}

383 - Foreign-Memory Access API (Second Incubator)

Le but est d’avoir une API sûr et efficace qui permette des accès mémoire au delà du tas ("heap").

Est-ce bien nécessaire ? Si le développeur classique Java n’a pas généralement ce type de besoin. C’est le cas de nombreux librairies comme Apache Ignite, mapDB, memcached, Apache Lucene et Netty par exemple.

Les points importants sont :

  • Généralité : Cette API devra concerner les différents types de mémoire (mémoire native, mémoire persistante, …​)

  • Sûr : Elle devra maintenir la stabilité de la JVM

  • Déterministe : Les opérations de dé-allocation devront être explicite dans le code.

  • Conviviale : L’API devra être facile à appréhender.

C’est la deuxième proposition (Seconde incubation). La première a été introduite dans le JDK 14 (JEP370). Elle a été revue en prenant en compte les retours des utilisateurs de la première implémentation. Plusieurs changements ont été apportés que nous ne détaillerons pas dans ce billet.

En Java, écrire que la dé-allocation doit être explicite dans le code n’est pas habituel car nous sommes habitués au ramasse-miette, mais cela permet d’éviter le coût de ce dernier et son côté imprévu (On ne se sait pas quand il passe). Cela est très important lors de la gestion de cache très volumineux. De plus, cela permet de gérer des scénarios divers comme avec de la mémoire partagée.

A l’heure actuelle, il existe trois possibilités :

  • ByteBuffer API (introduit dans le JDK 1.4). Plusieurs limitations sont associés à cette API dont la limitation d’allouer plus de 2 Giga (utilisation du type int).

  • sun.misc.Unsafe API (Le nom est évoqueur). L’API permet l’accès directe à la mémoire. Cela est efficient mais il n’y a pas non plus de protection. Un mauvais usage peut provoquer un crash de la JVM.

  • JNI (Java Native Interface). L’accès est sûr mais le coût associé à cette solution est important. Cela nécessite des développeurs qui écrivent et maintiennent du code en langage C. De plus, les accès JNI sont lents car cela nécessite la transition java à c.

Le succès de cette nouvelle API sera lié à sa performance vis à vis des solutions existantes (notamment les 2 premières cités).


Divers

Au delà des fonctionnalités décrites dans les JEPs, il reste intéressant de lire les notes de versions. Cela permet de récupérer des changements moins notables. Par exemple, nous retrouvons l’ajout ou le retrait d’un certificat d’une autorité de certification dans le magasin de clé du JDK. Mais certaines sont encore plus intéressantes comme la suivante :


Helpful NullPointerException (JDK-8233014)

Dans un autre billet, je vous avais parlé de cette fonctionnalité introduite dans le JDK14. Une option a été aussi ajoutée pour activer ou non la fonction le -XX:{+|-}ShowCodeDetailsInExceptionMessages

Cependant, la fonctionnalité était désactivée par défaut. Il fallait obligatoirement utiliser l’option correspondante. Maintenant, ce n’est plus la peine pour en profiter, elle est activée par défaut.

avertissementNéanmoins, il reste possible de la désactiver si nous le souhaitons.

Exemple de message d’information :

Cannot read field "c" because "a.b" is null

Fonctionnalités supprimées et dépréciées

372 - Remove the Nashorn JavaScript Engine

Nashorn est un moteur javascript. Il a été introduit dans le JDK 8 en remplacement de l’ancien Rhino.

astuce Nashorn veut dire Rhinocéros en allemand. Petit clin d’oeil de l’équipe envers Rhino (Implémentation de Mozilla).

C’est une implémentation complète de ECMAScript-262 5.1 standard. Pour le situer, cela correspond à une spécification de 2011.

Actuellement, nous sommes à la version 11, publiée en juin 2020. Le moteur de javascript était vraiment en retard sur les dernières versions.

A ce stade, le projet considère que le maitien est trop difficile vis à vis des évolutions du langages ECMAScript.

De plus, une piste est d’utiliser GraalVM qui permet d’exécuter du code javascript. Pour en savoir plus, je vous conseille cet article de l’équipe GraalVM (en)

Le moteur a été déprécié depuis JDK 11. Si votre programme l’utilise, vous devriez avoir le message ci-dessous :

Warning: Nashorn engine is planned to be removed from a future JDK release

Si ce n’est pas le cas, la suppression de Nashorn n’aura pas d’effet sur nos projets.

avertissementIl est à noter que durant la période du JDK 11, 12 et 13, aucun groupe de développeurs a exprimé clairement le désir de maintenir Nashorn.

Edition du 04/12

astuce Depuis le 27 novembre, Nashorn 15.0 est disponible de manière autonome sur Maven Central. Voici le mail de l’annonce de la nouvelle équipe sur la liste de diffusion nashorn-dev (Merci à @piradix pour l’information)


381 - Remove the Solaris and SPARC Ports

Le but est de supprimer le support de Solaris pour le JDK (sources spécifiques Solaris / SPARC).

Cela est la conséquence des parts de marchés de ce système d’exploitation.

C’est la suite logique de la Dépréciation du support Solaris (JEP362) incluse dans le JDK 14. Le principe était de laisser le temps à un ensemble de développeurs crédibles de se manifester pour le maintien de ce portage.

Beaucoup de projet actif comme Vahalla ou Loom sont en cours de développement. Le fait de supprimer un portage sur ce système permet de faciliter le travail et de profiter plus tôt des améliorations de ces projets.

astuce Pour information, Solaris est un système créé par Sun en 1991 qui est dans le giron d’Oracle depuis le rachat de Sun par ce dernier.


385 - Deprecate RMI Activation for Removal

Le but est de déprécier le mécanisme d’activation RMI afin de le supprimer plus tard.

Il est important de noter que RMI (Remote Method Invocation) reste supporté et ne change pas. C’est l’activation RMI qui est concerné (c’est à dire RMId). Cela consiste à la possibilité d’avoir des objets distribués actif plus tard, en mode paresseux ("lazy")

Cette JEP prend acte du contexte actuel où les services distribués sont basés actuellement sur des technologies web et non plus par des objets distribués. Ce changement permet de résoudre des problèmes liés aux réseaux (pare-feu, répartiteur de charge).

De ce fait cette fonctionnalité est devenue obsolète. Les multiples sondages ont montrés le peu d’intérêt sur cette fonctionnalité. Par exemple, la dernière question sur le forum JavaRanch date de 2003.

La maintenance du code associé entraîne forcément un coût à mettre en face de l’intérêt pour la communauté. De plus, des tests complexes existent (client, serveur, service d’activation) qui pénalisent l’exécution globale des tests.

avertissement Le service d’activation RMI est optionnel depuis Java 8.


374 - Disable and Deprecate Biased Locking

En Java, il existe des verrous pour protéger les accès concurrents :

  • Les moniteurs (gérés avec le mot clé synchronized)

  • java.util.concurrent.locks.Lock (seulement depuis JDK 1.5)

Le verrouillage biaisé est une optimisation de la JVM pour gérer les moniteurs afin d’éviter des sur-contrôles sur les blocs non contrôlés. Ainsi, le moniteur est supposé resté la propriété d’un thread donné jusqu’à ce qu’un thread différent tente de l’acquérir. De ce fait, les performances étaient meilleurs qu’un système classique de verrou.

Les gains de performance observés dans le passé sont moins significatif aujourd’hui. Il est à noter aussi que cela est particulièrement lié aux structures de l’époque (JDK 1.0). C’est à dire, cela concerne des anciennes structures comme Hashtable ou Vector où chaque accès est synchronisé.

En effet, actuellement, nous utilisons des collections non synchronisées HashMap et ArrayList (introduites dès le JDK 1.2) pour les scénarios mono-thread. Sinon, nous utilisons des structures concurrentes (introduites dans le JDK 5) pour les scénarios multi-threads.

De plus, les applications utilisant les pool de threads ont de meilleur performances sans cette optimisation.

Ce verrouillage biaisé s’accompagne aussi de beaucoup de code complexe dans le mécanisme de synchronisation. Cela fait un obstacle supplémentaire avec peu d’intérêt actuellement.

C’est pourquoi l’équipe OpenJDK souhaite désactiver, abandonner et éventuellement supprimer la prise en charge de ce type de verrouillage.

avertissement Il reste possible de réactiver ce comportement avec l’option -XX:+UseBiasedLocking.