关于Java值传递和引用传递之间的困扰

前言

最近由于要拓宽自己的技能和学习有关Java的课程需要学习Java,便开始了自学Java的过程,但是学习过程中往往会有一些难以解决的问题。今天出现的这个问题就是我迈入Java大门首次要击败的一个小boss。

Java有很多和C++类似的部分,比如new就是引用C++的关键字。在《Java核心技术》中有很多C++的辨析部分供读者参考。我也是本书受益者之一,但是由于曾经接触过别的一些类似的编程语言,所以我在学习的过程中出现了很多惯性思维导致理解错误的部分。

注:

问题的出现

在引用参数的过程中,我写了交换对象的方法,但是在运行后发现对象并没有被交换。

public static void cgobj(demo st1,demo st2) {
		demo swap=new demo();
		swap=st1;
		st1=st2;
		st2=swap;

在main中我写下了如下内容后对象被交换

demo swap=new demo();
		swap = stu[1];
		stu[1]=stu[2];
		stu[2]=swap;

便产生了很大的疑问,为什么对象没有被交换

两种参数的传递方式

有过编程基础的人应该知道参数传递的两种方式

  • 值传递
  • 引用传递

由于Java没有引入指针这一概念,我们首先得知道两种传递是什么

值传递

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

引用传递

函数参数的传递方式有引用传递。所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

用C语言解释值传递和引用传递

形参为形式参数,实参为实际参数,为了简写这一概念,下文我将会用形参和实参代替。
如果在C语言中,那么很好理解这一概念

#include<studio.h>
void swap(int x,int y)
{
	int t;
	t=x;
	x=y;
	y=t;
}
int main()
{
	int a=20,b=25;
	printf("a=%d,b=%d",a,b);
	swap(a,b);
	printf("a=%d,b=%d",a,b);
	return 0;
}

程序结果:
a=20,b=25
a=20,b=25

参数的传递过程是将实参的”值“传递给形参,因此参数的传递是单向传递。其本质是在函数调用发生时,系统给形参x,y开辟内存,然后将实参的”值“复制一份给了形参,随后结束参数的传递过程。在参数传递结束后,实参a,b和形参x,y之间没有关系,所以当被调用函数修改x,y的值时,不会对主调函数中的实参产生任何影响。所以变量a和b并没有发生实际上的交换,这便是所谓的值传递。

我们再来看一组例子

#include<studio.h>
void swap(int *x,int *y)
{
	int t;
	t=*x;
	*x=*y;
	*y=t;
}
int main()
{
	int a=20,b=25;
	int *pa=&a,*pb=&b;
	printf("a=%d,b=%d",a,b);
	swap(pa,pb);
	printf("a=%d,b=%d",a,b);
	return 0;
}

程序结果:
a=20,b=25
a=25,b=20

将形参修改指针变量后,主函数中的变量a和b发生了交换,这一交换显然是swap函数来完成的。

先来分析参数传递。由于实参pa和pb分别存放的是a和b的内存地址,因此pa和pa分别是a和b的间接引用形式。形参指针变量分别收到的是pa和pb中存放的内存地址。
然后再来看交换过程。x和y便指向的是a和b在内存中的地址,x和y通过内存地址间接访问的方式修改了a和b变量中存放的值,所以直接影响到了实际参数。

Java中的值传递

在Java中的数据类型分为两种,一种为基本类型,另一种为引用类型。基本类型的传递方式是值传递这点我相信大家都是毋庸置疑的。

Java中的基本类型
  • byte
  • short
  • int
  • long
  • float
  • double
  • boolean
  • char

参数的传递过程是将实参的”值“传递给形参,因此参数的传递是单向传递。形参并不会影响到实参。

引用类型

我们先来看一组例子

package demo;

public class demo {
	private String name;
	public demo(String pubname) {
		this.name=pubname;
	}
	public String getName() {
		return name;
	}
	public static void swapObj(demo swapObj1,demo swapObj2) {
		demo swap=swapObj1;
		swapObj1=swapObj2;
		swapObj2=swap;
	}
	
	public static void main(String[] args) {
		demo[] stu=new demo[3];
		stu[0]=new demo("Tusuki");
		stu[1]=new demo("Tom");
		stu[2]=new demo("Jarry");
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
        System.out.println("————————————————————");
		demo swap=stu[1];
		stu[1]=stu[2];
		stu[2]=swap;
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
	}
}

程序运行结果:
Tusuki
Tom
Jarry

————————————————————

Tusuki
Jarry
Tom

我们看到Tom和Jarry对象输出位置交换了,仔细读代码的同学会发现demo有一个swapObject的方法,我们试着使用swapObject方法来交换试试

package demo;

public class demo {
	private String name;
	public demo(String pubname) {
		this.name=pubname;
	}
	public String getName() {
		return name;
	}
	public static void swapObj(demo swapObj1,demo swapObj2) {
		demo swap=swapObj1;
		swapObj1=swapObj2;
		swapObj2=swap;
	}
	
	public static void main(String[] args) {
		demo[] stu=new demo[3];
		stu[0]=new demo("Tusuki");
		stu[1]=new demo("Tom");
		stu[2]=new demo("Jarry");
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
        System.out.println("————————————————————");
		demo.swapObj(stu[1], stu[2]);
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
	}
}

程序运行结果:
Tusuki
Tom
Jarry

————————————————————

Tusuki
Tom
Jarry

为什么会这样呢?对象没有发生交换,仔细阅读代码,代码也没有存在任何问题。

先来分析这里

demo[] stu=new demo[3];
		stu[0]=new demo("Tusuki");
		stu[1]=new demo("Tom");
		stu[2]=new demo("Jary");

创建一个demo类的对象数组stu
使用demo的构建函数分别赋值。
这里没有任何问题,我们创建了三个对象

demo swap=stu[1];
		stu[1]=stu[2];
		stu[2]=swap;

这时候我们要分清楚一个概念,对象和对象引用
如果用惯性思维去思考这段代码的话,我们会认为

  • 创建一个swap数组,把stu[1]赋给swap
  • stu[2]赋给stu[1]
  • swap赋给stu[2]
  • 完成对象的交换
    这样的思维是错误的,并没有直接改变内存中对象的值,只是对象引用的改变。虽然输出结果中看到对象交换了,但在堆中,对象并没有发生任何改变。
对象和对象引用

首先得理解什么是对象,什么又是对象的引用
我先举一个创建对象的例子

demo tsuki = new demo("Tsuki");

创建一个demo类的对象Tsuki,并使用demo类构建方法
我分成两个语句来解释这个比较友好

demo tsuki;
	tsuki = new demo("Tsuki");

这样应该比较容易理解,这里有两个实例,一个是对象引用变量,另一个是对象本身。
在堆空间里创建实例,与在数据段以及栈空间里创建的实例不同。一个类可以创建出无数个对象,这些对象都有各自不同的引用变量。对象连名都没有,没法直接访问它。我们只能通过对象引用来间接访问对象。这时候可能有点疑惑,tsuki不是对象的名字吗?tsuki是对象的引用而不是对象的实例,对象存放在堆空间中需要通过对象的引用来使用对象。说道这里应该有点恍然大悟的感觉,我们再来看这段代码

demo swap=stu[1];
		stu[1]=stu[2];
		stu[2]=swap;

这里并不是把对象的实例交换了而是仅仅交换对象的引用,我们用指针的思维去思考这个问题应该很容易理解,把stu对象数组和swap看成是指针,会很容易理解这个问题。

我们再重新来看这段代码

public static void swapObj(demo swapObj1,demo swapObj2) {
		demo swap=swapObj1;
		swapObj1=swapObj2;
		swapObj2=swap;
	}

这里是把对象引用变量的“值”传递给了形参,实参并没有发生任何实际的变化,形参是对象引用变量并不是对象本身,形参是通过对象引用来间接访问对象。在本例中,实参变量值是对象的引用,形参是实参值传递后的引用,形参发生了改变但是实参并没有发生改变。

再来看个例子就可以很容易理解这个概念

package demo;

public class demo {
	private String name;
	public demo(String pubname) {
		this.name=pubname;
	}
	public String getName() {
		return name;
	}
	public static void swapObj(demo [] swapObject) {
		demo swap=swapObject[1];
		swapObject[1]=swapObject[2];
		swapObject[2]=swap;
		for(int i=0;i<3;i++) {
			System.out.println(swapObject[i].getName());
		}
		System.out.println("————————————————————");
	}
	
	public static void main(String[] args) {
		demo[] stu=new demo[3];
		stu[0]=new demo("Tusuki");
		stu[1]=new demo("Tom");
		stu[2]=new demo("Jarry");
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
		System.out.println("————————————————————");
//		demo swap=stu[1];
//		stu[1]=stu[2];
//		stu[2]=swap;
		demo.swapObj(stu);
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
	}
}

程序运行结果:

Tusuki
Tom
Jarry
————————————————————
Tusuki
Jarry
Tom
————————————————————
Tusuki
Jarry
Tom

通过这个例子发现在方法内输出的内容发生了改变,而在main方法内没有发生任何改变。

我们再来修改一下swap的名字试试stu[1]会不会发生变化

package demo;

public class demo {
	private String name;
	public demo(String pubname) {
		this.name=pubname;
	}
	public String getName() {
		return name;
	}
	public static void swapObj(demo [] swapObject) {
		demo swap=swapObject[1];
		swapObject[1]=swapObject[2];
		swapObject[2]=swap;
		for(int i=0;i<3;i++) {
			System.out.println(swapObject[i].getName());
		}
		System.out.println("————————————————————");
	}
	
	public static void main(String[] args) {
		demo[] stu=new demo[3];
		stu[0]=new demo("Tusuki");
		stu[1]=new demo("Tom");
		stu[2]=new demo("Jarry");
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
		System.out.println("————————————————————");
//		demo swap=stu[1];
//		stu[1]=stu[2];
//		stu[2]=swap;
		demo.swapObj(stu);
		for(int i=0;i<3;i++) {
			System.out.println(stu[i].getName());
		}
	}
}

程序运行结果:

Tusuki
Tom
Jarry
————————————————————
Tusuki
Jarry
newName

看到这里我相信已经理解Java中的值传递和引用传递了