什么是等价性?为何要讨论等价性?
三种等价性的方式
==与equals()
不可变类型的等价性
对象契约
可变类型的等价性
自动包装和等价性java
ADT上的相等操做程序员
ADT是经过建立以操做为特征的类型而不是其表示的数据抽象。
对于抽象数据类型,抽象函数(AF)解释了如何将具体表示值解释为抽象类型的值,而且咱们看到了抽象函数的选择如何决定如何编写实现每一个ADT操做的代码。
抽象函数(AF)提供了一种方法来清晰地定义ADT上的相等操做。编程
数据类型中值的相等性?数组
在物质世界中,每一个物体都是不一样的 - 即便两个雪花的区别仅仅是它们在太空中的位置,在某种程度上,即便是两个雪花也是不一样的。
因此两个实体对象永远不会真正“相等”。 他们只有类似的程度。
然而,在人类语言的世界中,在数学概念的世界中,对同一事物能够有多个名称。安全
使用AF或使用关系数据结构
使用抽象函数。 回想一下抽象函数f:R→A将数据类型的具体实例映射到它们相应的抽象值。 为了使用f做为等价性的定义,咱们说当且仅当f(a)= f(b)时等于b。
使用关系。 等价关系是E⊆T x T,即:ide
等价关系:自反,对称,传递
这两个概念是等价的。函数
使用观察性能
咱们能够谈论抽象价值之间的等价性的第三种方式就是外部人(客户)能够观察他们的状况
使用观察。 咱们能够说,当两个对象没法经过观察进行区分时,这两个对象是相同的 - 咱们能够应用的每一个操做对两个对象都产生相同的结果。站在外部观察者角度测试
就ADT而言,“观察”意味着调用对象的操做。 因此当且仅当经过调用抽象数据类型的任何操做不能区分它们时,两个对象是相等的。
Java有两种不一样的操做,用于测试相等性,具备不一样的语义。
它测试引用等价性。 若是它们指向内存中的相同存储,则两个引用是==。 就快照图而言,若是它们的箭头指向相同的对象气泡,则两个引用是==。
换句话说,对象等价性。
必须为每一个抽象数据类型适当地定义equals操做。在自定义ADT时,须要重写对象的equals()方法
==运算符与equals方法
对于基本数据类型,您必须使用==对基本数据类型,使用==断定相等
对于对象引用类型对象类型,使用equals()
重写方法的提示
若是你想覆盖一个方法:
equals()方法由Object定义,其默认含义与引用相等相同。在对象中实现的缺省equals()方法是在判断引用等价性
对于不可变的数据类型,这几乎老是错误的。
咱们必须重写equals()方法,将其替换为咱们本身的实现。
重写与重载
在方法签名中犯一个错误很容易,而且当您打算覆盖它时重载一个方法。
只要你的意图是在你的超类中重写一个方法,就应该使用Java的批注@Override。
经过这个注解,Java编译器将检查超类中是否存在具备相同签名的方法,若是签名中出现错误,则会给出编译器错误。
instanceof
instanceof运算符测试对象是不是特定类型的实例。
使用instanceof是动态类型检查,而不是静态类型检查。
通常来讲,在面向对象编程中使用instanceof是一种陋习。 除了实施等价性以外,任何地方都应该禁止。
这种禁止还包括其余检查对象运行时类型的方法。
对象中equals()的契约
您重写equals()方法时,您必须遵照其整体契约:
Equals契约
equals方法实现等价关系:
equals是全部对象的全局等价关系。
打破等价关系
咱们必须确保由equals()实现的等价性定义其实是一个前面定义的等价关系:自反,对称和传递。
打破哈希表
散列表是映射的表示:将键映射到值的抽象数据类型。
哈希表如何工做:
哈希表的rep不变量包含密钥在由其哈希码肯定的时隙中的基本约束。
散列码的设计使密钥均匀分布在索引上。
但偶尔会发生冲突,而且两个键被放置在相同的索引处。
所以,不是在索引处保存单个值,而是使用哈希表实际上包含一个键/值对列表,一般称为哈希桶。
一个键/值对在Java中被简单地实现为具备两个字段的对象。
插入时,您将一对添加到由散列码肯定的阵列插槽中的列表中。
对于查找,您散列密钥,找到正确的插槽,而后检查每一个对,直到找到其中的密钥等于查询密钥的对。
hashCode契约
只要在应用程序执行过程当中屡次调用同一对象时,只要修改了对象的等值比较中未使用的信息,hashCode方法就必须始终返回相同的整数。
若是两个对象根据equals(Object)方法相等,则对这两个对象中的每一个对象调用hashCode方法必须产生相同的整数结果。等价的对象必须有相同的的hashCode
可是,程序员应该意识到,为不相等的对象生成不一样的整数结果可能会提升散列表的性能。不相等的对象,也能够映射为一样的的hashCode,但性能会变差
相等的对象必须具备相同的散列码
不相等的对象应该有不一样的哈希码
除非对象发生变化,不然散列代码不能更改
重写hashCode()
确保合约知足的一个简单而激烈的方法是让hashCode始终返回一些常量值,所以每一个对象的散列码都是相同的。
标准是计算用于肯定相等性的对象的每一个组件的哈希代码(一般经过调用每一个组件的hashCode方法),而后组合这些哈希码,引入几个算术运算。
打破哈希表
为何对象合同要求相同的对象具备相同的哈希码?
Object的默认hashCode()实现与其默认的equals()一致:
重写hashCode()
Java的最新版本如今有一个实用程序方法Objects.hash(),能够更容易地实现涉及多个字段的哈希码。
请注意,若是您根本不重写hashCode(),您将从Object得到一个Object,该Object基于对象的地址。
若是你有等价性的权利,这将意味着你几乎确定会违反合同
通常规则:
覆盖equals()时老是覆盖hashCode()。
等价性:当两个对象没法经过观察区分时,它们是等价的。
对于可变对象,有两种解释方法:
观察等价性:在不改变状态的状况下,两个可变对象是否看起来一致
行为等价性:调用对象的任何方法都展现出一致的结果
注意:对于不可变的对象,观察和行为的等价性是相同的,由于没有任何变值器方法。
Java中的可变类型的等价性
对可变类型来讲,每每倾向于实现严格的观察等价性
可是使用观察等价性致使微妙的错误,而且事实上容许咱们轻易地破坏其余集合数据结构的表明不变量。但在有些时候,观察等价性可能致使错误,甚至可能破坏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()的最终规则
对于不可变类型:
对于可变类型:
对象中的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)的一部分。
减小错误保证安全
容易明白
准备好改变