最近跟 Java 中的值传递和引用传递杠上了, 一度怀疑人生. 查了很多资料, 加上自己的理解, 终于搞清楚了, 什么是值传递和引用传递. 也搞明白了, 为什么大家都说 Java 只有值传递, 没有引用传递. 原来, 我一直以来的认知都是错误的...
首先, 需要了解一些概念性的东西.
形参与实参:
形参, 是指在定义函数时使用的参数, 目的是用于接收调用该函数时传入的参数. 简单理解, 就是所有函数 (即方法) 的参数都是形参.
实参, 是指调用函数时, 传递给函数的参数.
- public static void main(String[] args) {
- int num = 3;
- printVal(num); // 这里 num 是实参
- }
- private static void printVal(int num) {
- num = 5; // 这里 num 就是形参
- }
值传递和引用传递
值传递: 是指在调用函数时, 将实际参数复制一份传递给函数, 这样在函数中修改参数时, 不会影响到实际参数. 其实, 就是在说值传递时, 只会改变形参, 不会改变实参.
引用传递: 是指在调用函数时, 将实际参数的地址传递给函数, 这样在函数中对参数的修改, 将影响到实际参数.
这里, 需要特别强调的是, 千万不要以为传递的参数是值就是值传递, 传递的是引用就是引用传递. 也不要以为传递的参数是基本数据类型就是值传递, 传递的是对象就是引用传递. 这是大错特错的. 以前的我, 一直都是这样认为的, 现在想来真是太天真了. 判断是值传递还是引用传递的标准, 和传递参数的类型是没有一毛钱关系的.
下面三种情况, 基本上可以涵盖所有情况的参数类型.
当传递的参数是基本数据类型时:
- public class TestNum {
- public static void main(String[] args) {
- int num = 3;
- System.out.println("修改前的 num 值:"+num);
- changeValue(num);
- System.out.println("修改后的 num 值:"+num);
- }
- private static void changeValue(int num) {
- num = 5;
- System.out.println("形参 num 值:"+num);
- }
- }
打印结果:
修改前的 num 值: 3
形参 num 值: 5
修改后的 num 值: 3
可以发现, 传递基本数据类型时, 在函数中修改的仅仅是形参, 对实参的值的没有影响.
需要明白一点, 值传递不是简单的把实参传递给形参, 而是, 实参建立了一个副本, 然后把副本传递给了形参. 下面用图来说明一下参数传递的过程:
图中 num 是实参, 然后创建了一个副本 temp, 把它传递个形参 value, 修改 value 值对实参 num 没有任何影响.
传递类型是引用类型时:
- public class User {
- private int age;
- private String name;
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public User(int age, String name) {
- this.age = age;
- this.name = name;
- }
- public User() {
- }
- @Override
- public String toString() {
- return "User{" +
- "age=" + age +
- ", name='" + name + '\'' +
- '}';
- }
- }
- public class TestUser {
- public static void main(String[] args) {
- User user = new User(18, "zhangsan");
- System.out.println("修改对象前:"+user);
- changeUser(user);
- System.out.println("修改对象后:"+user);
- }
- private static void changeUser(User user) {
- user.setAge(20);
- user.setName("lisi");
- }
- }
打印结果:
修改对象前: User{age=18, name='zhangsan'}
修改对象后: User{age=20, name='lisi'}
可以发现, 传过去的 user 对象, 属性值被改变了. 由于, user 对象存放在堆里边, 其引用存放在栈里边, 其参数传递图如下:
user 是对象的引用, 为实参, 然后创建一个副本 temp, 把它传递给形参 user1. 但是, 他们实际操作的都是堆内存中的同一个 User 对象. 因此, 对象内容的修改也会体现到实参 user 上.
传递类型是 String 类型(Integer 等基本类型的包装类等同)
- public class TestStr {
- public static void main(String[] args) {
- String str = new String("zhangsan");
- System.out.println("字符串修改前:"+str);
- changeStr(str);
- System.out.println("字符串修改后:"+str);
- }
- private static void changeStr(String str) {
- str = "lisi";
- }
- }
打印结果:
字符串修改前: zhangsan
字符串修改后: zhangsan
咦, 看到这是不是感觉有点困惑. 按照第二种情况, 传递参数是引用类型时, 不是可以修改对象内容吗, String 也是引用类型, 为什么在这又不变了呢?
再次强调一下, 传递参数是引用类型, 并不代表就是引用传递, 其实它还是值传递. 此时的 lisi 和上边的 zhangsan 根本不是同一个对象. 画图理解下:
图中, str 是对象 zhangsan 的引用, 为实参, 然后创建了一个副本 temp, 把它传递给了形参 str1. 此时, 创建了一个新的对象 lisi , 形参 str1 指向这个对象, 但是原来的实参 str 还是指向 zhangsan. 因此, 形参内容的修改并不会影响到实参内容. 所以, 两次打印结果都是 zhangsan.
第三种情况和第二种情况虽然传递的都是引用类型变量, 但是处理方式却不一样. 第三种情况是创建了一个新的对象, 然后把形参指向新对象, 而第二种情况并没有创建新对象, 操作的还是同一个对象. 如果把上边 changeUser 方法稍作改变, 你就会理解:
- private static void changeUser(User user) {
- // 添加一行代码, 创建新的 User 对象
- user = new User();
- user.setAge(20);
- user.setName("lisi");
- }
运行以上代码, 你就会惊奇的发现, 最终打印修改前和修改后的内容是一模一样的.
这种情况, 就等同于第三种情况. 因为, 这里的形参和实参引用所指向的对象是不同的对象. 因此, 修改形参对象内容并不会影响实参内容.
修改对象前: User{age=18, name='zhangsan'}
修改对象后: User{age=18, name='zhangsan'}
总结:
从以上三个例子中, 我们就能理解了, 为什么 Java 中只有值传递, 并没有引用传递. 值传递, 不论传递的参数类型是值类型还是引用类型, 都会在调用栈上创建一个形参的副本. 不同的是, 对于值类型来说, 复制的就是整个原始值的复制. 而对于引用类型来说, 由于在调用栈中只存储对象的引用, 因此复制的只是这个引用, 而不是原始对象.
最后, 再次强调一下, 传递参数是引用类型, 或者说是对象时, 并不代表它就是引用传递. 引用传递不是用来形容参数的类型的, 不要被 "引用" 这个词本身迷惑了. 这就如同我们生活中说的地瓜不是瓜, 而是红薯一样.
参数传递时, 是拷贝实参的副本, 然后传递给形参.(值传递)
在函数中, 只有修改了实参所指向的对象内容, 才会影响到实参. 以上第三种情况修改的实际上只是形参所指向的对象, 因此不会影响实参.
来源: https://www.cnblogs.com/starry-skys/p/12168971.html