JDK源码阅读(七) : 容器——解锁HashMap

1. Map

Map是一种存储键值对(key-value)的容器,容器中一个key映射到一个唯一的value,所有的key不可以重复

Map提供了三种集合视图,分别是key的集合(不重复)、value的集合(可重复)和key-value对的集合(entry,不重复)。

一个Map实例自身不可以作为自己的key,但是可以作为自己的value。

Map家族使用较多的实现类是HashMap、LinkedHashMap。本文将主要介绍这两个类的实现方式 (jdk11版本)。

2. HashMap

HashMap是Map接口的一种实现类,其允许key为null,也允许value为null。

HashMap很大程度上类似于一个古老的Map实现类Hashtable,区别在于HashMap不是线程安全的,并且允许null的存储;而Hashtable则是线程安全的,不允许存储null。因此,HashMap的操作效率高于Hashtable。如果需要在多线程环境下使用HashMap,应该做好外部的同步操作。

HashMap不保证其存储的元素的顺序。

HashMap有两个重要的参数会影响其性能:初始容量装载因子

初始容量,即HashMap被创建时,表中槽(bucket)的个数。

装载因子,即hash表被填满的程度。装载因子越大,说明hash表中的空闲位置越少。装载因子默认是0.75,值越大,空间利用越高,但是查找效率越低。

如果节点的个数✖️装载因子的值,超过了hash表的容量,就会进行做rehash操作,加大了开销。因此,当我们在设置hash表容量的时候,应该综合考虑键值对的个数和装载因子。

另一个值得注意的情况是,不应该出现太多相同的键(key)。当键相同时,这些相同键的节点,将以链表或者红黑树的数据结构进行存储,查询效率低于hash方式。

2.1 类属性

①默认的初始容量为16,必须是2的整数次幂。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

②最大容量为2的30次幂。

static final int MAXIMUM_CAPACITY = 1 << 30;

③默认的装载因子是0.75。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

树化的阈值是8。jdk8以前,单个槽上的所有节点都以链表的形式存储。jdk8开始,当某个槽上装载的节点个数等于8,如果此时在这个位置再添加一个节点,将可能进行树化,改为使用红黑树存储。所以,树化的一个条件是:单个槽装载的节点个数大于8

static final int TREEIFY_THRESHOLD = 8;

⑤在重新调整hash表的大小时,如果某个槽上装载的节点个数小于TREEIFY_THRESHOLD,并且不超过UNTREEIFY_THRESHOLD,会进行去树化

static final int UNTREEIFY_THRESHOLD = 6;

⑥树化的最小容量值。如果某个槽的节点个数过多,但hash表中的节点总数小于MIN_TREEIFY_CAPACITY,将进行扩容。如果节点总数超过了该值,将进行树化。所以,树化的第二个条件是:hash表的节点总数大于等于64。

static final int MIN_TREEIFY_CAPACITY = 64;

链表形式的节点是以Node类来实现( jdk8开始 )。Node类实现了Entry接口。

static class Node<K,V> implements Map.Entry<K,V> {
  	//节点的哈希值,不会发生改变,所以用final修饰
    final int hash;
  	//键,不会发生改变,所以用final修饰
    final K key;
  	//值,会发生改变
    V value;
  	//由于是以链表形式相连,所以需要next指针
    Node<K,V> next;
  
		//构造器
    Node(int hash, K key, V value, Node<K,V> 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; }
  	//以key=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;
      	//如果是Entry类型的话
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
          	//如果key和value都相等的话,就是相同的
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

红黑树形式的节点是以TreeNode类来实现的。红黑树作为一种比较复杂的数据结构,我们需要了解其应用的场景,然后可以熟练地调用。红黑树适用于动态插入、删除和查找的场景,其查找的时间复杂度为O(logN)

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
  
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

2.2 实例属性

①Node数组作为hash表,存储键值对

transient Node<K,V>[] table;

②所有键值对的集合

transient Set<Map.Entry<K,V>> entrySet;

③容器中的键值对总数

transient int size;

④容器被修改的次数

transient int modCount;

⑤扩容的阈值。每次添加完一个节点,都要判断总节点数(size)是否大于threshold,如果大于,则进行扩容。

int threshold;

⑥hash表的装载因子,由final修饰,一旦被赋值,不可再修改。

final float loadFactor;

2.3 构造器

空参构造器。使用了默认的装载因子0.7。所有其他的属性都采用默认值。

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

②使用给定初始容量装载因子的构造器。

public HashMap(int initialCapacity, float loadFactor) {
  	//传入的初始容量必须大于等于0
    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);
}

tableSizeFor方法将返回大于等于容量cap最小的2的整数次幂。比如,当我们传入10,将得到16;当我们传入8,将得到8。

static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

那么,这个方法是如何实现的

以cap=10为例进行说明:

首先计算10-1 = 9 (十进制) = 1001 (二进制);

再计算1001的高位的0的个数,共28个;

然后,将-1(也就是二进制的32个1)逻辑右移28位,得到1111,于是n=1111 (十进制15);

最后结果为n+1=16。(注:逻辑右移时,高位全部填0。)

简而言之,就是cap-1有几位,就让n等于几个1,然后再加1,就可以取得大于等于cap的最小2的整数次幂。

至于为什么要将cap-1,是因为当cap正好等于2的整数次幂时,应该返回这个值自身。比如,cap=8,tableForSize(8)应该返回8,如果不减1,就会得到16。

并且实际上,这个threshold将会是第一次创建的hash表的长度

③使用给定初始容量的构造器。该方法使用了默认的装载因子0.75。

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

值得注意的点是,HashMap也使用了延时分配空间的策略,在构造实例时,并不创建用于存储键值对的hash表。

2.4 添加一个节点

put(K, V)方法向容器中添加键值对

public V put(K key, V value) {
  	//首先计算key的hash值,然后调用putVal方法,代码见下文
    return putVal(hash(key), key, value, false, true);
}

putVal方法。如果是刚创建了实例,第一次放入元素时,会调用扩容方法resize()来新建一个hash表。

然后通过哈希函数(n-1) & hash,确定新的元素放入哪个位置。

n为表的长度,由于它被设定为2的整数次幂,那么n-1的二进制数必定全部都是1,且n-1为数组的最后一位。

实际上就是取n-1的二进制位数(假设为m位),然后取hash值的最后m位的值,作为要添加的位置,这个值必定小于等于n-1。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
  	//如果还没有创建hash表,则调用resize方法,创建一张表,代码见下文。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//取得新表的长度
  	//如果当前位置上还没有存放节点
    if ((p = tab[i = (n - 1) & hash]) == null)
      	//新建一个节点并初始化
        tab[i] = newNode(hash, key, value, null);
    else {//如果当前位置上已经有节点了
        Node<K,V> e; K k;
      	//p为当前位置上的第一个节点,比较节点p的hash值是否相同,再比较是否为同一个key或者key的值相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
          	//如果要添加的key和已存在的key相同,用e来保存旧的节点
            e = p;
      	//如果p的key和要插入的key不同,则继续比较下一个节点。
      	//现在又分两种情况:①当前位置上存放的是红黑树
        else if (p instanceof TreeNode)
          	//则将新的节点插入到红黑树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //②当前位置上存放的是单链表
      	else {
          	//遍历单链表
            for (int binCount = 0; ; ++binCount) {
              	//当遍历到最后一个节点
                if ((e = p.next) == null) {
                  	//将新节点插入到单链表的尾部
                    p.next = newNode(hash, key, value, null);
               //如果当前位置上存放的元素数大于树化的阈值8(binCount是从0开始的)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                      	//尝试将单链表改为红黑树存储,代码见下文
                        treeifyBin(tab, hash);
                    break;
                }
              	//判断当前遍历的节点的hash值和key是否与要插入的节点相同
              	//判断顺序:(使用以下顺序,可以最快地判断这个key是否是我们要找的key)
              	//①hash值是否相同,不同则可以直接认为不是同一个key
              	//②hash值相同的情况下,判断key的地址是否相同,如果相同,则肯定是同一个key,不做后续判断
              	//③如果key的地址不同,则继续使用equals方法来判断
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
              	//p始终指向前一个节点,以保证链表可以插入
                p = e;
            }
        }
      	//如果当前位置存在节点与要插入的节点的key相同,那么e就指向那个节点
        if (e != null) {
          	//保存该节点的值
            V oldValue = e.value;
          	//该节点处,用新值替换旧值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
          	//返回旧值
            return oldValue;
        }
    }
    ++modCount;
  	//每次添加完元素,需要判断是否超过了threshold,如果超过,则进行扩容
    if (++size > threshold)
        resize();
    //如果是LinkedHashMap调用put方法时,这里需要做特殊的处理
    //如果是普通的HashMap,这里不做任何处理   
    afterNodeInsertion(evict);
  	//如果没有修改旧值,则返回null
    return null;
}

