Rétrospective java
Author: Unknown
Date: 12/10/2023
Pour des raisons qu'il me reste à détailler, je replonge en partie dans le code, et en java. La dernière fois que j'ai codé "pour de vrai", c'était pour faire des applications java sur des features phones (les truc entre les Nokia 3210 et les smartphones), en J2ME, et des applications Windows Mobile sur Windows Phone. Avant l'ère des smartphones donc. Avant cela, j'avais développé essentiellement en java pour des services web, via des servlets, un moteur de templating maison, puis via les Java Server Pages.
Donc, depuis, côté java, beaucoup, beaucoup, beaucoup de choses ont changé.
Avant de remettre les mains dans le cambouis cu code java en mode bac à sable, j'ai voulu faire une rétrospective purement bibliographique. Entretemps, le calendrier de l'avent du code a commencé, et avec quelques collègues, on a commencé à le faire. Cela m'a permis de recoder concrètement en java, sans avoir à gérer tous les à-côté du développement, en me concentrant sur le langage et l'algorithmie. Un vrai bonheur, et l'occasion d'utiliser en vrai toutes les évolutions de java depuis java 1.4.
Cette rétrospective n'aborde pas toute la partie "dénomination" de java. La définition précise et le périmètre respectif de "Java", "JDK", "J2SE", "J2EE", "JEE", "Jakarta" mériterait un article dédié.
Elle aborde très peu la partie run/ops, alors beaucoup de choses intéressantes sont apparues depuis les débuts (nouveaux garbage colletors, compression des String, monitoring d'une application dans la JVM, chargement plus rapide du bytecode, partage du bytecode entre différentes JVM etc).
Enfin, cette rétrospective java est forcément influencée par mon expérience et mes centres d'intérêts professionnels. Et elle n'est pas exhaustive: je ne suis pas historien du logiciel !
D'où je pars ?
J'ai appris le java en 2000. Principalement en lisant, en réfléchissant. Mon livre de chevet, c'était Java in a nutshell, chez O'Reilly, celui avec le tigre, qui devait couvrir, de mémoire, Java 1.3.
Puis, sur le tas, en l'utilisant en vrai. Pour faire des applets dans le cadre d'un job étudiant. Puis, cela a été le premier langage que j'ai utilisé pendant mon stage et ma vie de développeur web, en JDK1.3. En plus du Perl et du shell-script pour tous les à-côté.
Pas de framework à l'époque, à part des framework maison. La page était entièrement générée côté serveur, car les navigateurs savaient faire peu de choses. La grande époque du Web Atos Servlet, et d'une librairie maison. Librairie maison, com.utilities, qui s'est enrichie au fil des années pour combler les manques de l'API java de l'époque.
Maintenant, fin 2023, on est en java 21. Rien qu'au niveau du langage, sans parler des innombrables framework autour, que s'est-il passé depuis ?
Préambule: Comment Java évolue ?
Concernant la diffusion de nouvelles fonctionnalités instables, je suis plutôt habitué à la sortie de plusieurs variantes d'un même produit. Dans le monde du libre par exemple, il y a les build "stable", "unstable" voir, pire, les "nightly build". Par exemple, pour le navigateur libre Firefox, il est possible d'installer plusieurs variantes de Firefox:
- la version nightly: c'est une préversion instable, qui peut crasher en pleine navigation web
- la version beta: il s'agit d'une préversion stable, avec des nouvelles fonctionnalités pas encore complètement éprouvées
- la version nominale: réputée stable - c'est celle que tout le monde télécharge.
Concernant java, cela ne fonctionne pas ainsi: java est produit dans une seule variante. C'est un "tout en un". Sauf que, il faut quand même laisser du temps aux nouvelles fonctionnalités afin "d'essuyer les plâtres" avant de les considérer comme prêtes pour la production. Et on imagine bien qu'il y a, en termes de risques, une différence entre une nouvelle API et une évolution profonde du langage.
Comment font-ils, alors côté java ? Côté java, une évolution peut être de trois type différentes: preview feature, experimental feature ou incubator module.
Preview feature
Une fonctionnalité en preview peut être une nouveauté au niveau du langage, de la JVM ou de l'API. Elle est complètement spécifiée et implémentée. Cependant, afin d'éviter une erreur de conception (je me rappelle l’inconsistance des API dans java.util...), cette nouvelle fonctionnalité est mise à disposition auprès des développeurs pour avoir leur feedback. Il y a donc un risque à utiliser ce type de fonctionnalité.
L'implémentation doit être déjà de qualité et stable; elle doit être disponible sur toutes les plateformes où Java fonctionne déjà.
En théorie, la fonctionnalité peut être finalement complètement abandonnée. En pratique, cela ne semble jamais être arrivé. Une fonctionnalité en preview dans une release java peut continuer à l'être dans les suivantes, en 2nd preview, 3rd preview etc.
Historiquement, il y a eu au moins une fois un gros feedback pris en compte. En Java 12, la possibilité d'utiliser switch dans une évaluation d'expression (JEP 325) utilisait initialement le mot clé break pour retourner une valeur. En Java 13, ce mot clé breaka été remplacé par yield (JEP 354) pour éviter la confusion.
Dans le titre des JEP, les preview feature sont suffixées par (Preview), (Second Preview), (Third Preview) etc. Concrètement, l'activation de l'ensemble des fonctionnalités en mode preview se fait à la compilation et à l'exécution en rajoutant –-enable-preview à la ligne de commande. Par contre, l'intégration correcte dans les IDE est à la charge de chaque IDE, et cela peut prendre du temps.
Experimental feature
Une preview feature étant déjà très stable, comment fait-on pour partager quelque-chose d'instable, l'équivalent des branches unstable des projets open-source et/ou libres? On utilise le statut d'experimental feature. Contrairement à une preview feature, l'implémentation a le droit d'être instable, et l'intégration de la fonctionnalité par les fournisseurs de JVM est optionnelle..
Il s'agit très souvent, voir quasiment toujours de fonctionnalités au niveau de la JVM, par exemple l'introduction de nouveaux garbage collectors.
Dans le titre des JEP, les experimental feature sont suffixées par (Experimental). Concrètement, pour les utiliser, il faut rajouter à la ligne de commande un paramètre -XX, par exemple -XX:+EnableValhalla. L'activation s'effectue individuellement pour chaque fonctionnalité expérimentale, contrairement aux preview features où on active/désactive d'un bloc toutes les preview features.
Incubator module
Une fonctionnalité intégrée sous forme de modules d'incubations peut concerné une modification d'API et/ou des outils, pas encore assez matures pour être des preview features. Il s'agit au sens java du terme (introduit avec Java 9), d'un module, préfixé par jdk.incubator. dans le nom du module et des packages et/ou du nom de l'outil en ligne de commande éventuellement inclus. Contrairement, à une preview feature, le nommage n'est donc pas définitif.
Contrairement à une experimental feature, un incubator module fait partie du JDK, et tous les fournisseurs de JDK doivent les fournir.
Deux exemples, l'un passé : JEP-110: HTTP/2 Client (Incubator), un incubator module apparu avec Java 9 et intégré pleinement dans le JDK en java 10; et l'autre, encore en cours au moment de Java 21: JEP 460: Vector API (Seventh Incubator), en incubation depuis la JEP 338 de Java 16, qui consiste à utiliser le support matériel lors de calcul de Vecteur (au sens mathématique du terme - rien à voir avec java.util.Vector).
Dans le titre des JEP, les incubator modules sont suffixées par (Incubator). Concrètement, pour les utiliser, il faut rajouter à la ligne de commande un --add-modules jdk.incubator.vector (exemple de la JEP 460: Vector API (Seventh Incubator)). L'activation s'effectue individuellement pour chaque module, contrairement aux preview features où on active/désactive d'un bloc toutes les preview features.
La préhistoire: les évolutions de Java pendant l'ère Sun
Après cette petite introduction sur la différence entre preview feature, experimental feature et incubator module, passons au vif du sujet.
2002: Java 1.4
Une nouveauté importante pour la qualité du code: l'arrivée des assertions et donc de l'instruction assert .
Une autre nouveauté importante: On peut chaîner les exceptions. C'est dur à croire, mais avant, ce n'était pas possible, et c'était compliqué de remonter à l'exception racine.
Une nouvelle API pour lire et écrire sur disque, dix fois plus rapide que la précédente: Non blocking I/O.
tl;dr;
Pour le reste, des choses sans impact sur mon quotidien de l'époque (Java Web Start et Java Print Service, inutilisés côté serveur), dont pas mal d'intégration dans Java d'API déjà existantes ailleurs:
- Java intègre un parseur XML et d'un moteur XSLT. Du parsing à l'ancienne, avec des callback au début et à la fin de chaque élément XML rencontré. De mémoire, déjà existant par ailleurs.
- Les expressions régulières Perl sont intégrés dans Java. Mais avant, on pouvait utiliser de toute manière l'implémentation de la fondation Apache (package
org.apache.regexp.RE). - L'API des Preferences (
java.util.prefs): j'imagine bien que toutes les gros projets de l'époque avaient déjà des librairies utilitaires pour gérer cela, et sans doute de manière plus aboutie. - Une API de log. Lo4j existait déjà au sein de la fondation Apache.
Des améliorations qui ne m'étaient pas utiles à l'époque, et qui sont maintenant gérés directement par les frameworks java, comme le service d’authentification intégré à java.
Des améliorations pas immédiatement utiles au développeur web de l'époque (support d'IP v6, meilleur générateur de nombres aléatoires, JNDI pour parler avec un serveur LDAP - oui, à l'époque,cela ne m'était pas utile).
2004: Java 5 - génériques et annotations
Sur la forme, on passe de Java 1.4 à Java 5. Pourquoi un tel changement ? Pourquoi pas Java 1.5 ? Pur marketing, afin de marquer le coup au vu des gros changements (et c'est vrai) dans Java.
Beaucoup d'améliorations fort pratiques au quotidien:
- autoboxing/unboxing (plus besoin de convertir explicitement par exemple des int en Integer et inversement).
- les types génériques (faire un
List<Integer>=new ArrayList<>()par exemple) qui évitent de devoir caster en permanence son code. Un code au final plus lisible et moins sujet aux erreurs à l'exécution, avec une vérification plus strict du typage à la compilation. Je reviens dessus juste après. - le mot clé enum, qui permet d'avoir un support des énumérations plus propres qu'une suite de constantes
final static intet bien moins lourd à utiliser que la création de classes entières dédiées à cela. - une boucle for plus simple à écrire et à lire quand on veut juste itérer simplement sur une collection. Avant Java 5, il fallait écrire fastidieusement:
for (Iterator i = c.iterator(); i.hasNext(); ) {
String s = (String) i.next();
...
}
carrément plus lourd qu'un:
for (String s : c) {
...
}
- les arguments de méthode en nombre variables (je me rappelle que cela a permis d'alléger du code inutile où on s'emmerdait à déclarer les méthodes avec de plus en plus d'arguments).
- l'introduction du mécanisme des annotations. Je reviens également dessus juste après.
Petit focus sur les types génériques
Un ajout longuement discuté: la JSR 14 date de 2000, et le sujet est déjà abordé en 1998 par des pointures telles que Gilad Bracha, David Stoutamire, Philip Wadler et Martin Odersky. Avant, par exemple, dès qu'on avait une collection, il fallait passer son temps à caster les éléments quand on les récupérait. Avec énormément de risque de détection de problème uniquement à l'exécution:
List v = new ArrayList();
v.add(new Integer(42));
v.add("test"); // on pouvait même écrire ce genre de ligne qui passait à la compilation ET à l'exécution
Integer i = (Integer) v.get(0);
i=(Integer) v.get(1); // on récupère le "test", et donc exception à l'exécution, mais bien que cela passe crème à la compilation :-(
Et maintenant, c'est bien plus concis (en plus, je rajoute l'autoboxing ici), et fiable:
List<Integer> v = new ArrayList<>();
v.add(42);
// v.add("test"); // cette ligne ne passe plus à la compilation
int i = v.get(0); // ici, certitude que si cela passe à la compilation, cela passera
i= v.get(1); // aussi à l'exécution (pour le typage uniquement, il y aura quand même
// un IndexOutOfBoundsException)
Petit focus sur les annotations
Les annotations ne sont pas complètement une nouveauté. La javadoc repose sur le même mécanisme, mais utilisé hors compilateur. Au sein même du compilateur, @deprecated était une annotation utilisée. Et, avant java 5, il y avait déjà des outils pour modifier le code avant compilation, tel que XDoclet par exemple. La possibilité de créer ses propres annotations et leur intégration dans le bytecode ont permis d'aller beaucoup plus loin.
Au tout début, les annotations n'étaient finalement que de simple ordre/informations fournies au compilateur. Maintenant, elles sont un élément essentiel de l'écosystème Java.
L'arrivée des annotations dans le byte code a permis de définir le lien entre le code et la base de donnée (ORM) avec des annotations telles que @Entity, @Table, @Id, directement dans le code. Charge ensuite, à l'exécution, à l'implémentation de JPA (typiquement, Hibernate) de faire le lien "qui va bien" pour que tout cela tombe "automagiquement" en marche. Bye bye les fichiers de configuration XML. Cela a permis également à des frameworks comme Spring de permettre de permettre la déclaration de l'inversion de contrôle (@Autowired) ou de déclarer un web service (@GetMapping("/monEndpoint")) directement dans le code java, sans passer là encore par de lourds fichiers de configuration externe XML.
Clairement, un gros pas en avant dans la pratique quotidienne du code, avec un code plus compréhensible, moins source d'erreurs.
Quelques annotations sont définies en standard (@Override, @Deprecated, @SuppressWarnings, @SafeVarargs, @FunctionalInterface), d'autres dans la partie entreprise de java (Java EE / Java Enterprise Edition / Jakarta EE), comme les annotations JPA (@Entity, @ManyToMany, @JoinTable) mais souvent intégré de fait par les projets java, par des bibliothèques qui sont devenues des standard de fait, comme JUnit (@Test, @Before, @After). Enfin, pour se donner mal à la tête, il y a les annotations sur les annotations (@Retention, @Target, @Inherited, @Documented), utilisées quand on développe ses propres annotations.
Cela facilite grandement l'utilisation des framework. Revers de la médaille: le risque de ne pas maîtriser ce que fait le framework et comment il le fait. Notamment, dès qu'il y a un lien avec une base de donnée relationnelle ou non, ce côté magique peut s'accompagner de performances très moyennes dès qu'on a affaire à de gros volumes de données.
2006: Java 6
Intégration de la compilation et du scripting dans java. J'ai failli l'utiliser pour un projet client, mais finalement non. Pour le reste, des améliorations de performances, et des changements côté java sur le poste de travail.
Côté développement serveur, cela ne change pas grand chose donc.
Plus du tout développeur à temps plein depuis quelques années, et mon équipe passe pas mal de temps à faire des choses qui sont exotiques à l'époque: du développement d'applications mobiles pour Windows Mobile, pour les features phone en J2ME, voir pour de l'iPhone quelques temps après, quand Steve Jobs disait qu'il n'y aurait pas d'application tierce sur iPhone.
L'histoire: les évolutions de Java depuis le rachat de Sun par Oracle
Cinq ans se sont écoulés entre java 6 et Java 7. C'est long, et c'est la période la plus longue entre deux versions de Java. Pendant ce laps de temps, en 2009, Oracle rachète Sun pour 7,4 milliards de dollars. Cela faisait longtemps qu'Oracle était impliqué dans Java: on voit souvent passer des adresses mails Oracle dans les JSR bien avant 2009. Oracle était membre du Java Community Process - le mécanisme de développement et de révision des spécifications techniques de Java.
2011: Java 7
Je ne suis plus du tout dans le game, là, on passe dans le 100% lecture des release notes pour les évolutions.
Des petites évolutions très très sympathiques pour le développeur: on peut rendre plus lisibles des grands nombres en utilisant le séparateur de milliers _, ou les écrire en notation binaire, on peut faire des switch avec des Strings, on peut catcher plusieurs Exceptions d'un coup. On peut également se reposer sur le try-with-resources pour ne plus se soucier de refermer proprement une ressource dans un bloc finally. Franchement, pour l'avoir utilisé sur l'adventofcode 2023, tout cela est vraiment pratique.
2014: Java 8 (LTS) - fonctions lambda et API Streams
Sur la façon de faire évoluer la plateforme java, la grosse nouveauté, ce sont les Java Enhancement Proposal (JEP). La première, JEP 1 : JDK Enhancement-Proposal & Roadmap Process, introduit le concept de Java Enhancement Proposal en juin 2011.Pas de JEP avant pour suivre l'évolution du langage, mais des JSR (Java Specification Request). La première JEP opérationnelle est la JEP 101: Generalized Target-Type Inference, intégrée dès Java 8.
En nouveautés côté développement: l'intégration native d'un interpréteur javascript, et une nouvelle API de date/heure (qui était en effet pas terrible à la base, de mémoire), "piquée" à la librairie tierce Joda-Time. Enfin, pas vraiment piqué: c'est la même personne, Stephen Colebourne, qui est à l'origine de Joda-Time et cette nouvelle API. Et qui en profite pour corriger certains problèmes de Joda-time.
Les interfaces peuvent définir des méthodes statiques.
Et, last but not least, grosse, grosse évolution: les fonctions lambda. Une fonctionnalité dont l'introduction dans Java est discuté depuis 2006. Mais pas facile d'intégrer cela, tout en gardant la logique actuelle du langage. Il aura fallu 8 ans!
Cela permet par exemple d'éviter la création de classe anonyme, et allège donc la lecture de code.
Avant:
monObjetQuiEmetDesEvenements.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("quelque-chose est arrivé");
}
});
Après:
monObjetQuiEmetDesEvenements.addActionListener(event -> System.out.println("quelque-chose est arrivé"));
Il y a un petit côté déroutant car la simplicité de lecture/écriture repose sur l'inférence de type par le compilateur. Dans l'exemple précédent, celui-ci détermine "automagiquement" que event est un ActionEvent et que l'implémentation fournie correspond à la méthode actionPerformed.
Cela peut même être aussi déroutant qu'un code Perl quand on lit ce genre de chose, qui affiche tous les éléments d'une liste:
list.forEach(System.out::println);
qui, à l'ancienne mode s'écrirait ainsi:
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
D'ailleurs, autre similarité avec Perl, la valeur de la dernière expression vaut valeur de retour: dans BiFunction<Integer, Integer, Long> additionner = (val1, val2) -> (long) val1 + val2; , il n'y a pas le mot clé return, pourtant, la fonction additionner renvoie un Long. Il y a plein de magie derrière: techniquement, le compilateur infère le type des fonctions lambda pour les faire rentrer dans une des classes du package java.util.function. On retrouve dans ce package la classe BiFunction utilisée dans l'un des exemples précédents, qui représente une fonction prenant deux paramètres en entrée et produisant une sortie.
Enfin, s'ajoute la possibilité de désigner une méthode via des notations avec ::. On peut référencer n'importe quelle méthode, qu'elle soit statique (String::valueOf), d'instance (monObjet::toString) et même un constructeur (String::new).
Maintenant qu'on a vu tout cela, on peut s’atteler au gros morceau, dont la syntaxe déroute les anciens comme moi: les streams/filter/map/reduce sur les collections: on comprends bien le sens de ce que cela doit faire, mais aucune idée de pourquoi cela "tombe en marche". Alors, hop, un petit cours pour les anciens (j'en connais un qui m'a dit vouloir comprendre comment tout cela fonctionnait). Voici par exemple un morceau de code déroutant:
record Position(int x, int y) {};
List<Position> list = ...;
// find max lines and cols
int nbLines = 1 + list.stream().mapToInt(pos -> pos.y()).max().getAsInt();
int nbCols = 1 + list.stream().mapToInt(pos -> pos.x()).max().getAsInt();
Si on décompose de gauche à droite la ligne list.stream().mapToInt(pos -> pos.y()).max().getAsInt();:
list.stream().mapToInt(pos -> pos.y()).max().getAsInt();La méthodestreams()est définie dans l'interfaceCollection, donc autant dire qu'elle est présente un peu près tout le temps qu'on en a besoin. Si les données sont dans un tableau et pas dans une collection, la méthode statiqueArrays.stream(T[] array)effectue la même chose. Donc, ici, cela renvoie une instance deStream<Position>. Cet objetStreamest le point de départ, la source du flux: il fournit une séquence d'éléments qui vont passer dans un pipeline.list.stream().mapToInt(pos -> pos.y()).max().getAsInt();Ensuite, on appelle la méthodemapToIntde cette instance deStream<Position>. La classeStreampossède plusieurs méthodes du typemapToQuelqueChose, qui prends en paramètre une fonction lambda qui doit retourner un QuelqueChose, ici unInt. La fonction lambda en question,pos -> pos.y()extrait la coordonnée y d'une position sur le plan. La méthodemapToIntva appeler cette fonction lambda pour chaque élément du flux. Cette méthodemapToIntva elle même retourner un flux, qui sera unIntStream. Elle retourne donc également unStream, et on pourrait enchaîner à nouveau sur un appel à unmapToQuelqueChose...list.stream().mapToInt(pos -> pos.y()).max().getAsInt();… Mais on va ici utiliser une autre méthode deIntStreamque tous lesUnTypeNombreStreamont: la méthodemax(). Celle-ci va retourner la valeur maximale passée à travers le flux. Pas sous la forme d'un type primitif, mais sous la forme d'un OptionnalInt, un objet qui gère le fait que si le flux est vide, il n'y a pas de maximum connu.list.stream().mapToInt(pos -> pos.y()).max().getAsInt();Enfin, on appellegetAsInt()qui retourne donc la valeur maximale, si le flux n'était pas vide. Et qui lance une exception sinon.
Il y a certains méthodes deStreamqu'on retrouve souvent, et qui retournent également unStream:sorted(),sorted(Comparator<? super T> comparator),reversed()pour trier, etfilter(Predicate<? super T> predicate)pour filtrer.
En fin de traitement, soit on termine par type primitifint/longpar exemple en finissant par un.max().getAsInt();. On peut aussi terminer en récupérer uneListen finissant par un.collect(toList()), ou un tableau avec un.toArray().
Si on veut terminer par un traitement sur chacun des éléments, plutôt qu'en récupérer la liste et de la parcourir, on peut terminer le traitement du flux en passant une fonction lambda en paramètre à.forEach(Consumer<? super T> action). Cependant, attention si la fonction lambda a un effet de bord: cela pourrait mal se passer si le flux est traité en parallèle.
Il y a plein d'autres choses intéressantes à pousser sur le sujet, comme les optimisations internes (quand c'est possible, tout le flux n'est pas traité) ou les Streams parallèles. Un truc qui m'aurait grandement intéressé si j'avais pris le train en marche à l'époque avec du temps pour explorer tout cela en profondeur.
2017: Java 9 - REPL et modules
Un shell Java. Apparemment, principalement motivé par des raisons éducatives: Scala, Ruby, JavaScript, Haskell, Clojure, et Python sont utilisables en REPL (Read-Eval-Print Loop), et du coup, Java serait moins utilisé pour apprendre l'informatique. Marrant, cela me rappelle le REPL du GFA Basic sur Atari.
Diverses améliorations qui ne me semblent pas changer fondamentalement le quotidien en développement serveur (support des écrans à haute résolution, support des catalogues XML). Les String plus compactes en mémoire ne changent rien côté développement, mais sont appréciables côté charge mémoire sur les serveurs.
Après avoir "piqué" la gestion de date à la librairie Joda-Time dans Java 8, Oracle continue d'étendre java en intégrant en incubator module un client HTTP, qui subira quelques ajustements dans les versions 10 et 11.
Les interfaces peuvent définir des méthodes privées.
Retrait de JavaDB, un projet Apache permettant d'embarquer un moteur de base de données en java. Je ne savais même pas que cela existait. Vérification: il n'était embarqué dans les versions 7 et 8 du JDK.
Point amusant: suite à des modifications dans Java 8, le caractère _ pouvait être utilisé comme identifiant. Java 9 règle le problème en l'interdisant. Mais il reviendra 6 ans plus tard en preview dans Java 21 avec les variables non nommées.
Introduction de la notion de module qui doit permettre de résoudre l'enfer des JAR que j'ai en effet connu (quand ton code ne fonctionne plus parce qu'un jar et/ou une classe s'est intercalée ou a changé de place dans ton classpath). C'est une fonctionnalité qui a mis du temps pour être intégré dans java puisque les premières tentatives de l'intégration de Jigsaw datent de 2011, 6 ans auparavant, dans Java 7.
Un module est un "tout cohérent". Un module se traduit généralement par une unique archive jar. Les modules regroupent plusieurs packages. Jusque-là, pas vraiment de différence avec les pratiques de l'époque, finalement. Par contre, ce niveau intermédiaire impacte la visibilité des packages et classes contenues. Un module peut embarquer une certaine implémentation d'un package, mais ne pas l'exposer publiquement. Pour autant, cela ne permet pas de faire coexister plusieurs versions d'un même package, dans le cas de modules nécessitant malheureusement des versions d’implémentation différentes d'un même package.
Arrive une notion qui existait depuis des années dans d'autre écosystème type Perl: la notion de dépendances.
Le code java inclus dans le JDK a utilisé cette modularisation pour ne plus rendre accessible des API purement internes. Le code a été réorganisé. Cela a été transparent pour la totalité des personnes utilisant "normalement" java. Et a permis une maintenance interne plus facile côté Oracle. Enfin, cela a permis d'avoir un démarrage plus rapide de la JVM.
2018: Java 10 - var
On peut maintenant déclarer une variable locale avec var, et le compilateur infère son type. Cela va me faire bizarre, j'avais tellement l'habitude de savoir ce qu'était une variable avant de lire son nom. Après, je n'ai pas l'impression que ce soit finalement beaucoup utilisé (pas de statistique trouvée à ce sujet - c'est du feeling en parlant autour de moi et en cherchant sur le web).
Des améliorations internes qui ne me semblent pas changer fondamentalement le quotidien en build. Sans doute plus en run (garbage collector, JIT compiler, Class-Data sharing pour accélérer le démarrage et diminuer l'empreinte mémoire, certificats racines par défaut, etc).
2018: Java 11 (LTS) - première version payante du JDK d'Oracle
Des améliorations internes au langage, sans impact développeur. Par contre, un complément (JMX et la JConsole existaient précédemment) d'instrumentation de la JVM avec le Flight Recorder.
Un truc exotique: dans la continuité du REPL introduit dans Java 9, il est maintenant possible de lancer un code source java: l'interpréteur le compilera puis l'exécutera lui passant les arguments en paramètre. Et on peut même mettre un shebang au début d'un fichier .java qui serait exécutable, et ainsi le lancer comme un script. Étonnant, non ?
Des petites méthodes bien sympathiques sur les chaînes de caractères: sBlank(), lines(), strip(), stripLeading(), stripTrailing(), et repeat().
Support de TLS dans sa version 1.3, et d'HTTP 2 avec le package java.net.http qui n'est plus une preview feature.
Ah, ils virent CORBA, JavaFX, Java Web Start et les applets, bonne idée de mon point de vue!
Gros changement de licence: pour les entreprises, le JDK d'Oracle devient payant. Oracle continue de distribuer gratuitement java sous une licence libre (la GPL v2 avec la classpath exception) à travers le projet OpenJDK. Qui ne bénéficie pas des mêmes mises à jour de sécurité de la version payante.
Et, à partir de maintenant, nouvelle politique de parution des nouvelles versions de java: une nouvelle version tous les 6 mois. Avec une durée aussi courte entre deux versions, certaines versions apporteront peu de nouveautés. C'est le cas des deux suivantes, Java 12 et Java 13.
2019: Java 12
Encore des changements côté exécution de la JVM, avec un nouveau garbage collector expérimental, Shenandoah.
Pour les développeurs, une évolution, en mode preview: switch peut être utilisé dans une expression. Et apparition du mot clé yield - mais pas avec le même sens qu'en Python. Pratique pour faciliter la compréhension du code.
2019: Java 13
Encore des changements côté exécution de la JVM.
Pour les développeurs, la possibilité d'écrire des Strings sur plusieurs lignes en utilisant """ (text block). Pas un changement fondamental, mais c'est sympa, et étonnant que cela ne soit pas déjà intégré au langage alors que cela est énormément répandu ailleurs. En mode preview à ce stade. Ainsi, que les switch dans les expressions qui passent en seconde preview.
L'ère contemporaine: les 3 dernières années
2020: Java 14
Des petits sucres syntaxiques (Pattern matching for instanceof) pour alléger le code quand on fait un bloc suivant un test de instanceof.
Avant:
if (obj instanceof Machin) {
Machin m = (Machin) obj;
m.methodSpecificToAMachin();
}
Après:
if (obj instanceof Machin s) {
s.methodSpecificToAMachin();
}
Alors, c'est en effet moins lourdingue. Mais cela peut avoir conséquence des trucs tordus, dans certains cas: Par exemple:
if (obj instanceof String something) {
// something est ici un String, comme si on avait écrit String something=(String) obj
} else {
// mais ici, something n'est pas défini en tant que (String) obj. Si on tente de l'utiliser,
// soit cela ne compilera pas, soit cela compilera, si somethingsi est également une propriété de
// la classe englobant ce code
}
if (obj instanceof String s && s.length() > 5) {
// correct: s.length() compris comme ((String) obj).length()
}
if (obj instanceof String s || s.length() > 5) {
// incorrect: s n'est pas compris comme ((String) obj).length()
}
C'est en preview.
L'introduction de record, en mode preview, qui sont des tuples typés. On pouvait déjà en faire avant, en passant par une définition de classe, mais là, c'est franchement moins lourd à écrire et à lire.
Des NullPointerException plus parlantes.
switch, text block et record continuent d'évoluer, toujours en mode preview.
En dehors du développement pur, du nettoyage, un système de packaging application java+JRE, la possibilité d'accéder à des zones mémoires hors JVM, en incubator, du monitoring, et des choses côte garbage collector.
2020: Java 15
Les text block / Strings sur plusieurs lignes ne sont plus en preview et sont introduits définitivement.
Introduction, en preview, des Sealed classes & interfaces, qui permet, dans une classe ou une interface de contrôler/d'indiquer qui a le droit d'hériter de celle-ci. C'est bizarre, mais cela a une réelle utilité. Surtout quand on développe une librairie et qu'on ne sait jamais trop comment celle-ci va être utilisée/étendue par un tiers.
Le garbage collector ZCG, prévu pour moins freezer la JVM, est utilisable en production. Arrivée également en production du garbage collector Shenandoah.
Enfin, un peu de nettoyage (Solaris/SPARC, Nashorn JavaScript Engine - introduit en 2014 avec Java 8) ou de préparation au nettoyage (RMI)
2021: Java 16
En mode incubator, la possibilité d'utiliser l'accélération matérielle lors d'opération sur des vecteurs (au sens mathématique, pas l'objet Vector). Pour des applications web classiques, pas d'impact. Et une façon différente de se lier avec du code natif (alternative à JNI).
En incubator_ toujours, la possibilité d’appeler du code natif, sans la lourdeur de JNI ou de ses successeurs.
Ces deux dernières modifications, ainsi que la possibilité d'accéder à des zones mémoires hors JVM, en preview en Java 14 vont finalement dans le sens d'une adhérence plus forte avec l'extérieur du monde java.
Implémentation des sockets Unix, ce qui peut être pratique, côté serveur, pour communiquer avec des applications legacy.
Des modifications internes (C++, changement du système de versionning et du repository de code source, des portages sur de nouvelles plateformes), sans impact développeur. À part le portage sur Alpine Linux qui intéresse les personnes utilisant Docker.
Les Pattern matching for instanceof et les records sont définitivement entérinés.
2021: Java 17 (LTS)
Du nettoyage (l'API des applets devient deprecated, le Security Manager également. Une partie de RMI est retirée, compilateur expérimentaux AOT et JIT), des améliorations à la marge (générateurs pseudo-aléatoires, portage sur macOS, refonte de java2D sur Mac, sécurité, sealed classes et API Vector, toutes deux toujours en preview - sont améliorées). Ce qui était en incubator l'est toujours. Rien de franchement nouveau.
Côté développeur, la grosse nouveauté est finalement la possibilité, en preview, de pouvoir faire du pattern matching dans un switch, afin de sélectionner du code selon le type de l'objet:
Object o = ...;
return switch (o) {
case null -> "Null";
case String s -> String.format("String %s", s);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case Integer i && i>0 // refining patterns
-> String.format("positive int %d", i);
case Integer i && i==0
-> String.format("zero int %d", i);
case Integer i && i<0
-> String.format("negative int %d", i);
default -> o.toString();
};
};
2022: Java 18
Du nettoyage/tuyauterie interne (réimplémentation interne de java.lang.reflect). Des améliorations à la marge (API Vector, le pattern matching dans les switch, et la possibilité d'appeler du code natif - toujours en preview - sont améliorés).
La mise en place d'un mécanisme permettant de ne pas passer par l'OS pour la résolution de nom pourrait sembler être une amélioration minime. Mais c'est un passage obligé pour une future et utile implémentation de DNS au dessus de HTTPS.
L'UTF8 devient l'encodage de caractères par défaut. Pas trop tôt. Python le faisait depuis le début! Cela peut secouer un peu lors de la montée de version. D'autant plus qu'un problème d'encodage n'empêche généralement pas un démarrage de l'application. Pas de coupure franche. Il faudrait sans doute tester toutes les consommations et productions de fichiers (csv, txt, PDF).
Java intègre un serveur web statique, jwebserver pas à utiliser en production, juste en mode prototype. Même pas pour du développement un peu sérieux, vu la manque de fonctionnalité (pas d'ACL, pas de HTTPS, pas d'authentification). Apparemment, comme pour le REPL, c'est à but éducatif.
Le petit truc sympa: l'ajout de @snippet en alternative au <pre> utilisé pour encadrer du code d'exemple dans une javadoc. Et qui permet d'aller plus loin (ajout de lien, surlignage de passage particulier dans le code).
Le finally des clauses try {...} catch {...} finally {...} devient deprecated. Il faut utiliser le try-with-resources introduit en Java 7, 11 ans auparavant.
2022: Java 19
Des améliorations continues sur des éléments toujours en preview ou en incubator (API Vector, le pattern matching dans les switch, et la possibilité d'appeler du code natif).
En preview, la possibilité de faire du patttern matching, sur des record et pas uniquement sur des class. Une suite logique.
L'arrivée d'une vraie nouveauté: les threads virtuels. Java va pouvoir avoir des threads léger, comme d'autres langages/plateformes (cela me fait forcément penser à erlang)! Le gros intérêt des threads légers, par rapport aux threads système, c'est le gain de mémoire: un thread système, c'est de base 2MB de mémoire. En preview uniquement.
2023: Java 20
La seule nouveauté, en mode incubator: les valeurs étendues / scoped values, une fonctionnalité permettant de partager des variables immutables (donc qui ne changent plus après leur première affectation) entre threads. Intéressant quand on a énormément de threads légers. Alors certes, il y a déjà la possibilité d'avoir des variables locales à un Thread. Mais, d'une part, c'est openbar en terme d'accès en lecture/écriture à ces variables. Et d'autres part, quand on crée des Thread enfants, en fait les variables du thread parent sont dupliquées, ce qui est coûteux en temps et en mémoire. Clairement pas compatible avec un fonctionnement à base de plusieurs milliers de Thread légers.
Pour le reste, que des raffinements d'éléments déjà en mode preview ou incubator.
2023: Java 21 (LTS)
C'est une LTS. Oracle s'engage donc sur la durée du support auprès des clients qui utilisent leur version payante du JDK. Sans surprise, cela s'accompagne de l'intégration de deux éléments auparavant en preview:
- on peut utiliser officiellement du pattern matching dans les switchs, avec des
Classet desrecord. - on peut utiliser des Threads légers (mais attention, pas terrible si vous avez des appels IO au sein de blocs synchronized). S'y ajoute l'arrivée d'une interface pour unifier, enfin, la façon d'itérer, d'accéder au premier et au dernier élément d'une collection ordonnée, de l'inverser et d'ajouter un élément au début ou à la fin. Le truc un peu rigolo dans tout cela, c'est de voir comment cela s'intercale dans la hiérarchie des classes et interfaces existantes. Mais cela ne va sans doute rien révolutionner.
En preview, un truc bien sympathique: la possibilité d'utiliser des templates pour créer une chaine de caractères. Cela a la puissance des créations de rapport de Perl. Sans forcément aller jusque là, car les rapports Perl, s'ils avaient beaucoup de sens dans un monde de terminaux textes, en ont sans doute moins maintenant. C'est intéressant car cela peut rendre plus clair certains passages de code. D'autant plus que cela peut être utilisé pour produire en sortie d'autres choses que du texte, par exemple du json (au sens JSONObject), voir du SQL.
En preview toujours, l'introduction de variables non nommées. L'idée est, comme pas mal d'autres langages, de noter simplement _ une variable qu'on n'utilisera pas plus tard. Évidemment, personne ne déclarerait une variable pour ne pas l'utiliser ensuite. Mais dans le cas du pattern matching, ou dans le cas de retour inutilisé de certaines fonctions/méthodes, cela a du sens afin de faciliter la lecture du code.
En preview, toujours et encore, on peut faire un Hello World en java qui ressemble à un Hello World en C car on peut faire des classes non nommées, et avec une méthode d'instance main() plutôt qu'une méthode static. À tester en combinaison avec la JEP 330 pour faire du scripting java ?
Des améliorations de fonctionnalités déjà en preview ou en incubator. Un peu de nettoyage avec le passage en deprecated de la version 32bits x86. Quelques améliorations côté Z Garbage Collector. Et un truc très ops: la future interdiction du chargement dynamique d'agents. Cela posera problème pour les ops qui instrumentent la JVM après son démarrage.
2024: Java 22
Pas encore sortie, elle est prévue pour mars 2024. https://openjdk.org/jeps/461 - https://www.infoq.com/news/2023/12/stream-api-evolution/
Impressions finales et points marquants
Pas mal de changements côté ops (retrait de choses devenus inutiles, démarrage de JVM plus rapide, nouveaux garbage collectors). mais comme dit en introduction, je ne suis pas le mieux placé pour avoir un avis sur le sujet.
Alors, quels sont selon moi les principaux points à retenir de ces évolutions de java, côté développement, depuis un peu plus de vingt ans.
Beaucoup de choses ont été ajoutés dans Java en réponse à une certaine "hypitude" d'autres langages (le REPL, la possibilité de lancer un .java directement depuis le binaire java sans compilation explicite, le fait de pouvoir faire du code java sans définir de classe, serveur web statique). Je suis dubitatif sur l'intérêt pour du développement sérieux côté serveur. Je ne sais dire si cela a eu un réel intérêt sur l'apprentissage du langage.
Il y a des évolutions d'API / phagocytage d'APIs externes. Alors, évidemment, cela ne révolutionne rien, mais si cela peut éviter d'avoir sur chaque projet sa petite librairie utilitaire, ce n'est pas plus mal.
Certaines nouveautés, dans la théorie, changent peu de chose mais sont un réel changement au quotidien(*). Les lourdeurs syntaxiques que Java pouvait avoir par rapport à du Python et/ou du Perl ont disparues de mon point de vue (générique, nouvelle boucle for, autoboxing, pattern dans les switch, et même le tout récent '_').
Deux nouveautés théoriques et pratiques ont énormément changé java depuis ses débuts: les annotations, et les fonctions lambda/l'API Stream.
Je n'avais jamais pris de recul sur la façon de faire évoluer le langage et la JVM. J'ai pu constater que le retrait de fonctionnalité prends du temps, ce qui parait logique, via la mise en deprecated longtemps avant (10 ans pour le finally). À l'inverse, il faut 6 à 8 ans de maturation pour que les grosses évolutions soient définitivement intégrées (6 ans pour les génériques, 8 ans pour les closure/fonction lambda). Ce serait intéressant de comparer le temps nécessaire à l'intégration de telles évolutions avec le temps nécessaire dans d'autres langages. De même, java ne sort qu'en une seule variante, avec uniquement le mécanisme des preview features/experimental features/incubator modules. Ce n'est pas forcément le choix dans tous les langages.
(*) Grâce à quelques collègues, j'ai participé au calendrier de l'avent du code en utilisant java, et surtout mon java tout rouillé. Et, comme j'avais déjà terminé l'étude purement bibliographique des évolutions java, je savais que par moment, je pouvais faire autrement que d'utiliser du code de "grand père". Franchement, mettre en pratique, surtout via ce calendrier de l'avent du code qui permet de se concentrer sur le langage et l'algo, cela fait toute la différence entre vision purement bibliographique et quelque-chose de plus concret. Vu de loin, les Stringdans les switch, l'utilisation de switchdans les expressions, cela peut sembler être accessoire. Mais, en vrai, cela change la vie. Hâte d'utiliser tout ce qui est annotation. Le voyage continue...
Le voyage continue - Généré avec l’IA ∙ January 4, 2024 at 2:40 PM
Sources
- The role of preview features in Java 14, Java 15, Java 16, and beyond
- JEP 11: Incubator Modules
- JEP 12: Preview Features
- JEP draft: Preview Features: A Look Back, and A Look Ahead
- Java Preview Features
- Difference Between Preview, Experimental, and Incubating Features in Java
- Java version history
- Bracha, G., Odersky, M., Stoutamire, D., & Wadler, P. (1998). Making the future safe for the past: adding genericity to the Java programming language. In OOPSLA '98 Proceedings of the 13th ACM SIGPLAN conference on Object-oriented programming, systems, languages, and applications (pp. 183-200). ACM. https://doi.org/10.1145/286936.286957
- https://i11www.iti.kit.edu/~key/keysymposium10/slides/Bethlen_Closures_Java.pdf
- New Features in Java 11
- Java 12 et Java 13, des versions pour rien ? #CCLUB 4, Jean-Philippe Ehret