【问题标题】:Java: Find multiple min/max attribute values in a stream using lambdaJava:使用 lambda 在流中查找多个最小/最大属性值
【发布时间】:2017-02-03 10:45:11
【问题描述】:

我正在寻找一种简洁的方法来查找一组属性值,这些属性值在给定的对象流中是最小的或最大的。

例如:

class Dimensions {
    final int startX, startY, endX, endY; //Set by constructor
}

/**
 * For the given dimensions, looks where the dimensions intersect. 
 * These coordinates define the sub-array, which is applied to the given function. 
 * 
 * @return the value returned by applying the sub-array in the given dimensions to the given function
 */
<S, T> T performOnIntersections(Function<S, T> function, S[][] inputArray, Dimensions...dimensions){

    int maxStartX = Arrays.stream(dimensions).max(Comparator.comparingInt(d -> d.startX)).get().startX;
    int maxStartY = Arrays.stream(dimensions).max(Comparator.comparingInt(d -> d.startY)).get().startY;
    int minEndX = Arrays.stream(dimensions).min(Comparator.comparingInt(d -> d.endX)).get().endX;
    int minEndY = Arrays.stream(dimensions).min(Comparator.comparingInt(d -> d.endY)).get().endY;

    return applyInBetween(inputArray, function, maxStartX, maxStartY, minEndX, minEndY);
}

这是非常多余的,因为我必须为我需要的每个最小/最大属性创建一个新流。

在我的用例中,类似的方法是指数成本递归算法的一部分,因此拥有一个仅打开一次流的并发解决方案会很棒。更好的是一种解决方案,它可以在现有流上运行而不会终止(但我怀疑这是可能的)。

你知道如何改进它吗?

编辑:我忘了提,Dimension 是不可变的,这在使用 Supplier 时是相关的。

编辑 2: 使用 lambda 表达式在流上调用 collect() 而不是创建 DimensionsMinMaxCollector 的实例具有最佳的运行时性能。 jessepeng 首先提到它,所以我将他的帖子标记为解决方案。我现在的实现是:

return Arrays.stream(dimensions)
             .collect(() -> new int[4], (array, dimension) -> {
        array[0] = Math.max(array[0], dimension.startX);
        array[1] = Math.min(array[1], dimension.endX);
        array[2] = Math.max(array[2], dimension.startY);
        array[3] = Math.min(array[3], dimension.endY);
}, (a, b) -> {
        a[0] = Math.max(a[0], b[0]);
        a[1] = Math.min(a[1], b[1]);
        a[2] = Math.max(a[2], b[2]);
        a[3] = Math.min(a[3], b[3]);
});

【问题讨论】:

  • 我认为使用 Streams 是不可能的,因为它们只能使用一次。要一次执行多项操作,请使用老式的 for-each 循环。
  • 可能是 dimArray = Arrays.asList(dimensions); 等其他选项,然后尝试使用 Collections.max(dimArray, Comparator.comparingInt(d -&gt; d.startX)) 等?

标签: java lambda java-stream


【解决方案1】:

您可以使用collect() 将流的所有元素组合成一个包含所需值的Dimensions 对象。

来自 Stream 文档:

<R> R collect(Supplier<R> supplier,
             BiConsumer<R, ? super T> accumulator,
             BiConsumer<R, R> combiner);

对此流的元素执行可变归约操作。 可变归约是一种归约值是可变的 结果容器,例如 ArrayList,并合并元素 通过更新结果的状态而不是替换 结果。这会产生相当于:

 R result = supplier.get();
 for (T element : this stream)
     accumulator.accept(result, element);
 return result;

因此,在您的情况下,您需要一个创建新 Dimension 对象的供应商,并且累加器和组合器将进行比较和设置值。

