actu-image
Software engineering - 07 Avr 2026

Java 26, ce qu'il faut retenir

Article rédigé par
Achraf HASBI
Practice Leader, coach et Expert Java chez MARGO

Java 26, publié le 17 mars 2026, poursuit le rythme de parution semestriel d’Oracle en tant que première version non-LTS depuis Java 25. Alors que les équipes en entreprise suivant des cycles de mise à jour conservateurs pourraient s’en tenir à Java 25 (l’actuelle LTS), Java 26 apporte des améliorations significatives en termes de performance, de sécurité, d’expressivité du langage et de mise en réseau moderne.

Cette version comprend 10 JEPs : 5 finalisées, 4 en Preview et 1 en incubation. Dans cet article, nous explorerons les nouvelles fonctionnalités de Java 26, en mettant particulièrement l’accent sur les JEPs finalisées et leur impact sur le développement Java.

Note : Téléchargez l’OpenJDK 26 ici : https://jdk.java.net/26/


JVM / Nouvelles évolutions dans Java 26

JEP 516 : Ahead-of-Time Object Caching avec n’importe quel GC

La JEP 516 améliore le cache AOT afin qu’il puisse être utilisé avec n’importe quel ramasse-miettes, y compris le Z Garbage Collector (ZGC) à faible latence, en stockant les objets Java mis en cache dans un format neutre et agnostique du GC plutôt que dans un format spécifique à un GC.

Avant la JEP 516, le cache AOT stockait les objets dans un format qui était compatible au niveau des bits avec la disposition du tas des collecteurs Serial, Parallel et G1, mais fondamentalement incompatible avec ZGC.

Cela forçait les développeurs à choisir entre deux optimisations puissantes :

  1. Utiliser ZGC pour minimiser la latence de queue induite par le GC (pauses inférieures à 1ms).
  2. Utiliser le cache AOT pour accélérer le démarrage (par exemple, Spring PetClinic démarre 41 % plus vite).

La JEP 516 élimine ce compromis en stockant les références d’objets sous forme d’indices logiques au lieu d’adresses mémoire brutes, permettant à la JVM de diffuser et de matérialiser les objets mis en cache en mémoire au démarrage, quel que soit le GC utilisé.

Avant la JEP 516

# GC-specific format — raw memory address stored in reference field
value: 0x4002045278

Après la JEP 516

# GC-agnostic format — logical index stored in reference field
value: 5

La JVM applique une heuristique automatique au moment de la création du cache pour décider d’utiliser ou non le format diffusable (agnostique du GC). Elle s’active sous trois conditions lors de l’entraînement :

  1. ZGC a été utilisé (-XX:+UseZGC)
  2. Les pointeurs compressés (Compressed OOPs) ont été explicitement désactivés (-XX:-CompressedOops)
  3. Le tas était supérieur à 32 Go

Vous pouvez explicitement opter pour le format diffusable agnostique du GC en utilisant le nouveau drapeau -XX:+AOTStreamableObjects :

# Force streamable, GC-agnostic object caching
java -XX:+AOTStreamableObjects -cp app.jar com.example.App

JEP 522 : G1 GC : Améliorer le débit en réduisant la synchronisation

G1 est le ramasse-miettes par défaut de Java depuis Java 9, conçu pour équilibrer latence et débit. Pour accomplir sa tâche, G1 effectue son travail de manière concurrente avec l’application, ce qui signifie que les threads applicatifs et les threads GC partagent le processeur et doivent se synchroniser. Ce surcoût de synchronisation réduit le débit.

Pour suivre efficacement les références d’objets, G1 maintient une structure de données appelée card table, mise à jour à chaque fois qu’une référence d’objet change. Des threads d’optimisation en arrière-plan traitent cette card table, et le faire de manière sécurisée nécessite une synchronisation avec les threads applicatifs, ce qui conduit à un code de barrière d’écriture plus lent et plus complexe.

