Подтвердить что ты не робот

Группировка по значению объекта, подсчет, а затем установка группового ключа с помощью максимального атрибута объекта

Мне удалось написать решение с использованием Java 8 Streams API, который сначала группирует список объектов Route по его значению, а затем подсчитывает количество объектов в каждой группе. Он возвращает отображение Route → Long. Вот код:

Map<Route, Long> routesCounted = routes.stream()
                .collect(Collectors.groupingBy(gr -> gr, Collectors.counting()));

И класс Route:

public class Route implements Comparable<Route> {
    private long lastUpdated;
    private Cell startCell;
    private Cell endCell;
    private int dropOffSize;

    public Route(Cell startCell, Cell endCell, long lastUpdated) {
        this.startCell = startCell;
        this.endCell = endCell;
        this.lastUpdated = lastUpdated;
    }

    public long getLastUpdated() {
        return this.lastUpdated;
    }

    public void setLastUpdated(long lastUpdated) {
        this.lastUpdated = lastUpdated;
    }

    public Cell getStartCell() {
        return startCell;
    }

    public void setStartCell(Cell startCell) {
        this.startCell = startCell;
    }

    public Cell getEndCell() {
        return endCell;
    }

    public void setEndCell(Cell endCell) {
        this.endCell = endCell;
    }

    public int getDropOffSize() {
        return this.dropOffSize;
    }

    public void setDropOffSize(int dropOffSize) {
        this.dropOffSize = dropOffSize;
    }

    @Override
    /**
     * Compute hash code by using Apache Commons Lang HashCodeBuilder.
     */
    public int hashCode() {
        return new HashCodeBuilder(43, 59)
                .append(this.startCell)
                .append(this.endCell)
                .toHashCode();
    }

    @Override
    /**
     * Compute equals by using Apache Commons Lang EqualsBuilder.
     */
    public boolean equals(Object obj) {
        if (!(obj instanceof Route))
            return false;
        if (obj == this)
            return true;

        Route route = (Route) obj;
        return new EqualsBuilder()
                .append(this.startCell, route.startCell)
                .append(this.endCell, route.endCell)
                .isEquals();
    }

    @Override
    public int compareTo(Route route) {
        if (this.dropOffSize < route.dropOffSize)
            return -1;
        else if (this.dropOffSize > route.dropOffSize)
            return 1;
        else {
                // if contains drop off timestamps, order by last timestamp in drop off
                // the highest timestamp has preceding
            if (this.lastUpdated < route.lastUpdated)
                return -1;
            else if (this.lastUpdated > route.lastUpdated)
                return 1;
            else
                return 0;
        }
    }
}

То, что я хотел бы дополнительно достичь, - это то, что ключ для каждой группы будет иметь наибольшее значение lastUpdated. Я уже смотрел это решение, но я не знаю, как объединить подсчет и группировку по значению и максимальное значение MaximumUpdated Route. Вот пример данных, которые я хочу достичь:

Пример:

List<Route> routes = new ArrayList<>();
routes.add(new Route(new Cell(1, 2), new Cell(2, 1), 1200L));
routes.add(new Route(new Cell(3, 2), new Cell(2, 5), 1800L));
routes.add(new Route(new Cell(1, 2), new Cell(2, 1), 1700L));

ДОЛЖНО ПРЕВРАТИТЬСЯ:

Map<Route, Long> routesCounted = new HashMap<>();
routesCounted.put(new Route(new Cell(1, 2), new Cell(2, 1), 1700L), 2);
routesCounted.put(new Route(new Cell(3, 2), new Cell(2, 5), 1800L), 1);

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

4b9b3361

Ответ 1

Здесь один подход. Первую группу в списки, а затем обработать списки в нужные вам значения:

import static java.util.Comparator.comparingLong;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;


Map<Route,Integer> routeCounts = routes.stream()
        .collect(groupingBy(x -> x))
        .values().stream()
        .collect(toMap(
            lst -> lst.stream().max(comparingLong(Route::getLastUpdated)).get(),
            List::size
        ));

Ответ 2

Вы можете определить абстрактный метод "библиотека", который объединяет два сборщика в один:

