【问题标题】:How to get multiple values from an object using a single stream operation?如何使用单个流操作从对象中获取多个值?
【发布时间】:2019-05-30 11:05:27
【问题描述】:

我想确定显示一组点所需的最小面积。最简单的方法是像这样遍历集合:

int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (Point point: points) {
    if (point.x < minX) {
        minX = point.x;
    }
    if (point.x > maxX) {
        maxX = point.x;
    }
    if (point.y < minY) {
        minY = point.y;
    }
    if (point.y > maxY) {
        maxY = point.y;
    }
}

我开始了解流。为此,您可以执行以下操作:

int minX = points.stream().mapToInt(point -> point.x).min().orElse(-1);
int maxX = points.stream().mapToInt(point -> point.x).max().orElse(-1);
int minY = points.stream().mapToInt(point -> point.y).min().orElse(-1);
int maxY = points.stream().mapToInt(point -> point.y).max().orElse(-1);

两者都给出相同的结果。然而,虽然流方法很优雅,但速度要慢得多(正如预期的那样)。

有没有办法在单个流操作中获得minXmaxXminYmaxY

【问题讨论】:

  • 使用描述所有位置的自定义类型创建Collector,并不比您的迭代方法 imo 好多少(例如提取到自己的方法中)。
  • @AkinerAlkan: maxX 实际上设置为Integer.MIN_VALUE。这意味着条件if (point.x &gt; maxX) 肯定会变为true(除非所有x 值都等于Integer.MIN_VALUE,这是极不可能的)。

标签: java java-8 java-stream


【解决方案1】:

JDK 12 及更高版本有Collectors.teeingwebrevCSR),它收集到两个不同的收集器,然后将两个部分结果合并为最终结果。

您可以在这里使用它来收集两个IntSummaryStatisticsx 坐标和y 坐标:

List<IntSummaryStatistics> stats = points.stream()
    .collect(Collectors.teeing(
             Collectors.mapping(p -> p.x, Collectors.summarizingInt()),
             Collectors.mapping(p -> p.y, Collectors.summarizingInt()),
             List::of));

int minX = stats.get(0).getMin();
int maxX = stats.get(0).getMax();
int minY = stats.get(1).getMin();
int maxY = stats.get(1).getMax();

这里第一个收集器收集x 的统计信息,第二个收集器收集y 的统计信息。然后,xy 的统计信息通过接受两个元素的 JDK 9 List.of 工厂方法合并到 List 中。

List::of 的替代方案是:

(xStats, yStats) -> Arrays.asList(xStats, yStats)

如果您的机器上碰巧没有安装 JDK 12,这里是 teeing 方法的简化通用版本,您可以安全地将其用作实用方法:

public static <T, A1, A2, R1, R2, R> Collector<T, ?, R> teeing(
        Collector<? super T, A1, R1> downstream1,
        Collector<? super T, A2, R2> downstream2,
        BiFunction<? super R1, ? super R2, R> merger) {

    class Acc {
        A1 acc1 = downstream1.supplier().get();
        A2 acc2 = downstream2.supplier().get();

        void accumulate(T t) {
            downstream1.accumulator().accept(acc1, t);
            downstream2.accumulator().accept(acc2, t);
        }

        Acc combine(Acc other) {
            acc1 = downstream1.combiner().apply(acc1, other.acc1);
            acc2 = downstream2.combiner().apply(acc2, other.acc2);
            return this;
        }

        R applyMerger() {
            R1 r1 = downstream1.finisher().apply(acc1);
            R2 r2 = downstream2.finisher().apply(acc2);
            return merger.apply(r1, r2);
        }
    }

    return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::applyMerger);
}

请注意,我在创建返回的收集器时没有考虑下游收集器的特性。

【讨论】:

  • 感谢您的回答。当我们无法升级时,这些反向移植实现非常好用。 :)
  • @Naman 仍在 Java 8 上,我们很快就会“现代”... 到 Java 11(而 jdk 17 将于 9 月发布)???
【解决方案2】:

类比IntSummaryStatistics,创建一个类PointStatistics,它收集你需要的信息。它定义了两种方法:一种用于记录来自Point 的值,一种用于组合两个Statistics

class PointStatistics {
    private int minX = Integer.MAX_VALUE;
    private int maxX = Integer.MIN_VALUE;

    private int minY = Integer.MAX_VALUE;
    private int maxY = Integer.MIN_VALUE;

