关于hashcode & equals

文章目录

    • 1. 两个问题
    • 2. 相关知识梳理
        • hash & hashcode
        • Ojbect.hashcode() & System.identityHashCode(obj)
        • Object.equals(obj)
        • HashMap
        • String.hashcode() & String.equals(obj)
        • 关于 ==
    • 3. 反正法
    • 4. 结论

1. 两个问题

  1. 为何自定义类的hashcode和equals方法要同时覆写?
    学习java基础知识时,一直牢记hashcode和equals方法要同时覆写,要说清楚其背后的原因,涉及到的知识点还挺多,下文重点讲述。
  2. hashcode是内存地址吗?
    印象中好像hashcode默认是对象的内存地址,实际上是这样吗,本文也一并澄清下。

2. 相关知识梳理

hash & hashcode

hash函数是一套算法的统称。实际中的Hash函数是指把一个大范围映射到一个小范围。一般的说,hash函数可以简单的划分为如下几类:
  1. 加法Hash;
  2. 位运算Hash;
  3. 乘法Hash;
  4. 除法Hash;
  5. 查表Hash;
  6. 混合Hash;
显示接触到的,以除法Hash取余居多。

hashCode是 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(obj) 会返回的值。它是一个对象的身份标识。官方叫法为:标识哈希码( identity hash code),作为hash函数的入参。

Ojbect.hashcode() & System.identityHashCode(obj)

先看下jdk的源码:

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

// System.java
public static native int identityHashCode(Object x);


从源码中我们可以看到,这两种方法都是本地方法native,其实最终的实现类似,我们以Object.hashcode()为例,看下jvm是如何实现的。

//openjdk\hotspot\src\share\vm\prims\jvm.cpp

// java.lang.Object ///


JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_IHashCode");
  // as implemented in the classic virtual machine; return 0 if object is NULL
  return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
JVM_END

我们继续看ObjectSynchronizer::FastHashCode的实现:

// hotspot\src\share\vm\runtime\synchronizer.cpp
intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
  ... //忽略部分代码

  // Inflate the monitor to set hash code
  monitor = ObjectSynchronizer::inflate(Self, obj);
  // Load displaced header and check it has hash code
  mark = monitor->header();
  assert (mark->is_neutral(), "invariant") ;
  hash = mark->hash();
  if (hash == 0) {
    hash = get_next_hash(Self, obj);
    temp = mark->copy_set_hash(hash); // merge hash code into header
    assert (temp->is_neutral(), "invariant") ;
    test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
    if (test != mark) {
      // The only update to the header in the monitor (outside GC)
      // is install the hash code. If someone add new usage of
      // displaced header, please update this code
      hash = test->hash();
      assert (test->is_neutral(), "invariant") ;
      assert (hash != 0, "Trivial unexpected object/monitor header usage.");
    }
  }
  // We finally get the hash
  return hash;
}

从以上代码中可以看出,如何生成hashcode,jvm提供了基于某个hashCode 变量值的六种方法。怎么生成最终值取决于hashCode这个变量值。

0 - 使用Park-Miller伪随机数生成器(跟地址无关)
1 - 使用地址与一个随机数做异或(地址是输入因素的一部分)
2 - 总是返回常量1作为所有对象的identity hash code(跟地址无关)
3 - 使用全局的递增数列(跟地址无关)
4 - 使用对象地址的“当前”地址来作为它的identity hash code(就是当前地址)
5 - 使用线程局部状态来实现Marsaglia’s xor-shift随机数生成(跟地址无关)

我们从openjdk\hotspot\src\share\vm\runtime\globals.hpp 中可以看到jdk8的默认值为:

//openjdk\hotspot\src\share\vm\runtime\globals.hpp
product(intx, hashCode, 5,                                                \
          "(Unstable) select hashCode generation algorithm")   

可以知道,hashcode的默认值跟地址无关。

Object.equals(obj)

打开源码看下该方法的实现

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

可以看到,Object的实现是,基于地址的比较。

HashMap

关于HashMap,我们重点看下其get(key)方法的逻辑

// java.util.HashMap

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    
    //Node数组不为空,数组长度大于0,数组对应下标的Node不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        //也是通过 hash & (length - 1) 来替代 hash % length 的
        (first = tab[(n - 1) & hash]) != null) {
        
        //先和第一个结点比,hash值相等且key不为空,key的第一个结点的key的对象地址和值均相等
        //则返回第一个结点
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //如果key和第一个结点不匹配,则看.next是否为空,不为null则继续,为空则返回null
        if ((e = first.next) != null) {
            //如果此时是红黑树的结构,则进行处理getTreeNode()方法搜索key
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //是链表结构的话就一个一个遍历,直到找到key对应的结点,
            //或者e的下一个结点为null退出循环
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

总结HashMap的key的判断逻辑,当 hash(key1.hashcode) == hash(key2.hashcode) 并且key1.equals(key2) 时,判定key是相等的。

String.hashcode() & String.equals(obj)

java.lang.String类覆写了Object的hashcode和equals方法,我们简单看下其源码

// java.lang.String
	// 依赖字符串的char值
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
	// 比较的是字符是否完全一致。
    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()和hashCode()比较的问题,下面的讨论都是针对引用数据类型展开的。

对于引用对象而言,比较两个引用变量的引用的是否是同一个对象,即比较的是两个引用地址是不是一样

3. 反正法

我们以HashMap比较key值是否相同的场景为例,反证下hashcode和equal是否必须同时覆写,我们举例说明如下:
我们以Person(id,name等字段,当id相同时判定为相同)为例说明下为何hashcode和equal是必须同时覆写。

假如只覆写了equals方法,hashcode使用Object.hashCode(), Person(1)和Person(1)会在计算hashcode时,进行hash函数计算时,判定为不同的HashMap key,跟业务预期不符合。

只覆写hashcode方法时。Person(1)和Person(1)会有相同的hash函数结算结果,但是当比较equals时,因为默认比较的是地址,而二者的地址不同,会判定为不同的HashMap key,跟业务预期不符。

4. 结论

  1. equals和hashcode必须同时覆写。
  2. 默认hashcode的实现跟地址无关。

你可能感兴趣的:(java,jvm,java,开发语言)