HashMap详解

文章目录

1. 简介

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

HashMap是java中常用的数据结构,,HashMap继承AbstractMap。实现了Map接口,同时还实现了CloneableSerializable接口。所以HashMap是支持复制、序列化的。HashMap的存储结构在jdk1.7之前是数组加链表的方式存储的,但是,如果链表的长度非常长的时候,查询效率直线下降,查询时间复杂度是O(n),因此,JDK1.8 把它设计为达到一个特定的阈值之后(数组长度大于64且链表长度大于8),就将链表转化为红黑树。

简单说下红黑树的特点:

  1. 每个节点只有两种颜色,那就是红色或者黑色。
  2. 根节点必须是黑色。
  3. 每个叶子节点(NIL)都是黑色的空节点。
  4. 从根节点到叶子节点,不能出现两个连续的红色节点。
  5. 从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相同。
  6. 红黑树,是一个自平衡的二叉搜索树,因此可以使查询的时间复杂度降为O(logn)。

HashMap 结构示意图:
在这里插入图片描述

2. 成员变量

 //默认的初始化容量为16  必须是2的n次幂
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 //最大容量为 2^30
 static final int MAXIMUM_CAPACITY = 1 << 30;
 /** 
 默认的加载因子0.75 乘以数组容量得到的值,用来表示元素达到多少个数时,需要扩容
 为什么是0.75  而不是0.8或者0.5呢?
 如果是0.8 那么hash碰撞的概率会变高,数据都在链表上,影响查询效率。
 如果是0.5 则数组长度达到一半大小就需要扩容,空间使用率大大降低,
 所以这是一个时间和空间上的权衡,一般不建议修改
 */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 //当链表的长度大于8时,变为红黑树
 static final int TREEIFY_THRESHOLD = 8;
 //当红黑树上的元素个数,减少到6个时,就退化为链表
 static final int UNTREEIFY_THRESHOLD = 6;
 //链表转为红黑树,除了链表的长度大于8以外,还需要数组长度大于64
 //这是为了避免,数组扩容和树化阈值之间的冲突。
 static final int MIN_TREEIFY_CAPACITY = 64;
 //存放所有node节点的数组
 transient Node<K,V>[] table;
 //存放所有的键值对
 transient Set<Map.Entry<K,V>> entrySet;
 //map中实际键值对个数,即数组中元素个数
 transient int size;
 //每当结构发生改变时都会自增,fail-fast机制,这是一种错误检测机制
 //当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
 transient int modCount;
 //数组扩容阈值
 int threshold;
 //加载因子
 final float loadFactor;
 //普通单向链表节点类
 static class Node<K,V> implements Map.Entry<K,V> {
 		//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
        final int hash;
        final K key;
        V value;
        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;
        }
 }
 //转化为红黑树的节点类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	//当前节点的父节点
	TreeNode<K,V> parent;  
	//左孩子节点
	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);
	}
}	

3. 构造函数

HashMap有四个构造函数可供我们使用,一起来看下:

	//默认无参构造函数 默认加载因子0.75
	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    //有参构造函数  可自定义容量,但是需要注意的是指定的容量不一定是实际的容量,而是主要看tableSizeFor()方法。
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子
    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;
        /**
        这里就是把我们指定的容量改为一个大于它的最小2此幂,如传过来的容量是12,则返回16.
        注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?
        这里用到了懒加载的机制,等到了resize()再细说
        */
        this.threshold = tableSizeFor(initialCapacity);
    }
   //可传入一个已有的map
   public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
   //把传入的map里边的元素都加载到当前map
   final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();//传入的map元素个数
        if (s > 0) {//如果集合个数为0,直接不执行
            if (table == null) { // 此时说明数组中没有元素,属于第一次创建map,需要先赋值初始容量
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //数组中有元素且大于扩容阈值,则扩容
            else if (s > threshold)
                resize();
             //遍历集合然后put到新的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);
            }
        }
    }

4. 常用方法解析

4.1 tableSizeFor()方法详解

在上面的构造函数中我们发现,多次使用到了tableSizeFor()这个函数,但是具体是干什么用的,大家还不太清楚。下面看下源码。

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

