Рефакторинг с потоками Java 8

#java #java-8 #java-stream #refactoring

#java #java-8 #java-stream #рефакторинг

Вопрос:

Я реализовал следующий класс DateBucket , у которого есть вызываемый метод bucketize . Этот метод возвращает список DateBucket того, где каждый DateBucket элемент списка находится между датами fromDate и toDate и использует bucketSize и bucketSizeUnit в качестве шагов для установки значений переменных from и to DateBucket .

 class DateBucket {
    final Instant from;
    final Instant to;

    public static List<DateBucket> bucketize(
        ZonedDateTime fromDate, 
        ZonedDateTime toDate, 
        int bucketSize, 
        ChronoUnit bucketSizeUnit
        ) {
    List<DateBucket> buckets = new ArrayList<>();
    
    boolean reachedDate = false;
    
    for (int i = 0; !reachedDate; i  ) {
        ZonedDateTime minDate = fromDate.plus(i * bucketSize, bucketSizeUnit);
        ZonedDateTime maxDate = fromDate.plus((i   1) * bucketSize, bucketSizeUnit);
        reachedDate = toDate.isBefore(maxDate);
        buckets.add(new DateBucket(minDate.toInstant(), maxDate.toInstant()));
    }
    return buckets;
}
}
 

В качестве примера следующий ввод:

 fromDate -> 2020-12-07 00:00:00
toDate -> 2020-12-07 00:00:03
bucketSize -> 1
bucketSizeUnit -> ChronoUnit.SECONDS
 

Возвращает следующий DateBucket список:

 0. DateBucket(from = 2020-12-07  05:00:00, to = 2020-12-07 05:00:01)
1. DateBucket(from = 2020-12-07  05:00:01, to = 2020-12-07 05:00:02)
2. DateBucket(from = 2020-12-07  05:00:02, to = 2020-12-07 05:00:03)
3. DateBucket(from = 2020-12-07  05:00:03, to = 2020-12-07 05:00:04)
 

Итак, как я могу реорганизовать bucketize метод, используя ту же логику, но с потоками Java 8?

Комментарии:

1. Обратите внимание, что вы создаете каждый объект ZonedDateTime и Instant дважды. Но вы можете сделать то же самое с таким потоком, как return LongStream.rangeClosed(0, fromDate.until(toDate, bucketSizeUnit)) .mapToObj(l -> new DateBucket(fromDate.plus(l, bucketSizeUnit).toInstant(), fromDate.plus(l 1, bucketSizeUnit).toInstant())) .collect(Collectors.toList()); .

Ответ №1:

Вы могли бы сделать:

 public static List<DateBucket> bucketize(
        ZonedDateTime fromDate, 
        ZonedDateTime toDate, 
        int bucketSize, 
        ChronoUnit bucketSizeUnit
    ) {
    
    var result = new ArrayList<DateBucket>();
    IntStream.range(0, Integer.MAX_VALUE)
            .mapToObj(i -> fromDate.plus(i * bucketSize, bucketSizeUnit))
            .takeWhile(d -> d.isBefore(toDate))
            .map(d -> d.toInstant())
            .reduce((min, max) -> {
                result.add(new DateBucket(min, max));
                return max;
            });
    return resu<
}
 

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

Тем не менее, я бы, вероятно, предпочел вашу императивную реализацию.

Ответ №2:

Суть в том, что вы хотите передать значения, необходимые для создания сегментов, а затем map и collect их. Подойдет поток ZonedDateTimes , и вы можете использовать a Spliterator для создания потока. Похоже, это одна из возможных реализаций, которая является базовой и не поддерживает распараллеливание, но, похоже, работает.

 public class ZonedDateTimeSpliterator implements Spliterator<ZonedDateTime> {
    private final ZonedDateTime fromDate;
    private final ZonedDateTime toDate;
    private final int bucketSize;
    private final ChronoUnit bucketSizeUnit;
    private ZonedDateTime currentDate;

    public ZonedDateTimeSpliterator(ZonedDateTime fromDate, ZonedDateTime toDate, int bucketSize,
            ChronoUnit bucketSizeUnit) {
        this.fromDate = fromDate;
        this.toDate = toDate;
        this.bucketSize = bucketSize;
        this.bucketSizeUnit = bucketSizeUnit;
        currentDate = fromDate.truncatedTo(bucketSizeUnit);
    }

    public void forEachRemaining(Consumer<? super ZonedDateTime> action) {
        while (currentDate.plus(bucketSize, bucketSizeUnit).isBefore(toDate)) {
            getNext(action);
        }
    }

    private void getNext(Consumer<? super ZonedDateTime> action) {
        action.accept(currentDate);
        currentDate = currentDate.plus(bucketSize, bucketSizeUnit);
    }

    @Override
    public boolean tryAdvance(Consumer<? super ZonedDateTime> action) {
        if (currentDate.plus(bucketSize, bucketSizeUnit).isBefore(toDate)) {
            getNext(action);
            return true;
        } else // cannot advance
            return false;
    }

    @Override
    public Spliterator<ZonedDateTime> trySplit() {
        // implement for parallel processing
        return null;
    }

    @Override
    public long estimateSize() {
        return Duration.between(fromDate.truncatedTo(bucketSizeUnit), toDate.truncatedTo(bucketSizeUnit))
                .get(bucketSizeUnit);
    }

    @Override
    public int characteristics() {
        return ORDERED | SIZED | IMMUTABLE;
    }
}
 

Затем вы просто stream указываете значения, map сегменты и collect список.

 private List<DateBucket> doThing(ZonedDateTime fromDate, ZonedDateTime toDate, int bucketSize,
        ChronoUnit bucketSizeUnit) {
    return StreamSupport
            .stream(() -> new ZonedDateTimeSpliterator(fromDate, toDate, bucketSize, bucketSizeUnit),
                    Spliterator.ORDERED | Spliterator.SIZED | Spliterator.IMMUTABLE, false)
            .map(currentDate -> new DateBucket(currentDate.toInstant(),
                    currentDate.plus(bucketSize, bucketSizeUnit).toInstant()))
            .collect(Collectors.toList());

}
 

Комментарии:

1. Вы могли бы использовать простой LongStream like в комментарии к вопросу или реализовать Spliterator подобное, предложенное здесь. A Spliterator позволит вам перейти к распараллеливанию, если вы хотите, хотя я не думаю, что этот пример оправдывает это. В основном я посмотрел на вопрос и подумал: «Вау, это можно сделать с помощью разделителя». У меня не так много (читай: нет) практики реализации Spliterators , поэтому я использовал этот ответ как возможность попрактиковаться. Выбор читателей.

Ответ №3:

Вы можете варьировать количество требуемых сегментов, заранее рассчитав это число с помощью TemporalUnit#between :

 public static List<DateBucket> bucketize(ZonedDateTime fromDate, ZonedDateTime toDate,
    int bucketSize, ChronoUnit bucketSizeUnit) {

    return LongStream.range(0, bucketSizeUnit.between(fromDate, toDate)/bucketSize   1)
        .mapToObj( nb -> new DateBucket(
            fromDate.plus(nb * bucketSize, bucketSizeUnit).toInstant(),
            fromDate.plus((nb 1) * bucketSize, bucketSizeUnit).toInstant()))
        .collect(Collectors.toList());
}