hashMap复习

本文详细介绍了HashMap的工作原理,包括哈希算法、Node数据结构、存储结构以及put、扩容、get和remove等核心操作。HashMap通过哈希函数将键映射到数组位置,解决冲突时使用链表或红黑树。当冲突过多导致链表过长时,HashMap会进行扩容,以提高查询效率。此外,还分析了扩容的resize方法和putVal方法的细节,确保高效的数据存储和检索。
摘要由CSDN通过智能技术生成

hash算法

原理

基本原理是把任意长度的输入,通过hash算法变成固定长度的输出,这个映射的规则就是hash算法

特点

  1. 从hash值无法反导出原始的数据
  2. hash算法非常高效,长文本也能快速计算出哈希值
  3. hash算法冲突的概率小

Node数据结构

static class Node<K,V> implements Map.Entry<K,V> {
    //key的值计算得到hash值经过扰动函数得到
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    //省略...
}

hashMap存储结构

Node数据结构

put方法

  • 进行put操作如图,数组的长度是2的倍数,通过路由算法进行寻址(找出node应该存放的位置)的时候

  • n & (m - 1) 就相当于 n % m取模运算

  • key经过计算得到的hash值,经过路由寻址得到在数组的位置,如果这个位置已经有node节点,就发生hash冲突,连接在已有节点的后面

  • 如果一直hash冲突,链表的长度就会很长,如果get刚好是这个位置,查询的效率最低可能从o(1)退化到o(n)

put

扩容原理

为了解决数据多查询效率低的问题(很多数组的位置都转成红黑树了),进行扩容后,每个数组位置的数据(链表或者红黑树)就减少了,查询的效率就提高了

源码分析

数据结构

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 * 哈希表
 */
transient Node<K,V>[] table;

/**
 * Holds cached entrySet(). Note that AbstractMap fields are used
 * for keySet() and values().
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * The number of key-value mappings contained in this map.
 * 元素个数
 */
transient int size;

/**
 * The number of times this HashMap has been structurally modified
 * Structural modifications are those that change the number of mappings in
 * the HashMap or otherwise modify its internal structure (e.g.,
 * rehash).  This field is used to make iterators on Collection-views of
 * the HashMap fail-fast.  (See ConcurrentModificationException).
 * 结构修改次数
 */
transient int modCount;

/**
 * The next size value at which to resize (capacity * load factor).
 * 转为红黑树阈值
 * @serial
 */
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;

/**
 * The load factor for the hash table.
 * 负载因子,决定树化阈值
 * @serial
 */
final float loadFactor;

构造函数

public HashMap(int initialCapacity, float loadFactor)
/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
    //进行一些校验
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //限制最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //检验负载因子
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //长度只能是2的倍数,这个函数将initialCapacity修正为2的倍数
    this.threshold = tableSizeFor(initialCapacity);
}

修正函数tableSizeFor

