软件构造 3-5 Equality in ADT and OOP

3.5 ADTOOP 中的 等价性

一. 不可变类型的相等

1. 等价关系

  对于关系 E ⊆ T × T E\subseteq T\times T ET×T ,它满足:

  • 自反性 E ( t , t ) , ∀ t ∈ T E(t, t),\forall t\in T E(t,t),tT
  • 对称性 E ( t , u ) ⇒ E ( u , t ) E(t,u)\Rightarrow E(u,t) E(t,u)E(u,t)
  • 传递性 E ( t , u ) ∧ E ( u , v ) ⇒ E ( t , v ) E(t,u)\wedge E(u,v)\Rightarrow E(t,v) E(t,u)E(u,v)E(t,v)

  我们就说 a 等于 b 当且仅当 E(a, b)-。

2. 抽象函数

  由于抽象函数是
A F : R → A AF:R → A AF:RA
将具体的表示数据映射到了抽象的值。如果
A F ( a ) = A F ( b ) AF(a)=AF(b) AF(a)=AF(b)
我们说 ab 相等。

  这种方法不好,这是因为 AF 不一定是双射。

3.通过观察判定抽象值相等

  两个对象相等,当且仅当使用者无法观察到它们之间有不同,即每一个观察总会都会得到相同的结果

  从 ADT 来说,“观察”就意味着使用它的观察者/操作。所以也可以说两个对象相等当且仅当它们的所有 observer 方法返回相同的结果

  这里要注意一点,“观察者/操作”都必须ADT规格说明规定好的

4. ==equals()

  == 比较的是索引。更准确的说,它测试的是指向相等

  引用等价性:如果两个索引指向同一块存储区域,那它们就是==的。对于我们之前提到过的快照图来说,==就意味着它们的箭头指向同一个对象。一般使用在基本数据类型

  equals() 比较的是对象的内容。更准确的说,它测试的是对象值相等。在每个 ADT 中,equals 操作必须被合理定义。

5. 使用 equals()

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

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

  可以看到, equals()Object 中的实现方法就是测试指向/索引相等

  对于不可变类型的对象来说,这几乎总是错的。所以你需要重写(override) 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

在这里插入图片描述

  事实上, Duration 只是重载(overloaded)了 equals() 方法,因为它的方法标识Object中的不一样。

  也就是说,这是 Duration 中有两个 equals() 方法:一个是从 Object 隐式继承下来的 equals(Object) ,还有一个就是我们写的 equals(Duration)。

public class Duration extends Object {
    // explicit method that we declared:
    public boolean equals(Duration that) {
        return this.getLength() == that.getLength();
    }
    // implicit method inherited from Object:
    public boolean equals(Object that) {
        return this == that;
    }
}

  因为方法标识的原因重载而不是重写了的方法。在 Java 中,你可以使用 @Override 来提示编译器你是要后面的方法覆盖父类中的方法,而编译器会自动检查这个方法是否和父类中的方法有着相同的标识(产生覆盖),否则编译器会报错。


  instanceof 操作符用来测试一个实例是否属于特定的类型

   instanceof动态检查而非我们更喜欢的静态检查。

  普遍来说,在面向对象编程中使用 instanceof 是一个不好的选择。除了实现相等操作,instanceof 不能被使用。这也包括其他在运行时确定对象类型的操作,例如 getClass

  关于在 equals() 中使用 getClass 还是 instanceof 操作符存在一些争议,焦点集中于使用 instanceof 操作符可能会影响相等的对称性(父子类)。(《 Java 核心技术 卷一 第十版》的 5.2.2 节。)

二. Object 规约

1. 重写 equals

   Object 的规格说明有时候称为对象契约。当重写 equals 时,要遵守如下规定:

  • equals 必须定义一个等价关系:自反、对称、传递
  • equals 必须是确定的。即连续重复的进行相等操作,结果应该相同,除非其值进行了改变
  • 对于不是 null 的索引 xx.equals(null) 应该返回 false
  • 如果两个对象使用 equals 操作后结果为真,那么它们各自的 hashCode 操作的结果也应该相同

