第三章第五节 ADT和OOP中的等价性

在很多场景下,需要判定两个对象是否 “相等”,例如:判断某个Collection 中是否包含特定元素。 
==和equals()有和区别?如何为自定义 ADT正确实现equals()?

OutLine

  • 等价性equals() 和 ==
  • equals()的判断方法
    • 自反、传递、对称性
  • hashCode()
  • 不可变类型的等价性
  • 可变类型的等价性
    • 观察等价性
    • 行为等价性

Notes

##  等价性equals() 和 ==

  • 和很多其他语言一样,Java有两种判断相等的操作—— == 和 equals() 。
  • ==是引用等价性 ;而equals()是对象等价性。 
    • == 比较的是索引。更准确的说,它测试的是指向相等(referential equality)。如果两个索引指向同一块存储区域,那它们就是==的。对于我们之前提到过的快照图来说,==就意味着它们的箭头指向同一个对象。
    • equals()操作比较的是对象的内容,换句话说,它测试的是对象值相等(object equality)。e在每一个ADT中,quals操作必须合理定义。

Java中的数据类型,可分为两类:

  • 基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean

    • 他们之间的比较,应用双等号(==),比较的是他们的值。
  • 复合数据类型(类) 
    • 当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。
    • JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。
    • 对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。

 关于equals()与== 欢迎阅读 海子的博客

## equals()的判断方法

严格来说,我们可以从三个角度定义相等:

  • 抽象函数:回忆一下抽象函数(AF: R → A ),它将具体的表示数据映射到了抽象的值。如果AF(a)=AF(b),我们就说a和b相等。
  • 等价关系:等价是指对于关系E ⊆ T x T ,它满足:
    • 自反性: x.equals(x)必须返回true
    • 对称性: x.equals(y)与y.equals(x)的返回值必须相等。
    • 传递性: x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true。

以上两种角度/定义实际上是一样的,通过等价关系我们可以构建一个抽象函数(译者注:就是一个封闭的二元关系运算);而抽象函数也能推出一个等价关系。

  • 从使用者/外部的角度去观察:我们说两个对象相等,当且仅当使用者无法观察到它们之间有不同,即每一个观察总会都会得到相同的结果。例如对于两个集合对象 {1,2} 和 {2,1},我们就无法观察到不同:

    • |{1,2}| = 2, |{2,1}| = 2
    • 1 ∈ {1,2} is true, 1 ∈ {2,1} is true
    • 2 ∈ {1,2} is true, 2 ∈ {2,1} is true
    • 3 ∈ {1,2} is false, 3 ∈ {2,1} is false

## hashCode()方法

  • 对于不可变类型:

    • equals() 应该比较抽象值是否相等。这和 equals() 比较行为相等性是一样的。
    • hashCode() 应该将抽象值映射为整数。
    • 所以不可变类型应该同时覆盖 equals() 和 hashCode().
  • 对于可变类型:
    • equals() 应该比较索引,就像 ==一样。同样的,这也是比较行为相等性。
    • hashCode() 应该将索引映射为整数。
    • 所以可变类型不应该将 equals() 和 hashCode() 覆盖,而是直接继承 Object中的方法。Java没有为大多数聚合类遵守这一规定,这也许会导致上面看到的隐秘bug。
  • equals与hashCode两个方法均属于Object对象,equals根据我们的需要重写, 用来判断是否是同一个内容或同一个对象,具体是判断什么,怎么判断得看怎么重写,默认的equals是比较地址。
  • hashCode方法返回一个int的哈希码, 同样可以重写来自定义获取哈希码的方法。
  • equals判定为相同, hashCode一定相同。equals判定为不同,hashCode不一定不同。
  • hashCode必须为两个被该equals方法视为相等的对象产生相同的结果。
  • 与equals()方法类似,hashCode()方法可以被重写。JDK中对hashCode()方法的作用,以及实现时的注意事项做了说明:
    • hashCode()在哈希表中起作用,如java.util.HashMap。
    • 如果对象在equals()中使用的信息都没有改变,那么hashCode()值始终不变。
    • 如果两个对象使用equals()方法判断为相等,则hashCode()方法也应该相等。
    • 如果两个对象使用equals()方法判断为不相等,则不要求hashCode()也必须不相等;但是开发人员应该认识到,不相等的对象产生不相同的hashCode可以提高哈希表的性能。

## 不可变类型的等价性

首先来看Object中实现的缺省equals():

public class Object {
...
public boolean equals(Object that) {
return this == that;
}
}

在Object中实现的缺省equals()是在判断引用等价性。这通常不是程序员所期望的,因此需要重写,下面是一个栗子:

public class Duration {
...
// Problematic definition of equals()
public boolean equals(Duration that) {
return this.getLength() == that.getLength();
}
}

尝试如下客户端代码,可得到

