资讯专栏INFORMATION COLUMN

关于Hashmap的个人理解

Yujiaao / 3367人阅读

摘要:当容量超过容量负载因子时,进行扩容操作确定何时将冲突的链表转换成红黑树用来确何时将红黑树转换成链表当链表转换成红黑树时,需要判断数组容量。桶排序核心思想是根据数据规模划分,个相同大小的区间每个区间为一个桶,桶可理解为容器。

刚刚看到QQ群有人吹Hashmap,一想我啥都不懂,就赶快补了一波。下面分享一下我对Hashmap的理解,主要用于个人备忘。如果有不对,请批评。想要解锁更多新姿势?请访问http://blog.tengshe789.tech/

总起

Hashmap是散列表,存储结构是键值对形式。根据健的Hashcode值存储数据,有较快的访问速度。

它的线程是不安全的,在两个线程同时尝试扩容HashMap时,可能将一个链表形成环形的链表,所有的next都不为空,进入死循环;要想让它安全,可以用 Collections的synchronizedMap 方法使 HashMap具有线程安全的能力,或者使用ConcurrentHashMap 。

他的键值对都可以为空,映射不是有序的。

Hashmap有两个参数影响性能:初始容量,加载因子。

Hashmap存储结构

JDK1.8中Hashmap是由链表、红黑树、数组实现的

//用来实现数组、链表的数据结构
static class Node implements Map.Entry {
        final int hash;//保存节点的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()        { 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;
        }
    }

Hashmap构造方法

HashMap有4个构造方法。

代码:

//方法1.制定初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

//方法2.指定初始容量
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//方法三。无参构造。
     HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

//方法四。将另一个 Map 中的映射拷贝一份到自己的存储结构中来,这个方法不是很常用
    public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

Hashmap变量成员

//未指定容量的时候,数组的初始容量。初始容量是16
//为什么不直接写16?因为速度快。计算机里面要转换二进制。
//必须2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//负载因子。当hashmap容量超过 容量*负载因子 时,进行扩容操作(resize())
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//确定何时将hash冲突的链表转换成红黑树
static final int TREEIFY_THRESHOLD = 8;

//用来确何时将红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;

//当链表转换成红黑树时,需要判断数组容量。若数组容量太小导致hash冲突太多,则不进行红黑树操作,转而利用reseize扩容
static final int MIN_TREEIFY_CAPACITY = 64;

初始容量、负载因子、阈值.

一般情况下,使用无参构造方法创建 HashMap。但当我们对时间和空间复杂度有要求的时候,使用默认值有时可能达不到我们的要求,这个时候我们就需要手动调参。

在 HashMap 构造方法中,可供我们调整的参数有两个,一个是初始容量initialCapacity,另一个负载因子loadFactor。通过这两个设定这两个参数,可以进一步影响阈值大小。但初始阈值 threshold 仅由initialCapacity 经过移位操作计算得出。

名称 用途
initialCapacity HashMap 初始容量
loadFactor 负载因子
threshold 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容

默认情况下,HashMap 初始容量是16,负载因子为 0.75。 注释中有说明,阈值可由容量乘上负载因子计算而来 ,即threshold = capacity * loadFactor

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的幂。我们先来看看 tableSizeFor 方法的图解 :

图中容量是229+1,计算后是230

引用一下啊大神说的:

对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况(假设键值对节点均匀分布在桶数组中)。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。

插入PUT

过程:

对Key求hash值,然后计算下标

如果没有碰撞,就放入桶中

如果碰撞了,就以链表形式放到后面

如果链表长度超过阈值,就把链表转换成红黑树

如果链表存在则替换旧值

如果桶满了(容量*负载因子),则重新resize

public V put(K key, V value) {

    //调用核心方法
    return putVal(hash(key), key, value, false, true);
}

putVal

核心算法在putVal()中。要想理解,先要明白桶排序(Bucket Sort)

它是迄今为止最快的一种排序法,其时间复杂度仅为Ο(n),也就是线性复杂度。

桶排序核心思想是:根据数据规模n划分,m个相同大小的区间 (每个区间为一个桶,桶可理解为容器) 。将n个元素按照规定范围分布到各个桶中去 ,再对每个桶中的元素进行排序,排序方法可根据需要,选择快速排序,或者归并排序,或者插入排序 ,然后依次从每个桶中取出元素,按顺序放入到最初的输出序列中(相当于把所有的桶中的元素合并到一起) 。

