HashMap 的 key,是如何保证唯一的呢?

hashMap 的存储结构

HashMap 的存储结构,是通过用数组+链表实现。当 HashMap 添加一个元素 add(k,v)时,k、v 被封装为一个 entity 对象,jdk 8 中叫 node 节点对象。该对象会被存放到数组中,key 通过 hash 计算出所在数组唯一位置,如果该位置已被占用(hash 冲突),则用链表方式继续存储。

当数组容量使用到指定阈值时,会触发数组扩容;
当链表长度达到一定时,jdk 8 中,这个长度是 8 ,超过时,链表结构转为红黑树结构,以提高查询效率,当移除元素后长度小于 6 时,便又转回链接结构。

HashMap 添加元素时,判断是 key 是否已存在,主要通过 hash 值的对比,通过 equals 对比 key 内容的对比完成。
相关源码:

  
/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //tab数组为空,或者长度为0,通过resize()初始化tab数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //hash所在的数组位置为空,则new Node节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //判断hash值相同 && (通过==对比 || 通过equals对比),对相同key重新赋值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

如何保证 key 是唯一的

在 map 中,key 的比较,可以通过上述源码得知:

(
p.hash == hash 
&&
((k = p.key) == key || (key != null && key.equals(k))
)

也就是通过 hashCode 值、==、eqauls 三个方法对比。

hashCode 方法对比

Object 类定义的 hashCode 方法:
hashCode 方法,是 java 中 Object 对象定义的 native 方法,native 方法是 java 应用调用底层 c++程序的入口,参考 Java中如何打印对象内存地址 一文可知,其是根据对象内存地址计算出的 int 类型的值。

/**
* @return a hash code value for this object.  
* @see java.lang.Object#equals(java.lang.Object)  
* @see java.lang.System#identityHashCode  
*/  
public native int hashCode();

并且,在不同对象中,对 hashCode 的复写,也是不同的。

String 类中,复写了 hashCode 方法,通过一定的算法,保证计算返回的 hash 值尽量不冲突,既然是尽量不冲突,那么这种计算就还是有一定冲突概率的。了解 String 的 hashCode 算法,推荐阅读:一文说透String的hashCode。以下是 jdk 8 中,hashCode 方法的部分注释:

Returns a hash code for this string. The hash code for a String object is computed as s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

HashMap 中,对 hashCode 的复写,就简单的多:

public final int hashCode() {  
    return Objects.hashCode(key) ^ Objects.hashCode(value);  
}

/**  
* Objects 的hashCode方法,实际上丢回给了Object方法。
* Returns the hash code of a non-{@code null} argument and 0 for  
* a {@code null} argument.  
*  
* @param o an object  
* @return the hash code of a non-{@code null} argument and 0 for  
* a {@code null} argument  
* @see Object#hashCode  
*/  
public static int hashCode(Object o) {  
    return o != null ? o.hashCode() : 0;  
}

直接调用了 k/v 所属的各自类型的 hashCode 方法,将皮球踢回去了。

双等号== 对比

在 8 大基础类型中,== 对比的是类型的值,而在对象中,对比的是对象引用的内存地址。
例如:

public static void main(String[] args) {  
    Integer a = 1;  
    Integer b = 1;  
    System.out.println("a:" + a);  
    System.out.println("b:" + b);  
    System.out.println("ab:" + (a == b));  
    Integer c = new Integer(1);  
    System.out.println("c:" + c);  
    System.out.println("ac:" + (a == c));  
}
//输出的结果是
a:1
b:1
ab:true
c:1
ac:false

延伸:为什么 ab 是相同的呢?gpt 给的答案:

Integer a = 1 Integer b = 1,为什么 a、b 的对象地址是一样的

在 Java 中,对于整型数据的包装类 Integer,Java 提供了一个 Integer 对象缓存池,这个缓存池中缓存了一个固定范围的整数对象 (-128 到 127)。当我们使用自动装箱或者调用 valueOf() 方法创建一个整型包装类对象时,会先检查该对象是否在缓存池中存在,如果存在,则直接返回缓存池中的对象,而不是创建一个新的对象。

所以,当我们写下 Integer a = 1; Integer b = 1; 这两行代码时,由于 1 在缓存池中已经存在,所以 a 和 b 引用的是同一个对象,即它们的对象地址是一样的。

但是需要注意的是,如果超出了缓存池的范围,比如 Integer a = 128; Integer b = 128;,则 a 和 b 引用的将不再是同一个对象,而是分别创建了两个新的对象。这是因为超出了缓存池的范围,Java 会重新创建一个对象来表示该值。

equals 方法对比

java 对象的中的老大哥 Object 中,equals 方法实际上是调用了 == 对比。源码如下:

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

而在 String 类中,复写了 equals:


/**
 * Compares this string to the specified object.  The result is {@code
 * true} if and only if the argument is not {@code null} and is a {@code
 * String} object that represents the same sequence of characters as this
 * object.
 *
 * @param  anObject
 *         The object to compare this {@code String} against
 *
 * @return  {@code true} if the given object represents a {@code String}
 *          equivalent to this string, {@code false} otherwise
 *
 * @see  #compareTo(String)
 * @see  #equalsIgnoreCase(String)
 */
public boolean equals(Object anObject) {
    //先用==判断,如果对象引用的地址相同,那结果一定相同
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        //String类型,并且长度相同,遍历字符串的每个字符是否相同
        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;
}

既然 hashCode 可以通过计算每个属性的 hash 值,从而在属性维度对比两个对象是否相同,为什么还需要再通过 equals 方法对比呢?
Gpt 给了答案:

虽然 hashCode() 方法可以根据对象的属性计算出一个哈希码,但是哈希码并不是绝对唯一的,不同的对象可能会产生相同的哈希码(发生哈希冲突)。因此,仅仅通过比较哈希码是不足以确定两个对象是否相等的。

equals() 方法的作用是在逻辑上比较两个对象是否相等。它是 Object 类中的一个方法,但是在许多类中都会被重写来实现自定义的相等性判断逻辑。

在重写 equals() 方法时,通常会比较对象的属性值是否相等,以确定对象是否相等。这样可以更加精确地判断对象的相等性,不仅仅局限于哈希码。

当我们使用集合类(如 HashSet 或 HashMap)时,它们内部使用了 equals() 和 hashCode() 方法来判断是否已经包含某个对象或者查找某个对象的位置。如果只重写了 hashCode() 方法而没有重写 equals() 方法,可能会导致集合类无法正确地判断对象是否相等,从而引发一些意想不到的问题。

因此,为了保证对象的相等性判断的正确性,一般需要同时重写 equals() 和 hashCode() 方法。equals() 方法用于逻辑上的相等性判断,而 hashCode() 方法则用于高效地定位对象。

Map 中的 key 怎么找到在数组的位置

hashMap 的 key 通过 hash 计算后,如何找到在数组的位置呢?

未完待续...

延伸:
数组的扩容机制
红黑树数据结构简介

资料参考:
https://blog.csdn.net/sanmi8276/article/details/113839351

https://worktile.com/kb/ask/26327.html

https://blog.csdn.net/bbj12345678/article/details/120521490

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容