Fork me on GitHub

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。

鸣谢:

本博客参考如下内容:

Java中的等价性_实践求真知-CSDN博客

如何测试java中对象的等价性_Chen_dSir的博客-CSDN博客

Java hashCode() 方法 | 菜鸟教程 (runoob.com)

posted @ 2021-07-04 22:12  牺牲的钢铁侠  阅读(96)  评论(0)    收藏  举报