HashMap如何添加元素详解

Map接口结构

map接口是一个双边队列,拥有key,value两个属性,其中key在存储的集合中不允许重复,value可以重复。

Map HashMap LinkedHashMap Hashtable 实现map接口 实现map接口 继承HashMap 实现map接口 Map HashMap LinkedHashMap Hashtable

HashMap特点

  • 存储结构在jdk1.7当中是数组加链表的结构,在jdk1.8当中改为了数组加链表加红黑树的结构。
  • HashMap在多线程的环境下是不安全的,没有进行加锁措施,所以执行效率快。如果我么需要有一个线程安全的HashMap,可以使用Collections.synchronizedMap(Map<K,V> m)方法获得线程安全的HashMap,也可以使用ConcurrentHashMap类创建线程安全的map
  • 存储的元素在jdk1.7当中是Entry作为存储的节点,在jdk1.8当中用Node作为存储的节点,Node实现了Map.Entry接口。在map中其实是将keyvalue节点组合起来成为一个Node节点作为存储。
// jdk1.8Node节点
static class Node<K,V> implements Map.Entry<K,V> {
        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;
        }

		// 下面省略代码
  }
  • HashMapkey不允许重复,value可以允许重复,并且keyvalue都允许是null值,因为他们是另外存储的。
  • 当我们使用自定义类作为HashMap的key时我们需要重写object类的,hashCode()equals()方法。
  • HashMap中链表部分在jdk1.7中新的节点总是在链表的头部,旧的节点在新节点的next域当中,而在jdk1.8中是新的节点总是在链表的尾部,他们的指向都是由旧节点指向新的节点,由此我们可以记成七上八下

HashMap当中常见的名词

  • DEFAULT_INITIAL_CAPACITY 默认数组容量大小,16
  • MAXIMUM_CAPACITY 最大数组容量大小,2^30
  • DEFAULT_LOAD_FACTOR 默认的负载因子,0.75
  • TREEIFY_THRESHOLD 链表改成红黑树存储的最小长度
  • MIN_TREEIFY_CAPACITY 链表改成红黑树存储,map数组最小容量

HashMap存储元素过程源码解析(jdk1.8)


	// 存储元素的数组,加上transient关键字代表不可以被序列化
	transient Node<K,V>[] table;
	
	// 这个方法是HashMap对外公开的添加元素方法
	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;
        
        // 在jdk1.7当中,创建一个HashMap实例是直接分配一个长度为16的数组,
        // 在jdk1.8当中,创建HashMap时是分配了一个长度为0的数组,然后调用put方法时,如果数组长
        // 度为0时,则调用数组扩容方法,扩容数组到长度为16。当长度不为0时,扩容操作是扩大到原来
        // 长度的两倍
        if ((tab = table) == null || (n = tab.length) == 0)
			
			// 调用扩容方法,并利用n变量,记录数组的长度
            n = (tab = resize()).length;
        
        // 通过计算哈希散列,得出该key应该放在数组的哪一个位置上,用变量p存储该位置上的元素
        // 判断该位置上是否已经存在元素,如果不存在元素则直接将该元素放入数组中,存放
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

		// 如果数组的该位置已经存在了元素
        else {
            Node<K,V> e; K k;
            // 比较需要加入的元素,于原来的元素hash值进行比较,如果hash值和equals都为true那么
            // 判定两个元素为相同元素,用变量e来记录原来的元素,方便下面进行替换
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                
            // 如果上述条件不满足,但是是一个TreeNode类型,则当作树节点插入
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            // 当hash值不同,或者equals为false,也不是TreeNode时将新元素插入数组对应位置的
            // 链表中,遍历对应数组位置的链表
                for (int binCount = 0; ; ++binCount) {

					// 用变量e存储链表的下一个节点
                    if ((e = p.next) == null) {
                    
                    	// 如果该链表只有一个元素,则直接将旧节点的next域指向新的节点
                        p.next = newNode(hash, key, value, null);

						// 当单条链表上元素数量大于最大数量时则按照红黑树存储8,在进行红黑树时
						// 又会进行判断数组容量是否到达红黑树最小容量64,两个条件同时满足,则该
						// 条链表改造成红黑树存储
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 比较链表的下一个节点的hash值和equals
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
			
			// 如果e不等于null则证明,在链表中存在相同的元素,则进行替换
            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;
    }

	// 将链表改造成红黑树
	final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
		
		// 判断是否满足条件,数组大小达到64,没有达到则做扩容操作,达到则改成红黑树
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

HashMap常见的问题

为什么在使用自定义类型作为key需要我们重写hashCode()equals()方法

因为map的存值的时候是先计算hash值,然后再判断equals,通过这两个值是否都为true来判断该元素是否再map中已经存在。如果不重写这两个方法,可能会存在我们认为相同,但是他们的hash或者equals不同的元素也能存进map中,从而达不到key唯一的效果。

HashMap中为什么要存在DEFAULT_LOAD_FACTOR负载因子这个概念

因为我们map的存储数据结构是数组加链表加红黑树结构,如果没有负载因子,map是不可能满的,所以加上一个负载因子的概念来判断数组是否扩容,减少链表的负担。

发布了23 篇原创文章 · 获赞 22 · 访问量 6844

猜你喜欢

转载自blog.csdn.net/justLym/article/details/104894887