Hashmap笔记

今天在提交代码的时候。提交了一行

 

HashMap<String, Object> pageMap = new HashMap<>(3);

特傻逼的代码。被老大指出太业余。虽然自己也知道hashmap的容量要设置成2的幂次,但是实际使用起来却没有丝毫的注意。

说明还是对Hashmap的理解不够透彻,故决定整理一下Hashmap的笔记。

 

 

Hashmap具有数组和链表的优点。

使用hash算法加快访问速度,使用散列表解决碰撞冲突的问题,

数组的每个元素是单链表的头结点,链表是用来解决冲突。

hashmap是线程不安全的

 

HashMap中put方法,put(K key, V value)

如果key的哈希码不存在,加入元素。

否则,获取key的哈希码,计算出他的bucketIndex,

去除bucketIndex上的元素,循环单链表。有值得话把旧值替换了,没有值的话加入链表。

 

 

Hashmap的两个重要参数:初始容量和加载因子。

初始容量是hash数组的长度,

当前加载因子=当前hash数组元素/hash数组长度。

最大加载因子为最大能容纳的数组元素个数。

 

哈希表的容量一定是2的整数次幂。

(为不相等的对象生成不同整数结果可以提高哈希表的性能)

初始容量是16,初始加载因子是0.75。容量是哈希表中桶的数量,初始容量是哈希表在创建时的数量,加载因子是其容量在其自动增加前可以达到的最大的尺度,当哈希表中的数量超过最大加载因子和容量的乘积,就要对hashmap进行扩容。

看hashmap的构造方法,在初始化容量的时候,不管初始化容量是多少,都返回最近的不小于初始化容量的2的整数次幂

 

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);
}
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;
}

先让cap-1是为了找到目标值大于或等于原值,

再进行五个位移操作,是为了使cap的二进制从全部变成1(因为int为32位,所以最后位移16位就能实现)

对n |= n >>>?举例解释:

n=1000000

n|=n>>>1;n=n|(n>>>1)=1000000|0100000=1100000

n|=n>>>2;n=n|(n>>>2)=1100000|0011000=1111000

。。。。

所以不管初始值设置了多少。最后都会变成距离设置的值最近的不小于它的二次幂。

现在分析假如源码不做这个转化成二次幂的操作。

看put源码

 

if ((p = tab[i = (n - 1) & hash]) == null)

在put的时候先进行(n-1)&hash的判断。n是设置的hashma初始值,hash是put值得key生成的hashcode

假如n不是二次幂。那么n-1的二进制肯定有一位不为1

用15举例,15-1=14,二进制是1110,那么任何数和1110进行&操作,第一位永远不可能为1,所以第一位为1的空间全部都浪费了。

再回到老大指责我的问题,我的设置其实不会有什么影响,但是一眼就能看出,太不专业了。

 

补充Java8 hashmap的put原理,源码就不贴了,大致记录一下自己理解的流程。

先(n-1)&hash判断这个位置有没有值,没有的话直接插入,

有的话判断这个位置的类型是不是树结构,是的话直接添加树,

不是的话就加入链表,与1.7不同的是,1.7是头插法,后来的留在数组,1.8是先来的在数组,后来的链在尾上。

最后如果链表长度为7,再添加一个时候链表就要变成红黑树了。

优势在于,1.7的链表查询时间复杂度为o(n),1.8的红黑树查询时间复杂度是o(logn)

 

再补充一下HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值