软件构造课程总结(八)——ADT和OOP中的“等价性”

等价关系

ADT是对数据的抽象,体现为一组对数据的操作

对于抽象数据类型,抽象函数(AF)解释了如何将具体的表示值解释为抽象类型的值,我们看到了抽象函数的选择如何决定如何编写实现每个ADT操作的代码。

抽象函数(AF)提供了一种清晰地定义ADT上的等价操作的方法。

数据类型的等价性:

现实中的每个对象实体都是独特的,因此,两个物理对象永远不会真正“平等”;它们只有一定的相似程度。然而,在人类语言世界中,在数学概念世界中,你可以为同一件东西有多个名称。所以很自然会问当两个表达式代表相同的东西: 1+2,√9和3是相同理想数学值的替代表达式。

等价关系:自反、对称、传递

对于像==或equals()这样的布尔值二进制操作,等价值E是该操作返回true的成对(x,y)的集合。

不变类型的等价性

使用AF来定义等价关系

为了使用f作为等式的定义,我们假设a等于b当且仅当f (a)=f (b)。即AF映射到同样的结果,则等价。

由抽象函数引起的关系是一种等价关系。

使用观察来定义等价关系

我们谈论抽象价值之间平等的另一种方式是,客户端可以观察到它们的情况。

使用观察方法。当两个对象不能通过观察来区分时,我们可以说它们是相等的——我们可以应用的每一个操作都会对两个对象产生相同的结果。

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

== vs. equals()

Java有两个不同的操作来测试相等性,它们具有不同的语义。

  • 引用等价性==操作符比较引用。如果两个引用指向内存中的相同存储器,则它们是==。在快照图中,如果两个引用的箭头指向同一个对象气泡,则这两个引用为==

  • 对象等价性equals() 操作会比较对象的内容——换句话说,就是对象的相等性。

在自定义ADT时,需要重写Object的equals()

当我们定义一个新的数据类型时,我们的责任是决定对象相等对于数据类型的值意味着什么,并适当地实现equals()操作。

== 操作符 vs. equals()方法

  • ==对于基本数据类型,使用==判定相等
  • 对于对象类型,使用equals()判定相等

如果用==,是在判断两个对象身份标识 ID是否相等(指向内存里的同一段空间)。

对于对象类型,我们需要重写父类的equals()方法。

如果您想覆盖一个方法:

  • 确保签名匹配
  • 使用 @Override
  • 执行复制和粘贴spec(或让IDE为您执行)

实现equals()

不变类型的平等

equals()方法由Object定义,其默认实现如下所示:

请添加图片描述

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

在重写时需要注意方法签名中参数列表是一个Object类型,不应该更改原来的参数列表。

使用 @Override,Java编译器将检查超类中是否实际存在具有相同签名的方法,如果您在签名中犯了错误,则会给您一个编译器错误。

标准的写法(例):

@Override 
public boolean equals(Object o) {
	if (!(o instanceof PhoneNumber)) // Does null check
		return false;
	PhoneNumber pn = (PhoneNumber) o;
	return pn.lineNumber == lineNumber
			&& pn.prefix == prefix
			&& pn.areaCode == areaCode;
}

instanceof

  • instanceof操作符将测试对象是否为特定类型的实例。
  • 使用instanceof操作符是动态类型检查,而不是静态类型检查。
  • 一般来说,在面向对象编程中使用实例是一件坏事。除了实现equals()之外,任何地方都不允许它。
    • 这一禁令还包括检查对象的运行时类型的其他方法。例如,getClass()也不允许使用。(违反OOP原则)

建议用(类型)多态取代instanceof

对象的“契约”

对象中equals()的契约

当重写equals()方法时,必须遵守其契约:

  • 满足自反、传递、对称的等价关系
  • 除非对象被修改了,否则调用多次equals()应同样的结果
  • “相等”的对象,其hashCode()的结果必须一致

对于任何非空引用值x,x.equals(null)必须返回false。

equals是一个在所有对象上的全局等价关系,用“是否为等价关系”检验你的equals()是否正确。

对象中hashCode()的契约

