Tout langage s’exécutant sur une JVM ayant accès à la bibliothèque standard, les améliorations d’API ne seront pas un élément réellement différenciant. Néanmoins, il est intéressant de voir si les évolutions qui servent à Java servent également aux autres.
La bonne surprise de Java 9 ! S’il y a bien une bibliothèque utilisée au quotidien dans le monde du Web qui utilise massivement des backend Java, c’est bien la bibliothèque HTTP !
On ne compte plus les années passées à utiliser la très standard Apache Http Client ! Plus récemment, OkHttp a surfé sur une popularité bien méritée.
Ce qui surprend moins, c’est que le nouveau client Http pour Java adopte le même style “fluent” que OkHttp, en utilisant force builders pour permettre un DSL agréable à utiliser.
Alors, killer feature ou marronnier ? D’un côté, Java propose ce que Kotlin et Scala n’avaient pas non plus. D’un autre, tout le monde s’était habitué à utiliser autre chose !
OkHttp en Java, KoHttp en Kotlin (😉)... chaque bibliothèque proposant un DSL adapté à un paradigme POO (Programmation Orientée Objet) ou PF (Programmation fonctionnelle) et à un langage. On notera que Scala dispose d’un choix pléthorique, dont Akka Http, http4s, ZIO-web, … et de qualité.
En adoptant tardivement un client HTTP propre, Java a raté le virage du style d'architecture REST et des API HTTP en général. Ici, le langage rattrape un peu le retard grâce à un client compatible HTTP/2. Il ne manque plus qu’un protocole RPC moderne pour éviter de se reposer sur ceux des GAFA (exemple: gRPC)...
Le client HTTP de Java 9 est donc une bonne nouvelle pour les projets en Java, mais résonne comme un coup de canon en direction de l’océan pour les autres langages : ça fait du bruit mais pas d’impact.
Sur l’une des classes les plus centrales, un certain nombre de nouveautés bénéficieront forcément aussi aux autres langages.
La critique adressable à cette classe obèse, c’est l’absence de refactoring. Car en dehors d’être une “séquence de caractères”, String
commence vraiment à être une bibliothèque spécialisée dans le traitement de texte Unicode !
String::join
Ce nom très spécialisé sémantiquement appartient à la catégorie des fonctions de réductions. Scala possédait déjà mkString
, généralisée sur toutes les collections “traversables une fois”.
C’est Java 1.8, peut-être grâce à la thématique des Stream
et les opérations de traitement de flux et collections, qui a apporté ce nouveau membre à String
.
CharSequence::isEmpty
Cette méthode initialement sur String
remonte à l’interface CharSequence
. Rappel : contrairement à Scala, Java n’a pas de cohérence entre une CharSequence
et les autres séquences définies dans les collections. C’est à cet endroit que la définition initiale de isEmpty
devrait se trouver, non ?
Ce n’est qu’à partir de Java 9 qu’on voit apparaître des générateurs de Stream
supplémentaires sur String
: chars
et codePoints
(via l’interface CharSequence
). Il faudra attendre Java 11 pour le générateur lines
.
Java 11 apporte sur String un lot de méthodes qui auraient pu faire partie de la classe depuis longtemps. Comme avec le client HTTP, c’est la fondation Apache qui a longtemps pallié ce défaut au travers d'Apache Commons. On retrouve donc intégrées en standard les incontournables isBlank
, strip*
, mais aussi repeat
.
Java 12 prépare l’arrivée des chaînes littérales multi-lignes en preview dans Java 13 avec la méthode indent
et Java 15 ajoute stripIndent
et translateEscapes
pour la version finale des fameux blocs de texte littéraux.
Enfin, la méthode transform
est juste un nom particulier pour map. Alors pourquoi pas “map” ? Parce qu’il n’a pas été prévu de rajouter toutes les fonctions classiques de la PF sur les collections ; pour éviter la confusion (“tiens, alors String
est une monade maintenant ?”), le nom “transform” a été retenu. Pour les map/flatMap/fold, veuillez passer par les Streams.
Oh ! Mais on a oublié de mettre une méthode pour transformer une String
en Stream<Character>
...
Encore un point perdu par Java qui semble mettre un point d’honneur à ne pas rattraper complètement les langages plus récents !
Java 12 ajoute les implémentations de l’interface Constable
. Cela ne sert qu’un but très technique lié à des améliorations du bytecode, que nous verrons dans un prochain article.
Si Java ajoute des méthodes pratiques et améliore les performances, choses dont profiteront les autres langages, il rate complètement le virage fonctionnel qu’il avait pris à partir de Java 8 ; de même qu’une amélioration de la cohérence de cette classe ! On dirait que dans le monde Java, le fonctionnel est ghettoïsé dans la classe Stream
, bien gardé par une armée d’exceptions.
Kotlin et Scala disposent chacun d’un moyen propre d’étendre les fonctionnalités d’une classe existante, et proposent tous deux une méthode qui transforme une String
en véritable séquence de caractères. Par rapport à Java, on y gagne de la simplicité et les classiques filter
, map
, flatMap
, fold
, etc.
Java 12 apporte la possibilité de formater les nombres dans le traditionnel human readable format des commandes UNIX ou du langage parlé. Ainsi, formater 1 000 et 1 000 000 va respectivement donner “1K” et “1M” avec le code :
NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.SHORT);
… pourvu que la locale passée supporte ce style.
Java 14 apporte le support d’un format comptable normalisé avec la localisation Unicode “u-ufc-accound”. Avec la localité Locale.US, “-$3.27” devient “($3.27)”.
Si ces améliorations de formatage localisé semblent importantes et notamment pour le respect de certaines normes, j’ai de gros doutes sur la faculté des utilisateurs à les intégrer. Ce type d’amélioration arrive au compte-goutte dans l’API Java, or mon expérience du milieu bancaire tendrait à montrer que des bibliothèques non-officielles (mais implémentant totalement une norme), voire du code interne souvent erratique seront souvent préférés à une norme incomplète. J’ai parfois eu du mal à rationaliser des développements métier en ce sens (comme le permet l’API de localisation de Java), afin de supprimer des piles d’options et d'algorithmes de formatage.
Des améliorations ont clairement été faites en Java 9 (et un peu en 10) sur la construction de collections.
La plupart des collections ont désormais une factory method (méthode usine) of qui permet de construire une collection en un seul appel. Elle prend jusqu’à 10 arguments indépendants, puis au-delà vous utiliserez la signature avec vararg.
On remarquera en examinant le code source de la JDK que pour l’instant, un appel avec 1 ou 2 arguments bénéficie d’une implémentation spécifique où les éléments sont 2 attributs dont 1 peut être nul ; par exemple, pour une List
, l’implémentation est List12
(lire “liste un-deux”). Les autres appels sont équivalents à l’appel avec vararg, et donnent une instance de ListN
, c’est-à-dire une liste supportée par un tableau à taille fixe. Dans tous les cas, la liste obtenue est immuable, une grande nouveauté pour Java : avant, pour obtenir une liste immuable, il fallait envelopper une liste muable via une des méthodes Collections.unmodifiable*
.
Ceci est une amélioration majeure à la fois en matière de :
Vous vous demandez probablement pourquoi des signatures ayant de 3 à 10 arguments existent, si elles aboutissent toutes à la même implémentation que la signature avec vararg ? En regardant le source, on peut supposer que les auteurs de Java se réservent la possibilité de créer des implémentations particulières pour les signatures de 3 à 10 arguments. En effet, Scala a adopté la même stratégie pour les Maps. L’implémentation de Map
est remplacée par des implémentations à entrées fixes jusqu’à 4 entrées.
Map
bénéficiant d’une méthode usine au même titre que les autres, la création d’entrées (classe Map.Entry
) bénéficie elle aussi d’une méthode usine facile à utiliser pour simplifier l’écriture. On n’est pas encore au niveau de la concision des tuples Scala/Kotlin, mais c’est déjà intéressant pour le⋅a développeur⋅se.
Les collections gagnent également une méthode copyOf
(Java 10) pour construire par recopie. Pourquoi seulement les collections ? Kotlin et Scala disposent d’un constructeur par recopie pour toutes les data classes / case classes !
Java 15 améliore l’implémentation des méthodes putIfAbsent
, computeIfAbsent
, computeIfPresent
, compute
, and merge
de la classe TreeMap
(performances).
La trousse à outil des tableaux, qui pallie le fait que les tableaux natifs ne sont pas des objets avec des méthodes, gagne des comparateurs : mismatch
, une série de méthodes compare*
, et equals
.
Les collections gagnent une méthode d’instance pour être converties en tableaux : toArray
, qui prend en paramètre la lambda d’allocation en fonction de la taille.
Les problèmes résolus ici font typiquement partie de la bulle Java, Kotlin et Scala n’ayant pas de type natif tableau mais une classe Array
(qui sera supportée par un tableau natif au runtime).
Vous avez certainement rencontré des cas où vous êtes forcés d’utiliser des bibliothèques retournant des instances d’Enumeration
, cet ancêtre d’Iterator
(au sens de l’Histoire, pas de l’héritage POO 😉). C’est très irritant, c’est avec joie que nous accueillons Enumeration.asIterator
.
Nous avons là encore un problème spécifique à Java ; Scala par exemple disposait déjà de JavaConverters
pour traduire toutes les collections/itérateurs Java en équivalent Scala.
Une seule méthode est ajoutée par Java 10 à cette classe qui date de Java 8. Je tiens à en parler car elle démontre encore les errances de la programmation fonctionnelle (PF) dans Java et trop de développeur⋅se⋅s croient que Java a réellement introduit du fonctionnel avec Java 8.
Initialement, une méthode orElseThrow(exceptionSupplier)
permettait de récupérer la valeur d’une option, et de fournir un générateur d’exception en cas d’absence. Sur le sujet, on peut remarquer que la méthode get
lance déjà une exception.
Java 10 ajoute orElseThrow()
sans argument comme synonyme de get()
: la même exception est lancée en cas d’absence de valeur. Avec un nom plus explicite, elle est désormais officiellement à préférer. Est-ce que cela va mettre fin aux mauvaises pratiques (comme try
/catch
autour d’un affreux get
) ? Est-ce qu’en mettant en valeur le lancer d’exception, des développeur⋅se⋅s vont se demander s’il ne faut pas arrêter d’appeler cette méthode dans un if(opt.isPresent())
et enfin utiliser map
/flatMap
/filter
?
Je ne sais pas… Ce qui est sûr, c’est qu’avec les API incohérentes d’Optional
, des collections et des Streams, de surcroît avec des noms qui échappent aux conventions (ifPresent
au lieu de forEach
par exemple), Java n’a toujours pas la moitié d’un début de PF.
Avec la classe Stream
en Java 8, le langage est outillé pour traiter des flux de données potentiellement infinis dans un style un peu plus fonctionnel, avec une gestion du parallélisme.
En Java 9, c’est le tour des Reactive Streams de faire leur apparition. Il faut dire que l’API Future
/ Executor
n’est pas pratique à utiliser, surtout si on souhaite coder en priorité de manière asynchrone.
Au moment où cette API arrive, Node.js existe déjà. Ce serveur d’application ECMAScript avec la récente runtime V8 de Google Chrome pousse jusque dans ses APIs à coder en asynchrone. RxJava, cette bibliothèque pour créer des applications réactives, est également devenue populaire. Akka Stream, avec le même objectif, est connu dans le monde Scala et permet déjà de “brancher des tuyaux” de manière asynchrone, avec du parallélisme, en gérant la contre-pression (back pressure) sans se soucier des threads, sans la syntaxe très bas niveau des Futures et Executors. Toutes ces bibliothèques implémentent le Reactive Manifesto.
C’est donc avec un temps de retard que Java se nantit d’une implémentation de “flux réactifs”. Temps nécessaire pour valider un standard qui sera gravé dans l’API Java ?
Même s’ils sont indéniablement utiles, les Reactive Streams sont assez bas niveau par rapport à certains concurrents. Prenons l’exemple d’un simple flot “helloworld” dans lequel on publie une liste d’éléments pour les afficher 1 par 1. Le résultat ressemblera à :
Got : 1
Got : x
Got : 2
Got : x
Got : 3
Got : x
Done
Avec les Reactive Streams Java :
public class EndSubscriber
implements Flow.Subscriber
{ private Flow.Subscription subscription; public List
consumedElements = new LinkedList<>(); @Override public void onSubscribe(Flow.Subscription subscription) { this.subscription = subscription; subscription.request(1); } @Override public void onNext(T item) { System.out.println("Got : " + item); subscription.request(1); } @Override public void onError(Throwable t) { t.printStackTrace(); } @Override public void onComplete() { System.out.println("Done"); } } import java.util.List; import java.util.concurrent.SubmissionPublisher; public class ReactiveStreamDemo { public static void main(String[] args) { SubmissionPublisher
publisher = new SubmissionPublisher<>(); EndSubscriber
subscriber = new EndSubscriber<>(); publisher.subscribe(subscriber); List
items = List.of("1", "x", "2", "x", "3", "x"); items.forEach(publisher::submit); publisher.close(); try{ Thread.sleep(100); }catch (InterruptedException ex){ ex.printStackTrace(); } } }
Ci-dessus, nous constatons que :
on*
)Thread.sleep
par quelque chose avec des CompletableFuture
.Il manque donc un liant avec la notion de Future, et de quoi exploiter correctement les fonctionnalités introduites en Java 8 (Stream, lambda) !
La même chose en Akka Stream (code Scala ici)
object AkkaStreamDemo extends App {
implicit val system = ActorSystem("Demo")
implicit val execCtxt = system.dispatcher
Source(List("1", "x", "2", "x", "3", "x")).
runForeach(elt => println(s"Got : $elt")).
onComplete(_ => {
println("Done")
system.terminate()
})
}
Première remarque importante : le fait d’avoir un ActorSystem
maintient la JVM en vie. Au lieu d’attendre la fin, on la provoque si nécessaire. C’est logique, puisque ce genre de bibliothèque sert à traiter des flux infinis : l’application traite l’information jusqu’à ce que mort s’ensuive.
Ci-dessus, le code technique est minimum. On a une source, et on enchaîne le traitement. Comme avec les Reactive Streams, on dit ce qu’on souhaite faire à la fin. Il est également possible d’ajouter un gestionnaire d’erreur qui donne la stratégie : retry, ignore / simple log, terminate (arrêt du système d’acteurs), etc.
En Akka Java, le code est un peu plus verbeux du fait de la différence de syntaxe, mais n’atteindra pas le niveau “doctorat es-plomberie” des Reactive Streams.
La raison est simple : tout en adhérant au Reactive Manifesto, Akka masque la complexité inutile. La doc officielle vous expliquera que les actions de subscribe/submit/request sont masquées. Il existe même un “Graph DSL” Akka qui permet de faire ressembler la syntaxe à un jeu de briques ; la topologie de la tâche de streaming est rendue visuellement dans le code. Par ailleurs, rien ne vous empêche de descendre au niveau de l’implémentation des actions atomiques du monde reactive que sont subscribe/submit/request au moyen d’un CompletionStage
. Autrement dit, on a une voiture de sport facile à conduire qui laisse les passionnés la modifier.
Toujours dans les choses compliquées à implémenter dont la bibliothèque vous soulage : le modèle de threading. Akka permet de choisir entre un pool de threads (ex : connexion à une BD, IO bloquantes), du fork-join, le niveau de parallélisme (facteur du nombre de cœur avec min et max), etc. juste par la configuration. Le code technique ne sera nécessaire que pour injecter la bonne configuration aux bons acteurs.
Si nous ajoutons des transformations, filtres ou autres choses plus complexes (comme un graphe orienté avec du démultiplexage / multiplexage, des boucles), nul doute que les Reactive Streams seront un enfer là où les bibliothèques spécialisées sont faites pour atteindre le niveau productivité optimal.
Reactive Stream est donc beaucoup plus fait pour des frameworks / bibliothèques de support que pour une utilisation directe. Pour faire du réactif en Java, vous vous tournerez de préférence vers RxJava ou une de ses alternatives. En Scala, vous disposerez de tout un univers de plus haut niveau avec Akka, Monix, ZIO, etc.
Accéder de manière performante et sélective aux informations de la pile d’appels, telle est la promesse de la JEP-259. Très technique, cette API sert aux contrôles d’accès via identification du code appelant, aux API dont le comportement dépend de l’appelant, ou enfin à récupérer le contenu complet de la pile.
La JEP fait référence au langage Groovy comme bénéficiaire potentiel, mais d’autres langages, frameworks, ou même IDE peuvent en bénéficier. Ce n’est donc pas une amélioration du langage Java ou de son API à proprement parler mais de la plateforme qu’est la JVM et qui sert à tous les langages.
“Write once, run everywhere”. Le motto de Java l’a historiquement poussé à se concentrer sur des API manipulant des concepts génériques repris sur toutes les plateformes. Mais avec le temps sont apparues des fonctionnalités plus proches du système, comme la NIO de Java 4 ou les Path
de Java 7. La process API de Java 9 en fait partie.
L’intérêt est de nantir Java d’un outil capable de gérer des processus au sens système et d’accéder à leurs métadonnées, sans recourir à un programme tiers écrit en C, Rust, Go, etc. via des appels natifs et pour lesquels il faudra écrire différentes versions en fonction de la plate-forme cible.
Scala dispose d’un puissant DSL pour gérer des lignes de commandes comprenant des pipes et accéder facilement aux entrées et sorties standards, mais c’est Java qui reste le plus complet en termes de gestion de processus et d’accès aux métadonnées. Ses concurrents devront faire appel à son API et prendront éventuellement un petit malus de syntaxe pour convertir les résultats vers leurs propres types.
Java 11 apporte une amélioration sur le support des double-quotes sur la ligne de commande Windows.
Ces APIs, antérieures à Java 8, profitent de quelques clarifications et améliorations que nous ne détaillerons pas ici.
Cette API “légalise” l’accès à des variables de classe par introspection et permet d’y accéder en lecture comme en écriture. Avant Java 9, il fallait passer par des moyens “interdits” via sun.misc.Unsafe
.
L’apport par rapport à un simple accès introspectif via Field
est la possibilité de gérer la manière dont se font les accès concurrents et de faire avec n’importe quel type ce qu’il est déjà possible de faire avec les variables de type Atomic*
.
Cette API est réservée de préférence aux créateurs de bibliothèques de support et aux frameworks. Quand vous souhaitez créer une application, reposez-vous sur une bibliothèque qui gère l’asynchronisme, le parallélisme et la concurrence plutôt que de vous lancer dans l’utilisation de ce genre d’outils.
Foreign Linker API, qui fait son galop d’essai dans Java 16, va simplifier les accès au code natif depuis Java. Cela pourrait à terme permettre le remplacement de JNI et de sun.misc.Unsafe
avec l’action conjointe de la Foreign Memory API qui débarque également dans l’incubateur.
Autre incubateur Java 16, la Vector API (rien à voir avec java.lang.Vector
) va permettre d’exploiter le calcul vectoriel de manière hardware (utilisation des instructions SIMD du CPU par exemple) et de manière indépendante de la plate-forme.
En dehors des dépréciations d’algorithmes de sécurité obsolètes, chaque release a vu son lot de dépréciations pour des raisons de bugs provoqués, de performance, etc.
@Deprecated forRemoval
Longtemps dans Java, la dépréciation d’une API passait par un tag Javadoc @deprecated
. Java 5 apporta les annotations, permettant au compilateurs ou à des agents de repérer la dépréciation d’un élément de code.
Cependant, si vous n’êtes pas nouveau dans le monde Java, vous aurez certainement remarqué que ce qui est déprécié a tendance à rester ! En laissant perdurer des API dépréciées, Sun Microsystem (#refdevieux), puis Oracle et la communauté Java ont quelque peu galvaudé le sens de l’annotation @Deprecated
.
Afin de remettre en marche des cycles de “grand nettoyage de printemps”, il était nécessaire de pouvoir communiquer -- aussi bien aux développeur⋅se⋅s qu’aux compilateurs et outils d’analyse de code -- que l’avertissement de dépréciation d’une API avait suffisamment duré et que la suppression était imminente !
Java 9 ajoute donc à l’annotation l’attribut forRemoval
. Le mettre à vrai indique aux utilisateurs qui s’accrochent encore au pinceau que l’échelle va être retirée dans une prochaine release. Laquelle ? Vous ne voulez pas savoir, corrigez votre code maintenant !
Cet outil est bien sûr à utiliser dans les bases de codes internes à votre société si elles sont destinées à être réutilisées par d’autres équipes.
Scala dispose de sa propre annotation, qui inclut un argument permettant d’indiquer l’API de remplacement. Il n’y a pas l’indicateur forRemoval
, ce qui est dommage mais moins problématique dans une communauté très à cheval sur les bonnes pratiques et avec un compilateur qui signale par défaut la présence d’éléments dépréciés et propose d’être relancé avec le drapeau qui donne tous les détails.
Si Scala vous laisse responsable de décider si l’utilisation d’API dépréciées est une erreur ou pas, Kotlin permet d’indiquer la gravité de la dépréciation. Cela permet de l’augmenter progressivement jusqu’au niveau ERROR
qui bloque la compilation. C’est incontestablement supérieur au forRemoval
… L’annotation est également plus structurée puisque l’alternative n’est pas juste une chaîne de caractères, mais elle-même une annotation (donc une valeur typée).
Cette amélioration rentre dans la catégorie “pendant que Java se bat contre le temps, les jeunes langages innovent”.
Tout en continuant de déprécier des API obsolètes au comportement erratique ou non sécurisé, l’amélioration forRemoval
a permis de commencer des suppressions.
Par exemple, dès Java 11 des API annotées forRemoval
en Java 10 ou Java 9 sont supprimées. L’exemple le plus connu est constitué par les douloureusement fameuses “Applets” ! Dès Java 9, c’est le berceau de Java qui est marqué, ainsi que son remplaçant Java Web Start (équivalent de ClickOnce en .Net). Tout ça est supprimé en Java 11.
D’autres suppressions après dépréciation (parfois très longues) sont elles aussi des symboles :
sun.misc.Unsafe
, qui malgré un nom qui sent la mort (feu Sun) et le danger (Unsafe), continuait à être utilisé pour des bricolages non supportés et dangereux bien que parfois maîtrisés par certains experts du genre.finalize
Plus discrète mais importante car pesant sur les bonnes pratiques : la dépréciation des constructeurs des objets représentant des primitives. Ainsi Integer
, Long
, Double
, etc. ne doivent plus être instanciés via leurs constructeurs, et ce pour au moins deux bonnes raisons :
On peut clairement dire, au regard des durées entre les dépréciations et les suppressions, qu’Oracle et le JCP (Java Community Process) ont voulu commencer à faire table rase du passé encombrant en créant une annotation qui donne des moyens à une politique de nettoyage avec un rythme désormais plus soutenu. Cette politique a d’ailleurs d’ores et déjà été mise en œuvre, confirmant que la volonté est bien réelle et assumée.
En bref, on notera :
Nous sommes plus spécialistes du Cloud, donc nous allons passer rapidement sur les améliorations orientées “desktop” ; les améliorations graphiques sont en revanche notables car certaines solutions de traitement d’image distribuées, même purement Cloud, s’appuient sur des langages de la JVM. Or, sur le Cloud, il y a toujours une compétition entre les langages populaires tels Java, et les outils tout faits et experts, souvent embarquables dans du serverless.
Depuis Java 9, on peut capturer les événements de login, logout et verrouillage du poste. Le “HiDPI” est également supporté, ce qui permet d’afficher correctement les applications Java sur les machines avec une résolution physique supérieure à la résolution logique (Apple Retina et technologies concurrentes).
Pour les distributions Linux, support de GTK3 dans JavaFX, Swing et AWT.
Swing est dotée d’une véritable annotation @BeanInfo
en lieu et place d’un tag javadoc.
Depuis Java 9, l’implémentation de la disposition des caractères (font layout) est HarfBuzz et non plus ICU.
MultiResolutionImage
permet de gérer une image avec ses différentes variantes en résolution dans un même objet. Son contexte d’utilisation : vous avez une source qui vous fournit plusieurs résolutions pour une même image, et vous souhaitez récupérer simplement la version qui correspond à la résolution de l’appareil sur laquelle elle va être affichée. Clairement plus utile pour les applications desktop et mobile, je ne sais pas si cela peut être réutilisé dans le contexte des moteurs graphiques pour le mip-mapping…
Quand Java 6 arrive, le format GIF est tombé depuis peu dans le domaine public, il est donc supporté directement. De la même manière, Java 9 voit débarquer le format de conteneur d’image TIFF.
L’API standard de Java est probablement la partie qui bouge le plus et qui fait le plus de suppression de legacy code. Un effort a clairement été fait pour rembourser la dette technique, parfois à marche forcée.
On peut saluer ici l’effort de la communauté, et on remarquera que certaines évolutions (et surtout suppressions) d’API font partie de d’une ré-architecture modulaire du “JDK”.
Si les évolutions d’API permettent de moderniser Java, c’est à la plateforme et non au langage que cela bénéficie. Les jeunes langages de la JVM (Kotlin, Scala) bénéficient si besoin de ces évolutions, ou disposent de bibliothèques et frameworks plus appropriés et efficaces. L’enjeu n’est donc pas ici de différencier Java mais de le maintenir à flot sur le chemin d’un objectif plus exigeant : rester la plate-forme de préférence — et de référence, même face aux langages plus à l’aise dans un environnement Cloud Native : Python, Go, Rust…
D’ailleurs on peut distinguer les langages par leur génération : il y a clairement les récents Cloud Natives d’un côté, et leur aînés les Cloud Migrants de l’autre.