一、前言

在Java中,我们经常会使用到赋值操作符"=",但是一般都是直接使用,而没有去注意这个操作符带来的一些陷阱。小菜也是最近看了《Java编程思想第四版》才注意到这一点,也就是博文标题所说的“别名现象”。


二、别名现象

2.1 别名现象的发生场景

  • 对对象进行赋值时
  • 方法调用中,传递一个对象时



2.2 对对象进行赋值时的别名现象


  • Person.java:很简单的一个类,仅仅拥有一个属性
<span >public class Person {

	int age;
	
}</span>


  • Client.java:场景类或测试类
<span >public class Client {

	public static void main(String[] args) {
		Person p1 = new Person();
		Person p2 = new Person();
		
		p1.age = 18;
		p2.age = 21;
		System.out.println("1. p1.age: " + p1.age + ", p2.age: " + p2.age);
		
		p1 = p2;		// 使 p2 和 p1 拥有相同的对象引用
		System.out.println("2. p1.age: " + p1.age + ", p2.age: " + p2.age);
		
		// 注意 3 的输出
		p1.age = 18;
		System.out.println("3. p1.age: " + p1.age + ", p2.age: " + p2.age);
	}

}</span>



输出如下:



<span >/**********************
* 1. p1.age: 18, p2.age: 21 *
* 2. p1.age: 21, p2.age: 21 *
* 3. p1.age: 18, p2.age: 18 *
*******************************/</span>


  • 分析:


  主要关注第三行的输出,可以发现当 p1 的 age 值修改为 18 后,p2 的 age 也变为 18 了,这是为什么?why?


  原因是因为,当进行对象赋值操作时,如此处的 p2 = p1; 当该条语句执行完毕,p2 和 p1 将拥有对同一个对象的引用。当对 p1 中的属性进行修改时,因为是相同的对象引用,所以 p2 的值自然也是随之修改了。我们可以类比于C ,在 C 语言中使用指针操作一个内存块,内存块内存储的值被修改了,那么所有指向该内存的指针变量的值都会被修改[注:C语言学的不好]




2.3 方法调用中的别名现象


    将一个对象传递给方法时,也会产生别名问题。


  • Person.java
<span >public class Person {

	char sex;
	
}</span>


  • Client.java
<span >public class Client {

	public static void f(Person person){
		person.sex = 'W';
	}
	
	public static void main(String[] args) {
		Person person = new Person();
		person.sex = 'M';
		System.out.println("1. person.sex: " + person.sex);
		
		// 注意 2 的输出
		f(person);
		System.out.println("2. person.sex: " + person.sex);
		
	}
	
}</span>



输出如下:



<span >/**********************
* 1. person.sex: M *
* 2. person.sex: W *
**********************/</span>



  • 分析

实际改变的是 f() 函数之外的对象。






关于方法中别名现象问题的特别案例:我们知道对于数组对象或Objec类的导出类对象,在将这些对象作为参数传递给方法时,会产生别名现象。但是一个特别要注意的一点就是,别名现象出现的场景!!!只有我们在方法中,直接操作对象的方法来修改该对象的值时,才会产生别名现象。若是在方法内,对该对象重新赋值,即重新 new 一下,那么是不会出现别名现象的。原因待会儿解释,先给出案例。如下:



<span >public class Test {

	@org.junit.Test
	public void testArr() {
		int[] arr = new int[]{1, 2, 3, 4};
		changeArr(arr);
		for (int i = 0; i < arr.length; i ++) {
			System.out.print(arr[i] + " ");
		}
	}
	
	private void changeArr(int[] arr) {
		// 若是直接在 new 一个数组,则在 testArr 中打印,, arr 没有出现别名现象,还是 1 2 3 4
		arr = new int[]{2, 2, 3, 4};
		// 直接修改 arr 中元素的值,则 arr 出现改变。 2 2 3 4
//		arr[0] = 2;
	}
	
	@org.junit.Test
	public void testPerson () {
		Person p = new Person();
		p.setId(1);
		p.setName("johnnie");
		p.setAge(22);
		p.setSex(Sex.MAN);
		changePerson(p);
		System.out.println(p);
	}

	private void changePerson(Person p) {
//		p = new Person();
//		p.setId(1);
//		p.setName("Lisa");
//		p.setAge(22);
//		p.setSex(Sex.WOMAN);
		
		p.setAge(21);
	}
	
}</span>



在 Test.java 中,我们给出了 testArr() 和 testPerson() 方法,分别用于测试数组对象和普通对象在将对象作为参数在方法传递过程中别名现象的情况。我们使用两种情况,一种是直接 new 出一个新对象赋值给形参,然后再执行 testXXX 方法;另一种是直接操作形参,即使用形参的方法直接修改其内的值,然后再执行 testXXX 方法。分别查看两种情况的结果:



