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; } } }
疑问
- 为什么调用一次
Scanner.nextInt()会触发两次read()? - 不使用虚拟字符的情况下,如何让
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
相关产品推荐
相关产品推荐

