扫描仪应该只实例化一次吗?如果是这样,为什么会这样?

Should a Scanner only be instantiated only once? if that's the case why so?

我知道我要冒险了,但我似乎无法理解为什么我们不能只创建一个 Scanner class 的实例两次。我将添加一个示例以防万一。

import java.util.Scanner;

public class Nope
{
    public static void main(String[] args)
    {
        System.out.println("What's your name?");
        Scanner scanner = new Scanner(System.in);
        String name = scanner.nextLine();
        
        System.out.println("Welcome " + name + "!");
        scanner.close();
        
        // Now 
        System.out.println("where you do live?");
        Scanner sc = new Scanner(System.in);
        String country = sc.nextLine();
        
        System.out.println("That's a lovely place");
        sc.close();
        
    }
}

我收到一个运行时错误,看起来像这样

What's your name?
Kate
Welcome Kate!
Exception in thread "main" where you do live?
java.util.NoSuchElementException: No line found
    at java.base/java.util.Scanner.nextLine(Scanner.java:1651)
    at Nope.main(Nope.java:17)

我知道再次创建相同的新对象没有意义 class,鼓励冗余。但我只是觉得如果我知道为什么,我的头脑就会清醒,你不也是这样认为吗?

'java.util.NoSuchElementException: No line found' 机器是什么意思,人们说 Scanner 是不可克隆的。

PS:我特意关闭了我的第一个扫描仪并创建了一个新对象只是为了了解这个问题。

您应该只为每个输入流创建一个 Scanner 。除其他事项外,扫描仪会提前读取,因此会消耗比实际返回的更多的输入。 (就是这样知道的,比如输入是否hasNextInt()等等。)

如果您有多个输入流(例如处理多个文件),创建多个扫描器是完全明智的,但是System.in应该只使用一个扫描器。

这里实际上发生了两件不同的事情。

  1. 您应该为每个输入来源创建一个Scanner。例如,一个 Scanner 用于每个不同的输入文件,一个用于 System.in,一个用于每个不同的套接字输入流。

    原因是(正如 Chrylis 指出的那样)Scanner 的各种方法在扫描仪的输入源上提前读取。如果字符未被操作消耗,则它们不会放回输入源。相反,它们由 Scanner 缓冲,并保留以供下一个 Scanner 操作使用。因此,如果您有两个扫描器试图从同一个输入源读取数据,一个可能会窃取 用于另一个的输入。

    这就是 System.in 上打开多个 Scanner 对象不好的真正原因。不是您提出的“冗余”论点。一点冗余从根本上没有错......特别是如果它简化了应用程序。但是竞争输入的扫描器 可能 导致意外行为/错误。

  2. 第二个问题是当你close()一个Scanner也会关闭输入源。

    在您的情况下,这意味着您要关闭 System.in。然后您正在创建第二个 Scanner 以从(现已关闭)System.in.

    读取

    当您尝试 Scanner 从已关闭的 System.in 读取时,会导致 NoSuchElementException.

因此,如果您没有在第一个 Scanner 上调用 close(),您的代码 可能 有效,但这取决于您在第一个 Scanner.

上进行的操作

People are saying Scanner ain't cloneable.

他们是正确的。

很简单,您只需为每个输入创建一个扫描仪。 Scanner 使用 nextLine() 方法逐行读取。最后检查条件 hasNext() 以便找出参数。

试试这个

import java.util.Scanner;

public class Nope{
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("What's your name?");
        String name = scanner.nextLine();

        System.out.println("Welcome " + name + "!");
        // Now
        System.out.println("where you do live?");
        String country = scanner.nextLine();

        System.out.println(country +" Is a lovely place");
        if(!scanner.hasNext()){
        scanner.close();
        }
    }
}

输出像


What's your name?
xyz
Welcome xyz!
where you do live?
rjk
rjk Is a lovely place

