基本类型和引用类型的区别
int num = 10;
String str = "hello";
也就是说,对于基本类型的数据变量,他的值是直接存储在变量中,而str
是一个引用类型的变量,变量中保存的是我们实际的对象在堆内存中的地址,而我们真实的对象,其实是存储在堆空间中的。
下面我们再来看看==的作用
num=20
str="java"
经过这样的操作,内存中的数据又经历了如何的变化?实际是下面这个样子的。
对于基本类型 num
,赋值运算符会直接改变变量的值,原来的值被覆盖掉。
对于引用类型str
,赋值运算符会改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变(重要)。
如上图所示,"hello"
字符串对象没有被改变。(没有被任何引用所指向的对象是垃圾,会被垃圾回收器回收)
值传递和引用传递的区别
在介绍Java
到底是值传递还是引用传递之前,我们先来看看到底什么是值传递,什么是引用传递。
值传递(
pass by value
)指的是在把实参传递给形参的时候,将实际的参数复制一份传递给函数的形参,也就是说,在函数内部对形参的修改,不会改变原来实参的值。引用传递(
pass by reference
)指的是在实际的函数调用时,将实际参数的地址传递给函数的形式参数,也就是说其实形参和实参保存的是堆中同一个对象的地址。如果在函数中对堆中的对象属性进行修改,那么不用说,实参对应的此对象的属性也会改变。
基本类型作为参数传递
接下来我们步入正题,看看java
到底是值传递还是引用传递,看看下面这段代码。
public class ValueAndRef {
public static void main(String[] args) {
// 对于基本类型的传递
int num=10;
System.out.println("调用change()方法前num的值是:"+num);//10
change(num);
System.out.println("调用change()方法后num的值是:"+num);//10
}
public static void change(int number){
number=20;
}
}
// 输出结果
调用change()方法前num的值是:10
调用change()方法后num的值是:10
首先我在main
函数中定义了变量num=10
,然后打印调用change()
方法,并且把Num
作为形式参数传递给此方法对其值进行修改,我们可以看到在调用change()
方法修改之后,主方法中的num
变量的值仍然没有改变,那么对应在内存中究竟是怎么一回事呢?
可以看到在内存中其实是这样的一种情况,我们知道,每一个方法都对应一个栈帧,其中我们在main()
方法中声明的变量存储在main()
方法的局部变量表中,当我们把num
作为实际参数传递给change()
方法时候,那么此是就会在change()
方法的局部变量表中多出来一个变量number
,所以,我们可以从底层看到,其实main()
函数中的实参传递给change()
方法中后,两个变量实际上是相互独立的,相互不会影响,所以,在上面尽管调用了change()
去修改num
的值,但是实际上修改的是change()
自己局部变量表中number
的值,并不会影响main
方法中的num
值,所以问题就很清晰了,对于基本数据类型的变量,在java
中实际上是值传递的过程,不会修改原来的值。
对象作为参数传递
相比争论最多的就是对象作为参数传递的时候,请看下面的代码。
public class ValueAndRef {
public static void main(String[] args) {
Persion persion=new Persion("小明",10);
System.out.println("调用change()方法之前:"+persion.toString());
// 调用方法对person对象进行修改
change(persion);
System.out.println("调用change(0方法之后:"+persion.toString());
}
public static void change(Persion persion){
persion.setName("小菊");
persion.setAge(20);
}
}
//输出结果
调用change()方法之前:Persion{name='小明', age=10}
调用change(0方法之后:Persion{name='小菊', age=20}
class Persion{
private String name;
private int age;
public Persion(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Persion{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
看来是不是有些出乎意料,竟然把我们person
对象的属性值修改了,这是为什么呢?下面我们看看对象的内存布局就明白了。
其实上面的那一段代码,在内存中的逻辑是上图中所示那样的,在上面基本数据类型和引用类型的区别中,我们已经说明,引用数据类型实际上存储的是对象在堆内存中的地址,在main()
函数中,把person
引用存储的内容传递给了change()
函数的形参,实际上传递的是堆空间中对象的地址,也就是这两个引用,在内存中其实指向的是堆空间中的同一个对象。就像上面画的所示,所以,你在change
中对堆空间中这个对象进行修改,那么自然person
引用指向的对象内容也会发生改变。
- 现在我们再来看看另一段代码:
public class ValueAndRef {
public static void main(String[] args) {
Persion persion=new Persion("小明",10);
System.out.println("调用change()方法之前:"+persion.toString());
// 调用方法对person对象进行修改
change(persion);
System.out.println("调用change(0方法之后:"+persion.toString());
}
public static void change(Persion persion1){
persion1=new Persion("小菊",20);
}
}
//输出结果
调用change()方法之前:Persion{name='小明', age=10}
调用change(0方法之后:Persion{name='小明', age=10}
看到没有,现在调用change()
方法后,person
引用指向的对象的内容竟然没有发生改变,我们再来看看此时的内存布局:
- 在我们进入到
change()
方法后,没有执行persion1=new Persion("小菊",20);
这一句代码之前,内存布局其实是这样的,此时两个引用仍然指向堆内存的同一块区域。
- 当我们执行
persion1=new Persion("小菊",20);
后,我们的内存布局是这样的:
也就是说此时change()
方法中的person1
指向了堆内存中的另一块内存区域,现在main()
方法和change()
方法中的两个引用分别指向堆内存中的不同区域,相互独立互不影响,所以,在change()
方法中对对象内容修改,不会影响main()
函数中的对象。
好了,现在三种情况都讲完了,对于基本数据类型很好理解,但是对于引用数据类型,我们就要想明白,引用变量里面实际存储的是对象在堆空间中的真是地址,在发生方法调用需要传递对象的时候,其实是把对象的真是地址复制了一份作为实参传递给了形参,
- 现在,我们再来总结一下值传递和引用传递的区别:
值传递 | 引用传递 | |
---|---|---|
区别 | 会创建副本 | 不会创建副本 |
影响 | 函数中无法改变原始的对象值 | 函数中会改变原始的对象值 |
经过上面的分析,可以看到值传递和引用传递实际上的区别并不是传递的内容,而是实际参数到底有没有复制一份给形参,从这个角度将,java
肯定就是值传递,对于基本数据类型,是把值复制了一份给形式参数,而对于引用类型来说,其实就是把对象存储的地址复制了一份给形参,所以在判断我们的实参内容到底有没有收到影响,就要看你传递的是什么,如果你传递的是地址,那么就需要看形式参数看的指向到底有没有发生变化,所以总的来说,java
是值传递,对于引用类型,传递的是对象的地址,这一点没错。
关于String
String
类是个特殊的类,对它的一些操作符是重载的,如:
String str = “Hello”;
等价于String str = new String(“Hello”);
String str = “Hello”;
str = str + “ world!”;
等价于str = new String((new StringBuffer(str)).append(“ world!”));
因此,你只要按上面的方法去分析,就会发现String
对象和基本类型一样,一般情况下作为参数传递,在方法内改变了值,而原对象是不会被改变的。
参考资料
[1] https://www.zhihu.com/question/31203609
[2] https://www.cnblogs.com/hpyg/p/8005599.html
扫码关注作者公众号,获取更多关于大数据,java,数据结构与算法,jvm,数据库等资料。