前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JDK1.8HashMap源码学习-初始化

JDK1.8HashMap源码学习-初始化

作者头像
木左
修改2020-10-20 16:54:15
2810
修改2020-10-20 16:54:15
举报

本文将主要介绍New HashMap()做了些什么以及Node<K,V>数组是怎么进行初始化的。

01

默认属性

/**
* 数组 Node<K,V>[] table
* 默认初始容量 2^4 = 16
* 必须是2的多少次幂等
* 是数组的长度
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
* 数组 Node<K,V>[] table
* 最大容量 2^30=1073741824
* 可能用户使用带有容量的构造方法
* 初始化map的时候会写一个很大的值
* 比较后最大也就是 2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* 数组 Node<K,V>[] table
* 默认使用率 0.75f
* 如果用户没有指定则采用该值
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* 数组 Node<K,V>[] table
* 桶中存储元素优先采用单向链表
* 当链表节点数大于等于该值时
* 可能会触发单向链表转双向链表构造红黑树
* 还有条件是table表容量
* 必须大于等于 
* MIN_TREEIFY_CAPACITY
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* 当桶中节点数小于等于该值时
* 触发双向链表退化成单向链表
* 不再构造红黑树
*/
static final int UNTREEIFY_THRESHOLD = 6;

/**
* 当Node<K,V>[] table的容量
* 即数组长度 大于等于该值
* 且桶中节点数大于等于TREEIFY_THRESHOLD
* 触发桶中单向链表转双向链表构造红黑树
*/
static final int MIN_TREEIFY_CAPACITY = 64;

关于HashMap默认属性我们总结如下:

  1. 默认长度为2^4=16,最大2^30=1073741824
  2. 默认使用率75%
  3. 当数组长度大于等于64且桶中节点数大于等于8触发桶中数据节点的结构从单向链表转双向链表再构造红黑树
  4. 当桶中的节点小于等于6,触发桶中的节点从双向链表转单向链表

02

成员属性

/**
* 数组Node<K,V>[] table
* map的key hash后对数组长度取余的值
* 通过数组下标可以快速定位数据存储在数组的哪个位置上
* 也就是数据在哪个桶中
*/
transient Node<K,V>[] table;
/**
* 遍历HashMap入口 称为视图
*/
transient Set<Map.Entry<K,V>> entrySet;

/**
* map的长度 即有多少个key-value
*/
transient int size;

/**
* map结构修改次数
*/
 transient int modCount;

/**
* map容量触发扩容的阈值
* 如果当前map含有的节点数超过该值
* 就会调用resize()方法进行扩容
*/
int threshold;

/**
* table数组的使用率
* 用于计算触发下一次触发扩容的阈值
*/
final float loadFactor;

03

构造方法

/**
* 无参构造方法 我们也最常用
* 采用默认数组长度 16
* 默认使用率75% 
* 而我们只看到了使用率进行了赋值
* 那数组长度呢 在resize()中
* 我们下一小节初始化一起说
*/
public HashMap() {
   this.loadFactor = DEFAULT_LOAD_FACTOR;
}

/**
* 设置初始容量构造方法
* 这里调用的是另外一个构造方法
* 传入的参数是客户定义的容量
* 和默认的使用率75%
*/
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* 设置初始容量和使用率的构造方法
*/
public HashMap(int initialCapacity, float loadFactor) {
    //初始容量必须大于0
    if (initialCapacity < 0){
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
     }
     //如果容量大于最大值 着赋值容量为最大值
    if (initialCapacity > MAXIMUM_CAPACITY){
        initialCapacity = MAXIMUM_CAPACITY;
    }
    //使用率不能小于等于0
    if (loadFactor <= 0 || Float.isNaN(loadFactor)){
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    }
    //赋值使用率
    this.loadFactor = loadFactor;
    //赋值扩容阈值 而且是经过计算的 再看下计算了什么
    this.threshold = tableSizeFor(initialCapacity);
}
    
/**
* Returns a power of two size for the given target capacity.
* 看着这些不怎么使用的计算方式
* 复制测试了下 发现其实就是为了变为2的幂次方
* 比如传入10 返回16 传入16返回16 传入18返回32
*/
 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;
}
 
