Andrei
Andrei

Reputation: 7607

Scanner continuous loop

Why isn't my inputScanner blocking after the first input? It goes into a continous loop. Ignore other details of this code.

public class Test {
    public static void main(String[] args) {

        boolean finished;

        do {
            Scanner inputScanner = new Scanner(System.in);
            finished = inputScanner.hasNext("exit");
            boolean validNumber = inputScanner.hasNextDouble();
            if (validNumber) {
                double number = inputScanner.nextDouble();

                System.out.print(number);
            } else if (!finished) {
                System.out.println("Please try again.");
            }
            inputScanner.close();
        } while (!finished);
    }
}

EDIT: On a previous post which was related to this, it was mentioned that "So if you are going use System.in later don't close it (if it is closed, we can't reopen it and read any data from it, hence exception)". Why is this happening?

Upvotes: 1

Views: 2332

Answers (2)

Pshemo
Pshemo

Reputation: 124235

If you take a look at documentation of Scanners methods like hasNext(String) or hasNextDouble you will see that it

Throws:
IllegalStateException - if this scanner is closed

(emphasis mine)

So to throw IllegalStateException you first need to close Scanner, not stream from which it is reading data.

So lets take a look at this example:

Scanner sc = new Scanner(System.in);

System.out.println("type something:");
System.out.println(sc.hasNext());// true
System.out.println("your data: "+ sc.nextLine());

sc.close();

System.out.println("type something:");
System.out.println(sc.hasNext());// throws java.lang.IllegalStateException: Scanner closed

Last line throws IllegalStateException because you are invoking hasNext method on closed scanner (so we know that after invoking sc.close() stream from which it reads must be also closed so we can safely assume that there are no more elements to read, or since stream was closed we may not be allowed to read it).

Now if we don't close scanner but close System.in we will still be able to use this instance of scanner without getting exceptions. So lets simply change sc.close(); to System.in.close() (I will skip exceptions handling for simplicity):

Scanner sc = new Scanner(System.in);

System.out.println("type something:");
System.out.println(sc.hasNext());// true
System.out.println("your data: "+ sc.nextLine());

System.in.close();

System.out.println("type something:");
System.out.println(sc.hasNext());// false

As you can see there is no exception here because it wasn't scanner which was closed, but stream which scanner which was being read.

Why closing System.in doesn't cause scanner to throw exception?

I suspect that decision to not throw exception here was made with assumption that exception symbolize problem with code. If programmer allowed scanner to being closed he should also make sure that this particular closed instance of scanner will not be used anywhere.
Now returning false instead of throwing exception is normal reaction where there is no more elements to read. So if stream which scanner was reading was closed naturally (like when we read text file and read its last line so there is nothing more to read) scanner handles this situation like something normal (so there is no need to point that this is some exceptional situation).

Now in your loop you are kind of combining these two scenarios. Your code can be simplified to something like:

Scanner sc = new Scanner(System.in);

System.out.println("type something:");
System.out.println(sc.hasNext());// true
System.out.println("your data: "+ sc.nextLine());

System.in.close();
sc = new Scanner(System.in);//IMPORTANT

System.out.println("type something:");
System.out.println(sc.hasNext());// false

As you see in line

sc = new Scanner(System.in);//IMPORTANT

you are creating new instance of scanner which wasn't closed yet, so its hasXYZ methods always returns false because System.in can't provide no more values.

Additional trap

One problem which I didn't mentioned earlier is fact that in case of wrong input, which is neither "exit" nor double if you are are not consuming that invalid cached value from scanner by using any of nextXZY methods like hasNext("exit") or hasNextDouble will be still based on that invalid data, like:

Scanner sc = new Scanner("foo 1");
System.out.println(sc.hasNextInt());//false because `foo` is not integer
System.out.println(sc.hasNextInt());//also false because we are still 
                                    //reading `foo` which is not integer
String str = sc.next();//lets read (sonsume) foo
System.out.println(sc.hasNextInt());//true since 1 is integer

Solution

Simplest solution to such problem is creating only one instance of Scanner which will handle System.in and reuse it in your entire application. Then at the end of your application you can decide to close your scanner or System.in.

So your code can look like:

boolean finished;
Scanner inputScanner = new Scanner(System.in);
do {
    finished = inputScanner.hasNext("exit");
    boolean validNumber = inputScanner.hasNextDouble();
    if (validNumber) {
        double number = inputScanner.nextDouble();

        System.out.print(number);
    } else if (!finished) {
        System.out.println("Please try again.");
        inputScanner.next();// lets not forget to consume from scanner cached
                            // invalid data which is neither double or "exit"
    }
} while (!finished);
inputScanner.close();

Upvotes: 2

Frakcool
Frakcool

Reputation: 11153

Why isn't my inputScanner blocking after the first input?

Because you're creating a new Scanner each time you enter the loop, so it's not the same object on 1st iteration than in 2nd and further iterations

public class Test {
    public static void main(String[] args) {

        boolean finished;

        do {
            Scanner inputScanner = new Scanner(System.in); //Here you're making a new instance of inputScanner each time you come to this line after the do-while loop ends.
            finished = inputScanner.hasNext("exit");
            boolean validNumber = inputScanner.hasNextDouble();
            if (validNumber) {
                double number = inputScanner.nextDouble();

                System.out.print(number);
            } else if (!finished) {
                System.out.println("Please try again.");
            }
            inputScanner.close();
        } while (!finished);
    }
}

If you want it to be "blocked" or "closed", then move this line before the do { line.

Scanner inputScanner = new Scanner(System.in);

For your second question:

So if you are going use System.in later don't close it (if it is closed, we can't reopen it and read any data from it, hence exception)

From Oracle's docs:

"Attempting to perform search operations after a scanner has been closed will result in an IllegalStateException"

It's like trying to make a dead person to do something, it would be like a zombie! (And Java hates zombies!) D:

But you're not getting that IllegalStateException because as I said on the answer for your 1st question, you're making a new object each time you go into the do-while loop.

Edit

Why can't you reopen it? Also from Oracle's docs:

When a Scanner is closed, it will close its input source if the source implements the Closeable interface.

Thus inputScanner.close() closes System.in.

And because of the general contract for OutputStream's close (with the help of this answer):

public void close() throws IOException --> Closes this input stream and releases any system resources associated with this stream. The general contract of close is that it closes the input stream. A closed stream cannot perform input operations and cannot be reopened.

Upvotes: 4

Related Questions