Java集合之HashMap的实现原理

Java集合之HashMap的实现原理

HashMap是一个散列表,用来存储键值对的,在日常开发中用的很多,下面来学习一下它的内部实现原理

Java 8之前的实现原理:HashMap里面有个静态内部类:Entry,它是一个链表,主要变量有三个,key、value和next,分别代表HashMap的key、value和该链表下一个的地址。而HashMap里面有一个数组,数组元素就是Entry,所以HashMap保存数据的数据结构是一个链表数组。

当保存一个键值对时调用了put方法,源码如下:

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        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;
    }

首先判断key是不是空,如果为空,则直接调用key是空的那个方法保存value,如果不是,则根据key的hashCode计算得到一个hash值,然后根据这个hash值和目前数组的长度得到一个值i,这个值就是要把这个键值对保存到数组的下标。得到这个下标之后就取出这个链表,然后对这个链表按顺序进行for循环拿出每一个节点,得到节点之后首先比较这个节点的hash值刚才计算的hash值是否一样,然后判断两者的key是否相等,key的相等则通过==和equals两个比较,==或者equals有一个返回true则代表key是相等的。当这两者都相等,则代表已经有一个key存在了,则把value替换成新的value就行,同时把旧的key返回回去;如果其中之一不同,则保存这个键值对到别的地方,即调用addEntry方法,代码如下:

    /**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

保存键值对比较简单,先把该数组中i位置的Entry对象拿出来,然后创建一个链表对象,并且把刚才拿出来的Entry对象的地址赋值到刚创建的链表对象的next属性上,那么之前在该位置的对象则保存在第二个位置上了,最后判断当前数组的大小是否大于初始设置的一个数组大小,如果大于或者等于,则重新设置数组大小。

而get方法则是一个逆过程,代码如下:

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        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.equals(k)))
                return e.value;
        }
        return null;
    }

如果key是空,则从空key那里取value,如果不是,则根据key的hashCode计算一个hash值,然后根据这个hash值和目前数组的长度得到一个值,根据这个从数组中取出这个链表,然后遍历这个链表,根据每个节点的hash和key来跟计算得到的hash和当前传入的key比较,如果一直则就是这个节点,把value取出即可。如果没有找到,则返回null。

从以上的分析可以看出,当key的hash值一样的时候,则会出现一个冲突问题,而这个冲突则用链表解决了,如果当链表很长的时候,则会出现一个性能问题(链表增删快,查询慢,线程不安全,HashMap主要是查询多一点)。

以上是Java 8之前的一个实现原理,Java 8之后把HashMap的原理改了,里面有一部分是用红黑树实现的,目的是为了提高性能。所以它的实现原理结构是数组+链表+红黑树。

put方法代码如下:

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * 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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

Node和Entry是一样的,都是一个链表。首先判断这个数组是否为空,如果为空,则初始化这个数组,初始化之后则根据数组长度和hash找到链表,然后判断这个链表是否为空,为空则添加。如果不为空,则判断hash、key是否一样,一样则替换旧值(这里跟Java 8之前是一样的)。如果不为空,同时也没找到hash和key是一致的,则判断这个链表是否是红黑树,如果是,则根据红黑树的规则保存这个键值对,如果还不是,进行一个循环遍历这个链表,当链表最后一个节点为空时,则把键值对插入到这里,如果当这个链表的长度大于8的时候,则把这个链表转换成红黑树;如果key存在,则替换掉原来的value。完成一系列保存操作后,判断当前容量,如果超过了初始值,则扩容。其中注释了existing mapping for key是为LinkedHashMap保留的(这个看的不太明白,不知道是不是这样)。

现在来看一下它是怎么移除的,先看源码

/**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    /**
     * Removes and returns the entry associated with the specified key
     * in the HashMap.  Returns null if the HashMap contains no mapping
     * for this key.
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

从源码上可以知道根据key去移除会调用removeEntryForKey方法,先是计算出key的hash值,然后根据hash值与数组的长度计算出在数组中的位置,取出这个链表,然后遍历这个链表(为空则跳出循环),只要key符合要求,则说明找到了这个节点,同时把这个节点的下一个节点赋值到它上一个节点的next属性中,用来代替自己,从而删除掉了这个节点。如果一开始就为空,则把null返回回去。

HashMap还可以直接移除一个Entry,不需要根据key去移除,源代码如下:

/**
     * Special version of remove for EntrySet.
     */
    final Entry<K,V> removeMapping(Object o) {
        if (!(o instanceof Map.Entry))
            return null;

        Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
        Object key = entry.getKey();
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (e.hash == hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

从代码中可以看出,跟remove的代码基本一样,就是前面增加了一个判断,检查是否属于Map.Entry,这个就不多说了。

再来看包含的实现原理,代码如下:

/**
     * Returns <tt>true</tt> if this map maps one or more keys to the
     * specified value.
     *
     * @param value value whose presence in this map is to be tested
     * @return <tt>true</tt> if this map maps one or more keys to the
     *         specified value
     */
    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();

        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }

包含的原理其实就是遍历,然后查找对应的value,如果查找到了则返回true,没有查找到则返回false。

以上就是HashMap几个重要方法的实现原理,有哪里不对的欢迎指正

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值