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]);
});
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]);
}
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
.
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];
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With