Java 方法中的值传递与引用传递
总结
- 基本数据类型:方法形参改变不产生影响,原因在于方法传递过程中传递的是值的拷贝
- 包装数据类型:方法形参改变不产生影响,原因在于final修饰value
- String 字符串:方法形参改变不产生影响,原因在于final修饰value
- 引用类型未初始化:方法形参改变不产生影响,原因在于栈中句柄指向null,在堆空间中无地址,在方法中重新初始化,生成新的地址,但此地址未回传给调用方
- 引用类型已实例化:方法形参改变会产生影响,原因在于方法传递过程中传递的是地址,方法中与调用方法处均指向堆中同一个对象
一、值传递与引用传递
其实按值还是按引用的区别在于“是否在传递的时候进行对象的内存拷贝”,java中基本类型是由于在JVM中存储区域不同于普通对象所以传递前会拷贝,传递的是拷贝后的值,但是对象在传递的时候不拷贝,直接传“引用值”,指向同一片对象堆内存区域
二、基本数据类型
public class IntegerTest {
/**
* @param args
*/
public static void main(String[] args) {
int a = 10 ;
Integer b = new Integer(10);
IntegerTest test = new IntegerTest();
test.change(a);
test.change(b);
System.out.println(a);
// 10
System.out.println(b);
// 10
}
public int change(Integer a){
a = a + 10 ;
return a ;
}
}
main() 方法向 change() 中传递的是 栈中a 所存储的内容 10
change() 方法,形参 a ,虽然与 main() 中,名称相同,但是两个不同的变量
此时传递的是数值的拷贝
三、包装数据类型
// final 修饰:类不可被子类继承
public final class Integer extends Number implements Comparable<Integer> {
// final 修饰:存储堆空间的地址值不可改变
private final int value;
}
其底层实现中value是final修饰,是不可改变的;与String 类似,其值是不可变更的
若欲通过change() 修改数值:通过change()的返回值,重新赋值给 main()中的a
更改 a 句柄在栈中的指向,即指向新的堆空间中的地址,而原有的对象在堆空间中的地址并没有改变,只是此时没有指向,等待回收
结论:
基本数据类型及对应的包装类在传递过程中,不会改变其所指向的内容
四、String 类型
public class StringChangeTest {
/**
* @param args
*/
public static void main(String[] args) {
String str = "abc" ;
new StringChangeTest().change(str);
System.out.println(str);
}
public void change(String str){
str = "bcd";
}
}
字符串只要赋值就相当于新new一个对象,字符串变量指向这个新new的对象,之前的对象就成了没有引用指向的对象了。
1. String 的两种声明方式
String str = "abc"
栈中建立 str
判断堆中是否已有"abc",无则创建新的匿名空间,存放 "abc"
匿名空间首地址存放在 str 中,即 str 指向堆中的此空间备注:
提高程序运行效率,优选第一种方式
String str = new String("abc")
栈中建立 str
判断堆中是否已有"abc",无则创建新的匿名空间,存放 "abc"
堆中开辟新的空间,存放 "abc"
新的空间首地址存放在 str 中,匿名空间因无引用,过后被GC回收
2.
String str = "abc" ;
未显示调用 new 操作,在编译时 相当于 new String("abc");
3.
change(String str);
此处的str 不同于 main() 中的 str ,相当于重新开辟了一个空间,虽然名称相同
调用方法时,main() 方法将 str = "abc" 的堆空间地址传递给了 change() 中的str
此时 main() change() 中的两个不同的 str 均指向同一个堆内存块
但 str = "bcd" ; 相当于 在堆中新开辟了一块空间,存放 "bcd",并将地址存放在了str 中
即 change() 中的 str 指向了新的堆中的地址 -- "bcd" 的地址,与 main() 中的 str 指向的 "abc" 无关了,在 change() 中对 str 的修改,也与 main() 中无关,所以值没有改变
五、引用类型
public class ArrayIntegerTest {
/**
* @param args
*/
public static void main(String[] args) {
int [] a1 = {1,2,3,4,5};
new ArrayIntegerTest().change(a1);
for(int i = 0 ; i < a1.length ; i++){
System.out.println(a1[i]);
}
int [] a12 = new int[]{1,2,3,4,5};
new ArrayIntegerTest().change(a12);
for(int i = 0 ; i < a12.length ; i++){
System.out.println(a12[i]);
}
Integer [] a2 = {1,2,3,4,5};
new ArrayIntegerTest().change(a2);
for(int i = 0 ; i < a2.length ; i++){
System.out.println(a2[i]);
}
char [] s1 = {'1','2','3'};
new ArrayIntegerTest().change(s1);
System.out.println(new String(s1));
}
public void change(int[] a){
a[0] = 10 ;
}
public void change(Integer[] a){
a[0] = 10 ;
}
public void change(char[] a){
a[0] = 'a';
}
}
main() 中,建立引用对象,在堆中开辟新的空间
change() 中,形参声明新的对象,与main的不同
main() 调用 change() 时,将 栈中存放的堆中空间的首地址传递给 change() 中的参数
两者指向同一个数据地址
所以,change() 中的操作影响 main() 中的数据内容
public class PersonTest {
/**
* @param args
*/
public static void main(String[] args) {
Person p = new Person();
p.setAge(10);
new PersonTest().change(p);
System.out.println(p.getAge());
}
public void change(Person p){
p.setAge(20);
p = new Person();
p.setAge(30);
}
}
class Person{
private int age ;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
输出:20
调用 change() ,与 main() 中 p 是两个不同的句柄,但此时指向同一个堆中的地址
setAge(20) 有效
重新new 一个p ,此时 change() 中的 p 与 main() 的 p 指向的堆中的地址已经不一样了
new 新开辟空间,一定与之前的不同
所以 setAge(30) 对 main() 中的无影响
public class NumberAndAddressTest {
private Long id ;
private List<Integer> integerList ;
public static void main(String[] args) {
int a = 1 ;
changeInt(a);
System.out.println(a);
// 1
Integer b = 1 ;
changeInt(b);
System.out.println(b);
// 1
String str = "a" ;
changeStr(str);
System.out.println(str);
// a
NumberAndAddressTest test = null ;
changePO(test);
System.out.println(test);
// null
test = new NumberAndAddressTest();
test.setId(1L);
changeId(test);
System.out.println(test);
// id:2
changeIntList(test);
System.out.println(test.getIntegerList());
}
private static void changeInt(int a){
a = 2 ;
}
private static void changeStr(String str){
str = "abc" ;
}
private static void changePO(NumberAndAddressTest test){
test = new NumberAndAddressTest();
test.setId(2L);
}
private static void changeId(NumberAndAddressTest test){
test.setId(2L);
}
private static void changeIntList(NumberAndAddressTest test){
// 此处对 test 中的属性进行初始化,但 test 中属性原来指向 null
List<Integer> integerList = test.getIntegerList();
// 此处虽然进行了初始化,但两者并没有指向同一个地址
if(null == integerList){
integerList = new ArrayList<>();
}
integerList.add(1);
// 只有通过重新绑定两者之间的关系
// test.setIntegerList(integerList);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String toString() {
return "id:"+this.id;
}
public void setIntegerList(List<Integer> integerList) {
this.integerList = integerList;
}
public List<Integer> getIntegerList() {
return integerList;
}
}
public static void main(String[] args) {
// 此处传递空引用,并没有传递对象的地址指向
List<Integer> list = null;
// 方法中重新生成对象,但是地址并没有回传给调用方
remove(list);
// 所以方法传值过程中没有产生影响
System.out.println(list);
list = new ArrayList<>();
remove(list);
System.out.println(list);
list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,8,9));
removeAll(list);
System.out.println(list);
}
private static void remove(List<Integer> list){
list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,8,9));
list.remove(1);
}
private static void removeAll(List<Integer> list){
list.remove(1);
}
六、生产问题
- 数据传递过程中集合变为null
- 逐步在测试环境添加日志打印结果
- 定位在main方法中调用结果为null,在 add() 方法中有结果
- 但add()方法中对于引用数据类型的修改没有影响到main方法中的对象
package test;
import java.util.ArrayList;
import java.util.List;
/**
* DEMO 模拟
* 异常描述:
* 1.主方法调用add方法:
* 2.add方法内部对对象的属性进行处理
* 3.主方法使用对象属性,结果为空
*/
public class Test {
private List<Integer> integerList;
public List<Integer> getIntegerList() {
return integerList;
}
public void setIntegerList(List<Integer> integerList) {
this.integerList = integerList;
}
/**
* 场景模拟
* 1.integerList1 模拟问题异常场景
* 2.integerList2 模拟问题修复场景
* 异常疑惑
* 1.为何仅仅是移动了取值的顺序就可以解决取值为空的场景?
* 2.为何开发时没有发现会出现此类问题?
* 异常分析
* 1.主方法与从方法进行的是值传递,又因为是引用数据类型,从方法内部的修改会影响到主方法的属性,因为指向同一个堆空间地址
* 2.特殊场景:
* a.主方法中对象的属性为null,主方法中对象的属性 integerList 在堆空间中无指向
* b.又把 integerList 赋值给了 integerList1 , integerList1 为 null
* c.add 方法中对 integerList 重新初始化,生成堆空间,并添加内容
* d.主方法将 test 对象的地址给了 add() 方法中的 test 形参,形参的修改影响主方法中的 test 对象
* e.test.getIntegerList() 取到的是 add() 方法中的初始化后的对象信息
* 问题反思
* 1.因为方案设计时只考虑到了主方法中的属性一定非空的场景,那么add方法与主方法中的test对象均指向同一个堆空间
* 2.墨菲定律:如果可能出错就一定会出错;考虑为题要全面,如果空了该如何处理?
* 3.需要重点注意的场景:形参传递/数据拷贝/数值校验
* @param args
*/
public static void main(String[] args) {
Test test = new Test();
// List<Integer> integerList = test.getIntegerList();
// if(null == integerList){
// integerList = new ArrayList<>();
// test.setIntegerList(integerList);
// }
List<Integer> integerList1 = test.getIntegerList();
add(test);
List<Integer> integerList2 = test.getIntegerList();
System.out.println(integerList1);
System.out.println(integerList2);
// for (Integer integer : integerList1) {
// System.out.println(integer);
// }
}
private static void add(Test test){
List<Integer> integerList = test.getIntegerList();
if(null == integerList){
integerList = new ArrayList<>();
test.setIntegerList(integerList);
}
integerList.add(2);
}
}