① 第一种情况下:


  • testArr() 的显示:直接就是 1 2 3 4
  • testPerson() 的显示:直接就是 Person [id=1, name=johnnie, age=22, sex=男]

② 第二种情况下:



  • testArr() 的显示:直接就是 2 2 3 4 
  • testPerson() 的显示:直接就是 Person [id=1, name=johnnie, age=21, sex=男]


可以发现,第一种情况下,在外部打印的结果是对象没有改变。而第二种情况下,对象改变了。那么这是为什么呢?


原因很简单,我们以 changeArr 方法来解释。






在第一种情况下,我们在 changeArr() 中,是使用 new 来改变的,而使用 new 关键字呢,JVM 就会在堆中新开辟一个空间,存储新的数组对象,而 arr 就指向堆中新内存地址的引用。但是,在 changeArr() 中,该 arr 还仅仅是局部变量,其生命周期是和 changeArr() 一样的,与其共存亡的,而我们 changeArr() 方法是没有返回值的,也就是我们没有把 arr 返回,让 testArr() 中的 arr 发生改变,让其重新指向堆中新开辟的内存地址,因此第一种情况是不会发生变化,还是 1 2 3 4。






在第二种情况下,就更容易理解了。因为是直接使用 arr 来修改的,在该片内存空间中直接改变值,所以,即使没有返回值,形参 arr 与 changeArr() 同时挂逼了,外部的 arr 还是指向原来的空间,但是此时该片空间中的数据却已经发生了改变。因此,结果就变为 2 2 3 4 了。






三、综合案例:



     一般我们别写程序不会刚刚那样,而是综合起来的。如下:



  • Address.java:
<span >/**
 * Address:负责记录地址信息
 * @author johnnie
 *
 */
public class Address {
	
	private String address;

	public String getAddress() {
		return address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

}</span>



  • Person.java:
<span >public class Person {

	// 基本属性
	private String name;
	private Address address;
	
	// Getter和Setter 
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	
	public Address getAddress() {
		return address;
	}
	
	public void setAddress(Address address) {
		this.address = address;
	}
	
}</span>



  • Client.java:
<span >/**
 * 场景类
 * @author johnnie
 *
 */
public class Client {

	public static void main(String[] args) {
		// 创建一个 Person 和 Address 类实例
		Person person = new Person();
		Address address = new Address();
		address.setAddress("WuHan");
		System.out.println("1. 将作为参数传递的地址信息为 address:" + address.getAddress());
		
		// 初始化 Person 类实例
		String name = "johnnie";
		person.setName(name);
		person.setAddress(address);
		System.out.println("2. " + person.getName() + "住在" + person.getAddress().getAddress());
		
		// 取得地址信息,并放在另一个 Address 类对象中,修改地址信息
		Address addr = person.getAddress();
		addr.setAddress("BeiJing");
		System.out.println("3. 新修改的地址信息 addr: " + addr.getAddress());
		System.out.println("4. 当时作为参数传递的地址信息 address: " + address.getAddress());
		System.out.println("5. Person 类对象中的地址信息 person.address: " + person.getAddress().getAddress());
		
		// 获取姓名,我要改名字了,从现在开始,我叫小明
		String xiaoming = person.getName();
		xiaoming = "XiaoMing";
		System.out.println("6. xiaoming: " + xiaoming);
		System.out.println("7. Person 类对象中的姓名 person.name:" + person.getName());
	}
	
}</span>


输出如下:


<span >1. 将作为参数传递的地址信息为 address:WuHan
2. johnnie住在WuHan
3. 新修改的地址信息 addr: BeiJing
4. 当时作为参数传递的地址信息 address: BeiJing
5. Person 类对象中的地址信息 person.address: BeiJing
6. xiaoming: XiaoMing
7. Person 类对象中的姓名 person.name:johnnie</span>


通过输出,我们可以发现,String 类型的数据操作,并不会出现别名现象。而在操作对象的过程中,别名现象出现了。




四、小结


值传递和引用传递的区别。



附注:


1. 对于“在Java中,参数传递都是按值传递”这一句话,这句话说的也并没有错。因为若是“值传递”,那么是传递的值的拷贝,这句话肯定是对的。那么若是“按引用传递”,可以理解为传递的是引用的地址,这也是一个值。从这种情况来理解的话,这句话是没错的。


2. 在 Java 中,只有基本数据类型和 String 类型是按值传递,在操作过程中也不会出现别名现象,而其他数据类型(对象)调用时,一定得注意别名现象[注:对基本数据类型赋值时,不会出现别名现象。因为基本数据类型存储了实际的数值,而并非是指向一个对象的引用]




Note:前往 bascker/javaworld 获取更多 Java 知识