源码解析系列:HashMap(1) - 常量和一些工具方法


前言

最近开始学HashMap,该系类文章对HashMap的源码进行一句一句解析,会基本包括HashMap的关键的方法,在其中有一些比较特殊的方法的时候也会单独注释讲解。本文基于jdk1.8进行源码解读。 学习的视频地址:https://www.bilibili.com/video/BV1LJ411W7dP?from=search&seid=4711005967875614637&spm_id_from=333.337.0.0

第一篇文章:源码解析系列:HashMap(1)
第二篇文章:源码解析系列:HashMap(2)
第三篇文章:源码解析系列:HashMap(3)
第四篇文章:源码解析系列:HashMap(4)
第五篇文章:源码解析系列:HashMap(5)

1. 源码前的准备

1.1 HashMap的存储结构

数组+链表+红黑树
使用红黑树是为了在长度足够长的时候提高查找效率,在链表足够长的情况下,查找效率和插入效率都会减低。红黑树又类似于ALV树,会不断平衡调整。但红黑树对比ALV树又有几个优势
1、红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。
2、红黑树对平衡调整的次数比ALV要少,红黑树根据颜色进行调整,达到一种不完美的平衡,而ALV严格根据高度来调整。每次增删都有可能造成树的调整,效率比红黑树低
在这里插入图片描述

1.2 Hash和Hash碰撞

hash(散列、杂凑)函数,是将任意长度的数据映射到有限长度的域上。映射的规则就是对应的hash算法,而原始数据映射后的二进制串就是哈希值。
Hash的特点:

  1. 从hash值不能反向推导出原始的数据。
  2. 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值。
  3. 哈希算法的执行效率要高效,长的文本也能快速地计算出哈希值。
  4. hash算法的冲突概率要小

由于hash算法的最终目的是将输入的值转化成为一定空间内的hash空间值,而hash空间值远远小于输入的值,所以就有可能发生hash碰撞。而由于hash算法中特殊的运算公式,所以必然会存在不同的hash值经过计算得到相同空间地址的情况。当一个结点的hash值经过计算得到了在空间中的存储位置的时候,就要考虑存储结构的变化,是以什么样的存储结构来存储才能使得查询等操作效率更高。

1.3 HashMap对hash碰撞的处理

当有两个或者两个以上的结点处于同一个下标的时候,会先形成链表,在长度大于8的时候进行树化成红黑树。在键值对数量大于初始容量*负载因子的时候进行扩容(默认不指定参数情况下)
为什么长度是8?

 	 * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006

上面数字是官方给出的在HashMap源码中,理想情况下,在随机hashCodes下,频率容器中的节点遵循泊松分布。随着结点越来越多,
可以观察到当下标是8的时候,概率已经是0.00000006了,这时候的概率是非常小的,几乎为不可能事件。所以在0-7,也就是8个的
时候使用树化是最好的。也就是当插入第9个元素的时候开始树化。

为什么负载因子是0.75?
HashMap就是一个空间换时间的存储结构,对于HashMap有插入慢查询快的特征。对于加载因子总是要维持一个平衡的状态,如果太高了,比如1.0,就会导致空间利用率变大的同时冲突概率也变大了,从而查询时间成本也提高了。而太低了,比如0.5会导致空间利用率减低的同时查询效率提高,但是太低导致频繁的扩容操作也会浪费很多存储空间。所以为了寻求折中,定义了0.75作为负载因子。当然这里只是简单说一下,要具体了解还得查更多资料。


