【软件构造】第三章第五节 ADT和OOP中的等价性

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

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

OutLine

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

Notes

##  等价性equals() 和 ==

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

Java中的数据类型,可分为两类: 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()的判断方法

严格来讲,咱们能够从三个角度定义相等:ide

  • 抽象函数:回忆一下抽象函数(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();        
    }
}

尝试以下客户端代码,可获得this

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

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

  • 即便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() 方法:code

@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对象的两个方法便可。 若是必定要判断两个可变对象看起来是否一致,最好定义一个新的方法。

相关文章
相关标签/搜索