下面是代码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //n是数组长度
        Node[] tab; Node p; int n, i;
        //判断桶数组是否是空
        if ((tab = table) == null || (n = tab.length) == 0)
            //是就用resize()初始化
            n = (tab = resize()).length;
        //根据 hash 值确定节点在数组中的插入位置
        //若此位置没有元素则进行插入,注意确定插入位置所用的计算方法为 (n - 1) & hash,由于 n 一定是2的幂次,这个操作相当于hash % n
        if ((p = tab[i = (n - 1) & hash]) == null)
            //将新节点引入桶中
            tab[i] = newNode(hash, key, value, null);
        else {
            //临时变量e进行记录。如果有值,说明仅仅是值的覆盖。
            Node e; K k;
            // 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)// 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
                e = ((TreeNode)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;
                }
            }
            //临时变量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;
    }

HASH

hash算法,高十六位与低十六进行异或运算,这样做的好处是使得到结果会尽可能不同。

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

resize

HashMap 的扩容机制与其他变长集合的套路不太一样,HashMap 按当前桶数组长度的2倍进行扩容,阈值也变为原来的2倍(如果计算过程中,阈值溢出归零,则按阈值公式重新计算)。扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去。

resize总共做了3件事,分别是:

计算新桶数组的容量 newCap 和新阈值 newThr

根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的

将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组。

