Des articles approfondis sur les technologies qui façonnent l'avenir.

Java s'améliore sans cesse et personne ne le remarque

La cadence de six mois de Java a discrètement transformé le langage. Pattern matching, threads virtuels et records le rendent méconnaissable.

Une tasse de café lumineuse tandis qu'un vieux bureau terne se transforme en espace moderne et lumineux

Java est le Rodney Dangerfield des langages de programmation : personne ne le respecte. Mentionnez Java à la plupart des développeurs de moins de 30 ans et ils imagineront des fichiers de configuration XML, des enterprise beans et AbstractSingletonProxyFactoryBean. La réputation du langage est figée en 2008, à l'époque de Java 6, quand écrire du Java signifiait se noyer dans le code répétitif.

Pourtant, quelque chose de remarquable s'est produit depuis que Java est passé à une cadence de sortie de six mois en 2017. Le langage progresse à un rythme qui aurait été inimaginable pendant les écarts de quatre ans entre Java 6, 7 et 8. Records. Classes scellées. Pattern matching. Threads virtuels. Templates de chaînes. Concurrence structurée. Chaque version ajoute des fonctionnalités qui rendent le code Java plus court, plus expressif et plus agréable à écrire. Java 26 poursuit cette série, et l'écart entre « Java tel qu'il est » et « Java tel que les gens l'imaginent » n'a jamais été aussi grand.

Les fonctionnalités qui ont tout changé

Si vous n'avez pas écrit de Java depuis Java 8, vous avez manqué une décennie d'améliorations. Voici à quoi ressemble le langage aujourd'hui.

Records (Java 14) sont des classes de données immuables. Ce qui exigeait autrefois 50 lignes de code répétitif (champs, constructeur, getters, equals, hashCode, toString) tient désormais sur une seule ligne.

// Java 8: 50 lines of boilerplate
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
// Java 14+: one line
record Point(int x, int y) {}

Pattern matching (Java 16 à 21, toujours en évolution) permet de déconstruire des objets dans les expressions switch et les tests instanceof. Cela élimine les chaînes de if-else-instanceof en cascade qui encombraient le code Java.

// Pattern matching with sealed types and records
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Exhaustive pattern matching — compiler verifies all cases covered
double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var b, var h) -> 0.5 * b * h;
};
}

Threads virtuels (Java 21) sont des threads légers gérés par la JVM et non par le système d'exploitation. Vous pouvez en créer des millions sans manquer de mémoire ni de handles de threads OS. Cela rend le modèle « un thread par requête », le modèle de concurrence le plus simple, praticable à grande échelle.

// Create a million concurrent tasks — try this with OS threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
// Each task gets its own virtual thread
// Blocking I/O (HTTP, DB, file) automatically yields
var response = httpClient.send(request, BodyHandlers.ofString());
return response.body();
});
});
}
// All done — executor auto-closes, waits for completion

Les threads virtuels sont la réponse de Java aux goroutines de Go et aux coroutines de Kotlin. La différence : pas besoin de syntaxe particulière ni de colorer vos fonctions. Le code bloquant classique fonctionne, la JVM suspend et reprend les threads virtuels de façon transparente aux points de blocage. Votre client HTTP bloquant, votre driver JDBC et votre code d'E/S fichiers en profitent sans modification.

Pourquoi personne ne le remarque

La trajectoire d'amélioration de Java est vraiment impressionnante. Alors pourquoi la perception est-elle toujours figée en 2008 ?

D'abord, l'inertie des entreprises. Beaucoup d'organisations utilisent Java 8 ou 11, car mettre à niveau une grande base de code coûte cher et comporte des risques. Quand les développeurs parlent de « Java », ils parlent de la version utilisée par leur entreprise, pas de la dernière version. Les développeurs de Java 8 n'ont jamais connu les records, le pattern matching ou les threads virtuels.

Ensuite, la couche des frameworks. Spring Boot, Jakarta EE et d'autres frameworks d'entreprise ajoutent leur propre complexité au-dessus du langage. Les développeurs accusent « Java » de la lourdeur des annotations de Spring, alors que c'est le framework, pas le langage. Un Java moderne sans framework lourd est étonnamment léger.

Enfin, l'inertie culturelle. Dans le monde de la tech, les langages sont rattachés à des tribus. Java est le langage « entreprise », associé aux grandes sociétés, aux processus bureaucratiques et à la sur-ingénierie. Cette réputation était méritée : le développement Java vers 2005 méritait ces critiques. Mais les réputations survivent aux causes qui les ont fait naître, de plusieurs années.

L'avantage de la JVM

