web-dev-qa-db-fra.com

Java 8, Lambda: Tri dans des listes groupées et fusion de tous les groupes dans une liste

Basé sur la réponse suivante: https://stackoverflow.com/a/30202075/8760211

Comment trier chaque groupe par stud_id puis renvoyer une liste avec tous les étudiants résultant du regroupement par stud_location puis du tri par stud_id)?

Ce serait bien d'avoir ceci comme extension à l'expression Lambda existante:

Map<String, List<Student>> studlistGrouped =
    studlist.stream().collect(Collectors.groupingBy(w -> w.stud_location));

J'ai besoin du regroupement basé sur l'ordre des éléments dans la liste d'origine.

First group: "New York"
Second group: "California"
Third group: "Los Angeles"

1726, "John", "New York"
4321, "Max", "California"
2234, "Andrew", "Los Angeles"
5223, "Michael", "New York"
7765, "Sam", "California"
3442, "Mark", "New York"

Le résultat ressemblerait alors à ce qui suit:

List<Student> groupedAndSorted = ....

    1726, "John", "New York"
    3442, "Mark", "New York"
    5223, "Michael", "New York"
    4321, "Max", "California"
    7765, "Sam", "California"
    2234, "Andrew", "Los Angeles"

J'ai essayé ce qui suit:

studlistGrouped.entrySet().stream().sorted(Comparator.compar‌​ing(Map.Entry::getVa‌​lue))

Mais ça ne marche pas.

6
ThomasMuller

Si je vous comprends bien, vous voulez un List<Student> (pas une carte) dans lequel les étudiants sont regroupés par leur emplacement et triés par ID dans les groupes et où les groupes sont également triés par ID, et non par nom d'emplacement. C'est possible, mais nécessite un regroupement et deux triages:

//first, use your function to group students
Map<String, List<Student>> studlistGrouped = students.stream()
        .collect(Collectors.groupingBy(Student::getLocation, Collectors.toList()));

//then sort groups by minimum id in each of them
List<Student> sorted = studlistGrouped.entrySet().stream()
        .sorted(Comparator.comparing(e -> e.getValue().stream().map(Student::getId).min(Comparator.naturalOrder()).orElse(0)))
        //and also sort each group before collecting them in one list
        .flatMap(e -> e.getValue().stream().sorted(Comparator.comparing(Student::getId))).collect(Collectors.toList());

Cela produira les éléments suivants:

Student{id='1726', name='John', location='New York'}
Student{id='3442', name='Mark', location='New York'}
Student{id='5223', name='Michael', location='New York'}
Student{id='2234', name='Andrew', location='Los Angeles'}
Student{id='4321', name='Max', location='California'}
Student{id='7765', name='Sam', location='California'}

Peut-être que cela peut être fait plus élégamment, les suggestions sont les bienvenues

EDIT: Au moment de l'écriture de cette réponse, il n'y avait aucune mention sur Le regroupement basé sur l'ordre des éléments dans la liste d'origine dans la question des OP. Donc, mon hypothèse était de trier la liste et les groupes par identifiants. Pour des solutions basées sur l'ordre de la liste d'origine, voir d'autres réponses, par exemple celle de Holgers.

4
Kirill Simonov

Étant donné que le résultat est censé être une liste, vous ne faites pas de regroupement mais simplement un tri (dans le sens de changer l’ordre en fonction d’une règle définie). L'obstacle principal est que vous souhaitiez que les emplacements soient commandés après leur première rencontre dans la liste d'origine.

L’approche directe consiste à fixer d’abord cet ordre de localisation, suivi d’une opération de tri unique:

Map<String,Integer> locationOrder = studlist.stream()
    .collect(HashMap::new,
             (m,s)->m.putIfAbsent(s.stud_location, m.size()),
             (m1,m2)->m2.keySet().forEach(l->m1.putIfAbsent(l, m1.size())));

studlist.sort(Comparator.comparingInt((Student s) -> locationOrder.get(s.stud_location))
                        .thenComparing(s -> s.stud_id));

