logo le blog invivoo blanc

JAVA 16 & 17 : Évolutions et nouveautés – Partie 1

25 novembre 2021 | Java | 0 comments

Introduction : Java 16 & 17

“Java 17 est déjà là ! Euh…on va encore migrer ? Quelle version choisir ? Migrer vers Java X ou vers la dernière version LTS ?”

En fait, Oracle a accéléré depuis quelque temps le rythme de publication des mises à jour Java afin que les développeurs puissent s’appuyer sur un calendrier de sorties prévisible. Une nouvelle version est livrée tous les six mois en vue d’améliorer constamment la performance, la stabilité et la sécurité du langage. Les développeurs de leur côté, doivent se tenir informés des nouveautés du langage pour en tirer le meilleur parti en tant que professionnels de l’IT.

Dans cet article, inspiré de la conférence Devoxx France “Java™ 16 & 17 What’s new and noteworthy?” présentée par Piotr Przybył, nous allons présenter les nouvelles fonctionnalités et améliorations du langage fournies par JDK 16 & 17. Si vous voulez avoir un aperçu des évolutions de JAVA 9 à JAVA 15, vous pouvez consulter cet article.

JAVA 16 :

1. Stream.toList()

Depuis java 8, on invoque souvent l’opération terminale .collect(Collectors.toList()).Cette pratique assez courante ressemble à du code boilerplate.

List<String> colors = 
         Stream.of("Blue ", "Yellow", "RED")
        .map(String::toLowerCase)
        .filter(s -> s.contains(" "))
        .collect(Collectors.toList());

Dans Java 16, une nouvelle opération terminale toList() a été ajoutée à l’API Stream et ceci va  nous permettre de remplacer le code précédent comme suit :

List<String> colors = 
         Stream.of("Blue ", "Yellow", "RED")
        .map(String::toLowerCase)
        .filter(s -> s.contains(" "))
        .toList();

Toutefois, une différence entre les deux opérations est à noter ici :

Stream.toList() renvoie une liste immuable (ne peut pas être changée, triée…).

Toute opération de type add(), sort()… sur cette liste retourne java.lang.UnsupportedOperationException.

Contrairement à Stream.collect(Collectors.toList()) qui renvoie un ArrayList mutable (peut être changée, triée…).

2.  Stream.mapMulti()

mapMulti(), introduite dans java 16, est une opération intermédiaire de Stream qui permet de remplacer les éléments d’un stream avec multiples éléments.

<R> Stream<R> mapMulti​(BiConsumer<T, Consumer<R>> mapper)

Cette méthode est similaire à Stream :: flatMap dans le sens où elle applique une transformation un-à-plusieurs aux éléments du flux et aplatit les éléments de résultat dans un nouveau flux.

Cette méthode est préférable à flatMap dans les circonstances suivantes :

  • Lors du remplacement de chaque élément de flux par un petit (éventuellement zéro) nombre d’éléments. L’utilisation de cette méthode évite la surcharge liée à la création d’une nouvelle instance de Stream pour chaque groupe d’éléments de résultat, comme requis par flatMap.
  • Lorsqu’il est plus facile d’utiliser une approche impérative pour générer des éléments de résultat que de les renvoyer sous la forme d’un Stream.

Exemple :

Stream.of("Audi", "Bmw", "RENAULT", “Peugeot”)
  .mapMulti((str, consumer) -> {
    consumer.accept(str.toUpperCase());
    consumer.accept(str.toLowerCase());
  })
  .forEach(System.out::println);
 
Output:

AUDI
audi
BMW
bmw
RENAULT
Renault
PEUGEOT
peugeot

3. Records

Java 16 intègre la finalisation des records précédemment introduits en préversion dans Java 14. Un Record (java.lang.Record) en Java est une forme spéciale d’une classe qui ne contient que des données (similaire à la notion de tuple en base de données).

Les records sont immuables et n’ont pas de méthodes de type setter. Une fois qu’un record est instancié avec certaines valeurs, on ne peut plus le modifier. Les classes records sont finales et permettent ainsi de bien modéliser des classes de données.

