Java-лямбда-выражение, позволяющее избежать нескольких итераций

#java #lambda #java-8 #java-stream

#java #лямбда #java-8 #java-stream

Вопрос:

Люди,

Рассмотрим следующий пример: учитывая список торговых объектов, мой код должен возвращать массив, содержащий объем торговли за 24 часа, 7 дней, 30 дней и все время.

Используя обычный старый итератор, для этого требуется только одна итерация по коллекции.

Я пытаюсь сделать то же самое, используя потоки Java 8 и лямбда-выражения. Я придумал этот код, который выглядит элегантно, работает нормально, но требует 4 итерации по списку:

 public static final int DAY = 24 * 60 * 60;

public double[] getTradeVolumes(List<Trade> trades, int timeStamp) {
    double volume = trades.stream().mapToDouble(Trade::getVolume).sum();
    double volume30d = trades.stream().filter(trade -> trade.getTimestamp()   30 * DAY > timeStamp).mapToDouble(Trade::getVolume).sum();
    double volume7d = trades.stream().filter(trade -> trade.getTimestamp()   7 * DAY > timeStamp).mapToDouble(Trade::getVolume).sum();
    double volume24h = trades.stream().filter(trade -> trade.getTimestamp()   DAY > timeStamp).mapToDouble(Trade::getVolume).sum();
    return new double[]{volume24h, volume7d, volume30d, volume};
}
 

Как я могу добиться того же, используя только одну итерацию по списку?

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

1. Общий вопрос: почему вы хотите сделать это только за одну итерацию?

2. Поскольку это более эффективно.

3. Нет! Это не более эффективно! Я задал вопрос, поскольку ожидал такого ответа. И ваше предположение неверно: кто сказал, что две итерации, каждая из которых выполняет одну операцию, должны быть менее эффективными, чем только одна итерация, выполняющая две операции?

4. Рассмотрим случай, когда у меня есть 1 000 000 сделок и 100 разных объемов для вычисления, а метод trade.getTimestamp() является дорогостоящим. Используя итератор, мне нужно вызвать его только 1 000 000 раз, при использовании лямбда-выражения мне нужно вызвать его 100 миллионов раз.

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

Ответ №1:

Эта проблема аналогична сборщику «сводной статистики». Взгляните на IntSummaryStatistics класс:

 public class IntSummaryStatistics implements IntConsumer {
    private long count;
    private long sum;
    ...

    public void accept(int value) {
          count;
        sum  = value;
        min = Math.min(min, value);
        max = Math.max(max, value);
   }

   ...
 

}

Он предназначен для работы с collect() ; вот реализация IntStream.summaryStatistics()

 public final IntSummaryStatistics summaryStatistics() {
    return collect(IntSummaryStatistics::new, IntSummaryStatistics::accept,
                   IntSummaryStatistics::combine);
}
 

Преимущество написания Collector подобного заключается в том, что ваша пользовательская агрегация может выполняться параллельно.

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

1. Это был мой первоначальный подход, но обратите внимание на другой фильтр, применяемый перед сопоставлением. Как бы вы определили коллектор и при этом применили фильтр?

2. Метод accept будет условно увеличивать различные суммы. По сути, вы пропускаете все данные через сборщик, который может выбирать нужные биты. Для последовательной обработки это в основном эквивалентно циклу for, но оно распараллеливается чисто, тогда как цикл for — нет.

Ответ №2:

Спасибо, Брайан, в итоге я реализовал приведенный ниже код, это не так просто, как я надеялся, но, по крайней мере, он повторяется только один раз, готов к параллели и проходит мои модульные тесты. Любые идеи по улучшению приветствуются.

 public double[] getTradeVolumes(List<Trade> trades, int timeStamp) {
    TradeVolume tradeVolume = trades.stream().collect(
            () -> new TradeVolume(timeStamp),
            TradeVolume::accept,
            TradeVolume::combine);
    return tradeVolume.getVolume();
}

public static final int DAY = 24 * 60 * 60;

static class TradeVolume {

    private int timeStamp;
    private double[] volume = new double[4];

    TradeVolume(int timeStamp) {
        this.timeStamp = timeStamp;
    }

    public void accept(Trade trade) {
        long tradeTime = trade.getTimestamp();
        double tradeVolume = trade.getVolume();
        volume[3]  = tradeVolume;
        if (!(tradeTime   30 * DAY > timeStamp)) {
            return;
        }
        volume[2]  = tradeVolume;
        if (!(tradeTime   7 * DAY > timeStamp)) {
            return;
        }
        volume[1]  = tradeVolume;
        if (!(tradeTime   DAY > timeStamp)) {
            return;
        }
        volume[0]  = tradeVolume;
    }

    public void combine(TradeVolume tradeVolume) {
        volume[0]  = tradeVolume.volume[0];
        volume[1]  = tradeVolume.volume[1];
        volume[2]  = tradeVolume.volume[2];
        volume[3]  = tradeVolume.volume[3];
    }

    public double[] getVolume() {
        return volume;
    }
}
 

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

1. Вы установили, что getTimestamp was the expensive part, so you could factor that out within accept` вместо того, чтобы выполнять его три раза.

2. Теперь, после вашего редактирования, вы сравниваете timestamp с timestamp тем, что не имеет смысла. Если вы называете локальную переменную так же, как поле экземпляра, вы должны квалифицировать доступ к последнему this. . И обратите внимание, что ваши условия связаны: для положительного i результата вы можете сказать, что если x i>y это true так, вы это знаете x 7*i>y и x 30*i>y так true же хорошо (если вы можете исключить переполнение). Или, если x 30*i>y есть false , x 7*i>y и x i>y должно быть false также. Так что есть место для улучшения вашего условного кода…

3. Исправлено, я пытаюсь упростить сложный код до упрощенного примера, что вызывает эти проблемы.

Ответ №3:

Возможно, можно было бы использовать Collectors.groupingBy метод для разделения данных, однако уравнение было бы сложным и не раскрывало намерений.

Поскольку getTimestamp() это дорогостоящая операция, вероятно, лучше сохранить ее как итерацию до Java 8, поэтому вам нужно вычислять значение только один раз за Trade .

Только потому, что Java 8 добавляет блестящие новые инструменты, не пытайтесь превратить его в молоток, чтобы забивать все гвозди.