BT

GS Collections par l’exemple – Partie 2

| Écrit par Donald Raab Suivre 0 Abonnés , traduit par François Wu Suivre 0 Abonnés le 13 janv. 2016. Durée de lecture estimée: 16 minutes |

Dans la première partie de GS Collections par l’exemple, j’ai présenté plusieurs moyens de filtrer une collection dans GS collections grâce aux méthodesselect et selectWith.

Afin d’utiliser select, nous avons donné comme paramètre un objet Predicate, le plus souvent en utilisant une fonction lambda. Pour utiliser selectWith, nous avons donné comme paramètre un objet Predicate2, le plus souvent comme référence de méthode.

Un grand nombre de méthodes fournies par GS Collections prennent un objet Predicate comme paramètre, parmi lesquelles select, reject, detect, anySatisfy, allSatisfy, noneSatisfy, count et partition.

Il existe également des variantes utilisant un objet Predicate2 appelées sselectWith, rejectWith, detectWith, anySatisfyWith, allSatisfyWith, noneSatisfyWith, countWith et partitionWith.

Dans cette deuxième partie, nous allons étudier en détails les méthodes qui utilisent des objets Predicates comme paramètres, ainsi que certaines méthodes « modifiantes » (collect, flatCollect, groupBy, groupByEach) qui utilisent toutes le type Function en argument.

Nous verrons ensuite des exemples qui montreront comment utiliser l’API de GS Collections pour passer d’un conteneur d’objets à un conteneur de primitives. Nous utiliserons dans nos exemples  les classes du domaine d’objets illustré ci-dessous.

Les exemples sont écrits comme des tests unitaires et requièrent l’utilisation de Java 8.

[Cliquez sur l’image pour l’agrandir]

Nous espérons que ces exemples vous donneront envie d’explorer plus en détails l’API riche et complète que nous avons développée dans GS Collections.

Exemple 2 : Existe-t-il au moins un élément d’une collection satisfaisant un prédicat donné ?

Utilisez anySatisfy :

@Test
public void doAnyPeopleHaveCats()
{
    Predicate predicate = person -> person.hasPet(PetType.CAT);
    boolean result =
	this.people.anySatisfy(predicate);
    Assert.assertTrue(result);

    boolean result1 =
	this.people.anySatisfyWith(Person::hasPet, PetType.CAT);
    Assert.assertTrue(result1);
}

Exemple 3 : Est-ce que tous les éléments d’une collection satisfont un prédicat donné ?

Utilisez allSatisfy :

@Test
public void doAllPeopleHaveCats()
{    
    boolean result =        
        this.people.allSatisfy(person -> person.hasPet(PetType.CAT));    
    Assert.assertFalse(result);    

    boolean result1 =        
        this.people.allSatisfyWith(Person::hasPet, PetType.CAT);    
    Assert.assertFalse(result1);
}

Exemple 4 : Est-il vrai qu’aucun élément d’une collection ne satisfait un prédicat donné ?

Utilisez noneSatisfy :

@Test
public void doNoPeopleHaveCats()
{
    boolean result =
	this.people.noneSatisfy(person -> person.hasPet(PetType.CAT));
    Assert.assertFalse(result);

    boolean result1 =
        this.people.noneSatisfyWith(Person::hasPet, PetType.CAT);
    Assert.assertFalse(result1);
}

Exemple 5 : Compter le nombre d’éléments qui correspondent à un prédicat donné

Utilisez count :

@Test
public void howManyPeopleHaveCats()
{
    int count =
	this.people.count(person -> person.hasPet(PetType.CAT));
    Assert.assertEquals(2, count);

    int count1 =
        this.people.countWith(Person::hasPet, PetType.CAT);
    Assert.assertEquals(2, count1);
}

Exemple 6 : Trouver le premier élément d’une collection qui satisfait un prédicat donné

Utilisez detect :

