码迷,mamicode.com
首页 > 其他好文 > 详细

HashMap源码解析

时间:2016-05-13 20:50:03      阅读:467      评论:0      收藏:0      [点我收藏+]

标签:

本解析源码来自JDK1.7

HashMap概要

  • HashMap是基于hash的map接口的非同步实现,允许使用null键和null值,不保证映射顺序
  • HashMap类头部
public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
  • 设计初衷
    Java中的两种存储结构
    数组:寻址容易,插入和删除困难
    链表:寻址困难,插入和删除容易
    折中方案,链表的数组,即散列表是HashMap的底层存储数据结构

Entry HashMap的基本节点

  • HashMap的静态内部类Entry,主要成员 Key,Value,next,hash
  • table Entry的数组,Entry包含指向下一节点的引用next,从而table为链表的数组
 /**
     * 大小必要时可调整的数组。 其长度必须是2的指数次.
     */ 
 transient Entry<K,V>[] table;   //transient 序列化时不被序列化
 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    ...........
}

构造方法

主要包含两种构造方法:
- HashMap(int initialCapacity, float loadFactor)
- initialCapacity table数组的初始化大小,默认为16,如果不是2的指数次幂,调整为大于initialCapacity的最小的2的指数幂,但是不能大于MAXIMUM_CAPACITY(默认为2^30)
- loadFactor 影响table容量调整,当数组容量*loadFactor

    /**根据指定容量和负载因子构造HashMap*/ 
    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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

    /**根据指定的容量和默认的负载因子构造HashMap*/
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //默认的空的构造器
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    /**通过指定一个Map对象进行构造*/
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

存入更新元素 put(key,value)

  • 如果是key为null,查找table中key是否有null,如果有更新value,否则添加null,value节点
  • 如果key不为null,根据Key的hashcode获取Hash值,根据Hash计算其在table中的索引,hash值计算时利用高位与低位进行异或操作,加入高位因素,来减少Hash碰撞
  • 由于tablelength 都是2的指数次幂,所以indexFor用 HashCode&(table.lenght-1)取HashCode的低位
  • 如果table[i]不为null(并不表示Hash值相同,HashCode不同也可能碰撞),也就是发生了Hash碰撞,如果存在与keyHash相等(equals)或相同(==)的key,那么更新value
  • 如果table[i]为null,或者table[i]链表中不存在Hash值与Key相同且equals函数返回true的情况就根据Hash值添加新的节点
  • addEntry()方法首先判断大小是否超过阈值,然后使用头插法,插入元素
    NOTE
    在判断插入Entry是否为覆盖时,会先判断Key的hashCode是否和map中的key相等,然后判断Equals方法或者==,所以如果重写了equals方法,要记得重写hashcode方法,使得其逻辑相同,否则即使equals方法判断相等也不会发生覆盖
public V put(K key, V value) {
    // HashMap允许存放null键和null值。
    // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
    if (key == null)
        return putForNullKey(value);
    // 根据key的keyCode重新计算hash值。
    int hash = hash(key);//注意这里的实现是jdk1.7和以前的版本有区别的
    // 搜索指定hash值在对应table中的索引。
    int i = indexFor(hash, table.length);
    // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
    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;
        }
    }
    // 如果i索引处的Entry为null,表明此处还没有Entry。
    modCount++;
    // 将key、value添加到i索引处。
    addEntry(hash, key, value, i);
    return null;
}
/**产生哈希码*/
final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        /*加入高位计算,防止低位不变,高位变化是引起hash冲突*/
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
/**产生索引,由于索引产生是不确定的,因此也就造成了HashMap顺序的不确定性。
   需要注意的是不同的hash产生的索引完全有可能相同的
  该方法的实现十分的巧妙,它通过h & (length-1)来的到对象保存的
  索引,有可知道底层数组为2的n次方,这在速度上就有了明显的优化
  */
static int indexFor(int h, int length) {
        return h & (length-1);
    }
    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, 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++;
    }  

读取元素 get(key)

  • 如果key为null,不能使用hash码来定址,只能通过遍历table的方法来找null
  • 如果key不为null,就可以通过计算key的hash值定位到table数组中对应的链表,然后遍历链表找到hash值和equals方法返回true或==成立的节点
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    } 
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }  
    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;
    } 

元素删除 remove(key)

  • 根据key的hash值定位table数组位置,然后遍历链表,找到hash值与equals为true或==成立点,做链表删除操作
  • 其中删除注意头结点的处理
    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);
        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;
    } 

元素查找 containsKey(key) containsValue(value)

  • containsKey(key) 先用key的hash码定位到table数组中的对应链表,然后遍历链表进行查询,效率取决于链表的长度
  • containsValue(value) 对table数组进行遍历,然后遍历数组中的每个链表,时间复杂度取决于table数组的长度与map.size(),效率低
    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    } 
    private boolean containsNullValue() {
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }  
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }
    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;
    } 

调整大小 resize(newLength)

  • 调整的时机 (负载因子)x(容量)>(Map 大小),则调整 Map大小 为之前的二倍,该过程包含了table的复制,性能消耗较大,如果map大小已知,可以在初始化时合理设定map的初始大小,避免扩容。
  • 如果数组大小已经到达最大容量,将阈值置为Integer.MAX_VAlUE,不再进行扩容
  • 新申请数组,重新定址并将原数组中的Entry转移到新数组中,由于容量变化,即使Hash值不变,Entry的index也会改变,index=hash&(length-1),取hash的低位,length增大,index取的位数增多
  • 依旧使用头插法将所有元素进行复制
    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);
    } 
    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    } 

