Tip-8 覆盖equals时请遵守通用约定
- 覆盖equals方法看起来很简单,但是有许多覆盖方式会导致严重错误,最容易避免这类问题的办法就是不覆盖equals方法。
-
不覆盖的理由
- 每个对象都是唯一的,不需要覆盖
- 不关心类是否提供了"逻辑相等"的函数或功能
- 父类已经覆盖了equals,对子类同样适用
- 类是私有的或包级别是私有的,可以确定它们的equals方法永远不会被调用
-
覆盖的时机
- 类具有特有的“逻辑相等”概念,类似String
- 超累没有覆盖,子类需要equals实现特定的功能
- 通常是类的成员变量的比值
-
特殊的“值类“
- 每个值只存在一个对象中,比如枚举类,对应这样的类,逻辑相同,与对象等同是一回事(所以枚举类常被用来做参数类型比较)
-
覆盖equals需要遵守的规则,来自JavaSE6规范
- 自反性 对于任何非null的引用值x,x.equals(x) 必须返回 true。
- 对称性 对于任何非null的引用值x,y 当且仅当y.equals(x)= true 时,x.equals(y)也必须为true
- 传递性 类似小学数学+-*/括号运算。。。
- 一致性 在对象的值没有被修改的情况下 无论几次调用equals 返回的值 应一致
- 非空性 对于任何非null对象,x.equals(null)必须返回 false;
-
警告
- 覆盖equals时总要覆盖hashCode
- 不要企图让equals方法过于智能
- 不要讲equals声明中的 Object 参数类型 替换为其他的类型
class Test{
private short areaCode;
private short prefix;
private short lineNumber;
public Test(){
}
public Test(int areaCode,int prefix,int lineNumber){
this.areaCode = (short) areaCode;
......同......
}
public boolean equals(Object o){
if(o == this){
return true;
}
if(!(o instanceof Test)){
return false;
}
....业务逻辑....
}
}
Tip-9 覆盖equals时总要覆盖hashCode
-
规则:在覆盖equals方法的类中也必须覆盖hashCode方法,如果不遵守,就会违反hashCode通用约定,从而导致该类无法于所有基于Hash(散列)的集合一起正常运行,这样的集合包括HashMap,HashSet,Hashtable
-
通用约定
- 在应用程序的一次执行期间如果equals方法没有修改,那么同一对象多次调用hashCode方法的结果必须一致,在同一个应用程序多次执行过程中,hashCode返回的结果可以不一致
- 如果两个对象在equals比较是相等的,那么两个对象的hashCode结果也必须一致(相同)
- 如果两个对象在equals比较是不相等的,那么两个对象的hashCode结果不一定要不相同(可以相同),但是给不相等的对象产生截然不同的hashCode结果,可能提高散列表的性能
-
失败例子
// 使用第8 条的 Test 类
Map<Test,Strign> map = new HashMap<Test,String>();
m.put(new Test(707,867,5309),"Lingyun");
// str 实际内容 是null
String str = m.get(new Test(707,867,5309));
- 这里实际涉及了两个Test对象,第一个被用于插入HashMap中,第二个用于获取str,但是由于 Test 没有覆盖HashCode方法,导致两个equals 或看似相等的对象实际拥有不同的Hash值,违反了HashCode约定
- 简单的HashCode覆盖方法
- 将一个非0的常数,比如17 赋值给 result 的int型变量中
- 对于对象中的每个关键域f,完成以下步骤
- 计算该域的散列码
- 按照公式 将计算出的散列码合并到result中 公式(result = 31 * result + c;)
- 返回result
- 写单元测试,验证是否equals相等的对象拥有相等的散列值,若不同修正错误
public int hashCode(){
int result = 17;
// 关键域
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return reuslt;
}
-
建议
- 如果一个类是不可变的,并且计算散列值得开销较大,应该考虑把散列值缓存在对象内部(一个成员变量上),而不是每次请求时重新计算,
- 如果一个类的大多数对象会计算散列值,就应该在创建对象时计算散列值,否则,可以选择“延迟初始化”散列值,一直到HashCode第一个调用时才初始化
-
警告
- 不要试图从散列值计算中排除一个对象的关键域来提高性能。虽然这样得到散列值速度加快,但是效果不一定好,可能会导致散列表慢到不可用
Tip-10 始终要覆盖toString
- 提供好的toString实现可以使用类时感觉更加舒适。
- toString 方法中应该返回对象中包含的所有值得关注的信息。
Tip-11 谨慎的覆盖clone
- 最好别用,想要实现类似功能最好的办法是提供一个拷贝构造器或拷贝工厂
Tip-12 考虑实现Comparable接口
-
建议
- 可以进行等同比较,也可以进行顺序比较
- 实现了Comparable接口,就表名类的对象具有内在的排序关系
- 如果正在编写一个值类,并具有非常明显的内在排序关系,比如按字母顺序,按数值顺序或者按年代顺序,那么就应该考虑实现这个接口
-
通用约定 sng 为signum函数,根据表达式的值为负值,0 ,正值分别返回-1 ,0 或1
- 实现者必须确保所有x和y都满足 sng(x.compareTo(y)) == -sng(y.compareTo(x))
- 实现者必须确保这个比较关系是可传递的 x>y y>z 暗示着 x>z
- 实现者必须确保 x.compareTo(y)==0 暗示着 z 满足 sng(x.compareTo(z)) == sng(y.compateTo(z))
-
警告
- 不支持跨类比较
- 就好像违反了hashCode约定的类使用HashMap等会受到影响一样,违反compareTo约定的类也会在使用TreeSet,TreeMap以及工具类Collections和Arrays受到影响,因为这些类的内部含有搜索和排序算法。
- 有些技巧使用时要谨慎 比如 return x-y; 如果 x是一个特别大的正数,y是一个特别小的负数,返回的结果就会超过int最大值 ,导致程序崩溃,这些崩溃可能非常难以调试,因为这样的compareTo方法对于大多数的输入值都能正常工作
网友评论