自Java 8开始,使用Stream API的情况就很普遍。但是,使用基于批处理的算法可以轻松解决一些问题,而使用基于流的解决方案则可能无法轻松解决这些问题。
例如,给定一张信用卡按时间顺序进行的交易流,我希望在每24小时内每次找到该卡的总交易金额,以便将其与阈值进行比较猜测卡是否被盗。 流数据可以很简单
transaction time amount
2019-01-23T10:12:31.484Z 100
2019-01-24T00:12:30.004Z 50
2019-01-24T09:00:00.000Z 23
2019-01-27T05:10:00.300Z 65
这可以看作是滑动窗口问题,需要检查元素之间的关系。基于批处理的解决方案不是很复杂。我可以使用队列将所有交易保持在24小时之内。 该算法可以通过以下步骤大致描述:
创建一个队列并将第一个事务放入队列;
将下一个事务与队列头事务进行比较。
检查2笔交易的交易时间差
如果时间差小于24小时,
将事务添加到队列中并返回到步骤2。
否则,如果超过24小时,则
5.1。计算那些在24小时内发生的交易中队列中交易的总交易量
5.2将结果放入结果列表。
5.3轮询队列以删除最早的事务,直到新事务少于24小时且队列中有事务。
5.4循环至步骤2。
但是,我发现使用Java Stream API来实现上述算法很困难,所以我想知道使用Java流实现滑动窗口问题是一个好主意吗?如果是,那么有人可以使用Java Stream来实现一些提示或伪代码吗?不必使用上述算法。任何基于流的算法都可以。
答案 0 :(得分:0)
这是一个示例,如何使用reduce
完成。
用于收集数据的类已创建:
class Row {
private LocalDateTime date;
private Integer value;
public Row(LocalDateTime date, Integer value) {
this.date = date;
this.value = value;
}
// getters and setters
首先将示例数据读入流。
Stream<Row> readData = Stream.of(
"2019-01-23T10:12:31 100",
"2019-01-24T00:12:30 50",
"2019-01-24T09:00:00 23",
"2019-01-25T03:00:00 23",
"2019-01-27T05:10:00 65")
.map(s -> s.split("\\s+"))
.map(a -> new Row(LocalDateTime.parse(a[0], DateTimeFormatter.ISO_LOCAL_DATE_TIME), Integer.valueOf(a[1])));
比起reduce
方法,每隔24小时都要收集一行以分隔的列表并将其存储在主列表中。
List<List<Row>> all = new ArrayList<>();
all.add(readData
.map(Arrays::asList)
.reduce(new ArrayList<>(), (a, v) -> {
if (a.isEmpty()) {
a.addAll(v);
} else {
LocalDateTime first = a.get(0).getDate().plusHours(24);
if (first.isAfter(v.get(0).getDate())) {
a.addAll(v);
} else {
all.add(a);
LocalDateTime last = v.get(0).getDate().minusHours(24);
a = new ArrayList<>(a.stream()
.filter(r -> last.isBefore(r.getDate()))
.collect(Collectors.toList()));
a.addAll(v);
}
}
return a;
}));
最后,您可以打印出所有列表或计算每个期间的交易额。
all.forEach(System.out::println);
all.stream().map(l -> l.stream()
.map(Row::getValue)
.reduce(Integer::sum)
.get()
)
.forEach(System.out::println);
更新
reduce
也可以替换为简单的forEach
。
然后,主要部分将是这样,并且将不再那么复杂:
LinkedList<List<Row>> all = new LinkedList<>(Arrays.asList(new ArrayList<>()));
readData
.forEach(v -> {
if (!all.getLast().isEmpty()) {
// check if in 24h boundary
LocalDateTime upperValue = all.getLast().get(0).getDate().plusHours(24);
if (!upperValue.isAfter(v.getDate())) {
// create copy with row older earlier than 24h
LocalDateTime lowerValue = v.getDate().minusHours(24);
all.add(new ArrayList<>(all.getLast().stream()
.filter(r -> lowerValue.isBefore(r.getDate()))
.collect(Collectors.toList())));
}
}
all.getLast().add(v);
});
答案 1 :(得分:0)
通过流执行此操作的“问题”是,它们被设计为易于并行化。根据设计,它们不是仅顺序的,因此,纯粹的顺序算法(如基于队列的算法一样)不适用于Streams。这是从命令式编程到函数式编程的飞跃,有时您需要一种新的算法,新的方法。将命令性代码转换为功能性并不总是那么容易。
如果您的数据源可以轻松地split多次(我认为,这等效于能够高效且同时提供所需的窗口),则可以使用Streams做到这一点。例如,如果您的数据源是List
,则可以(重新)使用StreamEx的ofSublists()
方法。之所以可行,是因为任何子列表的创建都是有效的,并且与其他子列表无关,因此可以安全地并发调用它。
或者,您可以假设您的Stream永远不会并行运行,并使用专注于纯序列的内容,例如jOOL:Seq.sliding()
。这对任何Seq都有效,因为实现可以使用迭代状态而不必担心并行性。