JDK 20 Nouveautés

Contexte

La version 20 du JDK est disponible depuis mardi 21 mars 2023 sur le site jdk.java.net.

La version 20 n’est pas une version LTS (Long Term Support). Par conséquent, le support existe tant que la prochaine version n’est pas sortie, soit le JDK 21 (septembre 2023).

Pour Eclipse Adoptium, les packages sont en cours de construction selon les plateformes. L’avancé des travaux peut être suivi avec la fiche suivante issue #218

Fonctionnalités en mode Aperçu

432 - Record Patterns (Second Preview)

L’objectif est d’appliquer le filtrage par motif au niveau des enregistrements.

astucePour plus de précision sur le filtrage par motif, vous pouvez consulter ce billet.

astucePour plus de précision sur les enregistrements, vous pouvez consulter ce billet.

Pour un enregistrement, nous pouvons de définir des classes sous une forme simplifiée.

record Point(int x, int y) {}

A l’aide du filtrage par motif pour instanceof, nous pouvons écrire le code suivant :

static void afficheSomme(Object o) {
    if (o instanceof Point p) {
        int a = p.x();
        int b = p.y();
        System.out.println(a + b);
    }
}

Maintenant, nous allons pouvoir aller plus loin en écrivant tout simplement le code suivant :

void afficheSomme(Object o) {
    if (o instanceof Point(int a, int b)) {
        System.out.println(a + b);
    }
}

avertissementPlus d’informations sur le billet dédié sur le premier aperçu.

Ce second aperçu apporte

  • la suppression du support des filtrages par motif pour les enregistrements nommées

  • le support de l’inférence de type pour les filtrages par motif pour les enregistrements génériques.

  • le support de l’utilisation de l’enregistrement au niveau de l’instruction for

Nous allons détailler le dernier point par un exemple.

static void dump(Point[] pointArray) {
    for (Point(var x, var y) : pointArray) {           (1)
        System.out.println("(" + x + ", " + y + ")");
    }
}
  1. Filtrage par motif au niveau de l’en-tête de l’instruction for

433 - Pattern Matching for switch (Fourth Preview)

C’est le quatrième aperçu, l’objectif reste le même. C’est à dire simplifier l’écriture du code en utilisant le filtrage par motif pour les switch.

Par exemple, nous avons le code suivant :

static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

Avec le filtrage par motif au niveau des switch, nous obtenons le code suivant :

static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}

A travers les différentes propositions, nous avons les améliorations suivantes :

Dominance entre les types et les sous-types

static void error(Object o) {
    switch(o) {
        case CharSequence cs ->
            System.out.println("Longueur d'une séquence : " + cs.length());
        case String s ->    // Erreur, le motif est dominé par le motif précédent
            System.out.println("Une chaîne de caractère : " + s);
        default -> {
            break;
        }
    }
}

Pour rappel, nous avons la hiérarchie suivante pour la classe String

hierarchie string

Exhaustivité des expressions

static int coverage(Object o) {
    return switch (o) {         // Erreur - pas exhaustif
        case String s  -> s.length();
        case Integer i -> i;
    };
}

L’expression switch précédent est en erreur car le switch est réalisé sur Object, donc tous les cas ne sont pas traités.

static int coverage(Object o) {
    return switch (o) {
        case String s  -> s.length();
        case Integer i -> i;
        default -> 0;
    };
}

Donc, il est nécessaire d’ajouter des cas (ou à minima default) pour couvrir tous les autres cas.

Support du null

Cela devient tout simplement un cas que nous pouvons traiter sans être obligé de protéger le switch.

static void test(Object o) {
    switch (o) {
        case null      -> throw new NullPointerException();
        case String s  -> System.out.println("String: "+s);
        case Integer i -> System.out.println("Integer");
        default        -> System.out.println("default");
    }
}

Raffinement des cas

Le mot clé when est introduit afin d’affiner le cas du switch. Dans l’exemple ci-dessous, nous avons deux cas pour traiter les Triangles :

  1. Triangle avec une aire supérieur à 100

  2. Les autres triangles

