java-集合-HashMap

  • HashMap 数据结构图 (基于jdk1.8.0_92)

  1. 数组:黄色部分
  2. 链表:绿色部分
  3. 红黑树 :粉色部分
  • HashMap 存储数据流程图、原理

  1. 利用hash()方法计算key的哈希值
  2. 判断数组是否为空或者长度为0,是则调用resize()方法进行扩容
  3. 根据哈希值算出在数组中的位置,并判断当前位置是否有数据,若没有则直接插入新数据
  4. 若有数据(发生哈希碰撞),利用equals()方法判断key值是否相等,若相等直接覆盖原value值
  5. 若不相等,判断当前位置是不是红黑树节点,如果是则进行红黑树插值法,写入数据
  6. 如果不是红黑树节点,则遍历当前位置链表
  7. 如果与某一节点key值相等,则覆盖原value值,否则继续遍历链表
  8. 如果走到链表尾部,插入数据,如果当前位置处于链表第8个位置以上则调用红黑树插值法插入数据
  9. modCount 修改次数+1:它的作用是当集合被迭代的时候根据这个变量判断集合是否被修改,被修改则抛出异常
  10. 判断数组容量是否大于临界值,如果大于则进行扩容
// 对外接口
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) {
    
    // 定义一个数组,一个链表,n永远存放数组长度,i用于存放key的hash计算后的值,即key在数组中的索引        
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 判断table是否为空或数组长度为0,如果为空则通过resize()实例化一个数组并让tab作为其引用,并且让n等于实例化tab后的长度        
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 根据key经过hash()方法得到的hash值与数组最大索引做与运算得到当前key所在的索引值,并且将当前索引上的Node赋予给p并判断是否该Node是否存在
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);//若tab[i]不存在,则直接将key-value插入该位置上。
    
        // 该位置存在数据的情况  
    else {
        Node<K,V> e; K k; //重新定义一个Node,和一个k
        
	    // 该位置上数据Key计算后的hash等于要存放的Key计算后的hash并且该位置上的Key等于要存放的Key     
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;	//true,将该位置的Node赋予给e
	else if (p instanceof TreeNode)  //判断当前桶类型是否是TreeNode
	    // ture,进行红黑树插值法,写入数据
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 	
        else {	
	    // false, 遍历当前位置链表
            for (int binCount = 0; ; ++binCount) {
                // 查找当前位置链表上的表尾,表尾的next节点必然为null,找到表尾将数据赋给下一个节点
                if ((e = p.next) == null) {
                     p.next = newNode(hash, key, value, null);	//是,直接将数据写到下个节点
                    // 如果此时已经到第八个了,还没找个表尾,那么从第八个开始就要进行红黑树操作
		    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);	//红黑树插值具体操作
                        break;
                }
                // 如果当前位置的key与要存放的key的相同,直接跳出,不做任何操作   
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 将下一个给到p进行逐个查找节点为空的Node
		p = e;
            }
        }
        // 如果e不为空,即找到了一个去存储Key-value的Node 
	if (e != null) { // existing mapping for key
            V oldValue = e.value;    
	    if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 当最后一次调整之后Size大于了临界值,需要调整数组的容量
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
  • HashMap 取数据原理
  1. 计算key的哈希值
  2. 根据哈希值找到第一个节点位置,如果有数据利用equals()方法判断key值是否相等,如果相等直接返回数据
  3. 如果不相等,判断下个节点是否存在
  4. 如果存在,判断是否是红黑树节点,是则调用红黑树取值法获取数据
  5. 如果不是红黑树节点,则遍历链表
  6. 找到key值相等的节点返回数据,否则返回null
// 对外接口
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;
	// 保证Map中的数组不为空,并且存储的有值,并且查找的key对应的索引位置上有值
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // always check first node 第一次就找到了对应的值
	if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
	// 判断下一个节点是否存在
	if ((e = first.next) != null) {
            //判断节点是否是TreeNode,是则通过TreeNode的get方法获取值
            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);
        }
    }
    // 未找到,返回null
    return null;
 }

 

  • HashMap 扩容机制
  1. 先计算出新数组的容量和扩容临界值
  2. 创建新数组
  3. 将旧数组数据复制到新数组