我们以传入参数为12 来举例,计算这个过程。
首先,12传进去之后先减1,n此时为11。然后是一系列的无符号右移运算。

//1. 此时n的值为11,11的二进制
0000 0000 0000 0000 0000 0000 0000 1011
//2. 无符号右移1位,高位补0
0000 0000 0000 0000 0000 0000 0000 0101
//3.然后做或运算,此时n的值 15
0000 0000 0000 0000 0000 0000 0000 1111
//4.无符号右移2位,高位补0
0000 0000 0000 0000 0000 0000 0000 0011
//5.然后做或运算,此时n的值15
0000 0000 0000 0000 0000 0000 0000 1111

//我们会发现,再执行右移 4,8,16位,同样n的值不变
//当n小于0时,返回1,否则判断是否大于最大容量,是的话返回最大容量,否则返回 n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
//很明显我们这里返回的是 n+1 的值,也就是16
0000 0000 0000 0000 0000 0000 0000 1111
+                                     1
0000 0000 0000 0000 0000 0000 0001 0000

将它转为十进制,就是 2^4 = 16 。我们会发现一个规律,以上的右移运算,最终会把最低位的值都转化为 1111 这样的结构,然后再加1,就是1 0000 这样的结构,它一定是 2的n次幂。因此,这个方法返回的就是大于当前传入值的最小(最接近当前值)的一个2的n次幂的值。

4.2 put()方法详解

	/**
	put()方法:在调用putVal()前会先调用hash(key)方法。
	hash(key):主要是为了减少hash碰撞,得到当前key的一个hash值,用于确定当前key应该存放在数组的哪个下标位置
	*/
	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
	传过来的hash值,key和value
	onlyIfAbsent:如果是true表示我们不能修改已存在的值,所以这里传入false。
	evict:只有在方法 afterNodeInsertion(boolean evict) { }用到,可以看到它是一个空实现,因此不用关注这个参数
	*/
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断数组是否为空,为空则先调用resize()
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //根据当前key的hash值和(n-1)找到他在数组中的下标,判断当前下标是否存在元素
        //如果没有元素,则把key、value包装成Node节点,直接添加到此位置。
        //i = (n - 1) & hash 是计算下标位置的,为什么这样算,后边讲
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        	//如果当前位置已经有元素了,分为三种情况。
            Node<K,V> e; K k;
            //1. 当前位置的hash值跟传过来的hash值一样,并且key也相等
            //则把p赋值给e,跳转到①处,后续需要做值的覆盖处理
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //2.如果当前是红黑树结构,则把它加入到红黑树 
            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,则转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                       //插入成功之后,跳出循环,跳转到①处
                            treeifyBin(tab, hash);
                        break;
                    }
                    //若在链表中找到了相同key的话,直接退出循环,跳转到①处
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //①
			//说明发生了碰撞,e代表的是旧值,因此节点位置不变,但是需要替换为新值
            if (e != null) { // existing mapping for key
            	//用新值替换旧值,并返回旧值。
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //只有在 LinkedHashMap才会实现,用于实现根据访问先后顺序对元素进行排序,hashmap不提供排序功能
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //fail-fast机制
        ++modCount;
        //如果当前数组中的元素个数超过阈值,则扩容
        if (++size > threshold)
            resize();
        //同样的空实现
        afterNodeInsertion(evict);
        return null;
    }

4.3 hash()计算原理

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

从源码可以看到,会先判断key是否为空,为空则返回0,这也说明了hashMap是支持key传 null 的。若非空,则先计算key的hashCode值,赋值给h,然后把h右移16位,并与原来的h进行异或处理。为什么要这样做,这样做有什么好处呢?
我们运行一段程序,然后把它的hashCode打印出来:

		String str="张三";
        System.out.println(str.hashCode());
        System.out.println(Integer.toBinaryString(str.hashCode()));
        //输出结果
        //774889
        //10111101001011101001

然后,进行 (h = key.hashCode()) ^ (h >>> 16) 这一段运算。

