容器Map和HashMap底层原理分析

来源:互联网 发布:iso9001 软件开发规范 编辑:程序博客网 时间:2024/06/06 09:14

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

hashmap继承关系

  • 概述
    • 基于哈希表的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

treemap继承关系
- 概述
- 底层红黑数实现,键不能为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

hashtable继承关系

  • 概述

    • 基本与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

weakhashmap继承关系

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

LinkedHashMap

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

ConcurrentHashMap

concurrenthashmap继承关系

  • 概述
    • 行为与Hashtable类似,底层数组加链表,线程安全
    • 但是取操作不加锁,更新操作时分段锁
    • 初始容量16,加载因子0.75
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 儿子深一点用点力阅读张研 儿子你用点力全文阅读 啊快一点啊用点力啊视频 儿子再用点力妈要飞了视频 中国真实的母教子视频 四川单亲妈教子性知识 52岁全程落脸视频 我尝到了母爱的滋味开头 我尝到了母爱的滋味txt 我尝到了母爱的甜蜜400 真实母教子视频 我尝到了母亲的滋味400 我尝到了母爱的滋味450 单亲家庭母教子 啊儿子深点妈要来了中文字 美国真实的母教子视频 儿子别 停妈还要中文字 尝到了母爱的滋味400 让儿子尝试了一次中文字 我尝到了母爱的滋味40 我尝到了母爱的滋味400小说下载 好胀胀死妈了乖乖儿子中文字 我尝到了母爱的滋味300 儿子别射J去妈会怀孕视频中文 全屏儿子射J去妈怀孕了漫画 全屏无遮单身妈和儿 四川真实亲妈视频y 全屏无遮单身妈和儿子漫画 全屏无遮单身妈和儿子线播放 青岛重庆真实儿子亲妈 全屏无遮单身在线播放 四川真实亲妈视频链接 全屏无遮单身妈和儿子在线播放中文字 白边液越用白边越大 四川亲妈真实视频 普通话对白边电话边看边干 浙江边干边对白边对白边对白 离婚后一直跟儿子做 边骂脏话对白 离婚多年生理需求和儿子 儿子很帅没忍住和他