HashMap源码解析(基于JDK1.7)

·  阅读 179

一 简介

java.lang.Object  

↳     java.util.AbstractMap  

↳           java.util.HashMap  

public class HashMap  

extends AbstractMap  

implements Map, Cloneable, Serializable { }  
复制代码

HashMap是基于哈希表实现的,每一个元素是一个key-value对,实现了Serializable、Cloneable接口,允许使用null值和null键。不保证映射的顺序,内部通过单链表解决冲突问题,容量超过(容量*加载因子)时,会自动增长。(除了不同步和允许使用null之外,HashMap类与Hashtable大致相同)
HashMap不是线程安全的,如果想获取线程安全的HashMap

  • 1通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
      Map map = Collections.synchronizedMap(new HashMap());
    复制代码
  • 2使用concurrent并发包下的concurrentHashMap。

二 数据结构

HashMap由数组+链表组成的,主干是一个Entry数组,每一个entry包含一个(key-value)键值对,链表则是主要为了解决哈希冲突而存在的,HashMap通过key的hashCode来计算hash值,当hashCode相同时,通过“拉链法”解决冲突,如下图所示。

如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,只需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以性能考虑,HashMap中的链表出现越少,性能才会越好。

三 源码分析

1 关键属性

//默认初始化化容量,即16  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//最大容量,即2的30次方  
static final int MAXIMUM_CAPACITY = 1 << 30;  
//默认加载因子,当容器使用率达到75%的时候就扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;  
//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态  
static final Entry<?,?>[] EMPTY_TABLE = {};  
//空的存储实体  
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  
//实际存储的key-value键值对的个数
transient int size;
//扩容的临界点,如果当前容量达到该值,则需要扩容了.
//如果当前数组容量为0时(空数组),则该值作为初始化内部数组的初始容量
int threshold;
//由构造函数传入的指定负载因子
final float loadFactor;
//修改次数,用于快速失败机制
transient int modCount;
//默认的threshold值  
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
复制代码

2 构造方法

构造方法主要完成容量和加载因子的设置

      /**
     * 通过初始容量和状态因子构造HashMap 
     * @param initialCapacity 容量
     * @param loadFactor 加载因子
     */
    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;  
        threshold = initialCapacity;  
        //init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
        init();
    }  
复制代码
   /**
     * 通过扩容因子构造HashMap,容量去默认值,即16 
     */
    public HashMap(int initialCapacity) {  
        this(initialCapacity, DEFAULT_LOAD_FACTOR);  
    }  
复制代码
      /**
     * 加载因子取0.75,容量取16,构造HashMap 
     */
    public HashMap() {  
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);  
    }  
复制代码
    /**
     * 通过其他Map来初始化HashMap,容量通过其他Map的size来计算,加载因子取0.75
     */
    public HashMap(Map<? extends K, ? extends V> m) {  
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);  
        //初始化HashMap底层的数组结构
        inflateTable(threshold);
        //添加m中的元素
        putAllForCreate(m);  
    }  
复制代码

3 存储数据(put)

    /**
     * 存入一个键值对,如果key重复,则更新value
     * @param key 键值名
     * @param value 键值
     * @return 如果存的是新key则返回null,如果覆盖了旧键值对,则返回旧value
     */
    public V put(K key, V value) {
        //如果数组为空,则新建数组
        if (table == EMPTY_TABLE) {
         //初始化HashMap底层的数组结构
            inflateTable(threshold);
        }
        //如果key为null,则把value放在table[0]中
        if (key == null)
            return putForNullKey(value);
        //生成key所对应的hash值
        int hash = hash(key);
        //根据hash值和数组的长度找到:该key所属entry在table中的位置i
        int i = indexFor(hash, table.length);
        /**
         * 数组中每一项存的都是一个链表,
         * 先找到i位置,然后循环该位置上的每一个entry,
         * 如果发现存在key与传入key相等,则替换其value。然后结束方法。
         * 如果没有找到相同的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))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //map操作次数加一
        modCount++;
        //查看是否需要扩容,并将该键值对存入指定下标的链表头中
        addEntry(hash, key, value, i);
        //如果是新存入的键值对,则返回null
        return null;
    }
复制代码
/**
  *  将该键值对存入指定下标的链表头中
     * @param hash hash值
     * @param key 键值名
     * @param value 键值
     * @param bucketIndex 索引
  */
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }
复制代码
 /**
     * 将键值对与他的hash值作为一个entry,插入table的指定下标中的链表头中
     * @param hash hash值
     * @param key 键值名
     * @param value 键值
     * @param bucketIndex 索引
     */
    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++;
    }