static <T, A1, A2, R1, R2, R> Collector<T, ?, R> pairing(Collector<T, A1, R1> c1, 
        Collector<T, A2, R2> c2, BiFunction<R1, R2, R> finisher) {
    EnumSet<Characteristics> c = EnumSet.noneOf(Characteristics.class);
    c.addAll(c1.characteristics());
    c.retainAll(c2.characteristics());
    c.remove(Characteristics.IDENTITY_FINISH);
    return Collector.of(() -> new Object[] {c1.supplier().get(), c2.supplier().get()},
            (acc, v) -> {
                c1.accumulator().accept((A1)acc[0], v);
                c2.accumulator().accept((A2)acc[1], v);
            },
            (acc1, acc2) -> {
                acc1[0] = c1.combiner().apply((A1)acc1[0], (A1)acc2[0]);
                acc1[1] = c2.combiner().apply((A2)acc1[1], (A2)acc2[1]);
                return acc1;
            },
            acc -> {
                R1 r1 = c1.finisher().apply((A1)acc[0]);
                R2 r2 = c2.finisher().apply((A2)acc[1]);
                return finisher.apply(r1, r2);
            }, c.toArray(new Characteristics[c.size()]));
}

После этого фактическая операция может выглядеть так:

Map<Route, Long> result = routes.stream()
        .collect(Collectors.groupingBy(Function.identity(),
            pairing(Collectors.maxBy(Comparator.comparingLong(Route::getLastUpdated)), 
                    Collectors.counting(), 
                    (route, count) -> new AbstractMap.SimpleEntry<>(route.get(), count))
            ))
        .values().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));

Обновление: такой коллекционер доступен в моей библиотеке StreamEx: MoreCollectors.pairing(). Аналогичный сборщик реализован в библиотеке jOOL, поэтому вы можете использовать Tuple.collectors вместо pairing.

Ответ 3

Изменены равенства и хэш-код, зависящие только от начальной ячейки и конечной ячейки.

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Cell cell = (Cell) o;

        if (a != cell.a) return false;
        if (b != cell.b) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = a;
        result = 31 * result + b;
        return result;
    }

Мое решение выглядит так:

Map<Route, Long> routesCounted = routes.stream()
            .sorted((r1,r2)-> (int)(r2.lastUpdated - r1.lastUpdated))
            .collect(Collectors.groupingBy(gr -> gr, Collectors.counting()));

Конечно, приведение к int должно быть заменено чем-то более соответствующим.

Ответ 4

В принципе кажется, что это должно быть выполнимо за один проход. Обычная морщина заключается в том, что для этого требуется ad-hoc кортеж или пара, в этом случае с Route и счетчиком. Поскольку Java не хватает этих данных, мы в конечном итоге используем массив Object длиной 2 (как показано в ответе Тагира Валеева) или AbstractMap.SimpleImmutableEntry или гипотетический класс Pair<A,B>.

Альтернативой является создание небольшого класса значений, содержащего Route и count. Конечно, в этом есть какая-то боль, но в этом случае я думаю, что это окупается, потому что это дает место для объединения логики комбинирования. Это, в свою очередь, упрощает работу с потоком.

Здесь класс значений, содержащий a Route и число:

class RouteCount {
    final Route route;
    final long count;

    private RouteCount(Route r, long c) {
        this.route = r;
        count = c;
    }

    public static RouteCount fromRoute(Route r) {
        return new RouteCount(r, 1L);
    }

    public static RouteCount combine(RouteCount rc1, RouteCount rc2) {
        Route recent;
        if (rc1.route.getLastUpdated() > rc2.route.getLastUpdated()) {
            recent = rc1.route;
        } else {
            recent = rc2.route;
        }
        return new RouteCount(recent, rc1.count + rc2.count);
    }
}

Довольно просто, но обратите внимание на метод combine. Он объединяет два значения RouteCount, выбирая Route, который был обновлен совсем недавно, и используя сумму счетчиков. Теперь, когда у нас есть этот класс значений, мы можем написать однопроходный поток, чтобы получить желаемый результат:

    Map<Route, RouteCount> counted = routes.stream()
        .collect(groupingBy(route -> route,
                    collectingAndThen(
                        mapping(RouteCount::fromRoute, reducing(RouteCount::combine)),
                        Optional::get)));

Как и другие ответы, он группирует маршруты в классы эквивалентности на основе начальной и конечной ячейки. Фактический экземпляр Route, используемый в качестве ключа, не является значительным; это просто представитель своего класса. Значение будет единственным RouteCount, которое содержит экземпляр Route, который был обновлен совсем недавно, вместе с количеством эквивалентных экземпляров Route.

Как это работает, каждый экземпляр Route, который имеет одинаковые стартовые и конечные ячейки, затем подается в сборщик ниже по потоку groupingBy. Этот коллектор mapping отображает экземпляр Route в экземпляр RouteCount, а затем передает его коллектору reducing, который уменьшает экземпляры, используя описанную выше логику объединения. Затем и-часть collectingAndThen извлекает значение из Optional<RouteCount>, которое производит сборщик reducing.

(Обычно голый get является опасным, но мы не попадаем в этот коллекционер, если не имеется хотя бы одно значение. Таким образом, get в этом случае безопасен.)