android面试灵魂拷问(一) --->Object重写equals方法后,为什么需要重写hashcode方法?

97 阅读5分钟

每日一道面试题

Java 中深拷贝与浅拷贝的区别?

浅拷贝:在拷贝对象的时候,只对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝

深拷贝:在拷贝对象的时候,只对基本数据类型进行值传递,对引用数据类型,创建一个新的引用对象,并复制其内容,此为深拷贝

结论:深、浅拷贝的区别是对引用对象进行传递般的引用还是创建一个新的对象

前言

1、hash函数:将任意长度的输入值转变成固定长度的值输出,该值称为散列值。hash函数不是指某种特定的函数,而是一类函数,它有各种各样的实现。比如:md5、sha-1。

2、hash表:根据hash计算输入的值(Key)而直接访问在内存存储位置的数据结构,其底层实现有两种方式:数组+链表、数组+二叉树。

3、hashcode:一个用于标识对象的唯一标识符。

4、equals:比较两个对象的内容是否相等。默认情况下(即没有重写object的equals),比较两个对象的引用是否相同。默认equals源码,如:

//Object的equals源码
public boolean equals(Object obj) {
    return (this == obj);
}

正文

为了更加理解equals方法和hashcode方法的作用,下面将使用例子进行假设验证:

例1:重写equals方法,不重写hashcode方法
public class Cat {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return false; //引用相同
        if (obj instanceof Cat) {
            Cat cacheCat = (Cat) obj;
            return cacheCat.age == this.age && cacheCat.name.equals(this.name);
        }
        return false;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}
例2:不重写equals方法,重写hashcode方法
import java.util.Objects;

public class Cat {
    private String name;
    private int age;
   //...省略...
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    //...省略...
}
例3:重写equals方法,重写hashcode方法
import java.util.Objects;

public class Cat {
    private String name;
    private int age;
    //...省略...
    @Override
    public boolean equals(Object obj) {
        if (obj == this) return false; //引用相同
        if (obj instanceof Cat) {
            Cat cacheCat = (Cat) obj;
            return cacheCat.age == this.age && cacheCat.name.equals(this.name);
        }
        return false;
    }

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

创建相同内容不同的cat对象进行比较,并将它们加入HashSet集合中,输入结果:

Cat cat = new Cat();
cat.setAge(11);
cat.setName("rank");
Cat cat1 = new Cat();
cat1.setAge(11);
cat1.setName("rank");
System.out.println("cat对象比较是否相同 = "+cat.equals(cat1));

HashSet<Cat> hashSet = new HashSet<>();
hashSet.add(cat);
hashSet.add(cat1);
// 打印 Set 中的所有数据
hashSet.forEach(System.out::println);

例1运行的结果:true   Cat{name='rank', age=11}   Cat{name='rank', age=11}

例2运行的结果:false   Cat{name='rank', age=11}   Cat{name='rank', age=11}

例3运行的结果:true   Cat{name='rank', age=11}

what?为什么会产生这种情况呢?请细细听我讲。

HashSet是一个没有重复元素的集合。那么为什么在例1、例2运行后输入的元素有重复呢?HashSet究竟做了什么?

HashSet-->put()
HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

从HashSet的add源码了解到,它其实调用的是HashMap的put方法进行存储数据的,那么HashMap的put方法到底做了什么?

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
    //注释1
    if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
    else {
        K k;
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
        //注释2
            e = p;
        else if (p instanceof TreeNode)
          //...省略
        else {
          //...省略
        }    
    }
}

HashMap是一种key-value的数据存储结构,数据是否需要进行存储或更新,根据key的hashcode和equals进行比较确定。 如注释1if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);中根据key的hashCode判断数组该下标是否存在数据;注释1不成立时,进入注释2;注释2if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))中根据key的hashcode和equals进行判断注释1中获取到的对象与需要存储对象的值是否相等(hashcode相同,值不一定相同),如相等,则只需更新对象的值即可。

简单了解完hashMap的存储逻辑后,再后顾上面的例1、例2、例3运行产生的结果是否了如指掌了呢

没错,例1由于没有重写对象的hashcode方法,在HashMap存储数据中导致注释1的条件成立,直接将数据存储到了一个新的位置;例2中由于没有重写对象的equals方法,导致注释2判断值出现错误,产生了新节点进行存储数据(hash冲突);例3中由于重写了equals方法和hashcode方法,导致注释1不成立,注释2成立,所以只需更新数据即可。(由于篇章问题,HashMap是如何处理hash冲突、数组扩容等这些问题在后面文章再详细介绍,敬请期待~)

结论

重写Object的equals方法时,为什么需要重写Object的hashcode方法?根据上面分析,某些集合或者函数等等中使用了对象hashcode的方法进行判断,如果我们不重写hashcode方法可能会出现程序运行错误,产生的结果和我们期望的结果不一致。但hashcode方法是否必须重写,这个需要根据不同的问题进行具体的分析,它不是必须重写的