treeifyBin方法尝试将hash表中某一上的所有节点改为使用红黑树存储。当表的长度大于等于64时,才会进行树化;否则执行扩容操作。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
  	//如果当前hash表的长度小于可以树化的最小表长度64,则不进行树化,而是扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

resize方法。在扩容时,新容量设为旧容量的两倍。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
  	//第一次添加元素时,没有新建表,oldCap为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
  	//获得初始时设置的threshold
    int oldThr = threshold;
    int newCap, newThr = 0;
  	//如果是扩容的情况
    if (oldCap > 0) {
      	//如果旧的capacity已经超出了最大容量,则将threshold设为int的最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
          	//表不作扩容
            return oldTab;
        }
      	//否则,新的容量设为旧容量的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
          	//当旧的容量大于等于初始默认容量时,新的threshold设为旧threshold的2倍
            newThr = oldThr << 1; // 否则,按照新的capacity乘以装载因子,来计算新的threshold
    }
    else if (oldThr > 0) // 使用之前设置的threshold作为新的capacity
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
  	//第一次添加元素时或者oldCap小于初始默认容量时,newThr为0
    if (newThr == 0) {
      	//计算新的threshold,为容量capacity乘以装载因子
        float ft = (float)newCap * loadFactor;
      	//如果新的capacity大于最大容量,或者计算出的ft大于最大容量,则取int的最大值作为新的threshold
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
  	//新建一张hash表,长度为之前的threshold
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
  	//如果是扩容的情况,需要将旧表的元素迁移到新表
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
              	//如果该位置就一个节点
                if (e.next == null)
                  	//重新计算在新表中的位置,然后直接放入
                    newTab[e.hash & (newCap - 1)] = e;
              	//如果是红黑树节点,做对应的操作
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
              	//如果是链表节点
                else { // 将该链表上的节点分为两类:
                  		// 第一类节点的hash&oldCap==0,第二类节点的hash&OldCap!=0
 								// 第一类节点在扩容一倍的hash表中还是存放在原处
               // 而第二类节点在新hash表中将存放在当前位置向后偏移oldCap的位置(newCap等于2倍的OldCap)
          //这种方式只是简化了重新计算在新表的存放位置,因为它等价于将节点的hash&(newCap-1)取得的新位置
                    //第一类节点组成的链表,将用loHead保存头指针,loTail保存尾指针
                  	Node<K,V> loHead = null, loTail = null;
                  	//第二类节点组成的链表,将用hiHead保存头指针,hiTail保存尾指针
                    Node<K,V> hiHead = null, hiTail = null;
                  	//next用于保存原链表上的下一个节点
                    Node<K,V> next;
                    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);//直到下一个节点为null
                    //将第一类链表接到新表的j位置,j也是其在旧表中的位置
                  	if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                  	//将第二类链表接到新表的j+oldCap位置
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
  	//返回新建的hash表
    return newTab;
}

注意:

java8以前,将发生冲突的节点以链表形式连接,执行put方法时,以头插法的形式,将新节点插入到链表头部。在多线程环境下,使用这种方式来put元素,可能会导致链表成环。

