【问题标题】:How to preserve state when performing operations on Java 8 Stream?在 Java 8 Stream 上执行操作时如何保留状态?
【发布时间】:2015-11-20 08:49:47
【问题描述】:

我需要解析一个由不同整数组成的字符串,这些整数代表某个用户看电视或不看电视的时间段。

我首先拆分字符串并将其收集到一个 ArrayList 中:

final List<String> separated = stream(split(s, "A"))
  .map(str -> "A" + str)
  .flatMap(str -> stream(split(str, "B")).map(s2 -> s2.startsWith("A") ? s2 : "B" + s2))
  collect(Collectors.toList());

棘手的事情现在来了。我需要将这些字符串转换为具有from/to 字段的域对象。

所以,为了正确映射它,我的映射函数需要知道前一个元素。所以我做了以下事情:

LocalDateTime temp = initial;

final ArrayList<WatchingPeriod> result = new ArrayList<>();

for (final String s1 : separated) {
    final WatchingPeriod period = new WatchingPeriod(temp, temp.plusMinutes(parseLong(substring(s1, 1))),
                    s1.startsWith("A"));
                    result.add(period);
        temp = period.getTo();
    }
    return result;

我觉得这是一个巨大的倒退,因为我打破了整个流管道只是为了回到旧学校 for-each。

有什么方法可以在一个流管道中完成整个处理?我正在考虑创建一个自定义收集器,它会查看集合中的最后一个元素并基于此计算正确的 LocalDateTime 对象。

例子:

输入字符串:“A60B80A60”,表示有人看了60分钟,然后停了80分钟,又看了60分钟

因此我想获得一个包含对象的列表:

1) 从:0:00,到:1:00,观看:true

2) 从:1:00,到:2:20,观看:假

3) 从:2:20,到:3:20,观看:true

计算每个对象都需要了解前一个对象

【问题讨论】:

  • 您能提供输入字符串的样本吗? (以及想要的输出?)
  • @RobAu,我刚刚编辑了我的答案
  • 重复问题的答案应该包含足够的信息:)

标签: java string java-8 java-stream


【解决方案1】:

这不是关于连续对,而是关于收集累积前缀。在函数式编程中,这种操作通常称为scanLeft,它存在于许多函数式语言中,例如Scala。不幸的是,Java 8 Stream API 的当前实现中没有它,所以我们只能用forEachOrdered 来模拟它。让我们创建一个模型对象:

static class WatchPeriod {
    static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
    final LocalTime start;
    final Duration duration;
    final boolean watched;

    WatchPeriod(LocalTime start, Duration duration, boolean watched) {
        this.start = start;
        this.duration = duration;
        this.watched = watched;
    }

    // Takes string like "A60" and creates WatchPeriod starting from 00:00
    static WatchPeriod forString(String watchPeriod) {
        return new WatchPeriod(LocalTime.of(0, 0),
                   Duration.ofMinutes(Integer.parseInt(watchPeriod.substring(1))),
                   watchPeriod.startsWith("A"));
    }

    // Returns new WatchPeriod which start time is adjusted to start
    // right after the supplied previous period
    WatchPeriod after(WatchPeriod previous) {
        return new WatchPeriod(previous.start.plus(previous.duration), duration, watched);
    }

    @Override
    public String toString() {
        return "from: "+start.format(FORMATTER)+", to: "+
            start.plus(duration).format(FORMATTER)+", watched: "+watched;
    }
}

现在我们可以将"A60B80A60" 之类的输入字符串拆分为标记"A60", "B80", "A60",将这些标记映射到WatchPeriod 对象,然后将它们存储到结果列表中:

String input = "A60B80A60";
List<WatchPeriod> result = new ArrayList<>();
Pattern.compile("(?=[AB])").splitAsStream(input)
    .map(WatchPeriod::forString)
    .forEachOrdered(wp -> result.add(result.isEmpty() ? wp : 
                          wp.after(result.get(result.size()-1))));
result.forEach(System.out::println);

输出是:

from: 00:00, to: 01:00, watched: true
from: 01:00, to: 02:20, watched: false
from: 02:20, to: 03:20, watched: true

如果您不介意使用第三方库,我的免费 StreamEx 增强了 Stream API,添加了缺少的 scanLeft 操作以及其他功能:

String input = "A60B80A60";
List<WatchPeriod> result = StreamEx.split(input, "(?=[AB])")
        .map(WatchPeriod::forString).scanLeft((prev, next) -> next.after(prev));
result.forEach(System.out::println);

结果是一样的。

【讨论】:

    猜你喜欢
    • 2017-11-06
    • 2018-08-04
    • 2014-08-14
    • 2018-08-04
    • 1970-01-01
    • 2015-07-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多