第3章:抽象数据类型(ADT)和面向对象编程(OOP) 3.5 ADT和OOP中的等价性

大纲

什么是等价性?为何要讨论等价性?
三种等价性的方式
==与equals()
不可变类型的等价性
对象契约
可变类型的等价性
自动包装和等价性java

什么是等价性?为何要讨论等价性?

ADT上的相等操做程序员

ADT是经过建立以操做为特征的类型而不是其表示的数据抽象。
对于抽象数据类型,抽象函数(AF)解释了如何将具体表示值解释为抽象类型的值,而且咱们看到了抽象函数的选择如何决定如何编写实现每一个ADT操做的代码。
抽象函数(AF)提供了一种方法来清晰地定义ADT上的相等操做。编程

数据类型中值的相等性?数组

在物质世界中,每一个物体都是不一样的 - 即便两个雪花的区别仅仅是它们在太空中的位置,在某种程度上,即便是两个雪花也是不一样的。
因此两个实体对象永远不会真正“相等”。 他们只有类似的程度。
然而,在人类语言的世界中,在数学概念的世界中,对同一事物能够有多个名称。安全

  • 当两个表达式表示相同的事物时,很天然地:1 + 2,√9和3是同一个理想数学值的替表明达式。

三种等价性的方式

使用AF或使用关系数据结构

使用抽象函数。 回想一下抽象函数f:R→A将数据类型的具体实例映射到它们相应的抽象值。 为了使用f做为等价性的定义,咱们说当且仅当f(a)= f(b)时等于b。
使用关系。 等价关系是E⊆T x T,即:ide

  • 自反:E(t,t)∀t∈T
  • 对称:E(t,u)⇒E(u,t)
  • 传递:E(t,u)∧E(u,v)⇒E(t,v)
  • 用E做为等价性的定义,当且仅当E(a,b)时,咱们会说a等于b。

等价关系:自反,对称,传递
这两个概念是等价的。函数

  • 等价关系致使抽象函数(关系分区T,所以f将每一个元素映射到其分区类)。
  • 抽象函数引起的关系是等价关系。

使用观察性能

咱们能够谈论抽象价值之间的等价性的第三种方式就是外部人(客户)能够观察他们的状况
使用观察。 咱们能够说,当两个对象没法经过观察进行区分时,这两个对象是相同的 - 咱们能够应用的每一个操做对两个对象都产生相同的结果。站在外部观察者角度测试

就ADT而言,“观察”意味着调用对象的操做。 因此当且仅当经过调用抽象数据类型的任何操做不能区分它们时,两个对象是相等的。

==与equals()

Java有两种不一样的操做,用于测试相等性,具备不一样的语义。

  • ==运算符比较引用。

它测试引用等价性。 若是它们指向内存中的相同存储,则两个引用是==。 就快照图而言,若是它们的箭头指向相同的对象气泡,则两个引用是==。

  • equals()操做比较对象内容

换句话说,对象等价性。

必须为每一个抽象数据类型适当地定义equals操做。在自定义ADT时,须要重写对象的equals()方法

  • 当咱们定义一个新的数据类型时,咱们有责任决定数据类型值的对象相等是什么意思,并适当地实现equals()操做。

==运算符与equals方法

对于基本数据类型,您必须使用==对基本数据类型,使用==断定相等
对于对象引用类型对象类型,使用equals()

  • ==运算符提供身份语义若是用==,是在判断两个对象身份标识ID是否相等(指向内存里的同一段空间)
  • 彻底由Object.equals实现
  • 即便Object.equals已被覆盖,这不多是你想要的!
  • 你应该(几乎)老是使用.equals

重写方法的提示

若是你想覆盖一个方法:

  • 确保签名匹配
  • 使用@Override编译器有你的背部
  • 复制粘贴声明(或让IDE为你作)

不可变类型的等价性

equals()方法由Object定义,其默认含义与引用相等相同。在对象中实现的缺省equals()方法是在判断引用等价性
对于不可变的数据类型,这几乎老是错误的。
咱们必须重写equals()方法,将其替换为咱们本身的实现。

重写与重载

在方法签名中犯一个错误很容易,而且当您打算覆盖它时重载一个方法。
只要你的意图是在你的超类中重写一个方法,就应该使用Java的批注@Override。
经过这个注解,Java编译器将检查超类中是否存在具备相同签名的方法,若是签名中出现错误,则会给出编译器错误。

instanceof

instanceof运算符测试对象是不是特定类型的实例。
使用instanceof是动态类型检查,而不是静态类型检查。
通常来讲,在面向对象编程中使用instanceof是一种陋习。 除了实施等价性以外,任何地方都应该禁止。
这种禁止还包括其余检查对象运行时类型的方法。

  • 例如,getClass()也是不容许的。

对象契约

对象中equals()的契约

