容器Map和HashMap底层原理分析

map 接口

  • 概述
    • 保存key-value的键值对,允许key和value为null值
    • key值不允许重复
    • 不推荐将可变对象 自定义对象作为key使用
    • map自身不能作为key,但可以作为value,也不推荐这么做
  • 内部实现
    • 内部包含Entry接口
    • key和value保存在entry中
  • 方法
    • clear():清空
    • isEmpty():空判断
    • containsKey():包含key判断
    • containsValue():包含value判断
    • get(key):通过key取value
    • put():放入元素
    • putAll(map):将另一个map的值放入
    • size():返回键值对的个数
    • 遍历
      • values():返回value的集合视图,用于遍历value
      • entrySet():返回entry的set集合视图,用于遍历entry,通过entry取key和value
      • keySet():返回key的set集合视图,用于遍历key,然后可通过key取值value

HashMap

容器Map和HashMap底层原理分析_第1张图片

  • 概述
    • 基于哈希表的map接口的实现,允许key和value为null值
    • 不保证顺序,尤其不保证顺序不变
    • HashMap的遍历性能与其底层容量和大小成比例,所以如果遍历性能重要时,不要把初始容量设置太高,也不要把加载因子设置太低
    • 加载因子过高会降低空间开销,但是会增加查询成本
    • 线程不安全
    • 迭代器是快速失败的,也是通过modcount值实现
  • HashMap—数据结构的发展过程
    • 数组
      • 存储空间连续,占用内存会有浪费,所以空间复杂度大,但是查找复杂度小,特点是查询快,增删慢
    • 链表
      • 存储空间不连续,使用逻辑关联,内存占用相对宽松,所以空间复杂度小,但是查找复杂度大,特点是查找慢,增删快
    • 哈希表
      • 综合两者,使用哈希表
      • 数组加链表,数组的每个位置存储一个链表
  • 底层实现

    • 基于哈希表的map的实现,即底层通过数组加链表实现
    • 哈希散列过程:将元素均匀分布于底层数组中
      • 主要通过hashcode和equals方法
      • 区分不同元素时,先计算hashcode,hashcode不一样肯定不是同一个元素
      • 如果hashcode一致时,通过equals方法确认是否相同
      • HashMap散列时,二次hash,通过hashcode再计算出一个hash值。使元素散列更均匀
    • 创建

      • 空构造时默认底层数组长度为16,加载因子为0.75
      • 可通过构造器确定数组长度length,不过最终长度为大于等于此length的2的n此方。
        • 举例:构造指定长度为7,实际长度为8
      • 可通过构造器确定加载因子
      • 可通过其他map构造一个新的HashMap
    • 存取过程

      • 如果key为null,那就放在数组的第一个位置
      • 根据key的hashcode计算出hash值,通过hash%length计算出数组中的位置(索引)index
        • 源码是计算:hash & length-1。
        • 两个计算一样,但是与运算更快
      • 索引相同的键值对会组成链表存入数组的该位置
      • 存储时,是存入entry内
        • entry包含hash,key,value,和next
          • hash代表key的哈希值
          • next即下个entry的引用来实现链表存储
        • 每次存储新entry时,新的entry会存入数组位置中
        • 然后entry.next指向老的entry
      • 通过key取value值时,也是如此先计算出数组索引,找到链表,然后遍历链表,通过equals方法找到和key相等的entry,再取出value值
    • 扩容过程

      • 扩容阀值:数组长度*加载因子
      • 当数组内元素个数达到扩容阀值时,会发生数组扩容
        • 不是数组被占用的位置数,而是所有元素的个数,即size属性,每次此加入元素都会加1
        • 即使所有元素都在数组的同一个位置,也会进行扩容
      • 数组扩容为原始数组大小的两倍,依然为2的指数次幂
      • 最大长度为2的30次方,此时扩容阀值int的最大值
    • 重新散列过程rehash

      • 底层数组扩容时,会对每个元素重新计算索引值,根据新索引值将元素放到新的位置
      • 高并发下,可能会出现环形链表,需要加锁解决,或换线程安全的map,如ConcurrentHashMap
  • 方法

    • 基本都是实现的Map的方法

HashMap的数组容量为2的解释

hashMap源码获取元素的位置:

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : length must be a non-zero power of 2";
    //h:为插入元素的hashcode
    //length:为map的容量大小
    //&:与操作 比如 1101 & 1011=1001

    return h & (length-1);
}
  • 效率因素
    • 如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,在二进制与操作中效率会非常的快
  • 空间利用因素,即元素更均匀的分布在数组中
    • 如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在和h与操作时,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大
    • 更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!即其他位置中的链表会变长,遍历会更慢

