对所有对象都通用的方法也一般指的是Object类里的方法,包含equals,hashCode,toString,clone方法。他们都有明确的通用约定,遵循这些约定可以让依赖于这些约定的类正常运作(如HashMap等)
1. equals的约定
首先我们需要知道什么时候要覆盖equals,一些人可能认为可以用==代替equals,但是==是比较两个对象的地址是否相等的。而当类需要具有自己特有的逻辑相等,则需要覆盖equals。比如,Date类并不会想比较两个Date的地址,而是比较年月日是否相等,这时候就要覆盖equals方法。
覆盖equals需要遵守下面的约定
- 自反性
a.equals(a)
应该返回true - 对称性
a.equals(b)
与b.equals(a)
应该返回同样的结果 - 传递性
a.equals(b)
b.equals(c)
则a.equals(c)
- 一致性
- 非空性 所有对象都不等于null
这些约定很简单,但是有时候我们可能会疏忽,下面是几个可能引起问题的原因
1.1 因为a与b不是同一类引起的对称性问题
比如我们有一个不分大小写字符串的类IgnoreCaseString,ignoreCaseString.equals(string)
返回true,但是string.equals(ignoreCaseString)
不一定返回true。
1.2 由于继承引起的问题
可能由于继承给类添加了属性,而equals直接继承下来了,导致equals返回结果不对,所以我们需要在继承后考虑一下equals是否适合新的类。
1.3 范例
#!java
class Point{
int x;
int y;
@Override
public boolean equals(Object obj) {
if(obj == null) {
return false;
}
if(!(obj instanceof Point)) {
return false;
}
Point that = (Point) obj;
if(that.x != this.x) {
return false;
}
if(that.y != this.y) {
return false;
}
return true;
}
}
-
equals(Object obj)
这里一定是Object,不能是自己的类,否则会覆盖不到Object的方法 - 首先检查非空性
- instanceof判断是否为同类,如果类不对的话在后面强制转换会抛出异常
最后我们要注意对于float和double可以使用Float.compare。因为存在着NaN,-0.0等特殊的常量。
2.覆盖equals后要覆盖hashCode
如果两个对象根据equals比较是相等的,那两个对象的hashCode方法必须返回同样的整数
如果没覆盖的话,用到例如HashMap等依赖于hashCode的类就会出bug。
我们可以用简单的方法来计算hashCode
- 取关键的域,把它转换成int类型
- 定义result,对每一个域,result = 31 * result + 域
以下是一个只含有X和Y域的点的hashCode方法。
@Override
public int hashCode() {
int result = 0;
result = 17;
result = 31 * result + x;
result = 31 * result + y;
return result;
}
一般这样的方法已经能生成够好的哈希值了。
如果hash计算比较费时,可以考虑缓存到类中。
3.始终覆盖toString
toString会在拼接字符串或者print时自动调用,覆盖了会让我们的类调试方便许多。
因为有可能会有依赖于toString返回的字符串储存数据的时候,覆盖完toString后应该在文档中写明格式并且提醒程序员改变这个方法可能会带来的影响。
4.考虑实现comparable接口
这个接口在许多类中都有用到,比如Arrays.sorts()
与equals不同,equals只是查看两个类是否相等,而comparable接口是让类能比较哪一个大,哪一个小。
强烈建议(x.compareTo(y))==0 == (x.equals(y))
否则一些集合接口可能会出问题,虽然后果并不严重,但是还是应该在文档中说明。