    public void accept(Point p) {
        minX = Math.min(minX, p.x);
        maxX = Math.max(maxX, p.x);

        minY = Math.min(minY, p.y);
        maxY = Math.max(minY, p.y);
    }

    public void combine(PointStatistics o) {
        minX = Math.min(minX, o.minX);
        maxX = Math.max(maxX, o.maxX);

        minY = Math.min(minY, o.minY);
        maxY = Math.max(maxY, o.maxY);
    }

    // getters
}

然后您可以将Stream&lt;Point&gt; 收集到PointStatistics

class Program {
    public static void main(String[] args) {
        List<Point> points = new ArrayList<>();

        // populate 'points'

        PointStatistics statistics = points
                    .stream()
                    .collect(PointStatistics::new, PointStatistics::accept, PointStatistics::combine);
    }
}

更新

我完全被 OP 绘制的the conclusion 弄糊涂了,所以我决定写JMH benchmarks。

基准设置:

# JMH version: 1.21
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# Warmup: 1 iterations, 10 s each
# Measurement: 10 iterations, 10 s each
# Timeout: 10 min per iteration
# Benchmark mode: Average time, time/op

对于每次迭代,我都会生成一个大小为 100K、1M、10M 的随机 Points (new Point(random.nextInt(), random.nextInt())) 共享列表。

结果是

100K

Benchmark                        Mode  Cnt  Score   Error  Units

customCollector                  avgt   10  6.760 ± 0.789  ms/op
forEach                          avgt   10  0.255 ± 0.033  ms/op
fourStreams                      avgt   10  5.115 ± 1.149  ms/op
statistics                       avgt   10  0.887 ± 0.114  ms/op
twoStreams                       avgt   10  2.869 ± 0.567  ms/op

1M

Benchmark                        Mode  Cnt   Score   Error  Units

customCollector                  avgt   10  68.117 ± 4.822  ms/op
forEach                          avgt   10   3.939 ± 0.559  ms/op
fourStreams                      avgt   10  57.800 ± 4.817  ms/op
statistics                       avgt   10   9.904 ± 1.048  ms/op
twoStreams                       avgt   10  32.303 ± 2.498  ms/op

10M

Benchmark                        Mode  Cnt    Score     Error  Units

customCollector                  avgt   10  714.016 ± 151.558  ms/op
forEach                          avgt   10   54.334 ±   9.820  ms/op
fourStreams                      avgt   10  699.599 ± 138.332  ms/op
statistics                       avgt   10  148.649 ±  26.248  ms/op
twoStreams                       avgt   10  429.050 ±  72.879  ms/op

【讨论】:

  • 太棒了!感谢您的基准测试。知道为什么我的自定义收集器这么慢吗?有趣的是,forEach 仍然表现最好。当您切换到 parallelStrream 时,这种情况会改变吗?
  • @MWB 它使用HashMap
  • @MWB 是的,.parallel() 会积极地改变它,但不会彻底
【解决方案3】:

感谢大家的所有建议和回答。这很有帮助,我学到了很多东西!

我决定试一试您的大多数解决方案(JDK12 解决方案除外)。对于其中一些,您已经向我提供了代码。另外,我自己做了Collector

class extremesCollector implements Collector<Point, Map<String, Integer>, Map<String , Integer>> {

    @Override
    public Supplier<Map<String, Integer>> supplier() {
        Map<String, Integer> map = new HashMap<>();
        map.put("xMin", Integer.MAX_VALUE);
        map.put("yMin", Integer.MAX_VALUE);
        map.put("xMax", Integer.MIN_VALUE);
        map.put("yMax", Integer.MIN_VALUE);
        return () -> map;
    }

    @Override
    public BiConsumer<Map<String, Integer>, Point> accumulator() {
        return (a, b) -> {
            a.put("xMin", Math.min(a.get("xMin"), b.x));
            a.put("yMin", Math.min(a.get("yMin"), b.y));
            a.put("xMax", Math.max(a.get("xMax"), b.x));
            a.put("yMax", Math.max(a.get("yMax"), b.y));
        };
    }

    @Override
    public Function<Map<String, Integer>, Map<String, Integer>> finisher() {
        return Function.identity();
    }

