Dernière modification : 02/01/2026

Évolution de Java de 8 à 21 : Les nouveautés majeures du langage

 

Java 8 (2014) a marqué un tournant majeur avec les lambdas et l’API Stream par exemple, introduisant une programmation plus fonctionnelle. Depuis, Java adopte un rythme de sortie semestriel, accélérant fortement l’évolution du langage, des bibliothèques et de la JVM.

De Java 8 à Java 21, plus de 230 évolutions majeures (JEP) ont été intégrées. La plupart sont disponibles par défaut, tandis que certaines fonctionnalités en preview, expérimentales ou incubatrices nécessitent une activation explicite.

 

Depuis Java 8, le langage Java a beaucoup évolué. Voici un récapitulatif des améliorations syntaxiques et sémantiques majeures apportées ces dernières années :

  • Pattern Matching pour switch (Java 21) – Le mot-clé switch supporte désormais les patterns (modèles) de type avec "garde", au-delà des simples valeurs de constantes. Cela permet d’évaluer des variables de n’importe quel type et de filtrer selon des conditions.

    Object o = 15;
    String description = switch (o) {
        case Integer i when i > 10 -> "Entier large (" + i + ")";
        case Integer i             -> "Entier petit (" + i + ")";
        case Long l                -> "Un Long (" + l + ")";
        default                    -> "Autre chose";
    };
    // Pour o = 15, description vaudra "Entier large (15)"

 

  • Records Patterns (Java 21) – Les patterns s’étendent également aux records, permettant de déconstruire des objets à l’intérieur des conditions instanceof ou des switch. On peut ainsi extraire en une fois plusieurs propriétés imbriquées.

    if (obj instanceof ColoredPoint(Point2D(int x, int y), Color c)) {
        System.out.printf("Point de couleur %s aux coordonnées (%d,%d)%n", c, x, y);
    }

 

  • Variables anonymes (Java 21, Preview : Unnamed Variables) – Il est possible d’ignorer purement et simplement une variable dont on n’a pas usage en la nommant _.

    var _ = maListe.add(element);       // on ignore le booléen retourné par add
    try {
        // ...
    } catch (Exception _) {
        // on ignore l'objet d'exception attrapé
    }
    liste.stream().map(_ -> /* ... */).toList();  // on ignore le paramètre du lambda

 

  • Motifs anonymes (Java 21, Preview : Unnamed Patterns)

    if (p instanceof Point(int x, _)) {
        System.out.println("Point avec x = " + x);
    }

 

  • Templates de chaînes de caractères (Java 21, Preview : String Templates) – Il s’agit d’un système d’interpolation de chaînes sécurisé et extensible. Entre le délimiteur STR."", on peut insérer des expressions Java entre accolades, qui seront évaluées et intégrées dans la chaîne.

    String nom = "Duke";
    String message = STR."Je m'appelle \{nom}";
    // message vaudra : "Je m'appelle Duke"

 

  • Classes anonymes et méthodes d’instance main (Java 21, Preview : Unnamed Classes and Instance Main Methods) – Il est désormais possible de définir une classe sans nom directement dans un fichier source, avec une méthode main non statique.

    void main() {
        System.out.println("Bonjour tout le monde !");
    }

 

  • Classes scellées (Java 17 : Sealed Classes) – Le mot-clé sealed permet de restreindre explicitement quelles classes peuvent hériter d’une classe ou implémenter une interface.

    public abstract sealed class Forme permits Cercle, Rectangle { ... }
    public final class Cercle extends Forme { ... }     // OK
    public final class Rectangle extends Forme { ... }  // OK
    public final class Triangle extends Forme { ... }   // Erreur de compilation

    Un avantage supplémentaire est que, si l’on énumère exhaustivement les sous-types scellés dans un switch, le cas default devient inutile – le compilateur sait qu’il n’y a pas d’autres possibilités.

    double aire = switch (forme) {
        case Cercle c    -> Math.PI * c.rayon() * c.rayon();
        case Rectangle r -> r.longueur() * r.largeur();
    }; // pas de default nécessaire car tous les sous-types de Forme sont couverts

 

  • Records (Java 16) – Les records introduisent une syntaxe ultra-compacte pour définir des classes immuables portant des données. Un record s’écrit en spécifiant simplement ses champs, et le compilateur génère automatiquement le constructeur, les accesseurs, les méthodes equals(), hashCode() et toString().

    record Point(int x, int y) { }
    Point p = new Point(1, 2);
    int a = p.x();  // vaut 1
    int b = p.y();  // vaut 2

 

  • Pattern Matching pour instanceof (Java 16) – Il n’est plus nécessaire de faire un cast explicite après un test instanceof. Java permet d’associer directement une variable au résultat du test de type.

    Object animal = "chat";
    if (animal instanceof String s && s.length() > 5) {
        System.out.println("Chaine en majuscules : " + s.toUpperCase());
    }

 

  • Blocs de texte (Java 15 : Text Blocks) – Java prend désormais en charge les strings sur plusieurs lignes de façon native, sans avoir à concaténer manuellement ou insérer des \n.

    String html = """
        <html>
            <body>
                <p>Bonjour, monde</p>
            </body>
        </html>
        """;

 

  • NullPointerExceptions plus explicites (Java 15) – Un effort a été fait pour rendre les NPE plus compréhensibles.

    a.b.c = 100;
    // Provoque : Exception in thread "main" java.lang.NullPointerException:
    //   Cannot read field "c" because "a.b" is null

 

  • Expressions switch (Java 14 : Switch Expressions) – Le switch en Java peut désormais être utilisé comme expression produisant une valeur, avec une syntaxe simplifiée.

    DayOfWeek jour = DayOfWeek.MONDAY;
    int nbLettres = switch (jour) {
        case MONDAY, FRIDAY, SUNDAY -> 6;
        case TUESDAY                -> 7;
        case WEDNESDAY, THURSDAY    -> 8;
        case SATURDAY               -> 8;
        default -> {
            String nom = jour.toString();
            yield nom.length();
        }
    };

 

  • Inference de type locale avec var (Java 10, étendu aux lambdas en Java 11) – Il est possible de déclarer les variables locales sans préciser explicitement leur type, en utilisant le mot-clé var. Le compilateur inférera automatiquement le type à partir de l’expression d’initialisation.

    var compteur = new HashMap<String, Integer>(); // type inféré : HashMap<String,Integer>
    var keys = compteur.keySet();                  // type inféré : Set<String>
    var values = compteur.values();                // type inféré : Collection<Integer>

    A noter que les var ne peuvent pas être initialisés avec le mot clé null.

 

  • Système de modules Java (Java 9) – Avec le Project Jigsaw, Java 9 a introduit le module system, une refonte de l’organisation du JDK en modules explicites. Ceci permet de mieux encapsuler les API internes et d’éviter les conflits de classe au runtime. Les développeurs peuvent déclarer un module dans un fichier module-info.java :

    module com.example.monmodule {
        requires org.util.dep;    // dépendance à un autre module
        exports com.example.api;  // paquet exporté
    }

 

  • Méthodes privées dans les interfaces (Java 9) – Les interfaces Java 8 avaient introduit les méthodes par défaut (default methods). Java 9 permet en plus de définir des méthodes private dans les interfaces. Celles-ci servent uniquement de fonctions utilitaires internes à l’interface, pouvant être appelées par les méthodes par défaut pour factoriser du code, sans faire partie de l’API publique de l’interface.

 

  • Opérateur diamant (<>) avec les classes anonymes (Java 9) – Le diamond operator, qui évite de répéter les types génériques lors de l’instantiation (introduit en Java 7), fonctionne désormais aussi avec les classes internes anonymes. Cela réduit la verbosité lorsque l’on crée une instance anonyme d’une classe générique.

 

  • Variables effectively final dans les try-with-resources (Java 9)

 

  • Annotation @SafeVarargs étendue (Java 9) – L’annotation @SafeVarargs peut maintenant être appliquée aux méthodes d’instance private (en plus des méthodes static et final). Cela permet d’éviter les avertissements de type “unsafe operation” sur les méthodes varargs internes qui sont sûres par construction.

 

  • Plus d’avertissement de dépréciation sur les imports (Java 9) – Le compilateur ne produit plus de warning lorsqu’on importe une classe marquée @Deprecated.

 