2. HashMap的基本参数


    /**
     * 初始容量大小:就是HashMap的桶的个数,也就是数组长度
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量,这个是扩容阈值最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 负载因子,默认0.75,当然也可以自己指定
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 树化条件,链表个数大于8进行扩容
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树数量结点个数降到6的时候开始红黑树转化成链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 最小树化容量大小,当hash表的table的长度达到64的时候才可以升级成树,尽管长度大于8,如果数组长度没达到64,还是链表
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
  		结点
     */
    static class Node<K,V> implements Map.Entry<K,V> {
    	//hash值
        final int hash;
        //泛型key
        final K key;
        //泛型value
        V value;
        //下一个结点
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
	
	//桶
	transient Node<K,V>[] table;

    //keys和values
    transient Set<Map.Entry<K,V>> entrySet;

    //键值对的数量,用来和threshold对比的
    transient int size;

    //修改次数,替换不算,增加和删除会+1
    transient int modCount;

    //阈值,也就是容量*负载因子
    int threshold;

    //负载因子
    final float loadFactor;


3. HashMap的一些基本的方法

3.1 hash

static final int hash(Object key) {
 	 int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

hash方法使用了key的hashCode方法来和高16位进行异或, >>> 表示无符号移位,也是逻辑位移。作用,让key的hash值得高16位也参与路由运算。
假设:hashcode=0b 0010 0101 1010 1100 0011 1111 0010 1110,进行异或运算:
   0b 0010 0101 1010 1100 0011 1111 0010 1110
^ 0b 0000 0000 0000 0000 0010 0101 1010 1100
=>    0010 0101 1010 1100 0001 1010 1000 0010

为什么需要这样?
我们在通过hash求数组下标的时候使用的方法是: (length-1) & hash = index。
当数组长度很短的时候,比如初始值16,那么按照32位来进行位与运算。就是0000 0000 0000 0000 0000 0000 0000 1111和hash值进行位与的运算,但是这时候我们发现15的前28位都是0,不管hash值是什么,结果只看后四位,因为前面28位一定是0,那么这样不好的一点就在于,比如一个0101 0000 0000 0000 0000 0000 0000 1011的hash和一个0000 0000 1111 0000 0101 1110 0101 1011的hash,两个hash虽然值有很大不同,但是由于后四位相同,就造成了求出的下标相同。这样高位的28位就失去了意义了。所以我们在求hash值的时候把hashCode的高16位也用上可以减少冲突的概率。
总结目的:1. 增加散列度        2. 减少冲突


3.2 comparableClassFor

作用:如果一个对象实现了Comparable接口,就返回这个对象的类,否则返回null。

static Class<?> comparableClassFor(Object x) {
	//如果x实现了该接口
    if (x instanceof Comparable) {
    	  //c就是x的运行时类
          Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
          //如果是String类,直接返回String,因为String实现了Comparable接口,源码可查
          if ((c = x.getClass()) == String.class) // bypass checks
              return c;
          //当前对象所表示的类或接口直接实现的接口类型,不包括继承的
          if ((ts = c.getGenericInterfaces()) != null) {
              for (int i = 0; i < ts.length; ++i) {
              	  //判断下面3种情况
              	  //1. 如果是实现了ParameterizedType接口的,也就是泛型的
              	  //2. 如果泛型类的实现了Comparable接口
              	  //3. 泛型类的参数只有一个并且这个参数类型就是c
              	  //满足上面三种情况,表示了这个类是一个实现了Comparable接口并且泛型参数只有c的类,返回
                  if (((t = ts[i]) instanceof ParameterizedType) &&
                      ((p = (ParameterizedType)t).getRawType() ==
                       Comparable.class) &&
                      (as = p.getActualTypeArguments()) != null &&
                      as.length == 1 && as[0] == c) // type arg is c
                      return c;
              }
          }
      }
      //没有就返回null
      return null;
  }

在上面的方法中,其中有几个陌生的方法和类
1、getGenericInterfaces
作用:放回当前类或接口实现的接口的类型
例子:

public class MyTest {
    public static void main(String[] args) {
        Type[] genericInterfaces = Test.class.getGenericInterfaces();
        for (Type genericInterface : genericInterfaces) {
            System.out.println(genericInterface);
        }
        //interface com.jianglianghao.myInterface1
        //interface com.jianglianghao.myInterface2
    }
}

class Test  extends myClass implements myInterface1,myInterface2{}

interface myInterface1{}

interface myInterface2{}

class myClass{}


2、ParameterizedType
类型:参数化类型
作用:我们可以通过这个类判断是不是一个泛型参数,也可以获取到泛型参数
例子:

public class MyTest {
    public static void main(String[] args) {
        for (Field declaredField : test1.class.getDeclaredFields()) {
            //getGenericType:能拿到类和参数类型
            if(declaredField.getGenericType() instanceof ParameterizedType){
                System.out.println("是参数化类型");
                Type rawType = ((ParameterizedType) declaredField.getGenericType()).getRawType();
                System.out.println("原始类型名字是:" + rawType.getTypeName());
                Type[] actualTypeArguments = ((ParameterizedType) declaredField.getGenericType()).getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    System.out.println("泛型参数是:" + actualTypeArgument.getTypeName());
                }
                System.out.println("******************************************************************");
            }else{
                System.out.println("不是参数化类型");
                System.out.println("******************************************************************");
            }
            //获取到参数化类型中的泛型参数
            //是参数化类型
            //原始类型名字是:java.util.List
            //泛型参数是:java.lang.String
            //******************************************************************
            //不是参数化类型
            //******************************************************************
            //是参数化类型
            //原始类型名字是:java.util.Map
            //泛型参数是:java.lang.String
            //泛型参数是:java.lang.String
            //******************************************************************
            //是参数化类型
            //原始类型名字是:java.lang.Comparable
            //泛型参数是:java.lang.String
            //******************************************************************
            //不是参数化类型
            //******************************************************************
            //
            //Process finished with exit code 0
        }
    }
}

class test1{
    List<String> list1; //是参数化类型
    List list2;         //不是参数化类型
    Map<String, String> map1;   //是参数化类型
    Comparable<String> comparable;  //是参数化类型
    String string;                  //不是参数化类型
}


3.3 compareComparables

@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
	//判断如果对象x是空或者x的类型不是kc,就返回0,如果是就和k进行比较返回大小
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}


