BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles GS Collections par l'exemple - Partie 1

GS Collections par l'exemple - Partie 1

Je suis ingénieur développeur Java, Tech Fellow et Managing Director à Goldman Sachs et suis le créateur de GS Collections, une bibliothèque utilisée par Goldman Sachs et rendue libre en janvier 2012. Je suis également un ancien développeur Smalltalk.

Lorsque j’ai commencé à travailler avec Java, il me manquait principalement deux choses :

  1. Les fermetures lexicales de Smalltalk (lambdas)
  2. La bibliothèque Collections de Smalltalk, très riche en fonctionnalités

J’aurais aimé avoir à la fois ces deux fonctions et la compatibilité avec les interfaces existantes des collections natives de Java. Vers 2004, j’ai fini par comprendre que personne n’allait m’offrir tout ce que je voulais sur un plateau. Je savais aussi que j’allais vraisemblablement travailler avec Java pour les 10 à 15 années à venir de ma carrière. J’ai donc décidé de créer moi-même ce dont j’avais besoin.

10 ans ont passé. Je dispose à présent de presque tout ce que j'ai toujours voulu concernant Java : Java 8 supporte les fonctions « lambda », que je peux à présent utiliser ainsi que les références de méthodes avec la collection probablement la plus riche de Java, GS Collections.

Le tableau suivant montre une comparaison des fonctionnalités disponibles dans GS Collections, Java 8, Guava, Trove et Scala. Peut-être manque-t-il encore la fonction dont vous rêvez pour une API collections, mais il s’agit là des fonctions dont moi-même ainsi que les développeurs ayant travaillé avec moi au sein de Goldman Sachs ont eu besoin durant les 10 dernières années.

Features

 

GSC 5.0

Java 8

Guava

Trove

Scala

Rich API

 

Interfaces

Readable, Mutable, Immutable, FixedSize, Lazy

Mutable, Stream

Mutable, Fluent

Mutable

Readable, Mutable, Immutable, Lazy

Optimized Set & Map

✓ (+Bag)

   

 

Immutable Collections

 

 

Primitive Collections

✓ (+Bag, +Immutable)

   

 

Multimaps

✓ (+Bag, +SortedBag)

 

✓ (+Linked)

 

(Multimap trait)

Bags (Multisets)

 

   

BiMaps

 

   

Iteration Styles

Eager/Lazy,
Serial/Parallel
Lazy,
Serial/Parallel
Lazy,
Serial
Eager,
Serial

Eager/Lazy, Serial/Parallel (Lazy Only)

Lors d’une interview avec jClarity l’an dernier, j’ai présenté une liste de fonctions qui rendent GS Collections intéressant à mon sens. Vous pouvez en trouver la version originale ici.

Pourquoi encore utiliser GS Collections, maintenant que Java 8 est disponible et que l’on peut utiliser l’API Streams ? Bien que Streams représente un progrès considérable pour les Collections de Java, il lui manque encore certaines fonctions bien utiles. Comme le montre le tableau précédent, GS Collections contient des multimaps, des sacs (bags en anglais), des collections immuables et des collections de types primitifs. GS Collections contient également des versions optimisées de HashSet et HashMap, et les collections Bag et Multimap sont construites à partir de celles-ci. Les patterns d’itération de GS Collections sont accessibles depuis les interfaces des collections, pas besoin donc d’entrer dans l’API en utilisant stream() ou d’en sortir avec collect(). Cela permet dans bien des cas d’écrire un code beaucoup plus court et facile à lire. Enfin, GS Collections est compatible avec toutes les versions de Java à partir de Java 5. Cette propriété est particulièrement importante pour les développeurs de bibliothèques, puisqu’ils doivent la plupart du temps assurer la compatibilité de celles-ci avec des versions antérieures de Java, souvent bien après la parution d’une nouvelle version.

Dans les exemples qui suivent, nous allons montrer comment utiliser ces fonctions de différentes manières. Ces exemples sont des variations sur les exercices du kata de GS Collections, une formation enseignée au sein de Goldman Sachs pour apprendre à se servir de GS Collections. Nous avons également rendu cette formation libre et elle est à présent disponible dans un autre dépôt de GitHub.

Exemple 1 : Filtrer une collection

Une des tâches les plus fréquentes que l’on aura à effectuer avec GS Collections sera de filtrer une collection. Il y a plusieurs manières de procéder.