@Test
public void findPersonNamedMarySmith()
{
    Person result =
        this.people.detect(person -> person.named("Mary Smith"));
    Assert.assertEquals("Mary", result.getFirstName());
    Assert.assertEquals("Smith", result.getLastName());

    Person result1 =
        this.people.detectWith(Person::named, "Mary Smith");
    Assert.assertEquals("Mary", result1.getFirstName());
    Assert.assertEquals("Smith", result1.getLastName());
}


Les méthodes any/all/noneSatisfy et detect sont des exemples de méthodes « court-circuit » pouvant renvoyer un résultat sans itérer sur toute la collection. Par exemple, la fonction anySatisfy agit comme un connecteur logique ou ; elle retourne vrai aussitôt qu’elle a trouvé un élément satisfaisant le prédicat, ou à défaut, itère sur toute la collection et retourne faux.

Les exemples suivants prennent aussi des prédicats mais ne court-circuitent pas l’itération.

Exemple 7 : Sélectionner des éléments dans une collection

Nous avons montré plusieurs exemples de filtration dans la première partie de cette série. Voici un petit rappel, en utilisant le domaine Person/Pet. La méthode pour filtrer une collection est appelée select.

@Test
public void getPeopleWithCats()
{
    MutableList peopleWithCats =
        this.people.select(person -> person.hasPet(PetType.CAT))
    Verify.assertSize(2, peopleWithCats);

    MutableList peopleWithCats1 =
        this.people.selectWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(2, peopleWithCats1);
}

Exemple 8 : Trouver les éléments d’une collection ne satisfaisant pas un prédicat donné

Utilisez reject :

@Test
public void getPeopleWhoDontHaveCats()
{
    MutableList peopleWithNoCats =
        this.people.reject(person -> person.hasPet(PetType.CAT));
    Verify.assertSize(5, peopleWithNoCats);
   
    MutableList peopleWithNoCats1 =
        this.people.rejectWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(5, peopleWithNoCats1);
}

Exemple 9 : Partitionner les éléments d’une collection en séparant les éléments satisfaisant un prédicat donné de ceux qui ne le satisfont pas

Utilisez partition :

@Test
public void partitionPeopleByCatOwnersAndNonCatOwners()
{
    PartitionMutableList catsAndNoCats =
	this.people.partition(person -> person.hasPet(PetType.CAT));
    Verify.assertSize(2, catsAndNoCats.getSelected());
    Verify.assertSize(5, catsAndNoCats.getRejected());

    PartitionMutableList catsAndNoCats1 =
        this.people.partitionWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(2, catsAndNoCats1.getSelected());
    Verify.assertSize(5, catsAndNoCats1.getRejected());
}

Dans cet exemple, la fonction partition renvoie un type spécial, PartitionMutableList, qui est une interface héritant de PartitionIterable. Cette interface possède deux méthodes : getSelected et getRejected. Celles-ci varient en fonction des sous-types de PartitionIterable. Ainsi, pour PartitionMutableList, le type de retour pour ces deux méthodes est MutableList. Si l’on avait utilisé l’interface mère, PartitionIterable, le type de retour aurait été RichIterable.

Ceci conclut la section sur les API prenant les types Predicate/Predicate2 en paramètre. Présentons maintenant celles prenant les types Function/Function2.

Exemple 10 : Transformer une collection d’un type en un autre

Utilisez collect :

@Test
public void getTheNamesOfBobSmithPets()
{
    Person person =
	this.people.detectWith(Person::named, "Bob Smith");
    MutableList pets = person.getPets();
    MutableList names =
        pets.collect(Pet::getName);
    Assert.assertEquals("Dolly, Spot", names.makeString());
}

Dans l’exemple ci-dessus, nous avons trouvé une personne appelée « Bob Smith », puis nous avons pris la liste de ses animaux et nous l’avons convertie en MutableList<String>. Nous avons collecté la liste des noms des animaux de la collection.

Exemple 11 : Aplanir une collection en fonction d’un attribut

