最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

java scanner weird double read() call while using custom inputStream that has modifiable buffer - Stack Overflow

programmeradmin1浏览0评论

Why does Java's Scanner make double calls to read() and how can I manage custom input for game simulations?


I'm trying to simulate Codingame's way of managing game input, which typically looks like this:

Scanner in = new Scanner(System.in);
int width = in.nextInt(); // columns in the game grid
int height = in.nextInt(); // rows in the game grid

// Game loop  
while (true) {  
    int entityCount = in.nextInt();  
    // Process more input  
}

To achieve this, I created a custom InputStream. The problem is that Scanner seems to make double calls to the read() method for a single call to nextInt().

Issue

  1. If I return -1 twice in succession during the read() calls, the Scanner treats the stream as closed, causing it to throw a NoSuchElementException.
  2. To handle this, I found that returning a dummy space character (' ') after -1 prevents the stream from being marked as closed.
  3. Additionally, I want Scanner to wait for the next call to nextInt() before reading more data from the stream.

Here's my current solution:

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 {
        // Stop reading until the next call to 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;

            // Prevent Scanner from closing the stream
            setBuffer(" ");
            return -1;
        }
    }
}

Questions

  1. Why does Scanner call read() twice for a single nextInt() call?
  2. What's the proper way to make Scanner wait for new data when calling nextInt() without resorting to dummy characters (' ')?

simple code showing the problem

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;
        }
    }
}

usage

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());

out put

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

I hope I explained my issue clearly. Any insights would be greatly appreciated!

Why does Java's Scanner make double calls to read() and how can I manage custom input for game simulations?


I'm trying to simulate Codingame's way of managing game input, which typically looks like this:

Scanner in = new Scanner(System.in);
int width = in.nextInt(); // columns in the game grid
int height = in.nextInt(); // rows in the game grid

// Game loop  
while (true) {  
    int entityCount = in.nextInt();  
    // Process more input  
}

To achieve this, I created a custom InputStream. The problem is that Scanner seems to make double calls to the read() method for a single call to nextInt().

Issue

  1. If I return -1 twice in succession during the read() calls, the Scanner treats the stream as closed, causing it to throw a NoSuchElementException.
  2. To handle this, I found that returning a dummy space character (' ') after -1 prevents the stream from being marked as closed.
  3. Additionally, I want Scanner to wait for the next call to nextInt() before reading more data from the stream.

Here's my current solution:

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 {
        // Stop reading until the next call to 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;

            // Prevent Scanner from closing the stream
            setBuffer(" ");
            return -1;
        }
    }
}

Questions

  1. Why does Scanner call read() twice for a single nextInt() call?
  2. What's the proper way to make Scanner wait for new data when calling nextInt() without resorting to dummy characters (' ')?

simple code showing the problem

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;
        }
    }
}

usage

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());

out put

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

I hope I explained my issue clearly. Any insights would be greatly appreciated!

Share Improve this question edited Feb 2 at 2:57 Ahmed Mazher asked Feb 2 at 0:16 Ahmed MazherAhmed Mazher 3333 silver badges7 bronze badges 3
  • Have you tried running your code in a debugger? If your IDE has a copy of the source code for your version of Java, you could trace the code into java.util.Scanner. You might improve the question by editing to include what you learned from debugging. – Old Dog Programmer Commented Feb 2 at 0:37
  • 1 More importantly, please edit the question to include a minimal reproducible example. As it is now, I don't know how you know there is a double read. Your minimal reproducible example should demonstrate that it does. – Old Dog Programmer Commented Feb 2 at 0:41
  • BTW standard Java comes with a ByteArrayInputStream, or a StringReader - both can be used with Scanner... but Scanner itself has a constructor for reading from a String (e.g. Scanner in = new Scanner("first turn finish_");) – user85421 Commented Feb 2 at 10:35
Add a comment  | 

2 Answers 2

Reset to default 2
  1. Why does Scanner call read() twice for a single nextInt() call?

Because it obviously has to. Imagine we have a user typing at a keyboard. They type '1', and that's all they type.

