关系操作符会根据操作数的值之间的关系生成一个布尔结果。如果关系为真,关系表达式会生成 true;如果关系不为真,则生成 false。关系操作符包括小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有的基本数据类型,但其他比较操作不适用于 boolean 类型。这是因为 boolean 值只能为 true 或 false, “大于” 或 “小于” 对其没有实际意义。
测试对象是否相等
关系操作符 == 和 != 适用于所有对象,但它们的执行结果可能会让人困惑:
// operators/Equivalence.java
public class Equivalence {
static void show(String desc, Integer n1, Integer n2) {
System.out.println(desc + ":");
System.out.printf(
"%d==%d %b %b%n", n1, n2, n1 == n2, n1.equals(n2));
}
@SuppressWarnings("deprecation")
public static void test(int value) {
Integer i1 = value; // [1]
Integer i2 = value;
show("Automatic", i1, i2);
// 在Java 9及更新版本中已被弃用的旧方式:
Integer r1 = new Integer(value); // [2]
Integer r2 = new Integer(value);
show("new Integer()", r1, r2);
// Java 9及更新版本中提倡的方式:
Integer v1 = Integer.valueOf(value); // [3]
Integer v2 = Integer.valueOf(value);
show("Integer.valueOf()", v1, v2);
// 基本类型不能使用equals()方法:
int x = value; // [4]
int y = value;
// x.equals(y); // 无法编译
System.out.println("Primitive int:");
System.out.printf("%d==%d %b%n", x, y, x == y);
}
public static void main(String[] args) {
test(127);
test(128);
}
}
/* 输出:
Automatic:
127==127 true true
new Integer():
127==127 false true
Integer.valueOf():
127==127 true true
Primitive int:
127==127 true
Automatic:
128==128 false true
new Integer():
128==128 false true
Integer.valueOf():
128==128 false true
Primitive int:
128==128 true
*/
show() 方法将 == 的行为和每个对象都有的 equals() 方法进行了比较。printf() 通过使用指定的符号来对参数进行格式化处理,%d 用于 int 类型参数的输出,%b 用于 boolean 类型的输出,%n 用于换行。
对于 “不等于”,请使用 n1 != n2 和 !n1.equals(n2) 这两种方式。
在 test() 中,整数值对象以 4 种不同的方式创建。
[1] 自动转换为 Integer。这其实是通过对 Integer.valueOf() 的自动调用来完成的。
[2] 使用标准的对象创建语法 new。这是以前创建 “包装/装箱” Integer 对象的首选方法。
[3] 从 Java 9开始,valueOf() 优于 [2]。如果尝试在 Java 9 中使用方式 [2],你将收到警告,并被建议使用[3] 代替。很难确定 [3] 是否的确优于 [1] ,不过 [1] 看起来更简洁。
[4] 基本类型 int 也可以当作整数值对象使用。
对 Java 8 而言,以上代码里的 @SuppressWarnings(“deprecation”) 并不是必需的,但如果使用 Java 9 或更新版本编译代码就需要它了。
对于参数值 127 来说,比较操作产生了预期的结果,不过方式 [2] 中 == 操作的结果却是 false。这是因为,虽然参与比较的两个引用包含的内容相同,但它们指向了内存中的不同对象。操作符==
和!=
比较的是对象的引用,而通过不同方式创建的 Integer 对象,会让操作符产生不同的结果——比如说,方式 [1] 和方式 [3] 会生成指向内存中相同位置的 Integer 对象。对于值范围在-128~127
的 Integer 类型来说,它生成的对象就是这样的3,这影响到了操作符==
和!=
的比较结果。但在该范围之外的值则不会这样,正如 test(128) 所演示的那样。
3出于效率原因,Integer会通过享元模式来缓存范围在-128~127内的对象,因此多次调用Integer.valueOf(127) 生成的其实是同一个对象。而在此范围之外的值则不会这样,比如每次调用Integer.valueOf(128) 返回的都是不同的对象。因此需要特别注意,在进行 == 和 != 的比较时,范围不同的值生成对象的方式并不一样,这会影响到比较的行为,从而产生不同的结果。另外,通过 new Interger() 生成的对象都是新创建的,无论其值处于什么范围。所以通过不同方式创建的 Integer 对象,也会影响到比较的结果。——译者注
在使用 Integer 的时候,你应该只使用 equals()。如果不小心使用了==
和!=
,并且没有测试 -128~127 范围外的值,那么虽然你的代码能运行,但在运行中可能悄悄地就会出现错误。如果使用了基本类型 int, 你就不能使用 equals() 而必须使用 == 和 != 。如果你开始使用基本类型 int,然后更改为包装类型 Integer,这可能会导致问题,反之亦然。
在Java 9及更新版本中已经弃用 new Integer(),因为它的效率远远低于 Integer.valueOf() 。因此,你应该避免使用 new Integer()、new Double() 之类的方法,在 Java 8 中也一样。我之前没有遇到过为了效率而弃用某些东西的例子(也有可能发生过,但这是我第一次有所了解)。
处理浮点数的时候,你会遇到不同的相等比较问题,这不是 Java 的问题,而是因为浮点数的本质:
// operators/DoubleEquivalence.java
public class DoubleEquivalence {
static void show(String desc, Double n1, Double n2) {
System.out.println(desc + ":");
System.out.printf(
"%e==%e %b %b%n", n1, n2, n1 == n2, n1.equals(n2));
}
@SuppressWarnings("deprecation")
public static void test(double x1, double x2) {
// x1.equals(x2) // 无法编译
System.out.printf("%e==%e %b%n", x1, x2, x1 == x2);
Double d1 = x1;
Double d2 = x2;
show("Automatic", d1, d2);
Double r1 = new Double(x1);
Double r2 = new Double(x2);
show("new Double()", r1, r2);
Double v1 = Double.valueOf(x1);
Double v2 = Double.valueOf(x2);
show("Double.valueOf()", v1, v2);
}
public static void main(String[] args) {
test(0, Double.MIN_VALUE);
System.out.println("------------------------");
test(Double.MAX_VALUE,
Double.MAX_VALUE - Double.MIN_VALUE * 1_000_000);
}
}
/* 输出:
0.000000e+00==4.900000e-324 false
Automatic:
0.000000e+00==4.900000e-324 false false
new Double():
0.000000e+00==4.900000e-324 false false
Double.valueOf():
0.000000e+00==4.900000e-324 false false
------------------------
1.797693e+308==1.797693e+308 true
Automatic:
1.797693e+308==1.797693e+308 false true
new Double():
1.797693e+308==1.797693e+308 false true
Double.valueOf():
1.797693e+308==1.797693e+308 false true
*/
理论上浮点数的比较应该是很严格的——两个数值之间即使只有小数部分有极小的不同,它们仍然应该不相等。test(0, Double.MIN_VALUE) 的运行结果就是这样的,其中 Double.MIN_VALUE 是最小的可表示值。( printf() 中的 %e 表示以科学记数法显示结果)。
然而,第二个 test() 的运行结果却不是这样的,这里参数 x2 是 x1 的值减去100万倍的Double.MIN_VALUE。看起来 x2 应该与 x1 明显不同,但这两个数值的比较结果仍然相等。几乎在所有编程语言中都是这样的,这是因为当一个非常大的数值减去一个相对较小的数值时,非常大的数值并不会发生显著变化。这叫作舍入误差,这种误差之所以发生,是因为机器不能存储足够的信息来表示一个大数值的微小变化。
你可能会误以为这种情况下使用 == 会产生正确的结果,但并不是这样的——它只是单纯比较了引用。
当操作非基本类型时,直接使用 equals() 似乎是理所当然的选择,不过没有那么简单。考虑类 ValA:
// operators/EqualsMethod.java
// 默认的equals()方法并不是比较内容的
class ValA {
int i;
}
class ValB {
int i;
// 对这个示例是适用的,但这里并不是一个完整的equals()方法
public boolean equals(Object o) {
ValB rval = (ValB)o; // 将对象o转型为ValB
return i == rval.i;
}
}
public class EqualsMethod {
public static void main(String[] args) {
ValA va1 = new ValA();
ValA va2 = new ValA();
va1.i = va2.i = 100;
System.out.println(va1.equals(va2));
ValB vb1 = new ValB();
ValB vb2 = new ValB();
vb1.i = vb2.i = 100;
System.out.println(vb1.equals(vb2));
}
}
/* 输出:
false
true
*/
在 main() 中,va1 和 va2 包含相同的 i 值,但使用 equals() 比较的结果是 false,这令人困惑。这是因为 equals() 方法的默认行为是比较引用。如果只想比较内容,你必须像 ValB 所示的那样重写 equals() 方法。 ValB.equals() 方法只包含了解决示例问题所必需的最简代码,但这不是一个恰当的 equals()。注意 equals() 方法的标准参数是一个 Object 类型(而不是 ValB 类型),我们必须通过代码 (ValB)o 将 o 强制类型转换为 ValB。然后我们就可以用 == 直接比较两个 i 的值了,因为它们是基本类型。另外,在类型转换前一般要先检查类型,我们暂时先跳过这一点,后面的章节会对此进行讲解。
到了后面的第 8 章,你会了解重写,另外你需要在进阶卷附录 C 中学习如何正确定义 equals() 方法。在此之前留意 equals() 方法的行为会为你省却不少麻烦。
大多数标准库会重写 equals() 方法来比较对象的内容而不是它们的引用。