19.java基础-值传递

91 阅读11分钟

★JAVA是值传递

值传递:会创建副本

引用传递:不会创建副本

值传递和引用传递的误区

错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。

错误理解二:Java是引用传递。

错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。

我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法:

public static void main(String[] args)

这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。

形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。

实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。

在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。

public static void main(String[] args) {
    ParamTest pt = new ParamTest();
    String parameter = "Hollis";
    //实际参数为 parameter
    pt.sout(parameter);
}

//形式参数为 name
public void sout(String name) { 
    System.out.println(name);
}

实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。

值传递(pass by value)是指在调用函数时将实际参数parameter复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

**引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。** 

img

值传递:你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把新钥匙做什么都不会影响你手里的这把钥匙。

引用传递你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。

钥匙就是栈内地址,房子就是堆中的对象。

但是,不管上面那种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。

那你说你会不会受到影响?

而我们在pass方法中,改变user对象的name属性的值的时候,不就是在“砸电视”么。

什么是形参?什么是实参?

package base03_值传递;

public class ValueTransfer {

    private String name;
    private int age;

    public ValueTransfer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "ValueTransfer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public static void main(String[] args) {
        String name1 = "a";
        pass(name1);
        System.out.println(name2);
        ValueTransfer v = new ValueTransfer("a", 1);
        pass(v);
        System.out.println(v);
        ValueTransfer v2 = new ValueTransfer("a", 1);
        pass2(v2);
        System.out.println(v2);
    }

 
    public static void pass(String name2) {
        //相当于 new String("b");
        //这个可以通过pass2验证
        //和这个方法是同样的结果,不会影响实际参数。
        //然后再把这个新创建的字符串的引用交给name。
        //这个时候 main方法中name字符串指向的是 a
        //这个时候 main方法中name字符串指向的是 b
        name2 = "b";
        //方法执行完毕name被回收
    }

    public static void pass(ValueTransfer v) {
        v.name = "b";
        v.age = 2;
    }

    public static void pass2(ValueTransfer v) {
        v = new ValueTransfer("c", 3);
         v.age=4;
    }
}
/***
结果:
a
ValueTransfer{name='b', age=2}
ValueTransfer{name='a', age=1}
***/



/***
如果是值传递
假设name2的地址值是 0x123456
假设name1的地址值是 0x456789
那么形参和实参的地址值是不是指向了同一个对象?
答案:是,因为形参是实参地址的拷贝 形参和实参的地址值可能不一样,但是指向的对象是同1个。

对于方法pass(String name2)
String类的存储是通过final修饰的char[]数组来存放结果的,不可更改。一旦定义就是最终形态,任何试图改变String值的操作都只能重新开辟地址。
故当外部一个String类型的引用name1传递到方法内部时,只是把“外部String实例对象”的引用传递给了方法参数变量name2,使得外部String类型变量name1和方法参数变量name2都是实际char[]数组的引用而已。
当我们在方法体中改变name2时,因为char[]数组是不可变的,故每次修改都会导致创建一个新的String实例对象,而方法体中的方法参数name2就会指向这个新创建的String实例对象,而非指向原来外部的String实例对象了。故从方法执行前到方法执行后,外部String类型的引用name1始终指向原String实例对象。

但是对于方法pass(ValueTransfer v),对象的引用也有2个,同时指向了同一个内存地址的堆内存里的对象,当堆内存中的数据发生改变,
也就意味着这2个引用指向的堆内存里的对象发生了改变。

但是对于方法pass2(ValueTransfer v),对象的引用也有2个,同时指向了同一个内存地址的堆内存里的对象,当执行代码:
v = new ValueTransfer("c", 3);
意味着这2个引用指向的堆内存里的对象已经不是同1个对象了。
所以即使我们执行
 v.age=4;
也不会影响到外部的实际参数对象。

***/
package study;

