一个Scanner对象应该只被实例化一次吗?如果是的话,为什么?

5

我知道我冒了风险,但我似乎无法理解为什么我们不能只是创建两个Scanner类的实例。 为了防范万一,我会举个例子。

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)

我知道再创建一个同类的新对象没有意义,这会增加冗余。但是,如果我知道为什么这样做,我的理解会更加清晰,你不也这么认为吗?

机器所说的“java.util.NoSuchElementException: No line found”是什么意思?人们说Scanner无法克隆。

PS:我故意关闭了第一个scanner并创建了一个新对象,只是为了理解这个问题。


虽然我不知道有哪个应用程序需要同时使用2个扫描仪,但它确实可以工作。 您示例中的问题在于,在使用第二个扫描仪之前关闭了第一个扫描仪。您需要级联第二个扫描仪。特别是,这意味着必须按创建顺序的相反顺序关闭所有扫描仪。使用自动关闭机制是最安全的方法。请参见页面下方的示例... - Kaplan
5个回答

11

实际上这里有两件不同的事情。

  1. 你应该为每个输入创建一个Scanner。例如,每个不同的输入文件都需要一个ScannerSystem.in需要一个,每个不同的套接字输入流也需要一个。

    原因是(正如Chrylis所指出的那样),Scanner的各种方法会在扫描器的输入源上进行预读取。当字符没有被操作消耗时,它们不会被放回到输入源中。相反,它们由Scanner本身缓冲,并保留供下一个Scanner操作使用。因此,如果您有两个尝试从同一输入源读取的Scanner实例,其中一个可能会“窃取”另一个的输入。

    这就是为什么在System.in上打开多个Scanner对象很糟糕的真正原因。而不是您提出的“冗余”论点。有一点冗余并没有什么根本性的错误...尤其是如果它简化了应用程序。但是竞争输入的扫描器可能会导致意外行为/bug。

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

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

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

因此,如果您没有在第一个 Scanner 上调用 close(),则您的代码可能会工作,但这取决于您对第一个 Scanner 进行的操作顺序。


人们说 Scanner 无法克隆。

他们是正确的。


4
这篇答案主要关注于close()操作,以及为什么在Scanner实例之前关闭System.in后就没有再次从中读取的选项,正如顶部答案已经给出了正确的信息。只是出于好奇。

扫描仪

当扫描仪关闭时,如果源实现了Closeable接口,它将关闭其输入源。未经外部同步,扫描器不适用于多线程使用。

  • 您应该为每个要读取的源创建一个扫描器实例。
  • 如果必须共享相同的实例,则应实现同步机制,因为它不是线程安全的。
  • 正如其他答案已经指出的那样,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 上执行了两个操作除了空值检查之外):


1. in.close()

这个方法实际上什么也不做System.in 的类 (InputStream) 只是继承了 Closeable 中的close() 方法并留下了一个空的实现。

/**
 * 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也掩盖不了事实真相

InputStream的close方法什么也不做


2. in = null

这就是为什么你无法再次从System.in读取的真正原因。将其设置为null会阻止使用新的Scanner尝试从该源读取。


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

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

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对象不为空并通过了过滤器。但是内部的InputStream,即System.in,确实为null,因为在前一个close()操作中将其设置为null:

enter image description here

这是第二次初始化 Scanner 时使用的 lock 对象,用于处理 System.in。由于被包装的 BufferedInputStream 的缘故,Scanner 的初始化成功了,但它并不知道接下来会发生什么,仍然认为它的 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的结果将返回null。
由于在close()操作中之前将System.in设置为null,因此尝试从第二个Scanner实例读取时会出现错误:NoSuchElementException

3

每个输入流只应该创建一个Scanner。除其他事项外,扫描器会预读并消耗比实际返回的更多的输入。 (例如,这就是它如何知道输入是否有hasNextInt()等)

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


1

Scanner 实现了 AutoCloseable 接口
不要单独调用close,而是使用自动关闭机制

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

现在一切都按预期运行,当退出 try-with-resources 块时,close 会自动调用...

0

这很简单,您只需要为每个输入创建一个Scanner。 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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接