3.4 tableSizeFor

JDK1.8

属于HashMap中比较重要的一个方法,在jdk8和jdk11中的表示方法都不同。
作用:由于hashMap的长度只能是2的n次方倍,该方法作用就是返回一个>=cap的2的n次方的数

// >>> 无符号位移,前面统一补0
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;
      //只有n开始等于0的时候才小于0
      return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  }

原理:对于或运算,只要有一个1,结果就是1,该方法的最终目标是从最高位往右数第一个1开始,后面的数全部变成1,比如对于一个数0000 0000 0000 0000 0000 0000 1000 1101,141。运行该方法之后就会变成0000 0000 0000 0000 0000 0000 1111 1111,255。 我们假设一个数n为0000 0000 0000 0000 01xx xxxx xxxx xxxx, 我们先忽略第一行cap - 1,从第二行开始运行n的或运算。第一次,右移一位,此时的n1 = 0000 0000 0000 0000 001x xxxx xxxx xxxx, 运行或运算,我们就得到了两个1,0000 0000 0000 0000 0011 xxxx xxxx xxxx,再右移两位,变成 0000 0000 0000 0000 0000 11xx xxxx xxxx, 此时或运算我们得到了四个1,0000 0000 0000 0000 0011 11xx xxxx xxxx。以此类推,等到运行到最后个16的时候,就能得到我们想要的结果,1+2+4+8+16=31,容量足够大。图解如下:
在这里插入图片描述
最后说下为什么开始需要-1,比如开始是0000 1000,如果不-1直接位移或运算,得到的结果就是0000 1111,再+1,就变成了0001 0000,不符合我们的要求。可以自己算下。而对于平常的254,253等数,尽管-1,最终得到的结果也是256,不会因为-1就导致结果有什么区别。


JDK11