Si vous souhaitez collecter un attribut d’une collection qui pointe vers une autre collection, le tout dans une seule et unique collection « plane », utilisez flatCollect.

@Test
public void getAllPets()
{
    Function> function = person ->
person.getPetTypes();
    Assert.assertEquals(
        UnifiedSet.newSetWith(PetType.values()),
        this.people.flatCollect(function).toSet()
    );
    Assert.assertEquals(
        UnifiedSet.newSetWith(PetType.values()),
        this.people.flatCollect(Person::getPetTypes).toSet()
    );
}

Dans le premier exemple, la fonction lambda a été extraite dans une variable séparée pour illustrer quel type flatCollect prend en argument. La méthode collect prend un argument de type Function<? super T, ? extends V>. La méthode flatCollect, elle, prend un argument de type Function<? super T, ? extends Iterable<V>>. En d’autres termes, la classe Function passée à flatCollect doit retourner un type pouvant être itéré.

Exemple 12 : Grouper une collection par une fonction

Utilisez groupBy :

@Test
public void groupPeopleByLastName()
{
    Multimap<String, Person> byLastName =
this.people.groupBy(Person::getLastName);
    Verify.assertIterableSize(3, byLastName.get("Smith"));
}

Dans l’exemple ci-dessus, les personnes ont été groupées en fonction de leur nom de famille. Le test montre qu’il y a trois personnes dont le nom de famille est Smith. Le type de retour de groupBy est une Multimap. On peut se représenter une Multimap comme un type à peu près équivalent à Map<K, Iterable<V>>. Il existe beaucoup de spécialisations de Multimap dans GS Collections (List, Set, Bag, SortedBag, SortedSet) qui possèdent toutes à la fois les formes Mutable et Immutable. Dans cet exemple, nous avons seulement utilisé le type parent appelé Multimap, mais nous aurions pu l'a spécialiser en ListMultimap ou MutableListMultimap.

Exemple 13 : Grouper une collection par une fonction renvoyant plusieurs clés

Utilisez groupByEach :

@Test
public void groupPeopleByTheirPets()
{
    Multimap peopleByPets =
        this.people.groupByEach(Person::getPetTypes);
    RichIterable catPeople = peopleByPets.get(PetType.CAT);
    Assert.assertEquals(
        "Mary, Bob",
        catPeople.collect(Person::getFirstName).makeString()
    );
    RichIterable dogPeople = peopleByPets.get(PetType.DOG);
    Assert.assertEquals(
        "Bob, Ted",
        dogPeople.collect(Person::getFirstName).makeString()
    );
}

Tout comme la fonction flatCollect, la méthode groupByEach prend un argument de type Function<? Super T, ? extends Iterable<V>>. La Multimap<K, T> qui est retournée est en effet plusieurs fois indexée, chaque valeur pouvant avoir plusieurs clés. Dans cet exemple, nous avons groupé les personnes par leurs types d’animaux. Une personne peut avoir plus d’un animal, ce qui explique pourquoi Bob se retrouve dans les deux Strings. Nous avons utilisé collect pour convertir chaque Person en leur prénom, puis utilisé makeString pour les convertir en une String contenant ces valeurs séparées par des virgules. Il existe des versions surchargées de makeString qui prennent un séparateur différent en paramètre, ainsi qu’un caractère de début et de fin.

Exemple 14 : Additionner les valeurs d’un attribut primitif renvoyé par une méthode

Utilisez l’une des quatre variantes de sumOf disponibles sur l’interface RichIterable : sumOfInt, sumOfFloat, sumOfLong et sumOfDouble.

@Test
public void getTotalNumberOfPets()
{
    long numberOfPets = this.people.sumOfInt(Person::getNumberOfPets);
    Assert.assertEquals(9, numberOfPets);
}

Dans l’exemple ci-dessus, nous avons additionné le nombre d’animaux que chaque personne possède, ce qui nous donne le nombre total d’animaux. Pour additionner des ints ou des floats, on utilise les types long ou double. La méthode sumOfInt prend une forme spécialisée de Function appelée IntFunction.