此答案主要针对 close() 操作以及为什么没有从 重新读取的选项System.in 如果 Scanner 实例之前关闭了它,因为最佳答案已经给出了正确的信息。就是好奇。


Scanner

When a Scanner is closed, it will close its input source if the source implements the Closeable interface. A Scanner is not safe for multithreaded use without external synchronization.

  • 您应该为每个要读取的源创建一个 Scanner 实例。
  • 如果你必须共享同一个实例,你应该实现同步机制,因为它不是线程安全的。
  • 正如其他答案已经指出的那样,close() 是一个“危险”操作。

System.in close()

假设 System.in 被指定为来源。

这是来自 InputStreamReader

的关闭方法
public void close() throws IOException
{
    synchronized (lock)
    {
       // Makes sure all intermediate data is released by the decoder.
       if (decoder != null)
          decoder.reset();
       if (in != null)
          in.close();
       in = null;
       isDone = true;
       decoder = null;
     }
}

引用System.in的变量就是in.

在这个InputStream上执行了两次操作除了null检查):


1.in.close()

This does nothing at all: System.in 的 class (InputStream) 只留下继承的 close() 方法的空实现(来自 Closeable )

/**
 * Closes this stream. Concrete implementations of this class should free
 * any resources during close. This implementation does nothing.
 *
 */
public void close() throws IOException {
    /* empty */
}

javadocs都没有 隐瞒真相:

The close method of InputStream does nothing.


2.in = null

这就是您无法再次阅读System.in的真正原因。将其设置为 null 将无法使用新的 Scanner.

从此源进行任何进一步的读取尝试

但是...为什么它会抛出 NoSuchElementException 而不是 NullPointerException

Scanner 的初始化不会 在创建其 Reader 实例的步骤中失败.这是因为 InputStream 被包装成一个新的 BufferedInputStream。因此 lock 对象在 Scanner 的 Reader:

初始化时不为空
public InputStreamReader(InputStream in) 
{
    super(in);  
    this.in = in;
    ...
}

.

protected Reader(Object lock) 
{
    if (lock == null) {    
        throw new NullPointerException();   
    }
    this.lock = lock;
}

您将能够从 System.in 创建第二个 Scanner 实例,而不会抛出任何异常;由于 InputStream 被包装到一个新的 BufferedInputStream 实例中,因此 lock 对象不为空并通过过滤器。但是里面的InputStreamSystem.in,确实是null,从之前的close()操作中设置为null的那一刻起:

这是System.inScanner第二次初始化中的lock对象。 Scanner 仍然不知道会出现什么问题,因为它的初始化是成功的( 由于包装的 BufferedInputStream)并且仍然相信它的 InputStream 是有效的。

但是在第一次尝试从 System.in 再次读取时,会发生这种情况:

public String nextLine() {
    if (hasNextPattern == linePattern())
        return getCachedResult();
    clearCaches();

    String result = findWithinHorizon(linePattern, 0);
    if (result == null) /* result is null, as there's no source */
       throw new NoSuchElementException("No line found");

     (...)
 }

那是 Scanner 注意到 最后 事情进展不顺利 的时刻。 findWithinHorizon 的结果将 return 为空,因为没有从哪里可以找到的来源。

由于先前在 close() 操作中将 System.in 设置为 null,因此在尝试从第二个 Scanner实例:NoSuchElementException.

Scanner 实现 AutoCloseable 接口
不要单独调用 close 而是使用 autocloseable mechanism

public class Nope
{
  public static void main(String[] args)
  {
    System.out.println("What's your name?");
    try( Scanner scanner = new Scanner(System.in) ) {
      String name = scanner.nextLine();

      System.out.println("Welcome " + name + "!");

      // Now 
      System.out.println("where you do live?");
      try( Scanner sc = new Scanner(System.in) ) {
        String country = sc.nextLine();

        System.out.println("That's a lovely place");
      }
    }
  }
}

现在一切都按预期工作,并且 close 在退出 try-with-resources 块时自动调用…