Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java: Find multiple min/max attribute values in a stream using lambda

I'm looking for a concise way to find a set of attribute values, that are minimal or maximal in a given stream of objects.

For example:

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);
}

This is very redundant, as I have to create a new stream for every minimal/maximal attribute I need.

In my usecase, a similar method is part of an recursive algorithm of exponential costs, so having a concurrent solution, that opens the stream just once would be great. Even better would be a solution, that works on an existing stream without termination (but I doubt that's possible).

Do you have an idea how to improve it?

EDIT: I forgot to mention, that Dimension is immutable, which is relevant when using a Supplier.

EDIT 2: Calling collect() on the stream using a lambda expression rather than creating an instance of DimensionsMinMaxCollector has the best runtime performance. jessepeng mentioned it first, so I marked his post as solution. My implementation is now:

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]);
});
like image 584
SME_Dev Avatar asked Feb 03 '17 10:02

SME_Dev


3 Answers

You can use collect() to combine all the elements of the stream into a single Dimensions object that holds the desired values.

From the Stream documentation:

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

Performs a mutable reduction operation on the elements of this stream. A mutable reduction is one in which the reduced value is a mutable result container, such as an ArrayList, and elements are incorporated by updating the state of the result rather than by replacing the result. This produces a result equivalent to:

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

So in your case, you would need a supplier that creates a new Dimension object, and the accumulator and combiner would do the comparing and setting the values.

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);

Edit Since Dimensions is immutable, it is not suitable for performing a mutable reduction operation. A simple array can be used to store the four values instead.

<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]);
}
like image 131
jessepeng Avatar answered Sep 30 '22 22:09

jessepeng


If the intended result value is the same property that you are comparing, there is no need to use custom comparators, just map to the property before getting the minimum resp. maximum. This may have additional benefits in simplicity and efficiency, if the property has a primitive type:

<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);
}

Whether avoiding multiple iterations over an ordinary array has any benefit, is unclear. If you want to try it, you can use

<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);
}

The key point is the join function which adjust its first argument to be the intersection of the two dimensions. This is called mutable reduction and avoids creating a new Dimensions instance on every evaluation. For this to work, the collect method needs a Supplier as its first argument, which produces a new instance in a neutral initial state, i.e. a Dimensions instance spanning the entire integer range. For this, I assumed that you have a constructor accepting initial startX, startY, endX, endY values.

A non-mutable reduction is also possible:

<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);
}

For smaller arrays, this might be even more efficient (for the special case of a single element array, it will just return the element). This would also work with an immutable version of Dimensions.

like image 36
Holger Avatar answered Sep 30 '22 22:09

Holger


How about a custom collector that would collect elements to an array of dimension 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];
    }

}
like image 25
Eugene Avatar answered Sep 30 '22 22:09

Eugene