public class User {
    String gender;
    String name;

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getGender() {
        return gender;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "User{" +
                "gender='" + gender + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

package study;

/***
 * 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
 * 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
 */
public class 值传递还是引用传递 {
    public static void main(String[] args) {
        值传递还是引用传递 pt = new 值传递还是引用传递();

        System.out.println(" ------------基本类型------------------ ");
        int i =20;
        System.out.println("print in main , i is " + i);
        pt.pass(i);
        System.out.println("print in main , i is " + i);

        System.out.println(" -------------引用类型1----------------- ");
        String name = "Hollis";
        System.out.println("print in main , name is " + name);
        pt.pass(name);
        System.out.println("print in main , name is " + name);

        System.out.println(" --------------引用类型2---------------- ");
        User hollis = new User();
        hollis.setName("Hollis");
        hollis.setGender("Male");
        System.out.println("print in main , hollis is " + hollis);
        pt.pass(hollis);
        System.out.println("print in main , hollis is " + hollis);


        System.out.println(" --------------引用类型3---------------- ");
        User hollis2 = new User();
        hollis2.setName("Hollis");
        hollis2.setGender("Male");
        System.out.println("print in main , tyrant is " + hollis2);
        pt.passAndNew(hollis2);
        System.out.println("print in main , tyrant is " + hollis2);

        System.out.println(" --------------引用类型4---------------- ");
        User hollis3 = new User();
        hollis3.setName("Hollis");
        hollis3.setGender("Male");
        System.out.println("print in main , tyrant is " + hollis3);
        pt.pass(hollis3);
        System.out.println("print in main , tyrant is " + hollis3);
    }

    public void pass(int j) {
        j=100;
        System.out.println("print in pass , j is " + j);
    }

    public void pass(String name) {
        //因为String是不可变对象
        //这里相当于new了一个String赋值给了形参
        //形参和实参指向的不是一个对象
        //修改不会影响实参属性
        
        //可以理解为name这个形参 和 实参指向了同1个对象 a
        //但是给name赋值的时候 ,由于String是不可变对象
        //因此 name = ("hollischuang"); 相当于  name = new String("hollischuang");
        //此时 name 相当于指向了 1个新对象 b
        name = ("hollischuang");
        System.out.println("print in pass , name is " + name);
    }



    public void passAndNew(User user) {
        //这个和String一样
        //形参 实参指向的不是同1个对象的引用
        //不会影响实参的值
        user = new User();
        user.setName("hollischuang");
        user.setGender("female");
        System.out.println("print in pass , user is " + user);
    }

    public void pass(User user) {
        //形参 实参指向1个对象的引用
        //修改会影响实参属性
        user.setName("hollischuang");
        System.out.println("print in pass , user is " + user);
    }

}

 ------------基本类型------------------ 
print in main , i is 20
print in pass , j is 100
print in main , i is 20
 -------------引用类型1----------------- 
print in main , name is Hollis
print in pass , name is hollischuang
print in main , name is Hollis
 --------------引用类型2---------------- 
print in main , hollis is User{gender='Male', name='Hollis'}
print in pass , user is User{gender='Male', name='hollischuang'}
print in main , hollis is User{gender='Male', name='hollischuang'}
 --------------引用类型3---------------- 
print in main , tyrant is User{gender='Male', name='Hollis'}
print in pass , user is User{gender='female', name='hollischuang'}
print in main , tyrant is User{gender='Male', name='Hollis'}
 --------------引用类型4---------------- 
print in main , tyrant is User{gender='Male', name='Hollis'}
print in pass , user is User{gender='Male', name='hollischuang'}
print in main , tyrant is User{gender='Male', name='hollischuang'}

以引用类型2为例

img

在参数传递的过程中,实际参数的地址0X1213456被拷贝给了形参,在这个方法中,并没有对形参本身进行修改,而是修改的形参持有的地址中存储的内容。即堆内存中的内容。

所以,值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。在判断实参内容有没有受影响的时候,要看传的的是什么,**如果你传递的是个地址,那么就看这个地址的变化会不会有影响,而不是看地址指向的对象的变化。**就像钥匙和房子的关系。钥匙就是栈内地址,房子就是堆中的对象。

以引用类型1为例

既然这样,为啥上面同样是传递对象,引用类型1和引用类型2的结果不一样呢?即传递的String对象和User对象的表现结果不一样呢?

一开始形参和实参指向的是同一个对象。

String name = "Hollis";

在调用方法时,因为String是不可变对象

name = ("hollischuang");

这里相当于new了一个String赋值给了形参

这时形参和实参指向的不是一个对象,所以实参对象的值还是Hollis

我们在引用类型1中的pass方法中使用name = "hollischuang";

试着去更改name的值,阴差阳错的直接改变了形参name的引用的地址。

因为这段代码,会new一个String,在把引用交给name。

name = "hollischuang";等价于name = new String("hollischuang");

而原来的那个”Hollis”字符串还是由实参持有着的,所以,并没有修改到实际参数的值。

img

以引用类型3为例

img

稍微解释下这张图,当我们在main中创建一个User对象的时候,在堆中开辟一块内存,其中保存了name和gender等数据。然后hollis持有该内存的地址0x123456(图1)。当尝试调用pass方法,并且hollis作为实际参数传递给形式参数user的时候,会把这个地址0x123456交给user,这时,user也指向了这个地址(图2)。然后在pass方法内对参数进行修改的时候,即user = new User();,会重新开辟一块0X456789的内存,赋值给user。后面对user的任何修改都不会改变内存0X123456的内容(图3)。

上面这种传递是什么传递?肯定不是引用传递,如果是引用传递的话,在user=new User()的时候,实际参数的引用也应该改为指向0X456789,但是实际上并没有

通过概念我们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。所以上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数。

所以说,Java中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用。

还有一个问题就是等号左边的内容是栈内存中的内容。也就是说 实参和形参指向了同一个内存的地址。

在引用类型1中,String被重新赋值,相当于new String 赋值给了形参。但是实际参数的指针还是指向原来的String。所以经过pass方法不会改变。

在引用类型2中,User的name属性被重新赋值。实参和形参同时指向的是同一个内存的地址。因此影响到了实参的内容。

在引用类型3中,新创建了一个User。实参和形参同时指向的不是同一个内存的地址。因此影响不到实参的内容。