陷阱将变量视为对象
没有 Java 变量表示对象。
String foo; // NOT AN OBJECT
任何 Java 数组都不包含对象。
String bar[] = new String[100]; // No member is an object.
如果你错误地将变量视为对象,Java 语言的实际行为会让你感到惊讶。
-
对于具有基本类型的 Java 变量(例如
int
或float
),变量保存值的副本。原始值的所有副本都是难以区分的; 即第一个只有一个int
值。原始值不是对象,它们的行为不像对象。 -
对于具有引用类型(类或数组类型)的 Java 变量,该变量包含引用。参考文献的所有副本都难以区分。引用可能指向对象,或者它们可能是
null
,这意味着它们指向没有对象。但是,它们不是对象,它们的行为不像对象。
在任何一种情况下,变量都不是对象,并且在任何一种情况下它们都不包含对象。它们可能包含对象的引用,但这就是说不同的东西。
示例类
以下示例使用此类,它表示 2D 空间中的一个点。
public final class MutableLocation {
public int x;
public int y;
public MutableLocation(int x, int y) {
this.x = x;
this.y = y;
}
public boolean equals(Object other) {
if (!(other instanceof MutableLocation) {
return false;
}
MutableLocation that = (MutableLocation) other;
return this.x == that.x && this.y == that.y;
}
}
这个类的一个实例是一个对象,它有两个字段 x
和 y
,它们的类型为 int
。
我们可以有很多 MutableLocation
类的实例。有些将代表 2D 空间中的相同位置; 即 x
和 y
的相应值将匹配。其他人将代表不同的地点。
多个变量可以指向同一个对象
MutableLocation here = new MutableLocation(1, 2);
MutableLocation there = here;
MutableLocation elsewhere = new MutableLocation(1, 2);
在上面,我们已经声明了三个变量 here
,there
和 elsewhere
,它们可以保存对 MutableLocation
对象的引用。
如果你(错误地)将这些变量视为对象,那么你可能会误读这些语句:
- 将位置“[1,2]”复制到
here
- 将位置“[1,2]”复制到
there
- 将位置“[1,2]”复制到
elsewhere
从那以后,你可能会推断我们在三个变量中有三个独立的对象。实际上,上面只创建了两个对象。变量 here
和 there
实际上指的是同一个对象。
我们可以证明这一点。假设变量声明如上:
System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
"elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
"elsewhere.x is " + elsewhere.x);
这将输出以下内容:
BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1
我们为 here.x
分配了一个新值,它改变了我们通过 there.x
看到的值。它们指的是同一个对象。但是我们通过 elsewhere.x
看到的价值没有改变,所以 elsewhere
必须引用不同的对象。
如果变量是一个对象,则赋值 here.x = 42
不会改变 there.x
。
等于运算符不测试两个对象是否相等
将等于(==
)运算符应用于引用值测试值是否指向同一对象。它并没有测试两个(不同的)对象是否在直观的感觉相等。
MutableLocation here = new MutableLocation(1, 2);
MutableLocation there = here;
MutableLocation elsewhere = new MutableLocation(1, 2);
if (here == there) {
System.out.println("here is there");
}
if (here == elsewhere) {
System.out.println("here is elsewhere");
}
这将打印这里有,但它不会打印这里是其他地方。 (here
和 elsewhere
中的引用是针对两个不同的对象。)
相比之下,如果我们调用上面实现的 equals(Object)
方法,我们将测试两个 MutableLocation
实例是否具有相同的位置。
if (here.equals(there)) {
System.out.println("here equals there");
}
if (here.equals(elsewhere)) {
System.out.println("here equals elsewhere");
}
这将打印两条消息。特别是,here.equals(elsewhere)
返回 true
,因为我们选择了两个 MutableLocation
对象相等的语义标准。
方法调用根本不传递对象
Java 方法调用使用 pass by value 1 来传递参数并返回结果。
将参考值传递给方法时,实际上是按值传递对象的引用,这意味着它正在创建对象引用的副本。
只要两个对象引用仍然指向同一个对象,你就可以从任一引用修改该对象,这就是导致某些对象混淆的原因。
但是,你没有通过引用 2 传递对象。区别在于,如果将对象引用副本修改为指向另一个对象,则原始对象引用仍将指向原始对象。
void f(MutableLocation foo) {
foo = new MutableLocation(3, 4); // Point local foo at a different object.
}
void g() {
MutableLocation foo = MutableLocation(1, 2);
f(foo);
System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}
你也没有传递对象的副本。
void f(MutableLocation foo) {
foo.x = 42;
}
void g() {
MutableLocation foo = new MutableLocation(0, 0);
f(foo);
System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}
1 - 在 Python 和 Ruby 等语言中,术语通过共享传递是对象/引用的按值传递的首选。
2 - 术语通过引用传递或通过引用调用在编程语言术语中具有非常特定的含义。实际上,它意味着你传递变量或数组元素的地址,以便当被调用的方法为形式参数赋值时,它会更改原始变量中的值。Java 不支持这一点。有关传递参数的不同机制的更完整描述,请参阅 https://en.wikipedia.org/wiki/Evaluation_strategy 。