public interface IntFunction
        extends Serializable
{
    int intValueOf(T anObject);
}

Les interfaces Procedures, Functions et Predicates dans GS Collections héritent toutes de Serializable. Ceci permet de les sauvegarder sur un disque, ou de les envoyer à travers un réseau sans avoir à créer sa propre extension de Serializable.

Exemple 15 : Compatibilité entre les collections d’objets et les collections de types primitifs

Si vous devez convertir une collection d’objets en une collection de types primitifs, vous pouvez utiliser l’une des huit formes spécialisées pour les types primitifs de collect (collectInt/Float/Long/Double/Byte/Short/Char/Boolean). Collect permet également de reconvertir une collection de types primitifs en une collection d’objets. Cette fonctionnalité apporte beaucoup de flexibilité dans l’utilisation de ces APIs.

@Test
public void getAgesOfPets()
{
    IntList sortedAges =
        this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collectInt(Pet::getAge)
            .toSortedList();
    IntSet uniqueAges = sortedAges.toSet();
    IntSummaryStatistics stats = new IntSummaryStatistics();
    sortedAges.forEach(stats::accept);
    Assert.assertTrue(sortedAges.allSatisfy(IntPredicates.greaterThan(0)));
    Assert.assertTrue(sortedAges.allSatisfy(i -> i > 0));
    Assert.assertFalse(sortedAges.anySatisfy(i -> i == 0));
    Assert.assertTrue(sortedAges.noneSatisfy(i -> i < 0));
    Assert.assertEquals(IntHashSet.newSetWith(1, 2, 3, 4), uniqueAges);
    Assert.assertEquals(2.0d, sortedAges.median(), 0.0);
    Assert.assertEquals(stats.getMin(), sortedAges.min());
    Assert.assertEquals(stats.getMax(), sortedAges.max());
    Assert.assertEquals(stats.getSum(), sortedAges.sum());
    Assert.assertEquals(stats.getAverage(), sortedAges.average(), 0.0);
    Assert.assertEquals(stats.getCount(), sortedAges.size());
}

Dans cet exemple, nous avons retrouvé quelques informations concernant l’âge des animaux que possède chaque personne d’une liste donnée. Nous utilisons d’abord la méthode asLazy(). Ceci nous permet de réduire le nombre de collections intermédiaires. Retirer l’appel à asLazy() ne change pas le résultat de ce code. Dans ce cas, il s’agit plutôt d’une optimisation de l’utilisation de la mémoire. Nous utilisons ensuite flatCollect, qui renvoie les animaux de toutes les personnes dans une seule et même liste. Nous appelons ensuite collectInt, qui permet de convertir le type LazyIterable<Pet> en IntIterable. Pour chaque animal, nous prenons son âge en utilisant la méthode getAge. Si nous n’avions pas utilisé asLazy, nous aurions converti une MutableList<Pet> en une IntList. Enfin, nous utilisons toSortedList qui prend l’IntIterable et la convertit en IntList en triant les entiers. Nous appelons finalement toSet sur l’IntList afin d’avoir le set unique contenant les âges des animaux.

A partir de ce résultat, nous avons illustré la richesse des collections de types primitifs de GS Collections. Les méthodes statistiques telles que min, max, sum, average (moyenne) ou median (médiane) sont directement disponibles sur toutes ces collections. Nous avons également utilisé l’une des nouvelles classes « statistiques » de java 8 appelée IntSummaryStatistics avec la IntList en utilisant une référence de méthode sur IntSummaryStatistics::accept qui prend un int. Nous avons également montré des méthodes que vous avez vu dans les exemples précédents, telles que any/all/nonSatisfy ; simplement, elles sont à présent appliquées à des collections de types primitifs.

Exemple 16 : Compter le nombre d’occurrences d’un élément dans une collection