Dans le kata de GS Collections, nous commençons souvent avec une liste de clients. Dans l’un des exercices, on veut filtrer celle-ci afin d’avoir une liste avec uniquement les clients se trouvant à Londres. Le code suivant illustre comment on peut réaliser cela à l’aide du pattern d’itération « select ».

import com.gs.collections.api.list.MutableListimport com.gs.collections.impl.test.Verify; 

@Test public void getLondonCustomers() { 
      MutableList<Customer> customers = this.company.getCustomers(); 
      MutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); 
      Verify.assertSize("Should be 2 London customers"2, londonCustomers); 
} 

La méthode select utilisée avec une MutableList renvoie à une MutableList. Ce code s’exécute immédiatement, ce qui signifie que lorsque la méthode select() a fini son exécution, tous les calculs nécessaires pour sélectionner les éléments correspondants dans la liste source et pour les ajouter à la liste cible ont été exécutés. Le nom « select » vient de Smalltalk, qui possède un ensemble basique de protocoles pour les collections appelées select (ou filter), reject (ou filterNot), collect (ou encore map ou transform), detectIfNone, injectInto (ou foldLeft), anySatisfy et allSatisfy.

Pour faire la même chose mais avec une évaluation retardée (lazy en anglais), on peut l’utiliser comme suit :

MutableList<Customer> customers = this.company.getCustomers(); 
LazyIterableustomer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Verify.assertIterableSize(2, londonCustomers); 

Cette fois, nous avons ajouté un appel à asLazy(), le reste du code restant inchangé. Le type de retour de select a maintenant changé, dû à l’utilisation de asLazy(). Au lieu d’une liste de type MutableList, nous avons maintenant une liste de type LazyIterable. Ceci est équivalent au code suivant utilisant la nouvelle API Streams de Java 8 :

List<Customer> customers = this.company.getCustomers(); 
Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); 
List londonCustomers = stream.collect(Collectors.toList()); 
Verify.assertSize(2, londonCustomers); 

Cette fois, la méthode stream() puis l’appel à filter() renvoie à une Stream. Pour en connaître la taille, nous devons soit la convertir en List comme précédemment, soit utiliser la méthode Stream.count() de Java 8 :

List<Customer> customers = this.company.getCustomers(); 
Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); 
Assert.assertEquals(2, stream.count()); 

Les interfaces MutableList et LazyIterable partagent un ancêtre commun appelé RichIterable. En fait, on pourrait écrire ce code en utilisant uniquement RichIterable. En voici un exemple, d’abord avec une évaluation retardée :

RichIterable<Customer> customers = this.company.getCustomers(); 
RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Verify.assertIterableSize(2, londonCustomers); 

Et en exécution immédiate :

RichIterable<Customer> customers = this.company.getCustomers(); 
RichIterable<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); 
Verify.assertIterableSize(2, londonCustomers); 

Comme le montrent les exemples précédents, RichIterable peut être utilisée à la place de LazyIterable et MutableList, puisqu’il s’agit d’une interface commune à ces deux classes.

Dans certains cas, il est possible que l’on ait une liste immuable. Voici comment les types auraient été changés si on avait eu une ImmutableList :

ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); 
ImmutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London"));
Verify.assertIterableSize(2, londonCustomers); 

Tout comme pour les autres RichIterables, on peut avoir une évaluation retardée en itérant sur une ImmutableList :

ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); 
LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

MutableList et ImmutableList héritent d’une interface mère commune appelée ListIterable qui peut être utilisée à la place de chacune d’entre elles. ListIterable hérite à son tour de RichIterable. Ainsi, ce code peut être réécrit plus généralement :

ListIterable<Customer> customers = this.company.getCustomers().toImmutable(); 
LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

Ou plus généralement encore :

RichIterable<Customer> customers = this.company.getCustomers().toImmutable(); 
RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

La hiérarchie des interfaces de GS Collections suit un schéma très simple. Pour chaque type (List, Set, Bag, Map), il y a une interface de lecture (ListIterable, SetIterable, Bag, MapIterable), une interface muable (MutableList, MutableSet, MutableBag, MutableMap), et une interface immuable (ImmutableList, ImmutableSet, ImmutableBag, ImmutableMap).

(Cliquez sur l'image pour l'agrandir)

Figure 1. Basic GSC Container Interface Hierarchy