final Node<K,V>[] resize() {
    // 旧数组
    Node<K,V>[] oldTab = table;
    // 旧数组容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧数组临界值
    int oldThr = threshold;
    // 定义新数组容量和新数组临界值
    int newCap, newThr = 0;
    // 如果旧数组容量大于零(非第一次put值)
    if (oldCap > 0) {
        // 如果旧数组容量超过最大限制容量(2^30),将系统临界值设置Integer的最大值 Integer.MAX_VALUE
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;	
            return oldTab;
        }
		// 如果旧数组容量小于最大限制容量的一半且大于等于初始容量16,对新数组临界值赋值
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
	// 如果旧数组容量为0,但旧数组临界值不为0,对新数组容量赋值
    else if (oldThr > 0) 
        newCap = oldThr;
    // 如果旧数组容量和旧数组临界值都为0;对新数组容量、新数组临界值赋值(系统默认值)
	else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
	// 如果新数组临界值为0,对其进行赋值操作
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
	// 修改系统临界值
    threshold = newThr;
    //创建新数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 遍历旧数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 如果旧数组bucket有值
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 当前bucket不存在链表,将当前值写到新数组
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 当前bucket存在链表,判断链表节点是不是红黑树节点,是则调用红黑树方法复制数据
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 当前bucket存在链表,链表节点不是红黑树节点(节点位置没有超过8)
                else {
                    // lo就是扩容后仍然在原地的元素链表
					// hi就是扩容后下标为  原位置+原容量  的元素链表,从而不需要重新计算hash。
					Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 循环链表直到链表末再无节点
					do {
                        next = e.next;
						//e.hash&oldCap == 0 判断元素位置是否还在原位置
                        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);
					// 循环链表结束,通过判断loTail是否为空来拷贝整个链表到扩容后table
                    if (loTail != null) {
                       loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
  • HashMap 容量为什么要设置为2的N次方
  1. 保证了散列的均匀
  2. 减小哈希碰撞的几率
  • 如何减少哈希碰撞
  1. 如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
  2. 使用不可变类(如String,Interger这样的wrapper类作为键是非常好的选择)将会减少碰撞的发生,提高效率
  3. 不可变性是必要的
  4. 因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象
  5. 不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。
  • 如何遍历 HashMap 
        Map map = new HashMap();
		map.put("1","111");
		map.put("2","222");
		map.put(3,333);
		map.put(4,444);

// 1、在for-each循环中遍历keys或values:如果只需要map中的键或者值,你可以通过keySet或values来实现遍历,而不是用entrySet
		for(Object obj : map.keySet()){
			System.out.println("Key = " + obj);
		}
		for(Object obj : map.values()){
			System.out.println("Value = " + obj);
		}

// 2、使用Iterator遍历
		Iterator entries  = map.entrySet().iterator();
		while(entries .hasNext()){
			Map.Entry entry = (Map.Entry) entries.next();
			System.out.println("Key = " + entry.getKey() + ", Value = " +             
                                entry.getValue());
		}
  • HashMap 与 HashSet   
  1. HashMap 实现了 Map接口
  2. HashSet 实现了 Set接口
  3. 虽然 HashMap 和 HashSet 实现的接口规范不同,但它们底层的Hash 存储机制完全一样,甚至 HashSet 本身就采用 HashMap 来实现的。
  4. HashSet的实现其实是封装了一个HashMap对象来存储所有的集合元素。所有放入HashSet中的集合元素实际上由HashMap的key来保存,而HashMap的value则存储了一个PRESENT,它是一个静态的Object对象。
    注意:由于HashSet的add()方法添加集合元素实际上转变为调用HashMap的put()方法来添加key-value对,当新放入HashMap的Entry中key与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true)时,新添加的Entry的value将覆盖原来Entry的value,但key不会有任何改变。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素(底层由HashMap的key保存)不会覆盖已有的集合元素。

 

  • HashMap 与 HashTable
  1. HashMap:非线程安全
  2. Hashtable:线程安全,是同步容器,所有的读写等操作都进行了锁(synchronized)保护
  3. 在HashMap中,key可以为null(只能有一个),value也可以为null,所有 null = get("x");不能说明x不存在,判断key是否存在应该使用containsKey()方法来判断
  4. 在HashTable中,key和value都不允许为null
  5. HashMap的迭代器(Iterator)是fail-fast迭代器,如果有其它线程对HashMap进行的添加/删除元素,将会抛出ConcurrentModificationException
  6. Hashtable的迭代器(enumerator)不是fail-fast的

注:java中的集合类都提供了返回Iterator的方法,就是迭代器,它和Enumeration的主要区别其实就是Iterator可以删除元素,但是Enumration却不能。Enumeration已经不是主流,Iterator是它的下一代替代品

  • HashMap 与 TreeMap
  1. HashMap通过hashcode对其内容进行快速查找
  2. TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap
  3. 在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好
  • 重新调整HashMap大小可能存在什么问题
  1. 当重新调整HashMap大小的时候,存在条件竞争
  2. 因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小
  3. 在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
  • 简单集合关系图

参考:

  1. HashMap Jdk8的实现原理    https://blog.csdn.net/goosson/article/details/81029729
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值