zoukankan      html  css  js  c++  java
  • 7. hashmap的底层实现

    (1).HashMap的概述

      HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。

    (2).HashMap的数据结构

      hashMap的存储原理为哈希表(hash table),也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。

       数据结构的物理存储结构只有两种:顺序存储结构链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式)。而哈希表的主干部分为数组。哈希表的新增或者查找就是通过哈希函数(关键字)计算出实际存储的位置,从而进行新增或者查询。(这个函数的设计好坏会直接影响到哈希表的优劣)

      

           哈希冲突

      然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

    (3).实现原理

      成员变量:

        源码分析:

    /** 初始容量,默认16 */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        /** 最大初始容量,2^30 */
        static final int MAXIMUM_CAPACITY = 1 << 30;
    
        /** 负载因子,默认0.75,负载因子越小,hash冲突机率越低 */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
        /** 初始化一个Entry的空数组 */
        static final Entry<?,?>[] EMPTY_TABLE = {};
    
        /** 将初始化好的空数组赋值给table,table数组是HashMap实际存储数据的地方,并不在EMPTY_TABLE数组中 */
        transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    
        /** HashMap实际存储的元素个数 */
        transient int size;
    
        /** 临界值(HashMap 实际能存储的大小),公式为(threshold = capacity * loadFactor) */
        int threshold;
    
        /** 负载因子 */
        final float loadFactor;
    
        /** HashMap的结构被修改的次数,用于迭代器 */
        transient int modCount;

      构造方法

        源码分析:

        public HashMap() {
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
        }
    
        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);
            // 设置负载因子,临界值此时为容量大小,后面第一次put时由inflateTable(int toSize)方法计算设置
            this.loadFactor = loadFactor;
            threshold = initialCapacity;
            init();
        }
    
        public HashMap(Map<? extends K, ? extends V> m) { //指定集合,转化为HashMap
            this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                          DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
            inflateTable(threshold);
            putAllForCreate(m);
        }

       put方法:

         源码分析:

    public V put(K key, V value) {  
        // 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
        // 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里 
        if (key == null)  
            return putForNullKey(value);  
        // 若“key不为null”,则计算该key的哈希值
        int hash = hash(key);  
        // 搜索指定hash值在对应table中的索引
        int i = indexFor(hash, table.length);  
        // 循环遍历table数组上的Entry对象,判断该位置上key是否已存在
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            // 哈希值相同并且对象相同
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                // 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出!
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        // 修改次数+1
        modCount++;
        // table数组中没有key对应的键值对,就将key-value添加到table[i]处 
        addEntry(hash, key, value, i);  
        return null;  
    }  

    可以看到,当我们给put()方法传递键和值时,HashMap会由key来调用hash()(也就是上面说到的哈希函数())方法,返回键的hash值,计算Index后用于找到bucket(哈希桶)的位置来储存Entry对象。

    如果两个对象key的hash值相同,那么它们的bucket位置也相同,但equals()不相同,添加元素时会发生hash碰撞,也叫hash冲突,HashMap使用链表来解决碰撞问题。

    分析源码可知,put()时,HashMap会先遍历table数组,用hash值和equals()判断数组中是否存在完全相同的key对象, 如果这个key对象在table数组中已经存在,就用新的value代替老的value。如果不存在,就创建一个新的Entry对象添加到table[ i ]处。

    如果该table[ i ]已经存在其他元素,那么新Entry对象将会储存在bucket链表的表头,通过next指向原有的Entry对象,形成链表结构(hash碰撞解决方案)。

    结论:也就是我们常说的:hashcode(位置)一样,值不一定相同;值相同,hashcode(位置)一定一样

     addEntry()

      源码分析:

    /*
     * hash hash值
     * key 键值
     * value value值
     * bucketIndex Entry[]数组中的存储索引
     * / 
    void addEntry(int hash, K key, V value, int bucketIndex) {
         if ((size >= threshold) && (null != table[bucketIndex])) { //如果数组长度大于等于容量*负载因子,并且要添加的位置为null resize(2 * table.length); //扩容操作,将数据元素重新计算位置后放入newTable中,链表的顺序与之前的顺序相反 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
    /*
    * 在链表中添加一个新的Entry的对象在链表的表头(1.8以前)
    */ void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }

    注意:在1.8之前,新插入的元素都是放在了链表的头部位置,但是这种操作在高并发的环境下容易导致死锁,所以1.8之后,新插入的元素都放在了链表的尾部。

      get方法:

        源码分析:

        public V get(Object key) {
            // 若key为null,遍历table[0]处的链表(实际上要么没有元素,要么只有一个Entry对象),取出key为null的value
            if (key == null)
                return getForNullKey();
            // 若key不为null,用key获取Entry对象
            Entry<K,V> entry = getEntry(key);
            // 若链表中找到的Entry不为null,返回该Entry中的value
            return null == entry ? null : entry.getValue();
        }
    
        final Entry<K,V> getEntry(Object key) {
            if (size == 0) {
                return null;
            }
            // 计算key的hash值
            int hash = (key == null) ? 0 : hash(key);
            // 计算key在数组中对应位置,遍历该位置的链表
            for (Entry<K,V> e = table[indexFor(hash, table.length)];
                 e != null;
                 e = e.next) {
                Object k;
                // 若key完全相同,返回链表中对应的Entry对象
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            // 链表中没找到对应的key,返回null
            return null;
        }

      在get方法中,首先计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值。

      remove方法:

        源码分析:

    public V remove(Object key) {
         Entry<K,V> e = removeEntryForKey(key);
         return (e == null ? null : e.value);
    }
    final Entry<K,V> removeEntryForKey(Object key) {  
         if (size == 0) {
             return null;
         }
         int hash = (key == null) ? 0 : hash(key);
         int i = indexFor(hash, table.length);//先计算指定key的hash值
         Entry<K,V> prev = table[i];
         Entry<K,V> e = prev;
         //判断当前位置是否Entry实体存在 
         while (e != null) {
             Entry<K,V> next = e.next;
             Object k;
             if (e.hash == hash &&
                 ((k = e.key) == key || (key != null && key.equals(k)))) {
                 modCount++;
                 size--;
                 if (prev == e) //若相等表明,table的当前位置只有一个元素,直接将table[i] = next = null 。
                     table[i] = next;
                 else
                     prev.next = next;
                 e.recordRemoval(this);
                 return e;
             }
             prev = e;  //则让prev -> next ,e 失去引用。
             e = next;
        }
    
        return e;
    }
  • 相关阅读:
    mysql 快速生成百万条测试数据
    DEV SIT UAT
    云计算的三层SPI模型
    go的下载
    redis主从 哨兵
    Mybatis 插入操作时获取主键 (Oracle 触发器与SEQ)
    oracle创建表空间
    mycat源码分析
    js判断是否是数字通用写法
    spring aop获取目标对象的方法对象(包括方法上的注解)
  • 原文地址:https://www.cnblogs.com/BlueSee/p/10477051.html
Copyright © 2011-2022 走看看