static void testTriangle(Shape s) {
    switch (s) {
        case null ->
            { break; }
        case Triangle t
        when t.calculateArea() > 100 ->
            System.out.println("Large triangle");
        case Triangle t ->
            System.out.println("Small triangle");
        default ->
            System.out.println("Non-triangle");
    }
}

Dans le dernier aperçu, nous avons quelques précisions supplémentaires :

  • Dans le cas de l’exhaustivité du switch, c’est un MatchException au lieu de IncompatibleClassChangeError si un label est manquant dans le cas de traitement d’un Enum (lors de l’exécution).

  • Simplification de la grammaire

  • Inférence des types pour le filtrage par motif pour les enregistrements génériques (comme pour la JEP précédente)

avertissementPlus d’informations sur le billet dédié sur le premier aperçu.

avertissementPlus d’informations sur le billet dédié sur le troisième aperçu.

434 - Foreign Function & Memory API (Second Preview)

Cet aperçu correspond à la réunification de JEP en incubation Foreign Memory Access API et Foreign Linker API.

Les travaux sont menés dans le cadre du projet Panama. L’objectif est de faciliter l’utilisation de module natif.

Dans ce cadre, il est un remplaçant de JNI (Java Native Interface). Il apporte un modèle purement Java.

Les performances ne sont pas oubliés. Il doit être à minima aussi mieux que l’utilisation de la fameux classe sun.misc.Unsafe

Le but est de fournir un moyen de manipuler des différents zones mémoires (la mémoire native, mémoire persistante, mémoire de la pile managé) et d’utiliser des fonctions écrites dans d’autres langages (comme C, C++).

Voici un exemple d’utilisation pour allouer de la mémoire

try (Arena offHeap = Arena.openConfined()) {
    MemorySegment nativeArray  = offHeap.allocateArray(ValueLayout.JAVA_INT, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
    MemorySegment nativeString = offHeap.allocateUtf8String("Hello!");
    MemorySegment upcallStub   = linker.upcallStub(handle, desc, offHeap.scope());
   ...
}  (1)
  1. La mémoire est libéré à ce niveau

Nous pouvons rechercher une fonction d’une librairie externe

try (Arena arena = Arena.openConfined()) {
    SymbolLookup opengl = SymbolLookup.libraryLookup("libGL.so", arena);
    MemorySegment glVersion = opengl.find("glGetString").get();
    ...
} (1)
  1. La librairie libGl.so est déchargée à ce niveau

Prenons un exemple concret de recherche et d’appel de fonction externe. Pour cela, nous allons utiliser la fonction strlen. La signature de la fonction est la suivante :

size_t strlen(const char *s);

Nous recherchons cette fonction en précisant le type de retour.

Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
    linker.defaultLookup().find("strlen").get(),
    FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);

Nous avons récupéré la référence strlen vers la fonction éponyme. Ainsi, nous allons pouvoir l’appeler.

try (Arena arena = Arena.openConfined()) {
    MemorySegment str = arena.allocateUtf8String("Hello"); // (1)
    long len          = strlen.invoke(str);                // (2)
} (3)
  1. Nous allouons de la mémoire native.

  2. La fonction retourne 5.

  3. La mémoire allouée dans le bloc est libérée.

436 - Virtual Threads (Second Preview)

C’est intégration des travaux du projet #loom.

L’objectif est de revoir la gestion de la concurrence. Les threads du système (appellés aussi threads plateforme) sont coûteux en mémoire et par conséquent limité vis à vis du système.

Pour cela, l’équipe a mis en place les threads virtuels. Ceux sont des threads gérés par la JVM.

Pour assurer la compatibilité, ils ont gardé la même interface Runnable

class HelloRunnable implements Runnable {
    public void run() {
        System.out.println(Thread.currentThread() + " dit Coucou");
    }
}

Pour la création et le lancement d’un thread virtuel, il suffit de passer par la méthode ofVirtual :

Thread.ofVirtual.start(new HelloRunnable());

Lors de l’affichage du thread virtuel, nous obtenons la sortie suivante :