Voici un exemple pour le même code, en utilisant un Set à la place de List :

MutableSet<Customer> customers = this.company.getCustomers().toSet(); 
MutableSet<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

Voici la même implémentation avec une évaluation retardée pour un Set :

MutableSet<Customer> customers = this.company.getCustomers().toSet(); 
LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London"));
Assert.assertEquals(2, londonCustomers.size()); 

Une implémentation pour un Set, en utilisant cette fois l’interface la plus générique :

RichIterable<Customer> customers = this.company.getCustomers().toSet(); 
RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

Nous allons illustrer à présent les mécanismes qui permettent de convertir un type de collection en un autre. Voyons d’abord comment convertir une List en Set avec un filtrage avec une évaluation retardée :

MutableList<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> lazyIterable = customers.asLazy().select(c -> c.livesIn("London")); 
MutableSet<Customer> londonCustomers = lazyIterable.toSet(); 
Assert.assertEquals(2, londonCustomers.size()); 

Grâce à la flexibilité de l’interface, nous pouvons enchaîner les appels à toutes ces méthodes :

MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .asLazy() 
       .select(c -> c.livesIn("London")) 
       .toSet(); 
Assert.assertEquals(2, londonCustomers.size()); 

Nous laissons au lecteur le soin de juger l’impact sur la lisibilité. J’ai moi-même tendance à introduire des variables intermédiaires si j’ai la conviction que cela peut aider les futurs développeurs à mieux comprendre le code. Cela augmente la taille du code, mais en facilite en même temps la compréhension, ce qui est souvent plus important pour les développeurs moins habitués à utiliser ces outils.

On peut également convertir la List en Set dans la méthode select elle-même. Cette méthode possède une version surchargée prenant un prédicat (Predicate) et une collection cible en paramètres :

MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .select(c -> c.livesIn("London"), UnifiedSet.newSet()); 
Assert.assertEquals(2, londonCustomers.size()); 

Notez que cette méthode peut être utilisée quel que soit le type de retour désiré. Dans le cas suivant, la méthode renvoie à une MutableBag :

MutableBag<Customer> londonCustomers = 
       this.company.getCustomers() 
       .select(c -> c.livesIn("London"), HashBag.newBag()); 
Assert.assertEquals(2, londonCustomers.size()); 

Dans l’exemple suivant, la méthode renvoie à une CopyOnWriteArrayList, qui fait partie de la JDK. Cette méthode renverra donc le type spécifié quel qu’il soit, tant qu’il s’agit d’une classe implémentant l’interface java.util.Collection :

CopyOnWriteArrayList<Customer> londonCustomers = 
       this.company.getCustomers() 
       .select(c -> c.livesIn("London"), new CopyOnWriteArrayList<>()); 
Assert.assertEquals(2, londonCustomers.size()); 

Nous avons utilisé une fonction « lambda » dans tous les exemples précédents. La méthode select prend en argument un prédicat, qui est une interface fonctionnelle dans GS Collections définie comme suit :

public interface Predicate extends Serializable { 
       boolean accept(T each); 
} 

La fonction « lambda » que nous avons utilisée est assez simple. On peut l’extraire dans une variable séparée afin de clarifier ce que cette fonction représente dans le code :

Predicate<Customer> predicate = c -> c.livesIn("London"); 
MutableList<Customer> londonCustomers = this.company.getCustomers().select(predicate); 
Assert.assertEquals(2, londonCustomers.size()); 

La méthode livesIn() définie dans la classe Customer est assez simple. Elle est définie comme suit :

public boolean livesIn(String city) { 
       return city.equals(this.city); 
} 

On aimerait avoir une référence de fonction au lieu d’une fonction « lambda » ici, en utilisant la méthode livesIn. Mais ce code renvoie à une erreur de compilation :

Predicate<Customer> predicate = Customer::livesIn; 

Le compilateur renvoie à l’erreur suivante :

Error:(65, 37) java: incompatible types: invalid method reference 
      incompatible types: com.gs.collections.kata.Customer cannot be converted to java.lang.String 

Ceci parce que la référence de fonction nécessite deux arguments, un Customer et une String pour la ville. Nous pouvons utiliser ici une forme alternative de Predicate appelée Predicate2 :

Predicate2<CustomerString> predicate = Customer::livesIn;

