Java 8 Streams:逐字读取文件

Tob*_*obi 5 java java-8 java-stream

我使用Java 8流来处理文件,但到目前为止总是逐行处理.

我想要的是一个函数,它获取BufferedReader br并且应该读取特定数量的单词(分隔"\\s+")并且应该将BufferedReader保留在达到单词数的确切位置.

现在我有一个版本,它按行读取文件:

    final int[] wordCount = {20};
    br
          .lines()
          .map(l -> l.split("\\s+"))
          .flatMap(Arrays::stream)
          .filter(s -> {
              //Process s
              if(--wordCount[0] == 0) return true;
              return false;
          }).findFirst();
Run Code Online (Sandbox Code Playgroud)

这显然使Inputstream处于第20个单词的下一行的位置.
有没有办法从输入流中获取少于一行的流?

编辑
我正在解析一个文件,其中第一个单词包含下列单词的数量.我读了这个词然后读了具体的单词数.该文件包含多个这样的部分,其中每个部分在所描述的函数中被解析.

阅读完所有有用的注释后,我很清楚,使用a Scanner是这个问题的正确选择,Java 9将有一个Scanner提供流功能的类(Scanner.tokens()Scanner.findAll()).
以我描述的方式使用Streams将无法保证读者将在流的终端操作(API文档)之后处于特定位置,因此使得流成为解析结构的错误选择,其中您只解析一个结构部分并且必须跟踪位置.

Tag*_*eev 5

关于你原来的问题:我假设你的文件是这样的:

5 a section of five words 3 three words
section 2 short section 7 this section contains a lot 
of words
Run Code Online (Sandbox Code Playgroud)

你想得到这样的输出:

[a, section, of, five, words]
[three, words, section]
[short, section]
[this, section, contains, a, lot, of, words]
Run Code Online (Sandbox Code Playgroud)

通常,Stream API非常适合此类问题.编写普通的旧循环在这里看起来更好.如果您仍想查看基于Stream API的解决方案,我可以建议使用我的StreamEx库,其中包含headTail()允许您轻松编写自定义流转换逻辑的方法.以下是使用以下方法解决问题的方法headTail:

/* Transform Stream of words like 2, a, b, 3, c, d, e to
   Stream of lists like [a, b], [c, d, e] */
public static StreamEx<List<String>> records(StreamEx<String> input) {
    return input.headTail((count, tail) -> 
        makeRecord(tail, Integer.parseInt(count), new ArrayList<>()));
}

private static StreamEx<List<String>> makeRecord(StreamEx<String> input, int count, 
                                                 List<String> buf) {
    return input.headTail((head, tail) -> {
        buf.add(head);
        return buf.size() == count 
                ? records(tail).prepend(buf)
                : makeRecord(tail, count, buf);
    });
}
Run Code Online (Sandbox Code Playgroud)

用法示例:

String s = "5 a section of five words 3 three words\n"
        + "section 2 short section 7 this section contains a lot\n"
        + "of words";
Reader reader = new StringReader(s);
Stream<List<String>> stream = records(StreamEx.ofLines(reader)
               .flatMap(Pattern.compile("\\s+")::splitAsStream));
stream.forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

结果与上面所需的输出完全一致.替换reader为您BufferedReaderFileReader从输入文件中读取.记录流是懒惰的:一次最多只有一条记录由流保存,如果你短路,其余的输入将不会被读取(当然,当前文件行将被读取到最后).该解决方案虽然看起来递归,但不会占用堆栈或堆,因此它也适用于大型文件.


说明:

headTail()方法采用两参数lambda,当请求流元素时,该参数在外部流终端操作执行期间最多执行一次.lambda接收第一个流元素(head)和包含所有其他原始元素(tail)的流.lambda应返回一个新流,而不是原始流.在records我们有:

return input.headTail((count, tail) -> 
    makeRecord(tail, Integer.parseInt(count), new ArrayList<>()));
Run Code Online (Sandbox Code Playgroud)

输入的第一个元素是count:将其转换为数字,创建为空ArrayList并调用makeRecord尾部.这是makeRecord辅助方法实现:

return input.headTail((head, tail) -> {
Run Code Online (Sandbox Code Playgroud)

第一个流元素是head,将它添加到当前缓冲区:

    buf.add(head);
Run Code Online (Sandbox Code Playgroud)

达到目标缓冲区大小?

    return buf.size() == count 
Run Code Online (Sandbox Code Playgroud)

如果是,recordstail再次调用for (处理下一条记录,如果有的话),并在结果流中添加单个元素:当前缓冲区.

            ? records(tail).prepend(buf)
Run Code Online (Sandbox Code Playgroud)

否则,调用自己的尾部(向缓冲区添加更多元素).

            : makeRecord(tail, count, buf);
});
Run Code Online (Sandbox Code Playgroud)