0.前言
因为之前有同学刚好问我一个关于Java引用传递的问题,为什么将ArrayList中拿到的一个元素,赋给一个对象,对象修改了属性,ArrayList中的对应的元素属性也会修改,这就牵扯出Java中一直讨论的一个问题:Java传参时是引用传递还是值传递。 在这篇文章里我们主要会对一下几个问题进行讨论。
- 1.基本类型和引用类型在内存中的保存
- 2.变量的基本类型和引用类型的区别
- 3.引用传递和值传递
1.基本类型和引用类型在内存中的保存
Java中数据类型分为两大类,
- 基本类型:`byte, short, int, long, char, float, double, Boolean,不过,Java字节码中还有额外的两种基础数据类型,分别是reference type与returnAddress type;
- 对象类型:类,接口和数组。
相应的,变量也有两种类型: - 基本类型:基本类型的变量保存原始值,即它代表的值就是数值本身;
- 引用类型:引用类型的变量保存引用值,"引用值"指向内存空间的地址,代表了某个对象的引用,而不是对象本身。
2.变量的基本类型和引用类型的区别
2.1 基本类型的初始化
基本数据类型是存储在栈内存中,当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
举个例子:
int a = 3;
首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。
题外话:不是所有的基本数据类型都存在java栈里,为了反驳观点" Java的基本数据类型都是存储在栈的 ",我们也可以随便举出一个反例,例如:
int[] array=new int[]{1,2};
由于new了一个对象,所以new int[]{1,2}
这个对象时存储在堆中的,也就是说1,2
这两个基本数据类型是存储在堆中,这也就很有效的反驳了基本数据类型一定是存储在栈中~~
2.2 引用类型的初始化
image.png上图表示的是在JVM中,Java对象的访问方式。从上图可以看出,对象的引用变量是存储在栈内存中的。我们通过一个例子来分析下这个过程。
Object obj = new Object();
- 第一步:
new Object()
:JVM将在GC堆中开辟一定大小的空间用以存放此Object,此Object是引用数据类型(注意不是reference类型,reference类型保存在Java栈中的本地变量表里)。另外,在JAVA堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现接口、方法等)的地址信息,这些类型数据存储在方法区中。 - 第二步:
Object obj
:JVM将在虚拟机栈中此线程栈开辟出一个slot用以存放obj,注意此处obj为reference type。(slot可以理解为用以存放基础数据类型的线程私有的空间) - 第三步:
=
:reference type 的obj被赋值为先前new Object() 指令所产生的引用数据类型的一个对象。
分析完不同类型的变量在内存中的储存形式之后,我们就可以来分析下到底什么时候是值传递,什么时候是引用传递。
3.引用传递和值传递
-
值传递:
方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值。 -
引用传递:
也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址;在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。
举例:
public class Demo0 {
private User user = new User("Jack", 1l);
private int num;
private String string ="";
//提供改变自身方法的引用类型
void fun0(User user){
user.setName("Lucy");
}
//基本类型作为参数传递时,是传递值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的
void fun1(int value) {
value = 100;
}
//没有提供改变自身方法的引用类型
void fun2(String text) {
text = "windows";
}
//在Java中对象作为参数传递时,是把对象在内存中的地址拷贝了一份传给了参数。
public static void main(String[] args) {
Demo0 demo = new Demo0();
demo.num = 10;
demo.string = "Linux";
System.out.println("before name: " + demo.user.getName());
demo.fun0(demo.user);
System.out.println("after name: " + demo.user.getName());
System.out.println("before num: " + demo.num);
demo.fun1(demo.num);
System.out.println("after num: " + demo.num);
System.out.println("before string: " + demo.string);
demo.fun2(demo.string);
System.out.println("after string: " + demo.string);
}
}
- 函数fun0运行结果:
before name: Jack
after name: Lucy
user的name属性被修改,原因是:fun0调用时,函数接收的是原始值的内存地址;方法执行setName操作,修改了堆中改内存地址上的User对象的属性。通过这个例子我们可以知道:引用类型传递的是对象的引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象。
- 函数fun1运行结果:
before num: 10
after num: 10
很显然输出的 是10,10。传递的是值得一份拷贝,这份拷贝与原来的值没什么关系。基本数据类型传值,对形参的修改不会影响实参。
- 函fun2运行结果:
before string: Linux
after string: Linux
这就很有意思了,为什么String是引用类型,为什么不会修改其值呢?原因是:String,以及Integer、Double等几个基本类型包装类,它们都是immutable类型,因为没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待,可以认为是和基本数据类型相似,传值操作。
现在来看一开始提出的那个问题:为什么从ArrayList中get第一个元素,赋给一个对象,对象修改了属性,ArrayList中的元素属性也会修改?
public class Demo2 {
public static void main(String[] args) {
List<User> list = new ArrayList<User>();
User user1 = new User("name1", 1111l);
User user2 = new User("name2", 2222l);
User user3 = new User("name3", 3333l);
list.add(user1);
list.add(user2);
list.add(user3);
System.out.println("--------------Before---------------");
for (User user : list) {
System.out.println("name = " + user.getName());
}
User userTemp = list.get(0);
userTemp.setName("xxxxxx");
System.out.println("--------------After---------------");
for (User user : list) {
System.out.println("name = " + user.getName());
}
}
}
输出:
--------------Before---------------
name = name1
name = name2
name = name3
--------------After---------------
name = xxxxxx
name = name2
name = name3
原因很简单:userTemp 和list的第一个对象都指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。
4.总结
java中方法参数传递方式是按值传递。
- 如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
- 如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝
- String,以及Integer、Double等几个基本类型包装类,它们都是immutable类型,因为没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待,可以认为是和基本数据类型相似,传值操作。
网友评论