Une autre version Java LTS est déjà là, apportant des changements et des améliorations passionnants. Analysons les fonctionnalités les plus importantes de Java 21, voyons comment elles fonctionnent dans la pratique et essayons de prédire leur importance pour l'avenir de cette technologie.
Depuis que la plateforme Java a adopté un cycle de publication de six mois, nous avons dépassé les questions récurrentes telles que « Java mourra-t-il cette année ? » ou "Vaut-il la peine de migrer vers la nouvelle version ?". Malgré 28 ans depuis sa première version, Java continue de prospérer et reste un choix populaire comme langage de programmation principal pour de nombreux nouveaux projets.
Java 17 a constitué une étape importante, mais Java 21 a désormais pris la place de 17 en tant que prochaine version de support à long terme (LTS). Il est essentiel pour les développeurs Java de rester informés des changements et des nouvelles fonctionnalités apportées par cette version. Inspiré par mon collègue Darek, qui a détaillé les fonctionnalités de Java 17 dans son article, j'ai décidé de discuter du JDK 21 de la même manière.
JDK 21 comprend un total de 15 JEP (JDK Enhancement Proposals). Vous pouvez consulter la liste complète sur le site officiel Java. Dans cet article, je mettrai en évidence plusieurs JEP Java 21 qui, à mon avis, sont particulièrement remarquables. A savoir :
Sans plus attendre, examinons le code et explorons ces mises à jour.
La fonctionnalité Spring Templates est toujours en mode aperçu. Pour l'utiliser, vous devez ajouter l'indicateur –enable-preview aux arguments de votre compilateur. Cependant, j’ai décidé de le mentionner malgré son statut d’aperçu. Pourquoi? Parce que je suis très irrité à chaque fois que je dois écrire un message de journal ou une instruction SQL contenant de nombreux arguments ou déchiffrer quel espace réservé sera remplacé par un argument donné. Et Spring Templates promet de m'aider (et vous) avec cela.
Comme le dit la documentation JEP, le but des modèles Spring est de « simplifier l'écriture de programmes Java en facilitant l'expression de chaînes qui incluent des valeurs calculées au moment de l'exécution ».
Vérifions si c'est vraiment plus simple.
L'ancienne méthode serait d'utiliser la méthode formatted() sur un objet String :
var msg = "Log message param1: %s, pram2: %s".formatted(p1, p2);
Maintenant, avec StringTemplate.Processor (STR), cela ressemble à ceci :
var interpolated = STR."Log message param1: \{p1}, param2: \{p2}";
Avec un texte court comme celui ci-dessus, le profit n'est peut-être pas si visible – mais croyez-moi, lorsqu'il s'agit de gros blocs de texte (jsons, instructions sql), les paramètres nommés vous aideront beaucoup.
Java 21 a introduit une nouvelle hiérarchie de collections Java. Regardez le schéma ci-dessous et comparez-le à ce que vous avez probablement appris lors de vos cours de programmation. Vous remarquerez que trois nouvelles structures ont été ajoutées (surlignées par la couleur verte).
Source : JEP 431
Les collections séquencées introduisent une nouvelle API Java intégrée, améliorant les opérations sur les ensembles de données ordonnés. Cette API permet non seulement un accès pratique aux premier et dernier éléments d'une collection, mais permet également un parcours efficace, une insertion à des positions spécifiques et une récupération de sous-séquences. Ces améliorations rendent les opérations qui dépendent de l'ordre des éléments plus simples et plus intuitives, améliorant à la fois les performances et la lisibilité du code lorsque vous travaillez avec des listes et des structures de données similaires.
Voici la liste complète de l'interface SequencedCollection :
public interface SequencedCollection<E> extends Collection<E> { SequencedCollection<E> reversed(); default void addFirst(E e) { throw new UnsupportedOperationException(); } default void addLast(E e) { throw new UnsupportedOperationException(); } default E getFirst() { return this.iterator().next(); } default E getLast() { return this.reversed().iterator().next(); } default E removeFirst() { var it = this.iterator(); E e = it.next(); it.remove(); return e; } default E removeLast() { var it = this.reversed().iterator(); E e = it.next(); it.remove(); return e; } }
Alors, maintenant, au lieu de :
var first = myList.stream().findFirst().get(); var anotherFirst = myList.get(0); var last = myList.get(myList.size() - 1);
On peut juste écrire :
var first = sequencedCollection.getFirst(); var last = sequencedCollection.getLast(); var reversed = sequencedCollection.reversed();
Un petit changement, mais à mon humble avis, c'est une fonctionnalité tellement pratique et utilisable.
En raison de la similitude de la correspondance de modèles pour les switch et les modèles d'enregistrement, je vais les décrire ensemble. Les modèles d'enregistrement sont une nouvelle fonctionnalité – ils ont été introduits dans Java 19 (en avant-première). D'un autre côté, Pattern Matching pour switch est en quelque sorte une continuation de l'instance étendue d'expression. Il apporte une nouvelle syntaxe possible pour les instructions switch qui vous permet d'exprimer plus facilement des requêtes complexes orientées données.
Oublions les bases de la POO pour le bien de cet exemple et déconstruisons l'objet employé manuellement (employé est une classe POJO).
Avant Java 21, cela ressemblait à ceci :
if (employee instanceof Manager e) { System.out.printf("I’m dealing with manager of %s department%n", e.department); } else if (employee instanceof Engineer e) { System.out.printf("I’m dealing with %s engineer.%n", e.speciality); } else { throw new IllegalStateException("Unexpected value: " + employee); }
Et si nous pouvions nous débarrasser de la vilaine instanceof ? Eh bien, maintenant nous pouvons, grâce à la puissance du Pattern Matching de Java 21 :
switch (employee) { case Manager m -> printf("Manager of %s department%n", m.department); case Engineer e -> printf("I%s engineer.%n", e.speciality); default -> throw new IllegalStateException("Unexpected value: " + employee); }
While talking about the switch statement, we can also discuss the Record Patterns feature. When dealing with a Java Record, it allows us to do much more than with a standard Java class:
switch (shape) { // shape is a record case Rectangle(int a, int b) -> System.out.printf("Area of rectangle [%d, %d] is: %d.%n", a, b, shape.calculateArea()); case Square(int a) -> System.out.printf("Area of square [%d] is: %d.%n", a, shape.calculateArea()); default -> throw new IllegalStateException("Unexpected value: " + shape); }
As the code shows, with that syntax, record fields are easily accessible. Moreover, we can put some additional logic to our case statements:
switch (shape) { case Rectangle(int a, int b) when a < 0 || b < 0 -> System.out.printf("Incorrect values for rectangle [%d, %d].%n", a, b); case Square(int a) when a < 0 -> System.out.printf("Incorrect values for square [%d].%n", a); default -> System.out.println("Created shape is correct.%n"); }
We can use similar syntax for the if statements. Also, in the example below, we can see that Record Patterns also work for nested records:
if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) { //sth }
The Virtual Threads feature is probably the hottest one among all Java 21 – or at least one the Java developers have waited the most for. As JEP documentation (linked in the previous sentence) says, one of the goals of the virtual threads was to “enable server applications written in the simple thread-per-request style to scale with near-optimal hardware utilization”. However, does this mean we should migrate our entire code that uses java.lang.Thread?
First, let’s examine the problem with the approach that existed before Java 21 (in fact, pretty much since Java’s first release). We can approximate that one java.lang.Thread consumes (depending on OS and configuration) about 2 to 8 MB of memory. However, the important thing here is that one Java Thread is mapped 1:1 to a kernel thread. For simple web apps which use a “one thread per request” approach, we can easily calculate that either our machine will be “killed” when traffic increases (it won’t be able to handle the load) or we’ll be forced to purchase a device with more RAM, and our AWS bills will increase as a result.
Of course, virtual threads are not the only way to handle this problem. We have asynchronous programming (frameworks like WebFlux or native Java API like CompletableFuture). However, for some reason – maybe because of the “unfriendly API” or high entry threshold – these solutions aren’t that popular.
Virtual Threads aren’t overseen or scheduled by the operating system. Rather, their scheduling is handled by the JVM. While real tasks must be executed in a platform thread, the JVM employs so-called carrier threads — essentially platform threads — to “carry” any virtual thread when it is due for execution. Virtual Threads are designed to be lightweight and use much less memory than standard platform threads.
The diagram below shows how Virtual Threads are connected to platform and OS threads:
So, to see how Virtual Threads are used by Platform Threads, let’s run code that starts (1 + number of CPUs the machine has, in my case 8 cores) virtual threads.
var numberOfCores = 8; // final ThreadFactory factory = Thread.ofVirtual().name("vt-", 0).factory(); try (var executor = Executors.newThreadPerTaskExecutor(factory)) { IntStream.range(0, numberOfCores + 1) .forEach(i -> executor.submit(() -> { var thread = Thread.currentThread(); System.out.println(STR."[\{thread}] VT number: \{i}"); try { sleep(Duration.ofSeconds(1L)); } catch (InterruptedException e) { throw new RuntimeException(e); } })); }
Output looks like this:
[VirtualThread[#29,vt-6]/runnable@ForkJoinPool-1-worker-7] VT number: 6 [VirtualThread[#26,vt-4]/runnable@ForkJoinPool-1-worker-5] VT number: 4 [VirtualThread[#30,vt-7]/runnable@ForkJoinPool-1-worker-8] VT number: 7 [VirtualThread[#24,vt-2]/runnable@ForkJoinPool-1-worker-3] VT number: 2 [VirtualThread[#23,vt-1]/runnable@ForkJoinPool-1-worker-2] VT number: 1 [VirtualThread[#27,vt-5]/runnable@ForkJoinPool-1-worker-6] VT number: 5 [VirtualThread[#31,vt-8]/runnable@ForkJoinPool-1-worker-6] VT number: 8 [VirtualThread[#25,vt-3]/runnable@ForkJoinPool-1-worker-4] VT number: 3 [VirtualThread[#21,vt-0]/runnable@ForkJoinPool-1-worker-1] VT number: 0
So, ForkJonPool-1-worker-X Platform Threads are our carrier threads that manage our virtual threads. We observe that Virtual Threads number 5 and 8 are using the same carrier thread number 6.
The last thing about Virtual Threads I want to show you is how they can help you with the blocking I/O operations.
Whenever a Virtual Thread encounters a blocking operation, such as I/O tasks, the JVM efficiently detaches it from the underlying physical thread (the carrier thread). This detachment is critical because it frees up the carrier thread to run other Virtual Threads instead of being idle, waiting for the blocking operation to complete. As a result, a single carrier thread can multiplex many Virtual Threads, which could number in the thousands or even millions, depending on the available memory and the nature of tasks performed.
Let’s try to simulate this behavior. To do this, we will force our code to use only one CPU core, with only 2 virtual threads – for better clarity.
System.setProperty("jdk.virtualThreadScheduler.parallelism", "1"); System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1"); System.setProperty("jdk.virtualThreadScheduler.minRunnable", "1");
Thread 1:
Thread v1 = Thread.ofVirtual().name("long-running-thread").start( () -> { var thread = Thread.currentThread(); while (true) { try { Thread.sleep(250L); System.out.println(STR."[\{thread}] - Handling http request ...."); } catch (InterruptedException e) { throw new RuntimeException(e); } } } );
Thread 2:
Thread v2 = Thread.ofVirtual().name("entertainment-thread").start( () -> { try { Thread.sleep(1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } var thread = Thread.currentThread(); System.out.println(STR."[\{thread}] - Executing when 'http-thread' hit 'sleep' function"); } );
Execution:
v1.join(); v2.join();
Result:
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#23,entertainment-thread]/runnable@ForkJoinPool-1-worker-1] - Executing when 'http-thread' hit 'sleep' function [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
We observe that both Virtual Threads (long-running-thread and entertainment-thread) are being carried by only one Platform Thread which is ForkJoinPool-1-worker-1.
To summarize, this model enables Java applications to achieve high levels of concurrency and scalability with much lower overhead than traditional thread models, where each thread maps directly to a single operating system thread. It’s worth noting that virtual threads are a vast topic, and what I’ve described is only a small fraction. I strongly encourage you to learn more about the scheduling, pinned threads and the internals of VirtualThreads.
Les fonctionnalités décrites ci-dessus sont celles que je considère comme les plus importantes de Java 21. La plupart d'entre elles ne sont pas aussi révolutionnaires que certaines des choses introduites dans le JDK 17, mais elles restent très utiles et agréables à utiliser. avoir des changements de QOL (Qualité de Vie).
Cependant, vous ne devriez pas non plus négliger les autres améliorations du JDK 21 – je vous encourage vivement à analyser la liste complète et à explorer plus en détail toutes les fonctionnalités. Par exemple, une chose que je considère particulièrement remarquable est l’API Vector, qui permet des calculs vectoriels sur certaines architectures de processeur prises en charge – ce qui n’était pas possible auparavant. Actuellement, il est encore au stade d’incubateur/phase expérimentale (c’est pourquoi je ne l’ai pas présenté plus en détail ici), mais il est très prometteur pour l’avenir de Java.
Dans l'ensemble, les progrès réalisés par Java dans divers domaines témoignent de l'engagement continu de l'équipe à améliorer l'efficacité et les performances des applications à forte demande.
Si Java vous intéresse, n'oubliez pas de consulter certains de nos autres articles :
Voici les réponses à quelques questions courantes concernant le JDK 21, ainsi que l'interface et les fonctionnalités natives de Java.
Java SE (Java Platform, Standard Edition) est une plateforme fondamentale pour développer et déployer des applications Java sur des postes de travail et des serveurs.
Il s'agit d'une fonctionnalité d'aperçu qui permet aux programmes Java d'interagir avec des données et du code en dehors du runtime Java. L'API permet aux programmes Java d'appeler des bibliothèques natives et de traiter les données natives de manière plus sûre que dans le cas de JNI. L'API est un outil permettant d'accéder en toute sécurité à la mémoire et au code étrangers et d'invoquer efficacement des fonctions étrangères.
L'un des aspects clés est la révision du code (vous pouvez utiliser les outils de révision du code de l'IA pour rendre ce processus un peu moins long).
Le chargement dynamique en Java fait référence au chargement de classes ou de ressources au moment de l'exécution, plutôt que lors du démarrage initial du programme.
La concurrence structurée en Java est une approche qui organise les processus simultanés de manière contrôlée, visant à améliorer la maintenabilité, la fiabilité et l'observabilité du code multithread.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!