HashMap底层原理总结

HashMap

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

HashMap底层存储原理详解

基本了解

存储》put; 查询》get; 其实存一个 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通过哈希算法算出哈希,然后对hash取模得出一个数组下标
  2. 然后通过下标去找到存储位置存储
  3. 如果有相同的下标,则需要把后来的对象当成最新的链表根节点,把next指向下一个节点即可

hashmap取出:

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

算法对比

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

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

定义一个接口

public interface Map {
    V put(K k, V v);
    V get(K k);
    int size();

    interface Entry {
        K getKey();
        V getValue();
    }
}

定义一个实现类

 package com.kuang.demo12_data_structure.data08_hashmap;

/**
 * @description:
 * @author: li
 */
public class HashMap implements Map {
    private Entry 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 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 entry = table[index];
        V v;
        if(entry.getHash() == hash) {
            //数组找到了
            v = entry.getValue();
        } else {
            //hash冲突了 向下查找链表
            Entry kvEntry = recurFind(entry, hash);
            if(null != kvEntry) {
                v = kvEntry.getValue();
            } else {
                v = null;
            }
        }
        return v;
    }
    /**
     * @param entry 当前根节点的链表对象
     * @param hash 需要查询hash
     * */
    private Entry recurFind(Entry entry, int hash) {
        //next指向entry
        Entry 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 implements Map.Entry {
        K k;
        V v;
        int hash;
        Entry next;

        public int getHash() {
            return hash;
        }

        public void setHash(int hash) {
            this.hash = hash;
        }

        public Entry getNext() {
            return next;
        }

        public void setNext(Entry next) {
            this.next = next;
        }

        public Entry(K k, V v, int hash, Entry 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
数据结构 数组+链表 数组+链表+红黑树
链表插入方式 头插法 尾插法
(因为1.7头插法在并发中会形成循环链表耗尽cpu性能)
hash算法 使用计算机底层位运算 简化
红黑树阈值 当链表长度大于8时使用红黑树
多线程是否安全 不安全 不安全
  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底层原理总结)