Java基础-容器Map(上)

103 阅读11分钟

1 Map 架构图

image.png

2 HashMap

2.1 基本实现原理

HashMap 基于 Hash 算法实现的

  1. 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。如果key相同,则覆盖原始值;如果key不同(出现冲突),则将当前的key-value 放入链表中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
  4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

2.2 JDK1.7底层实现

2.2.1 基本属性

    /** 1 << 4,表示1,左移4位,变成10000,即16,以二进制形式运行,效率更高
     * 默认的hashMap数组长度,它的大小一定是2的幂。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

    /**
     * hashMap的最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;       

    /**
     * 负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * An empty table instance to share when the table is not inflated.
     */
    static final Entry<?,?>[] EMPTY_TABLE = {};

    /**
     * hashTable,根据需要调整大小。长度一定是2的幂。
     */
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    /**
     * hashMap中元素的个数
     */
    transient int size;

    /**
     * 调整hashMap后的值,即容量*负载因子。
     */
    int threshold;

    /**
     * 负载因子
     */
    final float loadFactor;

    /**
     * 记录hashMap元素被修改的次数
     */
    transient int modCount;

2.2.2 Put

public V put(K key, V value) {
   if (Entry<K,V>[] table == EMPTY_TABLE) {
     //初始化表 (初始化、扩容 合并为了一个方法)
     inflateTable(threshold);    
   }

   //对key为null做特殊处理 
   if (key == null)        
     return putForNullKey(value);
     
   //计算hash值  
   int hash = hash(key);      
   //根据hash值计算出index下标
   int i = indexFor(hash, table.length);   
   
   //遍历下标为i处的链表
   for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
     Object k;
     //如果key值相同,覆盖旧值,返回新值
     if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
       V oldValue = e.value;
       e.value = value;    //新值 覆盖 旧值
       e.recordAccess(this);   //do nothing
       return oldValue;    //返回旧值
     }
   }
   
   //修改次数+1,类似于一个version number
   modCount++;         
   addEntry(hash, key, value, i);
   return null;
 }

inflateTable 扩容或是初始化

private void inflateTable(int toSize) {
  // Find a power of 2 >= toSize  返回大于或等于最接近输入参数的2的整数次幂的数
  int capacity = roundUpToPowerOf2(toSize);
  threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
  table = new Entry[capacity];    //初始化表
  initHashSeedAsNeeded(capacity);
}

private static int roundUpToPowerOf2(int number) {
   int rounded = number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY
     : (rounded = Integer.highestOneBit(number)) != 0 ? 
       (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
       : 1;
   return rounded ;
 }

public static int highestOneBit(int i) {
  i |= (i >>  1);
  i |= (i >>  2);
  i |= (i >>  4);
  i |= (i >>  8);
  i |= (i >> 16);
  return i - (i >>> 1);
}

putForNullKey 处理Null

/**
* 注意:table是存储数据数组  transient Node<K,V>[] table;
* 直接去 遍历table[0] Entry链表 ,寻找e.key==null的Entry,
*		如果找到了e.key==null,就保存null值对应的原值oldValue,然后覆盖原值,并返回oldValue;
* 	如果在table[0]Entry链表中没有找到就调用addEntry方法添加一个key为null的Entry
*/
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;  
} 

indexFor/Hash


/**
* 在1.8之前中才有indexFor()方法 
* h&(length-1)的意思就是取模,即h%length。位运算(&)比取模运算(%)效率高得多,毕竟是直接操控二进制位,而取模运算还要转换成十进制。
*/
static int indexFor(int h, int length) {
  return h & (length-1);
}

/**
* 根据k计算出一个hash值.这里用到了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
*/
 final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

addEntry

/**
* 这个方法首先判断是否要扩容,当现在hashmap中的Entry数大于等于扩容临界值(capacity*load factor)并且index对应的地方没有Entry就扩容
*/
void addEntry(int hash, K key, V value, int bucketIndex) {  
  if ((size >= threshold) && (null != table[bucketIndex])) {  
    resize(2 * table.length);  
    hash = (null != key) ? hash(key) : 0;  
    bucketIndex = indexFor(hash, table.length);  
  }  
  createEntry(hash, key, value, bucketIndex);  
}  

/**
* hashmap每次扩容的大小为2倍原容量,默认容量为16,hashmap的capacity会一直是2的整数幂。
*/
void resize(int newCapacity) {  
       Entry[] oldTable = table;  
       int oldCapacity = oldTable.length;  
       if (oldCapacity == MAXIMUM_CAPACITY) {  
           threshold = Integer.MAX_VALUE;  
           return;  
       }  
  
       Entry[] newTable = new Entry[newCapacity];  
       transfer(newTable, initHashSeedAsNeeded(newCapacity));  
       table = newTable;  
       threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);  
} 

/**
* 空表扩容后的拷贝到新表的方法transfer
*/
void transfer(Entry[] newTable, boolean rehash) {  
       int newCapacity = newTable.length;  
       for (Entry<K,V> e : table) {  
           while(null != e) {  
               Entry<K,V> next = e.next;  
               if (rehash) {  
                   e.hash = null == e.key ? 0 : hash(e.key);  
               }  
               int i = indexFor(e.hash, newCapacity);  
               e.next = newTable[i];  
               newTable[i] = e;  
               e = next;  
           }  
       }  
}

/**
* 新的entry复制到table[bucketIndex],并next引用原来的table[bucketIndex],完成。
*/
void createEntry(int hash, K key, V value, int bucketIndex) {  
  Entry<K,V> e = table[bucketIndex];  
  table[bucketIndex] = new Entry<>(hash, key, value, e);  
  size++;  
}  

2.2.3 Get

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

