JDK1.7 HashMap源码分析

来源:互联网 发布:淘宝原创衣服怎么弄 编辑:程序博客网 时间:2024/06/06 17:12

JDK1.7中HashMap采用的是数组+链表结构保存所有数据,其结构如下图:
JDK1.7之前HashMap结构
在JDK1.8 HashMap源码分析中已经分析了JDk1.8中HashMap的结构更改,但是和JDk1.7之前的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);        this.loadFactor = loadFactor;        threshold = initialCapacity;//初始阈值为初始容量,与JDK1.8中不同,JDK1.8中调用了tableSizeFor(initialCapacity)得到大于等于初始容量的一个最小的2的指数级别数,比如初始容量为12,那么threshold为16,;如果初始容量为5,那么初始容量为8        init();//空实现    } public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    } /**     * Constructs an empty <tt>HashMap</tt> with the default initial capacity     * (16) and the default load factor (0.75).     */    public HashMap() {        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);    }  public HashMap(Map<? extends K, ? extends V> m) {        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);        inflateTable(threshold);        putAllForCreate(m);    }

从上面可以看到,使用无参构造方法时,和JDK1.8的相同,默认的初始容量为16,默认的加载因子为0.75。但是在两个参数的构造方法时,实现稍有不同,关键是初始阈值的赋值。上面注释中已经说明了。

hash方法

JDK1.8中的hash方法如下:

static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    }

而JDK1.7中的hash方法如下:

