Java 中关于 Scanner.close 调用过程中程序抛出 java.util.NoSuchElementException 问题

问题演示

  • 执行演示代码
import java.util.Scanner;

public class Test {
    public static void main(String [] args) {
        int a;
        Scanner sc1 = new Scanner(System.in); // 1 次实例化

        a = sc1.nextInt(); // 1 次调用
        System.out.println("sc1:" + a);
        sc1.close(); // 1 次关闭

        transferFun(); // 此处调用测试函数
    }

    public static void transferFun() {
        int a;
        Scanner sc2 = new Scanner(System.in); // 2 次实例化

        a = sc2.nextInt(); // 2 次调用
        System.out.println("sc2:" + a);
        sc2.close(); // 2 次关闭
    }
}
  • 直接运行,查看结果
1
sc1:1
Exception in thread "main" java.util.NoSuchElementException
	at java.util.Scanner.throwFor(Scanner.java:862)
	at java.util.Scanner.next(Scanner.java:1485)
	at java.util.Scanner.nextInt(Scanner.java:2117)
	at java.util.Scanner.nextInt(Scanner.java:2076)
	at Test.transferFun(Test.java:19)
	at Test.main(Test.java:12)

Process finished with exit code 1
  • 程序报错,抛出 java.util.NoSuchElementException

通过帮助文档查阅该异常:

public class NoSuchElementException extends RuntimeException 
被各种访问器方法抛出,表示被请求的元素不存在

疑问

1. 为什么会发生“被请求的元素不存在”的异常呢 ?

2. 根据程序错误信息可以知道,抛出异常的原因是由于执行 sc1.close 之后进入 transferFun 方法中调用 sc2.nextInt 时导致的,可是 sc1 和 sc2 显然是两个相互独立的 Scanner 对象,为什么关闭 sc1 后却会对 sc2 的调用产生影响呢 ?


类解析

在回答上述疑问之前,我们有必要对一些相关的类进行重新认识:

Scanner sc = new Scanner(System.in)

这句代码很熟悉,通过 Scanner 类实例化一个 Scanner 对象,可是 System.in 又是个啥 ?

通过帮助文档查阅 System 类:

public final class System extends Object
System 类包含几个有用的类字段和方法,它不能被实例化
System 类提供的 System 包括标准输入,标准输出和错误输出流
public static final InputStream in 
“ 标准 ” 输入流
该流已经打开,准备提供输入数据
通常,该流对应于键盘输入或由主机环境或用户指定的另一个输入源

清楚了,in 是一个 static final,即静态常量,同时,它也是一个 InputStream 对象,那么 System.in 就是一个标准输入流对象

public abstract class InputStream extends Object implements Closeable 
这个抽象类是表示输入字节流的所有类的超类

顺带查阅相关的 InputStreamReader 类的帮助文档:

public class InputStreamReader extends Reader
InputStreamReader 是从字节流到字符流的桥

为了最大的效率,请考虑在 BufferedReader 中包装一个 InputStreamReader
eg.BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); 

联系 Java 中的特殊操作流,参考以上帮助文档,可以知道:System.in 其实是一个原始的,简陋的标准输入流对象,它相对于底层实现,通常不被允许直接用于读取用户输入的数据;而 Java 程序在实例化 Scanner 对象时通过传递 System.in 对象,使得通过 Scanner 类能够间接地实现读取用户输入的数据的操作

再通过帮助文档查阅 Scanner 类:

public final class Scanner extends Object implements Iterator<String>, Closeable
一个简单的文本扫描器,可以使用正则表达式解析原始类型和字符串

当一个 Scanner 关闭时,如果源实现了 Closeable 接口,它将关闭其输入源
A Scanner 对于无需外部同步的多线程使用是不安全的

根据上述帮助文档,可以认为:

1. Scanner 类对 System.in 对象进行了向上的包装

2. 由于 Scanner 对异步多线程是不安全的,所以不难认为,Scanner 所控制的系统资源是属于全局资源,是非线程私有的


源码剖析

  • Scanner 构造方法
public Scanner(InputStream source) {
    this(new InputStreamReader(source), WHITESPACE_PATTERN);
}

从 Scanner 的构造方法中发现,它向下调用了 InputStreamReader 类对传入的 System.in 进行实例化

  • InputStreamReader 类相关代码
private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
    super(in);
    try {
        sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
    } catch (UnsupportedEncodingException e) {
        // The default encoding should always be available
        throw new Error(e);
    }
}

这里出现了一个 StreamDecoder 类型的 sd 对象,它也是一个常量,跟上面提及过的 InputStream 类型的 in 对象都是不可变的

private Readable source;

通过后续对源码的查阅能够发现,其实 Scanner 类就是通过多层传递,间接地将传入的 InputStream 类型的 System.in 包装成为 InputStreamReader 类型,并将其最终包装为一个 Readable 接口 source

  • Scanner.close 方法
public void close() {
    if (closed)
        return;
    if (source instanceof Closeable) {
        try {
            ((Closeable)source).close();
        } catch (IOException ioe) {
            lastException = ioe;
        }
    }
    sourceClosed = true;
    source = null;
    closed = true;
}
  • InputStreamReader.close 方法
public void close() throws IOException {
    sd.close();
}

显然,一旦调用 Scanner.close 方法,外层包装的 source 接口将直接调用其内层包装的 sd.close,从而导致 InputStreamReader 引发关闭流并释放与之相关联的任何系统资源


疑问解答

1. 之所以会抛出 NoSuchElementException 异常,是因为 Java 程序所请求的 IO 流资源已经被释放,故被请求的元素不存在

2. 虽然 sc1 与 sc2 是两个相互独立的 Scanner 对象,但其引用的都是同一个 InputStream 标准输入流,当 sc1.close 被执行之后,流被关闭,这时即使仍存在其它多个未关闭的 Scanner 对象,若再次对它们进行调用,都有可能引发异常


解决方法

  • 通过参数传递
import java.util.Scanner;

public class Test {
    public static void main(String [] args) {
        int a;
        Scanner sc = new Scanner(System.in); // 1 次实例化

        a = sc.nextInt(); // 1 次调用
        System.out.println("sc1 : " + a);

       transferFun(sc); // 此处调用测试函数
        sc.close(); // 1 次关闭
    }

    public static void transferFun(Scanner sc) {
        int a;

        a = sc.nextInt(); // 2 次调用
        System.out.println("sc2 : " + a);
    }
}
  • 通过重写 InputStreamReader.close 方法
import java.io.InputStreamReader;
import java.util.Scanner;

public class Test {
    public static void main(String [] args) {
        int a;
        Scanner sc = new Scanner(new InputStreamReader(System.in){
            @Override
            public void close() { /* 不做任何执行 */ }
        }); // 1 次实例化(重写 close 方法的 Scanner 对象)

        a = sc.nextInt(); // 1 次调用
        System.out.println("sc1 : " + a);
        sc.close(); // 1 次关闭

       transferFun(); // 此处调用测试函数
    }

    public static void transferFun() {
        int a;
        Scanner sc = new Scanner(System.in); // 2 次实例化

        a = sc.nextInt(); // 2 次调用
        System.out.println("sc2 : " + a);
        sc.close(); // 2 次关闭
    }
}

总结

一般地,在启用 IO 流之后,为了避免其一直占用虚拟机内存,我们需要人为地去调用 close 方法关闭流,但流只能被启用和关闭一次,所以在需要多处调用标准输入流的项目中,需要谨慎地对程序进行优化

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值