HsahMap的入门讲解

一、什么是HashMap?

1、了解Map集合

        Map是一个双列集合(以键值对形式存储),键(key)不可以重复,而值(value)可以重复。

2、基本概念

        HashMap基于hash表的Map接口的非同步实现,此实现提供所有可选的映射操作。根据键的hashCode值存储数据,具有很快的访问速度。HashMap最多只允许一条记录的键为null,允许多条记录的值为null。不保证该顺序恒久不变。

3、数据结构

        在JDK1.7及以前是数组和链表,在JDK1.8后对HashMap的底层优化后加入红黑树。 优化方式是:当链表长度大于阈值(8)的时候,将链表转化为红黑树,以减少搜索时间。

        红黑树是一种自平衡二叉查找树(二叉排序树)。

         特点:1)每个节点要么是黑色, 要么是红色。2)根节点是黑色。3)每个叶节点(Null,空值)都是黑色。4)每个红色节点的两个子节点一定都是黑色(但黑色节点的子节点可以也是黑色)。5)任意一个节点到每个叶子节点的路径都包含相同数量的黑节点。

        总结:根黑叶黑,红不相邻,黑色平衡。

二、存储过程和扩容机制

1、源码分析

属性

//默认初始容量—必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,如果一个较大的值由带参数的构造函数中的任何一个隐式指定,则使用该值。
//必须是2的幂<= 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;
//在构造函数中没有指定时使用的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//使用树而不是列表的桶计数阈值。 当将元素添加到至少有这么多节点的bin中时,bin将被转换为树。 
//该值必须大于2且至少为8,以便与树移除中关于收缩后转换回普通箱的假设相匹配
static final int TREEIFY_THRESHOLD = 8
//可将容器树形化的最小表容量
static final int MIN_TREEIFY_CAPACITY = 64;

//在第一次使用时初始化,并根据需要调整大小。在分配时,长度总是2的幂
transient Node<K,V>[] table;
//哈希表的负载因子
final float loadFactor;
//要调整大小的下一个大小值(容量*负载因子)
int threshold;

方法

//存储数据
public V put(K key, V value);
//主要是对哈希值进行二次运算,快速失败(modCount),
//与原有threshold进行比较调用resize方法的过程。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict);
//扩容机制
final Node<K,V>[] resize();
//根据key删除HashMap数据
public V remove(Object key);
//清除HashMap中所有的数据,置为null值
public void clear();

2、存储和扩容

        在存储Map数据使用put方法,先计算元素的哈希值,再通过putVal方法进行二次运算,使得结果随机散落在桶中。桶中有多个数据时会以链表的形式存储,当链表大于阈值时,会进行扩容,以红黑树的形式存储。扩容之后,将HashMap中的所有的元素重新计算哈希值。

        put方法用于存储数据,存储时会用hash函数计算key,获得hash值。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

        putVal方法主要是对哈希值进行二次运算,快速失败(modCount),与原有threshold进行比较调用resize方法的过程。

        resize方法有两个作用:

        1)初始化HashMap

final Node<K,V>[] resize() {
		//oldTab存储HashMap扩容前的旧值(若运行resize()前并未初始化,则table = null)
        Node<K,V>[] oldTab = table;
        //旧值数组的长度(HashMap是数组+链表或者数组+红黑树)
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //旧值的阈值(阈值,当hashMap的数组长度到达阈值时,需要进行扩容)
        //(若运行resize()前并未初始化,则threshold为HashMap桶容量
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//已经初始化过(现在HashMap当中有值),则进行的是扩容操作
            if (oldCap >= MAXIMUM_CAPACITY) {//容量已经最大,无法扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //1.将旧值容量进行扩容(使用向左一位(旧容量乘以2)) 
            //2.若扩容之后的容量满足小于最大容量并且旧容量值大于小于默认的容量(16)
            //新的阈值则为旧阈值扩大一倍(必须满足这两个条件)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

		//若没有经历过初始化,并且在使用时通过构造函数指定了initialCapacity, 
        //则table大小为threshold, 即大于指定initialCapacity的最小的2的整数次幂
        else if (oldThr > 0) 
            newCap = oldThr;
        else {
        //若没有经历过初始化,并且没有通过构造函数指定initialCapacity,
        //则赋予默认值(数组大小为16,加载因子为0.75)
            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;

        2)当容量的大小到达阈值时进行扩容,

        //根据新的容量重新定义一个数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //将新建立数组赋值到HashMap成员变量
        table = newTab;
        //若存在旧数据则进行后续数据迁移
        if (oldTab != null) {
        	//因为HashMap是数组+链表或者数组+红黑树
            //(根据数组下标找寻到相应链表或者红黑树)
            //遍历原本数组,找到相应的链表或者红黑树进行操作
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //数组[j]有数据,进行后续操作
                //将数组[j]下代表的链表或者红黑树的根节点赋予e
                if ((e = oldTab[j]) != null) {
                //将旧数组[j]处置为空
                //将原本数组下标处的数据从原本位置挪出
                    oldTab[j] = null;
                    if (e.next == null)
                    //就只有根节点直接就可按照e.hash & (newCap - 1)的计算法
                    //计算出相应在新table里的位置进行插入
                    //与put方法里面的操作一致
                        newTab[e.hash & (newCap - 1)] = e;
                    
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                    	//首先定义五个变量
                    	//loHead(头部)  loTail(尾部) 
                    	//hiHead(头部)  hiTail(尾部) 
                        //next
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //do while遍历整个链表
                        do {
                            next = e.next;
                            //进行lo相关变量的操作,不符合进行hi相关变量的操作
                            if ((e.hash & oldCap) == 0) {
                                //将一个节点插入链表所需要做的操作
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                //同lo相关操作
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        //根据(e.hash & oldCap) == 0 这个条件进行筛选,
                        //将原本的一个链表划分成两个链表
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

三、哈希及哈希冲突

1、概念        

        哈希也称作散列。可以是一种数据结构,也可以是一种函数概念。

        哈希值,把任意长度的输入,通过Hash算法变成固定长度的输出,该输出就是哈希值。

        哈希冲突,两个不同的输入值(key),根据同一哈希函数据算出相同的哈希值的现象。

2、常用的解决哈希冲突的方法      

        1)闭散列(开放定址法)

        当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。找寻方法:若没有冲突则直接插入值,若有冲突则向后查找至空位插入值。

        弊端:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”(查找时会多次重复比较,大大降低查找效率)。可以通过平方的跳跃来解决弊端,但是会造成空间利用率低的缺陷。

                        

        2)开散列(链地址法) ---HashMap采用的解决方法

        首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

        出现了极端的情况,所有的数此时都冲突到一个桶中,可以将此时的链表改换挂红黑树。

                               

    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值