==、equals、hashcode

  • ==
  • equals方法
  • hashCode方法
  • 不重写equals方法和hashCode方法
  • 只重写equals方法
  • 重写equals方法和hashCode方法
  • 总结


==

在Java中,==的作用是比较两个值是否相等,对于基本数据类型,比较的是数值。而对于引用类型,则比较两个对象的内存地址。

int A = 10;
	int B = 20;
	System.out.println(A == B); //比较数值大小
    Animal animal1 = new Animal("狗","汪汪汪");
    Animal animal2 = new Animal("狗","汪汪汪");
    System.out.println(animal1 == animal2); // 比较引用类型变量内存地址的值

	false
	false

尽管对象animal1和animal2的内容相同,但它们是new出来的,Jvm为它们分配了两块不同的堆内存空间,因此他们内存地址的值不同。

equals方法

equals方法定义在Object类中,因此所有的类都拥有equals方法。equals方法的默认行为是比较两个对象的内存地址是否相等,相等返回true,否则返回false,下面给出Object中equals方法的源码:

public boolean equals(Object obj) {
        return (this == obj);
    }

可以看出,equals方法的默认行为就是用”==“来比较两个对象的内存地址的值。

一种更为实用的做法是:定义类时,覆盖equals方法,让equals方法按照程序员的意愿来比较两个对象:

// Animal.java
	@Override
    public boolean equals(Object obj) {
        if(obj == null) return false;
        if(obj instanceof Animal){
            Animal animal = (Animal) obj;
            return this.name.equals(animal.getName());
        }
        return super.equals(obj);
    }

根据name这个属性来判断两个对象是否相等。

hashCode方法

hashCode方法也是定义在Object类中,因此每个对象都拥有hashCode方法:

// Object.java
	public native int hashCode();

可以看出,Object类中的hashCode方法是本地方法,hashCode方法的默认行为是对堆上的对象产生独特值。如果一个类没有重写 hashCode方法,则该类的两个对象无论如何都不会相等。

下面给出HashCode的官方文档定义:

hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。 

hashCode 的常规协定是: 
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。 
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。 
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。 
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。) 

当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

官方文档说道:当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。 也就是说,重写equals方法后,也要重写hashCode方法是Java中的一个协议,需要程序员去遵守。

至于为什么要有这个协议,是因为Java存在集合这样的数据结构,这种数据结构需要对象持有一个特有的哈希值来确定对象在集合底层结构中的位置。

下面基于HashSet来举三个例子,HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合

不重写equals方法和hashCode方法

class Person {
    private String name;
    private int age;
    private String sex;

    Person(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}

public static void main(String[] args) {
	HashSet<Person> set = new HashSet<>();
	Person p1 = new Person("张三",18,"男");
	Person p2 = new Person("张三",18,"男");
	set.add(p1);
	set.add(p2);
	
	for (Person e: set) {
	    System.out.println(e.toString());
	}
}

结果是:
Person{name='张三', age=18, sex='男'}
Person{name='张三', age=18, sex='男'}

main函数中,我们用new符号创建了两个不同的对象,因此在内存地址不一样,但是它们的内容完全一致相同,都是name='张三', age=18, sex='男'
我们使用HashSet是不允许存储两个相同的对象的,可上述结果却违背了该原则,哪里出了问题?HashSet的源码当然是没问题的,因此问题出在Person类上。是不是没有重写equals方法导致的?下面重写equals方法,具体的做法是,比较两个对象的所有属性,如果都相等,则表示这两个对象相等,否则表示这两个对象不相等。

只重写equals方法

class Person {
    private String name;
    private int age;
    private String sex;

    Person(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name) &&
                Objects.equals(sex, person.sex);
    }
}


public class HashCodeTest2 {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        Person p1 = new Person("张三",18,"男");
        Person p2 = new Person("张三",18,"男");
        set.add(p1);
        set.add(p2);

        for (Person e: set) {
            System.out.println(e.toString());
        }
    }
}

结果是:
Person{name='张三', age=18, sex='男'}
Person{name='张三', age=18, sex='男'}

不幸的是,结果仍然和第一个例子一样,原因是因为HashSet会先根据hashCode来确定存储索引,如果发生哈希冲突(hashCode值一样,使得对象映射到相同的存储索引上)才会去调用equals方法来比较两个对象内容是否相等。现在只重写了equals方法,没有重写hashCode方法,而hashCode方法的默认行为上面也提到了,返回的是内存地址对应的整数,这里的两个对象的内存地址显然不相等,因此两个对象在存储时没有发生哈希冲突,它们分别被定位到集合的不同的位置上,equals方法没有起作用。所以,必须重写hashCode方法,重写的方式也必须与重写equals的方式一致,这样才能确保equals方法和hashCode方法具有一致性(详细看上述Java对hashCode方法的官方说明)。

重写equals方法和hashCode方法

class Person {
    private String name;
    private int age;
    private String sex;

    Person(String name,int age,String sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name) &&
                Objects.equals(sex, person.sex);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }
}


public class HashCodeTest2 {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();
        Person p1 = new Person("张三",18,"男");
        Person p2 = new Person("张三",18,"男");
        set.add(p1);
        set.add(p2);

        for (Person e: set) {
            System.out.println(e.toString());
        }
    }
}

结果是:
Person{name='张三', age=18, sex='男'}

这次结果终于对了!这里我调用的是Java的Objects类的工具函数,构造了一个与equals方法一致的hashCode函数,这里的一致是指,对象的hashCode是根据对象的name、age、sex三个属性来产生的,与equals的实现方式对应。所以,由于这两个对象的内容相等,因此必定会产生相同的hashCode值。add这两个对象到HashSet时会发送哈希冲突,hashSet就会调用equals方法来比较对象的内容,所以这时HashSet就不会把他们都add进去了。

总结

  1. equals方法的默认行为是比较两个对象的内存地址值,hashCode方法的默认行为是返回内存地址值对应的int整数。
  2. 如果程序中不涉及集合结构(Set的子类),那么重写了equals方法,不一定需要重写HashCode方法;如果涉及到集合结构,重写了equals方法,必须需要重写HashCode方法。
  3. 重写equals方法和hashCode方法的方式要对应!比如,重写equals方法时涉及到Person对象的name、age、sex属性,那么重写hashCode方法也必须根据Person对象的name、age、sex属性来确定返回的int整数。这就是官方文档所说的如果根据 equals方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode方法都必须生成相同的整数结果。

学习总结,若有不妥当的地方,望指正。