Java的按值传递

问题:
最近在看Martin Fowler的《重构》一书,书中在讲临时变量的时候提到,编程的时候尽量不要去改变入参的值,因为这样的当时开发者来说是比较能理解的,但是对于后续维护者来说,这个就会比较头大。因为有时候我们根本就搞不明白为什么进入的时候是这样的,出来的为什么不是我要的值呢。因此, Martin Fowler建议如果要对入参做改变,可以定义一个返回值,然后把这个返回值重新复制给一个新的变量。
e.g.
public class Demo {
	static User user1 = new User("clu", 111);
	
	public static void main(String args[]){
		User user2 = changeUser(user1);
		System.out.println(user2.getName());
	}
	
	public static User changeUser(User user) {
		User userchanged = user;
		userchanged.setName("jack");
		return userchanged;
	}
}


output:
jack

虽然,代码多了一些,但是对于后续的维护是有大大的好处的,毕竟程序是写给人看的。

为了不改变入参的值,Martin Fowler还建议要给形参加上final修饰符,这样这个变量就不会被强制复制了。
说到这里的时候,提到了java开发经常会出错的一个点就是:java 的值传递问题。


名词解释:
值传递:java程序中,调用者调用被调用者的时候,通常都是把调用者的参数的值“拷贝”一份来进行操作的。 Java里面所有的传递都是按值传递

简单说就是上面的main方法在调用changeUser方法的时候,会进行user=user1的赋值操作。也就是说,实际上我们在changeUser里做的任何改动都是对拷贝出来的副本进行的改动,不会对原来的值user1造成任何影响。那么上面的代码为什么user的值被改变了呢? 我们这里的原因有两个,一个是我们在main方法里对user重新进行了赋值。 第二个我想说的是,这里即使没有赋值也就是代码变成这样:
public class Demo {
	static User user1 = new User("clu", 111);
	
	public static void main(String args[]){
//		User user2 = changeUser(user1);
		System.out.println(user1.getName());
	}
	
	public static User changeUser(User user) {
		User userchanged = user;
		userchanged.setName("jack");
		return userchanged;
	}
}

output也还是这样的
output:
jack


下面就是解释一下原因:

例子1:基本数据类型:
//code from refactorying
	public static void main(String[] args){
		int x = 5;
		triple(x);
		System.out.println("x after triple: " + x);
	}
	
	/**
	 * @param x
	 */
	private static void triple(int x) {
		// TODO Auto-generated method stub
		x = x *3;
		System.out.println("x in triple :  " + x);
	}

output:
x in triple :  15
x after triple: 5


也就是说我们期望的想把5,放大3倍的效果并没有出来。为什么,因为在triple方法体内,我们是对另外一个副本进行了操作,但是在方法体外,x还是原来的那个值,除非,你对它重新赋值。
例如这样:
public static void main(String[] args){
		int x = 5;
		x= triple(x);
		System.out.println("x after triple: " + x);
	}
	
	/**
	 * @param x
	 */
	private static int triple(int x) {
		// TODO Auto-generated method stub
		x = x *3;
		System.out.println("x in triple :  " + x);
		return x;
	}



例子2:引用类型(对象,数组):
对象:
	public static void main(String[] args){
		User user = new User("clu" ,  123);
		rename(user);
		System.out.println(" user after User name: " + user.getName());
	}
	
	/**
	 * @param user
	 */
	private static void rename(final User user) {
		// TODO Auto-generated method stub
		if(user != null) {
			user.setName("xxxxxx");
		}
		System.out.println(" user in User name: " + user.getName());
	}


output:
 user in User name: xxxxxx
 user after User name: xxxxxx

你会发现username被修改了,你很知道,为什么,不是说只对副本进行操作么,这里的值为什么会被修改了呢。
那就让我来解释一下吧。 我们都知道对象是引用类型,那么引用类型,它的值是什么呢,它总有一个值吧,不然这个东西不可能凭空存在的啊。对的,引用对象它说存的值是一个对象实例在堆内存中的地址,也就是类似:0x0000F000的值,它也是一个值,只不过它指向了另一个内存区域。这就是为什么说java中只有按值传递的原因了。
我们这个例子中,我们调用rename方法,这时我们把user的引用地址copy了一份,但是,当我们执行.操作的时候,我们是去修改的这个引用所指向的真实内存空间里的值。 就好比我在淘宝上买了个电视,我把我家的地址copy一份给快递员,快递员拿到我家的地址之后,就往我家里送电视,等电视送到了,我家里就多了一个电视的。而不是别人的家里多了电视。

同样的,如果我改动代码如下,这里先把final去掉:
	public static void main(String[] args){
		User user = new User("clu" ,  123);
		rename(user);
		System.out.println(" user after User name: " + user.getName());
	}
	
	/**
	 * @param user
	 */
	private static void rename( User user) {
		// TODO Auto-generated method stub
//		if(user != null) {
//			user.setName("xxxxxx");
//		}
		user = new User("jack", 333);
		System.out.println(" user in User name: " + user.getName());
	}


output就变成这样了。
output:
 user in User name: jack
 user after User name: clu

原因就是,user这个地址指向了其他人的地址,也就是我给了一个其他人的地址给快递员,东西并没有送到我家里来,那么我家里还是原来的样子,东西不会多也不会少。

数组也是一样的道理, 如下:
	public static void main(String[] args) {
		int[] count = { 1, 2, 3, 4, 5 };    
		change(count);
		System.out.println(" user after User name: " + count[0]);
	}
	
	/**
	 * @param count
	 */
	private static void change(int[] count) {
		// TODO Auto-generated method stub
		count[0] = 6;
	}


output:
user after User name: 6

这个值会被改变就等同于,user的真实内存内容被修改了。

例子3:final修饰

	public static void main(String[] args) {
		User user = new User("clu", 1111);
		rename(user);
	}
	
	/**
	 * @param user
	 */
	private static void rename(final User user) {
		if(user != null) {
			user.setName("xxxxxx");
		}
//		user = new User("jack", 333);
		System.out.println(" user in User name: " + user.getName());
	}

当用final修饰时,这时的形参是不能被改变的,也就是这里那个注释掉的代码是不能被执行的,原因就是地址是不能再被重新赋值的,而setter能执行就说明,当我们执行setter的时候,并没有对这个地址值本身进行操作,而是对这个地址所指向的堆内存进行了操作。


不知道各位有没有跟理解一点呢 。 。 。
----EOF----

猜你喜欢

转载自xfxlch.iteye.com/blog/2273518