java中常见的Map

一、线程不安全的Map

HashMap

获取entry数组下标的方式:按位与

  • 根据key获得一个hashValue[注:hashValue=hash(key)],然后用hashValue对length-1进行按位与运算得到数组的下标,即:hashValue&(length-1)
  • 数组的length必须是2的整数次幂,原因如下:
    • 第一:若length是2的整数次幂,则hashValue&(length-1)等价于hashValue%length,那么hashValue&(length-1)同样也实现了均匀散列,但是(位运算)效率会更高。
      1>归纳:
          2^1 -1 = 0000 0001 
          2^2 -1 = 0000 0011 
          2^3 -1 = 0000 0111 
          2^n -1 = 0000 (n个1) 
      2>举例:
          若:hashValue=29,length=16
          则:hashValue & (length -1)  ==>  29 & (2^4-1)  ==>  00011101 & 00001111 = 00001101 ==>  13  ==>  0<= hashValue & (length -1) <=length -1
              hashValue % length         ==>  29 % 16        ==>     13                 ==>  0<= hashValue % length <=length -1
          故:hashValue & (length -1) == hashValue % length
      3>结论:当length=2^n时,hashValue & (length -1) == hashValue % length,且二者的结果范围都是:0到length-1之间的整数。
    • 第二:若length为奇数,则length-1为偶数,偶数(二进制)的最后一位是0,从而导致hashValue&(length-1)的最后一位永远为0,即:hashValue&(length-1)的结果永远为偶数,最终导致数组中下标为奇数的空间全部被浪费掉。

 代码:

int hash = hash(key);
int i = indexFor(hash, table.length);
static int indexFor(int h, int length) {
     return h & (length-1);
}


HashMap在jdk8中的优化:

  • 数据结构:
    •  jdk7:entry数组,entry是一个单向链表。
    • jdk8:entry数组,entry是一个 单向链表 或 红黑树。
  • 扩容:
    • jdk7:初始容量16,容量到达阈值后进行扩容,每次扩容后,容量变为之前的2倍。
    • jdk8:
      •  初始容量16,容量到达阈值后进行扩容,每次扩容后,容量变为之前的2倍。
      • 当链表长度大于等于8且当前容量还没达到最小树化容量(64)时,会进行扩容以减少冲突。这个逻辑在树化方法中:java.util.HashMap#treeifyBin
      • 说明:
        • 刚开始时HashMap的容量较小,故哈希碰撞的几率会比较大一些,即出现长链表的可能性会稍微大一些。因为容量较小而产生的长链表,我们应该优先选择扩容来降低冲突,而不是树化。
        •  eg:可能存在多个长链表,一次扩容可以同时降低这些链表的长度,若选择树化来降低冲突,则需要操作n次。
  • jdk8的树化(将链表转化为红黑树)和去树化:
    • 树化:当容量超过最小树化容量(64)时,如果存在链表长度大于等于树化阈值(8)时就会树化,故最坏的情况下的查找时间复杂度为O(logN)。jdk7最坏的情况下会遍历整个链表,时间复杂度为O(N)。
    • 去树化:扩容的时候,若发现红黑树中节点的数量小于等于去树化阈值(6)时,会将红黑树转换为链表。
    • 说明:
      • 若将去树化的阈值也设计为7,则当一个HashMap不停的 插入元素,再删除元素 时就会导致不断地进行树化和去树化的操作,导致效率降低。
      • TreeNodes占用的空间是普通Node的两倍,这样设计是为了追求时间和空间的平衡。
  • 线程安全:
    • jdk7:线程不安全,多个线程同时扩容时,可能会生成循环链表,导致cpu飙高。
    • jdk8:线程不安全,不会生成循环链表,但是put操作时存在丢失数据的情况。
  • 链表插入新节点的方式:
    • jdk7:新节点添加到头部
    • jdk8:新节点添加到尾部

TreeMap

  • 数据结构:基于红黑树实现。
  • 特点:键值对默认根据key升序排序,也可以自己定义比较器。

二、线程安全的Map


ConcurrentHashMap

HashTable

获取数组下标的方式:取模法

  • 根据key获得一个hashValue[注:hashValue=hash(key)&0x7FFFFFFF],然后用hashValue对数组的长度取模得到数组的下标,即:hashValue%length
  • 取模法基本能保证元素在哈希表中散列的比较均匀,但是取模会用到除法运算,效率很低。

代码:
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
 

HashMap VS HashTable

不同点:

  • HashMap多线程下是不安全的,HashTable是线程安全的。
  • HashMap的key和value都允许为null,HashTable的key和value都不允许为null(key或value为null时会抛出空指针异常)。
  • HashMap的默认容量是16,扩容后的容量是之前的2倍;HashTable的默认容量是11,扩容后的容量是之前的2倍+1。
  • 获取bucket的方式不同:HashTable通过取模法来获取bucket的下标,  HashMap通过 按位与 来获取bucket的下标。

相同点:

  • 都是Map的子类。
  • 都是基于Entry数组实现的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值