Java 14 : quoi de neuf ?
Maintenant que Java 14 est features complete (Rampdown Phase One au jour d’écriture de l’article), c’est le moment de faire le tour des fonctionnalités qu’apporte cette nouvelle version, à nous, les développeurs.
Cet article fait partie d’une suite d’article sur les nouveautés des dernières version de Java, pour ceux qui voudraient les lires en voici les liens : Java 13, Java 12, Java 11, Java 10, et Java 9.
À l’inverse des précédentes version de Java, la version 14 apporte beaucoup de fonctionnalités, de mon avis, c’est une version qu’on pourrait qualifier de majeur au même titre que Java 5 ou 8 ! Records, Pattern Matching, Switch Expression, Text Block, … Plein de bonne choses dans cette version 😉
JEP 361: Switch Expressions (Standard)
Les Switch Expressions sortent de preview et deviennent une fonctionnalité standard !
Le nouveau mot clé yield a donc été validé.
Plus d’information dans la JEP : https://openjdk.java.net/jeps/361
Si vous aviez loupé cette fonctionnalité dans les précédentes versions, voici un article qui la décrit en détail : Definitive Guide To Switch Expressions
JEP 368: Text Blocks (Second Preview)
Text Blocks : la possibilité d’écrire des String Literals sur plusieurs lignes est toujours en preview avec l’ajout de deux nouvelles escape sequence : '\'
et '\s'
.
'\'
permet de mettre sur plusieurs lignes une String qui doit en être sur une seule (comme en shell).
String text = """ Lorem \ ipsum \ dolor \ """; System.out.println(text); // Lorem impsum dolor
'\s'
permet d’ajouter un espace (\u0020)
String colors = """ red \s green\s blue \s """;
Plus d’information dans la JEP : https://openjdk.java.net/jeps/368
Si vous aviez loupé la fonctionnalité elle est décrite dans mon article sur Java 13.
JEP 358: Helpful NullPointerExceptions
Les NullPointerException (NPE) sont monnaie courante en Java, et le message d’erreur est bien souvent peu utile car pointe uniquement la ligne à laquelle l’exception arrive, et pas exactement quelle instruction / portion de code a généré cette exception.
SAP a implémenté dans sa JVM (depuis 2006!) une version améliorée des messages des NPE. Fort de cette expérience, l’implémentation en a été revue dans OpenJDK, à mon grand regret il faut ajouter une option au démarrage de la JVM, -XX:+ShowCodeDetailsInExceptionMessages
, pour activer cette fonctionnalité fort utile.
Pour comparaison, voici les messages d’erreurs standard pour des NullPointerException.
On peut remarquer qu’on ne distingue pas quel objet est nulle : a ? a.s ?
$ jshell | Welcome to JShell -- Version 14-ea | For an introduction type: /help intro jshell> public class A { public String s;} | created class A jshell> A a; a ==> null jshell> a.s.toString() | Exception java.lang.NullPointerException | at (#3:1) jshell> a.s = "toto"; | Exception java.lang.NullPointerException | at (#4:1) jshell> a = new A(); a ==> A@3f8f9dd6 jshell> a.s.toString() | Exception java.lang.NullPointerException | at (#6:1) jshell>
Exécuter les même ligne de code mais en activant les messages d’erreur utile pour les NPE va nous donner précisément quel objet est nul et quelle opération a générée un NPE sur cet objet (read, write, appel de méthode).
$ jshell -R-XX:+ShowCodeDetailsInExceptionMessages | Welcome to JShell -- Version 14-ea | For an introduction type: /help intro jshell> public class A { public String s;} | created class A jshell> A a; a ==> null jshell> a.s.toString() | Exception java.lang.NullPointerException: Cannot read field "s" because "REPL.$JShell$12.a" is null | at (#3:1) jshell> a.s = "toto"; | Exception java.lang.NullPointerException: Cannot assign field "s" because "REPL.$JShell$12.a" is null | at (#4:1) jshell> a = new A(); a ==> A@3f8f9dd6 jshell> a.s.toString() | Exception java.lang.NullPointerException: Cannot invoke "String.toString()" because "REPL.$JShell$12.a.s" is null | at (#6:1) jshell>
Plus d’information dans la JEP : https://openjdk.java.net/jeps/358
JEP 359: Records (Preview)
En citant la JEP : Records provides a compact syntax for declaring classes which are transparent holders for shallowly immutable data.
C’est un nouveau type Java (comme class et enum), son but est d’être un conteneur de donnés. Les Records sont implicitement final et ne peuvent être abstrait.
Les Records fournissent une implémentation par défaut pour du code boilerplate que vous auriez sinon généré via votre IDE.
jshell> record Point(int x, int y) { } jshell> Point p = new Point(); // all fields need to be initialized at construction time | Error: | constructor Point in record Point cannot be applied to given types; | required: int,int | found: no arguments | reason: actual and formal argument lists differ in length | Point p = new Point(); | ^---------^ jshell> Point p = new Point(1, 1); p ==> Point[x=1, y=1]
Tous les Records ont des accesseurs publique (mais les champs sont privé) et une méthode toString
.
jshell> p.x //field is private | Error: | x has private access in Point | p.x | ^-^ jshell> p.x(); //public accessor $8 ==> 1 jshell> p.toString(); //default toString() $9 ==> "Point[x=1, y=1]"
Tous les Records ont les méthodes equals
et hashCode
dont les implémentations sont basé sur le type du Record et son état (ses champs donc).
jshell> Point other = new Point(1,1); other ==> Point[x=1, y=1] jshell> other.equals(p); $11 ==> true jshell> other == p $12 ==> false
Vous pouvez redéfinir les méthodes et constructeurs par défaut d’un Record.
Quand vous redéfinissez le constructeur par défaut, il n’y a pas besoin de répéter l’initialisation des champs et vous pouvez directement accéder aux champs du Record.
record Range(int lo, int hi) { public Range { if (lo > hi) /* referring here to the implicit constructor parameters */ throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi)); } }
Plus d’information dans la JEP : https://openjdk.java.net/jeps/359
JEP 305: Pattern Matching for instanceof (Preview)
Chaque développeur a déjà écrit du code qui ressemble à ça avec l’opérateur instanceof:
if (obj instanceof Integer) { int intValue = ((Integer) obj).intValue(); // use intValue }
Le cast après l’instanceof semble superflu car on vient de tester le type de l’objet.
Et c’est là qu’entre en scène le pattern matching, il va permettre de vérifier qu’un objet est d’un type précis (comme instanceof le fait) et « extraire » la « forme » de l’objet dans une nouvelle variable. On va donc pouvoir remplacer le code précédent par celui-ci
if (obj instanceof Integer intValue) { // use intValue }
Plus besoin de cast, et on assigne à une variable locale au bloc qui va permettre d’utiliser directement l’objet via son type vérifié par l’opérateur instanceof.
Pour utiliser le pattern matching, il faut ajouter l’option --enable-preview
a votre ligne de commande car c’est pour l’instant une fonctionnalité en preview.
Voici un exemple un peu plus complet via JSHell.
$ jshell --enable-preview | Welcome to JShell -- Version 14-ea | For an introduction type: /help intro jshell> public void print(Object o) { ...> if(o instanceof String s) System.out.println("String =>" + s); ...> if(o instanceof Integer i) System.out.println("Integer =>" + i); ...> } | created method print(Object) jshell> print(1) Integer =>1 jshell> print("toto") String =>toto
Plus d’information dans la JEP : https://openjdk.java.net/jeps/305
Brian Goetz a écrit un article plus vaste sur le Pattern Matching qui prélude de ce qui pourrais arriver dans les prochaines versions de Java sur le sujet : https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html
Divers
- JDK-8229485 : Ajout de StrictMath::decrementExact, StrictMath::incrementExact et StrictMath::negateExact()
- JDK-8232633 : Support de la pluralisation dans CompactNumberFormat
- JDK-8202385 : Ajout de l’annotation <code>@Serial</code> qui permet de marquer des champs/méthodes comme relatif à la sérialisation. Comme le protocole de sérialisation est basé sur des champs et méthodes standard et pas un mécanisme ancré dans le JDK, le compilateur ne pouvait pas valider la signature de ces champs et méthodes. Maintenant en annotant un champ (serialVersionUID par exemple) ou une méthode (writeObject par exemple) avec
@Serial
le compilateur pourra vérifier la bonne signature des champs et méthodes relatifs à la sérialisation et éviter les possibles erreurs de développement.
JEP 343: Packaging Tool (Incubator)
jpackage est un outil permettant de packager votre application dans un format natif à votre OS (attention, c’est le format de packaging qui est natif par votre application, cela n’a rien à voire avec GraalVM native image).
Mon OS étant Ubuntu, jpackage va me permettre de packager mon application au format .deb. Je vais prendre comme exemple l’application getting-started de Quarkus.
Tout d’abord, il faut utiliser jpackage pour générer un package. Il faut lui passer le répertoire de votre application (–input) et son main (ici –main-jar pour préciser le jar contenant le main).
$ jpackage --name getting-started --input target --main-jar getting-started-1.0-SNAPSHOT-runner.jar WARNING: Using incubator modules: jdk.incubator.jpackage
Ensuite, je peux utiliser dpkg pour installer ce package sur mon OS.
$ sudo dpkg -i getting-started_1.0-1_amd64.deb Sélection du paquet getting-started précédemment désélectionné. (Lecture de la base de données... 256348 fichiers et répertoires déjà installés.) Préparation du dépaquetage de getting-started_1.0-1_amd64.deb ... Dépaquetage de getting-started (1.0-1) ... Paramétrage de getting-started (1.0-1) ...
L’application sera installée dans /opt/getting-started, pour la lancer il y a un exécutable dans le répertoire bin.
$ /opt/getting-started/bin/getting-started 2019-12-31 17:17:25,933 INFO [io.quarkus] (main) getting-started 1.0-SNAPSHOT (running on Quarkus 1.0.1.Final) started in 1.130s. Listening on: http://0.0.0.0:8080 2019-12-31 17:17:25,937 INFO [io.quarkus] (main) Profile prod activated. 2019-12-31 17:17:25,937 INFO [io.quarkus] (main) Installed features: [agroal, cdi, resteasy]
Les fichiers de notre application se retrouvent dans /opt/getting-started/lib/app/, d’autre répertoires contiennent les fichiers propres à jpackage.
Pour finir, nous pouvons désinstaller le package via la commande dpkg
$ sudo dpkg -r getting-started (Lecture de la base de données... 256753 fichiers et répertoires déjà installés.) Suppression de getting-started (1.0-1) ...
Plus d’information dans la JEP : https://openjdk.java.net/jeps/343
JEP 352: Non-Volatile Mapped Byte Buffers
L’API MappedByteBuffer
a été mise à jour pour permettre le support des Non-Volatile Memory (NVM).
Plus d’information dans la JEP : https://openjdk.java.net/jeps/352
JEP 370: Foreign-Memory Access API (Incubator)
Un nouveau module incubator a été créé pour définir une nouvelle API d’accès à la mémoire hors-heap.
Cette fonctionnalité est plutôt à destination des développeurs de framework comme il est rare de devoir accéder à de la mémoire native depuis une application Java. Jusqu’alors ils utilisaient principalement ByteBuffer
et Unsafe
. Comme l’utilisation d’Unsafe
est fortement déconseillé et que ByteBuffer
a de nombreux désavantage, cette nouvelle API se veut plus pratique à utiliser et aussi performante qu’Unsafe
(voir plus car designer pour tirer parti des optimisations du JIT).
Voici un exemple d’usage simple (l’API est faite pour couvrir des cas beaucoup plus complexe avec la notion de memory layout et de stride pour les multi-dimension).
VarHandle intHandle = MemoryHandles.varHandle(int.class); try (MemorySegment segment = MemorySegment.allocateNative(100)) { MemoryAddress base = segment.baseAddress(); for (int i = 0 ; i &lt; 25 ; i++) { intHandle.set(base.offset(i * 4), i); } }
Plus d’information dans le JEP : https://openjdk.java.net/jeps/370
Garbage Collector
Beaucoup de nouveautés du côté des GC, des nouvelles fonctionnalité, de la performance, de la déprécation et de la suppression !
JEP 345: NUMA-Aware Memory Allocation for G1
G1 a été modifié pour être « NUMA-Aware ». NUMA – Non Uniform Memory Access – est une des caractéristiques des architectures CPU modernes ; pour faire très simples, le coût d’accès cache / mémoire peut être différent d’un cœur à l’autre de CPU vers une ligne de cache / une barrette de mémoire.
L’algorithme de G1 a été modifié pour en tenir compte (préférer une allocation plus proche du CPU). Le ParalleCG est NUMA-aware depuis longtemps, G1 comble donc juste son retard sur le sujet.
Cette fonctionnalité cible principalement les machines ayant plusieurs sockets ou un grand nombre de cœurs, certains benchmarks agressifs côté GC montrent une optimisation des performances de l’ordre de 5% avec l’option +UseNUMA
.
Plus d’information dans le JEP : https://openjdk.java.net/jeps/345
JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
Après avoir été déprécié avec Java 9, CMS est finalement supprimé avec Java 14, personne dans la communauté n’ayant voulu le maintenir.
CMS était un algorithme de GC concurrent très performant, ciblant les heap de taille moyenne et hautement configurable. Il souffrait du manque de phase de compaction et Oracle voulant investir de plus en plus dans G1 et ZGC ne désirait plus maintenir un algorithme concurrent de plus.
Jusqu’il y a peu, c’était un des algorithmes les plus performants pour les heap de moyennes tailles, et bien souvent on n’arrivait pas à configurer G1 pour avoir des performances aussi hautes. Espérons qu’avec les nouvelles optimisations réalisé dans G1 dans les dernières version de Java il arrive désormais à tenir la tête à CMS … ou alors il y a toujours ZGC et Shenandoah ….
Plus d’information dans le JEP : https://openjdk.java.net/jeps/363
JEP 365: ZGC on Windows et JEP 364: ZGC on macOS
Le Garbage Collector ZGC a été porté sur Windows (version 1803 de Windows 10 minimum) et macOS.
Plus d’information dans les JEP https://openjdk.java.net/jeps/364 et https://openjdk.java.net/jeps/365
Parallel GC
JDK-8204951 – Investigate to use WorkGang for Parallel GC : changement dans la gestion des threads du ParallelGC, il utilise désormais le même WorkGang que les autres GC. Moins de coût de maintenance donc et surtout des performances GC optimisés entre 0% et 40% dépendant du benchmark (moyenne de 10%).
Plus d’information dans le JBS : https://bugs.openjdk.java.net/browse/JDK-8204951
JDK-8220465 – Use shadow regions for faster ParallelGC full GCs : optimisation de l’implémentation des full collection pour le ParallelGC. Les tests ont montré des performances x2 à x3 en cas de full GC !
Plus d’information dans le JBS : https://bugs.openjdk.java.net/browse/JDK-8220465