VirtualThread[#25]/runnable@ForkJoinPool-1-worker-1 dit Coucou

Nous pouvons remarquer que le thread virtuel tourne bien au final sur un thread plateforme. Un pool ForkJoin spécifique a été mis en place pour cela.

L’intérêt est que nous pouvons avoir plein de threads virtuels. C’est la JVM qui les gère. Le thread virtuel reste associé à un thread système.

astuceL’équivalent existe pour les threads plateformes avec la méthode ofPlatform :

Thread.ofPlatform.start(new HelloRunnable());

avertissementPlus d’informations sur le sur le billet dédié.

Fonctionnalités en mode Incubation

429 - Scoped Values (Incubator)

Les valeurs délimitées (Scoped Values) sont introduites pour faciliter le partage des données immutables entre les threads. Leur utilisation est préférable à la place des variables locales (ThreadLocal). Surtout, si vous utilisez de nombreux, très nombreux threads virtuels.

Les variables locales sont attachées aux threads. La donnée peut être mutable avec quiconque qui peut faire un appel de la méthode get() ou set(). Le développpeur peut aussi oublier de nettoyer les variables locales. De ce fait, la portée est plus grande que prévue initialement au départ.

A la différence, les valeurs délimités ont une structure syntaxique qui permet de déterminer la période où le thread associé peut y accèder.

class Server {
    final static ScopedValue<Principal> PRINCIPAL = new ScopedValue<>();   // (1)

    void serve(Request request, Response response) {
        var level     = (request.isAdmin() ? ADMIN : GUEST);
        var principal = new Principal(level);
        ScopedValue.where(PRINCIPAL, principal)
                   .run(() -> Application.handle(request, response));      // (2)
    }
}
  1. Définition de la valeur délimitée

  2. La valeur est accessible uniquement dans la méthode run()

Pour y accèder, nous passons par le code suivant :

DBConnection open() {
    var principal = Server.PRINCIPAL.get();                               // (1)
    if (!principal.canOpen()) throw new  InvalidPrincipalException();
    return newConnection(...);
}
  1. Utilisation de la valeur délimitée

Cela fonctionne uniquement si l’appel de la méthode open() est effectuée dans un bloc where(…​) et run(…​)

437 - Structured Concurrency (Second Incubator)

Avec les threads virtuels, l’utilisation de pool avec les exécuteurs n’est plus le modèle de concurrence adéquate.

En effet, maintenant, il est possible d’avoir plusieurs milliers ou millions de threads virtuels. Pour cela, il est nécessaire d’avoir un nouveau style de programmation plus adapté qui pourra éliminer les problèmes d’annulation et d’arrêt de thread.

Voici le code où nous pouvons voir trois partie :

  • Définition de l’ensemble es Future

  • Mise en place de la barrière pour les threads créés précédemment.

  • Suite des opérations car la barrière a été franchie avec succès.

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {     // (1)
        Future<String>  user  = scope.fork(() -> findUser());
        Future<Integer> order = scope.fork(() -> fetchOrder());

        scope.join();           // Barrière pour les deux threads
        scope.throwIfFailed();  // ... et propage les erreurs s'il y a en

        // Ici, nous savons que les deux threads sont terminés avec succès
        // et que nous avons leur résultat.
        return new Response(user.resultNow(), order.resultNow());
    }
}
  1. Arrêt s’il y a un échec sur au moins un Thread

Il existe plusieurs politiques pour configurer la barrirère et surtout il est possible d’écrire la sienne.

438 - Vector API (Firth Incubator)

L’API Vectorielle a été proposée la première fois dans le JDK 16.

L’objectif est de fournir une API afin de fournir un mécanisme pour exprimer des calculs vectoriels. Elle utilise les instructions vectorielles optimales sur les architectures CPU prises en charge. Cela permet d’obtenir des performances accrues vis à vis des calculs scalaires

Les performances ont été améliorées dans cette version. Notamment, pour traduire les vecteurs d’objets vers et depuis les tableaux booléens.

Références