Equality
问:euqals()函数是用来做什么的?
答:equals()函数可以用来检查一个对象与调用这个equals()的这个对象是否相等。
问:为什么不用“==”运算符来判断两个对象是否相等呢?
答:虽然“==”运算符可以比较两个数据是否相等,但是要来比较对象的话,恐怕达不到预期的结果。就是说,“==”通过是否引用了同一个对象来判断两个对象是否相等,这被称为“引用相等”。这个运算符不能通过比较两个对象的内容来判断它们是不是逻辑上的相等。
问:使用Object类的equals()方法可以用来做什么样的对比?
答:Object类默认的eqauls()函数进行比较的依据是:调用它的对象和传入的对象的引用是否相等。也就是说,默认的equals()进行的是引用比较。如果两个引用是相同的,equals()函数返回true;否则,返回false.
问:覆盖equals()函数的时候要遵守那些规则?
答:覆盖equals()函数的时候需要遵守的规则在Oracle官方的文档中都有申明:
- 自反性:对于任意非空的引用值x,x.equals(x)返回值为真。
- 对称性:对于任意非空的引用值x和y,x.equals(y)必须和y.equals(x)返回相同的结果。
- 传递性:对于任意的非空引用值x,y和z,如果x.equals(y)返回真,y.equals(z)返回真,那么x.equals(z)也必须返回真。
- 一致性:对于任意非空的引用值x和y,无论调用x.equals(y)多少次,都要返回相同的结果。在比较的过程中,对象中的数据不能被修改。
- 对于任意的非空引用值x,x.equals(null)必须返回假。
问:能提供一个正确覆盖equals()的示例吗?
答:当然,请看代码清单8。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
class Employee { private String name; private int age; Employee(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (!(o instanceof Employee)) return false; Employee e = (Employee) o; return e.getName().equals(name) && e.getAge() == age; } String getName() { return name; } int getAge() { return age; } } public class EqualityDemo { public static void main(String[] args) { Employee e1 = new Employee("John Doe", 29); Employee e2 = new Employee("Jane Doe", 33); Employee e3 = new Employee("John Doe", 29); Employee e4 = new Employee("John Doe", 27+2); // 验证自反性。 System.out.printf("Demonstrating reflexivity...%n%n"); System.out.printf("e1.equals(e1): %b%n", e1.equals(e1)); // 验证对称性。 System.out.printf("%nDemonstrating symmetry...%n%n"); System.out.printf("e1.equals(e2): %b%n", e1.equals(e2)); System.out.printf("e2.equals(e1): %b%n", e2.equals(e1)); System.out.printf("e1.equals(e3): %b%n", e1.equals(e3)); System.out.printf("e3.equals(e1): %b%n", e3.equals(e1)); System.out.printf("e2.equals(e3): %b%n", e2.equals(e3)); System.out.printf("e3.equals(e2): %b%n", e3.equals(e2)); // 验证传递性。 System.out.printf("%nDemonstrating transitivity...%n%n"); System.out.printf("e1.equals(e3): %b%n", e1.equals(e3)); System.out.printf("e3.equals(e4): %b%n", e3.equals(e4)); System.out.printf("e1.equals(e4): %b%n", e1.equals(e4)); // 验证一致性。 System.out.printf("%nDemonstrating consistency...%n%n"); for (int i = 0; i < 5; i++) { System.out.printf("e1.equals(e2): %b%n", e1.equals(e2)); System.out.printf("e1.equals(e3): %b%n", e1.equals(e3)); } // 验证传入非空集合时,返回值为false。 System.out.printf("%nDemonstrating null check...%n%n"); System.out.printf("e1.equals(null): %b%n", e1.equals(null)); } } |
代码清单8声明了一个包含名字、年龄成员变量的Employee对象。这个对象覆盖了equals()函数来对Employee对象进行适当的对比。
ps:覆盖hashCode()函数
当覆盖equals()函数的时候,就相当于覆盖了hashCode()函数,我将在下篇文章讨论hashCode()的时候详细说明。
equals()函数首先要检查传入的确实是一个Employee对象。如果不是,返回false。这个检查是靠instanceof运算来判断 的,当传入null值的时候,同样也返回false。因此,遵守了“对于任意的非空引用值x,x.equals(null)必须返回假”这个规则。
这样,我们就保证了传入的对象是Employee类型。因为之前的instanceof判断保证了传入值必须是Employee类型的对象,所以在 这里我们就不必担心抛出ClassCastException异常了。接下来,equals()方法对两个对象的name和age的值进行了比较。
编译(javac EqualityDemo.java)并运行(java EqualityDemo)代码清单8,你可以看到以下输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
Demonstrating reflexivity... e1.equals(e1): true Demonstrating symmetry... e1.equals(e2): false e2.equals(e1): false e1.equals(e3): true e3.equals(e1): true e2.equals(e3): false e3.equals(e2): false Demonstrating transitivity... e1.equals(e3): true e3.equals(e4): true e1.equals(e4): true Demonstrating consistency... e1.equals(e2): false e1.equals(e3): true e1.equals(e2): false e1.equals(e3): true e1.equals(e2): false e1.equals(e3): true e1.equals(e2): false e1.equals(e3): true e1.equals(e2): false e1.equals(e3): true Demonstrating null check... e1.equals(null): false |
equals()和继承
当Employee类被继承的时候,代码清单8就存在一些问题。例如,SaleRep类继承了Employee类,这个类中也有基于字符串类型的变 量,equals()可以对其进行比较。假设你创建的Employee对象和SaleRep对象都有相同的“名字”和“年龄”。但是,SaleRep中还 是添加了一些内容。
假设你在Employee对象中调用equals()方法并且传入了一个SaleRep对象。由于SaleRep对象继承了Employee,也是一种 Employee的对象,instanceof判断会通过,并且执行equals()方法来判断名字和年龄。因为这两个对象有完全相同的名字和年龄,所以 equals()方法返回true。如果拿SaleRep对象中Employee的部分来和Employee比较的话,返回true值是正确的,但是,如 果拿整个SaleRep对象来和Employee对象比较,返回true值就不妥了。
现在假设在SaleRep对象中调用equals()方法并将Employee传入。因为Employee不是SaleRep类型的对象(否则的话,你可 以访问Employee对象中不存在的Region域,这会导致虚拟机崩溃),无法通过instanceof判断,equals()方法返回false。 综上,equals()在一种判断中为true却在另一判断中为false,违背了“对称性原则”。
Joshua Bloch在《Effective Java Programming Language Guide》第七版中指出:我们无法扩展可被实例化的类(例如Employee)并向其中增加一个域(如Region域),而同时维持equals()方 法的对称性。尽管也有办法来维持对称性,但代价是破坏传递性。Bloch指出解决这个问题需要在继承上支持组合:不是让SaleRep来扩展 Employee,SaleRep应该引用一个私有的Employee值。获得更多信息可以参考Bloch的书。
问:可以使用equals()函数来判断两个数组是否相等吗?
答:可以调用equals()函数来比较数组的引用是否相等。但是,由于在数组对象中无法覆盖equals(),所以只能对数组的引用进行比较,因为不是很常用。参见代码清单9。
1 2 3 4 5 6 7 8 9 10 |
public class EqualityDemo { public static void main(String[] args) { int x[] = { 1, 2, 3 }; int y[] = { 1, 2, 3 }; System.out.printf("x.equals(x): %b%n", x.equals(x)); System.out.printf("x.equals(y): %b%n", x.equals(y)); } } |
代码清单9的main()函数中声明了一对类型与内容完全相等的数组。然后尝试对第一个数组和它自己、第一个数组和第二个数组分别进行比较。由于 equals()对数组来说比较的仅仅是引用,而不比较内容,所以x.equals(x)返回true(因为自反性——一个对象与它自己相等),但是 x.equals(y)返回false。
编译(javac EqualityDemo.java) 并运行(java EqualityDemo)代码清单9,你将会看到以下输出结果:
1 2 |
x.equals(x): true x.equals(y): false |
如果你想要比较的是两个数组的内容,也不要绝望。 可以使用java.util.Arrays 类中声明的 static boolean deepEquals(Object[] a1, Object[] a2) 方法来实现。代码清单10演示了这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import java.util.Arrays; public class EqualityDemo { public static void main(String[] args) { Integer x[] = { 1, 2, 3 }; Integer y[] = { 1, 2, 3 }; Integer z[] = { 3, 2, 1 }; System.out.printf("x.equals(x): %b%n", Arrays.deepEquals(x, x)); System.out.printf("x.equals(y): %b%n", Arrays.deepEquals(x, y)); System.out.printf("x.equals(z): %b%n", Arrays.deepEquals(x, z)); } } |
由于deepEquals()方法要求传入的数组元素必须是对象,所以之前在代码清单9中的元素类型要从int[]改为Integer[]。 Java语言的自动封装特性会把integer常量转换成Integer对象存放在数组中。接下来要将数组传入到deepEquals()就是小事一桩 了。
编译(javac EqualityDemo.java)并运行(java EqualityDemo)代码清单10,你将看到以下输出结果。
1 2 3 |
x.equals(x): true x.equals(y): true x.equals(z): false |
用deepEquals()方法比较的相等是“深度”的相等:这要求每个元素对象所包含的的成员、对象相等。成员对象如果还包含了对象,也要相等, 以此类推,才算是“相等”(另外,两个空的数组引用也是“深度”的相等,因此Arrays.deepEquals(null, null)返回true)。
下期预告:
在第二部分,我将讨论finalize()方法和析构的话题。我也会讨论getClass()方法并指出获得java.lang.Class对象的几种方式,哈希码和hashCode()方法。
英文原文由Jeff Friesen首次发表在Java World,译文由赖信涛 首次发表在Importnew。
Pingback: Java 问答:终极父类(一)——clone()方法 | 赖信涛的个人网站