每当在应用程序执行过程中对同一对象进行多次调用时,hashCode方法必须一致地返回相同的整数,只要不修改在该对象的平等比较中使用的信息。

如果两个对象根据 equals(Object) 方法相等,那么对这两个对象分别调用hashCode()方法必须产生相同的整数结果。

等价的对象必须有相同的hashCode。

不相等的对象,也可以映射为同样的hashCode,但性能会变差。

  • 相等的对象必须有相等的哈希代码
    • 如果重写equals(),必须也重写hashCode()
  • 不相等的对象应该有不同的哈希代码

    • 在构造它时考虑到所有的值字段
  • 除非对象突变,哈希代码必须不改变

对象的默认hashCode()实现:

请添加图片描述

重写hashCode()

确保满足契约的一个简单而粗暴的方法是,hashCode总是返回一些常量值,因此每个对象的哈希代码都是相同的。

这满足对象契约,但它会产生灾难性的性能影响,因为每个键都将存储在同一个插槽中,并且每个查找都将退化为沿着一个长列表的线性搜索。

标准方法是为用于确定相等式的对象的每个组件计算哈希代码(通常通过调用每个组件的hash code方法),然后将这些组件组合起来,加入一些算术运算

Java的最新版本现在有了一个实用程序方法对象。Objects.hash() ,它使实现涉及多个字段的哈希代码变得更容易。

请注意,如果您根本不重写hashCode(),那么您将从Object中得到一个,它是基于对象的地址的。但是需要注意保证两个equal的objects,一定要有同样的hashcode.

标准的写法(例):

使用字段构造哈希值:

@Override 
public int hashCode() {
	int result = 17; // Nonzero is good
	result = 31 * result + areaCode; // Constant must be odd
	result = 31 * result + prefix; // " " " "
	result = 31 * result + lineNumber; // " " " "
	return result;
}

使用对象组件构造哈希值:

@Override 
public int hashCode() {
	short[] hashArray = {areaCode, prefix, lineNumber};
	return Arrays.hashCode(hashArray);
}

可变类型的等价性

平等:当两个物体不能通过观察来区分时,它们是相等的。

对于可变对象,有两种方法来解释这一点:

  • 观察等价性:在不改变状态的情况下,两个可变对象是否看起来一致
  • 行为等价性:调用对象的任何方法都展示出一致的结果

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

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

  • Java对其大多数可变数据类型(如集合)使用观察性相等,但其他可变数据类(如StringBuilder)使用行为相等。

  • 如果两个不同的List对象包含相同的元素序列,则equals() 表示它们相等。

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

例:当把List存入HashSet中后,改变List的内容,从而改变了List的哈希值,导致判断HashSet中是否包含List的返回为False。

如果某个mutable的对象包含在HashSet集合类中,当其发生改变后,集合类的行为不确定 ,务必小心!

在JDK中,不同的mutable类使用不同的等价性标准:

  • 观察等价性

    • Date类的equals()
    • List类的equals()
  • 行为等价性

    • StringBuilder类的equals()

对可变类型,实现行为等价性即可。也就是说,只有指向同样内存空间的objects,才是相等的。所以对可变类型来说,无需重写这两个函数,直接继承Object的两个方法即可。

如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。

总结:

  • 不可变类型:重写equals()hashCode()
  • 可变类型:不该重写equals()hashCode()

clone() in Object

clone()将创建并返回此对象的一个副本。“复制”的确切含义可能取决于对象的类别。

一般的意图是,对于任何对象x:

请添加图片描述

从这些Clone()的契约中无法确保是深度复制!

自动包装和等价性

原始类型及其对象类型的等价物,例如,int和Integer。

如果创建两个具有相同值的Integer对象,它们将满足equals() ,但是不满足==

但是强制转换为int后,就可以满足==

Integer x = new Integer(3);
Integer y = new Integer(3);
System.out.println(x.equals(y));        //true
System.out.println(x == y);             //false
System.out.println((int)x == (int)y);   //true

请添加图片描述

但是,对于数字在-128到127间,则相等!(数字存于缓冲区,直接从缓冲区中取出,两次地址相同)

请添加图片描述

例:

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
发出的红包

打赏作者

梚辰

感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值