HashMap源码解析(存储结构)

最近终于来的及看一遍hashMap了,不过注意的是主要还是看其他人文章还有借鉴以前看过的视屏记忆来学习的,在此不得不说网上博客写的很好。
https://www.cnblogs.com/tuyang1129/p/12362959.html

一、HashMap的底层结构

结构图

HashMap的结构
jdk1.7:由数组+链表组成的,以数组为主体,链表来解决哈希冲突(两个对象调用的hashCode方法计算的hash值一致导致计算的数组索引值相同)。
jdk1.8:在链表足够长了以后,及链表长度大于阈值(默认为8),并且当前数组长度大于64时,此时索引位置上的所有数据改为使用红黑树存储。
因为红黑树会进行左旋、右旋、变色操作来保持树的平衡,会影响效率,在数组长度小于64时,相对而言搜索时间更短。

  • 数组
 /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
//
/**
   *该表在首次使用时初始化,并根据需要调整大小。
   *分配时,长度始终是2的幂。
   *(在某些操作中,我们还允许长度为零,以允许使用当前不需要的引导机制。)
   */
transient Node[] table;

可以看到这个桶数组是一个Node类型,我们可以查看这个Node的数据结构:

 /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
 static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;

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

        public final K getKey()        { ...}
        public final V getValue()      { ...}
        public final String toString() {...}
        public final int hashCode() {...}
        public final V setValue(V newValue) {...}
        public final boolean equals(Object o) {...}
}

看到这个是不是有一种很熟悉的感觉,熟悉的键值对,实现了Map.Entry接口,上面的结构图中的Entry指的就是我们的Node对象。
在HashMap中,我们使用链地址法来处理哈希冲突,并且使用hash算法和数组的扩容机制来减小哈希碰撞的概率和数组的占用空间
在了解hash算法以前,我们先来了解一些基本的信息:

  public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
 final Node getNode(int hash, Object key) {
 Node[] tab; Node first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) 
...
}

在HashMap中的get方法中,有这样一串代码,简化一下就是

int len = table.length;
Node first = hash(key) & (len -1)

大家都知道HashMap定位数据在数组的位置是利用该数据的hash值来对数组容量直接取模,但是在这里使用的是与运算取模
其实这里是使用与运算来替代了取模运算,但是这里是有限制条件的,那就是HashMap的数组容量必须是2的次方,我们以数据的hash值为23,数组容量为8来举例。
23的二进制:        00010111
8-1=7的二进制:00000111
可以看到,因为len是2的n次方,那么len-1的二进制则会是末位n个1,其余位都为0,我们知道一个数与1做与运算会将数据保留,而与0做与运算则会将数据置0,与数据做与运算则会将数据的前n位保留下来,而其他位会被置0,那我们知道对数据取模就是除到最后取剩下来的数,而对2的次方进行除法时就相当于进行移位操作,同理比2的次方小的数就都会被余下来,以此来代替取模操作。

//table长度的默认值
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//table长度的最大值
static final int MAXIMUM_CAPACITY = 1 << 30;

在HashMap中,保证了存储元素的数组的大小一定是2^n,所以在内部,通过hash值与数组容量取余的操作,都用上面说的与运算取代了。这样做的好处是,与运算直接操作内存,效率极高,而在HashMap中,获取数组下标是一个非常频繁的操作,无论是get还是put都要用上,所以这种优化对HashMap的查询效率有很多的提升。在HashMap中,有两个静态变量,分别是默认初始容量和最大容量,可以看到,它们都是都是2的n次方,而且没有直接写成数字,而是一个移位公式,如 1 << 4,就是为了提醒大家HashMap的容量机制。
这里有一个问题:HashMap中是有构造器的,我们在使用HashMap的时候是可以为其指定容量的,如果我们指定的容量大小不是2^n,就可能会破化这种机制
:这个问题其实很简单:虽然我们可以指定HashMap的初始容量,但是不代表我们会直接使用它,当我们指定一个初始容量时,会根据这个容量计算出第一个大于等于这个容量且大小为2^n的数,如果这个数大于HashMap运行的最大值,则直接使用最大值

其实,使用位运算代替取模运算,除了性能之外,还有一个好处就是可以很好的解决负数的问题。因为我们知道,hashcode的结果是int类型,而int的取值范围是-2^31 ~ 2^31 - 1,即[ -2147483648, 2147483647];这里面是包含负数的,我们知道,对于一个负数取模还是有些麻烦的。如果使用二进制的位运算的话就可以很好的避免这个问题。首先,不管hashcode的值是正数还是负数。length-1这个值一定是个正数。那么,他的二进制的第一位一定是0(有符号数用最高位作为符号位,“0”代表“+”,“1”代表“-”),这样里两个数做按位与运算之后,第一位一定是个0,也就是,得到的结果一定是个正数。

//默认负载因子值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//此映射中包含的键-值映射数。
transient int size;
//记录hashMap对象的修改次数
transient int modCount;
//要调整大小的下一个大小值(容量*负载系数)
//table.length()*loadFactor
int threshold;
//哈希表的负载因子。(默认为0.75)
final float loadFactor

当size > threshould时,会进行数组扩容(resize()方法,具体在下一节讲)

二、hash方法

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

HashMap中的hash方法在get、put都有用到,这个代码唯一的一个点就是用key的hashCode值和key的hashCode值不带符号右移16位得到的数进行了异或运算。
为什么要进行异或运算而不直接使用key的hashCode值,这其实和我们上面讲的取模有关:
假如有两个数的hashCode值:

0111 1101 0011 1111 1011 1001
0000 0000 0001 0001 0001 1001

我们对其进行数组容量为16的取模运算,会发现结果相同,我们会发现问题就很大了,因为两个相差这么大的数得出的结果居然相同,因为使用这种方法基本只看数的低位而忽略了高位,会造成hashMap的散列不均衡。
为了避免这种情况,我们需要将数据的高位也考虑进来,由于int类型的数据时32位的,所以我们折中取16位,进行移位异或操作
作用:可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

三、链表转换红黑树

//当链表的长度大于8且数组的长度大于64以上时会将链表转换位红黑树
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;

/**
   *在进行resize扩容时(此时HashMap的数据存储位置会重新计算)
   *在重新计算存储位置后,当原有的红黑树内节点数量<6时,会将红黑树转换成表。
   */
static final int UNTREEIFY_THRESHOLD = 6;

你可能感兴趣的:(HashMap源码解析(存储结构))