扩展:jdk8使用这些方法使得效率提高了不少,而jdk11中的tableSizeFor方法和jdk8又有所不同,下面是jdk11的实现。JDK11的实现基于Integer的numberOfLeadingZeros方法,该方法可以获取到从高位往右数第一个1之前的0的个数。获取到之后再使用-1进行右移0的个数位。比如一个数 0000 0000 0000 0000 0000 0100 0000 0110。-1的补码是1111 1111 1111 1111 1111 1111 1111 1111(计算机存储二进制数使用补码形式存储),求出该数的0的个数是21个,把-1往右移位21位,得到0000 0000 0000 0000 0111 1111 1111,就是我们要求的个数。下面就是具体的实现细节,核心就是怎么求出0的个数。

 static final int tableSizeFor(int cap) {
   int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
     //1073741824就是HashMap默认的最大存储容量
     return n < 0 ? 1 : (n >= 1073741824 ? 1073741824 : n + 1);
 }
 
 //核心方法,其中核心思想就是我们用全部0的个数减去从高位往右数第一个1开始后面的个数
 //比如0000 0001 0011 0001 0011 0101 1111 1111,我们的思想就是用31减去从第一个1(25)开始往后数的24个数,31-24=7
 public static int numberOfLeadingZeros(int i) {
   //如果i <= 0;同样返回32个0
   if (i <= 0) {
        return i == 0 ? 32 : 0;
    } else {
    	//定义n为31,为什么是31?因为上面i ==0的情况已经返回了,所以最小是1,也就是说极端情况下,前面最多31个0
        int n = 31;
        //65535 = 2^16
        //如果i >= 65536,那么极端情况下第17个是1,我们用全部的0减去后面的16个数
        if (i >= 65536) {
            n -= 16;
            i >>>= 16;
        }
		//256 = 2^8
		//如果i >= 256,那么极端情况下第9个是1,我们用全部的0减去后面的8个数
        if (i >= 256) {
            n -= 8;
            i >>>= 8;
        }
		//16 = 2^4
		//同上
        if (i >= 16) {
            n -= 4;
            i >>>= 4;
        }
		//4 = 2^2
		//同上
        if (i >= 4) {
            n -= 2;
            i >>>= 2;
        }
		//到这一步
		//一点只有三种结果 00 01 10 11
		//这里要向右移位一次,如果你理解了上面为什么要设置极限情况,这里就容易理解。因为到了这里,一定是高位为上面这四种情况,
		//这时候我们要减去第一个1开始的后面的0或1的个数,对于00和01,因为没有1或者1是最后面的,所以移位之后也是0,而10和11,
		//第一个1后面是0和1,所以我们移位一次,减去1.
        return n - (i >>> 1);
    }
}

图解如下:
在这里插入图片描述



4. 四个构造方法

1、自定义容量和负载因子
2、自定义容量,使用默认负载因子0.75
3、使用默认的容量和负载因子
4、通过Map和默认负载因子初始化

注意:初始化容量和阈值是在put方法调用才初始化,懒加载

 //1、自定义容量和负载因子
 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或者负载因子不是一个非法数,比如超过最大浮点值或者1.0/0.0这种
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
        //赋值负载因子                                    
        this.loadFactor = loadFactor;
        //调用方法,阈值要=2的n次方
        this.threshold = tableSizeFor(initialCapacity);
    }

    //自定义容量,使用默认负载因子0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //使用系统默认,在put方法的时候会初始化
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    //使用map
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
	//添加Map的键值对
	final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
	    //获取m的键值对大小
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                //s / loadFactor求出比s大的容量,最终目的是2的n次方
                float ft = ((float)s / loadFactor) + 1.0F;
                //如果容量小于最大容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                		 //取整数或者赋值最大容量
                         (int)ft : MAXIMUM_CAPACITY);
                //如果t比当前的阈值要大,一开始初始化这里为0
                if (t > threshold)
                	//调用tableSizeFor赋值,赋值后阈值为2的n次方
                    threshold = tableSizeFor(t);
            }
            //如果键值对比threshold要大
            else if (s > threshold)
            	//重新分配空间
                resize();
            //调用putVal方法一个个加入
            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);
            }
        }
    }





如有错误,欢迎指出!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值