重新散列过程

  • 源码过程简单了解(我源码没看懂,网上看来的)

当table需要扩容时,扩容后的table大小变为原来的两倍,接下来就是进行扩容后table的调整:
假设扩容前的table大小为2的N次方,元素的table索引为其hash值的后N位确定
那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位
因此,table中的元素只有两种情况:
元素hash值第N+1位为0:不需要进行位置调整
元素hash值第N+1位为1:调整至原索引的两倍位置
在resize方法中,第45行的判断即用于确定元素hashi值第N+1位是否为0:
若为0,则使用loHead与loTail,将元素移至新table的原索引处
若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处
扩容或初始化完成后,resize方法返回新的table

HashMap的红黑二叉树

这个完全看不懂,只是了解
JDK8引入红黑二叉树,目的还是使元素跟均匀分布,减少查询遍历的时间

  • 解决的问题
    • 某些情况下,底层数组的几个位置中的链表很长很长
  • 关键参数
    • TREEIFY_THRESHOLD=8
      • 链表转化为红黑树的阀值,即某个数组位置上链表超过8个就转化为红黑树结构
    • UNTREEIFY_THRESHOLD=6
      • 红黑树转化为链表的阀值,即某个数组位置上红黑树结构元素少于6个就转化为链表
    • MIN_TREEIFY_CAPACITY=64
      • 红黑树开始使用的数组容量阀值,即当数组容量达到此值时,才会发生上面两个转化过程

TreeMap

容器Map和HashMap底层原理分析_第2张图片
- 概述
- 底层红黑数实现,键不能为null,但是value值可以为null
- 所有的key必须可以排序
- 自然顺序
- Comparator接口排序
- 方法
- 取值:没有返回null
- ceilingEntry(key):大于等于entry
- ceilingKey(key):大于等于的key
- comparator():返回比较器
- firstEntry():第一个entry
- firdtKey():第一个key
- floorEntry(key):小于等于entry
- floorKey(key):小于等于key
- higherEntry(key):大于entry
- higherKey(key):大于key
- lowerEntry(key):小于entry
- lowerKey(key):小于key
- lastEntry():最后entry
- lastKey():最后key
- subMap(start,end):截取map,开始到结束-1
- headMap(key):截取map,1到key-1
- 遍历
- desendingKeySet()
- desendingMap()
- entrySet()
- keySet()
- navigableKeySet()
- values()

Hashtable

容器Map和HashMap底层原理分析_第3张图片

  • 概述

    • 基本与hashMap一致
  • 与HashMap不同点

    • 直接附类不一样,Hashtable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap
    • 只是线程安全,效率低一点
    • 键和值都不允许null值
    • 默认数组长度为11,默认加载因子为0.75
    • Hashtable的扩容长度为原始长度*2+1
    • 计算索引的方法不一样:
      哈希值和0x7FFFFFFF取与操作后,与数组容量取模
      因为一个对象的HashCode可以为负数,这样操作后可以保证它为一个正整数.然后以Hashtable的
      长度取模,得到该对象在Hashtable中的索引
      `int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;
  • 方法
    • keys():返回key的枚举,
    • elements():返回value值的枚举

Properties

  • 概述
    • 继承自Hashtable,键和值都是string,且可保存到流中或从流中加载
    • 用来保存属性列表
    • 大部分方法是线程安全的

WeakHashMap

容器Map和HashMap底层原理分析_第4张图片

  • 以弱键实现的基于哈希表Map,底层数组加链表
  • 键和值允许null值,与HashMap拥有相同的初始容量和加载因子
  • 弱键:当某个键不再使用时,自动移除
  • 线程不安全
  • 迭代器快速失败的

LinkedHashMap

  • 概述
    • 继承自HashMap,底层数组加链表,不过它是双向列表,HashMap是单向列表
    • 元素有迭代顺序,就是插入的顺序
    • 提供特殊的构造方法来创建链接哈希映射,该哈希映射的迭代顺序就是最后访问其条目的顺序,从近期访问最少到近期访问最多的顺序(访问顺序)。这种映射很适合构建 LRU 缓存
    • 与HashMap相同的初始容量和加载因子
    • 线程不安全
      • 迭代器快速失败
      • 迭代器的性能主要与容器内元素个数决定,与容量没关系。

ConcurrentHashMap

容器Map和HashMap底层原理分析_第5张图片

  • 概述
    • 行为与Hashtable类似,底层数组加链表,线程安全
    • 但是取操作不加锁,更新操作时分段锁
    • 初始容量16,加载因子0.75

你可能感兴趣的:(基础学习)