新手对HashMap(jdk7)的一点理解

1,HashMap的底层实现:数组+链表。

数组的特点:遍历查询速度较快,但增加和删除的代价较大。

链表的特点:遍历查询速度较慢,但增加和删除的代价较小。

而HashMap的底层是数组和链表,很好地集成了这两者的优点。

2,HashMap数组及链表的产生:

数组:当创建一个HashMap对象时,Map<k,v> map=new HashMap<k,v>();就会调用相应的空构造方法(HashMap有四种构造方法),返回一个默认初始容量为16,默认加载因子为0.75的table数组。数组中的元素是链表。

链表:链表产生的原因是发生了碰撞(哈希冲突)。至于为什么会发生碰撞呢?在向HashMap存储数据的过程中,有两种情况:第一种是在某一链表的表头table[bucketIndex]插入键值对元素;另一种情况是新加入的键值对元素覆盖了此前的键值对元素。第一种情况就会造成碰撞,从而形成链表。

3,HashMap的数据结构:

HashMap的底层实现是一个Entry数组(Entry<K,V>[ ] table),而且数组中的每一个元素都是链表(由多个Entry<K,V>连接而成的链表)。

先来看看每一个Entry<K,V>的结构:

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * 构造
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

可以看出:Entry元素中有四个量,Key,Value,hash和next,通过next来构成连接的关系从而形成链表。每个元素的next都是对下一个元素的引用。

4,HashMap的重要方法实现:

(1)存储实现put(Key k, Value v):

底层代码:

public V put(K key, V value) {
        //key==null
        if (key == null)
            return putForNullKey(value);
        //key!=null
        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;
    }

HashMap的存储分为两种情况:

    1)传入的key参数为null:

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

    再来看看putForNullKey方法:

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;
    }

    由此可以看到,当传入的key为null时,就会调用putForNullKey方法。底层就会找到table数组的第一个下标的头结点(Entry<K,V> e = table[0])。如果头结点存在即不为空时,就遍历这个链表,寻找有没有key为null的结点:如果找到了,就把传入的value值覆盖此前这个位置结点的value,并且返回旧的value值;如果在遍历的过程中没有找到key为null的结点,就执行addEntry()方法。

    来看看addEntry方法:

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);
    }

    还有createEntry方法:

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++;
    }
       调用addEntry方法后的第一步就是判断当前容量是否超出了极限容量,如果超出了就增大数组的长度为原来的两倍,并且重新计算哈希值,再调用createEntry方法。从createEntry方法中我们可以知道,Entry<K,V> e = table[bucketIndex]语句找到了table数组某个索引下的头结点,在这个位置创建了一个新的键值对元素。这说明,如果在遍历的过程中没有找到key为null的结点时,就会在这个链表的表头创建一个新的结点元素,这个结点的key为null。

所以总结起来,当传入的key为null时,新的键值对可能被存储在table[0]的链表头,也可能被存储在table[0]链

表中的某一结点位置。

    2)传入的key参数不为null:

    来看看源码:

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;
 }

    当传入的key不为null时,底层会先把key所对应的hash值求出来,再根据这个值来求出它在table数组中的索引i。找到这个索引后,就通过Entry<K,V> e = table[i]来判断这个索引下链表的表头是否为空。如果表头不为空,则遍历这个链表,查找链表中是否存在某个元素的hash值与key值都与这个新插入的元素相匹配。如果找到,直接替换value并返回旧的value。如果找不到,就会执行头插(在链表头插入这个新元素)。

    经过了简单的分析,很容易得出HashMap的存储主要是实现两种方式:替换和头插。

    (2)读取实现get(key):

    先来看看源码

 public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
final Entry<K,V> getEntry(Object key) {
        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;
    }

    读取的实现和存储有类似之处。先把key值转化为对应的hash值,通过这个值来计算出对应在table数组的哪个索引。如果索引处的表头不为空,就遍历这个链表来寻找hash值、key值均匹配的元素。找得到就返回这个元素e,否则返回null。最后通过entry.getValue()来获取这个key所对应的值。

    (3)HashMap中键的遍历:

        这里只简单地说一个特点:遍历出来的结果是无序的。

        这和HashMap的存储有关。最后一个存储的元素应该是第一个或者最后一个被遍历的,但是这最后一个存储进去的元素可能在表头,也可能在链表中间的某一个位置,因此就会造成这种无序性。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值