【HashMap-1.7】-从源码解析-通俗易懂【扩容】【线程不安全】【put&get原理】【遍历删除】


要点归纳

  • 数据结构
    • 1.7 HashMap 的数据结构 为 数组 + 链表 ,[1.8引用数组+链表+红黑树]
  • 初始化
    • 构造方法
    • 调用初始化
  • put 方法
    • 主要过程
    • 判断分配空间
    • key为空处理
    • 计算索引位
    • key冲突 处理过程
    • addEntry 、是否扩容
  • 扩容
    • 扩容过程
    • 环形链表问题

之前了解过的可按照上面提到的回想下,是否都掌握。



定义

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{ ...  }

可看到 HashMap 继承了 AbstractMap ,实现了 Map、Cloneable、Serializable
Cloneable:标记为colone、Serializable:标记为可被序列^化


来看下 定义的属性

	/**
	* 定义默认长度 16     2^4
	*/
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	/**
	* 最大长度         2^30
	*/
	static final int MAXIMUM_CAPACITY = 1 << 30;
	/**
	* 默认加载因子 [size >= 阈值(容量*加载因子) 则会进行扩容]
	*/
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	/**
	* 空的Entry 数组对象 
	*/
	static final Entry<?,?>[] EMPTY_TABLE = {};
	/**
	* 用于存放key value的对象 [还存储了当前对象的hash值 以及 next Entry *hash冲突时会形成链表结构]
	*/
	transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
	/**
	* 当前元素个数
	*/
	transient int size;
	/**
	* 当前数组的阈值
	*/
	int threshold;
	/**
	* 当前加载因子
	*/
	final float loadFactor;
	/**
	* 记录当前集合被修改的次数
	*/
	transient int modCount;

构造方法

	/**
	* 无参构造 则会取 默认长度 及 默认加载因子 调用下面的构造方法
	*/
	public HashMap() {
	        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
	    }
	
	/**
	* 手动定义长度 及 默认加载因子 调用下面的构造方法
	*/    
	public HashMap(int initialCapacity) {
	        this(initialCapacity, DEFAULT_LOAD_FACTOR);
	    }
    
    /**
	* map类型的参数 构造方法 
	*/
	public HashMap(Map<? extends K, ? extends V> m) {
	       this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
	                     DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
	       inflateTable(threshold);
	
	       putAllForCreate(m);
	  }
 	/**
	* 指定长度 及 加载因子
	*/
   	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;
	       threshold = initialCapacity;
	       init();
    }

以上则会发现,调用构造方法,虽然定义了 长度 及 加载因子,但并未进行初始化。
此时 table 对象 仍然为 EMPTY_TABLE


put方法

    public V put(K key, V value) {
        //校验 当前 数组为空 则进行初始化 [✨此过程为要点]
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //key 为 null 时,进行针对 null key 的赋值逻辑 [✨此过程为要点]
        if (key == null)
            return putForNullKey(value);
        //计算 key 对应的 hash 值  
        int hash = hash(key);
        //根据 hash 值 计算出 索引值 i [该key 对应的value 应放在 数组的索引位置] [✨此过程为要点]
        int i = indexFor(hash, table.length);
        //遍历 i 索引对应的 entry对象,如果hash 及 key 相等[引用 或 值 相等]
        //则将 value 替换 oldValue 并将 oldValue返回  [✨此过程为要点]
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
		//记录修改次数
        modCount++;
        //若上方没有匹配到相同的key 添加 Entry 对象
        addEntry(hash, key, value, i);
        return null;
    }

inflateTable 初始化方法

    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        //返回 第一个 大于或等于 参数 2 的 幂值 16 -> 16 、 17 -> 32
        //为什么一定要取 2的幂 后续会讲解 [✨此过程为要点]
        int capacity = roundUpToPowerOf2(toSize);
		//重新计算阈值 = 计算后的数组大小 * 加载因子 和 MAXIMUM_CAPACITY 比较取最小
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //创建Entry对象
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

putForNullKey方法

    private V putForNullKey(V value) {
    	//取出 table[0] 位置上的 entry对象 判断是否存在 null key 进行替换
        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++;
        //调用添加entry方法 设置 key 为 null
        addEntry(0, null, value, 0);
        return null;
    }

hash 方法

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        //获取k的hashCode值 自异或 
        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        //h 值 无符号右移 20 位 异或 无符号右移 12 位 并 自异或
        h ^= (h >>> 20) ^ (h >>> 12);
        //在 进行 一下的 移位 和 异或
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
多段移位[扰动处理 4次位运算 + 5次异或]  则是为了减少hash冲突 [✨此过程为要点]

    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        // h 按位与上 长度 -1 
        return h & (length-1);  [✨此过程为要点]
    }
比如:key = "ABCD"  计算后的 hash = "2054250"  按位与计算 转化为二进制 
当前 length = 16 
	h & (length-1) = h & 15
   h 1 1111 0101 1000 0110 1010
     0 0000 0000 0000 0000 1111
   = 0 0000 0000 0000 0000 1010
   = 10   

再比如:key ="LL" hahs="2315"  按位与计算 转化为二进制 
当前  length = 16
	h & (length-1) = h & 15
    h 1001 0000 1011
      0000 0000 1111
   =  0000 0000 1011
   =  11
过多就不举例了,结论是 无论 得到的hash值无论多少,只要 & 上当前长度 -1 
其取值范围 一定会在 [0,长度-1] 从而可保证在数组的 取值范围内 

[✨此过程为要点]

上面提到过 数组长度要取 2幂 此处做解释
例如 key ="LL" hahs="2315"  按位与计算 转化为二进制 
当前  length = 15  的话
	h & (length-1) = h & 14
	h 1001 0000 1011
      0000 0000 1110
   =  0000 0000 1010
   =  10
   
   假如 hash="2314"
	h & (length-1) = h & 14
    h 1001 0000 1010
      0000 0000 1110
   =  0000 0000 1010
   =  10
可见即使hash 值不一致,由于最后一位 为 0,得到的索引却相同,则大大增加了 碰撞的几率

addEntry方法

    void addEntry(int hash, K key, V value, int bucketIndex) {
    	//[✨此过程为要点]
        //判断是否进行扩容 当前长度是否 大于等于 阈值 并且 当前位置 不得空 才进行扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
        	//扩容为 当前length * 2
            resize(2 * table.length);
            //计算扩容后hash值
            hash = (null != key) ? hash(key) : 0;
            //计算扩容后 index
            bucketIndex = indexFor(hash, table.length);
        }
		//创建新的entry对象
        createEntry(hash, key, value, bucketIndex);
    }

createEntry方法

先看正常创建 entry 方法,下面讲扩容
	/**
	* hash hash值
	* key key值
	* value value值
	* bucketIndex  索引位置
	*/
    void createEntry(int hash, K key, V value, int bucketIndex) {
    	//e = 数组 当前索引上的值
        Entry<K,V> e = table[bucketIndex];
        //创建 Entry 
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
        /**
        * Entry对象 构造方法
        */
        Entry(int h, K k, V v, Entry<K,V> n) {
            hash = h;
            key = k;
            value = v;
            next = n;
        }
创建逻辑 将新建的 key-value 赋值为 数组的当前位置 
将数组原值 赋值到 当前对象 的 next ,从而形成链表结构 

此过程为 单链表 头插法  [✨此过程为要点]

在这里插入图片描述



resize 扩容

[✨此过程为要点]
hashMap 扩容 在多线程下 不安全,其原因是 容易出现 循环链表,导致get时 出现死循环
下面会 详细介绍 过程↓
    /**
    * newCapacity 扩容数组长度
    */
    void resize(int newCapacity) {
    	//oldTable 存储当前 table对象
        Entry[] oldTable = table;
        //校验当前长度 = 最大长度 则直接 赋值 Integer.MAX_VALUE 为 阈值 并返回
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
		//创建 新 Entry 用于存储 扩容后的 Entry
        Entry[] newTable = new Entry[newCapacity];
        //将 原数组的元素 移动到 新数组上 [✨此过程为要点]
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //将 扩容后的 entry对象 赋值 给 全局 table
        table = newTable;
        //重新计算 阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

	/**
    * newTable 用于存放 扩容后 新 Entry
    * rehash 是否重新进行hash
    */
    void transfer(Entry[] newTable, boolean rehash) {
    	//新数组长度
        int newCapacity = newTable.length;
        //遍历 -> 横向遍历数组上的 元素
        for (Entry<K,V> e : table) {
        	//遍历 当前位置上 存在可能为链表结构的 对象
        	//直到 当前对象为空
            while(null != e) {
            	//创建局部变量next作为 e对象中指向的 下一个元素 
                Entry<K,V> next = e.next;
                //判断是否重新hash
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //计算扩容后 新的 索引位
                int i = indexFor(e.hash, newCapacity);
                //将newTable[i] 作为 e.next 的值 
                e.next = newTable[i];
                //再将 e 作为 newTable[i]的值
                newTable[i] = e;
                //将局部变量next的值 赋值给e 进行下一次循环
                e = next;
            }
        }
    }

在这里插入图片描述

 [✨此过程为要点]
总结的来说:转移元素采用头插法
以下红框的代码 就是 将 oldTable中的值 转移到 newTable 上的过程;
entry对象 中 除了 记录了 key,value外,还记录了 next,和hash。
①:将 当前entry对象上的 next 对象 记录下来 ,作为 next 局部变量
②:将newTable[i] 当前位置上的元素 指向 entry的 next 对象
③:在将 e 放到 newTable[i] 的位置上
④:再将 第①步存储的 next 对象指向 e 对象,用于下次 while循环,从而转移链表中剩下的对象 

在这里插入图片描述

	//根据新长度 计算 索引值  	暂不考虑 调用重新计算 hash值
	int i = indexFor(e.hash, newCapacity);
	比如:h & (length-1)
	情况1: h = 1010 1010
	原数组长度为 16 
	h  = 1010 1010
	15 = 0000 1111
	i  = 0000 1010 = 10
	扩容后 数组长度为 32
	h  = 1010 1010
	31 = 0001 1010
	i  = 0000 1010 = 10
	情况2:
	情况1: h = 1011 1010
	原数组长度为 16 
	h  = 1011 1010
	15 = 0000 1111
	i  = 0000 1010 = 10
	扩容后 数组长度为 32
	h  = 1011 1010
	31 = 0001 1010
	i  = 0001 1010 = 26
	可见 扩容后 重新计算的 索引位置 与 扩容前 高一位 的 值有关
	若为 0 ,则计算结果不变,若为 1 ,则结果是 原来的 值 + length

举例:下面是将 oldTable[0] 位置上的元素 转移到 newTable[0] 上的过程
我们可看到 采用头插法 转移完 元素 ,原有链表上的 元素 在新 数组上 是倒置的链表

在这里插入图片描述
在这里插入图片描述


rehash 扩容时 重新计算hash值

至于再 转移前 是否进行 rehash [重新计算hash值],我们可看到 是根据 rehash 的值 决定。

在这里插入图片描述

我们来看下 调用transfer 之前的方法 是调用了 initHashSeedAsNeeded()

在这里插入图片描述

	/**
	* capacity 数组length
	*/
    final boolean initHashSeedAsNeeded(int capacity) {
     	//hashSeed 默认是 0  此时 currentAltHashing 为 false
        boolean currentAltHashing = hashSeed != 0;
        //sun.misc.VM.isBooted() jvm底层方法 此时会返回true
        //capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD
        //ALTERNATIVE_HASHING_THRESHOLD 这个值 会取  Integer.MAX_VALUE
        //所以得出结论 当 数组长度 大于 等于  Integer.MAX_VALUE 时 会 重新 计算hash值
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        // fasle 异或 true 才为 true 
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
所以得出结论 当 数组长度 大于 等于  Integer.MAX_VALUE 时 会 重新 计算hash值
正常运行时 如果手动不指定 数组长度 则 扩容是 进行 rehash的几率 很小,所以暂不考虑

多线程情况下 扩容 导致 循环链表问题 ***

在这里插入图片描述

引用如下图 所示

在这里插入图片描述

T1线程获得执行权,进行将K4,V4 移动到 newTable上  
如下图: T1线程移动完 的结果,注意此时 T2线程中 的引用还在,并且依然是 e = k4,v4  next = k3,v3 
T2线程获得了 cpu执行权 开始接着 执行 接下来的代码 转移元素

在这里插入图片描述

T2线程 继续执行下面的代码 将 e对象 移动到 T2的 newTable[i]的位置上
	Entry<K,V> next = e.next;
	···
 	e.next = newTable[i];
    newTable[i] = e;
    e = next;
移动后 结果如下图:
此时的e 指向 next指向的对象 k3,v3

在这里插入图片描述

继续进行下一次 移位
	Entry<K,V> next = e.next;
	···
	//此时 e.next 已经等于 newTable[i] (k4,v4已经在newTable[i]的位置) 了,所以此次没变化
 	e.next = newTable[i];
    newTable[i] = e;
    e = next;

在这里插入图片描述

继续进行下一次 移位
	//此时 next 为 k4,v4 的next ,为null
	Entry<K,V> next = e.next;
	···
	//e.next 指向了 k3v3 ,此时 整个newTable[i]链表 已经形成了 循环链表
 	e.next = newTable[i];

在这里插入图片描述

	//接着走下面的代码
    newTable[i] = e;
    e = next;
相当于 k3,v3 和 k4,v4 调换了下 ,next 指向了null,e = nuxt ,故 也为 null

在这里插入图片描述

 [✨此过程为要点]
e 为 null  下次 while 将中止 ,此时 T2 线程 扩容后的 newTable[i] 位置上的链表 形成了 循环链表。
当 某次 调用 get(key) 方法时,key 刚好所在 这个链表的位置时 ,进行遍历链表取值的时候 会造成死循环 !!!  

在这里插入图片描述

get方法

上面提到 若存在循环链表时,死循环是由于 get 方法 匹配到 该链表 后 进行遍历 时产生的。所以下面说下 get 方法:
    public V get(Object key) {
    	//若key为空 则调用 获取 getForNullKey 方法
        if (key == null)
            return getForNullKey();
        //否则 调用 getEntry方法
        Entry<K,V> entry = getEntry(key);
		//返回 enty不得null 则返回 entry的 value
        return null == entry ? null : entry.getValue();
    }
	/**
	* 获取 key 为空 索引上的值
	*/
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
		//根据key得到 索引位置 ,此处 判断 如果 key为null 则直接进入 0 位置找 ,因为 key为null 会put到 0位置
        int hash = (key == null) ? 0 : hash(key);
        //调用 indexFor 方法 得到 索引值 位置上的 entry对象
        //遍历 entry 对象 的链表结构 
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            //如果 判断 如果 hash相等 并且 ( k的引用相等 或者 k的值相等 ) 则返回该对象
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
由于 不同的 值 得到的 hash 值 可能相等 所以还要进一步判断 引用 或 值 是否相等
  if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))
            )

拓展

HashMap 遍历

方法一 entrySet
第一种是 通过 entrySet 方法 返回所有 entry的set集合 ,通过遍历这个集合 实现 :
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>(15);
        map.put("A","1");
        map.put("B","2");
        map.put("C","3");
        //获取entrySet
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        //遍历
        for (Map.Entry<String, String> e : entrySet) {
            System.out.println(e.getKey() + "->" + e.getValue());
        }
    }

在这里插入图片描述

方法二 迭代器
方法二 是通过迭代器 遍历
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>(15);
        map.put("A","1");
        map.put("B","2");
        map.put("C","3");
        //通过迭代器遍历
        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<String, String> e = iterator.next();
            System.out.println(e.getKey() + "->" + e.getValue());
        }
    }

在这里插入图片描述

HashMap 遍历时删除 元素

entrySet遍历
如果用第一种 遍历方式 删除 则会出现  ConcurrentModificationException 异常
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>(15);
        map.put("A","1");
        map.put("B","2");
        map.put("C","3");
        //获取entrySet
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        //遍历
        for (Map.Entry<String, String> e : entrySet) {
        	 if("B".equals(e.getKey())){
                map.remove(e.getKey());
            }
            System.out.println(e.getKey() + "->" + e.getValue());
        }
    }

在这里插入图片描述

这是由于 HashMap类中定义了 HashIterator 迭代器,初始化中 定义了一些属性
这里 主要看 expectedModCount 这个属性 为 modCount 
modCount:记录了 对map操作的次数 

在这里插入图片描述
当遍历 entry时 会调用 Map.Entry 中 的 nextEntry 方法 ,
该方法中 会判断 medCount 值 是否与 expectedModCount 一致 不一致则抛出异常
在这里插入图片描述

