街题系列 - Java 是值传递还是引用传递
前言
这个也是经常遇到的一个题目吧,可能是一道笔试题,给一段代码,问输出结果,也可能是直接被问到的,我就遇到了一个笔试题,问输入结果,虽然我知道答案,可是被问到为啥的时候,却不能打出来一个所以然,先说笔试题内容。
public static void main(String[] args) {
String str = new String("abc");
char[] c = new char[] { 'a', 'b', 'c' };
change(str, c);
System.out.println("str= "+str);
System.out.println("c= "+String.valueOf(c));
}
public static void change(String str, char[] c) {
str = "123";
c[0] = 'd';
}
先公布这个结果吧
str= abc
c= dbc
为毛线啊,不着急,且听我慢慢道来。。
话说 Java 的数据类型分为两种
- 基本数据类型
- 整型 :byte, int , short ,long
- 浮点型: float,double
- 字符型: char
- 布尔型: boolean
- 引用数据类型
- 数组
- 类
- 接口
- 基本数据类型,值就直接保存到变量中
- 引用数据类型,变量中保存中的是实际对象的地址。一般称为这种变量为引用。引用指向实际类型,实际类型保存着内容,
局部变量和方法参数都是 JVM 在栈中开启空间存储的,随着方法进入开辟,退出回收。以 32 位 JVM 为例,boolean/byte/int/short/char/float 以及引用都是在内存中开辟 4 个字节的空间,long/double 则是 8 个字节的空间,对于每个方法来说,最多占据多少空间是一定的,这在编译时期都计算好了,
每一个线程都分配一个独享的栈,所有的线程共享一个堆,对于每个方法的局部变量,是觉得不可能被其他方法,甚至其他线程的同一个方法所访问的,更别说修改了。
我们在方法中声明一个int i=0;或者Object obj=null的时候,仅仅涉及到栈,不影响到堆。当我们 new Object() 的时候,实际上是在堆中开辟一个块内存并初始化 Object 对象,当我们将这个对象赋值给 obj 变量的时候,仅仅是栈中代表 obj 的四个字节变更为这个对象的地址。
方法的参数分为
- 形式参数 定义方法时写的参数
- 实际参数 调用方法时写的具体参数
一般情况下,在数据作为参数传递的时候,
- 基本数据类型是值传递,
- 引用数据类型是引用传递(地址传递)
- String, Integer , Double 等 Immutable 类型因为没有提供自身修改的函数,所以每次操作都是生成一个新的对象,所以需要特殊对待,可以认为是值传递
接下来一个一个通过代码解释:
基本数据类型
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
结果为
a = 20
b = 10
num1 = 10
num2 = 20
解析
因为 a , b 是从 num1 和 num2 复制过来的,所以 a , b 不管怎么改变,不会影响到 num1 , num2 ,这就相当于你拿着身份证复印件去办理一项业务,很多情况下,为了安全,我们都会在身份证复印件上写上“该复印件只能用来办理 xxx 业务”之类的话,写的这话显示到你的身份证复印件上,可是你的身份证上并没有因此改变一个道理。只要记住是复制,不是本身,就好理解。
引用数据类型
public static class Employee {
public int age;
}
// 创建两个线程,交替打印数字
public static void main(String[] args) {
Employee employee = new Employee();
employee.age = 10;
changeEmployee(employee);
System.out.println("age = "+employee.age);
}
public static void changeEmployee(Employee emp ) {
emp = new Employee();
emp.age = 50;
System.out.println("changeEmployee age = "+emp.age);
}
运行结果如下:
changeEmployee age = 1000
age = 100
然后我们再添加一些 log ,查看运行结果
public static class Employee {
public int age;
}
// 创建两个线程,交替打印数字
public static void main(String[] args) {
Employee employee = new Employee();
employee.age = 10;
System.out.println("employee : "+employee);
changeEmployee(employee);
System.out.println("employee: "+employee+" age = "+employee.age);
}
public static void changeEmployee(Employee emp) {
System.out.println("changeEmployee before"+emp);
emp = new Employee();
System.out.println("changeEmployee end "+emp);
emp.age = 50;
System.out.println("changeEmployee emp = "+emp+" age: "+emp.age);
}
运行结果如下:
employee : top.hoyouly.sina.JavaReferenceTest$Employee@6a998c1
changeEmployee beforetop.hoyouly.sina.JavaReferenceTest$Employee@6a998c1
changeEmployee end top.hoyouly.sina.JavaReferenceTest$Employee@686baa51
changeEmployee emp = top.hoyouly.sina.JavaReferenceTest$Employee@686baa51 age: 50
employee: top.hoyouly.sina.JavaReferenceTest$Employee@6a998c1 age = 10
这应该能看出来点眉目吧
解析
- 因为 Employee 是一个类,创建该对象,这个对象会存入到堆中一个地址里面,而这个地址就是 6a998c1 ,然后把该对象赋值给变量 employee ,于是 employee 这个变量指向这个对象的地址,即 6a998c1 ,
- 引用数据类型在参数传递的时候传递的是引用地址,也就是在执行 changeEmployee() 方法时,变量 employee 会把引用的地址复制一份给 emp ,于是 changeEmployee() 方法第一行输出的 emp 地址就和变量 employee 变量一样
- 然而 changeEmployee() 的第二行确实重新再创建一个对象,在堆内存的地址是 6a998c1 ,然后把这个对象赋值给 emp ,也就是从这一刻开始, emp 指向的地址变量由原来的 6a998c1 变成了 6a998c1 ,那么以后对 emp 的操作,都是对 686baa51 地址的操作。所以尽管 emp 设置了 age 为 50 ,但是这个设置是对 686baa51 地址的对象操作的,而非 6a998c1 地址的对象,
- changeEmployee() 执行完成后,尽管 686baa51 对象的 age 值变化了,可是 6a998c1 对象的 age 依旧没改变,还是10
举个栗子:
你说你会清理抽烟机,我刚好需要,就告诉你我家地址。而你知道我家地址,可是竟然没来我家,去了另外一家,然后把油烟机清理干净了,但是我家的油烟机还是脏兮兮的啊,其实就是这个道理。
- 知道我家的地址,即Employee employee = new Employee();
- 发现我家油烟机很脏 employee.age = 10;
- changeEmployee() 可以理解为清理油烟机(),接受一个家庭地址
- 我把我家地址给你了,执行 changeEmployee(employee);
- 你知道了我家地址, System.out.println(“changeEmployee before”+emp);
- 可是你竟然私自做主张,更换地址,emp = new Employee();
- 那么就算你把油烟机清理的再干净,emp.age = 50; 那也不是我家的啊,我家的还是脏兮兮的,age = 10
接下来我们再改变一下代码
public static class Employee {
public int age;
}
// 创建两个线程,交替打印数字
public static void main(String[] args) {
Employee employee = new Employee();
employee.age = 10;
System.out.println("employee : "+employee);
changeEmployee(employee);
System.out.println("employee: "+employee+" age = "+employee.age);
}
public static void changeEmployee(Employee emp) {
System.out.println("changeEmployee before"+emp);
// emp = new Employee();
System.out.println("changeEmployee end "+emp);
emp.age = 50;
System.out.println("changeEmployee emp = "+emp+" age: "+emp.age);
}
我只是注释了一行代码, // emp = new Employee();,其他一样,可是结果却大不相同了,先看这次运行结果
employee : top.hoyouly.sina.JavaReferenceTest$Employee@2f63e9a1
changeEmployee beforetop.hoyouly.sina.JavaReferenceTest$Employee@2f63e9a1
changeEmployee end top.hoyouly.sina.JavaReferenceTest$Employee@2f63e9a1
changeEmployee emp = top.hoyouly.sina.JavaReferenceTest$Employee@2f63e9a1 age: 50
employee: top.hoyouly.sina.JavaReferenceTest$Employee@2f63e9a1 age = 50
其实这就好理解了,还以刚才清理油烟机的例子,这次请你清理油烟机,你学乖了,不敢私自改地址了,整个运行过程中,对象的地址一直都是 2f63e9a1 , 乖乖来我家了,把油烟机清理干净了,那样理所当然的我家的油烟机就干净了啊。
引用数据类型包括类,数组和接口,类刚才说过了,接口也属于类的一种,就不多解释了, 然后说一下数组
数组类型
public static void main(String[] args) {
char[] ch = new char[] { 'a', 'b', 'c' };
change(ch);
System.out.println("ch= "+String.valueOf(ch)+" ch的地址: "+ch);
}
public static void change( char[] c) {
c[0] = 'd';
System.out.println("c[0]: "+c[0]+" c :"+c);
}
运行结果:
c[0]: d c :[C@528f2588
ch= dbc ch的地址: [C@528f2588
看到了没, change() 中 c 的地址和 main() 中 ch 地址一样,所以在 change() 中对 c 的修改直接影响 main() 中 ch ,所以 ch 的值就变成了 dbc ,
然后看第三种,特殊的类,没有提供自身修改的函数的类,例如 String , Integer , Double 等 Immutable 类型
没有提供自身修改函数的类
说最简单的吧, String 类型,
public static void main(String[] args) {
String str = new String("abc");
System.out.println("before str: "+str.hashCode()+" str: "+str);
change(str);
System.out.println("end str: "+str.hashCode()+" str: "+str);
}
public static void change(String s) {
System.out.println("before s "+s.hashCode()+" s: "+s);
s = "123";
System.out.println("end s "+s.hashCode()+" s: "+s);
}
运行结果:
before str: 96354 str: abc
before s 96354 s: abc
end s 48690 s: 123
end str: 96354 str: abc
同样,进入到 change() 后, s 的 hashcode 变了,可以理解为不是同一个对象了, change() 中 s 值该为“123”,但是 str 并没有改变。
解析
String 中 API 中有这样一句话: “their values cannot be changed after they are created”,意思是 String 类的值创建后就不能被改变了。也就是说对 String 对象 s 的任何修改都等同于创建一个新对象,并将新的地址赋给 s 。 String 对象一旦创建,内容不可更改,每一次内容更改都是重新创建新的对象。
搬运地址:
既已览卷至此,何不品评一二: