De Java 12 à Java 14

Il y a un an, je vous faisais le bilan des nouveautés de Java dans les versions 9 à 11 ici : Java.Next.

Depuis Java 10, les versions de Java arrivent tous les 6 mois avec une version Long Term Support (LTS) toutes les 6 versions, la première LTS étant Java 11.

Maintenant que 3 nouvelles versions de Java sont sorties depuis le précédent article (la 14 il y a quelques jours seulement), il est grand temps de se pencher sur ce qu’elles apportent. 

Et on peut dire qu’en trois releases nous avons eu beaucoup de nouvelles fonctionnalités fort intéressantes : Switch Expressions, Text Blocks (des Strings sur plusieurs lignes), des NullPointerExceptions avec des messages d’erreurs plus parlants, les Records et un début de Pattern Matching.

Java 12

Switch Expressions

C’est la principale nouvelle fonctionnalité de Java 12, les switch expressions permettent de définir des switchs en tant qu’expression pour en récupérer le résultat dans une variable, le switch ne va donc plus être juste une structure de contrôle (ensemble de if/else) mais permettre de calculer un résultat.

Les switch expressions ont été modifiées dans Java 13 et sont sorties de preview pour Java 14. Je vais ici vous présenter directement leur forme finale qui ne marche donc qu’avec Java 14.

Une nouvelle syntaxe des switchs a été ajoutée, plus pratique d’utilisation et plus concise, qui utilise l’opérateur arrow déjà utilisé dans les lambda : ‘->’. On peut utiliser cette nouvelle syntaxe aussi bien dans un switch classique que dans un switch expression.

Voici un exemple de la nouvelle syntaxe qui utilise l’opérateur arrow, on peut noter que le break n’est plus nécessaire ici, cette nouvelle forme de switch ayant un break implicite :

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

Voici un exemple de switch expression qui permet de calculer un entier représentant la longueur du nom du jour, notez la présence du ‘;’ en fin d’expression qui indique bien que nous ne sommes plus ici dans une structure de contrôle mais bien une expression :

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

Dans cet exemple, chaque case étant sur une ligne, on a un retour implicite de la variable (ici un entier littéral). Si ce retour n’était pas implicite on devrait utiliser le nouveau mot clé yield introduit en Java 13 pour retourner cette valeur en lieu et place d’un return classique.

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

Divers

Il y a eu quelques ajouts à des API existantes :

  • String::align et String::indent : permettent d’aligner une String multi-lignes ou de l’indenter.
  • String::transform : crée une nouvelle String par transformation de la première via une lambda fonction.
  • Files::isSameFile : permet de comparer deux fichiers pour savoir s’ils sont identiques (même contenu).
  • Collectors::teeing : crée un collecteur qui est la composition de deux autres.

Une nouvelle API a vue le jour : java.text.CompactNumberFormat, permet de formater des nombres dans une forme compacte telle que définie par la norme LDML : 1000 -> 1K, 1000000 -> 1M, …

Shenandoah GC

Le petit nouveau Garbage Collector (GC) sorti avec Java 12 : Shenandoah. Développé par RedHat et déjà inclus depuis plusieurs mois dans leur JVM, il est intégré en tant que fonctionnalité expérimentale dans Java 12.

Comme ZGC, c’est un GC qui est concurrent à l’application et qui promet des pauses minimales (de l’ordre de la milliseconde) pour des heaps de très grande taille (plusieurs centaines de Go). Il vise les heaps de grande taille et les machines à plusieurs cœurs.

Pour l’activer, utilisez les arguments de JVM au lancement comme suit :

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

Si vous désirez en savoir plus sur ce nouveau GC, vous pouvez vous rendre sur son wiki. Ou regarder ce talk donné à Devoxx par Aleksey Shipilëv : https://vimeo.com/289626122 

Plus d’information

La liste des JEP (JDK Enhancement Proposal) de Java 12 : http://openjdk.java.net/projects/jdk/12/

L’article de blog duquel est tiré cette section : Java 12 quoi de neuf ?

Java 13

Text Blocks

C’est LA grande nouveauté de la version 13 de Java, la possibilité d’écrire des Text Blocks : un nouveau type de String Literal qui permet d’écrire une String écrite sur plusieurs lignes.

Les Text Blocks n’apportent pas énormément de nouvelles fonctionnalités (ce ne sont pas des Raw String, il n’y a pas d’interpolation de chaîne ou autre), ils permettent juste d’écrire des Strings sur plusieurs lignes, et gèrent automatiquement l’indentation pour nous. 

Au lieu d’utiliser un seul caractère d’échappement ils en utilisent une séquence : «  » »

System.out.println("""
    Hello,
    multiline
    text blocks!""")

