Java集合(2)——Map


在这里插入图片描述

HashMap

哈希表

是元素和地址之间的映射表。

根据设定的hash函数 H(key) 和处理冲突的方法将一组关键字【元素】映射到一个有限的连续的地址集【区间】上。

映像过程称为散列,所得的存储地址称为散列地址。

哈希函数

将元素的关键字作为哈希函数的参数,结果可以得出一个哈希地址。

注意:该哈希地址一定是在给定的区间内。因为哈希函数中,有一个操作是 所得的初始结果 mod (区间长度)=最终的结果。

处理冲突的方法

两个不同元素的关键字,通过哈希函数,得出了相同的地址映射。

注意:一定是两个不同的关键字,才叫做地址冲突。

所以在哈希表中,所有的元素,关键字都不相同。如果插入元素与表内元素关键字出现相同,则被认定为是替换表内元素,而不是插入新元素。

以下对不同元素key,产生相同的地址的叫做同义词。否则为非同义词。

  • 开放定址法:Address= ( H(key) + d ) MOD m

    其中Address为散列地址、H()为hash函数 、key 元素关键字、d为增量序列、m为哈希表长度。

    如果发生地址冲突,将依据该地址向左或向右寻找,直到有其他地址为空【即该地址无元素占据时】,将插入元素放入。hash表由线性表实现时,使用这种方法

    初始d=0 只有d=0出现冲突时,d开始变化,继续计算hash地址,直到无冲突的出现。d的变化方式有以下3种:

    • 线性探测再散列:向右边探测下去 d= 1 2 3 4 5 6 ···
    • 二次探测再散列:右边一次,左边一次 d= 1 -1^2 2 -2^2 3 -3^2 ···
    • 伪随机探测再散列:随机给一个数进行探测 d=Random number

    缺点:在处理同义词冲突过程中,又添加了非同义词的冲突【位置被占据】。发生二次聚集

  • 再哈希法:Address=RH(key) RH是多个不同的哈希函数,当元素关键字产生地址冲突时,就使用另一个hash函数计算地址,直到不出现冲突。

    缺点:不易产生聚集,但增加了计算时间

  • 链地址法:将所有关键字为同义词的记录存储在同一线性链表中。

    • 设立一个Node数组,每个数组节点都是一个链表head节点。
    • 将hash地址为 i 的元素,插入到 list[i] 为 head节点的链表中。
    • 保证了所有同义词都在一个链表中,非同义词之间不会互相影响。
    • 插入链表的过程中,进行必要的大小判断,可以做到链表元素有序。

节点大致结构

class Node
{
	T key; //元素的关键字
	T val; //元素值
	Node next; //指向下一个节点
}

JDK1.7 HashMap

此版本的HashMap的结构为 桶数组+链表
在这里插入图片描述

结构分析

该数组作为哈希表的桶数组

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry结构

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
        int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算

       //参数分别为 h哈希值 K键 V值 Entry下一个结点
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

构造方法

(1)真实 capacity 为大于 initialCapacity的第一个 2^n,比如initialCapacity=17,则capacity=32
(2)计算阈值
(3)创建哈希桶数组

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

        // Find a power of 2 >= initialCapacity
        //capacity 为大于 initialCapacity的第一个 2^n
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        //计算阈值
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //创建数组
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

hash算法

    final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
		
		// 0 ^ hashCode = hashCode
        h ^= k.hashCode();

        //一种算法,进行4次位移,得到相对比较分散的链表
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

根据hash获取桶的索引

注意:桶的长度始终为偶数,则length-1始终为奇数。 这个时候 h&(length-1) 等价于 h mod (length-1)

  static int indexFor(int h, int length) {
        return h & (length-1);
    }

put操作

