Java HashMap源码简析

来源:互联网 发布:javascript 书 编辑:程序博客网 时间:2024/06/07 04:54

Java HashMap的核心是数组加链表结构,数组主要用于存储和快速寻址,链表主要用户解决冲突。当然这是概念上的,源码体现的更加详细和丰满,下面是一些源码关键点:

成员变量:

     **     * 默认初始容量必须是2的幂(性能问题,后面会涉及)     */    static final int DEFAULT_INITIAL_CAPACITY = 16;    /**     * 最大容量     */    static final int MAXIMUM_CAPACITY = 1 << 30;    /**     * 默认负载参数     */    static final float DEFAULT_LOAD_FACTOR = 0.75f;        //Entry类型数组    transient Entry[] table;


Entry类:

static class Entry<K,V> implements Map.Entry<K,V> {        final K key;        V value;        Entry<K,V> next;        final int hash;        ……}
next是实现链表的关键,HashMap里的链表是个单链表,后插入的在链表头,先插入的在链表尾。


get方法:

public V get(Object key) {        if (key == null)            return getForNullKey();        int hash = hash(key.hashCode());        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.equals(k)))                return e.value;        }        return null;    }
注意到get方法是通过indexFor方法来寻找数组的索引号,indexFor的参数是数组长度和对key的hashCode再次hash的结果,找到数组索引号后遍历链表寻找元素。


hash方法:

static int hash(int h) {        // 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).        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }

indexFor方法:

static int indexFor(int h, int length) {        return h & (length-1);    }
根据key定位数组的索引号方法很多,比如我们都很熟悉的取模方法(数组长度模key的hashCode)。源码的做法是对key的hashCode再次进行哈希,然后与数组长度-1相与。

为什么要对key的hashCode再次哈希?源码里的javadoc的解释是避免过差的key的hashCode方法。我认为跟indexFor采用的跟数组长度-1相与来寻址有关,试想如果数组长度16(数组长度-1的二进制表示为:1111,1111),但是我的key为(1111,1111,1111),(1110,1111,1111),(1101,1111,1111),是不是三个key都映射到同一个数组索引号了?再次hash的原因大概如此,更详细原理可以参考:http://www.365doit.com/all/news/hashmapdeep.html。

那么为什么不用取模方法用相与呢?因为相与操作比取模性能更好。那么为什么是跟数组长度-1相与呢?因为在设置数组长度为2的幂的情况下,用数组长度-1相与可以减少冲突。更详细的可以参考:http://www.iteye.com/topic/539465


put方法:

public V put(K key, V value) {        if (key == null)            return putForNullKey(value);        int hash = hash(key.hashCode());        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;            }        }        modCount++;        addEntry(hash, key, value, i);        return null;    }
put方法源码现实很清晰的说明了一个事情:插入的key value pair如果key重复了,新的value将覆盖老的value。如果key不重复,那么说明冲突了,方法addEntry把元素加入到hashMap,下面看看addEntry的源码:

void addEntry(int hash, K key, V value, int bucketIndex) {        Entry<K,V> e = table[bucketIndex];        table[bucketIndex] = new Entry<>(hash, key, value, e);        if (size++ >= threshold)            resize(2 * table.length);    }

取出原来数组里面已经有的Entry对象,作为参数构造一个新的Entry,放回数组。这个Entry的构造函数式怎么样的?

Entry(int h, K k, V v, Entry<K,V> n) {            value = v;            next = n;            key = k;            hash = h;        }
真相大白,它把原来的Entry作为新插入Entry的next引用。即在冲突的情况下,新元素在链表头,老元素在链表尾。














0 0