青岛达内it培训 > 达内新闻 
        
        
            
            
            
            
            
                
                
                    Java集合类源码解析:HashMap(基于JDK1.8)1
                    
                        - 发布:青岛IT培训
 
                        - 来源:青岛IT培训
 
                        - 时间:2019-04-17 11:30
 
                        
                    
                 
                
                    
                             青岛IT培训的小编总结,今天我们来学习Java中较为常用的集合类 HashMap.
    另外说明一下,本文的 HashMap 源码是基于Jdk1.8版本的,如果没有特别说明的话,之后的集合类源码解析都是1.8的版本。
    HashMap的数据结构
    打开HashMap源码文件,可以看到它是继承自 AbstractMap,并实现了Java集合的根接口Map,以及Cloneable和Serializable接口,所以HashMap可以被序列化。
    HashMap的底层结构是哈希表的具体实现,通过相应的哈希运算就可以很快查询到目标元素在表中的位置,拥有很快的查询速度,因此,HashMap被广泛应用于日常的开发中。理想的情况就是一个元素对应一个Hash值,这样的查询效果是最优的。
    但实际这是不可能的,因为哈希表存在“hash (哈希) 冲突” 的问题。当发生hash冲突时,HashMap采用 “拉链法” 进行解决,也就是数组加链表的结构。在HashMap的代码注释中,数组中的元素用 “bucket” (中文读作 桶) 来称呼,而哈希函数的作用就是将key寻址到buckets中的一个位置,如果一个 bucket 有多个元素,那么就以链表的形式存储(jdk1.8之前单纯是这样)。
    这是HashMap的存储结构图:
    关于 “拉链法” 和 “hash冲突” 的知识点有疑问的读者可以看下我之前的文章 数据结构:哈希表以及哈希冲突的解决方案 .
    为了方便,下文的 “bucket” 都用 “桶” 替代。
    深入源码
    两个参数
    在具体学习源码之前,我们需要先了解两个HashMap中的两个重要参数,“初识容量” 和 “加载因子”,
    初识容量是指数组的数量。加载因子则决定了 HashMap 中的元素在达到多少比例后可以扩容 (rehash),当HashMap的元素数量超过了加载因子与当前容量的乘积后,就需要对哈希表做扩容操作。
    在HashMap中,加载因子默认是0.75,这是结合时间、空间成本均衡考虑后的折中方案,因为 加载因子太大的话发生冲突的可能性会变大,查找的效率反而低;太小的话频繁rehash,降低性能。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
    成员变量
    好了,前面说了那么多,现在开始深入源码学习吧,先了解一下HashMap的主要的成员变量:
    static final int DEFAULT_INITIAL_CAPACITY = 1 《 4;
    static final int MAXIMUM_CAPACITY = 1 《 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75F;
    static final int TREEIFY_THRESHOLD = 8;
    static final int MIN_TREEIFY_CAPACITY = 64;
    transient HashMap.Node<K, V>[] table;
    transient Set<Entry<K, V》 entrySet;
    transient int size;
    int threshold;
    final float loadFactor;
    可以看出,HashMap主要的成员变量比较多,有些变量还初始化了值,下面一个个来做解释。
    DEFAULT_INITIAL_CAPACITY:默认初识容量 1 《 4 ,也就是16,必须是2的整数次方。
    DEFAULT_LOAD_FACTOR:默认加载因子,大小为0.75.
    MAXIMUM_CAPACITY:最大容量, 2^ 30 次方。
    TREEIFY_THRESHOLD :树形阈值,大于这个数就要树形化,也就是转成红黑树。
    MIN_TREEIFY_CAPACITY:树形最小容量。
    table:哈希表的链接数组,对应桶的下标。
    entrySet:键值对集合。
    size:键值对的数量,也就是HashMap的大小。
    threshold:阈值,下次需要扩容时的值,等于 容量*加载因子。
    loadFactor:加载因子。
    介绍玩变量,下面介绍HashMap的构造方法。
    四个构造方法
    HashMap共有四个构造方法,代码如下:
    //加载默认大小的加载因子
    public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    //加载默认大小的加载因子,并创建一个内容为参数 m 的内容的哈希表
    public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //添加整个集合
    putMapEntries(m, false);
    }
    //指定容量和加载因子
    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);
    }
    //指定容量,加载因子默认大小
    public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    不难发现,上面第三个构造函数可以自定义加载因子和容量,首先判断传入的加载因子是否符合要求,然后根据制定的容量执行 tableSizeFor() 方法,它会根据容量来指定阈值,为何要多这一步呢?
    因为buckets数组的大小约束对于整个HashMap都至关重要,为了防止传入一个不是2次幂的整数,必须要有所防范。tableSizeFor()函数会尝试修正一个整数,并转换为离该整数最近的2次幂。
    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;
    }
    比如传入一个整数244,经过位移,或运算后会返回最近的2次幂 256
    插入数据的方法:put()
    在集合中最常用的操作是存储数据,也就是插入元素的过程,在HashMap中,插入数据用的是 put() 方法。
    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
	    }
	
    put方法没有做多余的操作,只是传入 key 和 value 还有 hash 值 进入到 putVal方法中并返回对应的值,点击进入方法,一步步跟进源码:
    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;
    //要插入位置没有元素,直接新建一个包含key的节点
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    //如果要插入的桶已经有元素,替换
    else {
    Node<K,V> e; K k;
    //key要插入的位置发生碰撞,让e指向p
    if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
    //没碰撞,但是p是属于红黑树的节点,执行putTreeVal()方法
    else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p)。putTreeVal(this, tab, hash, key, value);
    //p是链表节点,遍历链表,查找并替换
    else {
    //遍历数组,如果链表长度达到8,转换成红黑树
    for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
    p.next = newNode(hash, key, value, null);
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
    break;
    }
    // 找到目标节点,退出循环,e指向p
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    break;
    p = e;
    }
    }
    // 节点已存在,替换value,并返回旧value
    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;
    }
    代码看上去有点复杂,参数有点乱,但理清逻辑后容易理解多了,源码大概的逻辑如下:
    先调用 hash() 方法计算哈希值
    然后调用 putVal() 方法中根据哈希值进行相关操作
    如果当前 哈希表内容为空,做扩容
    如果要插入的桶中没有元素,新建个节点并放进去
    否则从要插入的桶中第一个元素开始查找(这里为什么是第一个元素,下面会讲到)
    如果没有碰撞,赋值给e,结束查找
    有碰撞,而且当前采用的还是 红黑树的节点,调用 putTreeVal() 进行插入
    链表节点的话从传统的链表数组中查找、找到赋值给e,结束
    如果链表长度达到8,转换成红黑树
    最后检查是否需要扩容
    put方法的代码中有几个关键的方法,分别是:
    hash():哈希函数,计算key对应的位置
    resize():扩容
    putTreeVal():插入红黑树的节点
    treeifyBin():树形化容器
    前面两个是HashMap的桶链表操作的核心方法,后面的方法是Jdk1.8之后有关红黑树的操作,后面会讲到,先来看前两个方法。
    哈希函数:hash()
    hash() 方法是HashMap 中的核心函数,在存储数据时,将key传入中进行运算,得出key的哈希值,通过这个哈希值运算才能获取key应该放置在 “桶” 的哪个位置,下面是方法的源码:
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    从源码中可以看出,传入key之后,hash() 会获取key的hashCode进行无符号右移 16 位,然后进行按位异或,并把运算后的值返回,这个值就是key的哈希值。这样运算是为了减少碰撞冲突,因为大部分元素的hashCode在低位是相同的,不做处理的话很容易造成冲突。
    之后还需要把 hash() 的返回值与table.length - 1做与运算,得到的结果即是数组的下标(为什么这么算,下面会说),在上面的 putVal() 方法中就可以看到有这样的代码操作,
    table.length - 1就像是一个低位掩码(这个设计也优化了扩容操作的性能),它和hash()做与操作时必然会将高位屏蔽(因为一个HashMap不可能有特别大的buckets数组,至少在不断自动扩容之前是不可能的,所以table.length - 1的大部分高位都为0),只保留低位,这样一来就总是只有最低的几位是有效的,就算你的hashCode()实现得再好也难以避免发生碰撞。这时,hash()函数的价值就体现出来了,它对hash code的低位添加了随机性并且混合了高位的部分特征,显着减少了碰撞冲突的发生。
    另外,在putVal方法的源码中,我们可以看到有这样一段代码
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    上面的注释也说明了,这是检测要插入位置是否有元素,没有的话直接新建一个包含key的节点,那么这里为什么要用 i = (n - 1) & hash 作为索引运算呢?
    这其实是一种优化手段,由于数组的大小永远是一个2次幂,在扩容之后,一个元素的新索引要么是在原位置,要么就是在原位置加上扩容前的容量。这个方法的巧妙之处全在于&运算,之前提到过&运算只会关注n
    – 1(n =
    数组长度)的有效位,当扩容之后,n的有效位相比之前会多增加一位(n会变成之前的二倍,所以确保数组长度永远是2次幂很重要),然后只需要判断hash在新增的有效位的位置是0还是1就可以算出新的索引位置,如果是0,那么索引没有发生变化,如果是1,索引就为原索引加上扩容前的容量。
    这样在每次扩容时都不用重新计算hash,省去了不少时间,而且新增有效位是0还是1是带有随机性的,之前两个碰撞的Entry又有可能在扩容时再次均匀地散布开,真可谓是非常精妙的设计。
    以上就是青岛IT培训给大家做的内容详解,更多关于UI的学习,请继续关注青岛IT培训
                     
                 
               
             
            
            
            
            
            
            
            
            
            
	最新开班时间
	
	  
		
		  - 北京
 
		  - 上海
 
		  - 广州
 
		  - 深圳
 
		  - 南京
 
		  - 成都
 
		  - 武汉
 
		  - 西安
 
		  - 青岛
 
		  - 天津
 
		  - 杭州
 
		  - 重庆
 
		  - 哈尔滨
 
		  - 济南
 
		  - 沈阳
 
		  - 合肥
 
		  - 郑州
 
		  - 长春
 
		  - 苏州
 
		  - 长沙
 
		  - 昆明
 
		  - 太原
 
		  - 无锡
 
		  - 石家庄
 
		  - 南宁
 
		  - 佛山
 
		  - 珠海
 
		  - 宁波
 
		  - 保定
 
		  - 呼和浩特
 
		  - 洛阳
 
		  - 烟台
 
		  - 运城
 
		  - 潍坊
 
		
	   
	  
	 
   
  
            
            
            
            
 
            
            
            
            
         
        
            
            
                    
                    
             
            
            
                
                    Java集合类源码解析:HashMap(基于JDK1.8)1
                    
                        - 发布:青岛IT培训
 
                        - 来源:青岛IT培训
 
                        - 时间:2019-04-17 11:30
 
                    
                 
                
                    
                            青岛IT培训的小编总结,今天我们来学习Java中较为常用的集合类 HashMap.
    另外说明一下,本文的 HashMap 源码是基于Jdk1.8版本的,如果没有特别说明的话,之后的集合类源码解析都是1.8的版本。
    HashMap的数据结构
    打开HashMap源码文件,可以看到它是继承自 AbstractMap,并实现了Java集合的根接口Map,以及Cloneable和Serializable接口,所以HashMap可以被序列化。
    HashMap的底层结构是哈希表的具体实现,通过相应的哈希运算就可以很快查询到目标元素在表中的位置,拥有很快的查询速度,因此,HashMap被广泛应用于日常的开发中。理想的情况就是一个元素对应一个Hash值,这样的查询效果是最优的。
    但实际这是不可能的,因为哈希表存在“hash (哈希) 冲突” 的问题。当发生hash冲突时,HashMap采用 “拉链法” 进行解决,也就是数组加链表的结构。在HashMap的代码注释中,数组中的元素用 “bucket” (中文读作 桶) 来称呼,而哈希函数的作用就是将key寻址到buckets中的一个位置,如果一个 bucket 有多个元素,那么就以链表的形式存储(jdk1.8之前单纯是这样)。
    这是HashMap的存储结构图:
    关于 “拉链法” 和 “hash冲突” 的知识点有疑问的读者可以看下我之前的文章 数据结构:哈希表以及哈希冲突的解决方案 .
    为了方便,下文的 “bucket” 都用 “桶” 替代。
    深入源码
    两个参数
    在具体学习源码之前,我们需要先了解两个HashMap中的两个重要参数,“初识容量” 和 “加载因子”,
    初识容量是指数组的数量。加载因子则决定了 HashMap 中的元素在达到多少比例后可以扩容 (rehash),当HashMap的元素数量超过了加载因子与当前容量的乘积后,就需要对哈希表做扩容操作。
    在HashMap中,加载因子默认是0.75,这是结合时间、空间成本均衡考虑后的折中方案,因为 加载因子太大的话发生冲突的可能性会变大,查找的效率反而低;太小的话频繁rehash,降低性能。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
    成员变量
    好了,前面说了那么多,现在开始深入源码学习吧,先了解一下HashMap的主要的成员变量:
    static final int DEFAULT_INITIAL_CAPACITY = 1 《 4;
    static final int MAXIMUM_CAPACITY = 1 《 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75F;
    static final int TREEIFY_THRESHOLD = 8;
    static final int MIN_TREEIFY_CAPACITY = 64;
    transient HashMap.Node<K, V>[] table;
    transient Set<Entry<K, V》 entrySet;
    transient int size;
    int threshold;
    final float loadFactor;
    可以看出,HashMap主要的成员变量比较多,有些变量还初始化了值,下面一个个来做解释。
    DEFAULT_INITIAL_CAPACITY:默认初识容量 1 《 4 ,也就是16,必须是2的整数次方。
    DEFAULT_LOAD_FACTOR:默认加载因子,大小为0.75.
    MAXIMUM_CAPACITY:最大容量, 2^ 30 次方。
    TREEIFY_THRESHOLD :树形阈值,大于这个数就要树形化,也就是转成红黑树。
    MIN_TREEIFY_CAPACITY:树形最小容量。
    table:哈希表的链接数组,对应桶的下标。
    entrySet:键值对集合。
    size:键值对的数量,也就是HashMap的大小。
    threshold:阈值,下次需要扩容时的值,等于 容量*加载因子。
    loadFactor:加载因子。
    介绍玩变量,下面介绍HashMap的构造方法。
    四个构造方法
    HashMap共有四个构造方法,代码如下:
    //加载默认大小的加载因子
    public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    //加载默认大小的加载因子,并创建一个内容为参数 m 的内容的哈希表
    public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //添加整个集合
    putMapEntries(m, false);
    }
    //指定容量和加载因子
    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);
    }
    //指定容量,加载因子默认大小
    public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    不难发现,上面第三个构造函数可以自定义加载因子和容量,首先判断传入的加载因子是否符合要求,然后根据制定的容量执行 tableSizeFor() 方法,它会根据容量来指定阈值,为何要多这一步呢?
    因为buckets数组的大小约束对于整个HashMap都至关重要,为了防止传入一个不是2次幂的整数,必须要有所防范。tableSizeFor()函数会尝试修正一个整数,并转换为离该整数最近的2次幂。
    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;
    }
    比如传入一个整数244,经过位移,或运算后会返回最近的2次幂 256
    插入数据的方法:put()
    在集合中最常用的操作是存储数据,也就是插入元素的过程,在HashMap中,插入数据用的是 put() 方法。
    public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
	    }
	
    put方法没有做多余的操作,只是传入 key 和 value 还有 hash 值 进入到 putVal方法中并返回对应的值,点击进入方法,一步步跟进源码:
    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;
    //要插入位置没有元素,直接新建一个包含key的节点
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    //如果要插入的桶已经有元素,替换
    else {
    Node<K,V> e; K k;
    //key要插入的位置发生碰撞,让e指向p
    if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
    //没碰撞,但是p是属于红黑树的节点,执行putTreeVal()方法
    else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p)。putTreeVal(this, tab, hash, key, value);
    //p是链表节点,遍历链表,查找并替换
    else {
    //遍历数组,如果链表长度达到8,转换成红黑树
    for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
    p.next = newNode(hash, key, value, null);
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
    break;
    }
    // 找到目标节点,退出循环,e指向p
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    break;
    p = e;
    }
    }
    // 节点已存在,替换value,并返回旧value
    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;
    }
    代码看上去有点复杂,参数有点乱,但理清逻辑后容易理解多了,源码大概的逻辑如下:
    先调用 hash() 方法计算哈希值
    然后调用 putVal() 方法中根据哈希值进行相关操作
    如果当前 哈希表内容为空,做扩容
    如果要插入的桶中没有元素,新建个节点并放进去
    否则从要插入的桶中第一个元素开始查找(这里为什么是第一个元素,下面会讲到)
    如果没有碰撞,赋值给e,结束查找
    有碰撞,而且当前采用的还是 红黑树的节点,调用 putTreeVal() 进行插入
    链表节点的话从传统的链表数组中查找、找到赋值给e,结束
    如果链表长度达到8,转换成红黑树
    最后检查是否需要扩容
    put方法的代码中有几个关键的方法,分别是:
    hash():哈希函数,计算key对应的位置
    resize():扩容
    putTreeVal():插入红黑树的节点
    treeifyBin():树形化容器
    前面两个是HashMap的桶链表操作的核心方法,后面的方法是Jdk1.8之后有关红黑树的操作,后面会讲到,先来看前两个方法。
    哈希函数:hash()
    hash() 方法是HashMap 中的核心函数,在存储数据时,将key传入中进行运算,得出key的哈希值,通过这个哈希值运算才能获取key应该放置在 “桶” 的哪个位置,下面是方法的源码:
    static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    从源码中可以看出,传入key之后,hash() 会获取key的hashCode进行无符号右移 16 位,然后进行按位异或,并把运算后的值返回,这个值就是key的哈希值。这样运算是为了减少碰撞冲突,因为大部分元素的hashCode在低位是相同的,不做处理的话很容易造成冲突。
    之后还需要把 hash() 的返回值与table.length - 1做与运算,得到的结果即是数组的下标(为什么这么算,下面会说),在上面的 putVal() 方法中就可以看到有这样的代码操作,
    table.length - 1就像是一个低位掩码(这个设计也优化了扩容操作的性能),它和hash()做与操作时必然会将高位屏蔽(因为一个HashMap不可能有特别大的buckets数组,至少在不断自动扩容之前是不可能的,所以table.length - 1的大部分高位都为0),只保留低位,这样一来就总是只有最低的几位是有效的,就算你的hashCode()实现得再好也难以避免发生碰撞。这时,hash()函数的价值就体现出来了,它对hash code的低位添加了随机性并且混合了高位的部分特征,显着减少了碰撞冲突的发生。
    另外,在putVal方法的源码中,我们可以看到有这样一段代码
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    上面的注释也说明了,这是检测要插入位置是否有元素,没有的话直接新建一个包含key的节点,那么这里为什么要用 i = (n - 1) & hash 作为索引运算呢?
    这其实是一种优化手段,由于数组的大小永远是一个2次幂,在扩容之后,一个元素的新索引要么是在原位置,要么就是在原位置加上扩容前的容量。这个方法的巧妙之处全在于&运算,之前提到过&运算只会关注n
    – 1(n =
    数组长度)的有效位,当扩容之后,n的有效位相比之前会多增加一位(n会变成之前的二倍,所以确保数组长度永远是2次幂很重要),然后只需要判断hash在新增的有效位的位置是0还是1就可以算出新的索引位置,如果是0,那么索引没有发生变化,如果是1,索引就为原索引加上扩容前的容量。
    这样在每次扩容时都不用重新计算hash,省去了不少时间,而且新增有效位是0还是1是带有随机性的,之前两个碰撞的Entry又有可能在扩容时再次均匀地散布开,真可谓是非常精妙的设计。
    以上就是青岛IT培训给大家做的内容详解,更多关于UI的学习,请继续关注青岛IT培训
                     
                 
                
             
            
            
            
            
            
                最新开班时间
                
                    
                    
                        - 北京
 
                        - 上海
 
                        - 广州
 
                        - 深圳
 
                        - 南京
 
                        - 成都
 
                        - 武汉
 
                        - 西安
 
                        - 青岛
 
                        - 天津
 
                        - 杭州
 
                        - 重庆
 
                        - 厦门
 
                        - 哈尔滨
 
                        - 济南
 
                        - 福州
 
                        - 沈阳
 
                        - 合肥
 
                        - 郑州
 
                        - 长春
 
                        - 苏州
 
                        - 大连
 
                        - 长沙
 
                        - 昆明
 
                        - 温州
 
                        - 太原
 
                        - 南昌
 
                        - 无锡
 
                        - 石家庄
 
                        - 南宁
 
                        - 中山
 
                        - 兰州
 
                        - 佛山
 
                        - 珠海
 
                        - 宁波
 
                        - 贵阳
 
                        - 保定
 
                        - 呼和浩特
 
                        - 东莞
 
                        - 洛阳
 
                        - 潍坊
 
                        - 烟台
 
                        - 运城