一个关于unmodifiableList()方法,可变/不可变类型和引用的小实验
由于复习时间比较紧张,同时前几天才刚学完设计模式(后来才知道这章才是和lab3关联最大的一章。。。),所以最近暂时搁置了正在写的Lab3和先复习完6,7,9的计划,开始从头进行系统复习,在昨天复习到可变/不可变类型的时候,有一个地方引起了我的注意:

在这里,不添加第四行代码的情况下运行结果是2,按照代码来看,使用unmodifiableList方法包装List后,返回不可变类型对象listCopy,如果这个listCopy是指向一个新的对象,那么对list改变应该不会造成ListCopy的改变,如果想要得到结果为2,只能说明:1.包装完成的listCopy还是指向了原本的list,2.指向同一对象时,改变原来的对象其实也是改变这个对象(java的引用在某种意义上可能就是指针的用法),因此对list的改变才会造成ListCopy的改变。之后我编写简单的代码对这两个想法进行了验证:
Step 1:
首先,复现一下图片中的代码,看看结果是否确实如此:

运行结果:

结果没错,这样一来,只能如猜想中解释为使用unmodifiableList方法返回的对象ListCopy还是指向了原来的List,只不过ListCopy经过了加工,不能更改。因为直接更改ListCopy会报错。
Step 2
先暂时搁置这个方法,进行第二个验证,也是我在继续复习类型这一章时产生了另一个疑问,在涉及到引用时,原对象的改变会直接改变另一个引用它的对象吗(其实就是验证一下java引用与指针是否是类似的),于是我编写了如下的测试函数,顺便在其中检验了可变类型与不可变类型的区别:

在这里,原对象ar,后面的arTest直接引用了ar。
如果将它们简单看作像c语言中的变量的话,那么在代码第三行ar被赋给arTest之后,arTest中的值就是1,之后ar的改变不会影响到arTest,arTest的改变也不会影响到ar,但在java中事实并不是这样,这里我对程序进行了两种修改,第一种是像上图代码中的注释一样,第四行对ar添加元素2,然后打印arTest的元素,一种是如上图,修改artest,打印ar,这两种实现的结果输出都是一样的:

事实证明,java的这种对象本质来看更像是一种指针,会根据它指向的对象的改变而改变。而不像是c语言中的两个彼此无关的变量,比如a = 3;b = a;之后改变a的值也对b=3没有任何影响。
同时,这个小测试还包含了对List的mutable性质的测试,ar和arTest指向同一个对象,但是arTest作为一个mutable类型List,它的改变会直接造成指向的对象的改变。
如果这段代码更改的是一个不可变类型String,会有怎样的结果呢?我又进行了测试:

输出:

对代码中的数据类型进行改变后,同样是arTest引用了ar,这次ar的改变却没能造成arTest的改变。这是由于String作为一个immutable对象,它的改变实际上是产生一个新的对象,然后指向这个新的对象。第三行代码中的ar = ar + “b”,实际上此ar已非彼ar,这行代码之后的ar指向了“ab”,但这个“ab”是一个新的对象,不再是原来那个“a”了。
原来ar和arTest都指向“a”,现在arTest依旧指向“a”,所以它最后也输出了a,ar却指向了“ab”,这里我将最后一行改成打印ar,结果就是ab了。这一切都是因为String是一个不可变类型,不可变类型的“改变”实际上只是扔掉了旧的,并没有改变它,然后换了一个新的。
由此,两个想法都得到了验证。
Step 3
回到unmodifiable方法。它并不是单纯的引用,但是返回的对象却指向了原来的对象,而一般像是new ArrayList(xx)方法,却不是对xx的引用,而是返回了一个新的对象,比如我将代码修改如下:

就是将一开始的引用改为了以ar为参数new一个List,这样得到的是新的对象,因为这段代码输出结果为1(ar的改变没有对arTest产生影响)。
那么我在想unmodifiable是怎样实现的,返回一个不能被更改的list,却实际上还是指向了原来的对象。我上网查找了这个方法的源代码,发现它是List接口的一个静态实现类,里面有一个final修饰的List,在构造器中这个List直接引用传入的参数List(实际上和参数List还是指向同一个对象),之后在接口方法中,实现了get方法,其余List接口中的方法实现都是直接抛出错误。即相当于对原本的list进行了修饰,只允许读数据,不允许修改数据。
由此,所有困惑都基本解决了,由于构造器中内部的list是直接引用外部的list,所以可以直接改变它(同时指向的是同一个对象),对于一些不可变封装类型,比如Integer,上网查询源码得它的构造器中内容为this.value = value,即使用int类型的值来初始化Integer内部的变量value。由于int本身就是不可变类型,那么这种引用自然也不会存在原来的int的值影响Integer的值的情况。
总结:
虽然这个小实验要验证的东西在java里可能是个十分基础的概念(因为这门课默认有一些java基础,而且本质上是对java的应用而非对语言的学习,所以课程内容中好像是没有提到这些基础中的基础hhh,而我的java也没有进行系统学习,直接跟着软构课进行应用了,确实有很多细节的部分以前没有注意过,主要是这些地方老师上课都说过,当时听了也能理解,但是重复听第二遍的时候却感觉到这些变来变去的引用和新的对象让人有点云里雾里,于是写了点小测试,但其实也只是对原来的理解进行验证),不过具体写代码验证一下这些性质也算是一个有意思的过程,折腾半天可能也不能算是完全的白折腾
从中其实也可以看出软构这门课中为什么总是强调表示泄露这类问题(其实上面的unmodifiable也不是完全安全的,比如说如果用户得到了原来的List,直接通过修改原来的list就可以避过不能修改的listCopy,对listCopy直接造成改变),所以说,如果能使用不可变类型,最好还是直接使用不可变类型,就算进行改变,也不用担心互相影响。

浙公网安备 33010602011771号