Java中对对象采用的是值传递还是引用传递呢?

    在理解Java中对对象采用的是值传递还是引用传递之前,我们需要明白Java中的形参和实参、值传递和引用传递的相关内容,以及Java虚拟机内存的划分。相关内容可阅读之前的博文,其中对于上述内容已经有了初步的讲解。

    图形化理解Java中的形参和实参

    那么究竟Java中对对象采用的是值传递还是引用传递呢?不管是基本数据类型还是对象,Java中的参数传递只有值传递一种!如果Java中的参数传递只有值传递一种,那么为什么还有值传递和引用传递呢?在之前的文章中已经通过例子说明了:引用传递的其实传递的是引用对象在堆内存中的地址,说到底也是值传递。

    下面我们再通过一个具体的例子深刻的理解一下,为什么说Java中只有值传递这一种参数传递类型。首先,我们创建一个Employee类,类中的成员属性有name和salary两个,然后添加全参构造方法和对应的getter和setter。

    1. public class Employee {
    2. private String name;
    3. private int salary;
    4. public Employee(String name, int salary) {
    5. this.name = name;
    6. this.salary = salary;
    7. }
    8. public String getName() {
    9. return name;
    10. }
    11. public void setName(String name) {
    12. this.name = name;
    13. }
    14. public int getSalary() {
    15. return salary;
    16. }
    17. public void setSalary(int salary) {
    18. this.salary = salary;
    19. }
    20. }

    首先,我们看一下基本类型的值传递。tripleSalary(int s)希望将某个雇员的工资变为三倍,方法定义如下:

    1. public class Test {
    2. public static void main(String[] args) {
    3. Employee e1 = new Employee("Forlogen", 1000);
    4. Employee e2 = new Employee("kobe", 2000);
    5. // 按值传递修改对象值
    6. System.out.println(e1.getSalary()); // 1000
    7. tripleSalary(e1.getSalary());
    8. System.out.println(e1.getSalary());
    9. System.out.println("-------------"); // 1000
    10. }
    11. public static void tripleSalary(int s){
    12. s = s * 3;
    13. System.out.println("s is : " + s); // 3000
    14. }
    15. }

    从输出结果中可以看出,虽然在方法中传入的参数发生了改变,但是e1对象的salary本身并没有改变。这也又验证了前面的说法,对于基本数据类型值传递来说,tripleSalary(int s)方法中的s是e1对象的salary的一个副本,s的变化并不会影响到e1对象的salary的值。

    如果我们直接传递的就是对象,然后在方法体中修改salary的值,那么对象本身的salary是否会发生变化呢?请看下面的实验结果:

    1. package OOP;
    2. public class Test {
    3. public static void main(String[] args) {
    4. Employee e1 = new Employee("Forlogen", 1000);
    5. Employee e2 = new Employee("kobe", 2000);
    6. // 按引用传递修改对象值
    7. System.out.println(e2.getSalary()); // 2000
    8. tripleSalary(e2);
    9. System.out.println(e2.getSalary()); // 6000
    10. }
    11. public static void tripleSalary(Employee e){
    12. e.setSalary(e.getSalary() * 3);
    13. System.out.println("e's salary is: " + e.getSalary()); // 6000
    14. }
    15. }

    从输出结果中可以看出,e2本身salary值为2000,将对象传入tripleSalary(Employee e),方法体中对于salary的改变也影响到了呃e2的salary的值。这里形参看起来就是引用传递,怎么就是值传递呢?下面我们通过图解的方法来看一下两次调用不同类型的`tripleSalary()在内存中发什么了什么变化:
    Java中的值传递-1.png

    如上图所示,值传递修改的是副本中的值,那么副本中的值改变自然不会影响作为实参的值。而在tripleSalary(Employee e)中传入的虽然是对象,但是这里的形参也是实参的一个副本,即方法中e的值是e2值的副本,它们都是对象在堆内存中的地址。由于它们的保存的地址值相同,因此实际指向了同一个对象,副本中值的改变也就表现在了它所指向的堆内存中对象的属性值的改变。

    那么如果形参是对象,方法体中并不涉及对象中属性值的更改,那么它还是值传递嘛?下面我们通过另一个例子看一下,假设新建swap(Employee x, Employee y),我们想要交换两个对象的引用,那么它们的交换会影响本身对象的指向嘛?根据前面的分析,我们可以猜想应该是不会的。而实际输出结果也验证了我们的想法,如下所示:

    1. public class Test {
    2. public static void main(String[] args) {
    3. Employee e1 = new Employee("Forlogen", 1000);
    4. Employee e2 = new Employee("kobe", 2000);
    5. // 引用传递交换
    6. System.out.println("MAIN -- e1's salary is: " + e1.getSalary() + " and e2's salary is: " + e2.getSalary());
    7. // MAIN -- e1's salary is: 1000 and e2's salary is: 6000
    8. swap(e1, e2);
    9. System.out.println("MAIN -- e1's salary is: " + e1.getSalary() + " and e2's salary is: " + e2.getSalary());
    10. // MAIN -- e1's salary is: 1000 and e2's salary is: 6000
    11. public static void swap(Employee x, Employee y){
    12. Employee e = x;
    13. x = y;
    14. y = e;
    15. System.out.println("SWAP -- x's salary is: " + x.getSalary() + " and y's salary is: " + y.getSalary());
    16. // SWAP -- x's salary is: 6000 and y's salary is: 1000
    17. }
    18. }

    怎么理解上面的输出呢?下面同样采用图解的方法来进行理解:
    Java中的值传递-2.png

    如上所示,swap(Employee x, Employee y)中的x和y也是e1和e2分别对应的副本,它们分别保存的是e1和e2本身指向对象在堆内存中的地址。因此,x和y进行的一系列操作并不会影响到e1和e2。所以,虽然方法体内x和y的指向发生了改变,但这些改变仅限于方法体内部,e1和e2本身不受影响。

    如果真的想做到类似于交换对象的效果,只能采用分别交换传入的对象的属性值实现,如下所示:

    1. package OOP;
    2. public class Test {
    3. public static void main(String[] args) {
    4. Employee e1 = new Employee("Forlogen", 1000);
    5. Employee e2 = new Employee("kobe", 2000);
    6. // 按值传递交换
    7. System.out.println("MAIN -- e1's salary is: " + e1.getSalary() + " and e1's name is: " + e1.getName());
    8. System.out.println("MAIN -- e2's salary is: " + e2.getSalary() + " and e1's name is: " + e2.getName());
    9. /*
    10. MAIN -- e1's salary is: 1000 and e1's name is: Forlogen
    11. MAIN -- e2's salary is: 6000 and e1's name is: kobe
    12. */
    13. swap(e1, e2);
    14. System.out.println("MAIN -- e1's salary is: " + e1.getSalary() + " and e1's name is: " + e1.getName());
    15. System.out.println("MAIN -- e2's salary is: " + e2.getSalary() + " and e1's name is: " + e2.getName());
    16. /*
    17. MAIN -- e1's salary is: 6000 and e1's name is: kobe
    18. MAIN -- e2's salary is: 1000 and e1's name is: Forlogen
    19. */
    20. }
    21. public static void swap(Employee e1, Employee e2){
    22. int t1 = e1.getSalary();
    23. e1.setSalary(e2.getSalary());
    24. e2.setSalary(t1);
    25. String name = e1.getName();
    26. e1.setName(e2.getName());
    27. e2.setName(name);
    28. }
    29. }

    综上所述,可得Java中对方法参数能做什么和不能做什么:

    • 方法不能修改基本数据类型的参数
    • 方法可以改变对象参数的状态
    • 方法不能让一个对象参数引用一个新的对象

    最后牢记:不管是基本数据类型还是对象,Java中的参数传递只有值传递一种!