在学习重写(overriding)时,老师在课堂上举了这样一个例子:
public class Name {
private final String first, last;
public Name(String first, String last) {
if (first == null || last == null)
throw new NullPointerException();
this.first = first;
this.last = last;
}
public boolean equals(Name o) {
return first.equals(o.first) && last.equals(o.last);
}
public int hashCode() {
return 31 * first.hashCode() + last.hashCode();
}
public static void main(String[] args) {
Set<Name> s = new HashSet<>();
s.add(new Name("Yutong", "Wang"));
System.out.println(s.contains(new Name("Yutong", "Wang")));
}
}
该程序重载了比较两个对象是否相等的equals方法(因为原本的equals方法的参数类型是Object,而在当前类中是Name)。contains方法判断一个对象是否在一个集合中,是基于比较的,即把查找的对象与集合中的所有对象用equals方法进行比对,若找到一堆相等的对象,则说明此对象包含在集合中。按照常理,我们对equals函数进行了重载,在与集合中的值进行比较时,应该调用重载后的equals函数,根据上面equals方法的实现代码,该程序的运行结果应该为true,即可以在集合中找到新构造的、属性与之前加入到集合中的对象相等的那个对象。然而,此程序运行的结果为:
false
下面来探究这个以外结果的原理。
实际上,观察HashSet类的源码可以发现,在实现HashSet时使用了泛型:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
...
}
所以HashSet的所有方法均不知道实例化一个HashSet类型的对象时,元素的数据类型具体是什么。因此,这些方法的参数类型只能是Object,即所有类的父类,包括我们调用的contain方法。下面是contains方法及这个方法中调用的其他方法的源码:
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
我们可以看到,getNode方法中调用了参数类型为Object类型的equals方法:
public boolean equals(Object obj) {
return (this == obj);
}
当我们重载了equals方法后,Name类中存在两个equals方法,一个是参数类型为Object的方法,另一个是参数类型为Name的方法。而在contain方法中运行时,向equals方法传递的参数是Object类型的。根据重载方法调用时根据参数类型进行最佳匹配的机制,实际调用的方法还是以Object为参数类型的equals方法。
而Object类的equals方法如上面所示,实际上就是进行“==”运算,这一运算判断的是两个对象的引用等价性,即若两个对象指向内存中的同一位置,则这两个对象相等,否则不等。而在上面的代码中可以看到,我们调用contains方法时,传入的参数是一个新构造的对象,与前面加入集合中的对象显然内存地址不同。因此,equals方法会判定这两个对象是不相等的,于是contains就会返回false。
而想要让得到的结果为true,则需要重写(override)equals函数,而不是重载(overload),即Name类中的equals函数的参数类型应该为Object。当contains中调用equals方法时,程序注意到Object类有一个重写的equals方法,所以会调用这个重写后的方法。而我们认为如果两个Name对象的属性属性first和last分别相等,则这两个对象就是相等的。根据此原则可以进行如下重写操作:
@Override public boolean equals(Object o) {
if (!(o instanceof Name))
return false;
Name n = (Name) o;
return first.equals(n.first) && last.equals(n.last);
}
重新运行程序,发现输出变为我们所期望的那样:
true
由此可见,在自己实现的一些类中,重写equals方法是非常重要的。这个例子还提醒我们,方法的重载和重写是有很大区别的,如果将其混淆可能会造成意想不到的bug。所以,当我们想要重写一个方法时,最好在前面加上
@Override
这时编译器就会强制检查该方法的参数列表是否与待重写方法的参数列表一致,如果不一致会报错。这样就避免了我们写方法的参数时粗心大意,将重写误写为重载。