ADT与OOP中的等价性
在软件构造课上,老师将等价性和hashCode方法作为重点讲解,这里写一篇blog来复习一下相关的内容。
一、等价性、equals()和"=="
1.等价关系
等价关系是指对于关系E ⊆ T x T,满足:
(1)自反性
x.equals(x)必须返回true
(2)对称性
x.equals(y)与y.equals(x)的返回值必须相等
(3)传递性
若x.equals(y)为true,且y.equals(z)也为true,那么x.equals(z)必须为true
2、关于等价性的分类
java中的对象分为immutable的和mutable的对象,课上说不可变对象的等价性分为引用等价性和对象等价性,可变类型分为观察等价性和行为等价性。
个人以为对于所有对象都分为引用等价性和对象等价性:引用等价性就是 ==、对象等价性就是 equals。
只是对于可变类型的对象的equals实现起来比较复杂,所以在实现equals的思路又分了两种观察等价性和行为等价性。
观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致,但是在集合类有关hashcode的时候可能会出现问题。
行为等价性:调用对象的任何方法都展示出一致的结果,其实就是继承Object.equals(),实现方式也是用“==”
3、equals函数
首先看一下Object类中的equals函数。
public class Object() {
...
public boolean equals(Object that) {
return this == that;
}
}
对于这个缺省的equals函数,我的理解是判断两个对象是否指向同一个内存空间(不太确定),所以一般来说,对于自己编写的类,需要以上文的原则重写这个函数。重写函数的代码基本如下:
1 @Override 2 public boolean equals(Object o) { 3 if (!(o instanceof Duration)) return false; 4 Duration that = (Duration) o; 5 return this.getLength() == that.getLength(); 6 }
注意,这里的参数必须是Object型。这是因为这是对父类函数的重写(override),参数列表必须相同,如果参数类型不同,那是重载(overload)。比如说我们把equals函数写成下面的格式:public boolean equals(Duration),则如果调用函数传入的参数类型是Duration,当然默认调用是我们写的这个函数,但如果参数是其他的类型,因为类型不匹配,所以会调用父类的equals函数,发生错误。
3.instanceof
instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
boolean result = obj instanceof Class
其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。obj 必须为引用类型,不能是基本类型。instanceof 运算符只能用作对象的判断。
int i = 0; System.out.println(i instanceof Integer);//编译不通过 System.out.println(i instanceof Object);//编译不通过
关于 null 类型的描述在官方文档:https://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.1 有一些介绍。一般我们知道Java分为两种数据类型,一种是基本数据类型,有八个分别是 byte short int long float double char boolean,一种是引用类型,包括类,接口,数组等等。而Java中还有一种特殊的 null 类型,该类型没有名字,所以不可能声明为 null 类型的变量或者转换为 null 类型,null 引用是 null 类型表达式唯一可能的值,null 引用也可以转换为任意引用类型。我们不需要对 null 类型有多深刻的了解,我们只需要知道 null 是可以成为任意引用类型的特殊符号。
System.out.println(null instanceof Object);//false
二、hashCode()方法
提到等价性,就不得不提一下Java中的hashCode()方法。
1、定义
在官方的文档中,hashCode有如下的定义 :
hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。
hashCode 的常规协定是:在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
2、提炼
1、hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;
2、如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
3、如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
4、两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
3、具体实例
1 public class HashTest { 2 private int i; 3 4 public int getI() { 5 return i; 6 } 7 8 public void setI(int i) { 9 this.i = i; 10 } 11 12 public int hashCode() { 13 return i % 10; 14 } 15 16 public final static void main(String[] args) { 17 HashTest a = new HashTest(); 18 HashTest b = new HashTest(); 19 a.setI(1); 20 b.setI(1); 21 Set<HashTest> set = new HashSet<HashTest>(); 22 set.add(a); 23 set.add(b); 24 System.out.println(a.hashCode() == b.hashCode()); 25 System.out.println(a.equals(b)); 26 System.out.println(set); 27 } 28 }
结果是:
true
false
[com.ubs.sae.test.HashTest@1, com.ubs.sae.test.HashTest@1]
以上这个示例,我们只是重写了hashCode方法,从上面的结果可以看出,虽然两个对象的hashCode相等,但是实际上两个对象并不是相等;,我们没有重写equals方法,那么就会调用object默认的equals方法,是比较两个对象的引用是不是相同,显示这是两个不同的对象,两个对象的引用肯定是不定的。这里我们将生成的对象放到了HashSet中,而HashSet中只能够存放唯一的对象,也就是相同的(适用于equals方法)的对象只会存放一个,但是这里实际上是两个对象a,b都被放到了HashSet中,这样HashSet就失去了他本身的意义了。
此时把equals方法给加上:
public class HashTest { private int i; public int getI() { return i; } public void setI(int i) { this.i = i; } public boolean equals(Object object) { if (object == null) { return false; } if (object == this) { return true; } if (!(object instanceof HashTest)) { return false; } HashTest other = (HashTest) object; if (other.getI() == this.getI()) { return true; } return false; } public int hashCode() { return i % 10; } public final static void main(String[] args) { HashTest a = new HashTest(); HashTest b = new HashTest(); a.setI(1); b.setI(1); Set<HashTest> set = new HashSet<HashTest>(); set.add(a); set.add(b); System.out.println(a.hashCode() == b.hashCode()); System.out.println(a.equals(b)); System.out.println(set); } }
此时得到的结果就会如下:
true
true
[com.ubs.sae.test.HashTest@1]
3、hashCode的不同针对
1.针对不可变类型
- equals()应该比较抽象值是否相等。
- hashCode()应该将抽象值映射为整数。
- 所以不可变类型应该同时覆盖equals()和hashCode()。
2.针对可变类型
- equals()应该比较索引,就像"=="一样。
- hashCode()应该将索引映射为整数。
- 所以可变类型不应该将equals()和hashCode()覆盖,而是直接继承Object中的方法。Java没有为大多数聚合类遵守这一规定,这也许会导致上面看到的隐秘bug。
鸣谢:
本博客参考如下内容:

浙公网安备 33010602011771号