简体   繁体   中英

Java 8, Lambda: Sorting within grouped Lists and merging all groups to a list

Based on the following answer: https://stackoverflow.com/a/30202075/8760211

How to sort each group by stud_id and then return a List with all Students as result of the grouping by stud_location and then sorting by stud_id)?

It would be great to have this as extension to the existing Lambda Expression:

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

I need the Grouping based on the order of the Elements in the origin List.

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"

The result would then look like the following:

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"

I have tried the following:

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

But this doesn't work.

If I get you right, you want a List<Student> (not a map) where students are grouped by their locations and sorted by ids inside groups and where groups are also sorted by ids, not by location names. This is possible, but requires one grouping and two sortings:

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

This will produce following:

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

Maybe this can be done more elegantly, suggestions are welcome

EDIT: At the time this answer was written there was no mention about Grouping based on the order of the Elements in the origin List in the OPs question. So my assumption was to sort both list and groups by ids. For solutions based on the order in the original list see other answers, for example, the Holgers one

Since the result is supposed to be a list, you're not grouping but simply sorting (in the sense of changing the order according to a defined rule). The main obstacle is that you want the locations to be ordered after their first encounter in the original list.

The straight-forward approach is to fix this location order first, followed by a single sort operation:

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

If you can not or do not want to modify the original list, you can simply use a copy:

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

It's also possible to solve this with a grouping operation, but that's not easier:

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

Note that you have to collect into a LinkedHashMap to ensure that the order of the groups is retained.

not 100% clear whether you're expected a Map<String, List<Student>> or just a List<Student> , nevertheless here are both solutions:

imports:

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

retrieving a Map<String, List<Student>> where each List<Student> contains students sorted by their ids.

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

on the other hand, if you want to retrieve just a list of Student objects sorted by a given property then it would be a waste of resources to perform a groupingBy , sorted , collect and somehow reduce the map values into a single list. Rather just sort the Student objects within the list providing a sort key ie

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

or

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

or depending on whether you want to sort by more than one property then you could do something like shmosel's answer .

You can add one line:

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

Or you can write your own collector.

I know which one I would choose.

try sort first and then groupinBy it works well. The below code sorts the Students within the location.

students.stream().sorted().collect(Collectors.groupingBy(Student::getLocation))
The output in this case is

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

If you would like to have locations also to be sorted, use a code snippet like below

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

The output in this case is {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 class implements Comparable and the compareTo method is is based on the id.

First, about sorting within each group. Collectors.groupingBy has a second variant which allows you to specify a collector which is used to generate the groups. The collector you specify can be one which collects the items in a sorted way (eg a TreeSet ), and as a finishing operation transforms it to a sorted list. Such a collector can be created with Collectors.collectingAndThen() .

For example, with Integers I tried:

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

Output:

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

I'm sure you manage to translate this to your case. You might need to create a TreeSet with a custom comparator so that the students in each group are ordered the way you want, or, if students are sorted in the same way always, make Student implement Comparable .

Second about sorting the groups. Collectors.groupingBy by default creates a HashMap , which does not have a specified order of the keys (above, the keys are ordered correctly by chance). So, to also order the keys, you need to use the Collectors.groupingBy variant which also lets you create the result map, which luckily also exists:

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

I specified a custom comparator for the map to show that the ordering is indeed different. The result is:

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

Now, whether this is more readable and maintainable than a good-old pre-Java 8 solution is a matter of taste...

If you just want to group and sort, you don't need a groupingBy() collector, or even a stream at all. Just use a composite sort:

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

You don't need to group by location, as the input and output have same datatype. You can just chain multiple Comparators and sort the input.

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

As an offtopic, I would seggest that you change you data structure to use Lombok to auto-generate getters/setters/constructors https://projectlombok.org/features/Data . You should also use a more generic naming conversion, ie remove the "stud_" prefix of attributes.

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

I was facing same issue and I tried all the solutions but not able to fix the problem, Then I tried following way and finally I am able to fix my problem.

In above example my studlist was already sorted but still its generating map in any order that was main question.

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

So using this above solution it sorted all the keys in Map. Our goal to get result into Map only then why people try to converting it into list?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM