Преобразуйте Java Stream и возвращайте уменьшенные значения как - PullRequest
2 голосов
/ 08 июля 2019

Предполагая, что я потребляю Поток сущностей из источника, который я не хочу материализовать, и я хочу и преобразовать элементы, и вернуть некоторое глобально уменьшенное значение, каков идиоматический способ с java (8)?

По сути, это попытка выполнить и reduce(), и collect().

Пример:

class Person {
    public String firstname,
    public String lastname,
    public int age;
}

class TeamSummary {
    public List<String> fullnames, // firstname and lastname of all
    public Person oldest
}

public TeamSummary getSummary(Stream<Person> personStream) {
   final TeamSummary summary = new Summary();
   summary.fullnames = personStream
       .peek(p -> if (summary.getOldest() == null || summary.getOldest.age < p.age) {
           summary.oldest = p;
       })
       .map(p -> p.firstname + ' ' + p.lastname)
       .collect(toList());
   return summary;
}

Выглядит уродливым взаимодействие с переменной вне потока внутриметод peek, но какие есть хорошие альтернативы, кажется, мне нужно объединить collect() и reduce().

Хуже, если я хочу получить уменьшенное значение из всего потока (например, средний возраст)) и отфильтрованный список (например, лица старше 18 лет).Также становится хуже, если TeamSummary является неизменяемым классом и требуются дополнительные изменяемые переменные.

В таких случаях более идиоматично использовать цикл while в stream.iterator (), чтобы избежать объединения потоковых методов и переменных.?Или это естественно использовать сокращение к кортежу типа (самый старый, накопленный).

Я знаю, что этот вопрос является вопросом мнения, если не существует очевидного способа (например, специального сборщика), который решает это элегантно.

Ответы [ 3 ]

4 голосов
/ 08 июля 2019

То есть вы хотите уменьшить свою коллекцию до одного значения?Вот где Collectors.reducing вступает в игру (Альтернатива: вы можете использовать Stream.reduce, но с другими модификациями).Кроме того, вы хотите каким-то образом агрегировать ваши значения, а также иметь идеальный аккумулятор: TeamSummary.

Теперь в приведенном ниже коде я внес следующие изменения:

  • КомандаСводка имеет функцию слияния / идентификации, необходимую для сокращения, поскольку она служит аккумулятором
  • . Я использую Нулевой объект вместо null для несуществующего лица, что делает коднамного более читабельный без нулевых проверок (NPE во время конвертера является одной из проблем).Задумывались ли вы о выводе, если поток пустой?
  • Я добавил конструктор Person для собственного удобства.Но подумайте об использовании получателей и окончательных полей (даже если вы считаете, что получатели и вся ложная инкапсуляция являются шаблонными: вы можете использовать ссылки на методы, например, для перехода к компаратору, но не ссылки на поля)

Здеськод:

static class Person {
    public String firstname;
    public String lastname;
    public int age;

    public Person(String firstname, String lastname, int age) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.age = age;
    }

    public static Person getNullObjectYoung() {
        return new Person("", "", 0);
    }
}

static class TeamSummary {
    public List<String> fullnames;
    public Person oldest;

    public static TeamSummary merge(TeamSummary lhs, TeamSummary rhs) {
        TeamSummary result = new TeamSummary();
        result.fullnames = new ArrayList<>();
        result.fullnames.addAll(lhs.fullnames);
        result.fullnames.addAll(rhs.fullnames);
        result.oldest = Comparator.<Person, Integer>comparing(p -> p.age).reversed()
                .compare(lhs.oldest, rhs.oldest) < 0
                ? lhs.oldest
                : rhs.oldest;
        return result;
    }

    public static TeamSummary of(Person person) {
        TeamSummary result = new TeamSummary();
        result.fullnames = new ArrayList<>();
        result.fullnames.add(person.firstname + " " + person.lastname);
        result.oldest = person;
        return result;
    }

    public static TeamSummary identity() {
        TeamSummary result = new TeamSummary();
        result.fullnames = new ArrayList<>();
        result.oldest = Person.getNullObjectYoung();
        return result;
    }
}

public static void main(String[] args) {        
    Stream<Person> personStream = Arrays.asList(
            new Person("Tom", "T", 32),
            new Person("Bob", "B", 40))
            .stream();

    TeamSummary result = personStream.collect(
            Collectors.reducing(
                    TeamSummary.identity(),
                    TeamSummary::of,
                    TeamSummary::merge
            ));
    System.out.println(result.fullnames + " " + result.oldest.age);

}

Примечание. Вы запросили версию Java 8.Возможно, в java 12 вы могли бы также использовать Collectors.teeing, так как вы в основном хотите сделать два разных сокращения одновременно (для которых мы в настоящее время можем использовать аккумулятор).


