一段递归代码引发的对于传参以及关于基本类型的一点了解
Posted Ben_Mario
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一段递归代码引发的对于传参以及关于基本类型的一点了解相关的知识,希望对你有一定的参考价值。
首先附上为了模拟场景简化的递归代码:
public class Recursion { public static void main(String[] args) { Recursion recursion = new Recursion(); List<Long> list = new ArrayList<Long>(); list.add(100L); Long num = 100L; recursion.recursionFunction(5L, list); recursion.recursionFunction(5L, num); System.out.println("recursionFunction:list:" + list.get(0)); System.out.println("recursionFunction:num:" + num); } //为了后面方便描述,以后称:listfunction private void recursionFunction(Long i, List<Long> list) { i--; if (i == 0) { list.set(0,i); } else { recursionFunction(i, list); } } //为了后面方便描述,以后称:longfunction private void recursionFunction(Long i, Long num) { i--; if (i == 0) { num = i; } else { recursionFunction(i, num); } } }
模拟出来的递归场景,有几个朋友在我跟他们说了打印结果并不是“recursionFunction:list:0”和“recursionFunction:list:0"的时候他们也表示很惊讶,为什么不是呢?
其实我也正是因为打印结果不是这样而觉得不可思议。
当我遇到这个现象的时候我的第一反应是:怎么会这样,明明逻辑都是一样的,然后是把参数num改外全局变量,打印结果和预期的一样。然后问题就来了,为什么参数list不用改为全局变量就可以呢?
接着是第一个猜测:是不是java虚拟机在声明变量的时候分配内存空间的不同?据我了解:只要声明变量,java虚拟机都会开辟内存空间。我还是仔细的想了一下,自己否定了;
然后是有第一个猜想引发的第二个猜想:会不会调用方发的时候形参和实参用的不是同一个对象,可问题就出在这里:如果用的不一样,那为什么list就可以,而num就不可以呢?于是我陷入了一个很迷茫的地步。
第三个猜想:是不是因为Long封装类的原因?经过测试,发现并不是因为这样。
第四个猜想:是不是因为基本类型的原因?经测试,直接想到的就是Object类,但结果也并不是我想的那样。
于是开始带着这段自认为神奇的代码,给朋友看,我大概都能想到朋友在得知结果与预期结果不相同时的惊讶。
有朋友让我用别的类型试试比如:StringBuffer和StringBuilder试试,我在没试之前就觉得不行的,结果我只是了一个真的不行,第二个就没有试了。
和另一朋友开始了讨论:
第一:Long型的比较他建议我用longValue()这个方法[补充一],但是因为和0比较,肯定是不存在问题的,不过这点还是很需要注意的,因为往往能用到Long型都是ID相关的,
通常都是一些ID生成算生成的大多数都在16位左右。
第二:Long类型的自动封装,这个和我的第三个猜想是一样的。那为什么全局变量就可以呢?
第三:递归方法中num出栈就会销毁,也就是递归方法用完,数据就会销毁。那还是同样的问题,为什么list就可以呢?
然后他查看了class文件反编译的代码,我突然想到,ArrayList的底层就是用可变数组实现的,然后我就试了一下数组,结果和list的效果一样,赋值成功。这个时候,我似乎有点顿悟。
好像明白为什么list的值可以赋值成功,但是不明白num为什么不成功。
第四:他告诉我,如果在listfunction中第一局执行list = new ArrayList<Long>();结果也会不止不成功,并告诉我好像破案了。[ps:这个过程真的像是在破案或者是玩寻宝游戏,我很喜欢]
这个时候我们好像都明白了为什么打印结果会出乎意料的是“recursionFunction:list:0”和“recursionFunction:list:100",为了肯定答案,我们又去查了资料。
我将理解整理如下:
关于传参:java的传参有两种,分别是{值传递和引用传递},八种基本类型就是属于值传递,而他复合类型都是引用传递。
值传递————就是把实际对应的对象值传递过去,比如代码中,虽然看上去传递的是num这个变量对象,但实际是传递了100L过去的,就直接是个数字传过去了,方法中的运算和num并没有什么关系。
引用传递————就是,传递的实际对象的内存地址过去,比如代码中,真的是把list传递过去了,list实际上是一个引用是new ArrayList()这个对象的内存地址。[补充二]。
那么String也是复合类型,为什么也不能赋值成功呢?【注】
现在我们走一遍代码:listfunction方法,传进去了list指向的new ArrayList()对象的内存地址,然后再对这段地址的中的第一个单元进行赋值为0,然后递归结束,0对象销毁。
而本身list这个引用就指向new ArrayList()这个对象,再拿出来这个对象第一个单元的值,现象是赋值成功。
反例,我们再listfunction方法中加上list = new ArrayList();依然传进来的是list指向的new ArrayList()对象的内存地址,但第一句,改变了list的指向地址,
而且每一次递归调用都是指向了一个新的new ArrayList()对象地址,那么在最后一次赋值成功,其实是给new ArrayList()这个最新的对象的第一个单元地址赋值,递归结束,这些在递归局部内的对象销毁,
list又指向了原来的对象地址,而原来的对象是main方法中的new ArrayList(),所以赋值失败。
关于基本类型的赋值:
java说一切都是对象。随机即使是基本类型在赋值的时候本质上其实是new了一个新的对象,改变引用地址。比如说:
int i = 0;(Integer i = 0;) ==> int i = new Integer(0);(Integer i = new Integer(0);)
所以你可能就瞬间懂了,为什么递归中赋值语句 num = i;明明赋值成功了,为什么递归结束后,num还是100L,因为基本类型传递值 i = 0,其实就是 num = new Long(0),递归结束,new Long(0)销毁。
那么String也是复合类型,为什么也不能赋值成功呢?【注】
[补充一:如果直接用“==”比较,比较范围只能是在Long型范围在Byte范围内,如果超出了Byte类型的范围(-128~127)需要使用equals()或者longValue()比较]
[补充二:声明变量赋值 格式是:类型名称 变量名称 = 对象;,其实本质的理解并不是变量名称等于对象,而是变量名称等于对象所在的内存地址,所以我们也叫这个变量名称为引用。
它并不是对象,只是引用了对象。所以在复合类型传参时,其实传递的是:变量名 = 内存地址,但在基本类型中传参就是值(num = 100L),而不是值对应的内存地址]
【注】:String虽然是复合类型,但string对象和其他对象是不同的,string对象是不能被改变的,内容改变就会产生新对象。也就是每次赋值都是改变变量名所指向的内存地址。
也就是每次String类型赋值时是new除了一个新的对象。比如 String str = “str”; ==>String str = new String("str");,
那么就可以理解成其他的复合类型在赋值的时候其实是 变量名 = 内存地址(这部分是不变的),改变的是这段内存地址里面存放的数据。
以上是关于一段递归代码引发的对于传参以及关于基本类型的一点了解的主要内容,如果未能解决你的问题,请参考以下文章