You call .nextInt() on this stream, and the scanner dutifully calls read() which returns the unicode value for '1'.

Is that the number? 1 is a number. But wait! Next, the user types 5! Oh dear, they actually meant to type 15.

Hence why scanner has to keep reading. nextInt() does not mean 'read a single digit'. It means 'read a single number' and it can consist of multiple digits. Hence, scanner must keep reading; it either [A] finds a delimiter (e.g. after the '1', the user types a space or enter), or [B] end of stream.

Either way, once scanner sees that it can return an actual int value.

  1. What's the proper way to make Scanner wait for new data when calling nextInt() without resorting to dummy characters (' ')?

By not responding at all. Let's go back to that user behind the keyboard.

We have your java program that has:

Scanner s = new Scanner(System.in);
// `.useDelimiter` is not called, so the delimiter is
// the default one, which is `\\s+`, i.e. 1 or more whitespace.

s.nextInt();

The nextInt() will immediately call .read() on the thing scanner wrapped (so, System.in), and this blocks. That read() call does not return at all, it freezes your java program. Only when you type an actual '1' does read() unfreeze to return 49 (the unicode value for the character '1'). scanner reads it, and immediately calls read() because it cannot return '1' (maybe you're going to type a 7 next, after all), and then that read() call blocks. You then type 7, so 7 goes in, and scanner bookkeeps 'we've got 17 so far', and then finally you hit space, and scanner can now return and is not going to call read() again until some code of yours calls one of the nextX() methods of scanner.

At no point does anything go into an endless loop. I have the feeling you think this is happening inside .nextInt():

// THIS IS NOT HOW IT WORKS!

int out = 0;
while (true) {
  // 'in' = System.in here.
  int v = in.read();

  // If no user input is available, keep waiting.
  if (v == -1) continue;

  // Did we hit the delimiter? Then return the number.
  if (Character.isWhitespace(v)) return out;

  // 'stuff' the new digit into our output and go back
  // to asking for more keystrokes.
  int digit = v - '0';
  out = (out * 10) + digit;
}

If it actually worked like that, your computer's fans would spin up and the CPU load goes t 100%. while (true) { continue; } makes things become very very hot and sluggish, you don't wanna do that. Instead, in.read(); freezes the thread, until you actually type something.

So that's how you make scanner wait for more input. By not returning at all inside your implementation of read(), until you actually have stuff to return.

But how do I wait?

It's not exactly trivial to update your code to do this. You'll need to involve threads. Most likely you want your GameStream to be a separate thread, which brings in the considerable mental load of knowing how to 'safely' manage them. Basically, if 2 threads interact with the same field, your code is broken in 'evil coin' ways (the JVM flips an evil coin and chooses whether the write to the field done in thread A is actually propagated to the reads done in thread B. The coin is evil in that it isn't random; it does that all day today and then tomorrow all of a sudden it will not. The solution is to tell the JVM not to flip the coin at all; you do this by establishing Happens Before relationships; this is all rather tricky!).

It's solvable of course, just, you need to do some reading. such as Java Concurrency in Practice. Or if you want to leave that for later, make sure all interaction with that buffer class is protected with synchronized or, better yet, with something appropriate from the java.util.concurrent package.

If you just want to keep sending new data to a Scanner after its creation, consider using a PipedReader/PipedWriter pair.

var writer = new PipedWriter();
var reader = new PipedReader(os);
writer.write("first turn finish_ "); // remember the delimiter at the end!
Scanner in = new Scanner(reader);
System.out.println(in.next() + " " + in.next() + " " + in.next());
writer.write("next turn_ ");
System.out.println(in.next()+ " " + in.next());

You should include delimiters (strings that match the delimiter() pattern, e.g. a space) at the end of the inputs, so that the scanner knows that that is a complete token, or else it will keep trying to read more data and cause a deadlock (the thread is blocked while the scanner tries to read, and you cannot write more data unless you are on a different thread). Of course, the same thing happens if you try to call next when all the currently written data has been read already.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论