(1)【key判断】key是否为null? 如果key=null,则hash为0,桶index=0,进行(3.2)。否则(2)
(2)【获得桶index】获取新元素key的hash值,根据hash获得桶index
(3.1)【遍历index桶】遍历 index桶链表 ,看看有没有 key重复,如果重复,则进行value替换,然后return。否则,出现两种情况 【桶链表=null】 or【没有发生key冲突】 先更新下集合操作次数modCount,进行(4)
(3.2)【遍历0桶】遍历 index桶链表 ,看看有没有 key重复,如果重复,则进行value替换,然后return。否则,出现两种情况 【桶链表=null】 or【没有发生key冲突】 先更新下集合操作次数modCount,进行(4)
(4)【插入结点之前的扩容判断】 判断 [当前大小集合size >= 阈值threshold的大小] 以及 [当前桶链表存在] 则扩容
(5)【插入元素】使用头插法

	//
    public V put(K key, V value) {
    	//HashMap允许key为空
        if (key == null)
            return putForNullKey(value);
        //获取新元素的哈希值
        int hash = hash(key);
        //根据哈希值获得桶的索引
        int i = indexFor(hash, table.length);
        //开始循环 准备插入
        //从index为i的桶的头节点开始进行
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //新元素 与 链表中原有元素的键完全相同 进行value的替换
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
		//否则 插入元素
		//modCount 表示对该集合有效修改的次数 insert delete
        modCount++;
        //添加链表结点
        addEntry(hash, key, value, i);
        return null;
    }
    //put操作涉及到的其他方法
    //单独对 key=null的元素 进行插入操作
    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;
    }
    //putForNullKey涉及的方法
        void addEntry(int hash, K key, V value, int bucketIndex) {
        //判断下 [当前大小size 和 阈值threshold] 以及 [当前桶是否存在] 
        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);
    }
    //addEntry涉及的方法
        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++;
    }

扩容操作

(1)容量的边界判断
(2)根据新容量创建新的哈希表
(3)进行transfer扩容 大循环每个桶,小循环每个桶的链表,依次取出每个链表结点根据hash获得新表的index,使用头插法即可。

//newCapacity = 2 * oldCapacity 
    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];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        //标记 扩容时是否需要重新hash
        boolean rehash = oldAltHashing ^ useAltHashing;
        //进行扩容
        transfer(newTable, rehash);
        //将扩容后的新数组 赋给 原来的引用
        table = newTable;
        //计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //for循环遍历old哈希桶
        for (Entry<K,V> e : table) {
            while(null != e) {
                //记录当前结点的下一个
                Entry<K,V> next = e.next;
                //true 表示每个元素都需要重新hash
                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 = e.next
                e = next;
            }
        }
    }

get操作

(1)根据key得出hash值,如果key=null 则hash=0
(2)根据hash得出对应的桶
(3)从桶头节点开始遍历,找到 key一致且hash一致的结点,返回该节点

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

    final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key); //key不为空,取得key的hash值
        //通过indexFor取得该hash值在数组table中的偏移量得到Entry类的单向链表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //通过循环在单向链表中寻找相同hash值,相同key值确定链表中的具体实例。
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

JDK1.8 HashMap

结构分析

结构
1.8的HashMap是 数组+链表+红黑树

在这里插入图片描述
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。

其中

  • 数组 通过 Node []table 实现
  • 链表 是由多个Node连接
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;
        }

其中Entry是Map中的一个内置接口,定义了map元素的存、取等方法
在这里插入图片描述

HashMap就是使用哈希表的方式来存储元素

字段解释

除此之外,还有几个HashMap中的必要字段
在这里插入图片描述

  • DEFAULT_INITIAL_CAPACITY:哈希表数组的默认大小 1<<4=16,会在第一次插入元素时,通过resize()扩容创建一个index在【0~15】的table哈希表数组 数组的元素为Node。 注意:执行对象无参数构造方法时,并不会创建table数组
  • MAXIMUM_CAPACITY:哈希表数组的最大容量,为2^30。
  • DEFAULT_LOAD_FACTOR:默认负载因子,0.75
  • size:map当前包含的键值对数量
  • modCount:用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。
  • threshold:map所能容纳的最大键值对个数,threshold = length * Load factor
  • loadFactor:负载因子,可以在创建HashMap对象时指定,默认为0.75

构造函数

根据初始化容量得出大于初始化容量的最小 2^n ,当前阈值直接等于2^n,并不会初始化哈希桶。