Dimensions searchDimensions = Arrays.stream(dimensions).collect(Dimensions::new, (dimension, dimension2) -> {
            dimension.endX = dimension.endX < dimension2.endX ? dimension.endX : dimension2.endX;
            dimension.endY = dimension.endY < dimension2.endY ? dimension.endY : dimension2.endY;
            dimension.startX = dimension.startX > dimension2.startX ? dimension.startX : dimension2.startX;
            dimension.startY = dimension.startY > dimension2.startY ? dimension.startY : dimension2.startY;
        }, (dimension, dimension2) -> {
            dimension.endX = dimension.endX < dimension2.endX ? dimension.endX : dimension2.endX;
            dimension.endY = dimension.endY < dimension2.endY ? dimension.endY : dimension2.endY;
            dimension.startX = dimension.startX > dimension2.startX ? dimension.startX : dimension2.startX;
            dimension.startY = dimension.startY > dimension2.startY ? dimension.startY : dimension2.startY;
        });

return applyInBetween(inputArray, function, searchDimensions.startX, searchDimensions.startY, searchDimensions.endX, searchDimensions.endY);

编辑 由于Dimensions 是不可变的,因此不适合执行可变归约操作。可以使用一个简单的数组来存储四个值。

<S, T> T performOnIntersections(Function<S, T> function, S[][] inputArray, Dimensions...dimensions){

    Supplier<int[]> supplier = () -> new int[]{Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE};
    BiConsumer<int[], Dimensions> accumulator = (array, dim) -> {
        array[0] = dim.startX > array[0] ? dim.startX : array[0];
        array[1] = dim.startY > array[1] ? dim.startY : array[1];
        array[2] = dim.endX < array[2] ? dim.endX : array[2];
        array[3] = dim.endY < array[3] ? dim.endY : array[3];
    };
    BiConsumer<int[], int[]> combiner = (array1, array2) -> {
        array1[0] = array1[0] > array2[0] ? array1[0] : array2[0];
        array1[1] = array1[1] > array2[1] ? array1[1] : array2[1];
        array1[2] = array1[2] < array2[2] ? array1[2] : array2[2];
        array1[3] = array1[3] < array2[3] ? array1[3] : array2[3];
    };

    int[] searchDimensions = Arrays.stream(dimensions).collect(supplier, accumulator, combiner);

    return applyInBetween(inputArray, function, searchDimensions[0], searchDimensions[1], searchDimensions[2], searchDimensions[3]);
}

【讨论】:

  • 您的代码假设默认构造函数创建了一个适合交集操作的Dimensions 实例,这不太可能。通常,此类类型的默认构造函数会生成一个空实例,其与另一个对象的交集将再次为空。除此之外,我建议消除具有两个相同BiConsumer 函数的代码重复。但总的来说,这是一种有效的方法。
  • 请看我更新的 OP。您可以通过提供一个接收结果值的整数数组来编辑答案。
【解决方案2】:

如果预期的结果值与您要比较的属性相同,则无需使用自定义比较器,只需映射到属性 before 获得最小响应。最大。如果属性具有原始类型,这可能会在简单性和效率方面带来额外的好处:

<S, T> T performOnIntersections(
         Function<S, T> function, S[][] inputArray, Dimensions...dimensions) {

    int maxStartX = Arrays.stream(dimensions).mapToInt(d -> d.startX).max().getAsInt();
    int maxStartY = Arrays.stream(dimensions).mapToInt(d -> d.startY).max().getAsInt();
    int minEndX = Arrays.stream(dimensions).mapToInt(d -> d.endX).min().getAsInt();
    int minEndY = Arrays.stream(dimensions).mapToInt(d -> d.endY).min().getAsInt();

    return applyInBetween(inputArray, function, maxStartX, maxStartY, minEndX, minEndY);
}

避免对普通数组进行多次迭代是否有任何好处,目前尚不清楚。如果你想试试,可以使用