2. 破坏哈希表 / 哈希桶

  一个哈希表表示的是一种映射:从键值映射到的抽象数据类型。哈希表提供了常数级别查找,所以它通常比数或者列表的性能要好。键不一定有序的,也不一定有什么特别的属性,除了类型必须提供 equalshashCode 两个方法。

   Object 默认的 hashCode() 实现和默认的 equals() 保持一致。

public class Object {
	...
	public boolean equals(Object that) { return this == that; }
	public int hashCode() { return /* the memory address of this */;}
}

  当你重写 equals 后,将 hashCode 也重写。等价的对象必须有相同的 hashCode 。不同对象的哈希值可以一样(只是查找效率可能变低)可以不一样。

  当然也可以不重写,只要你能保证你的 ADT 不会被放入到 Hash 类型的集合类中。

三. 可变类型的相等

1. 两种相等

  • 观察等价性:两个索引在不改变各自对象状态的前提下不能被区分。例如,只调用观察者、生产者、创建者。它测试的是这两个索引在当前程序状态下“看起来”相等。
  • 行为等价性:两个索引在执行任何代码的情况下都不能被区分,即使有一个对象调用了改造者。它测试的是两个对象是否会在未来所有的状态下“行为”相等。

  对于可变对象,Java 通常实现的是观察等价性

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!
for (List<String> l : set) { 
    set.contains(l);// → false! 
}

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

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

   equals()hashCode() 被改动破坏了哈希表利用对象作为键的不变量

  java.util.Set规格说明中:

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

  如果某个 mutable 的对象包含在 Set 集合类中,当其发生改变后,集合类的行为不确定。


  可变类型的 equals() 应该实现行为等价性。这通常都意味着两个对象只有在是索引别名的时候 equals() 才会返回真。

  索引可变类型的 equals()hashCode() 应该直接从 Object 继承。但 Java 坚持它对可变类型的 equals() 的实现(使用的是观察等价性)。

  但有一些如 StringBuilder 使用的是行为等价性

  对于需要观察等价性操作的可变类型,最好是设计一个新的操作,例如 similar()sameValue()

四. equals()hashCode() 的总结

  对于不可变类型

  • equals() 应该比较抽象值是否相等。这和 equals() 比较行为相等性是一样的。
  • hashCode() 应该将抽象值映射为整数

  所以不可变类型应该同时重写 equals()hashCode()


  对于可变类型

  • equals() 应该比较索引( == )。比较行为等价性。
  • hashCode() 应该将索引映射为整数。

  所以可变类型不需要重写 equals()hashCode() ,而是直接继承 Object 中的方法。Java 没有为大多数聚合类遵守这一规定。


  clone() 方法浅拷贝,拷贝各属性引用。深拷贝是拷贝的是属性内容,将其放入拷贝后的相应属性。

x.clone() != x;//true
x.clone().getClass() == x.getClass();//true
x.clone().equals(x);//true

五. 自动包装与相等

  对于原始基本类型和包装类型equals 比较的是两个对象的值,但包装类型的 == 被重载,判断的是索引相等,而原始类型仍然是值相等

Integer x = new Integer(3);
Integer y = new Integer(3);
x.equals(y);// → true
x == y // returns false
(int)x == (int)y // returns true

  所以能真正的将 Integerint 互换。事实上 Java 会自动对 intInteger 进行转换(这被称作自动装箱和拆箱),这也会导致 bug

Map<String, Integer> a = new HashMap<>();
Map<String, Integer> b = new HashMap<>();
a.put("c", 1);//实质是 Integer.valueOf(3) 调用,只在 -128 到 127 中调用
b.put("c", 1);
System.out.println (a.get("a")== b.get("a"));//true

Integer x = new Integer(2);
Integer y = new Integer(2);
System.out.println(x==y);//false

Integer x = 2;
Integer y = 2;
System.out.println(x==y);//true
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值