    @Override
    public BinaryOperator<Map<String, Integer>> combiner() {
        return (a, b) -> {
            a.put("xMin", Math.min(a.get("xMin"), b.get("xMin")));
            a.put("yMin", Math.min(a.get("yMin"), b.get("yMin")));
            a.put("xMax", Math.max(a.get("xMax"), b.get("xMax")));
            a.put("yMax", Math.max(a.get("yMax"), b.get("yMax")));
            return a;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        Set<Characteristics> characteristics = new HashSet<>();
        characteristics.add(Characteristics.UNORDERED);
        characteristics.add(Characteristics.CONCURRENT);
        characteristics.add(Characteristics.IDENTITY_FINISH);
        return characteristics;
    }
}

结果

我尝试了所有这些并比较了结果。好消息:对于他们所有人,就价值观而言,我得到了相同的结果!

关于速度,排名如下:

  1. for 循环
  2. 四个单独的流
  3. 使用自制收集器进行流式传输
  4. 自制Collector的并行流
  5. Andrew Tobilko 提供的统计方法

2 号和 3 号在速度方面实际上非常接近。并行版本可能比较慢,因为我的数据集太小了。

【讨论】:

  • 对不起,我做了快速而肮脏的方式。我测量了运行代码片段 50 次前后的时间(System.nanotime)并取平均值。对每种方法都这样做。
  • 如果您关心速度,我宁愿使用 4 个元素的数组或自定义对象而不是 HashMap。
【解决方案4】:

您可以使用summaryStatistics() 将迭代除以两次,同时保持代码不变:

IntSummaryStatistics stat = points.stream().mapToInt(point -> point.x).summaryStatistics();
int minX = stat.getMin();
int maxX = stat.getMax();

point.y做同样的事情。
您可以通过这种方式排除:

Function<ToIntFunction<Point>, IntSummaryStatistics> statFunction =
        intExtractor -> points.stream()
                              .mapToInt(p -> intExtractor.applyAsInt(pp))
                              .summaryStatistics();

IntSummaryStatistics statX = statFunction.apply(p -> p.x);
IntSummaryStatistics statY = statFunction.apply(p -> p.y);

自定义收集器是可能的,但请注意,您应该实现组合器部分,这会使您的代码更难阅读。
因此,但如果您需要使用并行流,则应保持命令式方式。
虽然您可以依靠 Math.minMath.max 函数来改进您的实际代码:

for (Point p : points) {
    minX = Math.min(p.x, minX);
    minY = Math.min(p.y, minY);
    maxY = Math.max(p.x, maxX);
    maxY = Math.max(p.y, maxY);
}

【讨论】:

  • 或者,您可以简单地提取一个方法,而不是创建 Function 对象:public static IntSummaryStatistics getSummaryStatistics(Collection&lt;Point&gt; points, ToIntFunction&lt;Point&gt; getter) { return points.stream().mapToInt(getter).summaryStatistics(); }
  • @Ricola 当然可以。有时这种可能性非常好。但在其他一些情况下,您的类的许多方法都依赖于提取函数。为每个方法定义一个方法(甚至是私有的)可以通过减少方法的数量使代码更难阅读。
  • 我明白,但我想说,如果在你的类中提取方法会使你的类太大或难以理解,那么问题可能是你的类做得太多,应该分成更小的组件.
  • @Ricola 这可能确实是原因。但有时这不是问题。假设您“只有”5 个需要特定参数化函数的公共方法。通过添加 5 个替换局部变量函数的私有方法,您可以扩大它们的范围。因此,它会在代码中创建读取间接性,但请注意,它也可能会产生副作用,例如调用“错误”方法。这就是为什么如果函数作为局部变量已经是直接可读的,我想知道将其提取到方法中的相关性。之后当然是主观的。
【解决方案5】:

您可以使用 Stream::reduce 使用 2 个 Streams 来获得一个具有最小值的点和一个具有最大值的点。我不建议将结果连接到一个流,因为可能很难区分最小值、最大值和坐标之间的差异。

Point min = points
    .stream()
    .reduce((l, r) -> new Point(Math.min(l.y, r.y), Math.min(l.y, r.y))
    .orElse(new Point(-1, -1));

Point max = points
    .stream()
    .reduce((l, r) -> new Point(Math.max(l.y, r.y), Math.max(l.y, r.y))
    .orElse(new Point(-1, -1));

作为BinaryOperator&lt;Point&gt;,使用随后的两个Points 和一个三元运算符来找出传递给新对象Point 并使用Optional::orElse 以默认-1, -1 坐标返回的最小值/最大值。

【讨论】:

    猜你喜欢
    • 2021-02-14
    • 1970-01-01
    • 1970-01-01
    • 2014-06-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多