问题演示
- 执行演示代码
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 方法关闭流,但流只能被启用和关闭一次,所以在需要多处调用标准输入流的项目中,需要谨慎地对程序进行优化