zoukankan      html  css  js  c++  java
  • 集合源码分析之 HashMap

    一 知识准备

       HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

    二  HashMap的数据结构:

      JDK 7.0及以前

         在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

       从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

     

      JDK 8.0(本文主要介绍JDK 8.0的实现)

    JDK7.0及以前,HashMap的结构都是基于一个数组以及多个链表的实现,处理Hash冲突的方法就是将对应节点以链表的形式存储。

    简单的实现是以HashMap性能牺牲为代价的,如果说有成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那将不可避免的花费0(N)的查找时间,严重影响性能。JDK 8.0开始使用数组+链表+红黑树的组合来实现HashMap

    (http://www.cnblogs.com/leesf456/p/5242233.html)


    三 字段

    //HashMap的散列表
    transient Node<K,V>[] table;
    
    //存放entry的set
    transient Set<Map.Entry<K,V>> entrySet;
    
    //记录HashMap中存储了多少个键值对<KEY-VALUE>
    transient int size;
    
    //mod是modify的缩写,hashMap的结构发生结构变化时会记录一次。
    transient int modCount;
    
    //默认初始化table的大小
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
    //table的最大大小
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //这是一个比例参数,当table中已经被占用的元素数与table总长度的比例不小于这个参//数的时候,就会发生table的扩容,每次扩容都以2倍大小进行扩容,注意resize()函数
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    //当size大于这个数时,就进行一次扩容,即调用resize()函数
    int threshold;
    
    //当节点冲突数达到8时,就会对hash表进行调整,如果table的长度小于64,那么会进//行table扩容,如果不小于64,那么会将因冲突形成的单链表调整为红黑树。
    static final int TREEIFY_THRESHOLD = 8;
    
    //在删除冲突节点之后,同hash的节点数低于这个值时,将红黑树重新恢复为单链表。
     static final int UNTREEIFY_THRESHOLD = 6;
    
    //注意到TREEIFY_THRESHOLD解释,不小于64时仅对table进行扩容,这个64就是//指这个值。
    static final int MIN_TREEIFY_CAPACITY = 64;

    四 构造函数

      /**
         * @param  initialCapacity 初始容量
         * @param  loadFactor      负载因子*/
        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; this.threshold = tableSizeFor(initialCapacity); } /** * @param initialCapacity 初始容量,默认负载因子0.75*/ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 默认初始容量16,默认负载因子 0.75 */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } /** * @param 使用一个map来初始化新的HashMap*/ public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }

    五 Get 和 Put 方法

    put方法

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

    putVal方法

     /**
         * Implements Map.put and related methods
         * @param hash hash for key
         * @param key 键
         * @param value 值
         * @param onlyIfAbsent 如果是true,不改变已存在的值,字面意思,只当map中该对象没有才存入,默认false
         * @param evict 如果 false, 散列表处于创建模式,默认true
         * @return 返回旧值或者null或者 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;
          //先判断,table大小,如果table为null,或者没分配空间,就resize一次
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            //如果首节点为null,就创建一个首节点。注意到tab[i = (n - 1) & hash],(n-1)&hash才是真正的hash值,也就是存储在table的位置(index)。
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);//创建一个新的节点
            else {//冲突处理
                Node<K,V> e; K k;
            //p这时候是指向table[i]的那个Node,这时候先判断下table[i]这个节点是不是和我们待插入节点有相同的hash、key值。如果是就e = p
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
          //这里说明第一个节点的hash、key值与我们带插入Node的hash、key值不吻合,那么要从这个节点之后的链表节点或者树节点中查找。由于之前提到过,1.8的HashMap存储碰撞节点时
          ,有可能是用红黑树存储,那么先判断首节点p的类型,如果是TreeNode类型(Node的子类),那么就说明碰撞节点已经用红黑树存储,那么使用树的插入方法,如果新插入了树节点,
          那么e会等于null,用于后面的判断与处理
    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) {//e引用下一个节点,如果是null,表示没有找到同hash、key的节点 p.next = newNode(hash, key, value, null);//创建一个新的节点,放到冲突链表的最后               // 注意到如果这时候冲突节点个数达到8个,那么就会treeifyBin(tab, hash)函数,看是否需要改变冲突节点的存储结构,
                   这个treeifyBin首先回去判断当前hash表的长度,如果不足64的话,实际上就只进行resize,扩容table,如果已经达到64,那么才会将冲突项存储结构改为红黑树。
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }             //如果找到了同hash、key的节点,那么直接退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;//调整下p节点 } } if (e != null) { // existing mapping for key          //注意到这时候要判断是不是要修改已插入节点的value值,两个条件任意满足即修改 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e);//这个是空函数,可以由用户根据需要覆盖 return oldValue; } } ++modCount;//当插入了新节点,才会运行到这儿,由于插入了新节点,整个HashMap的结构调整次数+1 if (++size > threshold)//HashMap中节点数+1,如果大于threshold,那么要进行一次扩容 resize(); afterNodeInsertion(evict);//这个是空函数,可以由用户根据需要覆盖 return null; }

    get方法,比较简单,就是在在table上根据key.hash查找,如果hash值相同有多个,则根据key.equals()在链表或者红黑树上遍历比较,得到最终值

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

    六 总结

    JDK 8.0HashMap中没当冲突节点个数大于8时,就先尝试table扩容,当table数达到64后,冲突节点数为8时,则进行链表向树结构转换,这样对于冲突节点的访问复杂度就会大幅度降低,当然这是建立在插入时冲突处理算法复杂度提升为代价的。

  • 相关阅读:
    科学计算和可视化
    利用Python制作GIF图片
    模拟体育竞技分析
    词云(傲慢与偏见)
    词频统计+词云(傲慢与偏见)
    汉诺塔问题
    Python 的turtle笔记
    有进度条的圆周率计算
    Python 第二周练习
    warning: deprecated conversion from string constant to ‘char*’
  • 原文地址:https://www.cnblogs.com/zabulon/p/5898334.html
Copyright © 2011-2022 走看看