如果你不知道这篇文章在讨论什么,可以翻看上一篇博客,那里说明了我遇到的错误情况:
点此跳转至上一篇博客
可能有关的异常如下:
java.util.InputMismatchException
java.util.NoSuchElementException
在上篇博客中我并没有完全解决问题,只是归纳了一下可能的错误原因,结果收到了几个夸我的评论…实在惭愧,所以,在今天完成了完整的错误分析后,以此献给爱打破砂锅问到底的你。
如果只想看结论可以跳转至本文的这部分:“至此,终于到了分析的尾声”
文章目录
一万次执行也不会出错的代码
import java.util.Scanner;
public class Main{
public static void main(String[] args) {
Scanner scIn = new Scanner( System.in );
String s = new String( scIn.next() );
System.out.println(s);
scIn.close();
}
}
执行结果:
以上是一段你执行一万次也不会出错的代码,
我就不详细讲解了,如果你还不能理解,去补一下Java基础吧…
简化错误代码
要找出错误,就得简化代码,以下是对于上一篇博客的简化代码
import java.util.Scanner;
class Test{
String s;
public Test(){//构造方法,无参
Scanner in = new Scanner(System.in);//声明并实例化一个对象
this.s = in.next(); //对这个对象的属性进行赋值
in.close();
}
public String get(){
return this.s;
}
}
public class Main{
public static void main(String[] args) {
Test test1 = new Test();
System.out.println((test1.get()));
Test test2 = new Test();
System.out.println((test2.get()));
}
}
简单叙述一下以上代码的期望效果
把目光转向main方法,
我声明并实例化了一个Test类的对象,把它取名为“test1”,
在它实例化的过程中,也就是类的构造方法中,我用标准输入(键盘)给了它一个字符串,用来赋值给类的成员‘s’;
然后,我调用了一下类的get方法,输出成员‘s’,验证是否写入成功。
第二个实例化和验证代码如上,复制黏贴,改个变量名就行。
我期望的效果应该是:
我输入一行字符串:比如,qwer123
程序输出一行字符串:qwer123
我再输入一行字符串:比如,qwer123
程序再输出一行字符串:qwer123
实际运行结果
OK那我们运行一下看看:
第一个对象完成我期望的工作了,而轮到第二个对象时,抛给我一个异常:
java.util.NoSuchElementException
我先不解释为什么,我现在领着大家分析一下这个问题
毕竟如果你看到了这里,我猜你是想学习更多的知识,而不是解决一个异常。
现在定位出错的代码块
首先怀疑的是,Test类中,那个无参的构造方法:
public Test(){
Scanner in = new Scanner(System.in);
this.s = in.next();
in.close();
}
我们按正常逻辑理解一下他干了什么
声明并创建一个Scanner类的对象,
用这个对象做些事,
把这个对象中的一些流关闭(注意不是销毁对象!)
很自然的,还有如下推测:
它被嵌套在一个大括号里,那么它的生命周期出了大括号就结束了,创建下一个test2对象时,已经与test1无关了…
等等!第一个可能的错误出现了!
在Java中,虽然说它出了大括号就结束生命周期没错,但是! 结束生命就会马上被销毁吗?
这里就涉及到了Java垃圾回收的问题了,简单的说,一个对象变成垃圾后,这个垃圾什么时候被彻底清除销毁?这是由垃圾回收器GC决定的。
所以说,有可能是我们第一个Scanner对象还没被销毁就创建了第二个Scanner。
那么我们的问题是这个原因造成的吗?
很抱歉,不是,具体的证明代码我没写,童鞋们可以自己写个代码证明一下,我就暂时略过了。
好的,接下来我们怀疑的对象自然是构造方法中第一、三行代码了。
一个创建
一个关闭
仔细看看是怎么创建的?
Scanner(System.in)
是Scanner的构造方法,参数我们给的是System.in
OK现在我们看看这个in是什么类型的?
Java文档中说是:static InputStream
其实看到这里有经验的童鞋已经反应过来了,但奈何本人此前无经验,我就按我的思路继续写了,如果你还没头绪,那就继续跟我一起debug吧~
接着查看这个构造方法对应的源码:(我的IDE是VScode)
OK它又套了一层,实际调用的是有两个参数的某个构造方法
注意第一个参数它是怎么写的,它new了一个临时的InputStreamReader类的对象作为Scanner的构造方法的参数,
而在实例化InputStreamReader时的构造方法中,用的是“source”作为参数,也就是Scanner构造方法传进来的参数System.in
第二个有关知识点出现了:
在Java中是这样规定的:
如果函数的参数是一个类对象(有别于基础数据类型如int、double)那么其实传进函数的是这个参数的“引用”。那么对于这个参数的任何改动,会真实的改动原来你传进来的那个参数。(你可以理解为它传了一个C语言的指针)
OK现在大部分童鞋应该都能明白问题出在哪里了
但我还没结束,我们继续打开这个this()构造方法的代码
为什么还没有结束?
第一因为你现在作出的结论并没有足够的证据支撑,也就是说还没有证明完你的结论呢。
第二,以下还有更多的知识可以挖掘
我们继续打开这个this()构造方法的代码
打开后如下:(多余的代码我用三个点“…”代替了,只留下我认为的“关键代码”)
private Scanner(Readable source, Pattern pattern) {
...
this.source = source;
...
}
这里建议你自己跟着打开看看
现在我们可以合理猜测,Scanner对象也有一个成员属性名字叫做“source”
验证一下:
的确有
那么我们现在可以思考一下了
以下代码的意义或者问题是什么?
this.source = source;
第三个知识点出现了,赋值符号和引用的联系
先叙述一下:
Java规定,一般地,两个对象用赋值符号连接,如:
appleClass1 = appleClass2;
在满足类型匹配的前提下,这样做的后果是:第一个对象成了第二个对象的“引用”
或者形式化地说,第一个对象指向了第二个对象(是不是有点指针的味道?)
注意,以上讨论的前提是这两个是对象,不是基础数据类型(如int、double)
举个栗子:对于以下代码
Apple app = new Apple();
Apple bpp = app;
我们说:“app指向了一个新创建的Apple对象,新对象是使用无参构造方法创建的;接着又有一个Apple类的对象bpp指向了app,bpp是app的一个引用。”
那么我们就有如下结论:
Scanner的成员“source”现在引用了传进构造方法的“source”
OK,我们的证明马上就结束了,再坚持一下~
之前怀疑的第三行代码
在我们定义的Test类的构造方法中
有三行代码
如下:
public Test(){
Scanner in = new Scanner(System.in);
this.s = in.next();
in.close();
}
我们看看在close()方法中发生了什么?(同样的,多余的代码我都用省略号代替了,只留下关键代码)
如下:
public void close() {
...
((Closeable)source).close();
...
}
注意看这行代码,它对Scanner里的source成员进行了操作,怎么操作的?用的是close()方法。那么我们大胆推测:不管怎么样,close()一定对source做了修改。
至此,终于到了分析的尾声:
由之前的分析可知,在构造方法一行:
Scanner in = new Scanner(System.in);
我们用“System.in”作为参数传进构造方法,
而构造方法中又用Scanner 自己的成员引用了“System.in”,
最后当我们调用这一行:
in.close();
关闭"in"时,Scanner 也关闭了自己的成员“source”
顺带着,关闭了“System.in”,
所以,Test的构造方法只能使用一次,因为第二次使用时,“System.in”已经被关闭了,自然构造Scanner 会有异常抛出。
最后的牢骚
以上的分析其实多多少少的有疏漏之处,各位读者可以自行补充,我就不继续了。
其实这个问题至少一个月前我就断断续续的遇到了,查询网上后发现并没有一篇细致深入的分析文章有关于此,最后终于在一次大作业里遇到而且躲不开了。
网上有很多方法可以帮你解决这个问题,比如:
1.最简单的,不写close方法;
2.在主函数main里完成所有Scanner的工作,最后close关闭Scanner;
3.如果非要在函数中用Scanner读数据,可以把Scanner当作参数传入函数(是我临时想的一个办法)
等等等等
但是,这是解决了问题吗?
我觉得不是,这些办法只是帮你把程序跑出了你预期的结果,甚至更直白的说,只是绕过了一个异常。但是别忘了Java的异常有几十种,总有你绕不过的,总有你得亲自理解底层的问题。现在不解决问题,到时候问题暴露出来时,会连本带利地让你付出。
所以也是建议各位读者吧,学习编程不要怕折腾,反而要爱折腾,更要打破沙锅问到底。
用我非常喜欢的翁恺老师的话作为结尾吧
(原句我记不太清了了,大概意思如下)
“作为一个CS学生你要知道,计算机中不存在黑魔法,所有的硬件和软件都是人实现出来的。你要有一个自信,那就是别人能理解的,你也能理解;别人能做出来的,你也能做出来。”
致谢
在分析的过程中我受到了老师和很多同学的帮助和启发,在此由衷感谢你们的无私帮助。
另:转载问题
如果要转载我的任何文章到任何平台,私信告诉我一声就行,不用等我回复。