Nouvelles API

Passons aux évolutions de la bibliothèque standard (JDK) de Java. De nombreuses API nouvelles ou méthodes utilitaires ont vu le jour depuis Java 8, facilitant le développement au quotidien. Voici un tour d’horizon des principales modifications.

  • Collections séquencées (Java 21) – Une nouvelle famille d’interfaces de collections, dites sequenced, permet de manipuler l'ordre d’itération de façon uniforme pour les listes, ensembles et maps ordonnés. Par exemple, SequencedCollection est implémenté par LinkedHashSet et ArrayList, ce qui offre des méthodes comme getFirst(), getLast(), removeFirst(), reverse() etc., peu importe le type de collection.

  • Math.clamp (Java 21) – Une méthode utilitaire Math.clamp(value, min, max) borne une valeur à un intervalle donné. Elle renvoie min si la valeur est inférieure, max si elle est supérieure, ou la valeur elle-même si elle est dans les bornes.

  • StringBuffer.repeat / StringBuilder.repeat (Java 21) – Ces deux classes disposent d’une méthode repeat(int) qui permettent d’ajouter des répétitions d’un caractère ou d’une séquence, sans avoir à écrire de boucle manuelle.

  • String.splitWithDelimiters (Java 21) – La classe String (et Pattern) gagne une variante de split qui, en plus des tokens, retourne les délimiteurs correspondants dans le tableau de résultats.

  • BigInteger.parallelMultiply (Java 19) – Cette méthode utilise des algorithmes parallèles pour accélérer le calcul sur des entiers très grand.

  • Constante BigDecimal.TWO (Java 19) – Une constante pour le BigDecimal 2 a été ajoutée.

  • Math.divideExact (Java 18) – Equivalent pour la division de ce que Math.addExact et Math.multiplyExact offrent pour l’addition et la multiplication. Ces méthodes réalisent l’opération sur int ou long et lancent une exception si un overflow arithmétique se produit.

  • Jeu de caractères par défaut en UTF-8 (Java 18) – Toutes les API Java qui dépendaient du charset par défaut du système utilisent désormais UTF-8 par défaut.

  • InstantSource (Java 17) – Une interface fonctionnelle introduite dans java.time pour abstraire la notion d’instant présent. L’idée est de découpler l’obtention du temps courant (Instant) de la zone horaire, en se focalisant uniquement sur le temps UTC.

  • HexFormat (Java 17) – Une nouvelle classe utilitaire pour formater en hexadécimal. On peut par exemple convertir un nombre en sa représentation hexadécimale zéro-complétée ou inversement.

  • Stream.toList() (Java 16) – Une méthode pour collecter un flux dans une liste non modifiable, sans avoir à écrire Collectors.toList().

    List<String> res = Stream.of("one", "two", "three", "four")
        .filter(s -> s.length() == 3)
        .toList();

 

  • Stream.mapMulti() (Java 16) – Une nouvelle méthode de flux en alternative à flatMap. Elle permet, pour chaque élément, d’émettre zéro, un ou plusieurs éléments dans le reste du flux pour un élément initial.

    List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
    double percentage = 10;
    List<Double> result = intList.stream()
      .<Double>mapMulti((integer, consumer) -> {
        if (integer % 3 == 0) {
            consumer.accept((double) integer * ( 1 + percentage));
        }
      })
      .collect(toList());
    
    // Equivalent
    
    List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
    double percentage = 10;
    List<Double> result = intList.stream()
      .filter(integer -> integer % 3 == 0)
      .<Double>map(integer -> ((double) integer * ( 1 + percentage)))
      .collect(toList());
  • Client HTTP standard (Java 11) – Une API HTTP modernisée est intégrée à Java. Elle supporte HTTP/2, les WebSockets et un mode entièrement non bloquant. Cette API remplace HttpURLConnection.

    HttpClient httpClient = HttpClient.newBuilder().build();
    
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://www.laulem.com/"))
        .GET()
        .build();
    
    HttpResponse<String> response =
        httpClient.send(request, BodyHandlers.ofString());

 

  • Nouvelles méthodes via String (Java 11) – Java 11 a enrichi la classe String avec plusieurs méthodes utilitaires : isBlank() (vérifie si la chaîne est vide ou ne contient que des espaces blancs), lines() (retourne un Stream de lignes d’une chaîne multi-lignes), repeat(n) (répète la chaîne n fois) et strip() (similaire à trim() mais supprime tous les espaces Unicode).

  • Fabriques immuables pour collections (Java 9) – Pour créer rapidement des collections non modifiables, on dispose désormais des méthodes List.of(...), Set.of(...) et Map.of(...).

  • API de flux réactifs (Flow) (Java 9) – Introduite dans java.util.concurrent, cette API suit le standard Reactive Streams et définit les interfaces Publisher, Subscriber, Subscription et Processor. Cela permet d’implémenter des traitements asynchrones publish-subscribe avec back-pressure (contre-pression).

  • Améliorations de CompletableFuture (Java 9)

  • Nouveaux opérateurs sur les Streams et Optionals (Java 9) – L’API Stream s’est enrichie de méthodes : dropWhile(predicate) et takeWhile(predicate), iterate, ofNullable(x). Côté Optional, la méthode stream() permet de transformer un Optional en Stream. Des collecteurs vers des collections immuables ont été ajoutés (Collectors.toUnmodifiableList()).

  • Arrays.mismatch (Java 9) – Une méthode utilitaire qui compare deux tableaux élément par élément et renvoie l’index du premier élément différent, ou -1 s’ils sont identiques sur toute la longueur commune.

  • System.Logger (Java 9) – Une API de logging simplifiée a été introduite. Elle permet aux classes de la plateforme d’utiliser une interface de log standard (System.getLogger(name)) qui peut être reliée à l’implémentation de logging choisie par l’application.

  • VarHandle (Java 9) – Une nouvelle API bas niveau pour manipuler de façon atomique des champs et éléments de tableau.

  • Politique de dépréciation améliorée (Java 9) – L’annotation @Deprecated dispose d’un élément forRemoval=true/false indiquant si l’élément est prévu pour suppression future.

 

