HashMap 细品底层源码(一)

常量参数及构造方法剖析

前言

作为一名java程序猿,与java都是老伙计了,在运用方面,相信读者都很熟练,然对其原理,有不少读者就很少探究,笔者也看过不少文档和视频,但终究还认为绝知此事要躬行。

常量参数概览

相信大多读者都了解就hashmap的底层结构,笔者也简单的介绍了一下。hashmap是一个k,v对存储的数组加链表结构,hashmap的长度即为数组的长度。本文是基于jdk1.8而写的,关于1.7的底层与1.8的差异,笔者简单概述一下,1.8相较于1.7最大的差异,底层的链表超过长度为8的时候,会可能转换成红黑树。

	// hashmap的默认长度16,1左移四位即为16
     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    // hashmap的最大长度,即2的30次方
	 static final int MAXIMUM_CAPACITY = 1 << 30;
	 // hashmap的默认负载因子,hashmap扩容时判断,
	 // 当前hashmap的数据长度达到了(总长度*负载因子),即进行扩容.
	 // 如hashmap总长度为16,即hashmap存储数据长度达到16*0.75=12的时候	 
	 // hashap将会自动进行扩容
	 static final float DEFAULT_LOAD_FACTOR = 0.75f;
	 // 链表转换树的长度阈值,即超过这个长度,链表将转换成红黑树
	 static final int TREEIFY_THRESHOLD = 8;
	  // 链表扩容阈值,即链表长度超过6的时候,会考虑优先扩容而不是转换成红黑树
	 static final int UNTREEIFY_THRESHOLD = 6;
	  // 链表转换成树的hashmap阈值,即hashmap.size()总长度超过64,才有机会将链表转换成红黑树
	 static final int MIN_TREEIFY_CAPACITY = 64;

构造方法概览

// 最常使用之一,默认无参构造,负载因子more的0.75,长度为初始化默认的16
 public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    // 最常使用之一,带整型参数构造,指定初始化长度
      public HashMap(int initialCapacity) {
      // 调用同名双参数构造方法
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
// 双参数型构造方法,指定负载因子以及初始化长度
 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
           // 参数长度是否超过hashmap的最大长度,2的30次方,超过即给定hashmap最大长度
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
            // 检查负载因子的值是否有效
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        // 计算hashmap的下次扩容的阈值,即达到阈值开始进行扩容
        this.threshold = tableSizeFor(initialCapacity);
    }

关于tableSizeFor(initialCapacity) 简单来讲就是返回到大于等于initialCapacity的最小的2的幂

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

方法主要涉及到移位以及或操作。简要剖析一下,比如说n 的值为01xx xxxx.
首先进行无符号右移一位操作,高位补0.即001x xxxx。在进行位或运算
001x xxxx
01xx xxxx 或运算
011x xxxx,即n |= n >>> 1; 的值为011x xxxx。
同理,将n右移两位,0001 1xxx,进行或运算
0001 1xxx
011x xxxx 或运算
0111 1xxx, 即n |= n >>> 2; 的值为0111 1xxx。
继续,右移四位, 0000 0111,进行或运算
0000 0111
0111 1xxx 或运算
0111 1111 即n |= n >>> 4; 的值为0111 1111。
相信读者看出来,每进行一次操作,最高位的1hou的xx陆续变成1,最终结果就是让高位为1的后面所有位数的值变成1,最后在进行+1运算。算出最接近参数且大于或等于参数的二次幂。
至于为什么n = cap -1.读者不妨实际计算一下,如果n直接等于cap,假如cap的值等于8.即0000 1000
0000 0100 右移一位
0000 1000 或运算
0000 1100 结果。
继续操作
0000 0011 右移二位
0000 1100 或运算
0000 1111 结果
继续
0000 0000 右移四位
0000 1111 或运算
0000 1111 结果
接着
0000 0000 右移八位
0000 1111 或运算
0000 1111 结果
最后
0000 0000 右移16位
0000 1111 或运算
0000 1111 结果
最终结果进行+1运算即为0001 0000 即值为16,而我们的预期值为大于或等于参数的二次幂,而参数8本身就是2的三次方,所以与预期结果不符,进行减一操作运算后最终结果即为8.
关于为何只右移16位就停止的操作因为int类型只有32位,每一次右移以及或运算后都已经将32位数的值进行陆续变更为1.读者可观察之前的过程,

第一次n |= n >>> 1; 后有两个位数为1
第二次` n |= n >>> 2;`` 后有四个位数为1.
可以推断第三次就有8位,第四次就16位,第五次就32位。那是在极端状况下,是一个非常大的数。通常初始化的map也就是在几十到几百的长度。

接下来看最后一个构造方法

// 参数为map的构造方法,构造出的hashmap包含参数里map的值
  public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    // 将入参中的map的entry即成员(k,v)放入新的map
 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
        	// 判断新的map空的刚初始化还是已经存在值了
            if (table == null) { // pre-size
            //  计算当前参数的扩容阈值,去比较默认的扩容阈值
                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();
                // 循环插入到新的map
            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);
            }
        }
    }

其实putMapEntries 这个方法在hashmap中被多次调用,不只是为hashmap的构造方法服务。

后话

在本文中笔者介绍了hashmap的常量参数以及构造方法,熟悉了tableSizeFor的底层思想和计算原理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMapJava中常用的一种数据结构,它底层采用的是哈希表的实现方式。下面是HashMap底层源码分析: 1. HashMap的数据结构 HashMap的数据结构是一个数组,数组中的每个元素是一个链表,链表中存放了哈希值相同的键值对。当发生哈希冲突时,新的键值对将会添加到链表的末尾。在查找键值对时,首先根据键的哈希值在数组中定位到对应的链表,然后再在链表中查找键值对。这种实现方式的时间复杂度为O(1),但是在发生哈希冲突时,链表的长度会变长,查找效率也会降低。 2. HashMap的put方法 当向HashMap中添加键值对时,首先会计算键的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置为空,则直接将键值对添加到该位置;否则,需要遍历链表,查找是否已经存在相同的键,如果存在,则将旧的值替换为新的值;如果不存在,则将新的键值对添加到链表的末尾。 3. HashMap的get方法 当从HashMap中获取键值对时,首先计算键的哈希值,然后根据哈希值在数组中定位到对应的链表。接着遍历链表,查找是否存在相同的键,如果存在,则返回对应的值;如果不存在,则返回null。 4. HashMap的扩容机制 当HashMap中的元素个数超过数组长度的75%时,会自动扩容。扩容时,会将数组长度扩大一倍,并将原来的键值对重新分配到新的数组中。重新分配时,需要重新计算键的哈希值和在新数组中的位置。这个过程比较耗时,但是可以避免链表过长导致的查找效率降低。 5. HashMap的线程安全性 HashMap是非线程安全的,因为在多线程环境下,可能会存在多个线程同时对同一个链表进行操作的情况,从而导致数据不一致。如果需要在多线程环境下使用HashMap,可以使用ConcurrentHashMap,它是线程安全的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值