Можно ли использовать Collectors.groupingBy() с Collectors.counting() для подсчета поля пользовательского объекта вместо создания карты и последующего ее преобразования.

У меня есть список пользователей, например:

public class User {
    private String firstName;
    private String lastName;
    // some more attributes

    // getters and setters
}

Я хочу посчитать всех пользователей с одинаковыми именем и фамилией. Поэтому у меня есть пользовательский объект, который выглядит так:

public static class NameGroup {
    private String firstName;
    private String lastName;
    private long count;

    // getters and setters
}

Я могу собрать группы имен, используя это:

List<NameGroup> names = users.stream()
        .collect(Collectors.groupingBy(p -> Arrays.asList(p.getFirstName(), p.getLastName()), Collectors.counting()))
        .entrySet().stream()
        .map(e -> new NameGroup(e.getKey().get(0), e.getKey().get(1), e.getValue()))
        .collect(Collectors.toList());

С помощью этого решения я должен сначала сгруппировать пользователей на карту, а затем преобразовать ее в свой пользовательский объект. Можно ли считать все имена непосредственно в nameGroup.count, чтобы избежать повторения дважды по списку (или карте) и повысить производительность?

6
Samuel Philipp 30 Апр 2019 в 21:52

5 ответов

Лучший ответ

Вы можете минимизировать размещение промежуточных объектов, например, все объекты Arrays.asList(...), создавайте карту самостоятельно, вместо использования потоковой передачи.

Это зависит от того, что ваш NameGroup изменчив.

Чтобы даже сделать код проще, давайте добавим двух помощников в NameGroup:

public static class NameGroup {
    // fields here

    public NameGroup(User user) {
        this.firstName = user.getFirstName();
        this.lastName = user.getLastName();
    }

    public void incrementCount() {
        this.count++;
    }

    // other constructors, getters and setters here
}

Имея это в виду, вы можете реализовать логику следующим образом:

Map<User, NameGroup> map = new TreeMap<>(Comparator.comparing(User::getFirstName)
                                                   .thenComparing(User::getLastName));
users.stream().forEach(user -> map.computeIfAbsent(user, NameGroup::new).incrementCount());
List<NameGroup> names = new ArrayList<>(map.values());

Или, если вам не нужен список, последнюю строку можно упростить до:

Collection<NameGroup> names = map.values();
2
Andreas 30 Апр 2019 в 19:50

Вы можете собирать напрямую в NameGroup.count, но это будет менее эффективно, чем у вас, не больше.

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

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

Честно говоря: то, что у вас есть сейчас, совершенно хорошо, и не является неэффективным во всех важных аспектах. Вы почти наверняка должны придерживаться того, что у вас есть.

2
Louis Wasserman 30 Апр 2019 в 19:21

Не очень чисто, но вы можете сделать это следующим образом:

List<NameGroup> convertUsersToNameGroups(List<User> users) {
    return new ArrayList<>(users.stream()
            .collect(Collectors.toMap(p -> Arrays.asList(p.getFirstName(), p.getLastName()),
                    p -> new NameGroup(p.getFirstName(), p.getLastName(), 1L),
                    (nameGroup1, nameGroup2) -> new NameGroup(nameGroup1.getFirstName(), nameGroup1.getLastName(),
                            nameGroup1.getCount() + nameGroup2.getCount()))).values());
}
2
Naman 30 Апр 2019 в 19:25
public static class NameGroup {
    // ...
    @Override
    public boolean equals(Object other) {
        final NameGroup o = (NameGroup) other;
        return firstName.equals(o.firstName) && lastName.equals(o.lastName);
    }
    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName);
    }
    @Override
    public String toString() {
        return firstName + " " + lastName + " " + count;
    }
}

public static void main(String[] args) throws IOException {
    List<User> users = new ArrayList<>();
    users.add(new User("fooz", "bar"));
    users.add(new User("fooz", "bar"));
    users.add(new User("foo", "bar"));
    users.add(new User("foo", "bar"));
    users.add(new User("foo", "barz"));
    users.stream()
        .map(u -> new NameGroup(u.getFirstName(), u.getLastName(), 1L))
        .reduce(new HashMap<NameGroup, NameGroup>(), (HashMap<NameGroup, NameGroup> acc, NameGroup e) -> {
            acc.compute(e, (k, v) -> v == null ? e : new NameGroup(e.firstName, e.lastName, e.count + acc.get(e).count));
            return acc;
        }, (a, b) -> {
            b.keySet().forEach(e -> a.compute(e, (k, v) -> v == null ? e : new NameGroup(e.firstName, e.lastName, e.count + a.get(e).count)));
            return a;
        }).values().forEach(x -> System.out.println(x));
}

Это выведет

fooz bar 2
foo barz 1
foo bar 2
0
w4bo 30 Апр 2019 в 20:10

Я не знаю, каковы ваши требования, но я изменил ваш класс NameGroup, чтобы он принимал класс User вместо имени и фамилии. Это тогда исключало необходимость выбора значений из промежуточного потока List и только из потока User. Но это все еще требует карты.

      List<NameGroup> names =
            users.stream().collect(Collectors.groupingBy(p -> p,Collectors.counting()))
                          .entrySet().stream()
                          .map(e -> new NameGroup(e.getKey(), e.getValue())).collect(
                              Collectors.toList());
0
WJS 30 Апр 2019 в 20:17