Сортировка и суб-листинг из выражения лямбда

У меня есть список массивов со следующими элементами.

List<Record> list = new ArrayList<>();
list.add(new Record(3, "32"));
list.add(new Record(4, "42"));
list.add(new Record(1, "12"));
list.add(new Record(1, "11"));
list.add(new Record(2, "22"));
list.add(new Record(5, "52"));
list.add(new Record(5, "53"));
list.add(new Record(5, "51"));

Запись - это простое POJO с идентификатором и именем

Я хочу сделать это в списке.

  • Создайте такую ​​карту, как Map<Integer, List<Record>>, у которой ключ - это идентификатор id и более тонкий ключ в качестве списка. Я сделал это как ниже.

    Map<Integer, List<Record>> map = list.stream()
        .collect(Collectors.groupingBy(Record::getId, HashMap::new, Collectors.toList()));
    
  • Теперь я хочу отсортировать список по имени и подкатегорию с предоставленным лимитом внутри карты

    map.forEach((k, v) -> v.stream().sorted(Comparator.comparing(Record::getName)));    
    map.forEach((k, v) -> map.put(k, v.subList(0, Math.min(**limit**, v.size()))));
    

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

Ответ 1

Вы можете использовать Java 8 collectingAndThen метод:

Map<Integer, List<Record>> map = list.stream()
    .collect(Collectors.groupingBy(
        Record::getId,
        Collectors.collectingAndThen(
            Collectors.toList(),
            records -> records.stream()
                .sorted(Comparator.comparing(Record::getName))
                .limit(limit)
                .collect(Collectors.toList()))));

Ответ 2

Вы можете использовать Collectors.collectingAndThen:

Map<Integer, List<Record>> result = list.stream()
    .collect(Collectors.groupingBy(
         Record::getId,
         Collectors.collectingAndThen(
             Collectors.toCollection(ArrayList::new),
             v -> {
                 v.sort(Comparator.comparing(Record::getName));
                 return v.subList(0, Math.min(LIMIT, v.size()));
             })));

Это решение позволяет избежать создания нового потока для каждой группы списков.

Как указано в этом ответе, используя Collectors.toCollection(ArrayList::new), мы гарантируем, что список изменен, поэтому мы можем впоследствии отсортировать его на месте.

Ответ 3

Вы можете использовать

Map<Integer, List<Record>> map = list.stream()
    .collect(Collectors.groupingBy(Record::getId,Collectors.toCollection(ArrayList::new)));
map.values().forEach(l -> {
    list.sort(Comparator.comparing(Record::getName));
    l.subList(limit, l.size()).clear();
});

Используя Collectors.toCollection(ArrayList::new), мы гарантируем, что список результатов будет изменен. Затем мы сортируем список на месте и удаляем ненужные значения. Вместо того, чтобы создавать подсписку, содержащую элементы, которые мы хотим (которые будут содержать ссылку на полный список), мы строим подсписку элемента, который нам не нужен, и clear() it, чтобы эффективно удалить этот элемент из исходного списка.

Вы также можете записать его как один оператор:

    Map<Integer, List<Record>> map = list.stream()
        .collect(Collectors.groupingBy(Record::getId,
            Collectors.collectingAndThen(
                Collectors.toCollection(ArrayList::new),
                l -> {
                    list.sort(Comparator.comparing(Record::getName));
                    l.subList(limit, l.size()).clear();
                    l.trimToSize();
                    return l;
                })));

в качестве бонуса, я также добавил l.trimToSize();, который направляет ArrayList на использование меньшего массива, если предыдущий .subList(limit, l.size()).clear() удалил много элементов. Поскольку это может означать операцию копирования, это компромисс между процессорным временем и памятью здесь. Поэтому, если результат будет использоваться только через довольно короткое время, вы не будете использовать trimToSize().


Операция становится еще более простой (и потенциально более эффективной) при использовании StreamEx:

Map<Integer, List<Record>> map = list.stream()
    .collect(Collectors.groupingBy(Record::getId,
             MoreCollectors.least(Comparator.comparing(Record::getName), limit)));

Ответ 4

list.stream()
    .collect(Collectors.groupingBy(
          Record::getId,
          Collectors.collectingAndThen(
                   Collectors.toList(),
                   x -> x.stream()
                         .sorted(Comparator.comparing(Record::getName))
                         .limit(limit)
                         .collect(Collectors.toList())))); 

Ответ 5

Вы можете просто сортировать, прежде чем собирать элементы на карте. Для предельного бита вы можете использовать collectingAndThen для последующей обработки списка и stream.limit его.

Map<Integer, List<Record>> map = list.stream()
        .sorted(Comparator.comparing(Record::getName))
        .collect(Collectors.groupingBy(Record::getId, 
                Collectors.collectingAndThen(Collectors.toList(), 
                        l -> l.stream().limit(limit).collect(Collectors.toList()))));

С limit = 2 это приводит к

{1=[Record(id=1, name=11), Record(id=1, name=12)], 
 2=[Record(id=2, name=22)], 
 3=[Record(id=3, name=32)], 
 4=[Record(id=4, name=42)], 
 5=[Record(id=5, name=51), Record(id=5, name=52)]}