//resize()函数在size > threshold时被调用
final Node[] resize() {

    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //oldCap大于 0 代表原来的 table 非空
    if (oldCap > 0) {
        // 当 table 容量超过容量最大值,则不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            //阈值设为整形最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }// 按旧容量和阈值的2倍计算新容量和阈值的大小
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        /*
        *oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为
   * HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
   * 或 HashMap(Map m),导致 oldTab 为 null,oldCap 为0,
   * oldThr 为用户指定的 HashMap的初始容量
        */
        newCap = oldThr;
    else { //设置新容量和新阈值大小
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
        // newThr 为 0 时,按阈值计算公式进行计算
    if (newThr == 0) {
        //计算新阈值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //准备初始化过程
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node[] newTab = (Node[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        //遍历。把 oldTab 中的节点 reHash 到 newTab 中去
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            //判断老数组是否为空
            if ((e = oldTab[j]) != null) {
                //不为空,设成空
                oldTab[j] = null;
                //若节点是单个节点,直接重新分配定位
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作
                else if (e instanceof TreeNode)
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                //若是链表,进行链表的 rehash 操作
                else { // preserve order
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;
                    // 遍历链表,并将链表节点按原顺序进行分组
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // rehash 后节点新的位置一定为原来基础上加上 oldCap
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;

    }

关于HashMap在什么时候时间复杂度是O(1),什么时候是O(n),什么时候又是O(logn)的问题

O(1):链表的长度尽可能短,理想状态下链表长度都为1

O(n):当 Hash 冲突严重时,如果没有红黑树,那么在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为O(N)。

O(logn):采用红黑树之后可以保证查询效率O(logn)

手写

/**
 * @author tengshe789
 */
public class 手写HashMap {
    public static class Node{
        K key;
        V value;
        Node next;
        public Node(K key, V value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public K getKey() {
            return this.key;
        }

        public V getValue() {
            return this.value;
        }

        public V setValue(V value) {
            this.value=value;
            return this.value;
        }
    }

    public static class HashMap{
        /*数据存储的结构==>数组+链表*/
        Node[] array=null;

       /* 哈希桶的长度 */
        private static int defaultLength=16;

        /*加载因子/扩容因子*/
        private static double factor=0.75D;

        /*集合中的元素个数*/
        private int size;

        /*打印函数*/
        public void print() {
            System.out.println("===============================");
            if(array!=null) {
                Node node=null;
                for (int i = 0; i < array.length; i++) {
                    node=array[i];
                    System.out.print("下标["+i+"]");
                    //遍历链表
                    while(node!=null) {
                        System.out.print("["+node.getKey()+":"+node.getValue()+"]");
                        if(node.next!=null) {
                            node=node.next;
                        }else {
                            //到尾部元素
                            node=null;
                        }
                    }
                    System.out.println();
                }

            }
        }

        //put元素方法
        public V put(K k, V v) {

            //1.懒加载机制,使用的时候进行分配
            if(array==null) {
                array=new Node[defaultLength];
            }

            //2.通过hash算法,计算出具体插入的位置
            int index=position(k,defaultLength);


            //扩容。判断是否需要扩容
            //扩容的准则,元素的个数   大于  桶的尺寸*加载因子
            if(size > defaultLength*factor) {
                resize();
            }

            //3.放入要插入的元素
            Node node=array[index];
            if(node==null) {
                array[index]=new Node(k,v,null);
                size++;
            }else {
                if(k.equals(node.getKey()) || k==node.getKey()) {
                    return node.setValue(v);
                }else {
                    array[index]=new Node(k,v,node);
                    size++;
                }
            }

            return null;
        }

        //扩容,并且重新排列元素
        private void resize() {
            //翻倍扩容
            //1.创建新的array临时变量,相当于defaultlength*2
            Node[] temp=new Node[defaultLength << 1];

            //2.重新计算散列值,插入到新的array中去。 code=key % defaultLength ==> code=key % defaultLength*2
            Node node=null;
            for (int i = 0; i < array.length; i++) {
                node=array[i];
                while(node!=null) {
                    //重新散列
                    int index=position(node.getKey(),temp.length);
                    //插入头部
                    Node next = node.next;
                    //3
                    node.next=temp[index];
                    //1
                    temp[index]=node;
                    //2
                    node=next;

                }
            }

            //3.替换掉老array
            array=temp;
            defaultLength=temp.length;
            temp=null;


        }

        private int position(K k,int length) {
            int code=k.hashCode();

            //取模算法
            return code % (length-1);

            //求与算法
            //return code & (defaultLength-1);
        }

        public V get(K k) {
            if(array!=null) {
                int index=position(k,defaultLength);
                Node node=array[index];
                //遍历链表
                while(node!=null) {
                    //如果key值相同返回value
                    if(node.getKey()==k) {
                        return node.getValue();
                    } else
                        //如果key值不同则调到下一个元素
                    {
                        node=node.next;
                    }
                }
            }


            return null;
        }


    }

        public static void main(String[] args) {
            HashMap map=new HashMap();
            map.put("001号", "001");
            map.put("002号", "002");
            map.put("003号", "003");
            map.put("004号", "004");
            map.put("005号", "005");
            map.put("006号", "006");
            map.put("007号", "007");
            map.put("008号", "008");
            map.put("009号", "009");
            map.put("010号", "010");
            map.put("011号", "011");
            map.print();

            System.out.println("========>"+map.get("009号"));
        }

    }

参考资料

coolblog

阿里架构师带你分析HashMap源码实现原理

感谢!

以下来自n天后的我:

补充一下看到一个非常好的:点击链接,值得学习

想要了解更多精彩新姿势?请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,juejin同步发出。如有雷同,那真是缘分~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76632.html

相关文章

  • 关于ConcurrentHashMap1.8个人理解

    摘要:数据结构重要成员变量代表整个哈希表。科普,解决多线程并行情况下使用锁造成性能损耗的一种机制,操作包含三个操作数内存位置预期原值和新值。 ConcurrenHashMap 。下面分享一下我对ConcurrentHashMap 的理解,主要用于个人备忘。如果有不对,请批评。 HashMap严重的勾起了我对HashMap家族的好奇心,下面分享一下我对ConcurrentHashMap 的理解...

    olle 评论0 收藏0
  • java源码

    摘要:集合源码解析回归基础,集合源码解析系列,持续更新和源码分析与是两个常用的操作字符串的类。这里我们从源码看下不同状态都是怎么处理的。 Java 集合深入理解:ArrayList 回归基础,Java 集合深入理解系列,持续更新~ JVM 源码分析之 System.currentTimeMillis 及 nanoTime 原理详解 JVM 源码分析之 System.currentTimeMi...

    Freeman 评论0 收藏0
  • HashMap 工作原理

    摘要:的工作原理是近年来常见的面试题。让我们再来看看这些问题设计哪些知识点的概念中解决碰撞的方法和的应用,以及它们在中的重要性不可变对象的好处多线程的条件竞争重新调整的大小总结的工作原理基于原理,我们通过和方法储存和获取对象。 HashMap 的工作原理是近年来常见的 Java 面试题。几乎每个 Java 程序员都知道 HashMap,都知道哪里要用 HashMap,知道Hashtable和...

    zhisheng 评论0 收藏0
  • Java相关

    摘要:本文是作者自己对中线程的状态线程间协作相关使用的理解与总结,不对之处,望指出,共勉。当中的的数目而不是已占用的位置数大于集合番一文通版集合番一文通版垃圾回收机制讲得很透彻,深入浅出。 一小时搞明白自定义注解 Annotation(注解)就是 Java 提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法。Annotion(注解) 是一个接口,程序可以通过...

    wangtdgoodluck 评论0 收藏0
  • Java HashMap 源码解析

    摘要:所以利用哈希表这种数据结构实现具体类时,需要设计个好的函数,使冲突尽可能的减少其次是需要解决发生冲突后如何处理。源码剖析首先从构造函数开始讲,遵循集合框架的约束,提供了一个参数为空的构造函数与有一个参数且参数类型为的构造函数。 本文章首发于个人博客,鉴于sf博客样式具有赏心悦目的美感,遂发表于此,供大家学习、批评。本文还在不断更新中,最新版可移至个人博客。? 继上一篇文章Java集合...

    Aklman 评论0 收藏0

发表评论

0条评论

Yujiaao

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<