Duration d1 = new Duration (1, 2);
Duration d2 = new Duration (1, 2);
Object o2 = d2;
d1.equals(d2) → true
d1.equals(o2) → false

基于以上结果进行以下解释:

  • 即使d2o2最终参照相同的对象在内存中,对他们来说你仍然得到不同的结果。
  • 事实证明,该方法Duration已经超载equals(),因为方法签名与Object’s 不相同。我们实际上有两种equals()方法:隐式equals(Object)继承Object,和新的equals(Duration)
  • 如果我们通过一个Object参考,那么d1.equals(o2)我们最终会调用equals(Object)实现。
  • 如果我们通过Duration参考,如在d1.equals(d2),我们最终调用equals(Duration)版本。
  • 即使发生这种情况o2d2两者都会在运行时指向同一个对象!平等已经变得不一致。

我们需要注释 @Override ,重写超类中的方法,因此,这里实施正确的 equals() 方法:

@Override
public boolean equals (Object thatObject) {
if (!(thatObject instanceof Duration)) return false;
Duration thatDuration = (Duration) thatObject;
return this.getLength() == thatDuration.getLength();
}

再次执行客户端代码,可得到:

Duration d1 = new Duration(1, 2);
Duration d2 = new Duration(1, 2);
Object o2 = d2;
d1.equals(d2) → true
d1.equals(o2) → true

## 可变类型的等价性

  回忆之前我们对于相等的定义,即它们不能被使用者观察出来不同。而对于可变对象来说,它们多了一种新的可能:通过在观察前调用改造者,我们可以改变其内部的状态,从而观察出不同的结果。

  • 所以我们重新定义两种相等:

    • 观察等价性:两个索引在不改变各自对象状态的前提下不能被区分。即通过只调用observer,producer和creator的方法,它测试的是这两个索引在当前程序状态下“看起来”相等。
    • 行为等价性:两个索引在任何代码的情况下都不能被区分,即使有一个对象调用了改造者。它测试的是两个对象是否会在未来所有的状态下“行为”相等。
  • 对于不可变对象,观察相等和行为相等是完全等价的,因为它们没有改造者改变对象内部的状态。
  • 对于可变对象,Java通常实现的是观察相等。例如两个不同的 List 对象包含相同的序列元素,那么equals() 操作就会返回真。

在有些时候,观察等价性可能导致bug,甚至可能破坏RI。

假设我们做了一个List,然后把它放到Set

List<String> list = new ArrayList<>();
list.add("a"); Set<List<String>> set = new HashSet<List<String>>();
set.add(list);

我们可以检查该集合是否包含我们放入其中的列表,并且它会:

set.contains(list) → true

但是如果我们修改这个存入的列表:

list.add("goodbye");

它似乎就不在集合中了!

set.contains(list) → false!

事实上,更糟糕的是:当我们(用迭代器)循环遍历这个集合时,我们依然会发现集合存在,但是contains() 还是说它不存在!

for (List<String> l : set) {
set.contains(l) → false!
}

  如果一个集合的迭代器和contains()都互相冲突的时候,显然这个集合已经被破坏了。

  发生了什么?我们知道 List<String> 是一个可变对象,而在Java对可变对象的实现中,改造操作通常都会影响 equals() 和 hashCode()的结果。所以列表第一次放入 HashSet的时候,它是存储在这时 hashCode() 对应的索引位置。但是后来列表发生了改变,计算 hashCode() 会得到不一样的结果,但是 HashSet 对此并不知道,所以我们调用contains时候就会找不到列表。

  当 equals() 和 hashCode() 被改动影响的时候,我们就破坏了哈希表利用对象作为键的不变量。

下面是 java.util.Set规格说明中的一段话:

注意:当可变对象作为集合的元素时要特别小心。如果对象内容改变后会影响相等比较而且对象是集合的元素,那么集合的行为是不确定的。 

  我们应该从这个例子中吸取教训,对可变类型,实现行为等价性即可,也就是说,只有指 向同样内存空间的objects,才是相等的。所以对可变类型来说,无需重写这两个函数,直接继承 Object对象的两个方法即可。 如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。