java8开始,将发生冲突的节点以链表或者红黑树连接。当发生冲突的节点较少时,以尾插法将节点插入链表;当发生冲突的节点较多时,使用红黑树存储,加快查询速度。

2.5 添加一个map

putAll方法向本HashMap中添加给定的map中的所有键值对。

public void putAll(Map<? extends K, ? extends V> m) {
    putMapEntries(m, true);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  	//s等于要添加的map的元素数
    int s = m.size();
    if (s > 0) {
      	//如果当前map是空的
        if (table == null) {
          	//计算新表的阈值
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
          	//如果计算出来的阈值大于初始阈值
            if (t > threshold)
              	//取大于等于t的最小的2的整数次幂作为threshold,后续该值将作为新表的capacity
                threshold = tableSizeFor(t);
        }
      	//如果要添加的元素数超过了本map的阈值,就算本map为空也放不下,所以要先扩容
        else if (s > threshold)
            resize();
      	//调用entrySet方法,取得要添加的所有键值对组成的集合,然后一个一个依次添加到本map中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
          	//在一个一个添加的过程中,也会涉及到扩容
            putVal(hash(key), key, value, false, evict);
        }
    }
}

2.6 删除一个节点

remove(key)方法给定一个key,删除相应的键值对。删除之前,需要查找到这个节点。如果找不到,则返回null;否则,返回改节点的value。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {//matchValue表明是否value也有匹配
    Node<K,V>[] tab; Node<K,V> p; int n, index;
  	//hash表不空时才可能删除,取得要删除节点的索引位置的第一个节点,p指向这个节点
    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;
      	//如果第一个节点就是要删除的节点,node指向这个节点
        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 {//如果是单链表,依次遍历每个节点,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);
            }
        }
      	//如果找到了要删除的节点,然后看要不要连value也匹配,remove(key)不匹配value,remove(key,value)需要匹配value
        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);
          	//如果是单链表的第一个节点
            else if (node == p)
                tab[index] = node.next;
            //否则,直接将当前节点的前一个节点的next指针指向下一个节点
          	else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
          	//找到就返回被删除的节点
            return node;
        }
    }
    return null;
}

remove(key, value)方法删除具体的键值对。只需要将参数matchValue设置为true,表明key和value都要匹配。

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

2.7 查找

2.7.1 查找key

(1)keySet方法获取HashMap中所有的key组成的集合KeySet(不重复)。

public Set<K> keySet() {
  	//keySet定义于HashMap的抽象父类AbstractMap中
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

KeySet的实现与普通的hashSet类似,区别在于使用了定制的迭代器KeyIterator,2.9节介绍该迭代器的实现。

final class KeySet extends AbstractSet<K> {
		...
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    ...
}

一个值得注意地方是,HashMap中发生的改变,将直接影响keySet的内容;反之亦然。比如:

HashMap<String, Integer> map = new HashMap<>(4, 0.75f);
//添加三个键值对
map.put("a",97);
map.put("e",101);
map.put("b",98);
//获取keySet
Set<String> keySet = map.keySet();
//使用迭代器,遍历keySet的所有元素,得到 a、e、b
Iterator<String> it = keySet.iterator();
while(it.hasNext()){
    String key = it.next();
    System.out.println(key);
}
System.out.println("***********");
//向容器中添加一个键值对
map.put("c",99);
//重新遍历keySet,得到 a、b、c、e
it = keySet.iterator();
while(it.hasNext()){
    String key = it.next();
    System.out.println(key);
}

另一个值得注意的地方是,如果在迭代器遍历keySet的过程中,修改了map,那么迭代器获取的结果将是不确定的。因为在修改map的过程中可能发生扩容,各个节点的存储位置将可能发生变化。

(2)containsKey方法判断map中是否包含给定的key。

public boolean containsKey(Object key) {
  	//调用getNode方法
    return getNode(hash(key), key) != null;
}

getNode方法根据传入的hash值和key对象,查找对应的节点。

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  	//找到给定hash值所应该存储的索引位置,first指向该位置第一个节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
      	//先判断第一个节点是否是我们要找的节点
        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;
}
2.7.2 查找value