Notez que Predicate2 prend deux arguments de type générique, Customer et String. Il existe une forme spécialisée de select appelée selectWith que l’on peut utiliser avec Predicate2.

Predicate2<CustomerString> predicate = Customer::livesIn; 
MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(predicate, "London"); 
Assert.assertEquals(2, londonCustomers.size()); 

Ceci peut être écrit plus simplement en incluant la référence dans l’appel à la méthode :

MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(Customer::livesIn, "London"); 
Assert.assertEquals(2, londonCustomers.size());

La chaîne de caractère London est passée en tant que second paramètre à chaque appel à la méthode définie dans Predicate2. Le premier paramètre est le Customer provenant de la liste.

La méthode selectWith, tout comme select, est déclarée dans RichIterable. Ainsi, tout ce que l’on a vu précédemment avec select fonctionne également avec selectWith. Ceci inclut l’utilisation de ces méthodes sur toutes les interfaces muables et immuables, la compatibilité pour les différents types de retour ainsi que l’évaluation retardée. Il existe également une forme de selectWith qui prend un troisième paramètre. De même que pour la méthode select avec deux paramètres, le troisième paramètre de selectWith peut être une collection cible.

Par exemple, le code suivant filtre une liste en set en utilisant selectWith :

MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .selectWith(Customer::livesIn, "London"UnifiedSet.newSet());
Assert.assertEquals(2, londonCustomers.size());

On peut également obtenir une évaluation retardée avec le code suivant :

MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .asLazy() 
       .selectWith(Customer::livesIn, "London") 
       .toSet(); 
Assert.assertEquals(2, londonCustomers.size()); 

Une dernière propriété que nous allons voir est que les méthodes select et selectWith peuvent être utilisées avec n’importe quelle collection héritant de java.lang.Iterable. Ceci inclut tous les types de la JDK ainsi que n’importe quelle autre bibliothèque de collections. La première classe conçue dans GS Collections fut une classe utilitaire appelée Iterate. Voici un exemple montrant comment utiliser select à partir d’une classe Iterable à l’aide de Iterate.

