Java源码学习--HashMap

Java源码学习--HashMap

由于HashSet的实现原理是HashMap,所以我们先从HashMap开始学起。

一、重要属性

HashMap的属性很多,比ArrayList还要多,这里就挑选一些重要的属性来说明一下。

静态属性

  • DEFAULT_INITIAL_CAPACITY:该属性是一个final的int,值为16,代表着默认初始容量
  • MAXIMUM_CAPACITY:该属性是一个final的int,值为2^30,代表最大容量
  • DEFAULT_LOAD_FACTOR:该属性是一个final的float,值为0.75f,在进行扩容的时候会用到

普通属性

  • table:该属性是一个Node[],和ArrayList类似,是HashMap的实际容器
  • size:该属性是一个int,代表当前容器中键值对的个数
  • threshold:该属性是一个int,大部分情况下为capacity*loadFactor
  • loadFactor:该属性是一个float,是用户在创建HashMap的时候指定的加载因子,限制table中可以存放元素所占table.length的比例,默认为0.75

二、Node类

HashMap中的key和value是作为一个Node进行存储的

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

    Node(int hash, K key, V value, Node next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry e = (Map.Entry)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}  

值得注意的有一点:Node的equals方法是将key和value的equals方法都返回true才返回true

三、重要方法

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;
    this.threshold = tableSizeFor(initialCapacity);
}  

可以看到,开始时检查传入的两个变量是否合法,之后便是简单的将两个属性初始化。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}  

这个重载的构造器只是第二个参数设置为了内置的默认值(0.75)

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}  

这是我常用的构造器,令人奇怪的是,本来以为会将两个参数都设置为内置的默认值,但是这里并不是回调第一个构造器,而是直接只初始化了loadFactor,其他属性不管了!!

还有一点要提醒的是:构造器干的事情太少了,只是处理了两个属性,根本没管table这个属性,所以到这里table还没有初始化(为null),而table的初始化就发生在第一次往HashMap中添加元素的时候,接下来先看看是如何初始化table的。

其在调用put方法添加元素的时候首先会有一个if语句单独处理table为null的情况:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;  
    ......  
}  

可见最终是在resize()方法处理的:

final Node[] resize() {
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node[] newTab = (Node[])new Node[newCap];
    table = newTab;
    return newTab;
}  

上面的代码是当使用无参构造器初始化HashMap且第一次调用put方法是resize()方法实际执行的代码(没有执行的代码删去了),此时HashMap的状态为:

  • table:初始化完毕,数组的长度为DEFAULT_INITIAL_CAPACITY=16
  • threshold:初始化为了(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)=0.75*16
  • loadFactor:在构造器中初始化为了DEFAULT_LOAD_FACTOR=0.75
hash方法

都知道HashMap的原理是哈希,就先来看看贯穿全文的hash方法的实现:

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

可见,HashMap用来进行坐标映射的值并不是key.hashCode()方法,而是(h = key.hashCode()) ^ (h >>> 16)

1. put方法

put方法的作用是将Map中特定的value和特定的key关联起来,如果key已经存在,则覆盖原来的value

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}  

可见put方法的实现是回调putVal方法

putVal

该方法的后两个参数在put方法调用的时候分别被赋予了false和true,该方法的注释中说:如果onlyIfAbsent被设置为了true,那么put就不会覆盖原来的value;如果evict被设置了false那么table属性处于正在建立的状态,我们先简单记住该属性一般情况调用的时候都是为true

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; 
    Node p;   // 为hash算得位置的节点对象
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;  

    -------------------重点 1-------------------
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node e; K k;
        
        --------------重点 2 ----------------
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
    
        --------------重点 3------------------
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }

        ----------------重点 4---------------
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }  

    ---------------重点 5----------------
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

该方法的实现有些长,需要关注的点有注释:

  • 重点 1:这里的if语句中透露出了HashMap中用作table数组下标的并不是hash方法生成的int,而是(table.length - 1) & hash(方法中的n实际为table.length这里直接替换了),这个等价于hash%(table.length)只不过按位操作更加快(注意:这里是因为n在后面可以看到,n永远都是2的幂,所以这里按位与就等价于取余)
  • 重点 2:这里的if语句的意义是,想要put的位置在table中已经有元素了,而且该位置的链表的头一个元素的key和putVal的参数是一致的(注意其判断条件:该节点的hash和参数一致,而且key的equals方法返回true,二者缺一不可)
  • 重点 3:能够进入该else语句说明,想要put的位置在table中已经有元素了,但是那个链表头部的key和参数中的key并不是同一个;该else分支里的第一个if语句的作用是:沿着table中该位置的元素链表往下找,最终添加到链表的尾部;第二个if语句的作用是如果在沿着链表找的过程中,有一个中间节点的key就是目标key,那么就停止往下找了
  • 重点 4:记得一点重点2、3、4中的代码能够执行的前提是想要put的位置已经有元素了;而重点2、3分别对应该位置链表头元素即为目标元素和相反情况;而经历了重点2、3之后,变量e即为需要put的元素的最终位置,这时候重点4只需执行写入value即可
  • 重点 5:该处的代码和重点4是互斥的,进入到这里说明put方法的结果是往table数组中添加了一个新元素,而不是在其某一个位置的链表尾部,这就涉及到是否需要扩容了