La JEP 522 introduit une seconde card table pour éliminer ce goulot d’étranglement de synchronisation. Les threads applicatifs écrivent sur une table sans aucune synchronisation, tandis que les threads d’optimisation travaillent sur l’autre. G1 permute atomiquement les deux tables selon les besoins.

Le résultat est un gain de débit de 5 à 15 % dans les applications qui modifient massivement les champs de référence d’objets, ainsi que jusqu’à 5 % de gains supplémentaires sur les architectures x64 grâce à un code de barrière d’écriture plus simple.


Core Libs

JEP 517 : HTTP/3 pour l’API HTTP Client

La JEP 517 met à jour le java.net.http.HttpClient — le client HTTP intégré ajouté dans le JDK 11 — pour supporter HTTP/3, le successeur de HTTP/2 standardisé par l’IETF en 2022.

HTTP/2 était une amélioration significative par rapport à HTTP/1.1, mais il fonctionne toujours sur TCP. Cela signifie qu’un seul paquet perdu bloque tous les flux jusqu’à sa retransmission — le redouté blocage en tête de ligne.

HTTP/3 résout ces problèmes en s’appuyant sur QUIC (Quick UDP Internet Connections), un protocole de transport basé sur UDP. Avec QUIC, chaque flux est indépendant, de sorte qu’un paquet perdu n’affecte que ce flux.

Avant la JEP 517 — HTTP/3 nécessitait une dépendance tierce

// Required external library e.g. Netty, Jetty, or Quiche4j
QuicChannel quicChannel = QuicChannel.newBootstrap(channel)
   .handler(new Http3ClientConnectionHandler())
   .remoteAddress(new InetSocketAddress("example.com", 443))
   .connect()
   .get();

Après la JEP 517 — HTTP/3 est une simple ligne d’activation sur le HttpClient standard

// Opt in at the client level — prefers HTTP/3, races HTTP/2 as fallback
var client = HttpClient.newBuilder()
   .version(HttpClient.Version.HTTP_3)
   .build();

// Or opt in at the request level — tries HTTP/3, falls back if unavailable
var request = HttpRequest.newBuilder(URI.create("https://example.com/api"))
   .version(HttpClient.Version.HTTP_3)
   .GET()
   .build();

var response = client.send(request, HttpResponse.BodyHandlers.ofString());

Les deux périmètres d’activation ont des stratégies de repli différentes. Définir HTTP_3 sur la HttpRequest envoie la première requête en utilisant HTTP/3 et revient à HTTP/2 ou HTTP/1.1 si une connexion HTTP/3 ne peut être établie à temps. Définir HTTP_3 sur le HttpClient lance à la place une course entre une connexion HTTP/3 et une connexion HTTP/2 ou HTTP/1.1 en parallèle, utilisant la première qui réussit.

La version par défaut du protocole reste HTTP/2, de sorte que tout le code existant continue de fonctionner sans changement.

JEP 500 : Préparer la transition pour rendre « Final » réellement définitif

La JEP 500 prépare l’écosystème Java pour une future version qui, par défaut, interdira la mutation des champs finaux via la réflexion profonde, rendant les programmes Java plus sûrs et potentiellement plus rapides.

Les champs finaux ont joué un rôle crucial dans le modèle de mémoire Java depuis le JDK 5 — leur immuabilité sous-tend l’initialisation sécurisée des objets dans un code multithread. Malheureusement, leur immuabilité entre également en conflit avec le fonctionnement des bibliothèques de sérialisation qui mutent les champs pour initialiser les objets lors de la désérialisation. Ce cas d’usage était suffisamment important pour justifier la modification de l’API de réflexion dans le JDK 5 afin qu’elle puisse être utilisée pour muter les champs finaux.

Avant la JEP 500, le code suivant compilait, s’exécutait et mutait un champ final silencieusement sans aucun avertissement :