复制代码

4 hash算法

static int hash(int h) {
    //此功能确保hash码不同,有限数量的碰撞
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
复制代码

5 调整容量(resize)

 /**
     * 对数组扩容,即创建一个新数组,并将旧数组里的东西重新存入新数组
     * @param newCapacity 新数组容量
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length
        //如果当前数组容量已经达到最大值了,则将扩容的临界值设置为Integer.MAX_VALUE(Integer.MAX_VALUE是容量的临界点)
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //创建一个扩容后的新数组
        Entry[] newTable = new Entry[newCapacity];
        //将当前数组中的键值对存入新数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //用新数组替换旧数组
        table = newTable;
        //计算下一个扩容临界点
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
复制代码
  /**
     * 将现有数组中的内容重新通过hash计算存入新数组
     * @param newTable 新数组
     * @param rehash  
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //遍历现有数组中的每一个单链表的头entry
        for (Entry<K,V> e : table) {
            //查找链表里的每一个entry
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //根据新的数组长度,重新计算此entry所在下标i
                int i = indexFor(e.hash, newCapacity);
                //将entry放入下标i处链表的头部(将新数组此处的原有链表存入entry的next指针)
                e.next = newTable[i];
                //将链表存回下标i
                newTable[i] = e;
                //查看下一个entry
                e = next;
            }
        }
    }
复制代码

6 数据读取(get)

/**
     * 返回此hashmap中存储的键值对个数
     * @return 键值对个数
     */
    public int size() {
        return size;
    }
复制代码
    /**
     * 根据key找到对应value
     * @param key 键值名
     * @return 键值value
     */
    public V get(Object key) {
        //如果key为null,则从table[0]中取value
        if (key == null)
            return getForNullKey();
        //如果key不为null,则先根据key,找到其entry
        Entry<K,V> entry = getEntry(key);
        //返回entry节点里的value值
        return null == entry ? null : entry.getValue();
    }
复制代码
    /**
     * 返回一个set集合,里面装的都是hashmap的value。
     * 因为map中的key不能重复,set集合中的值也不能重复,所以可以装入set。
     * 在hashmap的父类AbstractMap中,定义了Set<K> keySet = null;
     * 如果keySet为null,则返回内部类KeySet。
     * @return 含有所有keyset集合
     */
    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }
复制代码
    /**
     * 返回一个Collection集合,里面装的都是hashmap的value。
     * 因为map中的value可以重复,所以装入Collection。
     * 在hashmap的父类AbstractMap中,定义了Collection<V> values = null;
     * 如果values为null,则返回内部类Values。
     */
    public Collection<V> values() {
        Collection<V> vs = values;
        return (vs != null ? vs : (values = new Values()));
    }
复制代码

7 移除数据(clear remove)

    /**
     * 删除hashmap中的所有元素
     */
    public void clear() {
        modCount++;
        //将table中的每一个元素都设置成null
        Arrays.fill(table, null);
        size = 0;
    }
复制代码
/**
     * 根据key删除entry节点
     * @param key 被删除的entry的key值
     * @return 被删除的节点的value,删除失败则返回null
     */
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
复制代码
/**
     * 根据key删除entry节点
     * @param key 被删除的entry的key值
     * @return 被删除的节点,删除失败则返回null
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        //计算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        //计算所属下标
        int i = indexFor(hash, table.length);
        //找到下标所存储的单链表的头节点
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
        //迭代单链表找到要删除的节点
        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[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }
复制代码

8 Fail-Fast机制

“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。 注意 :记住是有可能,而不是一定。 例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。

四 JDK1.7和1.8 HashMap的区别

1 数据结构

使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构。如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个桶位。如果同一个桶位里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(logn)的开销。也就是说put/get的操作的时间复杂度最差只有O(log n)。
注意:有一个限制:key的对象,必须正确的实现了Compare接口。如果没有实现Compare接口,或者实现得不正确(比方说所有Compare方法都返回0)。那JDK1.8的HashMap其实还是慢于JDK1.7的。

2 hash算法

/**
   * @param key 
   * @return 
   */
static final int hash(Object key) {
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改