您重写equals()方法时,您必须遵照其整体契约:

  • 等于必须定义一个等价关系
  • 即一种等价关系:自反,传递,对称
  • equals必须一致:对方法的重复调用必须产生相同的结果,前提是没有在对象的等值比较中使用的信息被修改;除非对象被修改了,不然调用屡次等于应一样的结果
  • 对于非空引用x,x.equals(null)应返回false;
  • hashCode()必须为等于equals方法的两个对象产生相同的结果。 “相等”的对象,其hashCode()的结果必须一致

Equals契约

equals方法实现等价关系:

  • 自反:对于任何非空参考值x,x.equals(x)必须返回true。
  • 对称:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递:对于任何非空引用值x,y,z,若是x.equals(y)返回true而且y.equals(z)返回true,则x.equals(z)mus返回true。
  • 一致性:对于任何非空引用值x和y,若是修改了在对象上的等值比较中没有使用的信息,则x.equals(y)的多个调用始终返回true或始终返回false。
  • 对于任何非空引用值x,x.equals(null)必须返回false。

equals是全部对象的全局等价关系。

打破等价关系

咱们必须确保由equals()实现的等价性定义其实是一个前面定义的等价关系:自反,对称和传递。

  • 若是不是,那么依赖于等价性的操做(如集合,搜索)将表现出不规律和不可预测的行为。
  • 你不想用一个数据类型进行编程,其中有时等于b,但b不等于a。
  • 会产生微妙而痛苦的错误。

打破哈希表

散列表是映射的表示:将键映射到值的抽象数据类型。

  • 哈希表提供了恒定的时间查找,因此它们每每比树或列表执行得更好。 密钥没必要订购,或具备任何特定的属性,除了提供equals和hashCode。

哈希表如何工做:

  • 它包含一个数组,该数组的初始化大小与咱们但愿插入的元素的数量相对应。
  • 当提供一个键和一个值用于插入时,咱们计算该键的哈希码,并将其转换为数组范围内的索引(例如,经过模分割)。 该值而后插入该索引。

哈希表的rep不变量包含密钥在由其哈希码肯定的时隙中的基本约束。

散列码的设计使密钥均匀分布在索引上。
但偶尔会发生冲突,而且两个键被放置在相同的索引处。
所以,不是在索引处保存单个值,而是使用哈希表实际上包含一个键/值对列表,一般称为哈希桶。
一个键/值对在Java中被简单地实现为具备两个字段的对象。
插入时,您将一对添加到由散列码肯定的阵列插槽中的列表中。
对于查找,您散列密钥,找到正确的插槽,而后检查每一个对,直到找到其中的密钥等于查询密钥的对。

hashCode契约

只要在应用程序执行过程当中屡次调用同一对象时,只要修改了对象的等值比较中未使用的信息,hashCode方法就必须始终返回相同的整数。

  • 该整数不须要从应用程序的一次执行到同一应用程序的另外一次执行保持一致。

若是两个对象根据equals(Object)方法相等,则对这两个对象中的每一个对象调用hashCode方法必须产生相同的整数结果。等价的对象必须有相同的的hashCode

  • 根据equals(Object)方法,若是两个对象不相等,则不要求对两个对象中的每个调用hashCode方法都必须产生不一样的整数结果。

可是,程序员应该意识到,为不相等的对象生成不一样的整数结果可能会提升散列表的性能。不相等的对象,也能够映射为一样的的hashCode,但性能会变差

相等的对象必须具备相同的散列码

  • 若是你重写equals,你必须重写hashCode

不相等的对象应该有不一样的哈希码

  • 构建时考虑全部的值域

除非对象发生变化,不然散列代码不能更改

重写hashCode()

确保合约知足的一个简单而激烈的方法是让hashCode始终返回一些常量值,所以每一个对象的散列码都是相同的。

  • 这符合Object合同,可是它会带来灾难性的性能影响,由于每一个密钥都将存储在同一个槽中,而且每一个查找都会退化为沿着长列表的线性搜索。

标准是计算用于肯定相等性的对象的每一个组件的哈希代码(一般经过调用每一个组件的hashCode方法),而后组合这些哈希码,引入几个算术运算。

打破哈希表

为何对象合同要求相同的对象具备相同的哈希码?

  • 若是两个相等的对象有不一样的哈希码,它们可能被放置在不一样的槽中。
  • 所以,若是您尝试使用与插入值相同的键来查找值,则查找可能会失败。

Object的默认hashCode()实现与其默认的equals()一致:

重写hashCode()

Java的最新版本如今有一个实用程序方法Objects.hash(),能够更容易地实现涉及多个字段的哈希码。
请注意,若是您根本不重写hashCode(),您将从Object得到一个Object,该Object基于对象的地址。
若是你有等价性的权利,这将意味着你几乎确定会违反合同

  • 两个相同的对象,必定要有一样的hashcode。

通常规则:
覆盖equals()时老是覆盖hashCode()。

可变类型的等价性

等价性:当两个对象没法经过观察区分时,它们是等价的。

