为什么 .hasNext() 会消耗来自 BufferedReader 的元素?

Why does `.hasNext()` consume elements from `BufferedReader`?

问题陈述

在我在 wsl Ubuntu 上执行的已编译 .jar 文件中,我 运行 命令:task reportToGetUrgencyOfAllTasks 其中 returns 包含列的任务列表:id, uuid, urgency。我可以检查列表,它被打印到终端,它由 1793 个任务和 1798 行组成(一个 header 和一些关于需要同步的无关消息。)。当我用 reader.lines().count() 计算 objects 的数量时,它 returns 1798 并按预期消耗了这些行。

如果我创建一个 while 循环:

long counter = 0;
while (reader.lines().iterator().hasNext()) {
    counter++;
    System.out.println("counter=" + counter);
}

它列出了 1 到 1798 的数字,这很奇怪,因为我认为 .hasNext() 没有消耗项目,而只是检查下一个元素是否存在,而不是从流中取出它。 (如此处所建议:https://hajsoftutorial.com/iterator-hasnext-and-next/)。我期望数字的无限循环,因为迭代器将在 hasNext().

之后永远停留在第一个元素处

那么如果我想从 reader 中取出所有元素并将它们放入 ArrayList 中:

ArrayList<String> capturedCommandOutput = new ArrayList<String>();
while (reader.lines().iterator().hasNext()) {
    counter++;
    capturedCommandOutput.add(reader.lines().iterator().next());
}

每隔一行跳过一次。

问题

为什么 hasNext() 在这种情况下使用迭代器中的元素?

完整代码 对于 MWE,需要在 WSL Ubuntu 上安装 taskwarrior,但是,我认为这是一个 Java 问题,因为我可以看到 reader 确实包含所有 lines/information,因此为了完整性:运行命令的完整方法是:

package customSortServer;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Map;
import java.util.StringJoiner;

public class RunCommandsLongOutput2 {
    /**
     * This executes the commands in terminal. Additionally it sets an environment
     * variable (not necessary for your particular solution) Additionally it sets a
     * working path (not necessary for your particular solution)
     * 
     * @param commandData
     * @param ansYes
     * @throws Exception
     */
    public static ArrayList<String> executeCommands(Command command, Boolean ansYes) {
        ArrayList<String> capturedCommandOutput = new ArrayList<String>();
        File workingDirectory = new File(command.getWorkingDirectory());

        // create a ProcessBuilder to execute the commands in
        ProcessBuilder processBuilder = new ProcessBuilder(command.getCommandLines());

        // this is set an environment variable for the command (if needed)
        if (command.isSetEnvVar()) {
            processBuilder = setEnvironmentVariable(processBuilder, command);
        }

        // this can be used to set the working directory for the command
        if (command.isSetWorkingPath()) {
            processBuilder.directory(workingDirectory);
        }

        // execute the actual commands
        try {
            Process process = processBuilder.start();
            System.out.println("Started");
            if (command.isGetOutput()) {

                // capture the output stream of the command
                BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
//              System.out.println(reader.lines().count());
//              long counter = 0;
                while (reader.lines().iterator().hasNext()) {

                    capturedCommandOutput.add(reader.lines().iterator().next());
                }
//              while (reader.lines().iterator().hasNext()) {
//                  counter++;
//                  System.out.println("line=" + reader.lines().iterator().next());
//              }
            }

            // connect the output of your command to any new input.
            // e.g. if you get prompted for `yes`
            new Thread(new SyncPipe(process.getErrorStream(), System.err)).start();
            new Thread(new SyncPipe(process.getInputStream(), System.out)).start();
            PrintWriter stdin = new PrintWriter(process.getOutputStream());

            // This is not necessary but can be used to answer yes to being prompted
            if (ansYes) {
                stdin.println("yes");
            }

            // write any other commands you want here
            stdin.close();

            // If the command execution led to an error, returnCode!=0, or not (=0).
            int returnCode = process.waitFor();
            System.out.println("Return code = " + returnCode);
        } catch (IOException e1) {
            e1.printStackTrace();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // return output if required:
        return capturedCommandOutput;
    }

    /**
     * source: 
     * 
     * @param processBuilder
     * @param varName
     * @param varContent
     * @return
     */
    private static ProcessBuilder setEnvironmentVariable(ProcessBuilder processBuilder, Command command) {
        Map<String, String> env = processBuilder.environment();
        env.put(command.getEnvVarName(), command.getEnvVarContent());
        processBuilder.environment().put(command.getEnvVarName(), command.getEnvVarContent());
        return processBuilder;
    }
}

而生成命令的方法是:

    public static void createCommandToGetUrgencyList(HardCoded hardCoded) {

        // create copy command
        Command command = new Command();
        String[] commandLines = new String[2];
        commandLines[0] = "task";
        commandLines[1] = hardCoded.getGetUrgencyReportName();
        command.setCommandLines(commandLines);
        command.setEnvVarContent("/var/taskd");
        command.setEnvVarName("TASKDDATA");
        command.setWorkingPath("/usr/share/taskd/pki");
        command.setGetOutput(true);

        // execute command to copy file
        ArrayList<String> urgencyList = RunCommandsLongOutput2.executeCommands(command, false);
        System.out.println("The urgency list has length = "+urgencyList.size());
        for (int i = 0; i < urgencyList.size(); i++) {
            System.out.println("The output of the command =" + urgencyList.get(i));
        }
    }

您创建多个 Iterators - 一个在循环条件中,然后在循环的每次迭代中再创建一个。

应该是:

Iterator<String> iter = reader.lines().iterator();
long counter = 0;
while (iter.hasNext()) {
    counter++;
    System.out.println("counter=" + counter);
    capturedCommandOutput.add(iter.next());
}

由于每个 Iterator 都是从不同的 Stream<String> 生成的(由 lines() 返回),因此可以在每个 iterator() 上调用终端操作 (iterator()) Streams,但是当您调用 Iterators 的方法(hashNext()next())时,Iterators 使用来自其单一来源的数据数据 - BufferedReader.

正如 lines() 的 Javadoc 所说:

The reader must not be operated on during the execution of the terminal stream operation. Otherwise, the result of the terminal stream operation is undefined.

iterator()是一个终端操作,只要在它返回的Iterator上进行迭代,还是终端操作没有结束。因此,在完成 Iterator 之前,不应对 reader 进行操作。第二次调用 lines() 算作在 reader.

上操作

您正在一次又一次地从流创建流和迭代器。

调用 reader.lines() 每次都会创建一个 新流 - 它必须这样做,因为流不可重复使用。

在流上调用 iterator() 是一个 终端操作

所以您正在做的是在 reader 中的剩余元素上创建一个流,然后在其上创建一个迭代器。

迭代器契约没有说它不会消耗流中的任何元素。它说的是它不消耗迭代器本身中的任何元素。也就是说,如果您使用

Iterator<String> iter = reader.lines().iterator();

并且您调用 iter.hasNext(),您总是可以期望 reader 中可用的第一行是您从 iter 中读取的元素 - 而不是从任何其他迭代器中读取的元素同样的 reader.

实现这一点的一种方法是在您第一次调用 hasNext() 时从流中读取一个元素并将其缓冲。一旦你调用 next() 它会给你那个缓冲的元素。这维护了迭代器契约——但它仍然从 reader.

中读取一行

现在,如果您创建另一个流和另一个迭代器,它将只消耗下一行,依此类推。

您应该只在 reader 上调用一次 lines(),并且您应该只在结果流上调用一次 iterator() - 或者您应该只使用 "conservative"使用 readLine() 直到返回 null 的方法。