<S, T> T performOnIntersections(
         Function<S, T> function, S[][] inputArray, Dimensions...dimensions){

    BiConsumer<Dimensions,Dimensions> join = (d1,d2) -> {
        d1.startX=Math.max(d1.startX, d2.startX);
        d1.startY=Math.max(d1.startY, d2.startY);
        d1.endX=Math.min(d1.endX, d2.endX);
        d1.endY=Math.min(d1.endY, d2.endY);
    };
    Dimensions d = Arrays.stream(dimensions).collect(
        () -> new Dimensions(Integer.MIN_VALUE,Integer.MIN_VALUE,
                             Integer.MAX_VALUE,Integer.MAX_VALUE),
        join, join);

    int maxStartX = d.startX;
    int maxStartY = d.startY;
    int minEndX = d.endX;
    int minEndY = d.endY;

    return applyInBetween(inputArray, function, maxStartX, maxStartY, minEndX, minEndY);
}

关键是join 函数将其第一个参数调整为两个维度的交集。这称为可变缩减,避免在每次评估时创建一个新的Dimensions 实例。为此,collect 方法需要一个 Supplier 作为其第一个参数,这会产生一个处于中性初始状态的新实例,即跨越整个整数范围的 Dimensions 实例。为此,我假设您有一个接受初始 startXstartYendXendY 值的构造函数。

一个不可变的归约也是可能的:

<S, T> T performOnIntersections(
         Function<S, T> function, S[][] inputArray, Dimensions...dimensions){

    Dimensions d = Arrays.stream(dimensions)
        .reduce((d1,d2) -> new Dimensions(
            Math.max(d1.startX, d2.startX),
            Math.max(d1.startY, d2.startY),
            Math.min(d1.endX, d2.endX),
            Math.min(d1.endY, d2.endY)))
        .get();

    int maxStartX = d.startX;
    int maxStartY = d.startY;
    int minEndX = d.endX;
    int minEndY = d.endY;

    return applyInBetween(inputArray, function, maxStartX, maxStartY, minEndX, minEndY);
}

对于较小的数组,这可能更有效(对于单元素数组的特殊情况,它只会返回元素)。这也适用于 Dimensions 的不可变版本。

【讨论】:

  • 我喜欢缩减的简洁性,但不幸的是(在我快速而肮脏的 JUnit 基准测试中)它需要的运行时间至少是 collect() 的 2 到 3 倍。
  • 这就是可变归约存在的原因。理论上,JVM 可以优化所有这些开销,但在实践中,我们不能总是依赖它来实现今天的实现。不要忘记在我的collect 方法中提到的方面,初始状态必须适合交叉点,即跨度(Integer.MIN_VALUE,Integer.MIN_VALUE) - (Integer.MAX_VALUE,Integer.MAX_VALUE)。这同样适用于数组方案,new int[4] 创建的全零数组不适合。
  • 我认为运行时间的增加与以下事实有关在每个步骤中。
【解决方案3】:

自定义收集器如何将元素收集到维度为 4 的数组中:

static class DimensionsMinMaxCollector implements Collector<Dimensions, int[], int[]> {

    @Override
    public BiConsumer<int[], Dimensions> accumulator() {
        return (array, dim) -> {
            array[0] = dim.startX > array[0] ? dim.startX : array[0];
            array[1] = dim.startY > array[1] ? dim.startY : array[1];
            array[2] = dim.endX > array[2] ? dim.endX : array[2];
            array[3] = dim.endY > array[3] ? dim.endY : array[3];
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return EnumSet.of(Characteristics.IDENTITY_FINISH);
    }

    // TODO this looks like is not an identity for negative values
    @Override
    public BinaryOperator<int[]> combiner() {
        return (left, right) -> {
            for (int i = 0; i < 4; i++) {
                left[i] = left[i] > right[i] ? left[i] : right[i];
            }
            return left;
        };
    }

    @Override
    public Function<int[], int[]> finisher() {
        return Function.identity();
    }

    @Override
    public Supplier<int[]> supplier() {
        return () -> new int[4];
    }

}

【讨论】:

    猜你喜欢
    • 2016-10-13
    • 2018-09-11
    • 2017-04-20
    • 1970-01-01
    • 1970-01-01
    • 2022-09-29
    • 2014-06-26
    • 2022-08-23
    • 2019-04-22
    相关资源
    最近更新 更多