HashMap 的原理 以及结构

本文深入探讨了Java中HashMap的工作原理,包括其内部结构、Entry对象、put和get方法的实现细节,以及如何解决哈希冲突等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


数据存储 中一般都是用 数组 或 链接来实现。

单纯的数组存储查询的时间复杂度的确很低,但空间复杂度会要求很高。

单纯的链接,通过链接的方式,空间复杂度非常低了,但时间复杂度变的非常高。

两者结合的哈希表,完美的结合了彼此的优点,的确是数据存储的最佳选择。这篇文章就以Java中常用的HashMap为例,通过底层的代码,深入的了解它的原理以及存储的结构方式。


用过HashMap都知道,它最重要的就两个方法get和put。


我准备通过简单介绍HashMap的结构,Entry对象,在接着put和get方法,在这些都全部了解的情况下,在研究下哈希冲突,一步一步的了解HashMap。



1. HashMap的结构

HashMap的结构,我借鉴了别人的图,做个简单介绍。一个Entry(用于存放key,value对,后面介绍)和指下下一个节点的指针组成一个整体,然后通过指针相互链接,实现链接的结构。由这个结构可以看出来,Hash表空间复杂度的确不高,采取的是链表的形式。那么怎么解决时间复杂度呢,请往下看。




谢谢作者:http://blog.youkuaiyun.com/vking_wang/article/details/14166593



解决查询的时间复杂度,解决方案HashMap中这一段源代码:

    transient Entry<K,V>[] table;


HashMap 就是一头披着羊皮的狼,哦不对,是披着链表的数组。这就是HashMap的解决方案,采取链表和数组的长处,完美的融合。




2. Entry 对象

  上面其实已经涉及到Entry对象了。先上源代码:

   static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

       .....

      }

这部分代码是HashMap的内部类Entry的源代码。 key 和 value 值很好理解,next 就是指向下一个对象的指针, hash 存的是对应的hash值。

由此可知,HashMap主类并没有存key和value,都是它的内部类来负责存储的。这也验证了上面的图,Entry + 指向下一个Entry对象的指针(后面单独说)构成HashMap。



3.Put 方法

   继续通过源代码的方式学习:

    public V put(K key, V value) {
        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;
    }

  

   看似很长,我们分步骤来看一下:

   1) if (key == null)  return putForNullKey(value);  很好理解,当key是null时,返回将值存入到一个NullKey的地方。那么这是什么地方呢,继续看源代码

    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

   原来如此,是将值存入到HashMap中table[0]的位置,也就是这一句addEntry(0, null, value, 0); 

   如果这个位置已经有值了,怎么处理呢,也就是for (Entry<K,V> e = table[0]; e != null; e = e.next) 

     V oldValue = e.value;
       e.value = value;

   直接替换了值。 

  

    2)   int hash = hash(key); 没什么好说的,取了key的hash值,取值干什么呢,go on

    3)  int i = indexFor(hash, table.length);  获取table中的indexFor位置,去看电影,找到座位了,找到座位干嘛呢

    4)  addEntry(hash, key, value, i); 坐下来了。 当然这个是理想状态,万一你去电影厅一看,发现自己的位置上有人怎么办呢

    5)for (Entry<K,V> e = table[i]; e != null; e = e.next),Entry<K,V> e = table[i],说明e就是你的位置,但是e != null;别人已经占了你的位置了,

        V oldValue = e.value;
                e.value = value;

          对的,你替换掉座位上的那个人即可。


  由此可知:HashMap在put的时候,值都是存在Entry中的,而Entry存在HashMap的一个数组中。且key和value对是排斥的,即有冲突的时候会替换掉过去存的value的。这和接下来的hash冲突有所不同。


4.Get方法

  还是继续上面学习的方法论,直接上源代码:

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }


  继续解读源代码:

 1)  if (key == null)  return getForNullKey();  这涉及到getForNullKey()方法,还是直接上源代码

         private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

   一看恍然大悟,当key=null的时候,则从HashMap中Entry数组的第一个对象开始遍历,当e.key 也是null时,返回value。如果找不到还是返回null。 由此可见HashMap的key可以是null值。

  2) Entry<K,V> entry = getEntry(key); 这里涉及到 getEntry(key);  继续上源代码:

    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

     有几行,上面已经说过的就不继续说了。  for (Entry<K,V> e = table[indexFor(hash, table.length)]; 由这一行可以知道,根据key的hash值确定了table中数组的位置indexFor(hash, table.length)。

     如同你去电影院看电影了,好友通过你的短信码(key的哈希值),找到了你的位置indexFor(hash, table.length)。从这里开始寻找你,结果可想而知。最终返回e 对象。

   3) return null == entry ? null : entry.getValue();  很简单了,直接获取上一步返回的对象e的value值。



5. 哈希冲突

   了解了上面的内容后,就比较方便的了解哈希冲突了。Entry对象的源代码中有一个属性next,细心的人会发现很奇怪。Entry只是用来存Key和Value的,next的是什么Entry呢?

   static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

       .....

      }

HashMap 中有一行代码 int i = indexFor(hash, table.length);  最终的Entry对象会存到HashMap对象中的table数组中的第i的位置


Object 对象中有类方法:hashCode

public int hashCode() {
return super.hashCode();
}

如果我将两个对象的hashCode 都重写,然后固定成一个值。那么hashcode就会存在冲突,导致HashMap的table中,对象严重分不均匀。例如数字中存了100个对象,但hashcode都相同,如果在重写equals也相同(e.hash == hash && ((k = e.key) == key || key.equals(k)) 源代码在这里),那么table的某个位置需要存100个对象(实际上是通过next),这无疑会严重影响性能,这就是hash冲突。


怎么解决这个问题呢,方案有多重,这里我就说一个最简单的解决方案,升级JDK到1.8,让这些hash冲突的对象以二叉树的形式保存,解决HashMap偶尔的性能问题。




 综上所述:

HashMap中有一个内部类Entry,和一个叫table的数组。前者存着key和value值以及指向下一个Entry的对象的指针;后者就是一个Entry的数组。前者保证了链接的存储结构,满足降低空间复杂度;后者通过数组解决了时间的复杂度。


哈希冲突最佳解决方案是升级JDK到1.8.







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值