软件构造:对euqals()和hashCode()方法的理解

目录

前言

一、谈谈equals()方法

1、"相等"的三个角度

2、== vs. equals()

3、不可变类型的相等

 二、关于hashCode()方法

1、hashCode()有什么用?

2、重写不可变类型ADT的hashCode()方法


前言

        本部分内容是我结合老师上课讲授的内容,老师提供的ppt,MIT的讲义以及课后我自己的理解来进行总结,最终完成的。

一、谈谈equals()方法

1、"相等"的三个角度

(1)AF的角度

        AF,即抽象函数,它将具体的表示数据映射成抽象的值(AF:rep-->abstract),对于一个给定的AF,如果AF(a)=AF(b),我们就可以认为a和b在这个情况下是相等的。

(2)等价关系

        指的就是集合论中的等价关系,不妨设这样的关系为关系E,它有自反性、对称性和传递性。

(3)使用者/外部的角度

我们说a和b相等,当且仅当对这两个对象的每一个观察都能得到相同的结果。

                                        

        在上面的图中,对两个集合使用size()方法观察,大小一样,使用contains()方法观察,对应的输入返回值也相同,所以我们说这两个集合是相等的。

        这里要注意一点,就是使用者所使用的观察方法仅限于ADT的spec中规定的那些方法,两个对象的等价通过这些规定的观察方法来确定。

        之所以这样是因为java中有一些可以跨过ADT抽象层进行观察的方法。

        例如最简单的==可以判断两个变量的引用(地址)是否相同,很显然这不是一个抽象空间的东西。

        而System.identityHashCode()会根据存储地址计算返回值,这些都不是ADT规约中提供的观察方法,它们也都不能用来确定ADT的两个实例的等价性。

2、== vs. equals()

Java有两种基本的判断相等的操作——==和equals()。

==比较的是对象的索引,即它测试的是指向相等(referential equality)。如果两个索引指向同一块存储空间,那么==结果就为真。从快照图的角度讲,就是两个箭头指向同一个对象,==比较的就是两个箭头的指向。

而euqals()比较的是对象的内容,即它测试的是对象值相等(object equality)。对于需要调用equals()方法的ADT,它的equals()必须合理定义,也就是说必须仔细地重写。

 

值得注意的地方是==在python和java中的意义是相反的。

        总结一下,在Java中,两个对象进行==总是测试指向是否相等,而且这个运算符的动作我们也无法更改。但是当我们定义了一个新的ADT时,我们就需要判断对这个ADT来说两个对象值相等意味着什么,即如何去实现equals()方法。

3、不可变类型的相等

equals()方法是在Object类中定义的,它的默认实现如下:

        可以看到,这就是对==的简单包装而已,这个默认的equals()方法就是在进行指向相等测试,这对于不可变类型的对象来说几乎总是错的。

        想想String类型的对象,String类是不可变的,也就是说如果String类没有重写equals()方法,直接使用默认的,那么创建在两块不同存储空间的以"abcd"为其内容的String对象就不会是相等的,因为引用不同,而这是荒谬的,因为在client看来它们(的抽象值)就是相等的。

        因此对于那些新的、不可变的ADT,非常有必要重写它的equals()方法。

        在重写equals()方法时,有一个非常容易踩的坑,就是方法签名的参数没有写对,导致主观上的重写(Override)变成客观上的重载(Overload),如下:

         这是上课PPT中的例子,可以看到o2和d2引用同一块存储空间,但是d1和d2用equals()方法比较结果为真,但是d1和o2用euqals()方法比较结果却为false,这是怎么回事呢?

        原因其实非常简单,就是Duration类实现了两个equals()方法!可以看到Duration类里所谓重写的equals方法,它的签名中参数的类型用的是Duration类型,但是我们可以看到父类Object中equals()方法的签名参数的类型是Object,所以Duration类实际是重载了equals方法,在d1.equals(o2)中用的仍然是Object类的那个方法。

        总而言之,在重写equals()方法时,方法签名一定要写成下面这样:

                                  public boolen equals(Object that)

        除此之外,可以在方法签名上面加一个@Override标签,要求编译器帮你检查你重写的方法签名是否正确。

这是Duration类equals()方法正确的重写实现:

这里说一下instanceof,它是一个操作符,用来测试一个实例是否属于特定的类型。

instanceof是动态检查而非静态检查,在OOP编程中使用instanceof是一个不好的选择,仅建议在equals()方法的重写中使用instanceof。

Object类在equals()方法的规约中说明了重写它的一些要求:

1.equals必须定义一个等价关系。即一个满足自反性,对称性和传递性关系。

2.equals必须是确定的。即连续重复的进行相等操作,结果应该相同。

3.对于不是null的索引x,x.equals(null)应该返回false。

4.如果两个对象使用equals操作后结果为真,那么他们各自的HashCode操作的结果也应该相同。

 二、关于hashCode()方法

对于equals()方法的最后一点,有必要讲一下hashCode()方法。

1、hashCode()有什么用?

        有两种聚合类型HashSet和HashMap都使用到了哈希表的数据结构并且依赖hashCode()方法来保存集合中的对象和为键值对产生合适的索引。

        哈希表表示的是一种映射,它是将键值(Key)映射到值(Value)的ADT。它在开始时包含了一个初始化的数组,当一个键值对将要插入时,哈希表通过调用hashCode()方法计算这个这个键,产生一个索引,这个索引在数组大小的范围内(可以通过取模运算做到),然后将值放到索引对应的位置上。

        这就是hashCode()方法的意义所在,Object类的规约中要求两个相等的对象必须有相等的hashCode()返回值,如果两个相等的对象的hashCode()返回值不同,即hashCode()没有重写好,那么当这个对象类型作为Map的键类型时,如果你存入了一个对象,又用另一个equals()下相等的对象进行值查找,就有可能会在错误的索引下返回一个错误的值。

        默认的hashCode()方法会返回对象的内存地址,这对于不可变ADT来说显然是不行的,理由与前面的equals()方法的类似。

2、重写不可变类型ADT的hashCode()方法

        第一种方法简单粗暴,那就是让hashCode()返回一个常数,这会导致所有的键都计算出同一个索引,从而使哈希表退化为链表,不过在小规模的问题上,这种实现方法是可以接受的。

        第二种方法比较合理,就是让对象的每个字段都参与到一系列计算中,最后得到一个综合hashcode。一个比较方便的方法是调用Objects.hash()来计算多个字段的综合hashcode。

        最后一点需要注意的是,一定要写对方法签名,hashCode()的方法签名长这样:

                                               

关于equals()方法和hashCode()方法,我就分享这么多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值