final int hash(Object k) {        int h = hashSeed;//默认为0        if (0 != h && k instanceof String) {            return sun.misc.Hashing.stringHash32((String) k);        }        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).        h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }

从上面可以看到JDk1.8中的hash方法实现简单得多。

基本操作

put方法

HashMap中的put(K k,V v)方法用于将一对键值对插入到哈希表中,返回的键对应的旧的值。实现如下:

static final Entry<?,?>[] EMPTY_TABLE = {};    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; public V put(K key, V value) {        //哈希表还未被创建时,相同        if (table == EMPTY_TABLE) {            inflateTable(threshold);//创建哈希表        }        //如果键是null,调用putForNullKey方法        if (key == null)            return putForNullKey(value);        //计算hash值        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;            }        }        modCount++;        //桶中还不存在链表或者没有键值相同的,直接添加一个链表节点        addEntry(hash, key, value, i);        return null;    }    //插入键为null的值    private V putForNullKey(V value) {        //可以看到键为null的值永远被放在哈希表的第一个桶中        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            //一旦找到键为null,替换旧值            if (e.key == null) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        //如果第一个桶中为null或没有节点的键为null的,插入新节点        modCount++;        addEntry(0, null, value, 0);        return null;    }

从上面的代码可以看到put(K k,V v)有几步操作:
1. 如果哈希表还未创建,那么创建哈希表
2. 如果键为null,那么调用putForNullKey插入键为null的值
3. 如果键不为null,计算hash值并得到桶中的索引数,然后遍历桶中链表,一旦找到匹配的,那么替换旧值
4. 如果桶中链表为null或链表不为null但是没有找到匹配的,那么调用addEntry方法插入新节点

下面先分析当哈希表还未创建时调用的inflateTable()方法,其实现如下:

  private void inflateTable(int toSize) {        // Find a power of 2 >= toSize        int capacity = roundUpToPowerOf2(toSize);        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);        table = new Entry[capacity];        initHashSeedAsNeeded(capacity);//初始化hashSeed变量    } private static int roundUpToPowerOf2(int number) {        // assert number >= 0 : "number must be non-negative";        return number >= MAXIMUM_CAPACITY                ? MAXIMUM_CAPACITY                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;    }

从上面可以看到,初始化表是一个方法,其中也是根据初始容量得到不小于自己的最小的2的指数倍数的数,这个方法与1.8中tableSizeFor功能是一样的,所以初始化表时是相同的。
下面再分析addEntry()方法,其中第一个参数是hash值,第二个是键,第三个是值,第四个是哈希表中桶的索引。

void addEntry(int hash, K key, V value, int bucketIndex) {        //如果尺寸已将超过了阈值并且桶中索引处不为null        if ((size >= threshold) && (null != table[bucketIndex])) {            //扩容2倍            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);        //尺寸+1        size++;    }

从上面可以看到新加的节点将是作为头节点加入到链表中的,这点是与JDk1.8中的区别。另外,1.7的扩容是插入之前之前判断,而1.8是插入之后再判断是否需要扩容,不过都是扩容2倍。下面再来看resize()方法的实现:

//扩容到新容量 void resize(int newCapacity) {        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        //如果旧容量已经达到了最大,将阈值设置为最大值,与1.8相同        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);    }//如果hashSeed变了,那么rehash为true,否则为falsevoid transfer(Entry[] newTable, boolean rehash) {        int newCapacity = newTable.length;        //遍历旧表        for (Entry<K,V> e : table) {            //当桶不为空            while(null != e) {                Entry<K,V> next = e.next;                //如果hashSeed变了,需要重新计算hash值                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;            }        }    }

从上面的代码可以看到resize()中需要完成对hashSeed变量的更新,一旦更新成功,那么就需要rehash,否则不需要。而将链表转换时,新表中的链表与原链表中的顺序将会颠倒。这儿可以看到与1.8中的区别,1.7中是通过控制hashSeed的变化导致hash()方法得到的hash值,而JDK1.8中一旦得到了一个键的hash值后,就不会再改变了,而是通过hash&cap==0为区分,将链表分散,而1.7是通过更新hashSeed将旧表中的链表分散。
至此,我们可以发现JDK1.7中很多与1.8的实现区别,如下:
1. JDK1.8中resize()方法在表为空时,创建表,在表不为空时,扩容;而JDK1.7中resize()方法负责扩容,inflateTable()负责创建表
2. 1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表中第一个桶中,这一点两个版本是相同的。
3. 当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表了尾部,而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法。
4. addEntry中默认将新加的节点作为链表的头节点,而1.8中会将新加的结点添加到链表末尾
5. 1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的分散,而1.8中键的hash值不会改变,rehash时根据hash&cap==0将链表分散
6. 1.8rehash时保证原链表的顺序,而1.7中rehash时将改变链表的顺序

get方法

HashMap的get方法如下:

public V get(Object key) {        //如果键为null,调用getForNullKey方法        if (key == null)            return getForNullKey();        //键不为null,调用getEntry方法        Entry<K,V> entry = getEntry(key);        return null == entry ? null : entry.getValue();    }private V getForNullKey() {        if (size == 0) {            return null;        }        //键为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;        }        //计算hash值        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;    }

从上面的代码可以看到,get()方法与1.8中差别不大,只是区分出了键是否为null的情况,而1.8中则不区分这种情况。

remove方法

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;        }        //计算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;    }

从remove()方法可以看到与1.8中的实现也差距不大。

总结

下面对JDK1.7和JDk1.8中HashMap的相同与不同点做出总结。
首先是相同点:
1. 默认初始容量都是16,默认加载因子都是0.75。容量必须是2的指数倍数
2. 扩容时都将容量增加1倍
3. 根据hash值得到桶的索引方法一样,都是i=hash&(cap-1)
4. 初始时表为空,都是懒加载,在插入第一个键值对时初始化
5. 键为null的hash值为0,都会放在哈希表的第一个桶中

接下来是不同点,主要是思想上的不同,不再纠结与实现的不同:
1. 最为重要的一点是,底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构
2. 主要区别是插入键值对的put方法的区别。1.8中会将节点插入到链表尾部,而1.7中会将节点作为链表的新的头节点
3. JDk1.8中一个键的hash是保持不变的,JDK1.7时resize()时有可能改变键的hahs值
4. rehash时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序
5. JDK1.8是通过hash&cap==0将链表分散,而JDK1.7是通过更新hashSeed来修改hash值达到分散的目的

0 0
原创粉丝点击