首先附上为了模拟场景简化的递归代码:
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也是复合类型,为什么也不能赋值成功呢?【注】
关于基本类型的赋值:
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也是复合类型,为什么也不能赋值成功呢?【注】
现在我们走一遍代码: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(),所以赋值失败。
[补充一:如果直接用“==”比较,比较范围只能是在Long型范围在Byte范围内,如果超出了Byte类型的范围(-128~127)需要使用equals()或者longValue()比较]
[补充二:声明变量赋值 格式是:类型名称 变量名称 = 对象;,其实本质的理解并不是变量名称等于对象,而是变量名称等于对象所在的内存地址,所以我们也叫这个变量名称为引用。它并不是对象,只是引用了对象。所以在复合类型传参时,其实传递的是:变量名 = 内存地址,但在基本类型中传参就是值(num = 100L),而不是值对应的内存地址]
【注】:String虽然是复合类型,但string对象和其他对象是不同的,string对象是不能被改变的,内容改变就会产生新对象。也就是每次赋值都是改变变量名所指向的内存地址。也就是每次String类型赋值时是new除了一个新的对象。比如 String str = “str”; ==>String str = new String("str");,那么就可以理解成其他的复合类型在赋值的时候其实是 变量名 = 内存地址(这部分是不变的),改变的是这段内存地址里面存放的数据。