java之HashMap数据结构之源码分析

此篇文章基于java jdk8,作者在学习过程中总结的知识点,尽量严谨表述,如有错误,望指正

一、散列表的引入

首先看两种简单的数据结构

数组:查询快;增删慢

链表:查询慢,但是增删快

hash表(散列表的引入):

是数组+链表,综合了上述两种数据结构的优点,

什么是hash?什么是hash碰撞?

hash也称为散列,其原理就是把任意范围的输入经过映射转换成固定范围的输出,而这个映射规则就是hash算法,映射后的值的二进制串就是hash值;不同的输入值,得到的hash值一样,就是hash碰撞

hash的特点:

1、从hash值不能反向推导出原数据

2、相同的输入,对应同一个hash

3、hash算法的效率要高,对于大的数据也能很快的计算出hash值

4、hash算法的冲突概率要尽可能的小,即使数据尽可能均匀的分布。

二、HashMap的原理

先弄清楚了原理,最后分析源码会简单很多 

HashMap里面存放的key-value 对,其中key唯一,即不能有两个key是同一个对象,但是存放key的时候,还应该关注key的每一个属性(所以即使两个不同的对象,它们的所有属性都一样,我们都需要认为它是两个同样的对象,不能使它存放进来,所以需要重写存放的key对象的类的equals方法和hashcode方法(使它们和自身属性关联起来))

HashMap的底层结构图:

HashMap的结构图中,可以得知HashMap是由数组+链表+红黑树构成(当每个桶位的元素达到8个以上时,则有可能转为红黑树,实际转化红黑树中,还要看HashMap的容量是否达到64)

什么是链化:

数组的每一个索引位置,我们称为一个桶位,同一个桶位,已经存了一个元素后,后面的元素链接到第一个元素,这样就形成一个链表,这就是链化;

为什么要引入红黑树

当一个桶位,链化严重,(即形成的链表过长),链表的缺点是查询慢,链表过长,会使得查询效率变低,这时链表会转化成红黑树(本篇文章不分析具体是如何转变成红黑树,只需知道什么时候需要转换成红黑树即可)

Node结构分析:

为什么要分析Node,因为HashMap中有一个table属性用来存放我们put进去的元素,它是一个Node数组,我们的元素就是以Node形式存放的

transient Node<K,V>[] table;

Node是定义在HashMap中的一个内部类,主要关注以下几个属性,可知

1、其中除了存放key 和value,还有一个hash,这个hash值是key的hashcode返回值经过hashmap的hash方法得到的一个值,(注意这个hash方法不是之前提到的hash算法的体现,hash方法的作用在后面源码分析再解释,hash算法的体现实际是写在put方法内部)

2、其中hash和key都是final修饰,证明一旦创建Node,存的key对象和hash值都是不可变的(也就是说,一个元素存放到HashMap后,即使后期修改key这个对象里面的属性值,使得hashcode值发生改变,也不影响这个Node里面存放的hash值)

3、next属性也是一个Node类型的,用于存放下一个Node元素,这表示每个桶位中存放多个元素时是以单向链表的形式存放;

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

              ...//省略后面代码
        }

put()方法原理分析:

1、首先,对key的hash值(这个hash值是已经调用hash()方法处理过的)进行处理,得到一个[0 ,数组长度-1]的值,这就是hash算法的体现,得到的这个值就是将要存放元素的桶位位置索引

2、找到桶位后,如果桶位没有存元素,则把元素放到桶位,如果桶位有元素,则将要存放的元素和桶位里及其后面链接的元素的key逐个对比,如果没有相同的,则将待存放元素链接到最后面,有key相同的元素,则将待存放元素的value值进行替换

扩容的原理:

当我们存放的元素过多时,这时hash碰撞的概率增加,桶位下面链接的元素也会增加,查询效率变低,这时就需要我们将数组大小扩容为之前的两倍;

什么时候扩容呢?

存放的元素大于数组的长度*负载因子,负载因子的默认值是0.75,也可以调用构造器时,自己指定

三、HashMap的源码分析:

1、HashMap里面常用的属性,方法说明;

//--------------------方法说明-----------------------
static final int hash(Object key)                         //用于处理key的hashcode值得到一个hash值

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);   //put方法内部调用putval方法,我们真正需要分析的也是putval的源码
}
final Node<K,V>[] resize()                               //扩容方法,当数组需要扩容时,调用此方法

//  ----------------------常量说明--------------------------

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4       //默认的初始化数组容量
static final int MAXIMUM_CAPACITY = 1 << 30;            //系统允许的最大数组容量
static final float DEFAULT_LOAD_FACTOR = 0.75f          //默认加载因子
static final int TREEIFY_THRESHOLD = 8;                  //树化阈值 和 最小树化容量阈值,一起控制链表转换成红黑树的值
static final int UNTREEIFY_THRESHOLD = 6                 //解树化阈值,当桶位下的元素低于这个值时,由红黑树变成链表
static final int MIN_TREEIFY_CAPACITY = 64;          //最小树化容量阈值,

//------------------------属性说明-----------------------

transient Node<K,V>[] table;                           //元素存放的数组
transient int size;                                   //已经存放的元素个数
transient int modCount;                            //对hashmap进行修改的次数总和,增加,删除元素,都会增加这个值,修改不会
final float loadFactor;                             //加载因子,乘以数组长度得到扩容的阈值。
int threshold;                                      //需要扩容的阈值,默认是数组长度*加载因子

2、构造器说明:

2.1、new HashMap (initialCapacity ,  loadFactor) ;

// 传入需要new的数组容量大小,指定加载因子
public HashMap(int initialCapacity, float loadFactor){

                    ...

                this.loadFactor = loadFactor;

                this.threshold = tableSizeFor(initialCapacity);
            
                    ...

}

这个构造方法里面没有初始化table,只是调用一个tableSizeFor方法得到容量值赋值给threshold(threshold本来是存放扩容阈值的,但是这里存放的是table的长度,因为,第一次调用put方法时,如果table没有初始化,会调用resize方法初始化table,这里面会根据threshold值创建数组大小)

2.2、public HashMap(int initialCapacity)

(嵌套调用上面的构造器)

 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

2.3、public HashMap()

(啥也没做,只是给负载因子赋上默认值)

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }

3、tableSizeFor方法源码分析:

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

这个方法,经过一些列的位运算,得到一个大于或等于cap的最小2的次幂值,比如,如果cap是8,则得到8,如果是9,则得到16;所以当调用一个构造器指定创造数组容量为9时,实际这个数组在第一次扩容时会创建一个大小为16的数组;

4、put()方法源码分析(重点)

put方法里面调用putval方法,我们实际分析的是putval方法:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

5、resize()方法源码分析(重点)

6、get方法分析

7、remove方法分析

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值