Exemple :

public record Car(
    String model,
    String vendor,
    String color,
    int price) {
}

Les avantages d’un record ? Une fois que nous avons une déclaration d’un record, nous obtenons une classe qui a un constructeur canonique implicite acceptant toutes les valeurs des composants du Record. Nous obtenons automatiquement des implémentations pour les méthodes equals(), hashCode() et toString() ainsi que des méthodes d’accès pour chaque composant que nous avons dans les Records (en se référant à l’exemple ci-dessus: model(), vendor(), color(), price())

Par exemple, on peut utiliser les records pour créer des objets de transfert de données (DTO) vu que ces objets n’ont pas besoin d’identité ou de comportement et qu’ils servent uniquement à transférer des données.

Les Records peuvent :

  • redéfinir les constructeurs : canonique compact, canonique complet, personnalisé
  • redéfinir equals() et hashCode()
  • avoir leurs propres implémentations des méthodes générées (qui doivent obéir aux invariants/règles)
  • avoir des méthodes supplémentaires
  • avoir des champs et des méthodes statiques
  • implémenter des interfaces
  • être générique

Les Records ne peuvent pas :

  • hériter d’autres classes ou être étendus
  • avoir des setters
  • avoir des champs d’instance “supplémentaires”
  • avoir des constructeurs canoniques “moins visibles”
  • déclarer les méthodes natives
  • affecter des composants dans des constructeurs compacts
  • n’ont pas de méthode copy()

4. Pattern Matching avec instanceOf

JDK 16 intègre notamment la finalisation du Pattern Matching (filtrage par motif) pour l’opérateur instanceof.

Nous écrivons souvent des programmes qui combinent le test moyennant l’opérateur instanceof avec un cast explicite en vue d’appliquer un traitement ultérieur à l’objet en question.

Exemple :

if (object instanceof Integer){
   Integer i = (Integer) object;
   if (i % 2 == 0)
   System.out.println(" You’re an even number");
}

Il se passe trois choses ici : un test (l’objet est-il un Integer ?), une conversion (transtypage objet en Integer) et la déclaration d’une nouvelle variable locale (integer) d’où les 3 occurrences de type Integer.

Ce modèle n’est pas optimal. Pour y remédier, Java a adopté le pattern matching comme de nombreux langages, de Haskell à C#, qui ont adopté la correspondance de motifs pour sa brièveté et sa sécurité.

La correspondance de motif permet d’exprimer de manière concise la “forme” souhaitée d’un objet (le motif), et pour diverses déclarations et expressions de tester cette “forme” par rapport à leur entrée (la correspondance). De ce fait, on parle de smart casting (faire le test, la déclaration et le transtypage à la fois).

Ce qui donne pour notre exemple :

if (object instanceof Integer i){
   if (i % 2 == 0)
   System.out.println(" You’re an even number");
}

Ou encore:

if (object instanceof Integer i && i % 2 == 0){
   System.out.println(" You’re an even number");
}

5. Classes basées sur des valeurs (obsolescence)

Les classes basées sur des valeurs sont des classes qui représentent des objets immuables dont l’identité n’a pas d’importance pour le comportement de la classe.

De ce fait, elles sont comparées par valeur (equals()) et non pas par identité (==).

Exemple : les wrappers des primitives Integer, Double…

Dans java 16, des avertissements (WARNING) sont générés pour ces classes et leurs constructeurs sont dépréciés (deprecated) pour encourager leur suppression. Cette amélioration s’inscrit dans le cadre du projet Valhalla et vise à anticiper la migration de certaines classes basées sur des valeurs, pour devenir des classes primitives dans une future version. Les candidats à cette future migration sont :

  • Les wrappers : {Byte, Short, Integer, Long, Float, Double, Boolean, Character}
  • Les classes “optionnelles” dans java.util : Optional, OptionalInt, OptionalLong, et OptionalDouble
  • Nombreuses classes dans java.time.{Instant, ZonedDateTime, Duration,…}
  • Les implémentations de List.of(), List.copyOf(), Set.of() ,…, Map.entry()
  • Toute classe qui porte l’annotation jdk.internal.@ValueBase

