Hashmap之我见

最近研读了Hashmap的源码(jdk1.8)
对其中的三个方法有了新的认知,在此记录下来。
如有错误请告知!

1 数组求下标的核心方法

 i=hash&(length-1)

假设我们获得的hash满足期望(均匀的随机 如1~15随机出现但不重复)
为了使数据分布均匀 减少hash碰撞 我们一般会采取hash%length运算 但是取模运算效率低 我们采用了与运算
因为取模运算是10进制的概念运算 与运算是二进制的概念运算
计算机底层是二进制的 使用取模运算要先把数据转换成2进制 这一步消耗资源 所以与运算更加高效
但是这两个方法需要在特定的情况下才等价 :要使hash%length =hash&(length-1) 需要length为2的n次方

假定hashcode=19   数组长度=16      19%16=319&(16-1)=3
hash值:      0000 0001   0011
数组长度-10000 0000   1111
结果:        0000 0000   0011        (等价于  19%16=3
但是如果长度不是2的指数次幂的话     假定hashcode=19   数组长度=15  19%15=419&(15-1)=2
hash值:      0000 0001   0011
数组长度-10000 0000   1110
结果:        0000 0000   0010         (不等价于  19%15=4

19&15 可以理解为
19=1×16+3 把1×16转换为2进制 就是上面 0000 0001 0011 中第二部分的0001, 3就是第三部分 0011
第二部分与上0000 还是0000 第三部分与上1111 等于第三部分自己 相当于去掉了1个16 等价于10进制的取模运算
由此可得 hashmap的长度一定要是2的指数次幂 不是的话就两种运算不等价

2 使hashcode后半部分更加均匀的方法

hashcode 异或 hashcode无符号右移16位 获得一个比较均匀的hashcode

(h = key.hashCode()) ^ (h >>> 16)

由于取下标采用了hash&(length-1),进入计算的有效位数取决于length(数组长度)的大小,一般我们的数组长度都不会太大,这样造成了进行计算的仅仅是hashcode的后几位,hash碰撞理所当然会增多,为了减小碰撞
我们可以让hashcode后半部分更加均匀一点.

此算法可以理解为让 二进制hash前16位与后16位异或计算 所有元素全部参与计算后16位的值 保证了相对均匀性
与运算容易偏向0 (75%概率0) ,或运算容易偏向1(75%概率1) 所以选取异或运算 (50%概率1 ,50%概率0)
计算后的二进制hashcode后16位 是一种均匀性比较高的随机数。取下标的时候可以有效降低hash碰撞

3 传入任何一个数都会返回大于等于这个数的2的n次方的数

    static final int tableSizeFor(int cap) {
    	//n=参数-1
        int n = cap - 1;
        n |= n >>> 1; //n自或上自己无符号右移1位
        n |= n >>> 2; //n自或上自己无符号右移2位
        n |= n >>> 4; //n自或上自己无符号右移4位
        n |= n >>> 8; //n自或上自己无符号右移8位
        n |= n >>> 16;//n自或上自己无符号右移16位
        //返回n+1
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

前面说了 hashmap的长度一定要是2的指数次幂,但是hashmap的构造方法支持传入任意长度

    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); //此方法对长度进行计算
    }

其实看下源码我们就可以发现 最终还是调用了tableSizeFor()方法对数组长度进行了一个计算
传入任何一个数都会返回大于等于这个数的2的n次方的数

int n = cap - 1;
n |= n >>> 1; //n自或上自己无符号右移1位
n |= n >>> 2; //n自或上自己无符号右移2位
n |= n >>> 4; //n自或上自己无符号右移4位
n |= n >>> 8; //n自或上自己无符号右移8位
n |= n >>> 16;//n自或上自己无符号右移16位
return n+1;

一个非0的int数用2进制表示,可以表示为32位的二进制数,
如 19: 0000 0000 0000 0000 0000 0000 0001 0011
最高有效位总是为1,

  n |= n >>> 1 可以让最高有效位前2位为1
 n: 		 0000 0000 0000 0000 0000 0000 0001 0011 
 n >>> 10000 0000 0000 0000 0000 0000 0000 1001 
   结果:     0000 0000 0000 0000 0000 0000 0001 1011 
  n |= n >>> 2 可以让最高有效位前4位为1
   n: 		 0000 0000 0000 0000 0000 0000 0001 1011 
 n >>> 10000 0000 0000 0000 0000 0000 0000 1101 
   结果:      0000 0000 0000 0000 0000 0000 0001 1111 
  n |= n >>> 4 可以让最高有效位前8位为1
  ......
  n |= n >>> 8 可以让最高有效位前16位为1
  ......
  n |= n >>> 16 可以让最高有效位前32位为1  经过五次自或之后,任意n都变成了2的n次方减一
  ......
     return n+1; //n为2的n次方减一,n+1代表2的n次方

为什么n要先减一? int n = cap - 1;
因为如果方法参数为16 如果不减1 ,返回的将会是32,所以他让16先减一 为15 这样方法的返回值就是16。相对于返回数组长度为32,这样做可以不浪费内存空间。
如有错误请告知!
如有错误请告知!!
如有错误请告知!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用 JavaScript 重新编写的代码,实现与您提供的 Java 代码类似的功能: ```javascript const requestParam = new Map(); requestParam.set("app_key", "100001"); requestParam.set("timestamp", "2018-01-22 10:10:10"); requestParam.set("format", "json"); requestParam.set("v", "1"); requestParam.set("sign_method", "MD5"); requestParam.set("session", "ee45021744e2d32eab172a5b7b04ae793bc6e97d"); requestParam.set("qr_code", "4yZB6nPiS4SXXkLKIpFLVg%3D%3D@993830"); // 构造签名 const appSecret = "1590f337484080dfa05e949f6b2c3c0357948876"; const sign = generateSignature(requestParam, appSecret, "MD5"); requestParam.set("sign", sign); // 构造请求参数 let formData = ""; for (const [key, value] of requestParam.entries()) { formData += encodeURIComponent(key) + "=" + encodeURIComponent(value) + "&"; } formData = formData.slice(0, -1); // 发起 POST 请求 fetch("https://openapi.dianping.com/router/tuangou/receipt/scanprepare", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", }, body: formData, }) .then((response) => response.text()) .then((data) => console.log(data)) .catch((error) => console.log(error)); function generateSignature(params, appSecret, signMethod) { // 对参数名进行排序 const sortedKeys = Array.from(params.keys()).sort(); // 拼接参数名和参数值 let paramString = ""; sortedKeys.forEach((key) => { if (key !== "sign" && params.get(key) !== "") { paramString += key + params.get(key); } }); // 添加 appSecret paramString += appSecret; // 使用指定的签名方法对参数进行摘要 if (signMethod === "MD5") { return md5(paramString); } else { throw new Error("Unsupported sign method"); } } function md5(str) { const md5 = require("crypto-js/md5"); return md5(str).toString(); } ``` 请注意,上述代码中使用了 `fetch` 函数来发起 POST 请求。如果您在非浏览器环境下使用,可以使用其他适合的方式来发起请求,例如 `axios`、`node-fetch` 等。此外,`crypto-js` 库用于计算 MD5 摘要,请确保已经安装了该库。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值