it-swarm.com.ru

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

На основании следующего ответа: 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))

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

6
ThomasMuller

Если я вас правильно понял, вам нужен 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'}

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

Правка: На момент написания этого ответа не было упоминания о Группировка на основе порядка элементов в списке происхождения в вопросе OP. Таким образом, я предполагал отсортировать список и группы по идентификаторам. Для решений, основанных на порядке в исходном списке, см. Другие ответы, например, Holgers

4
Kirill Simonov

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

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

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, чтобы гарантировать, что порядок групп сохраняется.

4
Holger

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

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

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

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

1
Nagesh Dalave

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

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

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

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

Класс ученика реализует Comparable, а метод CompareTo основан на идентификаторе. 

1
practprogrammer

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

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

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

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

1
Bohemian

не на 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 .

1
Aomine

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

Например, с целыми числами я пытался:

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

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

1
Hoopje

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

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

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

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