初始化创建桶会在第一次扩容时创建,容量为当前的阈值,阈值=容量x负载因子,此后新容量即为旧容量的2倍。

    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);
    }
    //cap为初始化容量 
    //实际就是找出了
    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;
    }

hash算法

hash算法在插入、删除和查找键值对的过程中非常重要,定位到哈希桶数组的位置都是很关键的第一步。

HashMap的数据结构是数组链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。

HashMap中【JDK1.7、JDK1.8】,Hash算法本质上就是三步:取key的hashCode值、补充高位运算、取模运算。

JDK1.8 哈希算法

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

	根据hash可以确定该元素放在 哈希表数组的 index=(n - 1) & hash

问题1:对于hash & mask【mask=table.length-1】 起初有些不解,如果要保证每个元素都能得到 【0 ~table.length-1】区间的地址,不应该是对hash进行 MOD mask。因为如果mask为偶数,【即哈希表数组长度为奇数】,进行 hash & mask操作的话,岂不是 mask之前的hash得到的地址为 0,mask之后的hash为mask了么?

举例:
在这里插入图片描述
分析:通过阅读源码可知,哈希表数组的初始长度为16是偶数,这样保证了mask为奇数,执行 hash & mask不会出现问题。 且哈希表进行扩容后的数组长度仍然是偶数。

当length总是2的n次方时,hash & mask运算等价于对mask取模,也就是hash%mask,但是&比%具有更高的效率。

问题2:为什么不直接用 hashCode() 而是用它的高 16 位进行异或计算新 hash 值?

int 类型占 32 位,可以表示 2^32 种数(范围:-2^31 到 2^31-1),而哈希表长度一般不大,在 HashMap 中哈希表的初始化长度是 16(HashMap 中的 DEFAULT_INITIAL_CAPACITY),如果直接用 hashCode 来寻址,那么相当于只有低 4 位有效,其他高位不会有影响。这样假如几个 hashCode 分别是 2^10、 2^20、 2^30,那么寻址结果 index 就会一样而发生冲突,所以哈希表就不均匀分布了。

为了减少这种冲突,HashMap 中让 hashCode 的高位也参与了寻址计算(进行扰动),即把 hashCode 高 16 位与 hashCode 进行异或算出 hash,然后根据 hash 来做寻址。
在这里插入图片描述

插入

