14

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.

ThomasMuller
  • 279
  • 1
  • 3
  • 12
  • I tried this `studlistGrouped.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getValue))` but it didn't work! – ThomasMuller Dec 06 '17 at 17:09
  • 1
    Someone has down voted this question without any comment!!! It seems that some People have fun down voting other people's questions! – ThomasMuller Dec 06 '17 at 17:09
  • If you hover over the down arrow you can see the instruction for when it's appropriate to downvote: "this question does not show any research effort". That would be my best bet for the downvote. I will edit your attempt into your question. – Roddy of the Frozen Peas Dec 06 '17 at 17:12
  • Hi Roddy, thanks so much for your time. Downvoting: I would only downvote silly questions like "What is the result of 2 + 2?" More serious questions shouldn't be downvoted, because other people are more advanced in that theme. – ThomasMuller Dec 06 '17 at 17:25
  • Feel free to bring it up on Meta. I just indicated the official guidance on this matter. – Roddy of the Frozen Peas Dec 06 '17 at 17:28

9 Answers9

17

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 i.e.

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.

Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
  • Hi Aominè, thanks so much for your help. After comprehensive comparison, I found that Kirill's compact solution does solve the problem directly. Holger, thanks for your hint. – ThomasMuller Dec 07 '17 at 12:58
8

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

Kirill Simonov
  • 8,257
  • 3
  • 18
  • 42
  • Hi Kirill, thanks so much for your help. Your compact solution does solve the problem directly without the need of further coding. – ThomasMuller Dec 07 '17 at 12:51
  • 1
    Hmm, I would never have assumed that “grouping based on the order of the elements in the origin List” implies “sort groups based on their minimum id”, especially as the question’s example contradicts that assumption, as it shows “California”, smallest id `4321`, before “Los Angeles”, smallest id `2234`. You must have read the OP’s mind… – Holger Dec 07 '17 at 14:06
  • Hi Holger, with yours and Kirril's solution I get the same correct result `1726, 3442, 5223, 4321, 7765, 2234`!!! This differs from the result list which Kirill wrote in his answer! Please put me to the right direction, if you see any issue in Kirill's solution. Many thanks in advance. – ThomasMuller Dec 08 '17 at 09:18
  • My result list is different because I used a different input to experiment with various id combinations – Kirill Simonov Dec 08 '17 at 17:31
  • @ThomasMuller, by the way, my solution gives you `1726, 3442, 5223, 2234, 4321, 7765`, because minimum id in `New York` group is `1726` and next id is Andrew's `2234` in `Los Angeles` group, and only then goes `California` – Kirill Simonov Dec 08 '17 at 17:42
  • It differs from output in your question, but I tried to read your mind and assumed that you want to sort the list in this way :) – Kirill Simonov Dec 08 '17 at 17:45
  • Yes, I tried your answer, which was sent before Holger's correct solution and both work. Thanks again to you, Holger and other IT-Friends. – ThomasMuller Dec 11 '17 at 13:13
5

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
2

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.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • 3
    There is no guaranty that the result lists are mutable list, so you should change the collector to `Collectors.groupingBy(w -> w.stud_location, Collectors.toCollection(ArrayList::new))` to ensure that this post-processing is possible. – Holger Dec 07 '17 at 10:56
  • Hi Bohemian, thanks so much for your help. After comprehensive comparison, I found that Kirill's compact solution does solve the problem directly. Holger, thanks for your hint. – ThomasMuller Dec 07 '17 at 12:58
1

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.

  • Hi Practprogrammer, thanks so much for your help. After comprehensive comparison, I found that Kirill's compact solution does solve the problem directly. – ThomasMuller Dec 07 '17 at 12:48
1

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 (e.g. 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...

Hoopje
  • 12,677
  • 8
  • 34
  • 50
  • 1
    `new TreeMap<>((a, b) -> b.compareTo(a))` is equivalent to `new TreeMap<>(Comparator.reverseOrder())`. To keep the encounter order, you need `new LinkedHashMap<>()` instead. Note that `Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>()), set -> new ArrayList<>(set))`, resp. `Collectors.collectingAndThen(Collectors.toCollection(TreeSet::new), ArrayList::new)` is tempting, but using `Collectors.collectingAndThen(Collectors.toCollection(ArrayList::new), l->{ l.sort(null); return l; })` is more efficient for most use cases. – Holger Dec 07 '17 at 11:54
  • Hi Hoopje, thanks so much for your detailed Explanation. After comprehensive comparison, I found that Kirill's compact solution does solve the problem directly. Holger, thanks for your hint! – ThomasMuller Dec 07 '17 at 13:07
1

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));
Ousmane D.
  • 54,915
  • 8
  • 91
  • 126
shmosel
  • 49,289
  • 6
  • 73
  • 138
  • 2
    *Almost*. The OP doesn’t want the list to be sorted by the location’s natural order, but the order of their first encounter. – Holger Dec 07 '17 at 11:04
  • Hi Shmosel, thanks so much for your help. After comprehensive comparison, I found that Kirill's compact solution does solve the problem directly. Holger, thanks for your hint. – ThomasMuller Dec 07 '17 at 12:52
  • 1
    @Holger OP added that after I posted this. Most of the answers here don't respect encounter order. Anyway, it seems like an odd requirement, one that OP himself ignored when selecting the answer. – shmosel Dec 07 '17 at 15:35
  • 1
    @shmosel: indeed, I already commented on this at that answer. – Holger Dec 07 '17 at 15:41
  • Hi Holger, Hi shmosel: I added a comment to Kirill's answer. His answer and that from Holger lead to the same correct result! thanks again for your help. – ThomasMuller Dec 08 '17 at 09:23
1

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, i.e. remove the "stud_" prefix of attributes.

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

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?

Nagesh Dalave
  • 109
  • 1
  • 4