美文网首页
HashMap源码解析 三

HashMap源码解析 三

作者: 代码potty | 来源:发表于2018-09-16 18:23 被阅读0次

今天主要解析四个地方

1、getNode()方法
2、removeNode()方法
3、clone()方法
4、entrySet()、keySet()、values()

getNode()方法

getNode()方法是HashMap的内部方法,参数有两个,第一个参数是hash值,第二个参数是key,hash值是根据key来进行计算的,具体的代码如下:

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //先判断首节点的hash、key是否相同
            if (first.hash == hash &&
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

这段代码没什么可以讲的地方,主要的判断条件就是比较hash和key的值,先判断首节点的hash和key,如果匹配成功,则返回first,否则继续判断,一直到链尾都没有匹配的节点则返回Null。


removeNode()方法

removeNode()方法前半段跟getNode()的作用是相同的,根据hash和key来找到需要移除节点的位置,然后根据参数需求进行进一步的判断,参数列表中有个参数是matchValue,它的作用是提供给用户是否需要匹配value的值,正对应两个方法,一个是remove(Object key),另一个是remove(Object key,Object value)这两个方法传入的值如下:

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }


    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

我们可以看到,后一个方法需要对key所在位置的值进行判断,这样将更加的准确,减少误删的概率,如果只传入一个key,那么将默认不匹配value就进行删除工作,removeNode()的代码如下:

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //这部分跟getNode()方法差不多
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
              //注意e,node,p三个对象的关系
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //根据key获取到node的位置,然后进行删除工作
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //如果node==p,表示,首节点匹配key,那么直接将首节点移动到node.next位置即可
                else if (node == p)
                    tab[index] = node.next;
                //如果不是首节点匹配,就是首节点之后的元素进行匹配
                //那么将上一个节点的next跨过匹配节点到下个节点即可
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }   

这段代码需要注意一个点,那就是根据key获取node节点的时候,跟getNode()有个地方不一样,不一样的地方在,获取到node节点的时候,他既需要保存匹配节点,还需要保存匹配节点的前一个节点的位置,所以在匹配的时候有三个对象,node对象用来保存匹配对象,p对象用来保存node对象的前一个对象,e用来充当指针的效果,一个节点一个节点的往下移动。


clone()方法

HashMap的clone()是浅拷贝的,什么是浅拷贝?
对一个对象进行clone生成新的对象,新的对象要开辟一块新的内存来存储,新对象中的基本类型属性和String类型属性都会开辟新的空间存储,但是如果是引用类型的属性,那这个引用类型的属性还是指向原对象的引用属性内存,当对新的对象或原对象的引用属性做出改变的时候,两方的引用属性类型的值同时做出改变。
也就是说,拷贝后的HashMap对象是新创建的,但是里边的key和value指向的内存地址是一样的,在源码中也有如下的注释:

     /**
      * Returns a shallow copy of this <tt>HashMap</tt> instance: the keys and
      * values themselves are not cloned.
      *
      * @return a shallow copy of this map
      */

这段注释表示,HashMap的key和value值是不会被克隆的,好我们看下代码实现:

public Object clone() {
    HashMap<K,V> result;
    try {
        result = (HashMap<K,V>)super.clone();
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
    result.reinitialize();
    result.putMapEntries(this, false);
    return result;
}

void reinitialize() {
    table = null;
    entrySet = null;
    keySet = null;
    values = null;
    modCount = 0;
    threshold = 0;
    size = 0;
}

为了进一步加深对于浅拷贝的理解,我写了个测试代码,代码如下:

    public static void main(String[] args) {
        HashMap<String, Goods> map = new HashMap<>(20);
        map.put("1",new Goods("1","2"));
        map.put("Q",new Goods("2","3"));
        HashMap<String,Goods> mapT = (HashMap) map.clone();
        Goods goods = mapT.get("1");
        goods.setOutTradeNo("2");

        for (Map.Entry entry :map.entrySet()){
            System.out.println(entry);
        }
    }

因为key和value指向的内存空间不变,那意味着我们修改了goods对象的值,那么拷贝前的对象也会随之产生变化,控制台的输出结果如下:


image.png

从图中我们很容易看到,刚刚改变的outTradeNode的值变了,这就是浅拷贝的效果,HashMap中的key/values的内存地址不变。


entrySet(),keySet(),values()方法

我将这三个放一起讲,是因为这三个方法调用方法的过程其实是一样的,我们看下HashMap中对这三个方法的实现:

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}

从代码中也可以轻松看出,entrySet和keySet的返回值是Set集合,而values()方法的返回值是Collection类型。
这三个方法值得获取都需要借助它们在HashMap中的内部类中的迭代器来实现,也就是iterator()方法,三个内部类的iterator()方法都是创建迭代器对象,具体实现如下:

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }   

它们都继承了HashIterator类,并且实现了Iterator接口,方法中都调用了nextNode()方法,这个方法的实现在HashIterator类中,我们来看下这个类中我调试中遇到核心的两个方法,一个是nextNode()方法,另一个就是这个类的构造方法,代码如下:

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
        //找到第一个不为空的桶,将桶的节点赋给next
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        //如果遍历完当前桶,并且table不为空,那么继续找下一个不为空的桶进行操作
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

构造方法中,主要是通过循环找到第一个不为空的桶,然后将这个桶的首节点赋给next,然后当我们需要用到的时候,调用nextNode()方法,先判断next的指向的next是否为空,并且在table不为空的情况下,如果为空,进入if代码块,通过循环继续找下一个不为空的桶赋给next,如果不为空,则代表当前桶内还有节点没有遍历到,返回next,然后让next指向next.next。 keySet和values也是通过这种方式获取到key和value。

总结一下,HashMap的entrySet()方法返回一个特殊的Set,这个Set使用EntryIterator遍历,而这个Iterator则直接操作于HashMap的内部存储结构table上。通过这种方式实现了“视图”的功能。整个过程不需要任何辅助存储空间。

p.s. 从这一点也可以看出为什么entrySet()是遍历HashMap最高效的方法,原因很简单,因为这种方式和HashMap内部的存储方式是一致的。

参考链接:
https://blog.csdn.net/wangbiao007/article/details/52625099
https://www.cnblogs.com/leesf456/p/5250858.html
http://blog.sina.com.cn/s/blog_60efd9b70102vd5z.html
https://blog.csdn.net/luochoudan/article/details/51005997

相关文章

网友评论

      本文标题:HashMap源码解析 三

      本文链接:https://www.haomeiwen.com/subject/vydfnftx.html