Introduction : de Java 9 à Java 15
Utilisée par neuf millions de développeurs à plein temps, selon le dernier rapport d’IDC (International Data Corporation), Java reste un des langages le plus populaire et le plus utilisé dans le monde d’entreprise.
En effet, aujourd’hui, selon ORACLE, on compte 9 millions de développeurs Java (dans le monde et près de 51 milliards de JVM actives.
Pour conserver cette popularité et pour élargir d’avantage la communauté Java, ORACLE a accéléré le rythme des releases Java depuis la sortie de Java 9 en septembre 2017.
Au lieu de fournir des dizaines de milliers de correctifs et de nouvelles fonctionnalités dans une seule release tous les trois ans, des améliorations sont maintenant livrées dans des releases chaque six mois et une release LTS (long time support) est disponible tous les trois ans.
L’objectif est d’assurer une meilleure productivité aux développeurs et de leur permettre de s’adapter aux nouvelles pratiques du marché, tout en offrant une prévisibilité et une stabilité continue.
Cela facilitera également la migration vers des nouvelles versions et élargira la communauté Java et cet enjeu est déjà en œuvre avec Java 15 où plus de 20% des correctifs sont apportés par des développeurs non ORACLE.
Ce changement de rythme après Java 8 a été assez soudain et beaucoup d’entreprise n’ont pas encore emboîté le pas de ce nouveau rythme, par crainte ou par méconnaissance des bénéfices apportées par chacune des nouvelles versions. Cet article a pour but de donner une vue d’ensemble sur les différentes évolutions qui ont été amenées par les versions de Java 9 à Java 15 (sorti en septembre dernier) afin qu’elles se préparent au mieux à la migration de leur SDK.
Dans une première partie, nous allons nous intéresser aux évolutions techniques du langage qui ont simplifié la vie du développeur et amélioré son quotidien. Ensuite, nous allons étudier les différentes améliorations apportées à la JVM, notamment les « Garbage Collectors » récemment disponibles. Enfin, nous allons finir avec une troisième partie dédiée aux modules et aux API supprimés ou à supprimer du JDK. Nous conclurons avec quelques recommandations pour que vous puissiez aborder votre migration en toute sérénité. Si vous souhaitez être accompagné sur vos problématiques Java vous pouvez aussi vous rapprocher de notre expertise Java.
Partie 1 : Améliorations techniques
1.Les blocs de texte (MODE Standard depuis Java 15)
Les chaînes littérales dans la programmation Java ne sont pas limitées à des chaînes courtes mais peuvent aussi correspondre à des descriptions en XML, des requêtes SQL, des pages web en HTML, etc.
Elles peuvent contenir alors plusieurs séquences d’échappements, des retours en lignes, des caractères spéciaux…
Prenons l’exemple d’une page web en HTML :
Ces écritures sont lourdes et peuvent également être une source d’erreur pour le développeur.
Pour résoudre ce problème, Java 13 a apporté la nouvelle notion de « blocs de texte » bidimensionnels en mode préview et cette notion a été standardisée avec Java 15. Voici à quoi ressemblera l’exemple précédent :
Dans le même contexte, de nouvelles méthodes sont introduites par JAVA 13 pour la classe String :
String::translateEscape : retire les séquences d’échappement dans une chaîne de caractères
String::stripIndent : supprime l’indentation accidentelle au début de chaque ligne.
String::formatted : permet de formater un string selon les paramètres passés.
On trouve aussi d’autres méthodes pratiques introduites précédemment par Java 11 :
String::isBlank, String::repeat,
String::lines : retourne une stream à partir des lignes
String::strip, String::stripLeading , String::stripTrailing : permettent une meilleur gestion des espaces.
2. Les switchs (MODE Standard depuis Java 14)
Les blocs de textes ne sont pas les seuls à être assez verbeux en Java, le « switch » est aussi une instruction qui s’écrit historiquement sur beaucoup de lignes.
Voilà à quoi ressemble un « switch » avant Java 12 :
Après Java 12, on a quelque chose de beaucoup plus concis :
Il est possible également de mettre un bloc de code dans la close case. Java 13 a apporté à son tour un nouveau mot clé « yield » qui vient remplacer break et qui permet de sortir du switch. Ces nouveautés sont passées en mode standard avec Java 14.
3. VAR pour les variables locales (MODE Standard depuis java 10)
Prenons l’exemple suivant :
Après Java 10, il pourra s’écrire comme ceci :
L’inférence des types des variables locales est la plus grande nouveauté de Java 10. Elle permet d’éviter la redondance vue dans les exemples précédents et simplifie l’affichage pour les types compliqués.
Attention, l’utilisation excessive de var peut créer une confusion au niveau du compilateur. Pour cela Java a imposé quelques restrictions pour son utilisation. Voilà quelques exemples de code non autorisé :
Avec Java 11, Il est possible d'utiliser var à l'intérieur des fonctions lambdas. Cette utilisation présente un avantage majeur car ça permet d’annoter les paramètres.
4. Les streams
La nouvelle API stream introduite par Java 8 a modifié fondamentalement la façon de traiter les collections en proposant une alternative plus simple et plus performante pour le pattern « Iterator », relativement lourd à mettre en place.
Java 9 a fourni à son tour de nouvelles méthodes à cette API, voici quelques exemples :
Stream::takeWhile : permet de parcourir une collection en utilisant un « prédicat ». On parcourt la collection tant que la condition est validée.
Stream::dropWhile : suit le fonctionnement inverse de takeWhile. On ne commence à parcourir la collection que si la condition est validée.
Stream::Iterate : permet d’itérer sur une collection en précisant la valeur de départ, un prédicat et une fonction d‘incrémentation.
Stream::ofNullable : permet d’éviter les NPE en renvoyant une interface “Optional” quand la valeur est nulle.
Java 11 a introduit les Null/InputStream, OutputStream, Reader, Writer qui permettent d’initier un stream avec Null sans générer de NPE. Il est donc possible de traiter d‘une manière transparente des input/output même s’ils représentent des flux d’entrée/ sortie nulles.
Pour finir avec les « streams », Java 12 a introduit la méthode Stream::teeing sur l’interface java.util.stream.Collectors. Elle prend en entrée deux collections indépendantes et permet de les combiner en utilisant une bi-fonction :
5. Les classes « Sealed » (MODE PREVIEW)
Le mot clé sealed est l’une des plus importantes nouveautés de java 15. Il a pour objectif de restreindre l’implémentation ou l’héritage d’une classe/interface à une liste de classes définie par le mot clé « permits » :
Si les classes sont déclarées dans le même fichier source, la close « permits » peut être retirée.
Il faut garder à l’esprit que cette nouveauté reste toujours en mode « preview ». Elle sera éventuellement standardisée avec les versions suivantes.
6. Les classes records (MODE PREVIEW)
Les records sont des POJO moins verbeux. En effet, ils permettent de réduire la quantité de code requise pour créer une classe (constructeur, accesseurs getter/ setter, les méthodes equals(), hashcode()) et le temps nécessaire pour la maintenir à chaque fois qu’un nouvel attribut est créé.
Voici un exemple :
Ils étaient initialement introduits par Java 14, mais restent toujours en mode « preview » en s’adaptant aux améliorations apportées par Java 15. En effet, un record pourra implémenter une sealed interface, et peut être déclaré localement à l’intérieur d’une méthode.
7. Null Pointer Exception (MODE STANDARD depuis Java 15)
Comme dans cet exemple, un développeur peut souvent écrire un code qui enchaîne plusieurs méthodes. Mais lorsqu’une NPE est générée, il peut devenir difficile de savoir d’où provient l’erreur.
Avec Java 14, la JVM fournit un message plus explicite indiquant exactement d’où vient l’erreur.
Cette amélioration passe en mode standard avec Java 15.
8. JShell REPL « Read Evaluate Print Loop » (MODE STANDARD depuis Java 9)
« JShell », introduit par Java 9, est un outil de commande en ligne qui permet d’évaluer le code Java.
En effet, plus besoin de créer tout un programme (importer des bibliothèques, définir une classe avec une méthode main(), etc.) pour tester une simple expression. Cela parait utile surtout pour les développeurs qui débutent avec le langage Java.
Voici un exemple de code exécuté avec Jshell :
« Jshell » fournit une liste de commandes parmi lesquelles, on trouve :
Pour finir avec cette partie, voici brièvement quelques autres améliorations qui peuvent vous intéresser :
Les méthodes « private » dans les interfaces : Java 9
Cela Permet de faciliter l’encapsulation et éviter de dupliquer certaines parties du code et d’exposer uniquement les méthodes souhaitées.
Des fabriques pour des collections immutables : Java 9
List.of(),Set.of(),Map.of().
Les variables finales dans Final variables in « try-with-resources » Java 9 : Il est possible de déclarer les ressources « Closeable » à l’extérieur du bloc « try » si elles sont finales. Il est également possible pour faciliter la lisibilité de créer des méthodes utilitaires pour l’instanciation des ressources.
Predicate ::not Java11 : fournit un moyen facile d'inverser la valeur d'un « Predicate » exprimé sous la forme de lambdas ou de références de méthodes, ce qui réduit la complexité du code.
Partie 2 : Amélioration de la JVM
Quand on rencontre des problèmes de performance (une application qui met beaucoup de temps pour démarrer, des erreurs de type Java OutOfMemoryError ou autre), on pense souvent au code (améliorer sa qualité, optimiser les algorithmes, choisir les bonnes collections, etc.) mais on oublie souvent le choix du « garbage collector ».
Dans cette partie, on va se concentrer sur les améliorations de performances apportées par les dernières versions de Java, notamment les nouveaux «garbage collectors» et l'archivage des classes en Java.
1. Garbage Collector
Au cours de son cycle de vie, une application crée un certain nombre d’objets, dont la durée de vie varie selon son rôle au sein du programme.
Cette durée de vie est définie par un “compteur de référence”. Un objet dont le compteur de référence est à zéro est un objet non utilisé.
Un « garbage collector » permet d’identifier puis de supprimer ces objets non utilisés (ou déchets). Historiquement, il divise la mémoire en deux zones : la « YoungGen », qui stocke les objets récents, et l’«OldGen », qui stocke ceux à durée de vie longue. La libération de ces zones (collecte ou GC pour Garbage Collection) se fait ensuite suivant des algorithmes comme le Comptage de références, algorithme « Mark and Sweep », algorithme « Stop and Copy », etc.
Dans cette partie de l’article, on va parler des nouveaux GC (Garbage collectors) implémentés depuis Java 9.
EPSILON No-Op Garbage Collector
Introduit par Java 11, Epsilon est un GC no-op (passif). Il gère seulement les allocations mémoire mais ne permet pas le nettoyage des objets non utilisés. Quand le tas (« heap ») alloué par l’application est épuisé, la JVM s’arrête.
Epsilon est utilisé dans le cas des applications à courte durée, sans déchet ou quand on sait que la mémoire allouée (taille de heap) est largement suffisante pour l’application en cours.
Il peut être également utile pour réaliser des tests de performance (test des nouveaux algorithmes de GC, test de pression de la mémoire, etc.).
G1 Garbage First :
Le garbage collector « Garbage-first », utilisé par défaut à partir de Java 9, fonctionne principalement avec les threads d'application (comme le CMS) mais il permet d’offrir des temps de pause plus courts et plus prévisibles.
En effet, au lieu de diviser le tas en 2 grandes régions, il le divise en petit lots de taille égale. Les données utilisées dans chaque lot sont tracées. Lorsqu'une collecte est déclenchée, le G1GC effacera en “premier” les lots qui contiennent le plus de « déchets » - d'où son nom « first ».
Mais le G1 n’est pas optimal dans toutes les situations. En effet, s’il n’arrive pas à récupérer rapidement la mémoire non utilisée, il arrête les threads d’application pour faire un full GC.
Avec Java 10, au lieu d’utiliser un seul thread lors du full GC, il est devenu possible de lancer plusieurs threads en parallèle (Parallel full GC)
On peut alors personnaliser le nombre de threads utilisés avec l'option « -XX : ParallelGCThreads »
l existe également 2 améliorations importantes apportées par Java 12 à ce GC :
G1 commence par définir le temps nécessaire pour faire une collection (“collection set“). Une fois démarrée, G1 doit collecter tous les objets utilisés sans s'arrêter. Mais cela peut prendre beaucoup de temps si la “collection set” est trop grande. Pour résoudre ce problème, avec Java 12, G1 divise la collection set en deux parties : une partie obligatoire et une partie optionnelle qui ne sera effectuée que si le GC ne dépasse pas le temps de pause prévue.
Java 12 a fourni également une deuxième amélioration. Le G1, retourne automatiquement la mémoire non utilisée aux systèmes d’exploitation (non seulement lors du full GC comme avec les anciennes versions de Java).
Avec Java 14, G1 est devenu « NUMA-Aware » (NUMA : Non Uniform Memory Access) en utilisant l’option « +UseNUMA »
Cette fonctionnalité cible principalement les machines ayant plusieurs sockets ou un grand nombre de cœurs.
ZGC (Concurrent Garbage Collector)
ZGC est un GC évolutif et à faible latence. En effet, ses temps de pause n’excèdent pas les 10 millisecondes et son débit de réduction d’application est inférieur à 15% par rapport au G1.
Lors de son lancement avec Java 11, ZGC ne renvoyait pas la mémoire au système d’exploitation même si elle n’a pas été utilisée depuis longtemps. Java 13 a fourni cette nouvelle fonctionnalité. Cela est utile dans le cas des applications où l’empreinte mémoire pose problème, ou dans le cadre d’un système avec plusieurs applications actives.
ZGC se base sur des pointeurs de couleurs pour stocker des informations relatives au marquage et à la relocation de mémoire. Ça permet de garder tout type d’information et donc d’agir selon ces données (cela n’est possible qu’avec un processeur 64 bit).
Il faut noter aussi qu’à partir de Java 14, il est disponible avec « macOS » et « Windows ».
ZGC est aujourd’hui stable, performant, à faible latence et prêt à être utilisé en production à partir de Java 15.
Il pourrait rivaliser avec « Shenandoah », également destiné aux applications à grand segment de mémoire.
Shenandoah (Concurrent Garbage Collector)
Java 12 a introduit « Shenandoah » (il peut être configuré avec Java 8).
« Shenandoah » permet de diminuer le temps de pause du collecteur.
En effet, ses tâches (marquage, libération de mémoire, compactage) sont exécutées dans des threads concurrents aux threads applicatifs utilisés par le programme en cours.
Il est surtout utile dans le cas des applications nécessitant un temps de réponse rapide et prédictible. Il faut noter aussi que la taille des « heaps » n’affecte pas le temps de pause.
Ce GC est maintenant fourni en mode standard avec Java 15.
CMS Concurrent Mark and Sweep
Après avoir été déprécié avec Java 9, CMS est finalement supprimé avec Java 14. Cette décision a pour but de réduire la charge de maintenance de la base du code GC et accélérer le développement des nouveaux algorithmes.
Comparaison des nouveaux GC disponibles
Pour choisir le meilleur GC pour une application Java, trois aspects importants doivent être pris en compte :
Taille de la « heap » : taille de mémoire nécessaire pour faire exécuter une application
Le temps de pauses de l’application : le temps nécessaire pour le GC pour exécuter ses tâches (principalement le full GC).
« Throughput » ou débit de l’application : la vitesse à laquelle une application Java s'exécute (Considérer le temps nécessaire pour effectuer les tâches du GC et le temps consacré pour exécuter le code).