TL;DR
- JDK 24 (mars 2025) n'est pas un LTS mais embarque des features qui preparent le terrain pour JDK 25.
- Stream Gatherers (finalisees) permettent de creer des operations intermediaires custom sur les streams.
- Compact Object Headers reduisent la taille des headers d'objets de 12 a 8 octets. Ca sonne anodin, ca change tout pour l'empreinte memoire.
- Structured Concurrency et Scoped Values continuent leur progression en preview.
- Le fix du pinning des virtual threads avec synchronized est enfin la.
JDK 24 : pas un LTS, mais pas anodin
Tous les six mois, une nouvelle version de Java. C'est le rythme depuis JDK 10, et on s'y est habitues. Le reflexe de beaucoup de devs, c'est d'ignorer les versions non-LTS et d'attendre sagement le prochain LTS (JDK 25, prevu en septembre 2025).
C'est une erreur. JDK 24, sorti en mars 2025, finalise des JEPs qui trainaient en preview depuis des versions et en introduit d'autres qui vont devenir incontournables. Si tu veux etre pret pour JDK 25, c'est maintenant qu'il faut comprendre ce qui arrive.
Voici les features qui comptent vraiment, avec du code pour chacune.
Stream Gatherers : enfin des operations custom
C'est probablement la feature la plus attendue par les devs qui utilisent l'API Stream au quotidien. Depuis Java 8, les streams offrent map, filter, reduce, collect... mais pas de moyen propre de creer tes propres operations intermediaires.
Tu veux faire un "sliding window" sur un stream ? Un "distinct by key" ? Un "batch par groupes de N" ? Avant, tu devais soit ecrire un Collector custom (qui est une operation terminale, pas intermediaire), soit abandonner les streams pour une bonne vieille boucle for.
Stream Gatherers (JEP 485, finalise dans JDK 24) resout ca. Un Gatherer est une operation intermediaire custom que tu branches dans ton pipeline avec .gather().
// Regrouper les elements par paquets de 3
List<List<Integer>> batches = Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(Gatherers.windowFixed(3))
.toList();
// [[1, 2, 3], [4, 5, 6], [7]]
Les Gatherers builtin couvrent les cas classiques :
// Fenetre glissante de taille 3
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.toList();
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
// Fold : accumulation intermediaire (comme reduce, mais en stream)
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.fold(() -> 0, Integer::sum))
.toList();
// [15]
// Scan : accumulation avec emission de chaque etape
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
// [1, 3, 6, 10, 15]
Et tu peux creer les tiens. Un Gatherer custom se definit avec un initializer (etat initial), un integrator (comment traiter chaque element), un combiner (pour le parallelisme) et un finisher (quand le stream se termine).
// Gatherer custom : emettre seulement quand la valeur change
static <T> Gatherer<T, ?, T> distinctConsecutive() {
return Gatherer.ofSequential(
() -> new Object() { T last = null; boolean hasLast = false; },
(state, element, downstream) -> {
if (!state.hasLast || !Objects.equals(state.last, element)) {
state.last = element;
state.hasLast = true;
return downstream.push(element);
}
return true;
}
);
}
// Utilisation
Stream.of(1, 1, 2, 2, 3, 1, 1)
.gather(distinctConsecutive())
.toList();
// [1, 2, 3, 1]
C'est propre, c'est composable, et ca s'integre naturellement dans les pipelines existants.
Compact Object Headers : la memoire respire
Chaque objet Java a un header en memoire. Ce header contient des metadonnees : le hash code, les informations de verrouillage, le pointeur vers la classe. Historiquement, ce header fait 12 octets (avec les compressed oops actives, sinon 16).
12 octets, ca parait rien. Mais quand tu as des millions de petits objets (des Integer, des String courtes, des noeuds de structure de donnees), ca s'accumule. Sur une application avec 10 millions d'objets, c'est 120 Mo rien qu'en headers.
JEP 450 (Compact Object Headers), en experimental dans JDK 24, reduit ce header a 8 octets. 4 octets economises par objet. Sur nos 10 millions d'objets, ca fait 40 Mo de memoire recuperee. Pour les applications data-intensive, c'est significatif.
# Activer les Compact Object Headers (experimental)
java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders MonApp
Le gain varie selon les applications. Les benchmarks montrent des reductions de l'empreinte memoire de 10 a 20% sur des workloads typiques, avec un impact negligeable sur les performances CPU.
Structured Concurrency : la 4e preview
La programmation concurrente en Java, c'est historiquement un champ de mines. ExecutorService, Future, CompletableFuture... ca marche, mais c'est verbeux, c'est fragile, et la gestion des erreurs est un cauchemar.
Structured Concurrency (JEP 480, 4e preview dans JDK 24) propose un modele ou la duree de vie des taches concurrentes est liee a un scope syntaxique. Si le scope se ferme, les taches sont annulees. Si une tache echoue, les autres sont automatiquement arretees.
// Recuperer user + commandes en parallele
try (var scope = StructuredTaskScope.open()) {
Subtask<User> userTask = scope.fork(() -> fetchUser(userId));
Subtask<List<Order>> ordersTask = scope.fork(() -> fetchOrders(userId));
scope.join(); // Attend les deux taches
User user = userTask.get();
List<Order> orders = ordersTask.get();
return new UserProfile(user, orders);
}
// Si fetchUser echoue, fetchOrders est automatiquement annulee
// Pas de fuite de threads, pas de taches orphelines
Le try-with-resources garantit que tout est nettoye a la sortie du scope. C'est le meme principe que la gestion memoire structuree : les ressources sont liees a leur scope de creation.
Pourquoi encore en preview apres 4 iterations ? Parce que l'API evolue. Dans JDK 24, la factory method StructuredTaskScope.open() a remplace les sous-classes ShutdownOnFailure et ShutdownOnSuccess. L'API se simplifie a chaque preview.
Scoped Values : l'alternative a ThreadLocal
ThreadLocal est un des mecanismes les plus mal compris et les plus dangereusement utilises en Java. Il permet de stocker des valeurs liees a un thread, mais les fuites memoire sont frequentes (oubli de remove()), et avec les virtual threads, ca ne scale tout simplement pas.
Scoped Values (JEP 481, 4e preview) est le remplacement. Une valeur scopee est immutable, liee a un scope d'execution, et automatiquement nettoyee quand le scope se termine.
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
void handleRequest(Request req) {
User user = authenticate(req);
ScopedValue.runWhere(CURRENT_USER, user, () -> {
// Dans tout ce scope, CURRENT_USER.get() renvoie user
processRequest(req);
writeAuditLog(); // peut acceder a CURRENT_USER sans le passer en parametre
sendResponse();
});
// Ici, CURRENT_USER n'est plus accessible. Pas de fuite.
}
L'avantage principal : c'est immutable dans le scope. Pas de surprises, pas de modifications inattendues. Et c'est compatible avec les virtual threads et la structured concurrency.
Virtual Threads : le fix du pinning
Les virtual threads (JEP 444, finalisees dans JDK 21) avaient un probleme connu : quand un virtual thread entrait dans un bloc synchronized, il "pinnait" le carrier thread sous-jacent. En gros, le carrier thread etait bloque et ne pouvait plus executer d'autres virtual threads.
C'etait un vrai probleme en production. Si tu avais du code legacy avec beaucoup de synchronized (et en Java, il y en a partout), les virtual threads perdaient une grande partie de leur avantage.
JEP 491 dans JDK 24 resout ca. Les virtual threads peuvent maintenant etre demountes de leur carrier thread meme quand ils sont dans un bloc synchronized. Plus de pinning.
// Avant JDK 24 : ce code pinnait le carrier thread
synchronized (lock) {
// Operation bloquante (IO, sleep, etc.)
Thread.sleep(1000); // Le carrier thread est bloque pendant 1 seconde
}
// Apres JDK 24 : le carrier thread est libere pendant le sleep
// D'autres virtual threads peuvent l'utiliser
synchronized (lock) {
Thread.sleep(1000); // Le virtual thread est suspendu, le carrier est libre
}
Ca veut dire que tu peux migrer du code existant vers les virtual threads sans avoir a remplacer tous tes synchronized par des ReentrantLock. C'est un enorme gain de pragmatisme pour l'adoption en production.
Les autres JEPs qui meritent un coup d'oeil
JDK 24 contient 24 JEPs au total. Au-dela des principales, quelques-unes meritent d'etre mentionnees :
-
JEP 484 : Class-File API (finalisee). Une API pour lire, ecrire et transformer des fichiers
.class. Utile pour les frameworks qui font de la manipulation de bytecode (Spring, Hibernate). A terme, ca remplacera ASM comme dependance interne du JDK. -
JEP 478 : Key Derivation Function API (preview). Une API standard pour deriver des cles cryptographiques. Important pour la securite, surtout dans un monde post-quantique.
-
JEP 488 : Primitive Types in Patterns (2e preview). Tu peux utiliser des types primitifs dans les patterns de
switchet deinstanceof. Ca simplifie pas mal de code de conversion.
// Pattern matching avec primitifs
switch (statusCode) {
case int i when i >= 200 && i < 300 -> "Success";
case int i when i >= 400 && i < 500 -> "Client Error";
case int i when i >= 500 -> "Server Error";
default -> "Unknown";
}
Mon avis : preparer JDK 25 maintenant
JDK 24 n'est pas un LTS. En production, la plupart des equipes resteront sur JDK 21 jusqu'a JDK 25. C'est raisonnable.
Mais si tu attends JDK 25 pour decouvrir Stream Gatherers, Structured Concurrency et les Scoped Values, tu vas accumuler une dette de comprehension. Ces APIs changent la facon dont on ecrit du Java concurrent et fonctionnel. Elles meritent d'etre experimentees des maintenant, en developpement et en tests.
Le message de JDK 24, c'est que Java continue de se moderniser a un rythme soutenu. Les virtual threads tiennent leur promesse (avec le fix du pinning). Les streams deviennent vraiment extensibles. La gestion memoire s'ameliore sans effort de la part des devs.
Et pour ceux qui pensaient que Java etait un langage du passe : 24 JEPs en une seule release, ca ne ressemble pas a un langage qui ralentit.
Ressources
- JDK 24 Release Notes -- liste complete des JEPs
- JEP 485 : Stream Gatherers -- specification complete de l'API
- JEP 450 : Compact Object Headers -- details techniques de l'optimisation memoire
- JEP 480 : Structured Concurrency -- la 4e preview avec les changements d'API
- JEP 491 : Virtual Thread Pinning Fix -- le fix qui debloque l'adoption des virtual threads
- Inside Java -- blog officiel de l'equipe Java