Как использовать два фильтра в потоке для разных преобразований
Мне нужно выполнять преобразования только для определенного условия. Я делаю это преобразование:
// filter 1: less date - group by max date by groupId
List<Info> listResult = new ArrayList<>(listInfo.stream()
.filter(info -> info.getDate().getTime() < date.getTime())
.collect(Collectors.groupingBy(Info::getGroupId, Collectors.collectingAndThen(
Collectors.reducing((Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2),
Optional::get))).values());
Но для условия, когда есть больше, чем указанная дата, мне не нужно ничего преобразовывать, мне просто нужно вернуть эти данные:
// filter 2: more date - nothing change in list
List<Info> listMoreByDate = listInfo.stream()
.filter(info -> info.getDate().getTime() >= date.getTime())
.collect(Collectors.toList());
Далее, чтобы объединить эти два фильтра - я объединю два списка:
listResult.addAll(listMoreByDate);
Мой вопрос, это можно сделать в одном потоке? Поскольку фильтр 2 абсолютно бесполезен, он просто возвращает список для этого условия.
Можно ли выполнить эти преобразования одним непрерывным выражением?
Мой полный код:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) throws ParseException {
Info info1 = new Info(1L, getDateFromStr("2018-02-02T10:00:00"), 3L);
Info info2 = new Info(2L, getDateFromStr("2018-02-02T12:00:00"), 3L);
Info info3 = new Info(3L, getDateFromStr("2018-02-05T12:00:00"), 6L);
Info info4 = new Info(4L, getDateFromStr("2018-02-05T10:00:00"), 6L);
Date date = getDateFromStr("2018-02-03T10:10:10");
List<Info> listInfo = new ArrayList<>();
listInfo.add(info1);
listInfo.add(info2);
listInfo.add(info3);
listInfo.add(info4);
// filter 1: less date - group by max date by groupId
List<Info> listResult = new ArrayList<>(listInfo.stream()
.filter(info -> info.getDate().getTime() < date.getTime())
.collect(Collectors.groupingBy(Info::getGroupId, Collectors.collectingAndThen(
Collectors.reducing((Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2),
Optional::get))).values());
// filter 2: more date - nothing change in list
List<Info> listMoreByDate = listInfo.stream()
.filter(info -> info.getDate().getTime() >= date.getTime())
.collect(Collectors.toList());
listResult.addAll(listMoreByDate);
System.out.println("result: " + listResult);
}
private static Date getDateFromStr(String dateStr) throws ParseException {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(dateStr);
}
}
class Info {
private Long id;
private Date date;
private Long groupId;
public Info(Long id, Date date, Long groupId) {
this.id = id;
this.date = date;
this.groupId = groupId;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public Long getGroupId() {
return groupId;
}
public void setGroupId(Long groupId) {
this.groupId = groupId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Info info = (Info) o;
return Objects.equals(id, info.id) &&
Objects.equals(date, info.date) &&
Objects.equals(groupId, info.groupId);
}
@Override
public int hashCode() {
return Objects.hash(id, date, groupId);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Info{");
sb.append("id=").append(id);
sb.append(", date=").append(date);
sb.append(", groupId=").append(groupId);
sb.append('}');
return sb.toString();
}
}
Ответы
Ответ 1
Я не вижу ничего проще, чем
List<Info> listResult = Stream.concat(
listInfo.stream()
.filter(info -> info.getDate().getTime() < date.getTime())
.collect(Collectors.toMap(Info::getGroupId, Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Info::getDate))))
.values().stream(),
listInfo.stream()
.filter(info -> info.getDate().getTime() >= date.getTime())
)
.collect(Collectors.toList());
так как эти две операции принципиально разные. Создание Map
на первом этапе неизбежно, так как она будет использоваться для идентификации элементов с одинаковым свойством getGroupId
.
Тем не менее, вам следует подумать о переходе с использования Date
на java.time
API.
Ответ 2
Да, вы можете объединить два условия с помощью сборщика partitioningBy следующим образом:
List<Info> resultSet =
listInfo.stream()
.collect(collectingAndThen(partitioningBy(info -> info.getDate().getTime() < date.getTime()),
map -> Stream.concat(map.get(true)
.stream()
.collect(toMap(Info::getGroupId,
Function.identity(),
(Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2))
.values().stream(), map.get(false).stream())
.collect(Collectors.toCollection(ArrayList::new))));
По существу, он использует сборщик partitioningBy
для организации элементов таким образом, что все элементы, соответствующие критериям info.getDate().getTime() < date.getTime()
а также где это ложно, т.е. где info → info.getDate().getTime() >= date.getTime()
имеет значение true для Map<Boolean, List<T>>
.
Кроме того, мы используем сборщик collectingAndThen
чтобы применить завершающую функцию к Map<Boolean, List<T>>
возвращенному сборщиком partitioningBy
, в этом случае мы объединяем результат применения логики:
.collect(Collectors.groupingBy(Info::getGroupId,
Collectors.collectingAndThen(Collectors.reducing((Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2),
Optional::get))))
.values();
который я упростил до:
.collect(toMap(Info::getGroupId, Function.identity(), (Info i1, Info i2) -> i1.getDate().getTime() > i2.getDate().getTime() ? i1 : i2)))
.values();
с возвращенными элементами, где info.getDate().getTime() < date.getTime()
вернул false (map.get(false).stream()
).
Наконец, мы собираем результат в реализацию ArrayList
с toCollection
сборщика toCollection
.
Ответ 3
Другой подход (еще более многословный по определению, но гораздо менее многословный при использовании сайта) заключается в создании настраиваемого Collector
:
List<Info> listResult = listInfo.stream().collect(dateThresholdCollector(date));
где
private static Collector<Info, ?, List<Info>> dateThresholdCollector(Date date) {
return Collector.of(
() -> new ThresholdInfoAccumulator(date), ThresholdInfoAccumulator::accept,
ThresholdInfoAccumulator::combine, ThresholdInfoAccumulator::addedInfos
);
}
а также
class ThresholdInfoAccumulator {
private final Date date;
private final List<Info> addedInfos = new ArrayList<>();
ThresholdInfoAccumulator(Date date) {
this.date = date;
}
List<Info> addedInfos() {
return addedInfos;
}
ThresholdInfoAccumulator accept(Info newInfo) {
if (canAdd(newInfo)) {
addedInfos.add(newInfo);
}
return this;
}
boolean canAdd(Info newInfo) {
if (newInfo.getDate().compareTo(date) < 0) { // lower date - max date by groupId
return addedInfos.removeIf(addedInfo -> isEarlierDateInSameGroup(addedInfo, newInfo));
}
return true; // greater or equal date - no change
}
private boolean isEarlierDateInSameGroup(Info addedInfo, Info newInfo) {
return addedInfo.getGroupId().equals(newInfo.getGroupId())
&& addedInfo.getDate().compareTo(newInfo.getDate()) < 0;
}
ThresholdInfoAccumulator combine(ThresholdInfoAccumulator other) {
other.addedInfos().forEach(this::accept);
return this;
}
}
Примечание: это не будет так эффективно, если у вас есть огромное количество групп /Infos, потому что это не группа по getGroupId
(он перебирает весь список для каждого Info
будет добавлен).