medCount 不一致 是因为调用了 map.remove(key) 而 hashMap的remove 方法中并没有 更新 medCount 值 
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
iterator遍历
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>(15);
        map.put("A","1");
        map.put("B","2");
        map.put("C","3");
        //通过迭代器遍历
        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
        while(iterator.hasNext()){
            Map.Entry<String, String> e = iterator.next();
            if("B".equals(e.getKey())){
                iterator.remove();
            }
            System.out.println(e.getKey() + "->" + e.getValue());
        }
    }
原因是 iterator 的remove方法 同步了 expectedModCount 值 

在这里插入图片描述

之后再 判断 时 已经正确 不会抛出异常

在这里插入图片描述

总结

针对Jdk7

HashMap的默认长度是多少?

调用无参构造,默认长度为16

HashMap为什么Key可为空?

put方法校验当key为null 时,会将元素存储到 table[0] 的位置上

HashMap的数据结构与底层原理?

1.7 采用数组 + 链表 
当 key 冲突时 ,会形成链表
【拓展】关于1.8 后续会出文章详解
1.8 采用 数组 + 链表 + 红黑树 
当碰撞时 形成链表,当链表长度大于TREEIFY THRESHOLD会形成红黑树

HashMap计算索引为什么是h & (length-1) ?

由于底层存放的是entry数组结构,计算索引时,要使得 取值范围刚好在 0 ~ 数组长度-1 的范围内。
第一想法可能是想到 用 取模 运算 h % length ? 不是相同的结果么?
不用取模运算 是因为 取模运算是 是 进行一步一步的除法,得到最终的余数。
计算机在二进制位运算 时的效率 要远大于 取模运算的。

HashMap的长度为什么要取2的次幂?

如果不采取2的次幂值,当进行put时,计算索引 " h & (length-1) " 时,转换为二进制位运算 最后一位 为0 ,
会大大增加不同hash值 计算索引值相同的几率,从而倒置碰撞的几率增大。
 
例如 key ="LL" hahs="2315"  按位与计算 转化为二进制 
当前  length = 15  的话
	h & (length-1) = h & 14
	h 1001 0000 1011
      0000 0000 1110
   =  0000 0000 1010
   =  10
   
   假如 hash="2314"
	h & (length-1) = h & 14
    h 1001 0000 1010
      0000 0000 1110
   =  0000 0000 1010
   =  10
不同的h值计算的索引一致

HashMap是线程安全的么?为什么?

不是线程安全的。
多线程在扩容时容易产生循环链表情况,当下次get到当前位置上时,会造成死循环!
原因是:
由于在hashMap 在扩容时 会将原数组上的值 转移到 扩容后的数组上,采用头插法。
会产生链表元素倒置情况。
若两个线程都进行扩容,进行完 Entry<K,V> next = e.next;该操作时,线程2挂起。
线程1执行完transfer方法后,线程2继续执行,由于已经存储了 数组开始时的引用,
在转移元素时会产生循环链表情况。【具体过程 可看上面扩容过程】
在调用get方法时,key匹配到产生循环链表上时,会产生死循环。

线程安全的Map有哪些?

我们会想到 
HashTable 
  是因为 hashTable 在 put、get、size、remove 等等方法上 加上了synchronized 简单粗暴 保证了线程同步。
  因为是 直接 synchronized  加在 方法上  效率受很大影响
ConcurrentHashMap 
 底层采用分段的数组 + 链表实现 只锁住当前段,效率较高

HashMap为什么在1.8中引用了红黑树?

1.7 采用数组 + 链表 
当 key 冲突时 ,会形成链表
【拓展】关于1.8 后续会出文章详解
1.8 采用 数组 + 链表 + 红黑树 
当碰撞时 形成链表,当链表长度大于TREEIFY THRESHOLD会形成红黑树
因为:
	如果采用数组 + 链表的形式,当元素达到一定量的时候,并且key冲突的元素过多时,会导致链表过长。
	我们都知道,链表结构的缺点 遍历查找时 大大降低了效率。

以上内容,若有不足或错误,还望指正。

不止于前 未来可期 ···


在这里插入图片描述

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值