在这里插入图片描述

	//插入元素操作
	//hash() 哈希函数可以得出 该元素所对应的 初始地址 【即没有与mask进行& 操作的地址】 
	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
	//hash算法
	static final int hash(Object key) {
        int h;
		//如果key=null 则返回为0 【即一个map中,只能存在一个 key=null的键值对】
		// '>>>'表示无符号数右移 16位 高位补0
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
	
	//hash为插入元素的哈希值 key元素的键 value元素的值  
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
		//tab为全局变量table桶数组的引用 
		//p为根据hash&mask得出的桶节点 即table[i]
		//n为table数组长度
		//i为 hash&mask 		
        Node<K,V>[] tab; Node<K,V> p; int n, i;
		//***如果全局table数组==null 则进行创建***
		//并且将新创建的数组长度 赋值给n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
		//***如果根据 hash&mask 得出的桶节点=null 则直接进行插入***
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
		//***否则 此时出现hash冲突 开启判断过程***
		//1、遍历 以p为head节点的链表中 如果有与插入元素key相同的节点 则覆盖原有value
		//2、如果没有 则进行插入【此时 先进行插入 再根据原有链表的节点个数决定是否进行 链表——>红黑树操作 】
        else {
			//e是引用 插入节点是在e的引用上进行的 
			//k表示 p节点的键
            Node<K,V> e; K k;
			//***判断1、链表的head节点 和插入节点的 key一致***
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
				//e指向这个头节点
                e = p;
			//***否则 判断2、该链表p节点的类型是否是TreeNode,如果是,则处理冲突的结构已经从链表变为红黑树***
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
			//***否则 判断3、 处理冲突的结构还是一个链表 判断下该链表里 是否有与插入元素key相同的元素【从表头节点p开始遍历】***
            else {
					//binCount 边遍历边计数 当遍历到末尾时 也就知道链表一共有多少个节点了
                for (int binCount = 0; ; ++binCount) {
					//此时已经判断到链表尾部
					//链表里并没有与插入元素key相等的元素 准备进行插入操作
					//e指向当前节点的下一个节点
                    if ((e = p.next) == null) {
						//生成新节点 并插入
                        p.next = newNode(hash, key, value, null);
						//判断下此时 该链表的节点数是否>=8 如果满足 则链表转红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					//此时还没到链表尾部 
					//继续判断 下一个节点key与插入元素key的关系
					//如果等于则 break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
					//如果不等于则继续判断 p=p.next
                    p = e;
                }
            }
			//e不为null 说明链表中存在与插入元素key相同的元素
			//e指向的是被替换元素 即【break循环时 当前节点的下一个元素】
			//执行Value覆盖			
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                //执行完更新操作直接返回
                return oldValue;
            }
        }
		//该HashMap对象 内部结构发生变化的次数 +1
        ++modCount;
		//实际键值对数量+1 与 最大键值对容量 进行比较
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

扩容

JDK1.8做了哪些优化。

经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

看下图可以明白这句话的意思,n为table的长度

图(a)表示扩容前的key1和key2两种key确定索引位置的示例
图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

在这里插入图片描述
通过 hash & oldLength 即可得出 hash的高位是0 or 1。

  • 若为0,则该元素在新哈希表的位置与旧哈希表一致
  • 若为1,该元素在新哈希表的位置 为 旧哈希表位置+旧哈希表数组的长度【容量】
final Node<K,V>[] resize() {
		//oldTab为指向当前全局变量table的引用
        Node<K,V>[] oldTab = table;
		//oldCap 记录当前哈希表的容量 即数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
		//oldThr 记录当前哈希表最多可以存储的键值对
        int oldThr = threshold;
		//设置新的
        int newCap, newThr = 0;
		//当前哈希表容量>0  进行扩容前的判断
        if (oldCap > 0) {
			//如果当前容量已经超过了最大容量 那么不会发生扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
				//此时将threshold更新为最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
			//如果当前的哈希表容量*2<最大值 且 当前的哈希表容量>16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
				//对 threshold进行2倍扩容 
				//因为负载因子不变 所以表length*2 给threshold*2 即可
                newThr = oldThr << 1; // double threshold
        }
		
        else if (oldThr > 0) // initial capacity was placed in threshold
			//HashMap是带参构造 参数为initialCapacity
			//哈希表为null  
			//哈希表初始容量 设置为 哈希表键值对阈值
            newCap = oldThr;
		
        else {               // zero initial threshold signifies using defaults
			//HashMap是无参构造
			//说明此时哈希表为空 进行容量 和 最大存储键值对 的设置
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
		
        if (newThr == 0) {
			//进入此if有两种可能
            // 第一种:进入此“if (oldCap > 0)”中且不满足该if中的两个if
            // 第二种:进入这个“else if (oldThr > 0)”
			
			//分析:进入此if证明该map在创建时用的带参构造,如果是第一种情况就说明是进行扩容且oldCap(旧容量)小于16,
			//如果是第二种说明是第一次put
            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指向新数组
        table = newTab;
		//如果“oldTab != null”说明是扩容,否则直接返回newTab
        if (oldTab != null) {
			//对旧哈希表数组的所有元素 重新进行散列
            for (int j = 0; j < oldCap; ++j) {
				//指向——>旧哈希表数组节点
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
					//将当前旧节点对象 设置为null 以便于垃圾回收
                    oldTab[j] = null;
					//则e指向的以oldTab[j]为head节点的链表 只有一个元素
                    if (e.next == null)
						//在新哈希表 对元素 进行散列
                        newTab[e.hash & (newCap - 1)] = e;
					//如果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;//此对象接收会放在“j + oldCap”(当前位置索引+原容量的值)
                        Node<K,V> next;
                        do {
                            next = e.next;
							//oldCap 为偶数,正好 oldCap & e.hash,可以得出e.hash的【高一位 即第n位 2^n为旧的哈希表数组长度】
							//若e.hash高一位=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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

获取元素

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

1.8与1.7比较

  • 创建哈希桶的时机不同
    1.7 在调用构造函数的时候,就已经确定好了容量和阈值,并且创建好了哈希桶数组。
    1.8 调用构造函数时候,根据初始化容量 得出了大于的2^n 赋值给当前的阈值。在put操作时,当哈希桶数组为空才会去创建。此时再将阈值赋值给容量,将阈值=容量x负载因子。

  • 插入结点的方式不同
    1.7采用头插法,插入速度快。但是在多线程扩容情况下,如果一个桶上的所有节点均在新表的一个桶,会出现循环链表问题。

头插的原理:
1、先记录当前待插入结点的下一个结点
2、将待插入结点.next 置为新桶的头节点
3、更新桶的头节点为待插入节点。
在这里插入图片描述

旧表
在这里插入图片描述
(1)
线程1、2进行扩容操作
线程1先执行,记录了 next=b , e=a ,随后CPU切换调度到线程2,线程2完成了扩容后

如下图,其中的next 和 e是线程1的引用
在这里插入图片描述
此时线程1继续执行,next=b ,e=a,开始继续执行,结果如下
在这里插入图片描述
此时e=b,next=a
在这里插入图片描述
此时e=a,next=null

线程1 执行 a.next = bucket[7]头节点,又指向b 在这里插入图片描述
此时出现了循环链表,存在线程安全问题

1.8采用尾插法,插入速度慢,但是解决了这种问题。

  • 数组扩容后的索引计算方式不同
    JDK1.7的时候是直接用hash & (newLength-1),判断桶索引
    JDK1.8 hash & oldLength 计算出最高位是0 还是1

哈希表容量为2的n次幂原因

(1)根据hash计算索引的时候,直接是 & 操作即, hash&(length-1) ,& 比 mod更高效。
&操作必须保证 length-1为奇数,二进制全为1,保证了结果的正确性。如果length-1为偶数,则二进制最高位=1,其余为0,会出现散列全部向两端。
(2)1.8进行元素的扩容操作时,计算元素在新表的索引 是通过 hash & newLength-1 来判断hash最高位的,若为1,则该元素在新表的位置=旧表的位置+旧表长度。若为0,新老位置不变。

HashMap为什么线程不安全

put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。

为什么不直接使用红黑树

红黑树的插入、删除操作涉及到自旋平衡等一系列操作,比较麻烦。

在少数据量的情况下,还是使用链表直接插入、删除比较简单。

大数据量情况下,查询成本比较高,这时考虑使用红黑树。

6 和 8

从 treeifyBin 函数中可以看到,虽然链表个数>8触发红黑树转换,但是红黑树实际转化之前先会判断下当前的表长度,如果table.length < 64,则会进行resize扩容。

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        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);
        }
    }

Map的遍历

(1)map中的结点类型为Map.Entry ,全部都存储在EntrySet<Map.Entry<K, V>>中
这里直接遍历EntrySet的每个结点

        for(Map.Entry<Integer,Integer> entry : map.entrySet())
        {
            System.out.println(entry.getKey()+"-"+entry.getValue());
        }

(2)map的键key全部在 keySet()中,value全部在values()中

		for(Integer key: map.keySet()) {
            System.out.print(key+" ");
        }
        System.out.println();
        for(Integer value:map.values()) {
            System.out.print(value+" ");
        }

(3)迭代器遍历,访问EntrySet的迭代器

		Iterator it = map.entrySet().iterator();
        while(it.hasNext())
        {
            Map.Entry entry = (Map.Entry)it.next();
            System.out.println(entry.getKey()+"-"+entry.getValue());
        }

(4)根据keySet()取出key,get(key)遍历

TreeMap

作为一个Key-Value,底层使用红黑树作为实现

TreeMap节点结构

		K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;

get()时间复杂度

  • 在理想状态下,未发生任何hash碰撞,数组中的每一个链表都只有一个节点,那么get方法可以通过hash直接定位到目标元素在数组中的位置,时间复杂度为O(1)。
  • 若发生hash碰撞,则可能需要进行遍历寻找,n个元素的情况下,链表时间复杂度为O(n)、红黑树为O(logn)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值