Редактировать: также добавленорешение для Stream.reduce, для которого требуется BiFunction (резюме, человек) -> человек:

static class TeamSummary {

    ...

    public TeamSummary include(final Person person) {
        final TeamSummary result = new TeamSummary();
        result.fullnames = new ArrayList<>(fullnames);
        result.fullnames.add(person.firstname + " " + person.lastname);
        result.oldest = Comparator.<Person, Integer> comparing(p -> p.age).reversed()
                .compare(oldest, person) < 0
                        ? oldest
                        : person;
        return result;
    }
}

public static void main(final String[] args) {
    ...

    final TeamSummary reduced = personStream.reduce(
            TeamSummary.identity(),
            TeamSummary::include,
            TeamSummary::merge);
}
1 голос
/ 08 июля 2019

На основании таких требований, как - Stream в качестве ввода и вывод полного списка имен в выводе teamSummary. Вы можете выполнить операцию map, пропинговать человека и его имя в записи, а затем reduce их далее, например:

return personStream
        .map(p -> new AbstractMap.SimpleEntry<>(p, Collections.singletonList(p.getFirstname() + ' ' + p.getLastname())))
        .reduce((entry1, entry2) -> new AbstractMap.SimpleEntry<>(entry1.getKey().getAge() >= entry2.getKey().getAge() ?
                entry1.getKey() : entry2.getKey(), Stream.of(entry1.getValue(), entry2.getValue()).flatMap(List::stream).collect(Collectors.toList())))
        .map(entry -> new TeamSummary(entry.getKey(), entry.getValue()))
        .orElseThrow(IllegalArgumentException::new);

Для удобочитаемого и упрощенного подхода, хотя я бы предпочел передать коллекцию и работать с несколькими потоковыми операциями, чтобы создать TeamSummary как:

public TeamSummary getSummary(List<Person> people) {
    List<String> fullNames = people.stream()
            .map(p -> p.getFirstname() + ' ' + p.getLastname())
            .collect(Collectors.toList());
    Person oldestPerson = people.stream()
            .reduce(BinaryOperator.maxBy(Comparator.comparing(Person::getAge)))
            .orElseThrow(IllegalArgumentException::new);
    return new TeamSummary(oldestPerson, fullNames);
}
0 голосов
/ 08 июля 2019

Я не знаю, почему вы бы использовали Collectors.reducing(), когда вы можете stream.reduce() напрямую?

BinaryOperator<Player> older = (p1, p2) -> 
    Comparator.comparing(Player::getAge) > 0 
        ? p1 : p2;
TeamSummary summary = stream.reduce(
    TeamSummary::new, // identity
    // accumulator
    (ts, player) -> {
        ts.addFullnames(String.format("%s %s", player.firstName, player.lastName));
        ts.setOldest(older.apply(ts.getOldest(), player));
    }
    // combiner
    (ts1, ts2) -> {
        // we can safely modify the given summaries, they were all created while reducing
        ts1.setOldest(Math.max(ts1.getOldest(), ts2.getOldest()));
        ts1.addFullnames(ts2.getFullnames().toArray());
        return ts1;
    });

TeamSummary будет выглядеть так:

class TeamSummary {
    private int oldest; 
    public Player getOldest() { return oldest; }
    public void setOldest(Player newOldest) { oldest = newOldest; }

    private List<String> fullnames();
    public List<String> getFullnames() { return Collections.unmodifiableList(fullnames); }

    public void addFullnames(String... names) {
        fullnames.addAll(Arrays.asList(names));
    }
}

Alternative

Вы также можете расширить TeamSummary чем-то вроде addPlayer(Player p) и merge(), чтобы позволить ему поддерживать свою последовательность:

class TeamSummary {

    @Getter
    private int oldest;
    @Getter
    private List<String> fullnames = new ArrayList<>();

    public void addPlayer(Player p) {
        fullnames.add(String.format("%s %s", p.getFirstname(), p.getLastname()));
        oldest = olderPlayer(oldest, p);
    }
    public TeamSummary merge(TeamSummary other) {
        older = olderPlayer(oldest, other.oldest)
        fullnames.addAll(other.fullnames);
        return this;
    }

    final static Comparator<Player> BY_AGE = Comparator.comparing(Player::getAge);
    private static Player olderPlayer(Player p1, Player p2) {
        return BY_AGE.compare(p1, p2) > 0 ? p1 : p2;
    }
}

что сделало бы сокращение

stream.reduce(
    TeamSummary::new,
    TeamSummary::addPlayer,
    TeamSummary::merge
);
...