美文网首页
HashMap底层原理总结

HashMap底层原理总结

作者: Raral | 来源:发表于2021-12-18 17:59 被阅读0次

    HashMap

    1. 为什么是 大于等于 2的幂次运算
    2. key生成 数组 是为什么一位运算
    3. 程序是什么? 程序本质是数据结构和算法组成。

    HashMap底层存储原理详解

    基本了解

    存储》put; 查询》get; 其实存一个 <key, value>Entry对象

    • 底层结构

      jdk1.7:数组 + 链表

      jdk1.8: 数组 + 链表 + 红黑树 ( 红黑树解决1.7版本什么性能问题)

    • hash 算法

    • 数组:采用一段连续的存储单元来存储数据的;特点:查询时间复杂度o(1),删除插入时间复杂度o(n), 总结:查询快 增删慢

    • 链表:采用一种物理存储单元上非连续,非顺序的存储结构;特点:插入删除时间复杂度o(1), 查找遍历时间复杂度o(n), 总结 : 增删快 查询慢

    ArryList 底层使用数组结构

    [图片上传失败...(image-e4ba3c-1639821670796)]

    LinkedList底层使用双向链表结构

    [图片上传失败...(image-8002d9-1639821670796)]

    hash算法

    哈希算法(也叫散列),就是把任意长度值(key)通过散列算法变换成固定长度的key(地址)通过这个地址进行访问的数据结构

    它通过把关键码映射到表中一个。位置来访问记录,以加快查找的速度。

    [图片上传失败...(image-4af96-1639821670796)]

    ASCII码 md5

    HashCode: 通过字符串算出它额ascii码,进行mod(取模)(%), 算出哈希表中的下标

    public static void main(String[] args) {
            char c[] = "lies".toCharArray();
            for (int i = 0; i < c.length; i ++) {
                System.out.println(c[i] + ":" + (int)c[i]);//转换成ascii码
            }
        }
    //打印
    l:108
    i:105
    e:101
    s:115
    

    [图片上传失败...(image-6fbe73-1639821670796)]

    为什么要进行取模?

    为了节省数组空间,所以对429%10 等于 9;

    如果取模后后可能会产生hash冲突(hash碰撞),咋样解决? 使用链表解决

    [图片上传失败...(image-105780-1639821670796)]

    [图片上传失败...(image-4392a6-1639821670796)]

    hashmap存储:

    1. 将一个<key value>对象的key通过哈希算法算出哈希,然后对hash取模得出一个数组下标
    2. 然后通过下标去找到存储位置存储
    3. 如果有相同的下标,则需要把后来的对象当成最新的链表根节点,把next指向下一个节点即可

    hashmap取出:

    1. 通过key查出hash值和下标
    2. 通过下标去数组查询
    3. 如果有相同下标,则需要比较hash值,如果hash值不相同,则通过链表向下查找,直到查到hash值相同即可

    算法对比

    [图片上传失败...(image-64fcbe-1639821670796)]

    手写代码实现(数组 + 链表)

    定义一个接口

    public interface Map<K, V> {
        V put(K k, V v);
        V get(K k);
        int size();
    
        interface Entry<K, V> {
            K getKey();
            V getValue();
        }
    }
    
    

    定义一个实现类

     package com.kuang.demo12_data_structure.data08_hashmap;
    
    /**
     * @description:
     * @author: li
     */
    public class HashMap<K, V> implements Map<K, V> {
        private Entry<K, V> table[] = null;
        int size = 0;
        public HashMap() {
            this.table = new Entry[16]; //java中一般默认为 2^4=16 容量
        }
    
    
    
        /**
         * 1. 通过key hash算法 计算出hash值
         * 2. index下标数组 当前下标对象
         * 3. 判断当前位置是否有对象, 如果没有 直接存储
         * 4. 如果有对象,冲突, 使用next链表
         * 5. 返回当前的节点对象
         * */
        @Override
        public V put(K k, V v) {
            System.out.println("key:" + k + "::::hash值:" + k.hashCode() + ":::::存储位置:" + (k.hashCode() % 16) );
            int hash = k.hashCode();
            int index = hash(k);
            Entry<K, V> entry = table[index];
            if( null == entry) {
                table[index] = new Entry<>(k, v, hash, null);
                size ++;
            } else {
                //hash冲突了
                table[index] = new Entry<>(k, v, hash, entry);
            }
    
            return table[index].getValue();
        }
        private int hash(K k) {//简单取模实现hash算法,真实hash算法实现底层移位操作
            int index = k.hashCode() % 16;
            return index >= 0? index : -index;
        }
    
        /**
         * 1. 通过key hash算法 计算 数组索引
         * 2. 如果索引值和hash值一样 说明就是你找到的对象,如果hash值不相同,则通过链表继续往下查找(递归查找)
         * */
        @Override
        public V get(K k) {
            int hash = k.hashCode();
            int index = hash(k);
            Entry<K, V> entry = table[index];
            V v;
            if(entry.getHash() == hash) {
                //数组找到了
                v = entry.getValue();
            } else {
                //hash冲突了 向下查找链表
                Entry<K, V> kvEntry = recurFind(entry, hash);
                if(null != kvEntry) {
                    v = kvEntry.getValue();
                } else {
                    v = null;
                }
            }
            return v;
        }
        /**
         * @param entry 当前根节点的链表对象
         * @param hash 需要查询hash
         * */
        private Entry<K, V> recurFind(Entry<K, V> entry, int hash) {
            //next指向entry
            Entry<K, V> next = entry.getNext();
            if(next.getHash() == hash) {
                //next的对象的hash值与 需要hash值相同 就是找的对象
                return next;
            }else {
                //不相同继续向下查找
                recurFind(next, hash);
            }
    
            return null;
        }
    
    
        @Override
        public int size() {
            return 0;
        }
    
        class Entry<K, V> implements Map.Entry<K, V> {
            K k;
            V v;
            int hash;
            Entry<K, V> next;
    
            public int getHash() {
                return hash;
            }
    
            public void setHash(int hash) {
                this.hash = hash;
            }
    
            public Entry<K, V> getNext() {
                return next;
            }
    
            public void setNext(Entry<K, V> next) {
                this.next = next;
            }
    
            public Entry(K k, V v, int hash, Entry<K, V> next) {
                this.k = k;
                this.v = v;
                this.hash = hash;
                this.next = next;
            }
    
            @Override
            public K getKey() {
                return this.k;
            }
    
            @Override
            public V getValue() {
                return this.v;
            }
        }
    }
            
    

    测试打印

    public class Test2 {
        public static void main(String[] args) {
    //        Test2 test2 = new Test2();
    //        test2.put("刘一", "刘一");
            Map myMap = new HashMap<>();
            myMap.put("刘一", "刘一");
            myMap.put("张三", "张三");
            myMap.put("李四", "李四");
            myMap.put("王五", "王五");
    
            Object ly = myMap.get("刘一");
            Object zs = myMap.get("张三");
            Object ww = myMap.get("王五");
            System.out.println(ly);
            System.out.println(zs);
            System.out.println(ww);
    
    
        }
    
        public void put(String key, String value) {
            System.out.println("key:" + key + "::::hash值:" + key.hashCode() + ":::::存储位置:" + (key.hashCode() % 10) );
    
        }
    }
    

    [图片上传失败...(image-d75ca9-1639821670796)]

    1.7版本的实现的hashmap(数组+链表)缺点

    当存储数据过时,hash冲突很多,所以链表长度会很长,导致查询数据特别慢,查询性能会很低

    所以1.8后为了解决链表过长查询慢,使用红黑树结构;阈值为8才有红黑树;

    [图片上传失败...(image-c1ca12-1639821670796)]

    为什么链表长度大于8才使用红黑树?

    [图片上传失败...(image-9f8ad3-1639821670796)]

    [图片上传失败...(image-c9a177-1639821670796)]

    红黑树在插入操作为了保证红黑结构,左小右大...,会牺牲插入性能。

    并发包

    [图片上传失败...(image-baf59d-1639821670796)]

    [图片上传失败...(image-cc4ab7-1639821670796)]

    对于hashmap面试

    1.7 1.8
    数据结构 数组+链表 数组+链表+红黑树
    链表插入方式 头插法 尾插法<br />(因为1.7头插法在并发中会形成循环链表耗尽cpu性能)
    hash算法 使用计算机底层位运算 简化
    红黑树阈值 当链表长度大于8时使用红黑树
    多线程是否安全 不安全 不安全<br />
    1. 在初始化数组时,传入一个2的次幂的容量,put时,会初始这个数组,容量是大于等于初始化容量的最近的2次幂,比如初始容量是6,那么它初始化的就是8

    2. hash算法:

      • 首先调用hashCode方法获取key对应的hashCode值(h)
      • 然后进行高位运算:将h右移16位取得高16位,与之前低h 进行异或运算(h ^ (h >>> 16))
      • 然后再得到值 与 容量 -1进行与运算,从而得到改对象再数组的下标(hash & (length - 1) = index)
               //位运算效率高
           String key = "e";
              int h = key.hashCode();
              h = h ^ (h >>> 16);
              int index = h & (16 - 1);
              System.out.println(index);
      
           //效率低
              String key2 = "e";
              int index2 =  key2.hashCode() % 16;
              System.out.println(index2);
      
    key的hashCode 与 容量 - 1 进行 与操作,计算数组的下标(因为在计算机中与操作性能最好)
    
    1. 在存储数据时,有2个问题需要注意:扩容问题和树化问题;

    2. 扩容问题:加载因子0.75(3/4),达到容量的0.75就会扩容,两倍扩容(耗性能)

    3. 树化问题:数组容量达到64,链表长度大于等于8,后才会进行树化,链表长度小于6就会解除树化(树化耗性能)

    hashmap线程不安全

    put元素的时候,A线程对一个元素进行hash计算桶的索引坐标,然而正当它准备插入元素的时候,B线程被调度并且被完整执行,如果这个时候A和B线程获得索引坐标是一样的,那么B会插入元素形成新的链表,但是A线程拿到的旧链表信息,所以A线程执行完后会覆盖B线程插入的元素

    相关文章

      网友评论

          本文标题:HashMap底层原理总结

          本文链接:https://www.haomeiwen.com/subject/somgfrtx.html