Поток Java собирает счет в поле - PullRequest
5 голосов
/ 30 апреля 2019

Можно ли использовать 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, чтобы избежать повторения дважды по списку (или карте) и повысить производительность?

Ответы [ 5 ]

2 голосов
/ 30 апреля 2019

Не очень чисто, но вы можете сделать это как:

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 голосов
/ 30 апреля 2019

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

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

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

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

1 голос
/ 30 апреля 2019

Вы можете минимизировать распределение промежуточных объектов, например, всех 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();
0 голосов
/ 30 апреля 2019

Я не знаю, каковы ваши требования, но я изменил ваш класс 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 голосов
/ 30 апреля 2019
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
...