HashMap源码详解(一)

HashMap源码详解(一)

一、HashMap中重要的成员变量

1.size

作用:记录Map中KV键值对的个数

/**
* The number of key-value mappings contained in this map.
*/
transient int size;

2.loadFactor

作用:装载因子,用来衡量HashMap满的程度,默认值为0.75f

/**
 * The load factor for the hash table.
 */
final float loadFactor;
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

一般不建议修改loadFactor的值

3.capacity

作用:记录当前Map的容量

注:容量并未作为一个属性出现在源码之中,而是通过capacity()方法返回该Map的容量

transient Node<K,V>[] table;

final int capacity() {
    return (table != null) ? table.length :
        (threshold > 0) ? threshold :
        DEFAULT_INITIAL_CAPACITY;
}

容量默认为16,位运算的速度最快,所以写成1<<4而不是16,且必须是2的整数次幂

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

最大容量为2的30次方,同样必须是2的整数次幂

static final int MAXIMUM_CAPACITY = 1 << 30;

用户也可以通过构造函数自定义初始化容量,如果自定义容量不是2的整数次幂,HashMap则会选择第一个大于该数字的2的幂作为容量。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);//此装载因子为默认数值
}

例如(以下代码通过反射调用capacity()方法):

public static void main(String[] args)throws Exception {
    HashMap<String,String> map0 = new HashMap<>();
    HashMap<String,String> map1 = new HashMap<>(3);
    HashMap<String,String> map2 = new HashMap<>(5);
    HashMap<String,String> map3 = new HashMap<>(9);

    Class mc = map1.getClass();
    Method method = mc.getDeclaredMethod("capacity");
    method.setAccessible(true);
    System.out.println("capacity: "+method.invoke(map0));//capacity: 16
    System.out.println("capacity: "+method.invoke(map1));//capacity: 4
    System.out.println("capacity: "+method.invoke(map2));//capacity: 8
    System.out.println("capacity: "+method.invoke(map3));//capacity: 16
}

size和capacity的区别:

  • size为当前容器中已经存储的键值对个数

  • capacity为当前容器中可以存储的键值对个数

这里有一个小建议:在初始化HashMap的时候,应该尽量指定其大小。尤其是当你已知map中存放的元素个数时。(《阿里巴巴Java开发规约》)

4.threshole

作用:临界值,当实际KV键值对个数超过threshold时,HashMap将会扩大容量,threshold=容量*加载因子

/**
 * The next size value at which to resize (capacity * load factor).
 */
int threshold;

loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,即3/4;而capacity又是2的幂。所以,两个数的乘积都是整数。对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。

public static void main(String[] args)throws Exception {
    HashMap<String,String> map = new HashMap<>();

    Class mc = map.getClass();
    Method method = mc.getDeclaredMethod("capacity");
    method.setAccessible(true);

    for (int i = 0; i < 13; i++) {
        map.put(""+i,"hi");
        if (i==11||i==12)
            System.out.println("capacity: "+method.invoke(map));
    }
}

扩容时:新容量大小=旧容量大小*2

二、HashMap容量的初始化

1.容量初始化的好处

在上文中我们提到过,如果在已知元素个数的前提下,在HashMap初始化时,最好指定其大小。

原因:在默认初始化容量和指定初始化容量(合理指定),后者性能更好。

原理:当HashMap容量大小不够时,会进行扩容操作。由于HashMap的本身特性,每次扩容时都需要重新建立Hash表,重建操作十分耗时。所以合理的指定初始容量可以减少扩容操作,提高性能。

证明:

public static void main(String[] args) {
    int num = 10000000; //一千万

    //未初始化容量
    HashMap<String, String> map1 = new HashMap<>();
    long start1 = System.currentTimeMillis();
    for (int i = 0; i < num; i++) {
        map1.put(i+"", "hello");
    }
    long end1 = System.currentTimeMillis();
    System.out.println("未初始化容量,耗时 : " + (end1 - start1)/60 + " s");

    //初始化容量为元素数量一半
    HashMap<String, String> map2 = new HashMap<>(num / 2);
    long start2 = System.currentTimeMillis();
    for (int i = 0; i < num; i++) {
        map2.put(i+"", "hello");
    }
    long end2 = System.currentTimeMillis();
    System.out.println("初始化容量为元素数量一半,耗时 : " + (end2 - start2)/60 + " s");


    //初始化数量为元素数量
    HashMap<String, String> map3 = new HashMap<>(num);
    long start3 = System.currentTimeMillis();
    for (int i = 0; i < num; i++) {
        map3.put(i+"", "hello");
    }
    long end3 = System.currentTimeMillis();
    System.out.println("初始化数量为元素数量,耗时 : " + (end3 - start3)/60 + " s");
}