//h原来的值
0000 0000 0000 1011 1101 0010 1110 1001
//无符号右移16位,其实相当于把低位16位舍去,只保留高160000 0000 0000 0000 0000 0000 0000 1011 
//然后高16位和原 h进行异或运算
0000 0000 0000 1011 1101 0010 1110 1001
^
0000 0000 0000 0000 0000 0000 0000 1011 
=
0000 0000 0000 1011 1101 0010 1110 0010

可以看到,其实相当于,我们把高16位值和当前h的低16位进行了混合,这样可以尽量保留高16位的特征,从而降低哈希碰撞的概率。
思考一下,为什么这样做,就可以降低哈希碰撞的概率呢?先别着急,我们需要结合 i = (n - 1) & hash 这一段运算来理解。
(n-1) & hash 作用

//这是 put 方法中用来根据hash()值寻找在数组中的下标的逻辑,
//n为数组长度, hash为调用 hash()方法混合处理之后的hash值。
i = (n - 1) & hash

我们知道,如果给定某个数值,去找它在某个数组中的下标位置时,直接用模运算就可以了(假设数组值从0开始递增)。如,我找 14 在数组长度为16的数组中的下标,即为 14 % 16,等于14 。 18的位置即为 18%16,等于2。
(n-1) & hash,就是取模运算的位运算形式。以18%16为例

//18的二进制
0001 0010
//16 -1 即 15的二进制
0000 1111
//与运算之后的结果为
0000 0010
// 可以看到,上边的结果转化为十进制就是 2 。
//其实我们会发现一个规律,因为n是2的n次幂,因此它的二进制表现形式肯定是类似于
0001 0000
//这样的形式,只有一个位是1,其他位都是0。而它减 1 之后的形式就是类似于
0000 1111 
//这样的形式,高位都是0,低位都是1,因此它和任意值进行与运算,结果值肯定在这个区间内
0000 0000  ~  0000 1111
//也就是0到15之间,(以n为16为例)
//因此,这个运算就可以实现取模运算,而且位运算还有个好处,就是速度比较快。

为什么高低位异或运算可以减少哈希碰撞
我们想象一下,假如用 key 原来的hashCode值,直接和 (n-1) 进行与运算来求数组下标,而不进行高低位混合运算,会产生什么样的结果。

//例如我有另外一个h2,和原来的 h相比较,高16位有很大的不同,但是低16位相似度很高,甚至相同的话。
//原h值
0110 1101 0110 1111 0110 1110 0010 1000
//另外一个h2值
0100 0101 1110 1011 0110 0110 0010 1000
// n -1 ,即 15 的二进制
0000 0000 0000 0000 0000 0000 0000 1111
//可以发现 h2 和 h 的高位不相同,但是低位相似度非常高。
//他们分别和 n -1 进行与运算时,得到的结果却是相同的。(此处n假设为16)
//因为 n-1 的高16位都是0,不管 h 的高 16 位是什么,与运算之后,都不影响最终结果,高位一定全是 0
//因此,哈希碰撞的概率就大大增加了,并且 h 的高16 位特征全都丢失了。

爱思考的同学可能就会有疑问了,我进行高低16位混合运算,是可以的,这样可以保证尽量减少高区位的特征。那么,为什么选择用异或运算呢,我用与、或、非运算不行吗?
这是有一定的道理的。我们看一个表格,就能明白了。
在这里插入图片描述
可以看到两个值进行与运算,结果会趋向于0;或运算,结果会趋向于1;而只有异或运算,0和1的比例可以达到1:1的平衡状态。(非呢?别扯犊子了,两个值怎么做非运算。。。)

所以,异或运算之后,可以让结果的随机性更大,而随机性大了之后,哈希碰撞的概率当然就更小了。

以上,就是为什么要对一个hash值进行高低位混合,并且选择异或运算来混合的原因。

4.4 resize() 扩容机制

在上边 put 方法中,我们会发现,当数组为空的时候,会调用 resize 方法,当数组的 size 大于阈值的时候,也会调用 resize方法。 那么看下 resize 方法都做了哪些事情吧。

  final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//旧的数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //旧数组的扩容阈值,注意看,这里取的是当前对象的 threshold 值,下边的第2种情况会用到。
        int oldThr = threshold;
        //初始化新数组的容量和阈值,分三种情况讨论。
        int newCap, newThr = 0;
        /** 第一种
        当旧数组的容量大于0时,说明在这之前肯定调用过 resize扩容过一次,才会导致旧容量不为0。
        之前在初始化的时候,调用tableSizeFor(),需要注意的是,它返回的值是赋给了 threshold 而不是 capacity。
        我们在这之前,压根就没有在任何地方看到过,它给 capacity 赋初始值。
        */
        if (oldCap > 0) {
        	//容量达到了最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //新数组的容量和阈值都扩大原来的2倍
            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);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            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 { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

4.5 get()方法

如果大家对前面的put()方法了解了,get()对大家来说就是很简单的了。

 //根据key获取对应的值
 public V get(Object key) {
        Node<K,V> e;
        //如果节点为空,则返回为null,否则返回节点的value值,这也说明,hashMap是支持value为null的。
        //因此,我们就明白了,为什么hashMap支持Key和value都为null
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
/**
     * @param hash :key的hash值
     * @param key 
     * @return the node, or null if none
     */
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //首先确保数组不为空,然后取当前hash值计算出的下标位置的第一个元素。为空的话直接返回null。
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //若hash值和key都相等,则说明我们要找的就是第一个元素,直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
             //如果不是的话,就遍历当前链表(或红黑树)
            if ((e = first.next) != null) {
            //如果是红黑树结构,则找到当前key所在的节点位置
                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);
            }
        }
        //否则,说明没有找到,返回null
        return null;
    }	
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMapJava中最常用的哈希表实现之一,它基于哈希表实现了Map接口。以下是HashMap源码的详细解释: HashMap内部是由一个数组和链表组成的,数组的每个元素称为桶,每个桶存储一个链表(可能为空),链表中的每个节点都是一个键值对(key-value pair)。 以下是HashMap的主要属性: ```java transient Node<K,V>[] table; // 存储元素的数组 transient int size; // 元素大小 int threshold; // 扩容阈值 final float loadFactor; // 负载因子 ``` 其中,table是一个transient修饰的Node数组,存储HashMap中的元素;size表示HashMap中元素的个数;threshold表示HashMap的扩容阈值,即当元素个数达到这个值时就需要扩容;loadFactor是负载因子,用于决定HashMap何时需要扩容。 以下是HashMap的主要方法: 1. put(K key, V value) :将指定的键值对添加到HashMap中,如果键已经存在,则更新对应的值。 2. get(Object key):获取指定键对应的值,如果键不存在则返回null。 3. remove(Object key):从HashMap中删除指定的键值对,如果键不存在则返回null。 4. clear():从HashMap中删除所有的键值对。 5. resize():扩容HashMap,将table的大小增加一倍。 6. hash(Object key):计算键的哈希值。 7. getNode(int hash, Object key):获取指定键的节点。 8. putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict):实际执行put操作的方法,会根据指定的参数决定是否更新已有键的值、是否删除过期键等。 HashMap的put方法实现如下: ```java public V put(K key, V value) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 for (Node<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; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果键已经存在,则更新对应的值。否则,我们创建新的节点,并将其添加到桶的链表中。 HashMap的get方法实现如下: ```java public V get(Object key) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 for (Node<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 如果找到指定键,则返回其对应的值 return e.value; } } // 如果指定键不存在,则返回null return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果找到指定键,则返回其对应的值。 HashMap的remove方法实现如下: ```java public V remove(Object key) { // 计算键的哈希值 int hash = hash(key); // 计算键在table数组中的索引 int i = indexFor(hash, table.length); // 遍历桶中的链表,查找指定键 Node<K,V> prev = table[i]; Node<K,V> e = prev; while (e != null) { Node<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { modCount++; size--; if (prev == e) { table[i] = next; } else { prev.next = next; } e.recordRemoval(this); return e.value; } prev = e; e = next; } // 如果指定键不存在,则返回null return null; } ``` 在这个方法中,我们首先计算键的哈希值,然后计算键在table数组中的索引。接着,我们遍历桶中的链表,查找指定键,如果找到指定键,则从链表中删除节点,并返回其对应的值。否则,我们返回null。 以上就是HashMap源码的详细解释。HashMap是一个非常常用且实用的数据结构,它的实现原理也非常值得深入学习和理解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值