【软件构造】第三章第五节 ADT和OOP中的等价性的更多相关文章

  1. 【软件构造】第三章第四节 面向对象编程OOP

    第三章第四节 面向对象编程OOP 本节讲学习ADT的具体实现技术:OOP Outline OOP的基本概念 对象 类 接口 抽象类 OOP的不同特征 封装 继承与重写(override) 多态与重载( ...

  2. 第三百九十五节,Django+Xadmin打造上线标准的在线教育平台—Xadmin集成富文本框

    第三百九十五节,Django+Xadmin打造上线标准的在线教育平台—Xadmin集成富文本框 首先安装DjangoUeditor3模块 Ueditor HTML编辑器是百度开源的HTML编辑器 下载 ...

  3. 第三百四十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—爬虫和反爬的对抗过程以及策略—scrapy架构源码分析图

    第三百四十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—爬虫和反爬的对抗过程以及策略—scrapy架构源码分析图 1.基本概念 2.反爬虫的目的 3.爬虫和反爬的对抗过程以及策略 scra ...

  4. 第三百一十五节,Django框架,CSRF跨站请求伪造

    第三百一十五节,Django框架,CSRF跨站请求伪造  全局CSRF 如果要启用防止CSRF跨站请求伪造,就需要在中间件开启CSRF #中间件 MIDDLEWARE = [ 'django.midd ...

  5. NHibernate.3.0.Cookbook第一章第五节Setting up a base entity class

    Setting up a base entity class设置一个实体类的基类 在这节中,我将给你展示怎么样去为我们的实体类设置一个通用的基类. 准备工作 完成前面三节的任务 如何去做 1.在Ent ...

  6. .net架构设计读书笔记--第三章 第9节 域模型实现(ImplementingDomain Model)

        我们长时间争论什么方案是实现域业务领域层架构的最佳方法.最后,我们用一个在线商店案例来说明,其中忽略了许多之前遇到的一些场景.在线商店对很多人来说更容易理解. 一.在线商店项目简介 1. 用例 ...

  7. .net架构设计读书笔记--第三章 第10节 命令职责分离(CQRS)简介(Introducing CQRS)

    一.分离查询命令 Separating commands from queries     早期的面向DDD设计方法的难点是如何设计一个类,这个类要包含域的方方面面.通常来说,任务软件系统方法调用可以 ...

  8. .net架构设计读书笔记--第三章 第8节 域模型简介(Introducing Domain Model)

    一.数据--行为转变     很长的时间,典型的分析方法或多或少是以下两种,第一,收集需求并做一些分析,找出有关实体 (例如,客户. 订单. 产品) 和进程来实现. 第二,手持这种理解你尝试推断一个物 ...

  9. 微信小程序教学第三章第四节(含视频):小程序中级实战教程:下拉更新、分享、阅读标识

    下拉更新.分享.阅读标识 本文配套视频地址: https://v.qq.com/x/page/h0554i4u5ob.html 开始前请把 ch3-4 分支中的 code/ 目录导入微信开发工具 这一 ...

随机推荐

  1. linux中用管道实现父子进程通信

    1 用户要实现父进程到子进程的数据通道,可以在父进程关闭管道读出一端, 然后相应的子进程关闭管道的输入端. 2 先用pipe()建立管道 然后fork函数创建子进程.父进程向子进程发消息,子进程读消息 ...

  2. 51nod 1092【区间dp】

    思路: 简单的区间dp,从小区间到大区间,随便写. 还有一种是那啥,n-LCS...具体不说了,赶时间)))= =. #include <stdio.h> #include <str ...

  3. UIScrollView控件实现图片轮播

    http://www.cnblogs.com/dyf520/p/3805297.html 一.实现效果 实现图片的自动轮播            二.实现代码 storyboard中布局 代码: 1 ...

  4. 黑客攻防技术宝典web实战篇:攻击访问控制习题

    猫宁!!! 参考链接:http://www.ituring.com.cn/book/885 随书答案. 1. 一个应用程序可能通过使用 HTTP Referer 消息头实施访问控制,但它的正常行为并没 ...

  5. scikit-learning教程(二)统计学习科学数据处理的教程

    统计学习:scikit学习中的设置和估计对象 数据集 Scikit学习处理来自以2D数组表示的一个或多个数据集的学习信息.它们可以被理解为多维观察的列表.我们说这些阵列的第一个轴是样本轴,而第二个轴是 ...

  6. EXBSGS

    http://210.33.19.103/problem/2183 参考:https://blog.csdn.net/frods/article/details/67639410(里面代码好像不太对) ...

  7. linux常用的shell命令

    1.shell介绍 shell(外壳)是linux系统的最外层,简单的说,它就是用户和操作系统之间的一个命令解释器. 2.shell命名的使用 ls :查看当前目录的信息,list .        ...

  8. jmeter压测--从文本中读取参数

    由于之前从数据库获取查询结果作为请求的入参(使用场景:测试一个接口并发处理数据的能力,并且每次请求传入的参数都要不同.),会一定程度上造成对数据库的压测,在没有完全搞清楚多线程之间参数的传递之前,我们 ...

  9. ionic之自定义图片

    一个好的app,必须都有很好的ui设计师来设计界面,增强客户的体验,表现自己本身公司的特色,但是,在ionic中有些是无法用img标签直接引入图片,只能通过设定的css之后引入css. 页面: < ...

  10. 关于 a 标签 jquery的trigger("click"),无法触发问题。

    这个问题的原因不是jquery的trigger("click"), 函数的问题, 而是 a标签之间要有其他子标签,要对这个子标签调用trigger("click" ...