//未初始化容量,耗时 : 144 s
//初始化容量为元素数量一半,耗时 : 98 s
//初始化数量为元素数量,耗时 : 55 s

2.容量初始化扩容的算法

//cap为自定义容量
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;
}

>>>    :     无符号右移,忽略符号位,空位都以0补齐

上述代码主要分为两个部分:

第一部分:

int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;

理解:首先将原有数值-1,然后将该数值依次向右移位然后与原值取或。

原理:


整形为4个字节,则二进制位数最多有32位
原有数值-1后,从高位到低位,一定能找到第一个为1的数(先排除输入为0或1的情况)
设某一位为1,其它位为x(可能为0或1)

xxxx  x1xx  xxxx  xxxx  xxxx  xxxx  xxxx  xxxx

n |= n >>> 1;

xxxx  x1xx  xxxx  xxxx  xxxx  xxxx  xxxx  xxxx     (原n)

0xxx  xx1x  xxxx  xxxx  xxxx  xxxx  xxxx  xxxx     (n>>>1)

xxxx  x11x  xxxx  xxxx  xxxx  xxxx  xxxx  xxxx     (新n)

通过该行代码,保证了第一个1以及其后1个位置一定为1,此时1的个数一定>=2个

n |= n >>> 2;

xxxx  x11x  xxxx  xxxx  xxxx  xxxx  xxxx  xxxx     (原n)

00xx  xxx1  1xxx  xxxx  xxxx  xxxx  xxxx  xxxx     (n>>>1)

xxxx  x111  1xxx  xxxx  xxxx  xxxx  xxxx  xxxx     (新n)

通过该行代码,保证了第一个1以及其后3个位置一定为1,此时1的个数一定>=4个

n |= n >>> 4;

xxxx  x111  1xxx  xxxx  xxxx  xxxx  xxxx  xxxx     (原n)

0000  xxxx  x111  1xxx  xxxx  xxxx  xxxx  xxxx     (n>>>1)

xxxx  x111  1111  1xxx  xxxx  xxxx  xxxx  xxxx     (新n)

通过该行代码,保证了第一个1以及其后7个位置一定为1,此时1的个数一定>=8个

由上述规律可以看出,代码中移位操作的数值一定是已经确定的1的个数,每次经过移位或的操作之后,1的个数翻倍;

所以在最后一步移位16位并进行或操作之后,可以保证在这32位二进制数中,第一个为1的二进制位后所有的二进制数全为1;

最终结果:

xxxx  x111  1111  1111  1111  1111  1111  1111


第二部分:

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

该部分比较简单,是一个临界值的判断

  • 小于0(输入为0):结果为1
  • 大于等于0(输入为1)且小于最大值:结果为n+1
  • 大于最大值:结果为最大值

由于通过第一部分的转换,二进制数中1是连续的,所以在进行+1操作后,新的数值一定是2的整数次幂。

又因为在一开始,我们另输入数值减1,所以新的数值一定是第一个大于等于输入数值的2的整数次幂。

由此可见,一个很简单的容量扩容算法也包含了JAVA工程师很大的智慧。

3.合理的容量初始化数值

在知道了为什么要进行容量初始化和初始化扩容算法之后,一个新的问题出现了,什么样的数值是合理的初始化数值呢?

《阿里巴巴Java开发手册》有以下建议:

blogphoto.png

这个数值可以在我们需要存储的元素个数范围内减少扩容的次数,在性能上是一个良好的选择,尽管它需要牺牲一些内存。凡事都有两面性,由于在当今社会中,对于性能的提升已经成为了用户的主流需求,所在在时间和空间上,我们偏向于合理的牺牲空间换取时间。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值