/**
 * Returns a power of two size for the given target capacity.
 * 将n修正最接近的为2的倍数
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
扰动函数

key的哈希值经过扰动函数后得到在数组中的位置,作用是让哈希值的高16位也参与路由运算(因为让高位参与运算之后再&length-1更加均匀散列,减少哈希冲突)

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

如果key为null,哈希值是0,放在数组中索引为0的位置

例子

设原数据是0010 1100 1001 0101 1011 0001 1111 0011

无符号右移0000 0000 0000 0000 0010 1100 1001 0101

做异或运算0010 1100 1001 0101 1001 1101 0110 0110

异或的结果使得低16位带有高16位的特征

putVal
  • 如果未初始化,则进行初始化扩
  • 如果已经初始化
    • 如果对应的桶位为null,插入新节点数据
    • 如果找到hash和key值都相同的节点,则进行数据替换;否则尾插法插入到链表末尾,插入后判断新table的大小是否超过阔偶然那个阈值,如果超过则进行树化

1.8之后是尾插法,头插法会线程不安全,链表成环

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //tab 引用当前的hashMap的散列表
    //p 当前散列表的元素
    //n 散列表的长度
    //i 路由寻址的结果
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //延迟初始化逻辑,散列表new出来未占用内存,第一次put时初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //n在上面if中赋值了,用的很妙
    if ((p = tab[i = (n - 1) & hash]) == null)
        //如果这个位置还未插入数据,new一个节点封装k-v
        tab[i] = newNode(hash, key, value, null);
    else {
        //哈希冲突
        //e:node 临时元素 k:临时的key
        Node<K,V> e; K k;
        //p在上面的if中赋值了
        //当前桶位和插入的key完全一致,需要进行替换操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            //key不相等且已经变成红黑树了
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //是链表且链表的头元素与插入的key不一致
            for (int binCount = 0; ; ++binCount) {
                //迭代到最后一个元素且没有相同的key,插入到链表的最末端
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //判断是否树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //找到了一个key相同的元素,需要进行替换
                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;
}

resize扩容方法

如果散列表中的元素非常多,get效率退化,最差可能由o(1)退化到o(n),需要进行扩容操作减少每个桶位中的元素,提高查找的效率

过程:

  • 扩容新的table容量,计算新的扩容阈值
  • 删除旧的数组的值,方便回收内存
  • 链表的情况是将旧的链表拆为高低位链表,放入新数组的高低位
/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    //oldTab引用扩容之前的哈希表
    Node<K,V>[] oldTab = table;
    //oldCap表示扩容之前的数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr扩容之前的阈值
    int oldThr = threshold;
    //newCap扩容后的table数组的大小
    //newThr扩容之后的阈值
    int newCap, newThr = 0;
    //散列表已经初始化过了
    if (oldCap > 0) {
        //如果已经达到了最大的容量则不再进行扩容,并且修改扩容阈值为Integer.MaxValue
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //正常扩容,扩容一倍,条件是扩容之后的容量小于MAXIMUM_CAPACITY并且扩容之前的容量>=16
        //这个条件没有满足的话newThr
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //oldCap == 0,新的容量扩容为oldThr
    //public HashMap(int initialCapacity, float loadFactor)的时候初始化了阈值
    //public HashMap(int initialCapacity)初始化了阈值
    //public HashMap(Map<? extends K, ? extends V> m)初始化了阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        //可能导致newThr=0
        newCap = oldThr;
    //oldCap == 0,新的容量扩容为默认的阈值
    //new HashMap()
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //计算出新的阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建新的数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //本次扩容之前table不为空
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            //当前节点
            Node<K,V> e;
            //链表的头节点或者树的根节点
            if ((e = oldTab[j]) != null) {
                //方便JVM GC时回收内存
                oldTab[j] = null;
                //当前的桶位只有一个元素
                if (e.next == null)
                    //算出在新数组中的位置并赋值
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    //树化了
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    //链表的情况如何处理
                    //拆成两个链表,低位数组和高位数组
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //设旧的容量是16
                    do {
                        next = e.next;
                        //在数组15位置的hash值情况如下
                        //...0 1111
                        //...1 1111
                        //oldCap为1 0000
                        if ((e.hash & oldCap) == 0) {
                            //放到低位数组中去
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            //放到高位数组中去
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        //链表末尾置空
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

链表的情况如下,比如旧数组15的位置hash最低4位置肯定是1111,倒数第五位可能是0或者1,这个位置的元素可能放置到新数组的15或者31(因为新数组长度为32,进行寻址运算&length-1)

image-20210222224346542

get方法

核心是getNode方法

/**
 * 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) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法

/**
 * Implements Map.get and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    //tab引用table
    //first:桶位中的头元素
    //e:临时元素
    //n:table的长度
    //k:临时的key
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //该桶位中有元素的情况进入
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            //头元素就是目标元素
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                //树的情况
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                //链表的情况
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

remove方法

/**
 * 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) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

removeNode方法

/**
 * Implements Map.remove and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to match if matchValue, else ignored 如果为true则需要value也相等的时候才删除
 * @param matchValue if true only remove if value is equal
 * @param movable if false do not move other nodes while removing
 * @return the node, or null if none
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //tab:引用table
    //p:当前node元素
    //n:表示散列表的数组长度
    //index:寻址的结果
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //对应的桶位有数据
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //头元素就是要找的元素
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                //树化的情况
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                //链表的情况
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                //树的情况
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                //删除的是头元素
                tab[index] = node.next;
            else
                //对应链表的情况
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

replace方法

@Override
public boolean replace(K key, V oldValue, V newValue) {
    Node<K,V> e; V v;
    if ((e = getNode(hash(key), key)) != null &&
        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}
@Override
public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值