Iterable<Customer> customers = this.company.getCustomers(); 
Collection<Customer> londonCustomers = Iterate.select(customers, c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

La variation avec selectWith est également disponible :

Iterable<Customer> customers = this.company.getCustomers(); 
Collection<Customer> londonCustomers = Iterate.selectWith(customers, Customer::livesIn, "London"); 
Assert.assertEquals(2, londonCustomers.size()); 

Il existe aussi des variations prenant des collections cibles. Tous les protocoles d’itération basique sont disponibles dans Iterate. Il existe également une classe utilitaire (appelée LazyIterate) pour l’évaluation retardée, fonctionnant aussi pour n’importe quelle collection héritant de java.lang.Iterable. Par exemple :

Iterable<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> londonCustomers = LazyIterate.select(customers, c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

Une meilleure façon de faire serait d’utiliser des classes adaptateurs avec une API plus orientée objet. En voici un exemple utilisant ListAdapter avec une java.util.List :

List<Customer> customers = this.company.getCustomers(); 
MutableList<Customer> londonCustomers = 
       ListAdapter.adapt(customers).select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

Comme vous vous en doutez sans doute à présent, on peut écrire ce code avec une évaluation retardée.

List<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> londonCustomers = 
    ListAdapter.adapt(customers) 
    .asLazy() 
    .select(c -> c.livesIn("London"));
Assert.assertEquals(2, londonCustomers.size()); 

La méthode selectWith() fonctionne aussi en évaluation retardée avec ListAdapter :

List<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> londonCustomers = 
        ListAdapter.adapt(customers) 
        .asLazy() 
        .selectWith(Customer::livesIn, "London"); 
Assert.assertEquals(2, londonCustomers.size()); 

SetAdapter peut être utilisé de même que pour toute implémentation de java.util.Set.

A présent, si vous rencontrez un problème pour lequel le parallélisme au niveau des données serait bénéfique, vous pouvez utiliser l’une des deux approches disponibles pour rendre le problème parallèle. Voyons d’abord comment utiliser la classe ParallelIterate pour résoudre ce problème avec un algorithme parallèle avec exécution immédiate :

Iterable<Customer> customers = this.company.getCustomers(); 
Collection<Customer> londonCustomers = ParallelIterate.select(customers, c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

La classe ParallelIterate peut prendre n’importe quelle classe Iterable en argument et renverra toujours à une instance dejava.util.Collection en retour. Cette classe a été ajoutée à GS Collections en 2005. L’évaluation immédiate a été la seule forme de parallélisme disponible dans GS Collections jusqu’à la version 5.0 dans laquelle nous avons ajouté une API pour une évaluation retardée / exécution parallèle à RichIterable. Il n’y a pas d’API pour une évaluation immédiate / exécution parallèle sur RichIterable, ceci parce que nous estimons qu’il était plus logique d’avoir une évaluation retardée par défaut pour l’exécution parallèle. Il est possible que nous ajoutions une API pour une exécution immédiate, cela dépendra des retours et commentaires que nous recevrons à propos de l’utilité de l’API pour l’évaluation retardée.

Si nous voulons résoudre le même problème en utilisant l’API pour l’évaluation retardée, nous pourrions écrire le code suivant :

FastList<Customer> customers = this.company.getCustomers(); 
ParallelIterable<Customer> londonCustomers = 
     customers.asParallel(Executors.newFixedThreadPool(2), 100) 
        .select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.toList().size()); 

Pour le moment, la méthode asParallel() n’existe que sur un petit nombre d’implémentations de collections dans GS Collections. L’API n’a pas encore été promue pour être ajoutée aux interfaces comme MutableList, ListIterable ou encore RichIterable. La méthode asParallel() prend deux arguments – une instance d’Executor Service et une taille de batch. A l’avenir, nous pourrions ajouter une version de asParallel() qui calculerait automatiquement la taille du batch.

On peut choisir d’utiliser un type plus spécifique comme dans cet exemple :

FastList customers = this.company.getCustomers(); 
ParallelListIterable<Customer> londonCustomers = 
     customers.asParallel(Executors.newFixedThreadPool(2), 100) 
          .select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.toList().size());

Il existe une hiérarchie pour la classe ParalleIterable, qui comprend des classes comme ParallelListIterable, ParallelSetIterable et ParallelBagIterable.

Nous avons vu différentes manières de filtrer une collection avec GS Collections en utilisant select() et selectWith(). Nous avons montré plusieurs combinaisons d’itération séquentielle et parallèle avec évaluation immédiate ou retardée en utilisant différents types de retour issus de l’interface RichIterable de GS Collections.

Dans la seconde partie de cet article, qui paraîtra ultérieurement, nous verrons des exemples avec les fonctions collect, groupBy, flatCollect ainsi que des collections de types primitifs et leur très riche API. Les exemples que nous couvrirons dans la seconde partie ne seront pas aussi détaillés et nous n’explorerons pas la liste exhaustive des options disponibles, mais il est à noter que ces détails et ces options sont tout autant disponibles.

Au sujet de l'Auteur

Donald Raab dirige l’équipe JVM Architecture, qui fait partie du groupe Enterprise Platform dans la division Technology de Goldman Sachs. Donald Raab fait partie du groupe d’experts JSR 335 (Lambda Expressions for the Java Programming Language) et il est également l’un des représentants de Goldman Sachs au comité exécutif de JCP. Il a rejoint Goldman Sachs en 2001 en tant qu’architecte logiciel dans l’équipe PARA. Il a été nommé Technology Fellow en 2007 et Managing Director en 2013.

Traduction : François Wu a rejoint Goldman Sachs en Janvier 2013 en tant que développeur Java dans le département Controllers Technology, travaillant quotidiennement avec GS Collections. Il a rejoint l’équipe Corporate Treasury Strats en 2015.

Pour plus d’information à propos de GS Collections et Goldman Sachs Engineering, vous pouvez visiter www.gs.com/engineering.

Désistement :

Cet article renvoie uniquement à l’information disponible au sein de la division “Technology Division” de Goldman Sachs et non pas à celle des autres départements de Goldman Sachs. Il ne doit pas être considéré comme un support ou comme un conseil en investissement. Les opinions exprimées ne sont pas celles de Goldman Sachs à moins qu’il en soit expressément noté autrement. Goldman Sachs & Co. (“GS”) ne garantit pas l’exactitude, l’état complet ou la pertinence de cet article, et les destinataires ne devraient pas en tenir compte sauf à leurs propres risques. Cet article ne peut pas être transféré ou autrement divulgué sauf si cet avertissement y figure en entier.

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT