基于JDK7 的HashMap源码分析

本文详细分析了JDK7 HashMap的数据结构、存取实现、扩容机制和其他操作。HashMap是数组与链表的结合体,通过哈希算法定位元素。在存储时,如果key相同,会替换value;key为null时,有特殊处理。存取元素涉及到hash碰撞和链表处理,最大容量为2^30。在扩容时,采用头插法重新分配元素。同时,文章还探讨了modCount在确保线程不安全情况下迭代器的正确性。
摘要由CSDN通过智能技术生成

一、HashMap概述

  • HashMap是基于哈希表Map接口的非同步实现
  • key和value都允许使用null值
  • 不保证顺序恒久不变

二、HashMap数据结构

在编程语言中,最基本的结构有两种,数组和模拟指针(链表)

HashMap是数组链表的结合体

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bNs8wQgP-1586229617343)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1585753477429.png)]

从上图可以看出,HashMap底层是一个数组结构,数组中的每一个元素又是一个链表

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
	//成员变量Entry数组,Entry(存放key和value)
    transient Entry<K,V>[] table;
    
    //静态内部类,单链表结构
	static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//键
        V value;//值
        Entry<K,V> next;//next域,指向下一个节点
        int hash; //哈希值
       
        ……
    }
   
    ……
}

可以看出,Entry 就是数组中的元素,每个 Map.Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表

三、HashMap的存取实现

3.1 创建对象

HashMap的重要成员变量分析

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{

    /**
     * DEFAULT_INITIAL_CAPACITY默认的初始化容量大小为16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 最大的容量上限为2^30,(保证为2的k次方)
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的负载因子为0.75
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * Entry数组(存储key-value)
     */
    transient Entry<K,V>[] table;

    /**
     * 元素的个数
     */
    transient int size;

    /**
     *阀值(通过 容量 * 负载因子 计算出来的,当 size 到达这个值时,就会进行扩容操作)
     */
    int threshold;

    /**
     *负载因子
     */
    final float loadFactor;
    
    /**
     *用于记录修改的次数
     */
    transient int modCount;

3.1.1 创建HashMap对象

构造器(参数无Map集合)

	/**
     * 需要指定初始化的容量大小和负载因子
     *
     * @param initialCapacity 初始化容量大小
     * @param loadFactor 负载因子
     */
    public HashMap(int initialCapacity, float loadFactor) {
        //判断初始化容量大小的正确性
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        
        //初始化容量大小>容量的最大上限
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        
        //判断loadFactor负载因子的正确性
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // 保证容量大小为2的k次方
        int capacity = 1;
        //容量>初始化的容量
        while (capacity < initialCapacity)
            //capacity*2然后赋给capacity
            capacity <<= 1;

        //给成员变量(负载因子)赋值
        this.loadFactor = loadFactor;
        
        //阀值为 容量大小 * 负载因子 ( 若阀值已经大于容量的上限,使用最大容量+1 )
        threshold = (int)Math.min(capacity * loadFactor,
                                  MAXIMUM_CAPACITY + 1);
        
        //创建Entry数组,给定容量大小
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

    /**
		给定初始化容量大小
     */
    public HashMap(int initialCapacity) {
        //使用默认的负载因子(0.75)
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
		空参
     */
    public HashMap() {
        //使用默认的初始化容量大小(16)和默认的负载因子(0.75)
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

构造器(参数有Map集合)

	public HashMap(Map<? extends K, ? extends V> m) {
     	//调用双参构造器,初始化table,给 负载因子与阀值 赋默认值
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        
        //将m中的元素全部添加到Entry[]数组table中
        putAllForCreate(m);
    }	

	private void putAllForCreate(Map<? extends K, ? extends V> m) {
        //取出Map中的所有entry,遍历key和value,全部添加到table中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

总结

  • 创建HashMap对象时,需要给定初始化容量大小和负载因子
  • 没给定则使用默认的初始化容量大小(16)和负载因子(0.75)
  • 初始化容量大小经过运算变为2的k次方,并且最大为2^30次方
  • 阀值为 容量大小 * 负载因子
  • 创建HashMap对象时,会创建Entry数组

3.1.2 HashMap的最大容量

HashMap的最大容量为什么为2^30

	if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

1 << 30 的计算结果为 1073741824

由于Integer的最大上限为2147483647,若1<<31则为2147483648已经超过Integer的最大上限,造成溢出

扩容

//当size大于阀值threshold则扩容
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
    
    	//若oldCapacity旧容量大小=2^30次方,直接返回,不会扩容
        if (oldCapacity == MAXIMUM_CAPACITY) {
            
            //阀值设置为Integer的最大上限为2147483647
            threshold = Integer.MAX_VALUE;
            return;
        }
    Entry[] newTable = new Entry[newCapacity];
    ……
    /*
    	如果新的容量(旧的2倍) * 0.75已经大于2^30次方,则将阀值指定为2^30+1
    	意思为:newCapacity已经为最大的容量,HashMap不会再扩容
    */    
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

3.2 存储元素

3.2.1 put存储

put(key,value)

	public V put(K key, V value) {
        //key为null
        if (key == null)
            //调用key为null的存储方法
            return putForNullKey(value);
        
        //经过算法计算key的hash(不是hashCode)
        int hash = hash(key);
        
        //计算存储的下标
        int i = indexFor(hash, table.length);
        
        /**
         * 
         * 1、根据计算后得到下标 i 
         * 2、判断下标位置是否为空
         * 3、不为空,遍历该下标的链表,e为链表的每一个节点
         * 4、判断添加元素的key是否已存在HashMap中
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            
            /**
             * 1、定义一个辅助变量Object k来指向链表中的节点的key
             * 2、先判断链表中每个节点中的key的hash是否和添加元素的hash相同,
             *     不相同条件不成立
             * 3、hash相同
             *      3.1 先判断节点中的key==添加元素的key 相同不用调用equals,条件成立
             *      3.2 若内存地址不同,则判断节点中的key重写equals方法与添加元素的key比较是否返回true
             *      3.3 若返回false则条件不成立
             *      3.4 若返回true则条件成立
             * 4、只有hash相等、内存地址相同或equals判断返回true两个条件成立,则表示
             *   添加元素的key已存在
             * 5、添加元素操作变成替换value的操作,将新的value添加到key中,返回旧的value
             */
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                //替换value
                e.value = value;
                //记录访问,可以发现为空实现方法,在HashMap中无作用
                e.recordAccess(this);

                //返回旧的value
                return oldValue;
            }
        }
       
        //添加元素的key不存在HashMap中,执行添加操作
        
        //修改次数加一
        modCount++;

        //调用addEntry方法,addEntry(int hash, K key, V value, int bucketIndex)
        addEntry(hash, key, value, i);

        return null;
    }

总结:

  1. 从源代码中可以看出:往 HashMap 中 put 元素时,先根据 key 的 hashCode 重新计算 hash 值,根据 hash 值得到这个元素在数组中的位置(即下标)
  2. 如果数组该位置上没有元素,就直接将该元素放到 此数组中的该位置上
  3. 如果数组该位置上已经存放有其他元素了
  4. 判断添加的key是否存在,需要根据key重写的hashCode和equals方法判断
  5. key存在,则替换value,返回旧的value
  6. key不存在,添加的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾(addEntry方法)

注意:

hash和index的计算(算法有待研究)

  • hash是先根据key的hashCode方法计算出哈希值,然后再经过算法计算的
  • index下标是根据hash和Entry数组的长度计算的
  • 相同的哈希值计算的hash一定是相同的
  • 相同的hash计算的index一定是相同的
  • 但不同的hash计算的index可能也相同(hash碰撞)
	/**
     * 根据hashCode计算hash,算法有待研究
     */
    final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        //h根据key的hashCode方法计算
        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

	/**
     * 根据hash和Entry数组的长度计算下标,算法有待研究
     */
	static int indexFor(int h, int length) {
        return h & (length-1);
    }

判断key是否相同

  • 不同的hash对应的key一定不相同
  • 相同的hash对应的key也可能不同(重写后的hashCode方法是使用类的属性计算,属性值可能不同,但计算结果可能相同)
  • 所以判断相同的hash后,还要判断equals
  • 若重写后的hashCode相等与equals返回true,才逻辑上判定key相同

3.2.2 key为null的存储

putForNullKey(value)

	/*
		将key为null的元素存储在Entry数组下标为0的位置
	*/
	private V putForNullKey(V value) {
        
        //遍历Entry[0]的链表,看key=null的位置是否有值,e为Entry[0]的每一个节点
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            
            //找到key=null,即key=null有值,替换旧的value
            if (e.key == null) {
                V oldValue = e.value;
                
                //e.key指向新的value
                e.value = value;
                
                //记录访问,可以发现为空实现方法,在HashMap中无作用
                e.recordAccess(this);
                
                //返回旧的value
                return oldValue;
            }
        }
        //修改次数加一
        modCount++;
        
        //调用addEntry方法,addEntry(int hash, K key, V value, int bucketIndex)
        addEntry(0, null, value, 0);
        return null;
    }

	static class Entry<K,V> implements Map.Entry<K,V> {
		//记录访问,空实现
        void recordAccess(HashMap<K,V> m) {
        }
        ……
    }

3.2.3 modCount记录修改的次数

在ArrayList,LinkedList,HashMap等等的内部实现增,删,改方法中总能看到modCount,modCount字面意思就是修改次数,但为什么要记录modCount的修改次数呢

所有使用modCount属性的实现全是线程不安全,说明modCount与线程不安全有关

阅读源码,modCount在对应集合的内部迭代器类使用到,在HashMap中

modCount

	private abstract class HashIterator<E> implements Iterator<E> {

        //定义一个期望的修改次数
        int expectedModCount;   

        //构造器
        HashIterator() {
            //构造方法,将HashMap中的modCount赋值给了expectedModCount
            expectedModCount = modCount;
            ...
        }
        
        //获取下一个Entry,类同于Iterator中的next方法
        final Entry<K,V> nextEntry() {
            
            //判断修改次数与期望的修改次数是否相等
            if (modCount != expectedModCount) 
                //抛出异常
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }
        ...
    }

由以上代码可以看出

  • 在一个迭代器初始化时,会将HashMap中modCount赋值给迭代器中的expectedModCount
  • 在迭代器遍历的过程中,一旦发现这个对象的modCount和迭代器中存储的expectedModCount不一样就抛异常

这运用一个机制:快速失败机制(Fail-Fast

  • Java.util.HashMap不是线程安全
  • 在迭代器遍历时,若其他线程修改了Map,导致两个modCount不同
  • 抛出ConcurrentModificationException并发修改异常
  • 保证线程之间修改的可见性

建议:在遍历非线程安全的集合时,尽量使用迭代器

3.2.4 addEntry方法添加元素

实际添加Entry的方法

addEntry(int hash, K key, V value, int bucketIndex)

	/**
     * 判断是否扩容,重新计算hash和bucketIndex
     * 添加元素
     * 
     * @param hash 根据参数key的hashCode方法计算的哈希值,
     *             然后经过算法计算得到的hash
     * @param key   键key
     * @param value 值value
     * @param bucketIndex   将要存储的Entry数组的下标
     */
	void addEntry(int hash, K key, V value, int bucketIndex) {

        /**
         * 1、先判断size大小是否达到阀值 size*loadFactor
         * 2、然后判断将要存储的位置是否为null
         * 3、如果达到阀值和存储的位置不为空条件成立
         */
        if ((size >= threshold) && (null != table[bucketIndex])) {
            
            //扩容,扩容为原来容量的2倍
            resize(2 * table.length);
            
            //重新计算hash
            hash = (null != key) ? hash(key) : 0;
            
            //重新计算存储的下标
            bucketIndex = indexFor(hash, table.length);
        }
        
        //添加元素
        createEntry(hash, key, value, bucketIndex);
    }

	/**
     * 添加元素(使用头插法)
     * 
     * @param hash 根据参数key的hashCode方法计算的哈希值,
     *             然后经过算法计算得到的hash
     * @param key   键key
     * @param value 值value
     * @param bucketIndex   将要存储的Entry数组的下标
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        
        //定义辅助变量 e 指向Entry链表原来头部(保存原来链表的数据)
        Entry<K,V> e = table[bucketIndex];
        
        //创建一个新的Entry并放在Entry数组的bucketIndex位置,e作为新的Entry的next
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        
        //元素个数加一
        size++;
    }

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

        /**
         * 初始化Entry属性,传入一个Entry参数,该参数
         * 作为对象的next域
         * 
         * @param h 根据参数key的hashCode方法计算的哈希值,
         *  *             然后经过算法计算得到的hash
         * @param k 键key
         * @param v 值value
         * @param n 一个Entry为该对象的next域
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        ...
    }

总结:

addEntry方法执行

  • 首先判断是否需要扩容
    • 需要,扩容为原来容量的2倍
    • 重新计算hash
    • 重新计算要存储的下标(bucketIndex)
  • 然后添加元素
    • 首先定义一个辅助变量 e 来指向原来下标的链表
    • 创建一个新的Entry放在该下标位置上
    • 原来的链表作为新的Entry的next节点,形成新的链表
  • size++,添加元素完成

3.3 取元素

3.3.1 get取元素

get方式可以理解为查询,取出元素,HashMap集合不会移除

get(Object key)

public V get(Object key) {
    	//判断key是否为null
        if (key == null)
            return getForNullKey();
    
    	//key不为null,根据key获取Entry节点
        Entry<K,V> entry = getEntry(key);

    	//判断Entry节点是否为null返回value
        return null == entry ? null : entry.getValue();
    }

//kye=null的获取value方式
private V getForNullKey() {
   		 //遍历0下标的Entry链表,是否有key=null的节点,有返回该节点的value,没有返回null
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

//获取key对应的Entry节点
final Entry<K,V> getEntry(Object key) {
        //计算hash
        int hash = (key == null) ? 0 : hash(key);

        //根据hash计算index,并遍历该下标位置的Entry链表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {

            //判断存在该key,存在该key,返回该key对应的Entry节点,不存在,返回null
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

总结:

从源码看出:

  1. 先判断key是否为null,为null,则找Entry[0]位置,遍历链表,看是否有key=null的节点,有则返回该节点的value,没有则返回null

  2. 若key不为null,调用getEntry方法,获取Entry节点

    • 先计算hash,再计算index下标
    • 遍历该下标的链表,看是否有key=链表中某个节点的key,有则返回该Entry节点,没有则返回null
  3. 看得到的Entry节点是否为null,为null,则返回的value为null,节点不为null,则返回该节点的value

put与get

  • HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象
  • HashMap底层采用一个Entry[]数组来保存所有的key-value对
  • 当需要存储一个Entry对象时,会根据hash算法来决定其在数组中的存储位置,再根据equals方法决定是替换元素还是添加元素到链头
  • 当需要取出一个Entry时,根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry

3.3.2 remove取元素

remove方式可以理解为取出,HashMap集合移除该元素

remove(Object key)

	//移除HashMap中的元素,并获取该元素
    public V remove(Object key) {
        
        //通过key移除Entry链表中节点,并返回该节点
        Entry<K,V> e = removeEntryForKey(key);
        
        //返回节点的value
        return (e == null ? null : e.value);
    }


	/**
     * 通过key移除Entry链表中节点,并返回该节点
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        //计算hash
        int hash = (key == null) ? 0 : hash(key);
        //计算index下标
        int i = indexFor(hash, table.length);

        //定义一个指针指向该下标对应链表的头节点(Entry数组存储的元素都是链表的头节点)
        //prev指向当前节点的前一个节点(从头节点开始)
        Entry<K,V> prev = table[i];

        //定义一个指针指向当前节点(从头节点开始)
        Entry<K,V> e = prev;

        //遍历链表
        while (e != null) {
            //定义一个指针指向当前节点的下一个节点
            Entry<K,V> next = e.next;

            /**
             * 遍历链表,判断是否存在该key
             * 1、判断hash是否相等
             * 2、判断key存储地址是否相等(包括key=null的情况)
             * 3、判断key.equals方法是否相等(key!=null)
             * 4、满足条件则表示已找到要移除的Entry节点
             */
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {

                //修改的次数加一
                modCount++;

                //元素的个数减一
                size--;

                /**
                 * 判断前一个节点和当前节点的指针是否相等,
                 *      1、相等则表示移除的节点为链表的头节点
                 *      2、若不相等则表示移除的节点不是链表的头节点
                 */
                if (prev == e){
                    /**
                     * 移除的节点为链表的头节点
                     * 直接让该下标的元素指向当前节点的下一个节点
                     */
                    table[i] = next;
                }else {
                    /**
                     * 移除的节点不是链表的头节点
                     * 让当前节点的上一个节点指向下一个节点
                     */
                    prev.next = next;
                }

                //记录移除操作(和recordAccess记录访问一样也空实现方法)
                e.recordRemoval(this);

                //返回当前节点
                return e;
            }

            /**
             * 指针后移
             */
            prev = e;
            e = next;
        }

        //返回当前节点(遍历链表没有找到对应的key,返回null)
        return e;
    }

指针移动操作示意图:

  • 开始状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGzVomef-1586229617345)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1585804835557.png)]

  • 若移除的节点为第一个节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmE2KLyx-1586229617345)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1585805382731.png)]

  • 若移除的节点不是第一个节点,指针后移

    • 结束循环

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qSeKskVg-1586229617346)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1585806483054.png)]

    • 找到需要移除的节点

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PQSFQtq6-1586229617346)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1585807162258.png)]

3.4 扩容机制

	void resize(int newCapacity) {
        //取出旧的Entry数组
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;

        //如果原来的容量达到HashMap规定的最大的容量
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //将阀值设置为Integer最大值,不再扩容
            threshold = Integer.MAX_VALUE;
            return;
        }

        //根据新的容量创建新的Entry数组,newCapacity为原来的容量的2倍
        Entry[] newTable = new Entry[newCapacity];



        // oldAltHashing为旧的Entry数组是否使用哈希函数计算哈希值
        boolean oldAltHashing = useAltHashing;

        //根据newCapacity计算是否使用哈希函数重新计算哈希值
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

        //rehash为根据新旧的容量判断是否需要重新计算hash
        boolean rehash = oldAltHashing ^ useAltHashing;
        //给新的Entry[]数组赋值
        transfer(newTable, rehash);
        table = newTable;
        
        //重新计算阀值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 根据rehash判断是否需要重新计算hash
     * 并根据新的hash生成新的Entry[]数组位置
     * 然后给新的Entry[]数组赋值
     */
    void transfer(Entry[] newTable, boolean rehash) {
        //获取新的Entry数组长度
        int newCapacity = newTable.length;

        //遍历原来的Entry数组中每个链表的节点e
        for (Entry<K,V> e : table) {
            while(null != e) {

                //定义next指针指向当前节点e的下一个节点
                Entry<K,V> next = e.next;

                //根据rehash判断是否需要重新计算hash
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }

                //据新的hash生成新的Entry[]数组位置
                int i = indexFor(e.hash, newCapacity);

                //头插法,最新加入的节点为链表头
                e.next = newTable[i];
                newTable[i] = e;

                //指针下移
                e = next;
            }
        }
    }

给新的Entry[]数组赋值过程:

  • 开始状态

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fGGI1S8F-1586229617347)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1586228157355.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ILlP5ha-1586229617347)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1586228823309.png)]

  • 给第一个节点赋值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NoLpv0Y-1586229617348)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1586229105641.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmVSEzUl-1586229617348)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1586228751265.png)]

  • 指针下移

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHEbjSQM-1586229617349)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1586229143721.png)]

  • 给第二个节点赋值

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Npdvgbvp-1586229617349)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1586229442535.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Au5NTYFJ-1586229617349)(C:\Users\333\AppData\Roaming\Typora\typora-user-images\1586229414681.png)]

  • 如此类推,直到指针下移到最后(e == null),形成头插法

四、其他操作

4.1判断HashMap中是否包含某个key

containsKey(key)

	public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

4.2 判断HashMap中是否包含某个value

containsValue(value)

	public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();

        //遍历Entry[]
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }

4.3 判断HashMap中的value是否包含null

containsNullValue()

private boolean containsNullValue() {
    
       //遍历Entry[]
    	Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }

4.4 清空所有的元素

clear()

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

ll)
return containsNullValue();

    //遍历Entry[]
    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (value.equals(e.value))
                return true;
    return false;
}



## 4.3  判断HashMap中的value是否包含null

> containsNullValue()

```java
private boolean containsNullValue() {
    
       //遍历Entry[]
    	Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }

4.4 清空所有的元素

clear()

public void clear() {
        modCount++;
        Entry[] tab = table;
        for (int i = 0; i < tab.length; i++)
            tab[i] = null;
        size = 0;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值