总结:从这里可以看到整个put方法的哈希过程:1. 首先根据hash值计算对应的table数组的下标,通过hash值除以数组长度区域简单的到 2. 然后从该位置开始比对key是否相同,一直到该位置的链表的最后 3. 写入value值
还有一点需要注意的是:HashMap在put的时候对value没有任何约束和检查;而对于key,即使为null也是ok的,hash对为null的key,直接返回0,而在后面的对比key的时候null之间==直接返回true比对成功,所以key为null的永远只会占用一个位置

2. get方法

get方法是另外一个重要的核心方法,其作用是返回和特定的key绑定的value

public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}  

可见,其具体的实现是在getNode方法

getNode

该方法的两个参数的作用显而易见:

final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {  

        -----------------重点 1-------------------
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode)first).getTreeNode(hash, key);
            do {
                ---------------------重点 2---------------------
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}  

该方法的实现很简单:从first位置沿着链表一个一个找就是了,没有什么难点,唯一需要注意的就是判断是否找到的依据是该节点的hash和参数一致,而且key的equals方法返回true,二者缺一不可

总结:1. HashMap的put和get方法都支持key为null的情况 2. 通过get和put方法判断是否找到的依据我们可以理解在学习Java的时候强调的在重写hashCode方法以及equals方法时的要点:当equals方法返回true的时候,其hashCode方法返回的值一定要相同,否则作为key的时候value放入HashMap之后就找不到了;但是hashCode返回值相同并不要求equals方法返回为true。(equals返回true但hash不同并不会导致bug和程序崩溃,但是它违反了hash的初衷,hash的初衷就是为每一个key维护一个value,如果hash不同的话可能出现有两个相同的key同时存在)

3. resize

上面说到put方法时,其重点5处的代码就是进行扩容操作,接下来就来看看resize方法是如何实现扩容的:

