【问题标题】:Grouping by object value, counting and then setting group key by maximum object attribute按对象值分组,计数,然后按最大对象属性设置组键
【发布时间】:2015-05-13 09:18:55
【问题描述】:

我已经设法使用 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 值的键。我已经在查看this solution,但我不知道如何将按值计数和分组以及路由最大 lastUpdated 值结合起来。这是我想要实现的示例数据:

示例:

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 个路由)是 lastUpdated 值最大的一个

【问题讨论】:

  • 在示例中,您使用 new Route 和 3 个参数,而唯一的构造函数有 4 个参数。你能纠正一下吗?
  • 我的坏话。现在已修复。基本上 dropOffSize 的大小在这里并不重要,但我确实将它留在了代码中,因为我想显示所有被覆盖的方法,而 compareTo 方法确实使用了 dropOffSize。

标签: java java-8 grouping java-stream


【解决方案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
        ));

【讨论】:

  • 我认为这个解决方案的性能很高,因为它会创建中间列表。有可能解决这个问题吗?此外,我从编译器收到以下两个投诉:“无法解析方法 'stream()'”和“无法解析方法 'size()'”
  • 您的路线列表有多大?您当然可以以更高效的方式执行此操作,但要以简单为代价。至于编译器错误,你用的是eclipse吗?我刚刚用 jdk1.8.0_25 试了一下,它编译得很好。
  • 我正在使用 IntelliJ IDEA 14.1。路线列表正在根据时间调整大小。它实际上不是一个列表,而是一个 ArrayDeque,因为我正在模拟移动窗口。我估计它的大小应该在 1e4 到 2e4 左右。
  • 也许你没有静态导入?尝试 Collectors.groupingBy Comparator.comparingCollectors.toMap 而不仅仅是方法名称。
  • lst 确实是List 类型。我刚刚启动了 IntelliJ,它并没有抱怨。
【解决方案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

【讨论】:

  • 好吧,用Pair 或元组类型替换Object[] 可以解决它。现在轮到 JRE 开发人员了……
  • @TagirValeev 加一个用于努力和使用六个泛型类型变量。 :-)
  • @Holger 在 Java 9 之后出现实际值类型之前,我们可能不会做这样的事情。
  • 顺便发现jOOL库中出现了类似的收集器。他们可以使用 17 个通用参数合并多达 8 个收集器!
  • 这不是愚人节玩笑吗?哦等等,原来是这样:blog.jooq.org/2015/04/01/…
【解决方案3】:

将 equals 和 hashcode 更改为仅依赖于起始单元格和结束单元格。

@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 应该替换为更合适的东西。

【讨论】:

  • 请注意,groupingBy 的 javadoc 并不特别保证这会起作用。
  • 它实际上不适用于并行流。但这个想法看起来很有趣。
【解决方案4】:

原则上,这似乎应该一次性完成。通常的问题是,这需要一个临时元组或对,在这种情况下需要一个 Route 和一个计数。由于 Java 缺少这些,我们最终使用长度为 2 的 Object 数组(如 Tagir Valeev's answer 所示),或 AbstractMap.SimpleImmutableEntry,或假设的 Pair&lt;A,B&gt; 类。

另一种方法是编写一个包含Route 和计数的小值类。当然这样做会有一些痛苦,但在这种情况下,我认为它是有回报的,因为它提供了一个放置组合逻辑的地方。这反过来又简化了流操作。

这是包含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 方法。它通过选择最近更新的Route 并使用计数的总和来组合两个RouteCount 值。现在我们有了这个值类,我们可以写一个单程流来得到我们想要的结果:

    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 的 and-then 部分从 reducing 收集器生成的 Optional&lt;RouteCount&gt; 中提取值。

(通常一个裸露的get 是危险的,但除非有至少一个可用值,否则我们根本不会访问这个收集器。所以get 在这种情况下是安全的。)

【讨论】:

  • 很好的答案和很好的解释。我真的很喜欢你的解决方案。如果我理解正确,这意味着您的解决方案将在 O(n) 时间内运行,而 @Misha 解决方案是 2 * O(n),对吧?
  • @JernejJerin 严格来说,从计算机科学的角度来看,2*O(n) 与 O(n) 相同。但我认为您指的是我的方法是对数据进行一次传递,而 Misha 的方法是两次传递。确实如此,但并不一定意味着我的方法是 Misha 的 2 倍;两遍方法的速度可能与单遍方法的速度相同,单遍方法的工作量是每个元素的两倍。我不知道我的方法实际上是否将每个元素的工作量提高了 2 倍,但它似乎确实比 Misha 的每个传球所做的每个元素都多。总体工作量似乎相似。
  • @JernejJerin 我怀疑真正的成本是内存分配、内存消耗和 GC 压力。这是否重要取决于一大堆事情。如果流源未存储在内存中,例如,它来自数据库或其他地方,则一次性方法可能在节省内存方面具有很大优势。但前提是有很多重复的路线。如果没有重复,它最终会存储所有内容。无论如何,我认为您可以看到细微差别。要找出哪个更好,您必须进行基准测试。
  • @StuartMarks 你也可以使用稍短的toMap(r-&gt;r, RouteCount::fromRoute, RouteCount::combine) 代替groupingBy+reducing
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-22
  • 2021-11-19
  • 2019-04-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多