引用类型传参到方法内部后进行修改数据发生了怎样的变化?String类型传参无法做到在方法内部修改后传出方法外?
引言
假设有一个要求是这样的,需要你写一个方法在方法内实现对一个数组的排序,那么这应该是很简单的,没有什么可疑问的。现在有一个同样类似的的要求,需要你写一个方法,在方法内部对一个字符串"csdn"进行变换,将其转为大写的字符串"CSDN",我们又该怎么做呢?还是与数组排序那样操作就可以吗?事实上通过实验我们发现并非看上去这么简单,下面就让我们一起讨论其中的一些问题吧!
问题引入
问题一
- 请实现一个方法,数组传参,在该方法内对该数组进行排序(升序或降序任意),并在方法外输出。
问题二
- 请实现一个方法,字符串传参,在该方法内对该字符串进行大小写转换,并在方法外输出,问方法外输出的是否为转换后的字符串?
第一个问题看起来很简单,相信每个同学都能很轻易做到,第二个问题呢?你是不是想,直接在方法内直接对原字符串进行修改不就好了,这有何难?如果是这样那你就掉进了一个坑里了,或者说你对Java的堆、栈了解得并不是十分透彻。下面让我们来看看具体的代码吧!
程序示例
这里我们将两个问题的代码放在一个类里实现了,具体请看如下代码。
public class ChangeWorlds {
public static void main(String[] args) {
//数组排序部分
int[] arr = {1,4,2,3};
System.out.print("排序前:");
for(int i = 0; i< arr.length; i++) {
System.out.print(arr[i] + " ");
}
sort(arr);
System.out.println();
System.out.print("排序后:");
for(int i = 0; i< arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
//字符串转换大写部分
String str = "csdn";
System.out.println("变换前:" + str);
change(str);
System.out.println("变换后:" + str);
}
// 为了方便我们直接对字符串进行赋值大写的CSDN
public static void change(String str) {
str = "CSDN";
System.out.println("方法内输出:" + str);
}
// 数组的排序方法 用来对比String
public static void sort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for(int j = i +1; j < arr.length; j++) {
if(arr[i]>arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
}
代码介绍
- 我们在ChangeWorlds 类中实现了两个方法,sort()和change(),都是不返回参数的,如果直接返回参数的话我们这里的问题就毫无意义了,因此才不返回参数。
- 在main()方法内,我们先创建了一个数组,然后将该数组传入sort()方法进行排序,并且在main()方法中对该数组排序前后都做了一次输出。
- 我们又创建了一个String对象,请注意这里我用的是“创建”这个词,并且同上面的数组一样传入change()方法,并在方法内修改字符串,修改前后在main()方法中输出了两次以观察进入方法前后该字符串发生的变化。
- 并且在change()方法内,我们还增加了一次输出用来观察变化情况。
输出结果
排序前:1 4 2 3
排序后:1 2 3 4
变换前:csdn
方法内输出:CSDN
变换后:csdn
通过输出结果我们可以看到,数组的排序功能完美实现了,但是字符串在变换前后没有发生任何变化!但它在方法内部的输出却发生了变化。你可能会想,这还不简单,这就就是值传递吗,你在方法内修改它肯定不能修改到字符串本身。先不要着急,问题或许不是那么简单,我们到下一步接着进行详细分析。
分析
刚刚说了,数组排序完美实现,但字符串在方法前后没有发生任何变化,而在方法内却发生变化了。有的同学可能瞬间就想到了,这不就是和值传递的情况一模一样吗!看起来这确实和值传递一模一样,但你可不要忘了,String是什么类型?他可不是基本数据类型,它是引用数据类型,引用数据类型是不会进行值传递的!就和数组一样,数组也是引用数据类型,这才能够在方法修改后传出方法外。
那就有大问题了,都是引用数据类型,凭什么数组可以被修改后传出,而字符串不能被修改后传出?那String到底还是不是引用类型了?
它当然还是引用类型,只是在传参时发生的操作并非看上去那么简单。我们通过图解来具体分析一下其中的变化。
首先我们要对堆和栈具有一定的了解,否则分析就无法进行。堆存放的是对象,而对象的引用存放的在栈里,普通的数据类型和一些方法、局部变量也都存放在栈里。此外我们还要知道一点,堆和栈内数据的生存期是不同的,栈内的数据会随着方法的结束而被回收,然而堆并不是,堆内的数据是不会随着方法的结束而自动回收的,而Java的垃圾回收机制就是关于这一点的,那什么时候堆内的数据会被回收呢?只有当该对象没有被指向时,它就被自动回收了。
最后我们还要知道一点的是,java中没有地址传递,只有值传递。到这里你是不是有点蒙了,怎么又说没有地址传递?那前边说的又是什么意思??这里我们解释一下,java中的地址传递并不是传统意义上的,比如我们有一个对象Obj,传参时实际上是指向Obj的引用1被作为参数被复制了一份,因此当前这个引用2指向的同样是在堆内的Obj对象。如上图。
知道了前面的知识后,我们进入分析的正题!接下来的都是重点,请认真起来了!首先String当然是引用类型,因此传参时也一定同样是地址传递,所以当字符串str传参时,str的地址被复制了一份到栈中,此时复制的这个引用依然是指向原字符串"csdn"的。如上图,我们将原str依然用str表示,将传入方法后的参数str用str’表示。目前这两个引用指向的都是"csdn"对象。
然后我们进行了一步很重要的操作,一个看起来没有任何问题的操作,但恰恰问题就是出在了这里。我们对str’进行修改了,给它重新分配了一个新的对象"CSDN"。这直接导致了后面所有问题的出现,因为当我们这么做之后,str’就已经不指向原"csdn"对象了!(如上图)再对str’的任何操作都不会对原"csdn"对象有任何影响了,因为它俩指向的对象已经不是同一个了,所以字符串"csdn"不会发生任何变化!
结论
到这里对于该问题我们已经分析得很清楚了,依然有疑问的同学可以多看几遍理解理解。最后我们对前面的所有问题做个小结。小结如下。
- 基本类型的变量放在栈里;
- 封装类型中,对象放在堆里,对象的引用放在栈里。
- 当传参时,JVM是重新在栈里开辟了一块空间str’,str’与str的地址是相同的,很重要的一点,他俩本身在栈中是两块不同的内存!
- 在方法内,赋值操作相当于让JVM给str’又重新在堆内开辟了一块新的内存,str’ = new String(CSDN),当退出方法后,该内存str’就被回收了,所以,这里方法内部对str’的任何操作,实际上都没有对原来的对象进行过任何修改。
- 一句话来说就是,一旦你在方法内对字符串进行了赋值操作了,那么之后你在堆内的任何操作都不再是对原对象进行操作了。
- 变量存放在栈里,而变量指向是存放在堆内存!!!
关于复制后又新创建的对象"CSDN"去哪儿了的同学请看下面。
这里有一点需要注意,当change()方法运行结束时,回收的不只是str’,str’指向的对象也会被回收!上面说到了,堆内对象的生命周期与方法无关,即使方法被回收了,只要该对象仍有变量指向它,那么堆内的对象也不会被回收。它的生命周期从创建时开始,到没有变量指向该对象时,就会被自动回收了。但是,这里正是因为str’被回收了,也就间接导致了"CSDN"对象间接地没了指向它的引用,因此在这里它是与str’一起被回收的。