/**    
* 直接传入Map进行构造
* 首先赋值为默认使用率
* 再把整个Map遍历放入
*/
public HashMap(Map<? extends K, ? extends V> m) {
    //赋值使用率为75%
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //放入Map
    putMapEntries(m, false);
}

 /**
 * 装入Map
 * evict参数留给了子类去扩展使用
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //获取装入Map的长度
    int s = m.size();
    //长度大于0才执行操作 否则是个空的 不用执行遍历赋值操作
    if (s > 0) {
        //如果数组为空
        if (table == null) {
            //采用默认数组使用率计算数组的容量
            //+1应该是为了保证下面下取整不够了
            float ft = ((float)s / loadFactor) + 1.0F;
            //只要容量小于最大容量就下取整
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
            //如果此时的容量大于阈值 重新计算阈值
            if (t > threshold){
                threshold = tableSizeFor(t);
            }
        //如果数组不为空 插入数组的长度大于当前扩容阈值
        //直接先扩容
        }else if (s > threshold){
            //初始化和扩容都在这里
            resize();
        }
        //循环遍历 插入值
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

构造方法总结如下:

  1. 构造方法主要完成的就是使用率loadFactor和阈值threshold的赋值操作
  2. 阈值threshold在构造方法中也是2的幂次方

疑问如下:

  1. 不是要使用数组吗?数组呢,构造方法并没有看到?
  2. 阈值threshold咋也必须是2的幂次方了?

好,带着疑问我们接着往下看。

04

初始化

计算HashMap对象已经有了,那我们肯定是要向里面放数据。我们看下put方法。

/**
* put操作直接调用了 putVal
* 计算了要放入key的hash值
*/
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
* 计算key的hash值
*/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
* 实际插入值方法
* 先判断数组是否为空 为空则调用resize()
* 再次看到了resize()
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断Node数组是否为空 为空则进行初始化
    if ((tab = table) == null || (n = tab.length) == 0){
        n = (tab = resize()).length;
    }
    ...
}

关于hash(key)的实现,通过百度了解,主要是为了有更好的分散,减少hash碰撞。想了解可自行百度了解。接下来让我们看看resize()。

/**
* 实现数组的创建和扩容
*/
final Node<K,V>[] resize() {
    //赋值操作 oldTab为原数组
    Node<K,V>[] oldTab = table;
    //原数组长度 为空则为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //赋值原扩容阈值
    int oldThr = threshold;
    //定义新数组长度和扩容阀值
    int newCap, newThr = 0;
    //如果原数组长度大于0 那么初始化应该不走这里
    //扩容的时候会进入到这里计算新的数组长度和扩容阀值
    if (oldCap > 0) {
        //已经达到数组的最大长度值 不再进行扩容 直接使用旧数组
        if (oldCap >= MAXIMUM_CAPACITY) {
            //扩容阀值赋值最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        //数组容量直接扩大为原先的两倍 且小于最大容量 而且旧数组的容量也大于等于最大容量
        }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY){
            //使用率阈值也扩大为原先的两倍
            newThr = oldThr << 1; // double threshold
        }
    //如果原扩容阀值大于0 则赋值新数组长度为扩容阀值
    //数组初始化 对应的是含有容量的构造方法
    //这也是为什么要在构造方法中计算阀值是2的幂次方
   //疑问b解决了
    }else if (oldThr > 0){ // initial capacity was placed in threshold
        newCap = oldThr;
    //如果不是 那就采用默认参数
    //无参构造方法走这里
    }else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //含有容量额构造方法会走这里
    if (newThr == 0) {
        //计算新的扩容阀值
        float ft = (float)newCap * loadFactor;
        //判断是否达到最大值 是的话就返回最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    //赋值新的扩容阀值
    threshold = newThr;
    @SuppressWarnings({"unchecked"})
    //终于看到我们的Node数组实例化了
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //赋值给成员变量
    table = newTab;
    //其他操作暂时省略
    ...
    return newTab;
}

疑问解决:

  1. 构造方法除去直接以Map为参数外,其他的都没有进行数据的实例化。而Node<K,V>数组的实例化是在第一次放值的时候进行的。
  2. 阈值threshold在初始化数组中直接赋值为数组的长度,所以必须是2的幂次方。而为什么数组长度是2的幂次方,个人理解,计算机是二进制,采用2的幂次方能够使用一些&等快速操作,就是为了速度。

初始化总结:

  1. 数组创建延迟到了第一次放数据的时候
  2. 无参构造方法采用的是默认长度16的数组和使用率0.75
  3. 有容量构造方法传入的容量是通过扩容阀值进行中间传递赋值的

HashMap的初始化就学习到这里,想继续跟随我学习了解HashMap的其他源码,就给加个关注、点个在看。

记得关注哦,关注木左不迷路,木左带你上高速!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 木左侃技术人生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档