完全剖析:Scanner.close(),java.util.NoSuchElementException,InputStream,java.util.NoSuchElementException

如果你不知道这篇文章在讨论什么,可以翻看上一篇博客,那里说明了我遇到的错误情况:
点此跳转至上一篇博客

可能有关的异常如下:
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学生你要知道,计算机中不存在黑魔法,所有的硬件和软件都是人实现出来的。你要有一个自信,那就是别人能理解的,你也能理解;别人能做出来的,你也能做出来。”

致谢

在分析的过程中我受到了老师和很多同学的帮助和启发,在此由衷感谢你们的无私帮助。

另:转载问题

如果要转载我的任何文章到任何平台,私信告诉我一声就行,不用等我回复。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值