// Before JEP 500 — silent final field mutation, no warning
class Person {
    final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

var person = new Person("Alice");
Field field = Person.class.getDeclaredField("name");
field.setAccessible(true);
field.set(person, "Bob"); // mutates a final field - no warning at all
System.out.println(person.getName()); // prints: Bob

Après la JEP 500, le même code déclenche un avertissement au runtime par défaut :

WARNING: Final field name in class Person has been mutated reflectively
        by class Application in unnamed module.
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning.
WARNING: Mutating final fields will be blocked in a future release.

Le comportement est contrôlé par une nouvelle option --illegal-final-field-mutation, similaire dans l’esprit à --illegal-access introduite dans le JDK 9 et --illegal-native-access dans le JDK 24. Elle fonctionne comme suit :

  • --illegal-final-field-mutation=allow autorise la mutation à se poursuivre sans avertissement.
  • --illegal-final-field-mutation=warn autorise la mutation mais émet un avertissement la première fois que le code d’un module particulier effectue une mutation illégale de champ final. Au plus un avertissement par module est émis. Ce mode est le défaut dans le JDK 26. Il sera progressivement supprimé dans une version future et, finalement, retiré.
  • --illegal-final-field-mutation=debug est identique à warn sauf qu’à la fois un message d’avertissement et une trace d’empilement sont émis pour chaque mutation illégale de champ final.
  • --illegal-final-field-mutation=deny aura pour résultat que Field::set lèvera une IllegalAccessException pour chaque mutation illégale de champ final. Ce mode deviendra le défaut dans une version future.

Le JDK 26 intègre également cela avec le JDK Flight Recorder via un nouvel événement jdk.FinalFieldMutation, qui enregistre chaque tentative de mutation avec une trace d’empilement complète — utile pour auditer des arbres de dépendances entiers avant l’arrivée du blocage définitif.

Par exemple, voici comment créer un enregistrement JFR puis afficher les événements jdk.FinalFieldMutation :

$ java -XX:StartFlightRecording:filename=recording.jfr ...
$ jfr print --events jdk.FinalFieldMutation recording.jfr

La JEP 500 fait partie de l’initiative plus large Integrity by Default, qui a fermé les failles de sécurité des API une JEP après l’autre : encapsulation forte des composants internes du JDK (JEP 396/403), restriction du chargement dynamique d’agents (JEP 451), dépréciation de l’accès mémoire sun.misc.Unsafe (JEP 471/498), restriction de l’usage de JNI (JEP 472), et cible désormais la mutation des champs finaux.

JEP 504 : Suppression de l’API Applet

La JEP 504 supprime l’intégralité du package java.applet du JDK 26, ce qui en fait la première version de Java livrée sans l’API Applet — 10 ans après le début de son processus de dépréciation.

La classe java.applet.Applet a été livrée avec le JDK 1.0 en 1996. À l’époque, la majorité des cas d’utilisation de Java tournaient autour des applets — de petits programmes intégrés dans les navigateurs web qui ajoutaient des capacités interactives impossibles avec le HTML des débuts. Le cycle de vie de ces programmes était entièrement géré par l’API java.applet.

Pour le code qui utilisait Applet comme conteneur d’interface utilisateur, l’API AWT fournit des alternatives. Pour le code qui utilisait AudioClip pour la lecture audio, la classe javax.sound.SoundClip introduite dans le JDK 25 est le remplacement recommandé. Pour le déploiement d’applications autonomes, jpackage crée des installateurs natifs pour les plateformes.


JEPs en Preview / Incubation dans Java 26

Bien que cet article se concentre sur les JEPs finalisées de Java 26 — les fonctionnalités que vous pouvez utiliser en toute sécurité en production aujourd’hui — la version comprend également plusieurs JEPs en preview et en incubation. Celles-ci représentent des fonctionnalités encore en évolution : les previews sont presque terminées mais peuvent subir des ajustements, et les incubateurs exposent des API pour obtenir des retours précoces. Voici un aperçu rapide de ce qui se profile à l’horizon.

JEP 524 : Encodages PEM des objets cryptographiques (Seconde Preview)

Introduit une API pour l’encodage et le décodage d’objets cryptographiques — clés publiques, clés privées, certificats et listes de révocation de certificats — vers et depuis le format largement utilisé PEM (Privacy-Enhanced Mail), le standard de fait en dehors du monde Java. Les deux classes centrales sont PEMEncoder et PEMDecoder, avec une interface marqueur DEREncodable réadaptée sur les types existants comme X509Certificate.

Il s’agit de sa seconde preview, avec plusieurs changements par rapport à Java 25 : la classe PEMRecord est renommée PEM et gagne une nouvelle méthode decode() ; les méthodes encryptKey sur EncryptedPrivateKeyInfo sont renommées encrypt et acceptent désormais des objets DEREncodable plutôt que seulement PrivateKey, permettant également le chiffrement d’objets KeyPair et PKCS8EncodedKeySpec ; et EncryptedPrivateKeyInfo gagne de nouvelles méthodes getKeyPair pour déchiffrer le texte encodé en PKCS#8 qui contient une PublicKey.

JEP 525 : Concurrence structurée (Sixième Preview)

La JEP 525 simplifie la programmation concurrente en traitant des groupes de tâches liées s’exécutant dans différents threads comme des unités de travail uniques, rationalisant la gestion des erreurs, l’annulation et l’observabilité. L’API se concentre sur StructuredTaskScope, qui confine les durées de vie des sous-tâches à une portée lexicale claire, garantissant qu’elles réussissent, échouent et sont annulées ensemble — éliminant les fuites de threads et les tâches orphelines courantes avec les modèles ExecutorService/Future.

Il s’agit de sa sixième preview, s’appuyant sur la restructuration significative du JDK 25 qui a remplacé les constructeurs publics par des méthodes de fabrique statiques. La JEP 525 se concentre sur de petits raffinements incrémentaux : un nouveau rappel onTimeout() sur Joiner permettant aux joiners personnalisés de réagir aux délais d’expiration et de renvoyer des résultats partiels ou de repli au lieu de toujours lever une exception ; Joiner.allSuccessfulOrThrow() renvoie désormais directement une List<T> au lieu d’un flux de descripteurs Subtask ; anySuccessfulResultOrThrow() a été renommé anySuccessfulOrThrow() par souci de brièveté ; et la surcharge StructuredTaskScope.open() attend désormais un UnaryOperator<Configuration> au lieu d’une Function pour un typage plus strict.

JEP 526 : Constantes différées (Seconde Preview)

La JEP 526 introduit java.lang.LazyConstant<T> — un détenteur de valeur immuable qui est initialisé au plus une fois, garanti sûr pour les threads, combinant la sécurité des champs final avec la flexibilité d’une initialisation différée. Elle répond à une tension de longue date en Java : les champs final imposent l’immuabilité mais nécessitent une initialisation immédiate au moment de la construction, tandis que les champs mutables permettent une initialisation différée mais sacrifient les garanties de sécurité des threads et les optimisations de repliement de constantes de la JVM.

// Replaces double-checked locking, volatile fields, and holder class idioms
private static final LazyConstant<ConnectionPool> POOL =
   LazyConstant.of(() -> new ConnectionPool(MAX_CONNECTIONS));

// Access — initializes on first call, cached forever after
pool.get().acquire();

// Lazy collections — each slot initializes independently on first access
static final List<OrderController> ORDERS =
   List.ofLazy(10, _ -> new OrderController());

JEP 530 : Types primitifs dans les motifs, instanceof et switch (Quatrième Preview)

Étend le filtrage par motif, instanceof et switch afin que les types primitifs (par ex. int, long, float, double) puissent être utilisés dans tous les contextes de motifs, et pas seulement les types de référence. Cela élimine les frictions lors de l’intégration des primitifs avec les fonctionnalités modernes de filtrage par motif et rend le langage plus uniforme.

// Primitive types in switch patterns (preview)
Object value = getSomeValue();
switch (value) {
   case int i when i > 0  -> System.out.println("Positive int: " + i);
   case long l             -> System.out.println("Long: " + l);
   case double d           -> System.out.println("Double: " + d);
   default                 -> System.out.println("Other: " + value);
}

Il s’agit de sa quatrième preview — précédemment présentée en preview dans Java 23, 24 et 25.

JEP 529 : API Vector (Onzième incubation)

L’API Vector fournit un moyen explicite d’exprimer des calculs vectoriels de style SIMD qui correspondent à des instructions vectorielles CPU optimales au moment de l’exécution. Il s’agit de sa onzième incubation — elle reste en état d’incubation en attendant l’achèvement du Projet Valhalla, qui fournira les types de valeurs dont l’API a besoin pour être finalisée efficacement.


Conclusion

Java 26 réaffirme le rythme d’évolution semestriel régulier de la plateforme. Les JEPs finalisées apportent des améliorations concrètes et prêtes pour la production : un démarrage plus rapide désormais compatible avec ZGC, un meilleur débit pour G1 dès le départ, le support réseau HTTP/3, et un nettoyage d’API attendu depuis longtemps. Les fonctionnalités en preview et en incubation — constantes différées, concurrence structurée, motifs primitifs et encodages PEM — laissent présager la maturation continue des Projets Loom, Project Amber et, à terme, du Projet Valhalla.

Comme toujours, adopter les fonctionnalités stables dès aujourd’hui tout en gardant un œil sur celles qui émergent vous assurera d’obtenir le meilleur des deux mondes — fiabilité et innovation.

Java 26 est-elle une version LTS (Long-Term Support) ?

Non, Java 26 est une version standard avec 6 mois de support. La version LTS actuelle reste Java 25. Java 26 est idéal pour tester les dernières innovations avant qu’elles ne soient stabilisées dans une future version LTS.

Pourquoi la mutation des champs finaux déclenche-t-elle désormais un avertissement ?

Cela fait partie de l’initiative « Integrity by Default ». Autoriser la modification de champs immuables via la réflexion compromet les optimisations du compilateur et la sécurité des données. À terme, cette pratique sera totalement bloquée (mode deny).

Puis-je utiliser HTTP/3 avec n’importe quel serveur ?

Le HttpClient de Java 26 supporte HTTP/3, mais le serveur distant doit également supporter ce protocole (basé sur QUIC/UDP). Si le serveur ne le supporte pas, le client Java basculera automatiquement vers HTTP/2 ou HTTP/1.1 selon votre configuration.

Quel est l’avantage concret du cache d’objets AOT pour ZGC ?

Avant Java 26, les utilisateurs de ZGC ne pouvaient pas bénéficier de la mise en cache des objets au démarrage. Désormais, ils peuvent combiner la latence ultra-faible de ZGC (pauses < 1ms) avec un démarrage d’application nettement plus rapide (jusqu’à 41% sur certains frameworks).

Quelles sont les principales différences entre Java 25 et Java 26 ?

Alors que Java 25 se concentrait sur la stabilité en tant que version LTS avec 18 JEPs, Java 26 introduit 10 JEPs centrées sur le réseau moderne (HTTP/3) et l’optimisation des performances de la JVM. Java 26 renforce également la sécurité en avertissant contre la mutation des champs finaux, une pratique qui était encore silencieuse dans Java 25.

Dois-je passer de Java 25 à Java 26 ?

Si vous privilégiez la stabilité et le support à long terme, restez sur Java 25 (LTS). Cependant, si votre application nécessite le support HTTP/3 ou si vous utilisez ZGC et souhaitez des temps de démarrage plus rapides, passer à Java 26 vous offrira des avantages techniques immédiats.