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;
}

 

posted @ 2019-03-05 15:22  蓝空飞翔  阅读(287)  评论(0编辑  收藏  举报