(1)values()方法获取map中所有的value组成的集合Values(可重复)。

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

同样,Values与普通的Collection容器类似,只是使用了定制迭代器。

public final Iterator<V> iterator()     { return new ValueIterator(); }

map中发生的改变,将直接影响Values。

如果在迭代器遍历values的过程中,修改了map,同样,迭代器获取的结果将是不确定的。

(2)containsValue方法判断map中是否包含一个或多个value对象。由于只知道value的情况下,不能直接通过hash函数计算出所在的索引位置,所以要进行全表遍历

public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
      	//对hash表的每个位置进行遍历
        for (Node<K,V> e : tab) {
          	//对链表的每个节点进行遍历
            for (; e != null; e = e.next) {
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}
2.7.3 查找entry(key-value)

entrySet方法获取map中所有的键值对组成的集合EntrySet(不重复)。

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

map中发生的改变,将直接影响键值对集合。

如果在迭代器遍历该集合的过程中,修改了map,同样,迭代器获取的结果将是不确定的。

获取到entrySet后,可以取得每一个Entry,这里的Entry实际上是其实现类Node。然后可以通过调用entry.getKey()和entry.getValue()取得每个Node中的key和value。

public final K getKey()        { return key; }
public final V getValue()      { return value; }

2.8 修改

put(key,value)方法同时可以用于修改某个key对应的value。

2.9 迭代器

KeyIterator是keySet的迭代器,其继承自HashIterator。next方法先调用HashIterator类中的nextNode方法返回下一个节点,然后取得该Node中的key

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

HashIterator类基于hash的数据结构所使用的迭代器通常都继承了HashIterator。该类型迭代器的遍历顺序通常是按照hash表的索引顺序,依次在每个位置上进行遍历。如果遇到某个位置上使用了链表存储节点,则按照链表的方式进行遍历;如果遇到的是使用了红黑树存储节点,则按照红黑树的方式进行遍历。

abstract class HashIterator {
  	//没有修饰符,以下这些属性对子类可见
    Node<K,V> next;        // 要返回的下一个键值对
    Node<K,V> current;     // 当前迭代器所指向的键值对
    int expectedModCount;  // 期待的修改次数
    int index;             // 当前所在的hash表槽slot

    HashIterator() {
        expectedModCount = modCount;
      	//获取当前的hash表
        Node<K,V>[] t = table;
        current = next = null;
      	//起始时,在表索引为0的位置
        index = 0;
        if (t != null && size > 0) { // advance to first entry
          	//找到第一个存储了数据的索引位置,next指向该位置的第一个节点
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }
  
		//判断是否有下一个节点
    public final boolean hasNext() {
        return next != null;
    }
  
		//获取下一个节点
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
      	//e指向下一个节点
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
      	//链表结构下,将current指针下移一位到next,next指针也下移一位
      	//当next指向null时,说明当前索引位置已经没有元素了,现在需要找到一个存储了元素的索引
      	//next指向该索引位置的第一个节点
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
      	//返回当前位置的节点
        return e;
    }
		
  	//移除current位置的节点
    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
      	//根据key调用removeNode方法就可以删除节点了
        removeNode(p.hash, p.key, null, false, false);
        expectedModCount = modCount;
    }
}

ValueIterator也继承了HashIterator,在调用next获取下一个value时,调用了nextNode方法获取下一个节点,再取其value值。

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

EntryIterator也是继承了HashIterator,在调用next获取下一个节点时,直接调用了nextNode方法获取下一个节点。

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

2.10 fast-fail机制

在List源码阅读章节,介绍了fast-fail机制,参考https://blog.csdn.net/Longstar_L/article/details/111146006第2.5节fast-fail机制。java.util包下的容器类一般都存在这个错误检测机制。在本线程使用迭代器遍历期间,其他线程修改了容器或者本线程自身修改了容器(未使用迭代器修改),就会抛出ConcurrentModificationException异常。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值