/**
* Returns the entry associated with the specified key in the
* HashMap.  Returns null if the HashMap contains no mapping
* for the key.
* get方法也是需要先计算hash然后计算下标,再去寻找元素
*/
final Entry<K,V> getEntry(Object key) {
  if (size == 0) {
    return null;
  }

  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;
}

2.3 JDK 1.8 底层实现

2.3.1 基本属性

// 默认容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
 
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;    
 
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
 
// 链表节点转换红黑树节点的阈值, 9个节点转
static final int TREEIFY_THRESHOLD = 8; 
 
// 红黑树节点转换链表节点的阈值, 6个节点转
static final int UNTREEIFY_THRESHOLD = 6;   
 
// 转红黑树时, table的最小长度
static final int MIN_TREEIFY_CAPACITY = 64; 
 
// 链表节点, 继承自Entry
static class Node<K,V> implements Map.Entry<K,V> {  
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
 
    // ... ...
}
 
// 红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
   
    // ...
}

2.3.2 Hash

 final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);

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

对比JDK1.7中的HashMap我们发现在JDK1.8中,根据key进行hash运算中的左移,异或等运算明显变少了。原因是因为在1.7中hash方法的目的是为了让hash值更加的散列,也就是让每个位桶上面的链表长度更短防止链表过长从而影响运算效率。而在1.8中因为链表改为红黑树,不仅解决了链表长度问题,也优化了查询效率。而且减少hash方法中的运算过程也节约我们硬件cpu的损耗。

2.3.3 Put

流程图

image.png

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  //判断数组是否为空,为空则初始化一个数组(注意resize既包括初始化数组也包括数组的扩容)
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  //通过 hash(key)&(table.length-1) 得到key的下标位置
  //如果此位置是否为空,则创建node对象
  if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
  //如果此位置不为空
  else {
    Node<K,V> e; K k;
    //判断put进来的key的下标位置是否是e的位置,如果是将p赋值给e(put进来的key位置上存在node对象,那么就修改node对象对应的value值)
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p;
    //如果key位置上存在红黑树。
    else if (p instanceof TreeNode)
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    //如果key位置上存在链表
    else {
      //遍历链表
      for (int binCount = 0; ; ++binCount) {
        if ((e = p.next) == null) {
          //如果put进来的key与链表不存在冲突,则插入到链表的尾部
          p.next = newNode(hash, key, value, null);
          //判断链表长度是否大于8,如果是将链表改为红黑树
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
          break;
        }
        //判断链表的key是否与put进来的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;
  //判断是否需要扩容,与1.7里面一样
  if (++size > threshold)
    resize();
  afterNodeInsertion(evict);//这个方法没有用到,不用考虑
  return null;
}

2.3.4 Get

流程图

image.png

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
 
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 1.对table进行校验:table不为空 && table长度大于0 && 
    // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 6.找不到符合的返回空
    return null;
}

2.3.5 Resize

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 1.老表的容量不为0,即老表不为空
    if (oldCap > 0) {
        // 1.1 判断老表的容量是否超过最大容量值:如果超过则将阈值设置为Integer.MAX_VALUE,并直接返回老表,
        // 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯的将阈值扩容到最大
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 1.2 将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16, 则将新阈值设置为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 2.如果老表的容量为0, 老表的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为老表的阈值
    else if (oldThr > 0)
        newCap = oldThr;
    else {
        // 3.老表的容量为0, 老表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 4.如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 5.将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表。
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 6.如果老表不为空,则需遍历所有节点,将节点赋值给新表
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {  // 将索引值为j的老表头节点赋值给e
                oldTab[j] = null; // 将老表的节点设置为空, 以便垃圾收集器回收空间
                // 7.如果e.next为空, 则代表老表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 9.如果是普通的链表节点,则进行普通的重hash分布
                    Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
                    Node<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 9.1 如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null) // 如果loTail为空, 代表该节点为第一个节点
                                loHead = e; // 则将loHead赋值为第一个节点
                            else
                                loTail.next = e;    // 否则将节点添加在loTail后面
                            loTail = e; // 并将loTail赋值为新增的节点
                        }
                        // 9.2 如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
                        else {
                            if (hiTail == null) // 如果hiTail为空, 代表该节点为第一个节点
                                hiHead = e; // 则将hiHead赋值为第一个节点
                            else
                                hiTail.next = e;    // 否则将节点添加在hiTail后面
                            hiTail = e; // 并将hiTail赋值为新增的节点
                        }
                    } while ((e = next) != null);
                    // 10.如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点
                    // 的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 11.如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后
                    // 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 12.返回新表
    return newTab;
}

红黑树的重 hash 分布

大概过程就是将TreeNode链表,拆成两个index位置和index+oldTable.length位置的TreeNode,我们将index位置的TreeNode称为为loTail,index+oldTable.length位置的TreeNode称为hiTail。那么我们分成三种情况考虑

  • 如果拆分后的TreeNode的长度小于6,那么我们将该TreeNode转成Node链表。
  • 如果拆分后的TreeNode的长度大于6,那么则拆分后的将TreeNode的头节点存到对应位置。
  • 如果拆分后的两个TreeNode其中一个长度为0,那么我们直接转移原有TreeNode。

JDK1.7与JDK1.8不同点:

  • jdk1.7扩容条件是hashmap中存储的元素数大于阈值且发生hash碰撞。而jdk1.8中的扩容条件就是hashmap的存储的元素数大于阈值
  • 在1.8中HashMap的resize方法既包含了初始化的功能又包含了扩容的功能
  • 在1.8扩容遍历链表的时候,会将链表拆成两个小链表,然后将这两个小链表分别插入新数组的index位置和index+oldTable.length位置。也就是说在进行链表转移的时候,1.7中的HashMap会遍历一个转移一个,而1.8中的HashMap会全部遍历后,在转移。