精华内容
下载资源
问答
  • HashMap底层实现原理

    万次阅读 多人点赞 2019-04-24 13:57:56
    HashMap底层实现原理 HashMap是Java语言中用的最频繁的一种数据结构。 1.hashmap的数据结构 要了解hashmap首先要弄清楚他的结构。在java编程语言中最基本的数据结构有两种,数组和链表。 数组:查询速度快,可以根据...

    HashMap底层实现原理

    HashMap是Java语言中用的最频繁的一种数据结构。

    1.hashmap的数据结构
    要了解hashmap首先要弄清楚他的结构。在java编程语言中最基本的数据结构有两种,数组和链表。
    数组:查询速度快,可以根据索引查询;但插入和删除比较困难;
    链表:查询速度慢,需要遍历整个链表,但插入和删除操作比较容易。
    hashmap是数组和链表组成的,数据结构中又叫“链表散列”。
    数据结构
    2.hashmap特点
    1) 快速存储 :比如当我们对hashmap进行get和put的时候速度非常快
    2) 快速查找(时间复杂度o(1))当我们通过key去get一个value的时候时间复杂度非常的低,效率非常高
    3) 可伸缩:1数组扩容,边长。2,单线列表如果长度超过8的话会变成红黑树

    3.hashmap的Hash算法
    在聊哈算法之前我们要知道在Java中所有对象都有hashcode(使用key的),如果使用object对象get hashcode的话会得到要给int类型的指,我们在hashmap中主要是用他的key去计算它的值的。

    Hash值的计算
    Hash值=(hashcode)^(hashcode >>> 16)
    Hashcode予hashcode自己向右位移16位的异或运算。这样可以确保算出来的值足够随机。因为进行hash计算的时候足够分散,以便于计算数组下标的时候算的值足够分散。前面说过hashmap的底层是由数组组成,数组默认大小是16,那么数组下标是怎么计算出来的呢,那就是:
    数组下标:hash&(16-1) = hash%16
    对哈市计算得到的hash进行16的求余,得到一个16的位数,比如说是1到15之间的一个数,hashmap会与hash值和15进行予运算。这样可以效率会更高。计算机中会容易识别这种向右位移,向左位移。

    Hash冲突
    不同的对象算出来的数组下标是相同的这样就会产生hash冲突。
    Hash冲突会产生单线链表。在这里插入图片描述
    当单线链表达到一定长度后效率会非常低,那么在jdk1.8以后的话,加入了红黑树,也就是说单线列表达到一定长度后就会变成一个红黑树

    Hashmap底层原理扩容
    扩容
    数组长度变成2倍 0.75
    触发条件
    数组存储比例达到75% – 0.75

    一下是需要理解的:
    Hashmap的扩容并不是为单线链表准备的,单线链表只是为了解决hash冲突准备的。也就是说当数组达到一定长度,比如说hashmap默认数组长度是16,那么达到出发条件,数组存储比例达到了75% ,也就是16*0.75=12的时候就会发生扩容

    红黑树
    一种二叉树,高效的检索效率

    前面说了,当链表达到一定长度后,链表就会变成红黑树

    触发条件
    在链表长度大于8的时候,将后面的数据存在二叉树中
    在这里插入图片描述

    红黑树就是上图所示

    A下面有两个节点BC,B和C下面又有DEF

    总结
    Hashmap的数据结构
    数组,单线链表,红黑树(1.8)
    Hashmap的特点

    1. 快速存储
    2. 快速查找
    3. 可伸缩

    Hash算法
    为什么要用hash算法。在我们Java中任何对象都有hashcode,hash算法就是通过hashcode与自己进行向右位移16的异或运算。这样做是为了计算出来的hash值足够随机,足够分散,还有产生的数组下标足够随机

    还有就是hash冲突
    面试的时候问为什么会有hash冲突
    比如说我们同样要插入一个数组,大家计算出来都是13号。那么这个时候就会产生hash冲突,为了解决hash冲突,我们的数组就会变成单线链表,然后当我们的单线链表达到一定长度的时候就产生红黑树。

    展开全文
  • HashMap 底层实现原理

    2021-01-19 10:11:49
    HashMap 底层实现原理1、HashMap的底层数据结构2、Java7和Java8的区别3、HashMap的主要参数都有哪些4、默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?5、hash的计算规则6、HashMap的存取原理 1、HashMap...

    1、HashMap的底层数据结构

    HashMap 的底层实现原理,1.71.8 是有区别的
    1.7 及之前的版本,底层是 数组 + 链表 Entry
    1.8 及之后的版本,底层是 数组 + 链表 + 红黑树 NOde,引入红黑树主要是因为当 Hash 冲突较多时,链表就会变成长链表,操作效率会变慢。
    // TREEIFY_THRESHOLD = 8 树化阈值 
    // UNTREEIFY_THRESHOLD = 8 取消树化阈值
    引入红黑树的原则是,当链表长度大于 8 时,链表会变成红黑树;当红黑树节点小于 6 时,红黑树又会变成链表。
    底层就是一个数组,数组里面的元素是 Node<K,V> 链表
    transient Node<K,V>[] table;
    // Node 数据格式
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        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;
        }
    }
    

    2、Java7和Java8的区别

    1、数据结构不同
        1.71.7 之前的版本是:数组 + 链表
        1.81.8 之后的版本是:数组 + 链表 + 红黑树(为了解决Hash冲突,当Hash冲突较多时,链表长度过长,
        会影响数据操作效率,降低时间复杂度,由O(n) -> O(logn)2、存储数据时插入方法不一样
        1.71.7 之前采用的是:头插法
        1.81.8 之后采用的是:尾插法
        头插法,在多线程操作下,扩容的时候可能会产生环形链表,取值的时候,就会出现 Infinite Loop。
        尾插法就不会出现这样的问题,永远放在链表的尾部,扩容的时候,不会改变链表的顺序,不会出现环形链表。  
    总结:
    Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。
    Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。     
    

    3、HashMap的主要参数都有哪些

    // initialCapacity 初始容量 不传的话 默认为16  1 << 4
    // loadFactor 负载因子 不传的话 默认为0.75
    public HashMap(int initialCapacity, float loadFactor);
    // 构造方法有3个
    // 无参构造方法 初始容量 为 16 负载因子为 0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    // 一个参数构造方法 初始容量为传入容量 会转成 大于等于传参的最小的2的幂,负载因子为 0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    // 两个参数构造方法,初始容量 和 负载因子
    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);
    }
    

    4、默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

    // 默认初始化大小为 16 必须为2的幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    // 最大容量为 1 << 30  10 7374 1824,如果初始化时传参大于最大容量,则使用该容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 为啥是 2 的幂, tableSizeFor方法,对传入的容量做了以下操作
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1; // n 和 n >>> 1 做 或 操作
        n |= n >>> 2; // n 和 n >>> 2 做 或 操作
        n |= n >>> 4; // n 和 n >>> 4 做 或 操作
        n |= n >>> 8; // n 和 n >>> 8 做 或 操作
        n |= n >>> 16; // n 和 n >>> 16 做 或 操作
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    // 这样一番操作下来,结果只能是 2 4 8 16 32 ... 2^n,都是 2 的幂
    // 使用 2 的幂的好处 --> 尽可能实现均匀分布
    // 因为 HashMap 的下标算法为:index = HashCode(Key) & (Length- 1)
    // 使用 2 的幂的时候,Length- 1 的二进制就都是1,和 HashCode(Key)与操作的时候,index的结果就等于 HashCode(Key)的后面几位
    // 只要输入的 HashCode(Key)本身分布均匀,算出来的 index 就是均匀的,可以实现均匀分布。
    

    5、hash的计算规则

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

    6、HashMap的存取原理

    // HashMap 存储数据原理 --> put
    // 参数 hash: key 的hash值 key: key value: 存入的value 
    // onlyIfAbsent: 是否覆盖当前 key 对应的已存在的value值,如果为true时,表示不覆盖,false 表示覆盖,默认为 false
    // evict: 
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果 table 为空,没有初始化
        if ((tab = table) == null || (n = tab.length) == 0)
        // 调用resize() 方法,初始化table,长度为默认长度 16,负载因子为默认负载因子 0.75
            n = (tab = resize()).length;
        // tab[i = (n - 1) & hash] 是通过hash求出下标,如果当前下标的元素为 null,直接生成一个链表放在下标所在的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 如果下标所在的链表,头部已经有元素,执行下面的操作 
        else {
            Node<K,V> e; K k;
            // 如果当前key 已存在,把p赋给e,临时存储
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果 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;
                    }
                    // 如果当前key已存在,直接返回
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 元素递归操作    
                    p = e;
                }
            }
            // 如果当前 key 已存在,判断是否要元素覆盖
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // HashMap 结构调整的次数
        ++modCount;
        // 如果长度大于阈值,需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    // resize() 方法
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            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
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> 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;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    
    // HashMap 的 读取原理
    // 读取通过 key 
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 判断 数组不为空,数组长度不为空,下标所指向的链表头部节点数据不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 判断是否和头部元素相等,相等就返回    
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 链表遍历    
            if ((e = first.next) != null) {
                // 如果当前元素属于红黑树,走红黑树的获取元素方法
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 不是红黑树,遍历链表,获取元素    
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    
    展开全文
  • hashmap 底层实现原理

    2021-03-04 11:06:45
    Java HashMap底层实现原理 HashMap底层是哈希表(散列表),哈希就是一个数组,数组的每个元素是一个单向链表。 ● 在第一次执行put方法时,给哈希表的数组(哈希桶)默认初始化,容量: 16 ● hashMap加载因子是0.75 ● ...

    Java HashMap底层实现原理

    在这里插入图片描述

    HashMap底层是哈希表(散列表),哈希就是一个数组,数组的每个元素是一个单向链表。

    ● 在第一次执行put方法时,给哈希表的数组(哈希桶)默认初始化,容量: 16

    ● hashMap加载因子是0.75

    ● 当hashMap中<键,值>对的数量 > 哈希桶容量 * 加载因子时, 哈希桶(数组)要扩容 , 按2倍大小扩容

    ● HashMap可以指定初始化容量, 系统会自动调整为2的幂次方, 可以快速的计算数组的下标

    ● 如果单向链表中结点的个数超过8个时, 系统会自动的把单向链表转换为树形结构

    展开全文
  • hashmap底层实现原理

    2021-04-11 16:02:29
    hashmap底层实现原理 k=key; v=value; hashcode和equals方法在Object类中默认是内存地址需要重写 哈希表在链表中完成增删,查询只需扫描部分,效率高 一.put(k,v)实现原理 将(k,v)封装到Node对象当中。 它的底层...

    hashmap底层实现原理

    • k=key;
    • v=value;
    • hashcode和equals方法在Object类中默认是内存地址需要重写
    • 哈希表在链表中完成增删,查询只需扫描部分,效率高

    一.put(k,v)实现原理

    1. 将(k,v)封装到Node对象当中。
    2. 它的底层会调用hashcode()方法得出对应的hash值。
    3. 通过哈希表函数/哈希算法转换成数组的下标,若下标位置上没有任何元素,则直接将Node添加到位置上。若对应数组下标位置上有链表,则将k和链表上每个节点的k值进行equals;若所有equals方法返回的值都是false,则将节点添加在链表的末尾;若其中一个节点得到equals方法返回值是true,则将该节点的value值覆盖。

    二.get(k)实现原理

    1. 先调用hashcode()方法得出K值对应的hash值,并通过哈希表算法转换成数组的下标。
    2. 找到下标位置,若该位置上没有任何节点,则返回null;若该位置上有链表,则将k与每个节点上的k值进行equals,若所有equals方法都返回false,则返回null;若其中一个节点的equals方法返回true,那么该节点的value就是所要查找的,返回该节点的value。
    展开全文
  • HashMap底层实现原理及面试问题

    万次阅读 多人点赞 2018-08-29 11:00:56
    HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取...
  • HashMap底层实现原理详解

    万次阅读 多人点赞 2021-02-13 22:41:16
    文章目录前言一、快速入门二、使用步骤1.引入库2.读入数据总结学习内容:学习时间:学习产出:前言一、pandas是什么?二、使用步骤1.引入库2....随着JDK版本的跟新,JDK1.8对HashMap底层实现进行
  • HashMap的工作原理HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取...
  • HashMap底层实现原理解析

    万次阅读 多人点赞 2020-12-18 10:37:27
    一:HashMap底层实现原理解析 我们常见的有数据结构有三种结构:1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各自的数据结构的特点: 1、数组结构: 存储区间连续、内存占用严重、空间复杂度大 优点:...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,580
精华内容 1,432
关键字:

hashmap底层实现原理