对于可变对象,有两种解释方法:

  • 当它们不能经过不改变对象状态的观察进行区分时,即只经过调用观察者,生产者和建立者方法。这一般被严格地称为观察等价性,由于它在当前的程序状态下测试两个对象是否“看起来”是相同的。

观察等价性:在不改变状态的状况下,两个可变对象是否看起来一致

  • 当他们没法经过任何观察来区分时,即便状态发生变化。这个解释容许调用两个对象的任何方法,包括增变器。这被称为行为等价性,由于它测试这两个对象在这个和全部将来的状态中是否会“表现”相同。

行为等价性:调用对象的任何方法都展现出一致的结果

注意:对于不可变的对象,观察和行为的等价性是相同的,由于没有任何变值器方法。

Java中的可变类型的等价性

对可变类型来讲,每每倾向于实现严格的观察等价性

  • Java对大多数可变数据类型(例如Collections)使用观察等价性,但其余可变类(如StringBuilder)使用行为等价性。
  • 若是两个不一样的List对象包含相同的元素序列,则equals()报告它们相等。

可是使用观察等价性致使微妙的错误,而且事实上容许咱们轻易地破坏其余集合数据结构的表明不变量。但在有些时候,观察等价性可能致使错误,甚至可能破坏RI

这是怎么回事?

List <String>是一个可变对象。 在像List这样的集合类的标准Java实现中,突变会影响equals()和hashCode()的结果。
当列表第一次放入HashSet时,它将存储在当时与其hashCode()结果相对应的哈希桶/散列桶中。
当列表随后发生变化时,其hashCode()会发生变化,但HashSet没有意识到它应该移动到不一样的存储桶中。 因此它再也找不到了。
当equals()和hashCode()可能受突变影响时,咱们能够打破使用该对象做为关键字的哈希表的不变性。

若是可变对象用做集合元素,必须很是当心。

若是对象的值以影响等于比较的方式更改,而对象是集合中的元素,则不会指定集合的行为。 若是某个可变的对象包含在集合类中,当其发生改变后,集合类的行为不肯定,务必当心
不幸的是,Java库对于可变类的equals()的解释并不一致。 集合使用观察等价性,但其余可变类(如StringBuilder)使用行为等价性。 在JDK中,不一样的mutable类使用不一样的等价性标准...

从这个例子中学到的经验教训

equals()对可变类型,实现行为等价性便可

一般,这意味着两个引用应该是equals()当且仅当它们是同一个对象的别名。也就是说,只有指向一样内存空间的对象,才是相等的。
因此可变对象应该继承Object的equals()和hashCode()。 对可变类型来讲,无需重写这两个函数,直接继承Object对象的两个方法便可。
对于须要观察等价性概念的客户(两个可变对象在当前状态下“看起来”是否相同),最好定义一个新方法,例如similar()。

equals()和hashCode()的最终规则

对于不可变类型:

  • equals()应该比较抽象值。 这与equals()应该提供行为等价性相同。
  • hashCode()应该将抽象值映射到一个整数。
  • 因此不可变类型必须覆盖equals()和hashCode()。

对于可变类型:

  • equals()应该比较引用,就像==同样。 一样,这与等价性()应该提供行为等价性同样。
  • hashCode()应该将引用映射为一个整数。
  • 因此可变类型不该该重写equals()和hashCode(),而应该简单地使用Object提供的默认实现。 不幸的是,Java不遵循这个规则,致使咱们上面看到的陷阱。

对象中的clone()

clone()建立并返回此对象的副本。
“拷贝复制”的确切含义可能取决于对象的类别。
通常意图是,对于任何对象x:

x.clone() != x
x.clone().getClass() == x.getClass() 
x.clone().equals(x)

自动打包和等价性

基本类型及其对象类型等价性,例如int和Integer。
若是您建立两个具备相同值的Integer对象,则它们将相互为equals()。
可是若是x == y呢?
-----错误(由于引用等价性)
可是若是(int)x ==(int)y呢?
-----正确

总结

等价性是实现抽象数据类型(ADT)的一部分。

  • 等价性应该是一种等价关系(反身,对称,传递)。
  • 相等和散列码必须相互一致,以便使用散列表(如HashSet和HashMap)的数据结构可以正常工做。
  • 抽象函数是不变数据类型中等式的基础。
  • 引用等价性是可变数据类型中等价性的基础; 这是确保随时间的一致性并避免破坏散列表的不变式的惟一方法。

减小错误保证安全

  • 使用集合数据类型(如集合和地图)须要正确实现相等和散列码。 编写测试也是很是理想的。 因为Java中的每一个对象都继承了Object实现,因此不可变类型必须重写它们。

容易明白

  • 读过咱们规范的客户和其余程序员会但愿咱们的类型实现适当的等价性操做,若是咱们不这样作,会感到惊讶和困惑。

准备好改变

  • 为不可变类型正确实施的等价性将参考等价性与抽象价值的等价性分开,从客户身上隐藏咱们是否共享价值的决定。 选择可变类型的行为而不是观察等价性有助于避免意外的别名错误。
相关文章
相关标签/搜索