Si vous voulez rapidement connaître le nombre d’occurrences d’un élément, vous pouvez convertir la collection en Bag (sac). Un Bag est à peu près équivalent à une Map<K, Integer> où l’entier représente le nombre d’occurrences de l’élément K. Un Bag ressemble à n’importe quelle autre collection, supportant les méthodes add, remove, etc. Il possède également des méthodes spécialisées pour collecter efficacement les occurrences d’un élément, ainsi que pour ajouter ou retirer des éléments. Un Bag est équivalent à un Set qui autoriserait les dupliquas et retiendrait le nombre d’occurrences de chaque élément.

@Test
public void getCountsByPetType()
{
    Bag counts =
        this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collect(Pet::getType)
            .toBag();
    Assert.assertEquals(2, counts.occurrencesOf(PetType.CAT));
    Assert.assertEquals(2, counts.occurrencesOf(PetType.DOG));
    Assert.assertEquals(2, counts.occurrencesOf(PetType.HAMSTER));
    Assert.assertEquals(1, counts.occurrencesOf(PetType.SNAKE));
    Assert.assertEquals(1, counts.occurrencesOf(PetType.TURTLE));
    Assert.assertEquals(1, counts.occurrencesOf(PetType.BIRD));
}

Il n’existe pas d’équivalent à Bag pour le moment dans la JDK, tout comme il n’existe pas d’équivalent à PartitionIterable ou Multimap. Dans les méthodes de java 8 des classes Collectors, ces types sont simulés en utilisant Map<K, Integer> (Collectors.counting), Map<Boolean, List<V>> (Collectors.partitioning), et Map<K, List<V>> (Collectors.groupingBy).

Exemple 17 : Compter le nombre d’occurrences d’une valeur de type primitif dans une collection

Si vous voulez connaître le nombre d’occurrences d’une valeur de type primitif, vous pouvez utiliser un Bag de types primitifs.

@Test
public void getCountsByPetAge()
{
    IntBag counts =
        this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collectInt(Pet::getAge)
            .toBag();
    Assert.assertEquals(4, counts.occurrencesOf(1));
    Assert.assertEquals(3, counts.occurrencesOf(2));
    Assert.assertEquals(1, counts.occurrencesOf(3));
    Assert.assertEquals(1, counts.occurrencesOf(4));
    Assert.assertEquals(0, counts.occurrencesOf(5));
}

Dans cet exemple, nous avons créé un objet IntBag à partir de l’âge de tous les animaux. Ceci nous permet d’utiliser la méthode occurrenceOf sur IntBag pour trouver le nombre d’occurrences de chaque âge différent. Cet exemple est le dernier de la deuxième partie.


Ces exemples sont là pour vous donner un échantillon non-exhaustif de ce que vous pouvez accomplir avec GS Collections. Il existe aujourd’hui plus d’une centaine de méthodes disponibles sur RichIterable. Ceci permet aux développeurs de disposer d’un large éventail d’outils pour manipuler les collections.

Lors de la conférence JavaOne 2014, Craig Motlin et moi-même avons présenté plusieurs exemples permettant de comparer l’utilisation des Streams introduits dans Java 8 avec GS Collections lors d’une session intitulée : « GS Collections and Java8 : Functional, Fluent, Friendly and Fun! » Les slides de la présentation sont disponibles sur le site de JavaOne 2014 et peuvent être retrouvées sur le wiki GitHub de GS Collections.

Pour référence, voici le code que nous avons utilisé pour initialiser les tests ainsi que les classes domaines.