与JDK1.8的区别

  • JDK1.8不再单纯使用链表来进行存储,而是使用链表(元素较少)与红黑树(元素较多)
    在jdk8中,仍然会根据key.hashCode()计算出hash值,再通过这个hash值去定位这个key,但是不同的是,当发生冲突时,会采用链表和红黑树两种方法去处理,当结点个数较少时用链表(用Node存储),个数较多时用红黑树(用TreeNode存储),同时结点也不叫Entry了,而是分成了Node和TreeNode。再最坏的情况下,链表查找的时间复杂度为O(n),而红黑树一直是O(logn),这样会提高HashMap的效率。jdk8中的HashMap中定义了一个变量TREEIFY_THRESHOLD,当节点个数>= TREEIFY_THRESHOLD - 1时,HashMap将采用红黑树存储
  • 不再区别对待String类key
    由于String对象的Hash值计算方法较弱,jdk7中在面对key为String的时候采用了区别对待,会有alternative hashing,但是这个在jdk8中已经被删除了

与HashTable的区别

  • 继承的对象不同 HashMap extends AbstractMap HashTable extends Dictionary
  • HashTable 是同步的,且不允许key为null,其根据hash值获得索引的方法也不同,都是取模,但是HashMap采用位运算更高效
  public synchronized V put(K key, V value) {  //###### 注意这里1
    // Make sure the value is not null
    if (value == null) { //###### 注意这里 2
      throw new NullPointerException();
    }
    // Makes sure the key is not already in the hashtable.
    Entry tab[] = table;
    int hash = key.hashCode(); //###### 注意这里 3
    int index = (hash & 0x7FFFFFFF) % tab.length;### 注意这里 4
    for (Entry e = tab[index]; e != null; e = e.next) {
      if ((e.hash == hash) && e.key.equals(key)) {
        V old = e.value;
        e.value = value;
        return old;
      }
    }
    modCount++;
    if (count >= threshold) {
      // Rehash the table if the threshold is exceeded
      rehash();
      tab = table;
      index = (hash & 0x7FFFFFFF) % tab.length;
    }
    // Creates the new entry.
    Entry e = tab[index];
    tab[index] = new Entry(hash, key, value, e);
    count++;
    return null; 
}

解决hash碰撞的方法

  • 开放定址法
    从发生冲突的那个单元开始,按照一定的次序,从散列表中查找出一个空闲的存储单元,把发生冲突的待插入元素存入到该单元中的一类处理冲突的方法。
  • 再哈希法
  • 链地址法(JDK1.7使用)
  • 建立一 公共溢出区

modCount fast-fail机制

modCount 记录修改此列表的次数:包括改变列表的结构,改变列表的大小,打乱列表的顺序等使正在进行迭代产生错误的结果.(仅仅设置元素的值并不是结构的修改),如果在使用迭代器的过程中有其他的线程修改了Map就会抛出ConcurrentModificationException这就是Fail-Fast机制。

序列化

  • 存储hashmap的元素的数组被声明为transient,即在初始化时忽略,因为相同对象的Hash值在不同机器上可能是不同的,所以直接序列化后map的get(key)方法会出现错误
  • HashMap重写了readObject()和writeObject()方法来保证序列化的正确性
  • 策略在序列化时,针对Entry的key与value分别单独序列化,当反序列化时,再单独处理即可
 transient Entry<K,V>[] table;   //transient 序列化时不被序列化
private void writeObject(java.io.ObjectOutputStream s)
    throws IOException
{
    // Write out the threshold, loadfactor, and any hidden stuff
    s.defaultWriteObject();

    // Write out number of buckets
    if (table==EMPTY_TABLE) {
        s.writeInt(roundUpToPowerOf2(threshold));
    } else {
       s.writeInt(table.length);
    }

    // Write out size (number of Mappings)
    s.writeInt(size);

    // Write out keys and values (alternating)
    if (size > 0) {
        for(Map.Entry<K,V> e : entrySet0()) {
            s.writeObject(e.getKey());
            s.writeObject(e.getValue());
        }
    }
}

private static final long serialVersionUID = 362498820763181265L;

private void readObject(java.io.ObjectInputStream s)
     throws IOException, ClassNotFoundException
{
    // Read in the threshold (ignored), loadfactor, and any hidden stuff
    s.defaultReadObject();
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new InvalidObjectException("Illegal load factor: " +
                                           loadFactor);
    }

    // set other fields that need values
    table = (Entry<K,V>[]) EMPTY_TABLE;

    // Read in number of buckets
    s.readInt(); // ignored.

    // Read number of mappings
    int mappings = s.readInt();
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                           mappings);

    // capacity chosen by number of mappings and desired load (if >= 0.25)
    int capacity = (int) Math.min(
                mappings * Math.min(1 / loadFactor, 4.0f),
                // we have limits...
                HashMap.MAXIMUM_CAPACITY);

    // allocate the bucket array;
    if (mappings > 0) {
        inflateTable(capacity);
    } else {
        threshold = capacity;
    }

    init();  // Give subclass a chance to do its thing.

    // Read the keys and values, and put the mappings in the HashMap
    for (int i = 0; i < mappings; i++) {
        K key = (K) s.readObject();
        V value = (V) s.readObject();
        putForCreate(key, value);
    }
}
private void putForCreate(K key, V value) {
    int hash = null == key ? 0 : hash(key);
    int i = indexFor(hash, table.length);

    /**
     * Look for preexisting entry for key.  This will never happen for
     * clone or deserialize.  It will only happen for construction if the
     * input Map is a sorted map whose ordering is inconsistent w/ equals.
     */
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }

    createEntry(hash, key, value, i);
}

HashMap源码解析

标签:

原文地址:http://blog.csdn.net/stubbornant/article/details/51396437

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!