Les améliorations du langage comptent, mais le plus grand atout de Java reste la JVM. Après trois décennies d'optimisation, la JVM HotSpot est l'un des environnements d'exécution les plus sophistiqués jamais construits.

  • Compilation JIT. La JVM profile le code en cours d'exécution et compile les méthodes les plus sollicitées en code natif optimisé. Pour les applications longue durée (serveurs, traitement de données), les performances de la JVM égalent souvent, voire dépassent, celles du C++, car le JIT peut optimiser en fonction du comportement réel à l'exécution et non seulement d'une analyse statique.
  • Ramasse-miettes. Les implémentations modernes de GC (ZGC, Shenandoah) offrent des pauses inférieures à la milliseconde, même avec des téraoctets de tas. La critique « les pauses du GC tuent la latence » n'est plus vraie depuis des années : ces collecteurs fonctionnent en parallèle de l'application.
  • Observabilité. JFR (Java Flight Recorder) fournit un profilage utilisable en production avec un surcoût quasi nul. Vous pouvez profiler l'usage CPU, l'allocation mémoire, la contention sur les verrous, la latence des E/S et le comportement du GC sans ralentir l'application.
  • Maturité de l'écosystème. Maven Central compte des millions de paquets. Les outils de build (Gradle, Maven) sont bien maîtrisés. L'écosystème de tests (JUnit, Mockito, Testcontainers) est excellent. La chaîne de déploiement (conteneurs, images natives GraalVM) a considérablement mûri.

GraalVM et les images natives

La plus grande faiblesse de Java a toujours été son temps de démarrage. La JVM met des centaines de millisecondes à s'initialiser, charger les classes et chauffer le compilateur JIT. Pour les fonctions serverless et les outils en ligne de commande, ce surcoût est inacceptable.

GraalVM Native Image répond à ce problème en compilant les applications Java à l'avance (AOT) en exécutables autonomes. Plus besoin de JVM. Le temps de démarrage passe de centaines de millisecondes à un ordre de grandeur de quelques millisecondes. La consommation mémoire baisse aussi, car il n'y a ni interpréteur, ni compilateur JIT, ni métadonnées de classes à charger.

Le compromis : les images natives perdent l'optimisation adaptative de la JVM. Une application compilée à la volée devient plus rapide au fil du temps, à mesure que la JVM apprend les chemins chauds. Une image native démarre vite, mais ne s'améliore pas. Pour les processus éphémères (outils CLI, serverless), les images natives gagnent. Pour les serveurs longue durée, le JIT de la JVM finit par produire un code plus rapide. Choisissez l'outil adapté aux caractéristiques de votre runtime.

Java moderne face aux alternatives

Les langages censés avoir « remplacé » Java (Go, Kotlin, Rust) sont excellents. Mais Java moderne est plus compétitif qu'on ne le croit.

La simplicité de Go est séduisante, mais Java dispose désormais de threads virtuels (équivalents aux goroutines) et d'un système de types beaucoup plus riche. Les systèmes de types comptent pour les grandes bases de code, et celui de Java, notamment avec les types scellés et le pattern matching, détecte des erreurs que le système d'interfaces de Go laisse passer.

Kotlin améliore la syntaxe de Java, mais s'exécute sur la même JVM. La plupart des avantages de Kotlin sur Java 8 (sécurité face aux null, classes de données, coroutines) ont des équivalents partiels ou complets dans Java moderne (patterns Optional, records, threads virtuels). Kotlin reste plus concis, mais l'écart s'est nettement réduit.

Rust occupe une niche différente : la programmation système sans ramasse-miettes. Java n'a pas à se battre sur ce terrain. Mais pour les applications serveur, le traitement de données et les logiciels d'entreprise, l'association de performances, de sûreté et de maturité de l'écosystème de Java est difficile à battre.

La cadence de six mois, la vraie innovation

Plus que n'importe quelle fonctionnalité, le cycle de sortie de six mois a changé la trajectoire de Java. Avant 2017, les versions de Java étaient des événements tous les trois ou quatre ans, qui tentaient de tout livrer d'un coup. Des fonctionnalités prenaient du retard faute d'être prêtes, ce qui retardait l'ensemble de la version, ce qui poussait à tout embarquer dans la suivante, qui prenait à son tour du retard. Java 9 a mis plus de trois ans à sortir, en partie à cause de la complexité du système de modules.

Avec des versions tous les six mois, les fonctionnalités sortent quand elles sont prêtes. Le pattern matching a été livré progressivement de Java 16 à Java 24, chaque version ajoutant un élément. Les threads virtuels sont passés par deux versions en preview avant la livraison finale. Cette approche incrémentale permet à l'équipe de Java de recueillir des retours d'usage réels et d'ajuster les conceptions avant de figer des choix d'API définitifs.

La leçon dépasse Java : de petites versions fréquentes finissent par provoquer un changement profond. Chaque version de Java depuis la 17 est modeste prise isolément. Mises bout à bout, elles ont réinventé le langage. Si vous avez essayé Java il y a cinq ans et que sa verbosité vous a découragé, réessayez. Vous pourriez être surpris par ce que vous trouverez.