final Node[] resize() {
    Node[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }  

        -----------------重点 1-------------------
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }  

    ------------------重点 2--------------------
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node[] newTab = (Node[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            if ((e = oldTab[j]) != null) {  

                -----------------重点 3--------------------
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node next;  

                    -----------------重点 4------------------
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);  

                    -------------------重点 5--------------------
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}  

该方法有些长,我们还是通过几个重点来将其分割成几个小块:

  • 重点 1:此处实现了将newCap设置成oldCap的二倍,而oldCap为table.length;将newThr设置成了oldThr的二倍,而oldThr为HashMap的threshold属性(这里可以看出capacity被维护成了2的幂,所以才能够支持通过按位与来等价取余操作)
  • 重点 2:此处将HashMap的threshold属性设置为了newThr(也就是原来的两倍);同时,将table属性重新赋值为一个length为原来两倍的新数组
  • 重点 3:首先要注意的是这里第一句将原来数组该位置设置为了null,不仅想起ArrayList在扩容的时候也会将老数组的元素都置为空,还是为了能够更早的进行垃圾回收,这是一个优秀的习惯--把废弃的对象置为null
  • 重点 4:这里是一个doWhile循环,是用来处理链表的;其中e代表当前的链点,大多数情况下hiHead代表的是新链表的头部,hiTail代表新链表的尾部;而有些情况下((e.hash & oldCap) == 0),loHead代表的是新链表的头部,loTail代表新链表的尾部
  • 重点 5:与其说是重点,不如说是疑点,从重点4处的对特殊情况的处理((e.hash & oldCap) == 0);我们发现,新的table中将重点 4中特殊情况的节点都分布在了[0, oldCap]范围内;而其他的分在了剩余的地方没有看懂这个操作的深意,猜测是将所有链表更加均匀的分布在新的table数组中?

疑问:创建新的table自然希望将所有的链表“削的更短”,那为什么不将原链表上每一个节点通过hash值重新计算其在新的table数组中的位置呢,而是还将其保留在原链表上?

4. threshold和loadFactor属性的作用

上面提到过在使用无参构造器初始化HashMap对象的时候table数组的初始化时在第一次putVal的时候,回调resize()方法进行的。在resize()方法中第一组if-else语句由于oldCap和oldThr都为0,所以进入了最终的else分支

newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  

而在putVal方法中每次添加完元素后,最后会有一个判断是否需要resize():

if (++size > threshold)
     resize();  

从threshold被初始化为(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)时,以及判断是否需要resize扩容的条件(++size > threshold),可以看出:

  • loadFactor:该属性的意义是限定何时扩容的上限,当table中元素的个数超过这个比例的时候为了减少“冲突”就需要进行扩容,这个值设定的越大,则在HashMap添加的元素多了的时候,新的元素会有更大的概率添加到某一个链表的尾部,而不是直接添加到table中;这就导致了在调用get方法获取的时候,极大的可能是在链表上遍历,而不是直接通过table的下标索引得到,这就失去了哈希的优势;但是如果这个属性设置的太小了,则会增加扩容的频率,同样也会浪费很多性能。
  • threshold:该值无非就是capacity x loadFactor,在判断是否扩容的时候用到(++size > threshold)

5. remove方法

HashMap的remove相对于put和get来说,用的要少一些,但是也是很重要的一个方法。

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

可见其就是回调了removeNode方法

removeNode

该方法的第四个参数含义为当为true时,通过hash和key找到的节点的value和第三个参数相等时才能够移除;第五个参数的含义为当为false的时候,删除节点的时候不会删除其他的节点

final Node removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node[] tab; 
    Node p;          // table中通过hash算出的下标的元素
    int n, index;     // n为table的length;index为hash算出的下标;之后在沿着链表查找的时候代表当前链点的前一个链点,搭配node变量完成链点的删除
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node node = null, e;  // node为最终的节点;e为在链表中查找时当前的链点 
        K k; V v;  

        -------------------重点 1-----------------------
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;  

        --------------------重点 2---------------------
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode)node).removeTreeNode(this, tab, movable);  

            -----------------重点 3-----------------
            else if (node == p)
                tab[index] = node.next;  

            -----------------重点 4----------------------
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}  
  • 重点 1:这里的情况代表,需要删除的节点就是table通过index找到的元素,也即链表的表头,这里唯一的就是node = p这一句
  • 重点 2:这里的情况代表,需要删除的节点位于table某一个位置的链表之中,采用的就是一个个查找,这时候变量p表当前链点的前一个链点,变量e代表当前的链点,node代表目标链点,也就是说最后找到目标链点的时候p.next即为node
  • 重点 3:这里删除节点的操作与重点1的情况相呼应
  • 重点 4:这里删除节点的操作与重点2的请况相呼应

提醒:根据源码中“清空”必定要赋值为null的特点,我们可以看到remove中返回了node节点,这也就是为何没有将删除的节点赋值为null的代码,所以我们在使用完了node之后一定要手动将其赋值为null,以呼应源码

四、非重要方法

1. clear

该方法就是清空HashMap中的键值对,首先想到的就是直接将table重新new一个很方便,但是源码中并不是这么做的,其又是一个一个的赋值为null:

