Même un développeur Java de longue date peut ne pas avoir une bonne compréhension de la manière dont la plateforme est développée et maintenue. La principale leçon à retenir est qu'il s'agit vraiment d'un processus ouvert.
À la base du développement de Java, se trouve le Java Community Process (JCP). Il s'agit d'une sorte de document de base auto-conscient qui définit la manière d'introduire des modifications dans la plateforme et qui permet ainsi de modifier le processus lui-même. La dernière version du JCP est la 2.11, qui a été adoptée en 2019.
Le JCP formalise la manière dont les nouvelles fonctionnalités et les modifications apportées à Java (c'est-à-dire les spécifications techniques) sont proposées, examinées et approuvées, y compris la définition de divers rôles que les gens peuvent occuper. Ces rôles permettent d'offrir un lieu où la communauté des utilisateurs de Java peut participer à la gouvernance de la plateforme.
Lorsqu'un effort est suffisamment large, il est considéré comme un projet JDK. Ce terme recouvre un large éventail d'artefacts, de la documentation au code, incluant souvent une ou plusieurs propositions d'amélioration du JDK (JEP). Les projets impliquent un ou plusieurs groupes de travail. Les groupes sont dédiés à divers domaines de la plateforme Java. Un projet compte généralement plusieurs personnes actives dans le rôle d'auteur.
Pour proposer de nouvelles fonctionnalités et des changements, le JCP permet la création ("initiation") de Java Specification Requests (JSR). Cela se fait via un formulaire standardisé. Pour accéder au formulaire, vous devez vous inscrire pour obtenir un compte JCP gratuit.
De nouvelles versions de l'implémentation Java standard d'Oracle sont disponibles tous les six mois. Comme dit précédemment, parmi les autres caractéristiques possibles du JDK 21 figurent toutes les fonctions d'incubation et de prévisualisation du JDK 20, telles que les scoped values, les records patterns et les virtual threads. Les génériques universels et l'API VM asynchrone de suivi de pile pourraient également être inclus.
Bien que la page de publication du JDK 21 ne mentionne encore aucune fonctionnalité au 9 mars 2023, deux propositions d'amélioration de Java couvrant ces deux fonctionnalités ont déjà été désignées pour le JDK 21. Les propositions spécifiques sont les suivantes
Collections séquencées
Introduire de nouvelles interfaces pour représenter les collections avec un ordre de rencontre défini. Chacune de ces collections possède un premier élément bien défini, un deuxième élément, et ainsi de suite jusqu'au dernier élément. Elle fournit aussi des API uniformes pour accéder à ses premiers et derniers éléments, et pour traiter ses éléments dans l'ordre inverse.
Motivation
Le framework des collections de Java manque d'un type de collection qui représente une séquence d'éléments avec un ordre de rencontre défini. Il manque par ailleurs un ensemble uniforme d'opérations qui s'appliquent à ces collections. Ces lacunes ont été une source répétée de problèmes et de plaintes.
Par exemple, List et Deque définissent tous deux un ordre de rencontre, mais leur supertype commun est Collection, qui ne le définit pas. De même, Set ne définit pas d'ordre de rencontre et les sous-types tels que HashSet n'en définissent pas, alors que les sous-types SortedSet et LinkedHashSet en définissent un. La prise en charge de l'ordre de rencontre est donc dispersée dans la hiérarchie des types, ce qui rend difficile l'expression de certains concepts utiles dans les API.
Ni Collection ni List ne peuvent décrire un paramètre ou une valeur de retour ayant un ordre de rencontre. Collection est trop général, reléguant de telles contraintes à la spécification de la prose, ce qui peut conduire à des erreurs difficiles à déboguer. List est trop spécifique, excluant SortedSet et LinkedHashSet.
Un problème connexe est que les collections de vues sont souvent obligées d'adopter une sémantique plus faible. En enveloppant un LinkedHashSet avec Collections::unmodifiableSet, on obtient un Set, sans tenir compte de l'information sur l'ordre de rencontre.
Sans interfaces pour les définir, les opérations liées à l'ordre de rencontre sont soit incohérentes, soit absentes. Bien que de nombreuses implémentations permettent d'obtenir le premier ou le dernier élément, chaque collection définit sa propre méthode, et certaines ne sont pas évidentes ou sont totalement absentes.
Certaines d'entre elles sont inutilement lourdes, comme l'obtention du dernier élément d'une liste. D'autres ne sont même pas possibles sans faire preuve d'héroïsme : Le seul moyen d'obtenir le dernier élément d'un LinkedHashSet est d'itérer l'ensemble.
De même, l'itération des éléments d'une collection du premier au dernier est simple et cohérente, mais l'itération dans l'ordre inverse ne l'est pas non plus. Toutes ces collections peuvent être itérées vers l'avant à l'aide d'un Iterator, de la boucle for améliorée, de stream() ou de toArray(). L'itération en sens inverse est différente dans chaque cas. NavigableSet fournit la vue descendingSet() pour l'itération inverse :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | for (var e : navSet.descendingSet()) process(e); Deque does so with a reverse Iterator: for (var it = deque.descendingIterator(); it.hasNext();) { var e = it.next(); process(e); } List does so but with ListIterator: for (var it = list.listIterator(list.size()); it.hasPrevious();) { var e = it.previous(); process(e); } |
De même, le traitement des éléments d'une collection à l'aide de flux est une alternative puissante et efficace au traitement des éléments à l'aide de boucles, mais il peut être difficile d'obtenir un flux dans l'ordre inverse. Parmi les différentes collections qui définissent l'ordre de rencontre, la seule qui prenne en charge cette fonctionnalité est NavigableSet :
navSet.descendingSet().stream()
Les autres collections nécessitent soit de copier les éléments dans une autre collection, soit de créer un flux à partir d'un Spliterator personnalisé qui inverse l'itération.
Cette situation est regrettable. Le concept de collection avec un ordre de rencontre défini existe à plusieurs endroits dans le cadre des collections, mais il n'y a pas de type unique qui le représente. En conséquence, certaines opérations sur de telles collections sont incohérentes ou manquantes, et le traitement d'éléments dans l'ordre inverse va de l'inconfortable à l'impossible. Nous devons combler ces lacunes.
Description
L'équipe définit de nouvelles interfaces pour les collections séquencées, les ensembles séquencés et les cartes séquencées, puis nous les intégrons dans la hiérarchie des types de collections existants. Toutes les nouvelles méthodes déclarées dans ces interfaces sont implémentées par défaut.
Une collection séquencée est une collection dont les éléments ont un ordre de rencontre défini. (Le mot "séquencé" tel qu'il est utilisé ici est le participe passé du verbe séquencer, qui signifie « arranger les éléments dans un ordre particulier »). Une collection séquencée a un premier et un dernier élément, et les éléments entre eux ont des successeurs et des prédécesseurs. Une collection séquencée permet d'effectuer des opérations communes à chaque extrémité et de traiter les éléments du premier au dernier et du dernier au premier (c'est-à-dire en avant et en arrière).
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | interface SequencedCollection<E> extends Collection<E> { // new method SequencedCollection<E> reversed(); // methods promoted from Deque void addFirst(E); void addLast(E); E getFirst(); E getLast(); E removeFirst(); E removeLast(); } |
La nouvelle méthode reversed() permet d'obtenir une vue inversée de la collection d'origine. Toute modification apportée à la collection d'origine est visible dans la vue. Si cela est autorisé, les modifications apportées à la vue s'inscrivent dans la collection d'origine.
La vue inversée permet à tous les différents types séquencés de traiter les éléments dans les deux sens, en utilisant tous les mécanismes d'itération habituels : Enhanced for loops, explicit iterator() loops, forEach(), stream(), parallelStream(), and toArray().
Par exemple, il était auparavant assez difficile d'obtenir un flux ordonné à l'envers à partir d'un LinkedHashSet.
Code : | Sélectionner tout |
linkedHashSet.reversed().stream()
Les méthodes suivantes de SequencedCollection sont issues de Deque. Elles permettent d'ajouter, d'obtenir et de supprimer des éléments aux deux extrémités :
- void addFirst(E)
- void addLast(E)
- E getFirst()
- E getLast()
- E removeFirst()
- E removeLast()
Les méthodes add*(E) et remove*() sont facultatives, principalement pour prendre en charge le cas des collections non modifiables. Les méthodes get*() et remove*() lèvent l'exception NoSuchElementException si la collection est vide.
Les méthodes equals() et hashCode() ne sont pas définies dans SequencedCollection car ses sous-interfaces ont des définitions contradictoires.
Ensembles ordonnés
Un ensemble séquencé est un ensemble qui est une SequencedCollection ne contenant pas d'éléments dupliqués.
Code : | Sélectionner tout |
1 2 3 | interface SequencedSet<E> extends Set<E>, SequencedCollection<E> { SequencedSet<E> reversed(); // covariant override } |
Les collections telles que SortedSet, qui positionnent les éléments par comparaison relative, ne peuvent pas prendre en charge les opérations de positionnement explicite telles que les méthodes addFirst(E) et addLast(E) déclarées dans la superinterface SequencedCollection. Ces méthodes peuvent donc provoquer une exception de type UnsupportedOperationException.
Les méthodes addFirst(E) et addLast(E) de SequencedSet ont une sémantique particulière pour les collections telles que LinkedHashSet : Si l'élément est déjà présent dans l'ensemble, il est déplacé à la position appropriée. Cela permet de remédier à une lacune de longue date de [C=Java]LinkedHashSet, à savoir l'impossibilité de repositionner des éléments.
Schémas séquencés
Un schéma séquencé est une carte dont les entrées ont un ordre de rencontre défini.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | interface SequencedMap<K,V> extends Map<K,V> { // new methods SequencedMap<K,V> reversed(); SequencedSet<K> sequencedKeySet(); SequencedCollection<V> sequencedValues(); SequencedSet<Entry<K,V>> sequencedEntrySet(); V putFirst(K, V); V putLast(K, V); // methods promoted from NavigableMap Entry<K, V> firstEntry(); Entry<K, V> lastEntry(); Entry<K, V> pollFirstEntry(); Entry<K, V> pollLastEntry(); } |
Les méthodes suivantes de SequencedMap sont issues de NavigableMap. Elles permettent d'obtenir et de supprimer des entrées aux deux extrémités :
- Entry<K, V> firstEntry()
- Entry<K, V> lastEntry()
- Entry<K, V> pollFirstEntry()
- Entry<K, V> pollLastEntry()
Adaptation
Les trois nouvelles interfaces définies ci-dessus s'intègrent parfaitement dans la hiérarchie des types de collections existants :
En détail, l'équipe Java procède aux ajustements suivants pour adapter les classes et les interfaces existantes :
- List a pour superinterface immédiate SequencedCollection,
- Deque a SequencedCollection comme superinterface immédiate,
- LinkedHashSet implémente SequencedSet,
- SortedSet a pour superinterface immédiate SequencedSet,
- LinkedHashMap implémente SequencedMap, et
- SortedMap a SequencedMap comme superinterface immédiate.
Nous définissons des surcharges covariantes pour la méthode reversed() aux endroits appropriés. Par exemple, List::reversed est surchargée pour renvoyer une valeur de type List plutôt qu'une valeur de type SequencedCollection.
L'équipe a également ajouté de nouvelles méthodes à la classe utilitaire Collections afin de créer des wrappers non modifiables pour les trois nouveaux types :
- (Collections.unmodifiableSequencedCollection(collection)
- Collections.unmodifiableSequencedSet(sequencedSet)
- Collections.unmodifiableSequencedMap(sequencedMap)
Alternatives
Types
Une alternative à l'ajout de nouveaux types consisterait à réutiliser l'interface List en tant que type de collection séquencée générale. En effet, List est séquencée, mais elle supporte également l'accès aux éléments par index entier. De nombreuses structures de données séquencées ne supportent pas naturellement l'indexation et devraient donc la supporter de manière itérative. L'accès indexé aurait alors une performance de O(n) au lieu de O(1), perpétuant ainsi l'erreur de LinkedList.
Deque semble prometteur en tant que type de séquence général, puisqu'il prend déjà en charge le bon ensemble d'opérations. Cependant, il est encombré par d'autres opérations, notamment une famille d'opérations à retour nul (offer, peek et poll), des opérations sur la pile (push et pop) et des opérations héritées de Queue. Ces opérations sont judicieuses pour une file d'attente, mais le sont moins pour d'autres collections. Si Deque était transformé en type de séquence général, List serait également une file d'attente et prendrait en charge les opérations de pile, ce qui entraînerait une API encombrée et confuse.
Nommage
Le terme sequence choisi ici, implique des éléments disposés dans l'ordre. Il est couramment utilisé sur diverses plateformes pour représenter des collections dont la sémantique est similaire à celle décrite ci-dessus.
Le terme ordered n'est pas assez spécifique. Nous avons besoin d'itération dans les deux sens et d'opérations aux deux extrémités. Une collection ordonnée telle qu'une file d'attente est une exception notable : Elle est ordonnée, mais elle est aussi résolument asymétrique.
Le terme réversible, utilisé dans une version antérieure de cette proposition, n'évoque pas immédiatement le concept de deux extrémités. Le fait que la variante Map soit nommée ReversibleMap, qui implique de manière trompeuse qu'elle supporte la consultation par clé et par valeur (parfois appelée BiMap ou BidiMap), constitue peut-être un problème plus important.
Modèles de chaînes de caractères ("Preview")
Améliorez le langage de programmation Java avec des modèles de chaînes de caractères. Les modèles de chaînes complètent les chaînes littérales et les blocs de texte existants de Java en couplant le texte littéral avec des expressions et des processeurs intégrés pour produire des résultats spécialisés. Il s'agit d'une fonctionnalité de langage et d'une API de prévisualisation.
Motivation
Les développeurs élaborent régulièrement des chaînes de caractères à partir d'une combinaison de texte littéral et d'expressions. Java propose plusieurs mécanismes d’élaboration de chaînes de caractères, qui présentent malheureusement tous des inconvénients.
La concaténation de chaînes avec l'opérateur + produit un code difficile à lire :
Code : | Sélectionner tout |
String s = x + " plus " + y + " equals " + (x + y);
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | String s = new StringBuilder() .append(x) .append(" plus ") .append(y) .append(" equals ") .append(x + y) .toString(); |
java.text.MessageFormat nécessite trop de spécifications et utilise une syntaxe peu familière dans la chaîne de format :
Code : | Sélectionner tout |
1 2 | MessageFormat mf = new MessageFormat("{0} plus {1} equals {2}"); String s = mf.format(x, y, x + y); |
Les modèles de chaînes, qui apparaîtront en version Preview, complètent les chaînes littérales et les blocs de texte existants de Java en couplant le texte littéral avec des expressions et des processeurs intégrés pour produire des résultats spécialisés. Cette API est destinée à simplifier l'écriture de programmes Java en facilitant l'expression de chaînes de caractères comprenant des valeurs calculées au moment de l'exécution. Elle promet d'améliorer la lisibilité des expressions, la sécurité des programmes, la flexibilité et de simplifier l'utilisation des API qui acceptent les chaînes de caractères écrites dans des langages autres que Java. L'objectif est également de permettre le développement d'expressions autres que des chaînes, dérivées de la combinaison de texte littéral et d'expressions intégrées.
Interpolation de chaînes de caractères
De nombreux langages de programmation proposent l'interpolation de chaînes comme alternative à la concaténation de chaînes. Typiquement, cela prend la forme d'une chaîne littérale qui contient des expressions intégrées ainsi que du texte littéral. L'intégration d'expressions in situ permet aux lecteurs de discerner facilement le résultat escompté. Au moment de l'exécution, les expressions intégrées sont remplacées par leurs valeurs (stringifiées) - on dit que les valeurs sont interpolées dans la chaîne. Voici quelques exemples d'interpolation dans d'autres langues :
JavaScript `${x} plus ${y} equals ${x + y}`
C# $"{x} plus {y} equals {x + y}"
Visual Basic $"{x} plus {y} equals {x + y}"
Scala f"$x%d plus $y%d equals ${x + y}%d"
Python f"{x} plus {y} equals {x + y}"
Ruby "#{x} plus #{y} equals #{x + y}"
Groovy "$x plus $y equals ${x + y}"
Kotlin "$x plus $y equals ${x + y}"
Swift "\(x) plus \(y) equals \(x + y)"
Certains de ces langages permettent l'interpolation pour toutes les chaînes de caractères littérales, tandis que d'autres exigent que l'interpolation soit activée lorsque cela est souhaité, par exemple en préfixant le délimiteur d'ouverture de la chaîne littérale par $ ou f. La syntaxe des expressions intégrées varie également, mais elle implique souvent des caractères tels que $ ou { }, ce qui signifie que ces caractères ne peuvent pas apparaître littéralement à moins qu'ils ne soient échappés.
L'interpolation est non seulement plus pratique que la concaténation lors de l'écriture du code, mais elle offre également une plus grande clarté lors de la lecture du code. Cette clarté est particulièrement frappante avec les chaînes de caractères de grande taille. Par exemple, en JavaScript :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | const title = "My Web Page"; const text = "Hello, world"; var html = `<html> <head> <title>${title}</title> </head> <body> <p>${text}</p> </body> </html>`; |
L'interpolation de chaînes de caractères est dangereuse
Malheureusement, la commodité de l'interpolation présente un inconvénient : il est facile de construire des chaînes qui seront interprétées par d'autres systèmes, mais qui sont dangereusement incorrectes dans ces systèmes.
Les chaînes qui contiennent des instructions SQL, des documents HTML/XML, des extraits JSON, des scripts shell et du texte en langage naturel doivent toutes être validées et assainies selon des règles spécifiques au domaine. Comme le langage de programmation Java ne peut pas appliquer toutes ces règles, c'est aux développeurs utilisant l'interpolation qu'il incombe de valider et d'assainir.
En règle générale, cela signifie qu'il faut se souvenir d'envelopper les expressions intégrées dans des appels à des méthodes d'échappement ou de validation, et s'appuyer sur des IDE ou des outils d'analyse statique pour aider à valider le texte littéral.
L'interpolation est particulièrement dangereuse pour les instructions SQL car elle peut conduire à des attaques par injection. Prenons l'exemple d'un code Java hypothétique contenant l'expression intégrée ${nom} :
Code : | Sélectionner tout |
1 2 | String query = "SELECT * FROM Person p WHERE p.last_name = '${name}'"; ResultSet rs = connection.createStatement().executeQuery(query); |
Pour Java, nous aimerions disposer d'une fonction de composition de chaînes de caractères qui soit aussi claire que l'interpolation, mais qui permette d'obtenir un résultat plus sûr dès le départ, en échangeant peut-être un peu de commodité pour gagner beaucoup de sécurité.
Par exemple, lors de la composition d'instructions SQL, tous les guillemets dans les valeurs des expressions intégrées doivent être échappés, et la chaîne dans son ensemble doit avoir des guillemets équilibrés. Compte tenu de la valeur problématique de nom montrée ci-dessus, la requête qui devrait être composée est une requête sûre :
SELECT * FROM Person p WHERE p.last_name = '\'Smith\' OR p.last_name <> \'Smith\''
Il est préférable que le modèle de processeur apparaisse en premier, car le résultat de l'évaluation de l'expression du modèle dépend entièrement du fonctionnement du processeur de modèle.
Sources : Java (1, 2, 3)
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
Java 19 : nouvelles fonctionnalités avec exemples, elle apporte des méthodes pour créer des HashMaps préalloués
JDK 19 : les nouvelles fonctionnalités de Java 19 incluent la concurrence structurée, les modèles d'enregistrement et l'aperçu d'une API de fonction et de mémoire étrangères