En parlant d’indentation, c’est bien ce qui est le plus spécifique dans cette nouvelle fonctionnalité ; un algorithme un peu complexe a été implémenté pour conserver l’indentation telle que le développeur avait l’intention de la définir.
Concrètement, l’indentation est faite en supprimant l’indentation avant la première lettre de chaque ligne (donc ici on supprime l’indentation avant le H de Hello, avant le m de multiline et avant le t de text), c’est ce qu’on appelle l’indentation accidentelle. Dans le cas où le premier caractère serait un espace, il est conservé.

Les règles principales des Text Blocks sont :

  • Commence par «  » » et un retour à la ligne.
  • Suppression de l’indentation accidentelle et du premier retour à la ligne.
  • Conservation du reste de l’indentation.
  • Termine par «  » » sans retour à la ligne préalable. S’il y en a un il sera ajouté à la fin de la string !
  • S’il y a un retour à la ligne en fin de Text Block, sa position définira l’indentation accidentelle à la place de la première lettre du Text Block.
  • On peut utiliser une double-quote à l’intérieur d’un Text Block

Pour utiliser les Text Blocks, il faut ajouter l’option –enable-preview à votre ligne de commande car c’est pour l’instant une fonctionnalité en preview.

Pour l’implémentation des Text Blocks, des nouvelles méthodes ont été ajoutées à la classe String : 

  • String::formatted
  • String::stripIndent
  • String::translateEscapes.

Plus d’info dans l’article très complet de Nicolai Parlog sur le sujet : https://blog.codefx.org/java/text-blocks/

Vous pouvez aussi lire le Programmer’s Guide To Text Blocks par Jim Laskey et Stuart Marks.

Plus d’information 

La liste des JEP de Java 13 : http://openjdk.java.net/projects/jdk/13/

L’article de blog duquel est tiré cette section : Java 13 quoi de neuf ?

Java 14

Amélioration des Text Blocks

Text Blocks : la possibilité d’écrire des String Literals sur plusieurs lignes est toujours en preview avec l’ajout de deux nouvelles escape sequences : ‘\’ et ‘\s’.

‘\’ permet de mettre sur plusieurs lignes une String qui doit être sur une seule (comme en shell).

String text = """
                Lorem \
                ipsum \
                dolor \
                """;
System.out.println(text); // Lorem ipsum dolor

‘\s’ permet d’ajouter un espace (\u0020) en fin de ligne qui serait normalement supprimé lors de l’indentation automatique des Text Blocks

String colors = """
    red  \s
    green\s
    blue \s
    """;

Des NullPointerExceptions plus utiles

Les NullPointerExceptions (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 nul : 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)

Exécuter les mêmes lignes de code mais en activant les messages d’erreur utiles pour les NPE va nous donner précisément quel objet est nul et quelle opération a généré une 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)

Records

En citant la JEP : Records provide 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 “abstract”.

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 publics (mais les champs sont privés) 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ées 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));
  }
}

Pattern Matching pour instanceof

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 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 à 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

Brian Goetz a écrit un article plus vaste sur le Pattern Matching qui montre ce qui pourrait arriver dans les prochaines versions de Java sur le sujet : https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html

Divers

Il y a eu quelques ajouts à des API existantes :

  • StrictMath::decrementExact
  • StrictMath::incrementExact
  • StrictMath::negateExact()

Une nouvelle annotation, @Serial, a été créé. Elle 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 (serialVersionUid, readObject, …) et pas un mécanisme ancré dans le JDK, le compilateur ne pouvait pas valider la signature de ces champs et méthodes. En annotant avec @Serial le compilateur pourra alors en vérifier la bonne signature.

 Packaging Tool 

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 à voir 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 dont j’ai packagé le jar via Maven. En effet, jpackage nécessite un jar ou un ensemble de .class pour pouvoir réaliser un package de votre application.
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’autres 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) ...

Suppression du  Concurrent Mark Sweep (CMS) GC

Après avoir été déprécié avec Java 9, le Garbage Collector 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 heaps 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ées dans G1 dans les dernières versions de Java il arrive désormais à tenir la tête à CMS… ou alors il y a toujours ZGC et Shenandoah…

Plus d’information

La liste des JEP de Java 14 : http://openjdk.java.net/projects/jdk/14/

L’article de blog duquel est tiré cette section : Java 14 quoi de neuf ?

Conclusion 

Grâce au rythme soutenu d’une release tous les 6 mois, Java continue à avancer et à embrasser les techniques de développement moderne. 

Les apports dans ces versions le rendent attirant pour le développement de nouvelles applications et Java 14 sera, à partir d’aujourd’hui, la version de Java que j’utiliserai ;).
C’est aussi la version pour laquelle je formerais les gens via la formation Modern Java de Zenika (mal nommée Migration Java 12 mais qui est mise à jour à chaque nouvelle version de Java).

Et le mieux c’est qu’il y a encore pas mal de nouvelles fonctionnalités qui devraient arriver dans les prochaines versions : encore plus de pattern matching avec les patterns de déconstruction, les Fibers (project Loom) – des Threads légères dans la JVM, les Inline types (projet Valhalla), Vector API (projet Panama), …

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :