操作符=用来赋值。它的意思是 “取等号右边的值(一般称作右值),把它复制给等号左边(一般称作左值)”。右值可以是任何常量、变量或者可以产生值的表达式。但左值必须是一个独特的命名变量(也就是说,必须有一个物理空间来存储右值)。比如,你可将一个常量赋值给一个变量:
a = 4;
但不能把任何东西赋值给一个常量,常量不能作为左值(不能说4=a;)。

基本类型的赋值是很直观的。基本类型存储了实际的值,而非指向一个对象的引用,所以在为其赋值的时候,你直接将一个地方的内容复制到了另一个地方。例如,对基本数据类型而言,a = b 就是将 b 的内容复制给 a。如果你接着修改了 a,b 并不会受这个修改的影响,大多数情况下这也是我们所期望的。

不过在给对象赋值的时候,情况发生了变化。当操作一个对象的时候,我们真正操作的是这个对象的引用。所以当 “将一个对象赋值给另一个对象” 时,你其实是将这个引用从一个地方复制到另一个地方。这意味着对对象而言,c = d 就是将 c 和 d 都指向原本只有 d 指向的那个对象。下面这个示例将演示这种行为:

  1. // operators/Assignment.java
  2. // 使用对象赋值还是有点棘手的
  3. class Tank {
  4. int level;
  5. }
  6. public class Assignment {
  7. public static void main(String[] args) {
  8. Tank t1 = new Tank();
  9. Tank t2 = new Tank();
  10. t1.level = 9;
  11. t2.level = 47;
  12. System.out.println("1: t1.level: " + t1.level +
  13. ", t2.level: " + t2.level);
  14. t1 = t2;
  15. System.out.println("2: t1.level: " + t1.level +
  16. ", t2.level: " + t2.level);
  17. t1.level = 27;
  18. System.out.println("3: t1.level: " + t1.level +
  19. ", t2.level: " + t2.level);
  20. }
  21. }
  22. /* 输出:
  23. 1: t1.level: 9, t2.level: 47
  24. 2: t1.level: 47, t2.level: 47
  25. 3: t1.level: 27, t2.level: 27
  26. */

Tank 类非常简单,它的两个实例( t1 和 t2 )是在main()方法里生成的。每个 Tank 类对象的 level 字段都被赋了一个不同的值,然后 t2 被赋给 t1,接着又修改了 t1。在许多编程语言里,你可能会认为 t1 和 t2 总是相互独立的。但由于赋值操作的是引用,修改 t1 的同时显然也改变了 t2!这是因为 t1 和 t2 包含了指向相同对象的引用( t1 最初包含的引用指向了那个字段值为 9 的对象,当对 t1 重新赋值的时候,这个引用被覆盖了,因此对象丢失了,它对应的对象也会由垃圾收集器清理)。

这种现象通常称作 “别名”,是 Java 操作对象的一种基本方式。不过,如果你不想让别名出现在这里,应该怎么办呢?可以不按之前的赋值处理,而像下面这样写:
t1.level = t2.level;
这样就可以保持两个对象彼此独立,而不是丢弃一个对象,然后将 t1 和 t2 都绑定到剩下的那个对象上。直接操作对象内部的字段违背了 Java 的设计原则。这不是一个小问题,所以你需要时刻注意,为对象赋值可能会产生意想不到的结果。

方法调用中的别名

将一个对象作为参数传递给方法时,也会产生别名:

  1. // operators/PassObject.java
  2. // 给方法传递对象,可能并不是你所理解的那样
  3. class Letter {
  4. char c;
  5. }
  6. public class PassObject {
  7. static void f(Letter y) {
  8. y.c = 'z';
  9. }
  10. public static void main(String[] args) {
  11. Letter x = new Letter();
  12. x.c = 'a';
  13. System.out.println("1: x.c: " + x.c);
  14. f(x);
  15. System.out.println("2: x.c: " + x.c);
  16. }
  17. }
  18. /* 输出:
  19. 1: x.c: a
  20. 2: x.c: z
  21. */

在许多编程语言中,方法f()会在它的作用域内生成参数Letter y的一个副本。但这里因为传递的实际上是一个引用,所以代码
y.c = 'z';
实际上改变了f()之外的对象。
别名问题及其解决办法是个很复杂的话题,《On Java 中文版 进阶卷》1第2章涵盖了这一话题。你现在已经知道了这一点,在以后的使用中请注意这个陷阱。

1后简称“进阶卷”。——编者注