Concurrence

Virtual Threads (Java 21) — Threads légers gérés par la JVM permettant de créer des milliers/millions de tâches concurrentes sans le coût élevé des threads classiques liés au système d’exploitation. Chaque virtual thread est une instance de java.lang.Thread non liée à un OS-thread : quand il bloque (I/O, sleep, wait, join etc), la JVM libère le thread OS pour d’autres tâches, améliorant la scalabilité d’applications intensives en I/O.

void main() {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        IntStream.range(0, 500).forEach(i ->
                executor.submit(() -> {
                    System.out.println(Thread.currentThread() + " exécute la tâche " + i);
                    return null;
                })
        );
    }
}

⚠️Note : Les virtual threads ne s’exécutent pas tous en parallèle. Si un virtual thread se bloque sur une opération non reconnue comme bloquante par la JVM (par exemple un appel natif, une I/O bloquante legacy ou un mécanisme de synchronisation non compatible), il reste attaché à son carrier thread (pinning), ce qui monopolise ce thread OS et empêche d’autres virtual threads de progresser.

 

 

Conclusion

Depuis Java 9, le rythme de sortie semestriel a profondément modernisé Java. Migrer de Java 8 vers Java 17, 21 ou supérieur apporte des gains majeurs en performances, sécurité, productivité et expressivité du langage, grâce à de nombreuses fonctionnalités et API modernes. Avec plus de 230 évolutions intégrées, adopter une version récente est aujourd’hui un choix essentiel pour bénéficier d’un Java plus efficace, sûr et pérenne.

Aujourd’hui, seules les versions encore officiellement supportées doivent être privilégiées en production. Elles bénéficient de correctifs de sécurité, de mises à jour régulières et d’un écosystème actif, garantissant stabilité, performances et pérennité des applications. Choisir une technologie supportée, c’est réduire les risques techniques et faciliter l’évolution future des systèmes. Vous trouverez ici la roadmap des versions encore supportées : OpenJDK on RHEL / Temurin.

 

 

Sources

L'écriture de cet article s'appuie sur les articles suivants :

  • https://learn.microsoft.com/fr-fr/java/openjdk/transition-from-java-8-to-java-11
  • https://advancedweb.hu/a-categorized-list-of-all-java-and-jvm-features-since-jdk-8-to-21/
  • https://experienceleaguecommunities.adobe.com/t5/adobe-experience-manager-blogs/upgrading-from-java-11-to-java-21-a-guide-for-aem-developers/ba-p/734564
  • https://openjdk.org/jeps/0

 

À noter que cet article, pour des besoins de test, a été en partie rédigé par une IA et presque entièrement reformulé, complété et vérifié par un humain.