Как получить несколько значений из объекта, используя одну операцию потока? - PullRequest
0 голосов
/ 03 января 2019

Я хочу определить минимальную площадь, необходимую для отображения набора точек.Самый простой способ - просмотреть коллекцию следующим образом:

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

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

Есть ли способ получить minX, maxX, minY и maxY в однопотоковой операции

Ответы [ 5 ]

0 голосов
/ 04 января 2019

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

Я решил попробовать большинство ваших решений (кроме решения 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-loop
  2. четыре отдельных потока
  3. поток с самодельным коллектором
  4. параллельный поток с самодельным коллектором
  5. статистический подход предоставлен Эндрю Тобилко

Числа 2 и 3 на самом деле очень близки по скорости.Параллельная версия, вероятно, медленнее, потому что мой набор данных слишком мал.

0 голосов
/ 04 января 2019

JDK 12 будет иметь Collectors.teeing ( webrev и CSR ), который собирает в два разных сборщика и затем объединяет оба частичных результата в окончательный результат.

Вы можете использовать его здесь, чтобы собрать до двух IntSummaryStatistics как для координаты x, так и для координаты 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. Затем статистические данные для x и y объединяются в List с помощью фабричного метода JDK 9 List.of, который принимает два элемента.

Альтернативой 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);
}

Обратите внимание, что при создании возвращенного коллектора я не учитываю характеристики нижестоящих коллекторов.

0 голосов
/ 04 января 2019

Вы можете разделить на две итерации с 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);

Возможен пользовательский коллектор, но учтите, что вы должны реализовать часть объединителя, которая сделает ваш код труднее для чтения.
Так что, если вам нужно использовать параллельный поток, вы должны остаться наимперативный путь

0 голосов
/ 04 января 2019

По аналогии с 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<Point> в 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);
    }
}

UPDATE

Я был полностью сбит с толку заключением , сделанным OP, поэтому я решил написать JMH отметки.

Настройки бенчмарка:

# 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

Для каждой итерации я генерировал общий список случайных Point s (new Point(random.nextInt(), random.nextInt())) размером 100K, 1M, 10M.

Результаты

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
0 голосов
/ 04 января 2019

Вы можете использовать 2 потока, используя Stream::reduce, чтобы получить точку с минимумом и точку с максимумом. Я не рекомендую объединять результаты в один поток, поскольку может быть трудно различить разницу между минимумом, максимумом и координатами.

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<Point> используйте два последующих Points и троичный оператор для определения минимума / максимума, который передается новому объекту Point и возвращается с использованием Optional::orElse с координатами -1, -1 по умолчанию.

...