浅析java中的hashcode()方法与equals()方法

时间:2021-10-14 16:13:05

hashcode()方法与equals()方法在很多的场合都涉及到!本章主要在本人使用的过程中的一些简单的总结,错误之处欢迎广大博友批评指正!

浅析java中的hashcode()方法与equals()方法

(一) Object的hashcode()方法

java中很多地方使用hash表来提高查找效率。在Java的Object类中有一个方法: 

    public native int hashCode();

注释里面巴拉巴拉说了一大堆,Object类的hashCode方法返回的是一个int类型的数值,并且是一个本地的方法,在Object类中没有具体的实现。
ps:使用native关键字说明这个方法是原生函数,也就是这个方法是用c/c++语言实现的,并且被编译成了dll,由java去调用,这些函数的实现体在dll中,jdk的源代码中并不包含。

hashcode()返回的到底是不是内存的地址

hashCode返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现!

(二)Object中的equals()方法

再来看一下Object中的equals()方法

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

其注释中是这样说的:

     * <p>
* The {@code equals} method for class {@code Object} implements
* the most discriminating possible equivalence relation on objects;

* that is, for any non-null reference values {@code x} and
* {@code y}, this method returns {@code true} if and only
* if {@code x} and {@code y} refer to the same object
* ({@code x == y} has the value {@code true}).
* <p>

也就是说,x.equsls(y),当x和y是同一个对象的引用时返回true,否则返回false!

在注释,我们同样看到一些通用约定,在我们重写equals()方法时候必须准守的这些约定:

* <ul>
* <li>It is <i>reflexive</i>: for any non-null reference value
* {@code x}, {@code x.equals(x)} should return
* {@code true}.
* <li>It is <i>symmetric</i>: for any non-null reference values
* {@code x} and {@code y}, {@code x.equals(y)}
* should return {@code true} if and only if
* {@code y.equals(x)} returns {@code true}.
* <li>It is <i>transitive</i>: for any non-null reference values
* {@code x}, {@code y}, and {@code z}, if
* {@code x.equals(y)} returns {@code true} and
* {@code y.equals(z)} returns {@code true}, then
* {@code x.equals(z)} should return {@code true}.
* <li>It is <i>consistent</i>: for any non-null reference values
* {@code x} and {@code y}, multiple invocations of
* {@code x.equals(y)} consistently return {@code true}
* or consistently return {@code false}, provided no
* information used in {@code equals} comparisons on the
* objects is modified.
* <li>For any non-null reference value {@code x},
* {@code x.equals(null)} should return {@code false}.
* </ul>

咱还是翻译过来吧,

  • 自反性:对于任何非null的引用值x,x.equals(x)必须返回true
  • 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)则必须返回true。
  • 一致性:对于任何非null的引用值x、y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一直返回true,或者一直返回false。
  • 非空特性:对于任何非null的引用值x,x.equals(null)必须返回false。

java有些类已经重写了equals方法,如String。

    public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}

好的,下面来一个例子说明equals()方法的使用

public class Dog {
private String name;
private String color;
public Dog(String name,String color){
this.name=name;
this.color=color;
}
}

我家养了一匹黄狗,隔壁老王也养了一匹黄狗,还都叫阿黄,怎么办?

public class Test {

public static void main(String[] args) {
Dog dog1=new Dog("阿黄","yellow");
Dog dog2=new Dog("阿黄","yellow");
System.out.println(dog1.equals(dog2));
}

}

结果是false,这里使用构造方法Dog(name,color)在堆内存里面new出了两匹黄狗,名称和颜色都一样,由于两匹黄狗在堆内存中放在不同的空间里,dog1与dog2分别指向不同的地址,所以dog1和dog2永远不会相等。

浅析java中的hashcode()方法与equals()方法

好的,现在重写equals方法,规定dog的颜色和名称相同时为同一条dog:

    @Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}

Dog p = (Dog)obj;
return this.name.equals(p.name)
&& this.color == p.color;
}

再次运行结果为true,不好意思,跑到隔壁老王家那条名叫阿黄的黄狗也是我的!
浅析java中的hashcode()方法与equals()方法

(三)HashMap中hashcode()与equals()的使用

下面结合hashmap来说明hashcode()方法与equals()方法,在讲之间我们来总结一下:

  1. 也就是说对于两个对象,如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;
  2. 如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;
  3. 如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;
  4. 如果两个对象的hashcode值相等,则equals方法得到的结果未知。

有了这些,我们来看hashmap的put方法与get方法,看一下hashcode方法与equals方法在里面发挥的作用。先欣赏一下hashmap的put方法的源码:

    public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

我觉得hashmap的实现由很多的亮点,有空再总结一下hashmap的内部实现,现在先讲一讲hash与equals。

  • put方法首先根据hashmap的hash()方法获得key的hash值;
int hash = hash(key);

备注:在hash()方法中,k首先调用了Object的hashCode()方法,然后有一个二次hash的过程,为什么有二次hash?在这里先保留,在hashmap浅析的时候总结。

    final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
  • 根据hash值计算key在数组中的索引位置
int i = indexFor(hash, table.length);  

备注:在indexFor中使用了h & (length-1)获取索引,而为何不是h%length,这里面同样有很多的精妙之处,深深地被大师的功底折服,后续同样会剖析!

    static int indexFor(int h, int length) {
return h & (length-1);
}
  • put key

通过索引我们可以快速定位key对应的Entry,如果索引处的 Entry 不为 null,则更新value值,否则将新的元素添加到HashMap中。

        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

在上面我们看到,同样出现了equals()方法,hashmap是数组与链表的结合,通过hash可以快速定位key的位置,而对于数组的每一项(链表)需要对链表进行遍历,判断新加入的对象是否与之相等,除了判断hashcode之外还需要调用equals方法(上面已经讲过,hashcode相等equals不一定为true)!

e.hash == hash && ((k = e.key) == key || key.equals(k))

从hashmap中hash,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。

修改equals()方法为什么要重写hashcode

首先上代码,还是上面重写了equals()方法的Dog类:

        Dog dog1=new Dog("阿黄","yellow");
Dog dog2=new Dog("阿黄","yellow");
HashMap<Dog, String> map=new HashMap<Dog, String>();
map.put(dog1, "01");
System.out.println(map.get(dog2));

我们重写了equals()方法,也就是说dog1与dog2是一样的,但是结果取到的确是null,为什么会这样呢?好的,上面讲Object的hashCode()方法的时候是不是感觉篇幅有点少,下面贴上一段被忽略的注释:

     * <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.

也就说两个对象相等那么他们的hashcode必须相等,否则会出现一些意想不到的情况出现。

Dog类虽然重写了equals方法,但是对HashMap而言,get dog2时首先计算dog2的hashcode,dog1与dog2的hashcode不一样,那么get dog2肯定有问题,最好的解决方法就是重写Dog的hashcode方法。

下面简单的实现hashcode()方法:

    @Override
public int hashCode() {
return this.color.hashCode()+this.name.hashCode();
}

这下就ok了!