sort and group a java collection

前端 未结 5 548
无人及你
无人及你 2021-02-01 21:17

I have an object which has a name and a score. I would like to sort a collection of such objects so that they are grouped by name and sorted by maximum score in each group (and

5条回答
  •  情书的邮戳
    2021-02-01 22:07

    No, you can't do it with a single sort with a single Comparator.

    You have to:

    1. group by name
    2. sort the groups, by highest score in group
    3. Then you need to flatten the groups back to a list.

    With Java 8

    Edit: Since i wrote this answer, Java 8 has come out, which simplifies the problem a lot:

    import java.util.*;
    import static java.util.Comparator.*;
    import static java.util.stream.Collectors.*;
    

    List result = records.stream()
        .sorted(comparingInt(Record::getScore).reversed())
        .collect(groupingBy(Record::getName, LinkedHashMap::new, toList()))
        .values().stream()
        .flatMap(Collection::stream)
        .collect(toList());
    

    First we sort by score reversed, and then we group using a LinkedHashMap, which will preserve the insertion order for the keys, so keys with higher score will come first.

    Sorting first is OK if the groups are small, so the redundant compares between objects in different groups don't hurt so much.

    Also, with this method, duplicates are preserved.


    Alternatively, if you don't care about preserving duplicates, you can:

    Comparator highestScoreFirst = comparingInt(Record::getScore).reversed();
    
    List result = records.stream()
            .collect(groupingBy(Record::getName,
                    toCollection(() -> new TreeSet<>(highestScoreFirst))))
            .values().stream()
            .sorted(comparing(SortedSet::first, highestScoreFirst))
            .flatMap(Collection::stream)
            .collect(toList());
    

    Where the records are grouped into sorted TreeSets, instead of sorting the values as the first operation of the stream, and then the sets are sorted by their first, highest value.

    Grouping before sorting is appropriate if the groups are big, to cut down on redundant compares.


    Implementing Comparable:

    And you can make it shorter by having your record implement Comparable

    public class Record implements Comparable {
        @Override
        public int compareTo(Record other) {
            // Highest first
            return -Integer.compare(getScore(), other.getScore());
    
            /* Or equivalently:
               return Integer.compare(other.getScore(), getScore());
            */
        }
        ...
    }
    

    List result = records.stream()
        .collect(groupingBy(Record::getName, toCollection(TreeSet::new)))
        .values().stream()
        .sorted(comparing(SortedSet::first))
        .flatMap(Collection::stream)
        .collect(toList());
    

    Before Java 8

    Edit: Here is a really rough unit test that demonstrates one way to do it. I haven't cleaned it up as much as i would have liked.

    Stuff like this is painful in Java, and i would normally use Google Guava for this.

    import org.junit.Test;
    
    import java.util.*;
    
    import static java.util.Arrays.asList;
    import static org.junit.Assert.assertEquals;
    
    public class GroupSortTest {
    
        @Test
        public void testGroupSort() {
            List records = asList(
                    new Record("a", 3),
                    new Record("a", 9),
                    new Record("b", 7),
                    new Record("b", 10),
                    new Record("c", 8),
                    new Record("c", 3));
    
            List> recordsGroupedByName = groupRecordsByNameAndSortedByScoreDescending(records);
            Collections.sort(recordsGroupedByName, byHighestScoreInGroupDescending());
            List result = flattenGroups(recordsGroupedByName);
    
            List expected = asList(
                    new Record("b", 10),
                    new Record("b", 7),
                    new Record("a", 9),
                    new Record("a", 3),
                    new Record("c", 8),
                    new Record("c", 3));
    
            assertEquals(expected, result);
        }
    
        private List flattenGroups(List> recordGroups) {
            List result = new ArrayList();
            for (SortedMap group : recordGroups) {
                result.addAll(group.values());
            }
            return result;
        }
    
        private List> groupRecordsByNameAndSortedByScoreDescending(List records) {
            Map> groupsByName = new HashMap>();
            for (Record record : records) {
                SortedMap group = groupsByName.get(record.getName());
                if (null == group) {
                    group = new TreeMap(descending());
                    groupsByName.put(record.getName(), group);
                }
                group.put(record.getScore(), record);
            }
            return new ArrayList>(groupsByName.values());
        }
    
        private DescendingSortComparator descending() {
            return new DescendingSortComparator();
        }
    
        private ByFirstKeyDescending byHighestScoreInGroupDescending() {
            return new ByFirstKeyDescending();
        }
    
        private static class ByFirstKeyDescending implements Comparator> {
            public int compare(SortedMap o1, SortedMap o2) {
                return o2.firstKey().compareTo(o1.firstKey());
            }
        }
    
        private static class DescendingSortComparator implements Comparator {
            public int compare(Comparable o1, Comparable o2) {
                return o2.compareTo(o1);
            }
        }
    }
    

提交回复
热议问题