Si vous ne pouvez ou ne voulez pas modifier la liste d'origine, vous pouvez simplement utiliser une copie:

List<Student> result = new ArrayList<>(studlist);
result.sort(Comparator.comparingInt((Student s) -> locationOrder.get(s.stud_location))
                      .thenComparing(s -> s.stud_id));

Il est également possible de résoudre ce problème avec une opération de regroupement, mais ce n’est pas plus facile:

List<Student> result = studlist.stream()
    .collect(Collectors.collectingAndThen(
                Collectors.groupingBy(s -> s.stud_location,
                                      LinkedHashMap::new, Collectors.toList()),
                m -> m.values().stream()
                      .flatMap(l -> l.stream().sorted(Comparator.comparing(s->s.stud_id)))
                      .collect(Collectors.toList())));

Notez que vous devez rassembler dans une LinkedHashMap pour vous assurer que l'ordre des groupes est conservé.

4
Holger

Je faisais face au même problème et j’ai essayé toutes les solutions, mais j’ai été incapable de résoudre le problème, Ensuite, j’ai essayé de suivre et finalement je suis capable de régler mon problème.

Dans l'exemple ci-dessus, ma studlist était déjà triée mais sa carte Était générée dans l'ordre de la question principale.

Map<String, List<Student>> studlistGrouped =
    studlist.stream().collect(Collectors.groupingBy(w -> w.stud_location));
**studlistGrouped = new TreeMap<String, List<Student>>(studlistGrouped);**

Donc, en utilisant la solution ci-dessus, toutes les clés de Map ont été triées. Notre objectif est d’obtenir le résultat dans Map uniquement, alors pourquoi les gens essaient-ils de le convertir en liste?

1
Nagesh Dalave

Si vous souhaitez simplement grouper et trier, vous n'avez pas besoin d'un collecteur groupingBy(), ni même d'un flux. Il suffit d'utiliser un tri composite:

studlist.sort(Comparator.comparing(Student::getLocation).thenComparing(Student::getId));
1
shmosel

essayez d’abord de trier puis de grouperPar cela fonctionne bien. Le code ci-dessous trie les étudiants au sein de l'emplacement. 

students.stream().sorted().collect(Collectors.groupingBy(Student::getLocation))
La sortie dans ce cas est 

{New York=[1726  John  New York, 3442  Mark  New York, 5223  Michael  New York], Los Angeles=[2234  Andrew  Los Angeles], California=[4321  Max  California, 7765  Sam  California]}

Si vous souhaitez également que les emplacements soient triés, utilisez un extrait de code comme ci-dessous.

students.stream().sorted().collect(Collectors.groupingBy(Student::getLocation, TreeMap::new, Collectors.toList()))

La sortie dans ce cas est {California=[4321 Max California, 7765 Sam California], Los Angeles=[2234 Andrew Los Angeles], New York=[1726 John New York, 3442 Mark New York, 5223 Michael New York]}

La classe d'étudiant implémente Comparable et la méthode compareTo est basée sur l'id. 

1
practprogrammer

Vous pouvez ajouter une ligne:

studlistGrouped.values().forEach(list -> list.sort(Comparator.comparing(Student::getId)));

Ou vous pouvez écrire votre propre collectionneur.

Je sais lequel je choisirais.

1
Bohemian

pas tout à fait clair si vous êtes attendu à un Map<String, List<Student>> ou juste un List<Student>, néanmoins voici les deux solutions:

importations:

import static Java.util.stream.Collectors.*;
import Java.util.*;
import Java.util.function.Function;

récupérer un Map<String, List<Student>> où chaque List<Student> contient des étudiants triés par leur identifiant.

Map<String, List<Student>> resultSet = studlist.stream()
      .collect(groupingBy(Student::getLocation,
             mapping(Function.identity(),
                  collectingAndThen(toList(),
                      e -> e.stream().sorted(Comparator.comparingInt(Student::getId))
                                            .collect(toList())))));

