2017-02-03 3 views
3

Я ищу краткую возможность найти набор значений атрибутов, которые минимальны или максимальны в данном потоке объектов.Java: найти несколько значений атрибутов min/max в потоке с использованием lambda

Например:

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

Это очень избыточное, так как я должен создать новый поток для каждого минимального/максимального атрибута мне нужно.

В моей утилите подобный метод является частью рекурсивного алгоритма экспоненциальных затрат, поэтому наличие параллельного решения, которое открывает поток только один раз, будет большим. Еще лучше было бы решение, которое работает над существующим потоком без прекращения (но я сомневаюсь, что это возможно).

У вас есть идея, как улучшить его?

EDIT: Я забыл упомянуть, что Dimension неизменен, что актуально при использовании Supplier.

EDIT 2: Вызов 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]); 
}); 
+0

Я не думаю, что это можно использовать с помощью Streams, поскольку они могут использоваться только один раз. Чтобы сделать несколько вещей сразу, вы можете использовать старую схему для каждого цикла. – marknorkin

+0

Может быть другой вариант, например 'dimArray = Arrays.asList (размеры);', а затем попытка как 'Collections.max (dimArray, Comparator.comparingInt (d -> d.startX))' etc? –

ответ

2

Вы можете использовать collect() объединить все элементы потока в единый Dimensions объект, который содержит нужные значения.

Из документации потока:

<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]); 
} 
+0

Ваш код предполагает, что конструктор по умолчанию создает экземпляр 'Dimensions', подходящий для операции пересечения, что маловероятно. Более часто конструкторы по умолчанию таких типов создают пустой экземпляр, пересечение которого с другим объектом снова будет пустым. Кроме того, я бы рекомендовал устранить дублирование кода, имея две идентичные функции «BiConsumer». Но, как правило, это правильный подход. – Holger

+0

См. Мой обновленный OP. Вы можете отредактировать ответ, поставив целочисленный массив, который получает значения результата. –

1

не Как насчет пользовательского коллектора, который будет собирать элементы в массив размерности 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]; 
    } 

} 
1

Если предполагаемое значение результат будет таким же свойством, что вы сравниваете, нет необходимости использовать пользовательские компараторы, просто наведите указатель на объект до, чтобы получить минимальный уровень. максимум.Это может иметь дополнительные преимущества в простоте и эффективности, если свойство имеет примитивный тип:

<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, охватывающий весь целочисленный диапазон. Для этого я предположил, что у вас есть конструктор, принимающий начальные значения startX, startY, endX, endY.

Не-изменчивое сокращение также возможно:

<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.

+0

Мне нравится краткость редукции, но, к сожалению, (в моем быстром и грязном контрольном плане JUnit) это требует от 2 до 3 раз больше времени выполнения, чем 'collect()'. –

+0

Вот почему существует изменяемое сокращение. Теоретически все эти накладные расходы могут быть оптимизированы JVM, но на практике мы не можем всегда полагаться на нее для сегодняшних реализаций. Не забывайте аспект, упомянутый для моего подхода 'collect', что начальное состояние должно быть подходящим для пересечения, т. Е. Span' (Integer.MIN_VALUE, Integer.MIN_VALUE) - (Integer.MAX_VALUE, Integer.MAX_VALUE) '. Это также относится к решению массива, массив all-zero, созданный 'new int [4]', не подходит. – Holger

+0

Я полагаю, что увеличение времени выполнения связано с тем, что на каждом этапе уменьшения создается новый объект 'Dimensions', тогда как в подходе' collect() 'только каждый объект/массив создается и модифицируется на каждом шаге. – jessepeng

Смежные вопросы