You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Scanner为何两次调用read()?游戏模拟自定义输入如何处理?

关于Scanner与自定义InputStream的问题(模拟Codingame输入管理)

我正在模拟Codingame的游戏输入管理逻辑,常规实现代码如下:

Scanner in = new Scanner(System.in);
int width = in.nextInt(); // 游戏网格的列数
int height = in.nextInt(); // 游戏网格的行数

// 游戏循环  
while (true) {  
    int entityCount = in.nextInt();  
    // 处理更多输入  
}

为了实现这个逻辑,我自定义了InputStream,但发现调用一次Scanner.nextInt()时,会触发两次read()调用。

遇到的问题

  • 连续两次返回-1时,Scanner会判定流已关闭,抛出NoSuchElementException。
  • 我发现先返回-1再返回虚拟空格' '可以避免流被标记为关闭,但这只是临时 workaround。
  • 我希望Scanner调用nextInt()时能等待新数据,而不是提前读取。

当前解决方案

private class GameStream extends InputStream {

    private String buffer = "";
    private int position = 0;
    private int playerNum;
    private boolean spaceFound = false;

    public GameStream(int playerNum) {
        this.playerNum = playerNum;
        setBuffer(getTurnData(playerNum));
    }

    public GameStream setBuffer(String buffer) {
        this.buffer = buffer;
        position = 0;
        return this;
    }

    @Override
    public int read() throws IOException {
        // 停止读取,直到下一次nextInt()调用
        if (spaceFound) {
            spaceFound = false;
            return -1;
        }
        if (position < buffer.length()) {
            int c = buffer.charAt(position++);
            if (c == ' ') {
                spaceFound = true;
            }
            return c;
        } else {
            players.get(playerNum).getHisData = true;

            // 防止Scanner关闭流
            setBuffer(" ");
            return -1;
        }
    }
}

疑问

  1. 为什么调用一次Scanner.nextInt()会触发两次read()?
  2. 不使用虚拟字符的情况下,如何让Scanner调用nextInt()时等待新数据?

问题复现代码

class GameStream extends InputStream {

    private String buffer = "";
    private int position = 0;

    public GameStream(String buffer) {
        setBuffer(buffer);
    }

    public GameStream setBuffer(String buffer) {
        this.buffer = buffer;
        position = 0;
        return this;
    }

    @Override
    public int read() throws IOException {
        if (position < buffer.length()) {
            int c = buffer.charAt(position++);
            return c;
        } else {
            System.out.println("player read all data");
            //setBuffer("without_dummy_space_should_fail ");
            return -1;
        }
    }
}

使用示例

GameStream gs = new GameStream("first turn finish_");
Scanner in = new Scanner(gs);
System.out.println(in.next() + " " + in.next() + " " + in.next());
gs.setBuffer("next turn_");
System.out.println(in.next()+ " " + in.next());

输出

player read all data before
player read all data before
first turn finish_without_dummy_space_should_fail
player read all data before
player read all data before
next turn_without_dummy_space_should_fail

解答

1. 为什么Scanner.nextInt()会触发两次read()?

Scanner的工作机制是预读校验:当调用nextInt()时,它会先读取字符直到找到分隔符(默认是空白字符),之后会额外调用一次read()来确认后续是否还有数据,或者判断流是否已经结束。这第二次read()是为了校验流的状态,所以会出现单次nextInt()触发两次read()的情况。

另外,Scanner内部维护了一个缓冲区,它会尝试从输入流中读取更多数据填充缓冲区,这也可能导致多次read()调用,即使你只需要读取一个整数。

2. 不使用虚拟字符,让Scanner等待新数据的方案

核心思路是让read()方法在没有数据时阻塞,而非返回-1,直到新数据被写入。可以通过线程同步机制(Object.wait()/notify())实现:

private class GameStream extends InputStream {
    private final Object lock = new Object();
    private String buffer = "";
    private int position = 0;
    private int playerNum;
    private boolean closed = false;

    public GameStream(int playerNum) {
        this.playerNum = playerNum;
        setBuffer(getTurnData(playerNum));
    }

    public GameStream setBuffer(String buffer) {
        synchronized (lock) {
            this.buffer = buffer;
            this.position = 0;
            lock.notifyAll(); // 通知等待的read()线程有新数据
        }
        return this;
    }

    @Override
    public int read() throws IOException {
        synchronized (lock) {
            // 循环等待,直到有数据或流关闭
            while (position >= buffer.length() && !closed) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new IOException("Stream interrupted", e);
                }
            }

            if (closed) {
                return -1;
            }

            int c = buffer.charAt(position++);
            return c;
        }
    }

    @Override
    public void close() throws IOException {
        synchronized (lock) {
            closed = true;
            lock.notifyAll();
        }
    }
}

这个方案的关键逻辑:

  • 当缓冲区无数据时,read()进入阻塞状态(lock.wait()),直到调用setBuffer()写入新数据并唤醒等待线程(lock.notifyAll())。
  • 只有当流被显式关闭时,read()才返回-1,避免Scanner误判流结束。
  • 全程无需虚拟字符,通过线程同步实现“等待新数据”的需求,完美适配游戏循环场景。

如果不需要多线程支持,也可以考虑自定义Scanner的分隔符,或改用BufferedReader配合StringTokenizer读取数据,但上述同步流方案最贴合Codingame的输入逻辑。


内容的提问来源于stack exchange,提问作者Ahmed Mazher

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 14:17:13