本章主要内容是讲述Object类的可覆盖方法的通用规则,即equals、hashCode、toString、clone和finalize方法的使用规则,其中finalize方法在上一章已经讲述,所以本章主要讲另外四个方法的使用规则。
equals方法
如果类有不同于对象相等、而是逻辑相等的概念,并且超类没有覆盖equals实现当前类期望的行为
的时候,我们需要覆盖equals方法,通过在equals方法中重写对类对象中某个或某些指定值的大小比较来进行是否equals的判断。
下面是覆盖equals时要遵守的约定的内容:
- 自反性。对于任何非null的引用值x,x.equals(x)必须返回true;
- 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true;
- 传递性。对于任何非null的引用值x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也返回true;
- 一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true或false;
- 对于任何非null的引用值x,x.equals(null)必须返回false。
其中自反性、一致性一般情况来说很难违反,需要注意后面这几个约定。
对称性
这个约定违反的情况也不多,但是也存在,比如想强行将两个不同的类进行equals操作,如果两个类都能对equals进行重写还有可行性,若是其中一个类是String这种无法修改的系统类,就会出现只有一个方向可以equals,对称过来就会返回false的情况,继而违反对称性。
传递性
这个约定比较需要注意,因为无意识违反传递性的可能相比前两个要大很多。
典型的情况是父类重写了equals方法,子类扩展父类时增加了一个新的值组件,对子类的对象的equals操作就需要再增加对这个新的值组件的比较,那么父类对象与子类对象进行equals操作时,要么会违反对称性,要么会违反传递性,没有十分万全的方法可以解决这个问题。
书上有提到说有种方法是用getClass测试代替instanceof测试,来在equals方法中判断两个对象是否相同,这个方法的缺点是不论是否对传入的类进行了统一的向上转型,getClass获取的class都是转型之前的子类class,所以如果有需求是equals方法判断父类对象和子类对象只需要比较父类中存在的值组件的大小,那么如果用getClass来判断,即使两个对象的逻辑值相等也会返回false,相对来说还是用instanceof更合理。
非空性(书中命名)
最后一条性质,没什么好说的,但是有一个书中提及的小技巧,我们不需要在equals方法里进行专门的判空操作。
为了值比较时的参数等同性,equals方法必须先把传入的object对象通过instanceof进行判断,而instanceof本身就有如果前操作数为null则直接返回false的特性,所以不需要再单独进行判空测试。
诀窍
综上所述,有以下实现高质量equals方法的诀窍:
- 使用==判断传入的object是否为当前对象本身,如果是则直接返回true;
- 使用instanceof判断传入的对象是否为正确的类型,不是则直接返回false;
- 是正确的类型,则转换为正确的类型;
- 对于要比较的关键值一一进行比较。这里要说明的是,对于不是float和double的基本数据类型的比较可以直接用==进行,对于对象可以递归调用对象的equals方法,而对于float或double类型的数据,要调用Float或Double类对应的compare方法进行比较。
- 编写完成equals方法之后,要对对称性、传递性、一致性等五个约定进行测试.
最后的提醒
- 覆盖equals方法时总要覆盖hashCode
- 不要让equals方法过于智能,过于寻求各种等价关系往往会陷入各种麻烦之中
- 不要将equals声明中的Object参数改成其他类型
hashCode方法
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做,就会违反hashCode的通用约定,从而使该类无法结合所有基于散列的集合一起正常的工作,这样的集合包括HashMap、HashSet和HashTable。
好的hashcode方法实现需要一个好的散列函数,使集合中不相等的实例均匀的分布到所有可能的散列值上。想要完全达到这种理想的情形比较困难,但是有一个相对接近这种情形的解决办法:
- 把一个非零的常数值保存在名为result的int类型变量中;
- 对于对象中每个关键域(equals方法中涉及到的域),完成以下步骤:
a. 为当前运算的这个域计算int类型的散列码c
- 如果该域是Boolean类型的,则可以计算 f?1:0
- 如果该域是byte、char、short、int类型的,计算(int)f
- 如果该域是long类型的,计算(int)(f^(f>>>32))
- 如果是float类型,计算Float.floatToIntBits(f)
- 如果是double类型同样调用double的floattolongbits,然后调用long的转换到int的步骤
- 如果是对象,调用对象的hashCode
- 如果是数组,可调用数组的Arrays.hashcode方法
b. 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:
result = 31result + c;
注意,上面的两个步骤ab是对*每个关键域都要做的,即有多少个关键域,b中的result就要迭代多少次。
最后只需要返回result,测试是否相等的实例返回相同的hashCode。