Java集合源码分析--HashMap

转载自 http://www.cnblogs.com/zhangyinhua/p/7698642.html#_label0

一,关于HashMap API定义

1、哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value可以为null,(HashMap和HashTable大致上是一样的除了hashmap是异步的和允许key和value为null),
这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。
2.
HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子。

二,HashMap 的属性


HashMap的实例有两个参数影响其性能。

  初始容量:哈希表中桶的数量  加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度

  当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充至两倍的桶数。

//Java中默认初始容量为16,加载因子为0.75。        
HashMap hm=new HashMap(); for(int i=0;i<17;i++){ hm.put(i,i); } System.out.println(hm.size());//17 System.out.println(hm.table.length);//32

 1)loadFactor加载因子

    定义:loadFactor译为装载因子。装载因子用来衡量HashMap满的程度,是控制数组存放数据的疏密程度。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。

    loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,但我们在通过key拿到我们的value时,是先通过key的hashcode值,找到对应数组中的位置,

如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高,

    loadFactor越趋近于0那么数组中存放的数据也就越稀,分散的太开,浪费很多空间,这样也不好,

所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。

  2)桶

    数组中每一个位置上都放有一个桶,每个桶里就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。

  3)capacity

    capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是16。

  4)size的含义

    size就是在该HashMap的实例中实际存储的元素的个数

  5)threshold的作用

    threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准

    注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条件。

    什么时候会扩增数组的大小?在put一个元素时先size>=threshold并且还要在对应数组位置上有元素,这才能扩增数组。

二、HashMap的源码分析

2.1、HashMap的层次关系与继承结构

  1)HashMap继承结构

    

  上面就继承了一个abstractMap,也就是用来减轻实现Map接口的编写负担。

  2)实现接口

    

    Map<K,V>:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合都有这样的错误,也没过大影响

    Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响被拷贝的对象。

    Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。

2.2、HashMap类的属性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;    
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8; 
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table; 
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;   
    // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;
    // 填充因子
    final float loadFactor;
}

2.3、HashMap的构造方法

  有四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次数组的长度。)和初始化加载因子。注意,hashMap中table数组一开始就已经是个没有长度的数组了。

  构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个是初始化加载因子,另一个是用threshold记录下数组初始化的大小。注意是记录。

  

三、HashMap源码分析(二)

3.1、put方法

   /* 
     * 1. 通过key的hash值确定table下标
     * 2. 查找table下标,如果key存在则更新对应的value
     * 3. 如果key不存在则调用addEntry()方法
     */  
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

        if (key == null)
            return putForNullKey(value);

        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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;
            }
        }
        // key不存在时,加入新元素  
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

3.2、get方法

final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

3.3、resize方法

 /**
     * 扩展到指定的大小 
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        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);
    }

四、总结

4.1、关于数组扩容

  从putVal源代码中我们可以知道,当插入一个元素的时候size就加1,若size大于threshold的时候,就会进行扩容。假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75,

  此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还有31个桶是空的,也会进行扩容处理,其实,此时,还有31个桶是空的,好像似乎不需要进行扩容处理,

  但是是需要扩容处理的,因为此时我们的capacity大小可能不适当。我们前面知道,扩容处理会遍历所有的元素,时间复杂度很高;前面我们还知道,经过一次扩容处理后,元素会更加均匀的分布在各个桶中,

  会提升访问效率。所以,说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于元素在桶中均匀分布所带来的好处。 

4.2、总结

  1)要知道hashMap在JDK1.8以前是一个链表散列这样一个数据结构,而在JDK1.8以后是一个数组加链表加红黑树的数据结构。

  2)通过源码的学习,hashMap是一个能快速通过key获取到value值得一个集合,原因是内部使用的是hash查找值得方法。

原文地址:https://www.cnblogs.com/mcahkf/p/8891441.html