Les clients des classes basées sur des valeurs ne seront généralement pas affectés par la migration des classes primitives, sauf s’ils violent les recommandations d’utilisation de ces classes. En particulier, lors de l’exécution sur une future version Java dans laquelle la migration a eu lieu :

  • Les instances de ces classes qui sont égales (par equals) peuvent également être considérées comme identiques (par ==), ce qui risque de casser des programmes qui reposent sur un résultat != pour un comportement correct.
  • Les tentatives de création d’instances de classe wrapper avec new Integer, new Double, etc., plutôt qu’un boxing implicite ou des appels aux méthodes de fabrique valueOf, produiront des exceptions de type LinkageErrors.
  • Les tentatives de synchronisation sur des instances de ces classes produiront également des exceptions.

6. Améliorations de la gestion de la mémoire

Pour augmenter encore les performances, les fonctionnalités suivantes ont été introduites dans jdk 16 :

  • Méta-espace élastique

-XX:MetaspaceReclaimPolicy=(balanced|aggressive|none)

Cette nouvelle option a été introduite en vue de permettre au système d’exploitation de restituer plus rapidement la mémoire HotSpot inutilisée (c’est-à-dire le méta-espace, ou metaspace), diminuer l’empreinte du méta-espace et simplifier le code de cet espace pour réduire les coûts de maintenance.

  • ZGC Garbage Collector: Traitement simultané de la pile de threads

A partir de java 16, et afin de réduire le temps passé à l’intérieur des points de sécurité, le ramasse-miettes ZGC déplace le traitement de la pile des threads GC des points de sécurité (safe points) vers une phase concurrente. Ce travail supprime le dernier goulet d’étranglement significatif pour le traitement concurrent des piles. On parle de moins d’1 milliseconde passée sur un ZGC safepoint pour les machines standards.

7. Amélioration du réseau : Canaux des sockets de domaine UNIX

Pour renforcer la souplesse et la productivité des développeurs, Java 16 intègre également toutes les fonctionnalités des sockets de domaine Unix (communes à Windows et aux plus grands plateformes Unix) qui viennent s’additionner aux API de canaux de socket et de canaux de socket serveur dans le package java.nio.channels. Par ailleurs, les sockets de domaine Unix sont utilisés pour les communications inter-processus (IPC) sur le même hôte. Elles sont bien similaires aux sockets TCP/IP, sauf qu’elles sont adressées par des noms de chemin du système de fichiers plutôt que par des adresses et des numéros de port Internet Protocol (IP).

8. Outil de packaging : jpackage

L’outil jpackage est un utilitaire permettant le paquetage d’applications Java auto contenues. Cela signifie que le package inclut toutes les dépendances nécessaires ainsi qu’un Java Runtime Environment (JRE). L’application peut être fournie sous la forme d’une collection de fichiers JAR ordinaires ou d’une collection de modules. Les formats de package spécifiques à la plate-forme pris en charge sont :

Linux : debetrpm

macOS : pkgetdmg

Windows: msietexe

Exemple :

    Utilisation applications non modulaires

    $ jpackage –name myapp –input lib –main-jar main.jar \

  –main-class myapp.Main

   Utilisation applications modulaires

    $ jpackage –name myapp –module-path lib -m myapp/myapp.Main

9. Enable C++14 Language

Le but de cette fonctionnalité est d’autoriser formellement les modifications du code source C++ dans le JDK pour tirer parti des fonctionnalités du langage C++14, et de fournir des instructions claires sur l’utilisation des ces fonctionnalités dans le code HotSpot.

Nous aborderons les évolutions et nouveauté JAVA 17 dans un second article qui va paraître rapidement, inscrivez-vous à notre newsletter pour être averti à la sortie d’un nouvel article.