Java 8, лямбда: сортировка внутри сгруппированных списков и объединение всех групп в список

на основе следующего ответа:https://stackoverflow.com/a/30202075/8760211

как отсортировать каждую группу по stud_id, а затем вернуть список со всеми студентами в результате группировки по stud_location, а затем сортировки по stud_id)?

было бы здорово иметь это как расширение существующего лямбда-выражения:

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

мне нужна группировка на основе порядка элементов в исходном списке.

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"

в результат будет выглядеть следующим образом:

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"

я попробовал следующее:

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

но это не сработает.

9 ответов


Если я правильно понял, вы хотите List<Student> (не карта), где студенты группируются по их местоположениям и сортируются по идентификаторам внутри групп и где группы также сортируются по номерам, а не по именам расположение. Это возможно, но требует одной группировки и двух сортировок:

//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());

это произведет следующее:

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'}

возможно, это можно сделать более элегантно, предложения приветствуются

EDIT: в то время этот ответ был написано не было упоминания о группировка на основе порядка элементов в исходном списке в вопросе OPs. Таким образом, мое предположение заключалось в сортировке списка и групп по идентификаторам. Для решений, основанных на порядке в исходном списке, см. другие ответы, например, Holgers one


поскольку результат должен быть списком, вы не группируете, а просто сортируете (в смысле изменения порядка в соответствии с определенным правилом). Основным препятствием является то, что вы хотите, чтобы места были заказаны после их первой встречи в первоначальный список.

прямой подход заключается в том, чтобы сначала исправить этот порядок расположения, а затем выполнить одну операцию сортировки:

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));

если вы не можете или не хотите изменять исходный список, вы можете просто использовать копировать:

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

это также можно решить с помощью операции группировки, но это не проще:

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())));

обратите внимание, что вы должны собирать в LinkedHashMap для обеспечения сохранения порядка групп.


вы можете добавить одну строку:

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

или вы можете написать свой собственный коллектор.

Я знаю, какой из них я бы выбрал.


попробуйте сначала сортировать, а затем groupinBy он работает хорошо. Приведенный ниже код сортирует студентов в пределах местоположения.

students.stream().sorted().collect(Collectors.groupingBy(Student::getLocation))
Выход в этом случае

{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]}

если вы хотите, чтобы места также были отсортированы, используйте фрагмент кода, как показано ниже

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

выход в этом случае {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]}

класс Student реализует Comparable, а метод compareTo основан на идентификаторе.


во-первых, о сортировке внутри каждую группу. Collectors.groupingBy есть второй вариант, который позволяет указать коллектор, который используется для создания групп. Указанный вами коллектор может быть тем, который собирает элементы отсортированным способом (например, a TreeSet), и в качестве завершающей операции преобразует его в отсортированный список. Такой коллектор можно создать с помощью Collectors.collectingAndThen().

например, с целыми числами I попробовал:

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

выход:

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

Я уверен, что вам удастся перевести это в вашем случае. Возможно, Вам потребуется создать TreeSet С помощью пользовательского компаратора, чтобы студенты в каждой группе упорядочены так, как вы хотите, или, если студенты сортируются одинаково всегда, сделайте Student реализовать Comparable.

второй о сортировке групп. Collectors.groupingBy по умолчанию создает HashMap, который не имеет заданного порядка ключей (выше, ключи заказаны правильно случайно). Итак, чтобы заказать ключи, вам нужно использовать Collectors.groupingBy вариант, который также позволяет создавать карту результатов, которая, к счастью, также существует:

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

я указал пользовательский компаратор для карты, чтобы показать, что порядок действительно отличается. В результате:

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

теперь, является ли это более читаемым и поддерживаемым, чем старое доброе решение pre-Java 8, это вопрос вкуса...


Если вы просто хотите группировать и сортировать, вам не нужен groupingBy() коллектор или даже поток вообще. Просто используйте составную сортировку:

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

не 100% ясно, ожидаете ли вы Map<String, List<Student>> или просто List<Student>, тем не менее, вот оба решения:

импорт:

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

получение Map<String, List<Student>> каждая List<Student> содержит студентов, отсортированных по их идентификаторам.

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())))));

С другой стороны, если вы хотите получить просто список Student объекты, отсортированные по заданному свойству, тогда было бы пустой тратой ресурсов для выполнения groupingBy, sorted, collect и как-то уменьшить карту значения в один список. Скорее просто сортируйте объекты Student в списке, предоставляя ключ сортировки, т. е.

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

или

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

или в зависимости от того, хотите ли вы Сортировать по более чем одному свойству, вы можете сделать что-то вроде shmosel ответ.


вам не нужно, чтобы группы по местоположению, так как вход и выход имеют одинаковый тип данных. Вы можете просто связать несколько компараторов и отсортировать входные данные.

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

как offtopic, я бы хотел, чтобы вы изменили структуру данных, чтобы использовать Lombok для автоматического создания геттеров/сеттеров / конструкторов https://projectlombok.org/features/Data . Вы также должны использовать более общее преобразование именования, т. е. удалить префикс атрибутов" stud_".

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

я столкнулся с той же проблемой, и я попробовал все решения, но не смог решить проблему, Затем я попытался следовать пути, и, наконец, я могу исправить свою проблему.

в приведенном выше примере мой studlist был уже отсортирован, но все еще его генерация карты в любом порядке, который был основным вопросом.

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

поэтому, используя это решение выше, он отсортировал все ключи на карте. Наша цель получить результат в карту только тогда, почему люди пытаются преобразовать его в список?