Сортировка Списки после группировкиBy

Мне интересно, если в потоках (или сборщиках) уже реализована функция, которая сортировала списки как значения. Например. в следующих кодах приводятся полные списки лиц, отсортированные по возрасту. Первое решение имеет некоторую накладную сортировку (и выглядит немного неряшливо). Второе решение должно смотреть на каждого человека дважды, но делает работу красиво.

Первая сортировка, затем группировка в один поток:

Map<Gender, List<Person>> sortedListsByGender = (List<Person>) roster
        .stream()
        .sorted(Person::compareByAge)
        .collect(Collectors.groupingBy(Person::getGender));

Первая группировка, затем сортировка каждого значения:

Map<Gender, List<Person>> sortedListsByGender = (List<Person>) roster
        .stream()
        .collect(Collectors.groupingBy(Person::getGender));
sortedListsByGender.values()
        .forEach(list -> Collections.sort(list, Person::compareByAge));

Мне просто интересно, если уже что-то реализовано, что делает это за один проход, например groupingBySorted.

Ответ 1

При использовании sorted(comparator) в потоке перед операцией collect поток должен буферизовать содержимое всего потока, чтобы иметь возможность сортировать его, и сортировка может включать гораздо большее перемещение данных в этом буфере по сравнению с сортировкой меньшие списки групп впоследствии. Таким образом, производительность не так хороша, как сортировка отдельных групп, хотя реализация будет использовать несколько ядер, если включена параллельная обработка.

Но обратите внимание, что использование sortedListsByGender.values().forEach(…) не является параллелизуемой операцией, и даже использование sortedListsByGender.values().parallelStream().forEach(…) допускает только параллельную обработку групп, в то время как каждая операция сортировки по-прежнему является последовательной.

При выполнении операции сортировки в коллекторе, как в

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collectors.collectingAndThen(
        Collectors.toCollection(ArrayList::new), l->{ l.sort(c); return l; } );
}

Map<Gender, List<Person>> sortedListsByGender = roster.stream()
    .collect(Collectors.groupingBy(Person::getGender, toSortedList(Person::compareByAge)));

операция сортировки ведет себя одинаково (спасибо Тагиру Валееву за исправление меня), но вы можете легко проверить, как выполняется стратегия сортировки по вставке. Просто измените реализацию коллектора на:

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collectors.collectingAndThen(
        Collectors.toCollection(()->new TreeSet<>(c)), ArrayList::new);
}

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

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collector.of(ArrayList::new,
        (l,t) -> {
            int ix=Collections.binarySearch(l, t, c);
            l.add(ix<0? ~ix: ix, t);
        },
        (list1,list2) -> {
            final int s1=list1.size();
            if(list1.isEmpty()) return list2;
            if(!list2.isEmpty()) {
                list1.addAll(list2);
                if(c.compare(list1.get(s1-1), list2.get(0))>0)
                    list1.sort(c);
            }
            return list1;
        });
}

Эффективен для последовательного использования, но его функция слияния не является оптимальной. Алгоритм базового сортирования выиграет от предварительно определенных диапазонов, но сначала должен найти эти диапазоны, несмотря на то, что наша функция слияния действительно знает эти диапазоны. К сожалению, в JRE нет публичного API, позволяющего использовать эту информацию (эффективно, мы можем передать subList в binarySearch, но создание нового подписок для каждого элемента list2 может оказаться слишком дорогостоящим). Если мы хотим еще больше повысить производительность параллельного выполнения, мы должны повторно реализовать часть слияния алгоритма сортировки:

static <T> Collector<T,?,List<T>> toSortedList(Comparator<? super T> c) {
    return Collector.of(ArrayList::new,
        (l,t) -> l.add(insertPos(l, 0, l.size(), t, c), t),
        (list1,list2) -> merge(list1, list2, c));
}
static <T> List<T> merge(List<T> list1, List<T> list2, Comparator<? super T> c) {
    if(list1.isEmpty()) return list2;
    for(int ix1=0, ix2=0, num1=list1.size(), num2=list2.size(); ix2<num2; ix2++, num1++) {
        final T element = list2.get(ix2);
        ix1=insertPos(list1, ix1, num1, element, c);
        list1.add(ix1, element);
        if(ix1==num1) {
            while(++ix2<num2) list1.add(list2.get(ix2));
            return list1;
        }
    }
    return list1;
}
static <T> int insertPos(
    List<? extends T> list, int low, int high, T t, Comparator<? super T> c) {
    high--;
    while(low <= high) {
        int mid = (low+high)>>>1, cmp = c.compare(list.get(mid), t);
        if(cmp < 0) low = mid + 1;
        else if(cmp > 0) high = mid - 1;
        else {
            mid++;
            while(mid<=high && c.compare(list.get(mid), t)==0) mid++;
            return mid;
        }
    }
    return low;
}

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

Ответ 2

Вы можете использовать компаратор и решить проблему:

class Sorter implements Comparator<Person>{
@Override
public int compare(Person p1, Perspn p2) {
    if(p1.getGender().equals(p2.getGender())) {
        return Integer.compare(p2.getAge(),p1.getAge());
    } else if(p1.getGender().equals("M")){
        return -1;
    } else if(p1.getGender().equals("F")) {
        return 0;
    } 
}

}

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