par contre, si vous voulez récupérer uniquement une liste d'objets Student triés par une propriété donnée, il serait inutile de réaliser une groupingBy, sorted, collect et de réduire les valeurs de la carte en une seule liste. Il suffit simplement de trier les objets Student de la liste en fournissant une clé de tri, par exemple.

studlist.sort(Comparator.comparingInt(Student::getId));

ou 

studlist.sort(Comparator.comparing(Student::getLocation));

ou selon que vous souhaitiez trier plus d'une propriété, vous pourriez faire quelque chose comme la réponse de shmosel .

1
Aomine

Tout d’abord, à propos du tri au sein de chaque groupe. Collectors.groupingBy a une deuxième variante qui vous permet de spécifier un collecteur utilisé pour générer les groupes. Le collecteur que vous spécifiez peut être un collecteur qui collecte les éléments de manière triée (par exemple, une TreeSet) et le transforme en une opération de finition en une liste triée. Un tel collecteur peut être créé avec Collectors.collectingAndThen().

Par exemple, avec Integers, j'ai essayé:

List<Integer> list = Arrays.asList(9, 2, 43, 6, 5, 3, 87, 56, 87, 67, 77, 22, 23, 1);
System.out.println(
    list.stream().collect(Collectors.groupingBy(
        i -> i % 3,                                         // classifier
        Collectors.collectingAndThen(
            Collectors.toCollection(() -> new TreeSet<>()), // intermediate collector 
            set -> new ArrayList<>(set)))));                // finishing operation

Sortie:

{0=[3, 6, 9, 87], 1=[1, 22, 43, 67], 2=[2, 5, 23, 56, 77]}

Je suis sûr que vous parvenez à traduire cela dans votre cas. Vous devrez peut-être créer une TreeSet avec un comparateur personnalisé pour que les élèves de chaque groupe soient classés comme vous le souhaitez ou, si les élèves sont triés de la même manière, toujours, faites passer la Student implémenter Comparable.

Deuxièmement, à propos du tri des groupes. Collectors.groupingBy crée par défaut une HashMap, qui n'a pas un ordre spécifié des clés (ci-dessus, les clés sont ordonnées correctement par hasard). Donc, pour commander également les clés, vous devez utiliser la variante Collectors.groupingBy qui vous permet également de créer la carte de résultats, qui existe heureusement aussi:

System.out.println(
    list.stream().collect(Collectors.groupingBy(
        i -> i % 3,                                         // classifier
        new TreeMap<>((a, b) -> b.compareTo(a)),            // map creator
        Collectors.collectingAndThen(
            Collectors.toCollection(() -> new TreeSet<>()), // intermediate collector
            set -> new ArrayList<>(set)))));                // finishing operation

J'ai spécifié un comparateur personnalisé pour la carte afin de montrer que la commande est bien différente. Le résultat est:

{2=[2, 5, 23, 56, 77], 1=[1, 22, 43, 67], 0=[3, 6, 9, 87]}

Maintenant, savoir si cela est plus lisible et maintenable qu’une bonne vieille solution avant Java 8 est une question de goût….

1
Hoopje

Vous n'avez pas besoin de grouper par emplacement car l'entrée et la sortie ont le même type de données. Vous pouvez simplement chaîner plusieurs comparateurs et trier les entrées.

List<Student> groupedAndSorted = studlist.stream()
       .sorted(Comparator.comparing(Student::getStudLocation)
                .thenComparing(Comparator.comparing(Student::getStudId)))
                .thenComparing(Comparator.comparing(Student::getStudName)))
       .collect(Collectors.toList());

En tant qu'offtopic, je dirais que vous modifiez votre structure de données pour utiliser Lombok afin de générer automatiquement des getters/setters/constructors https://projectlombok.org/features/Data . Vous devez également utiliser une conversion de nom plus générique, c’est-à-dire supprimer le préfixe "stud_" des attributs.

import lombok.Data;
@Data
class Student {
    String id;
    String name;
    String location;
}
1