文章目录提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
- 前言
- LAB2学习之重写equals hashcode
- (一)前言复习——什么是等价性
- 等价性的划分
- 引用等价性与对象等价性的概念—— == 和 equals()
- 对象等价性equals()
- (二)、可变类型与不可变类型的判相等
- (三)、重写而不是重载!
- instanceof 是什么?
- (四)、可变类型的等价性
- 观察等价性和行为等价性
前言
现在已经做完lab2了,这篇博客里总结一下在lab2中用到的一些知识,顺带复习一下
提示:以下是本篇文章正文内容
LAB2学习之重写equals hashcode (一)前言复习——什么是等价性 等价性的划分不可变对象的引用等价性==和对象等价性equals()
可变对象的观察等价性和行为等价性
- 双等号 比较的是索引。更准确的说,它测试的是指向相等(referential equality)。若是两个索引指向同一块存储区域,那它们就是==的。对于咱们以前提到过的快照图来讲,这双等号就意味着它们的箭头指向同一个对象。多用于基本数据类型。
- equals()操做比较的是对象的内容,换句话说,它测试的是对象值相等(object equality)。在每个ADT中,equals操操作须合理定义。
equals()操作比较对象内容——换句话说,对象相等。
对对象类型,使用equals()
然而在Object 中实现的缺省equals() 是在判断引用等价性,所以应该重写equals()方法。我们在判断对象是否相等时往往使用一些属性,而地址不一定完全相同。
public boolean equals(Object obj) { return (this == obj); }
但其实在一些JAVA提供的类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而再也不是比较类在堆内存中的存放地址了。
这里String这个常用的类里面有些有意思的事情,不过也很容易理解,注意区分字符串和对象:
- 不可变类型:
equals() 应该比较抽象值是否相等。这和 equals() 比较行为相等性是一样的。hashCode() 应该将抽象值映射为整数。
所以不可变类型应该同时覆盖 equals() 和 hashCode().
- 可变类型:
equals() 应该比较索引,就像 ==一样。同样的,这也是比较行为相等性。hashCode() 应该将索引映射为整数。
所以可变类型不应该将 equals() 和 hashCode() 覆盖,而是直接继承 Object中的方法。Java没有为大多数聚合类遵守这一规定,这也许会导致上面看到的隐秘bug。
ps:可变类型在快照图中一直都指向那个对象,只更改内容,需要比较
注意:equals判定为相同, hashCode一定相同。equals判定为不同,hashCode不一定不同。(考试重点)
即:hashCode必须为两个被该equals方法视为相等的对象产生相同的结果。
不相等的对象产生不相同的hashCode可以提高哈希表的性能。
举一个例子:
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
这是为什么呢?
即使d2和o2最终参照相同的对象在内存中,对他们来说你仍然得到不同的结果。
在这里我们对equals方法实际上是进行了重载而不是重写,Object是一切类的父类,Duration中实际上有两个equals方法:
隐式equals(Object)继承Object,和新的equals(Duration)。因此这里的两个调用实际上是调用两个方法:
- d1.equals(o2)我们最终会调用equals(Object)实现。
- d1.equals(d2),我们最终调用equals(Duration)版本。
正确的重写方法如下:
@Override public boolean equals (Object thatObject) { if (!(thatObject instanceof Duration)) return false; Duration thatDuration = (Duration) thatObject; return this.getLength() == thatDuration.getLength(); }instanceof 是什么?
instanceof 是 Java 的保留关键字,它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。
- 基本类型不能用于 instanceof 判断.
- 对于 null,它只能用来判断是否是引用类型,并且返回的总是 false,表明它不是任意一个引用类型的对象实例。而且想要编译通过null 只能放在 instanceof 关键字的左边
- instanceof 关键字一般用于强制转换,在强转之前用它来判断是否可以强制转换:
public B convert(A a) { if (a instanceof B) { return (B) a; } return null; }
这里作为JAVA初学者,大佬跟我说,这个参数传进来的是Object类,他虽然更大,但是仍然可能是Duration的一个实例,Object类可以传入更多的东西,直接上贝爷的图:(感谢学校的大佬们!)
对于可变对象来说,它们多了一种新的可能:通过在观察前调用改造者,我们可以改变其内部的状态,从而观察出不同的结果。
观察等价性和行为等价性- 观察等价性: 两个索引在不改变各自对象状态的前提下不能被区分。即通过只调用observer,producer和creator的方法,它测试的是这两个索引在当前程序状态下“看起来”相等。
- 行为等价性: 两个索引在任何代码的情况下都不能被区分,即使有一个对象调用了改造者。它测试的是两个对象是否会在未来所有的状态下“行为”相等。
- 对于不可变对象 ,观察相等和行为相等是完全等价的,因为它们没有改造者改变对象内部的状态。
- 对于可变对象 ,Java通常实现的是观察相等。例如两个不同的 List 对象包含相同的序列元素,那么equals() 操作就会返回真。
PS:在有些时候,观察等价性可能导致bug,甚至可能破坏RI。
在Java对可变对象的实现中,改造操作通常都会影响 equals() 和 hashCode()的结果。以Set为例,在刚开始放入HashSet,它是存储在此时 hashCode() 对应的索引位置。但是如果发生了改变,计算 hashCode() 会得到不一样的结果,但是 HashSet 对此并不知道,所以我们调用contains时候就会找不到这个对象。
当 equals() 和 hashCode() 被改动影响的时候,我们就破坏了哈希表利用对象作为键的不变量。
所以对可变类型来说,无需重写这两个函数,直接继承 Object对象的两个方法即可。 如果一定要判断两个可变对象看起来是否一致,最好定义一个新的方法。这个在lab2中也有体现,在构建Vertex类和Edge类的时候,我对可变的类重新实现了一个check方法。