import com.gs.collections.api.RichIterable;
import com.gs.collections.api.bag.Bag;
import com.gs.collections.api.bag.MutableBag;
import com.gs.collections.api.bag.primitive.IntBag;
import com.gs.collections.api.block.function.Function;
import com.gs.collections.api.block.predicate.Predicate;
import com.gs.collections.api.list.MutableList;
import com.gs.collections.api.list.primitive.IntList;
import com.gs.collections.api.multimap.Multimap;
import com.gs.collections.api.partition.list.PartitionMutableList;
import com.gs.collections.api.set.primitive.IntSet;
import com.gs.collections.impl.bag.mutable.HashBag;
import com.gs.collections.impl.block.factory.Predicates2;
import com.gs.collections.impl.block.factory.primitive.IntPredicates;
import com.gs.collections.impl.list.mutable.FastList;
import com.gs.collections.impl.set.mutable.UnifiedSet;
import com.gs.collections.impl.set.mutable.primitive.IntHashSet;
import com.gs.collections.impl.test.Verify
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.IntSummaryStatistics;
 
public class PersonTest
{
    MutableList people;
 
    @Before
    public void setUp() throws Exception
    {
        this.people = FastList.newListWith(
            new Person("Mary", "Smith").addPet(PetType.CAT, "Tabby", 2),
            new Person("Bob", "Smith").addPet(PetType.CAT, "Dolly",
3).addPet(PetType.DOG, "Spot", 2),
            new Person("Ted", "Smith").addPet(PetType.DOG, "Spike", 4),
            new Person("Jake", "Snake").addPet(PetType.SNAKE, "Serpy", 1),
            new Person("Barry", "Bird").addPet(PetType.BIRD, "Tweety", 2),
            new Person("Terry", "Turtle").addPet(PetType.TURTLE, "Speedy",
1)
            new Person("Harry", "Hamster").addPet(PetType.HAMSTER, "Fuzzy",
1).addPet(PetType.HAMSTER, "Wuzzy", 1)
        );
    }

    public class Person
    {
        private String firstName;
        private String lastName;
        private MutableList pets = FastList.newList();
 
        private Person(String firstName, String lastName)
        {
            this.firstName = firstName;
            this.lastName = lastName;
       
 
        public String getFirstName()
        {
            return this.firstName;
        }
 
        public String getLastName()
        {
            return this.lastName;
        }

        public boolean named(String name)
        {
            return name.equals(this.getFirstName() + " " +
this.getLastName());
        }
 
        public boolean hasPet(PetType petType)
        {
            return
this.pets.anySatisfyWith(Predicates2.attributeEqual(Pet::getType), petType);
        }
 
        public MutableList getPets()
        {
            return this.pets;
        }
 
        public MutableBag getPetTypes()
        {
            return this.pets.collect(Pet::getType, HashBag.newBag());
        }
 
        public Person addPet(PetType petType, String name, int age)
        {
            this.pets.add(new Pet(petType, name, age));
            return this;
        }
 
        public int getNumberOfPets()
        {
            return this.pets.size();
        }
    }
 
    public class Pet
    {
        private PetType type;
        private String name;
        private int age;
 
        public Pet(PetType type, String name, int age)
       
            this.type = type;
            this.name = name;
            this.age = age;
        }
 
        public PetType getType()
        {
            return this.type;
        }
 
        public String getName()
        {
            return this.name;
        }
 
        public int getAge()
        {
            return this.age;
        }
    }
 
    public enum PetType
    {
        CAT, DOG, HAMSTER, TURTLE, BIRD, SNAKE
    }
}

Au sujet de l’Auteur

Donald Raab dirige l’équipe JVM Architecture, qui fait partie du groupe Enterprise Platform de la division Technology de Goldman Sachs. 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

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Donnez-nous votre avis

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet
Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet

Discuter

Se connecter à InfoQ pour interagir sur ce qui vous importe le plus.


Récupérer votre mot de passe

Follow

Suivre vos sujets et éditeurs favoris

Bref aperçu des points saillants de l'industrie et sur le site.

Like

More signal, less noise

Créez votre propre flux en choisissant les sujets que vous souhaitez lire et les éditeurs dont vous désirez suivre les nouvelles.

Notifications

Restez à jour

Paramétrez vos notifications et ne ratez pas le contenu qui vous importe

BT