public void clear() {
    Node[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}  

2. keySet

该方法是获取HashMap中所有的key,作为一个Set返回:

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

可见,由于第一次调用该方法的时候,keySet为null,所以第一次返回的是KeySet对象,而之后返回的一直是该对象了。

KeySet类

该类很简单,值提供了访问HashMap中Key的方法,并没有提供添加和修改的方法:

final class KeySet extends AbstractSet {
      public final int size()                 { return size; }
      public final void clear()               { HashMap.this.clear(); }
      public final Iterator iterator()     { return new KeyIterator(); }
      public final boolean contains(Object o) { return containsKey(o); }
      public final boolean remove(Object key) {
          return removeNode(hash(key), key, null, false, true) != null;
      }
      public final Spliterator spliterator() {
          return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
      }
      public final void forEach(Consumer action) {
          Node[] tab;
          if (action == null)
              throw new NullPointerException();
          if (size > 0 && (tab = table) != null) {
              int mc = modCount;
              for (int i = 0; i < tab.length; ++i) {
                  for (Node e = tab[i]; e != null; e = e.next)
                      action.accept(e.key);
              }
              if (modCount != mc)
                  throw new ConcurrentModificationException();
          }
      }
  }  

KeySet的clear是调用HashMap的clear方法,会清除所有的键值对;而remove方法也是调用的HashMap的同名方法。值得一提的是该类的forEach方法:同样因为其保存了HashMap当前状态的modCount属性,因此通过forEach方法获取到每一个key之后,不能够通过该key来调用remove、put等方法

3. values

该方法的思想和实现和keySet方法一致:

public Collection values() {
    Collection vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}  
Values类

该类的思想和实现和KeySet如出一辙,需要注意的点也是一样的:

final class Values extends AbstractCollection {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator iterator()     { return new ValueIterator(); }
    public final boolean contains(Object o) { return containsValue(o); }
    public final Spliterator spliterator() {
        return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer action) {
        Node[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node e = tab[i]; e != null; e = e.next)
                    action.accept(e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}    

小结:HashMap的values和keySet方法返回的并不是想当然的将其所有的key放入一个Set中或者将所有的value放入一个Collection中;而是各自内部实现的类,而且我们并不能够访问指定的Key或者Value,只能够通过forEach方法来访问目标的值(由于Key和Value都是引用类型对象,所以这里访问的都是复制的值,不管对该值怎么重新赋值,都不会影响到HashMap中的值;还有一点需要提示的是forEach方法的实现是一个一个遍历)

4. HashIterator类

上面的KeySet和ValuesSet类中都有iterator()方法,而返回的是不同的Iterator对象,而这两个方法返回的Itearator对象都继承自HashIterator类

abstract class HashIterator {
    Node next;        // next entry to return
    Node current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        Node[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node nextNode() {
        Node[] t;
        Node e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        Node p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}  
    1. 该方法相当于就是单单增加了一个remove方法,但是需要注意的是,该remove方法虽然最后将expectedModCount = modCount,但是刚开始的时候还是会校验modCount != expectedModCount;所以在使用Iterator不停的通过next()方法访问数据的时候,能够改变HashMap结构的方法只能够调用Iterator的remove方法,其他的一概不可!!!
final class KeyIterator extends HashIterator
    implements Iterator {
      public final K next() { return nextNode().key; }
  }

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

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

可见这三个都是增加了一个next方法而已

5. entrySet方法

keySet和values方法是分别获取所有的Key和Set,而该方法则是获取所有的Node节点,其实现和前者是类似的模式:

public Set> entrySet() {
    Set> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}  
EntrySet类
final class EntrySet extends AbstractSet> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator> iterator() {
        return new EntryIterator();
    }
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry) o;
        Object key = e.getKey();
        Node candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry e = (Map.Entry) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator> spliterator() {
        return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer> action) {
        Node[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node e = tab[i]; e != null; e = e.next)
                    action.accept(e);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

可以见得,该方法的forEach访问的是所有的节点,也是采用挨个遍历的方式,同样的特点,由于forEach虽然访问的是每一个节点,节点作为参数传递给action.accept的时候,由于是引用类型对象,这里传递的是一个拷贝,不管怎么改变该值,都不会影响到HashMap中的数据。

6. forEach方法

HashMap自己的forEach和其keySet或者valuesSet中需要注意的点一样:

@Override
public void forEach(BiConsumer action) {
    Node[] tab;
    if (action == null)
        throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (int i = 0; i < tab.length; ++i) {
            for (Node e = tab[i]; e != null; e = e.next)
                action.accept(e.key, e.value);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}  

五、总结

HashMap常用方法知识点:

  1. HashMap中所有通过哈希值查找节点的时候判断的依据都是(hash == nodeKey.hash && nodeKey.equals(key)),二者缺一不可。据此可以理解Java中对Class中hashCode()方法和equals()方法实现的相互制约的关系。
  2. 调用put方法的时候,并不是每一次put之后都会判断是否需要扩容;只有当put的节点直接添加到了table中(也就是成为了链表头部)的时候才会判断是否扩容
  3. HashMap的get和put方法都支持key为null的情况,null的hashCode返回值做0处理
  4. 其keySet和valuesSet方法并没有真正的一个Set对象,只是返回了两个内部类对象,提供了forEach方法来只读访问HashMap中的Key或者Values数据;当然每个都有自己的Iterator方法提供next()和remove()两个方法来访问数据
  5. 不管是谁的forEach方法,通过forEach方法可以只读访问数据,不能利用得到的数据更改HashMap的结构(put、set、remove等改变modCount的方法)

其他知识:

  1. loadFactor代表了table数组可以被占用的比例,超过这个比例就要扩容;capacity属性代表table数组的length,size属性代表的才是HashMap中节点的个数;而threshold一般为capacity * loadFactor,只在判断是否需要扩容的时候使用(++size > threshold);进行扩容后新的capacity和threshold都为原来的2倍
  2. HashMap的任何“删除”的方法(clear和remove系列),都会将每一个节点赋值为null来更好的垃圾回收
  3. HashMap是线程不安全的,除了同步问题之外,通过forEach或者iterator等访问数据也可能出现异常

你可能感兴趣的:(Java源码学习--HashMap)