#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 добавляет блестящие новые инструменты, не пытайтесь превратить его в молоток, чтобы забивать все гвозди.