Java魔法解密:HashMap底层机制大揭秘

文章目录

    • 一、 源码深度解析
      • 1.1 窥探Java集合框架中的设计思想
      • 1.2 逐行解读HashMap的源代码
        • 1.2.1 类信息
        • 1.2.2 常量属性
        • 1.2.3 变量属性
        • 1.2.4 节点信息
        • 1.2.5 构造方法
        • 1.2.6 put方法
          • 1.2.6.1 putVal方法
          • 1.2.6.2 putTreeVal方法
          • 1.2.6.3 tieBreakOrder方法
          • 1.2.6.4 treeifyBin方法
          • 1.2.6.5 treeify方法
        • 1.2.7 get方法
        • 1.2.8 remove方法
        • 1.2.9 resize方法
    • 二、 应用与最佳实践
      • 2.1 在实际项目中如何合理使用HashMap
      • 2.2 最佳实践和注意事项
    • 三、 结论
      • 3.1 对HashMap的全面总结
      • 3.2 鼓励读者深入学习和实践

一、 源码深度解析

1.1 窥探Java集合框架中的设计思想

Java集合框架是Java编程中非常重要的一部分,提供了各种数据结构和算法,使得开发者能够高效地组织和操作数据。

Java集合框架的设计思想主要包括以下几个方面

  1. 通用性(Generality)
    • Java集合框架被设计成通用的、可重用的组件。这样一来,同一套集合框架可以用于存储和处理各种类型的对象。
  2. 可扩展性(Scalability)
    • 集合框架提供了一系列接口和类,允许开发者实现自定义的集合类型。这种可扩展性使得开发者能够根据具体的需求创建新的集合类。
  3. 高性能(Performance)
    • 集合框架在设计时考虑了性能的因素,通过选择合适的数据结构和算法来提高操作的效率。例如,ArrayListLinkedList都是List接口的实现,但它们在插入和删除操作上有不同的性能表现。
  4. 互操作性(Interoperability)
    • 集合框架中的各个接口和类都被设计成可以互相操作的。这种互操作性使得不同的集合类之间能够轻松地进行转换和使用。
  5. 可读性和简洁性(Readability and Simplicity)
    • 集合框架的设计追求代码的可读性和简洁性。通过提供一组清晰的接口和方法,开发者可以更容易地理解和使用集合框架。
  6. 线程安全性(Thread Safety)
    • 部分集合类(如Hashtable、Vector)被设计成线程安全的,适用于多线程环境。此外,Java提供了一些在多线程环境中使用时更安全的并发集合类,如ConcurrentHashMap
  7. Fail-Fast机制(快速失败机制)
    • 集合框架在迭代过程中使用了快速失败机制,当多个线程对同一集合进行修改时,可能会抛出ConcurrentModificationException,以提醒开发者在迭代过程中不要修改集合。

总体来说,Java集合框架的设计思想注重通用性、性能、可扩展性和简洁性,为开发者提供了丰富而强大的工具,用于处理各种不同类型的数据。

1.2 逐行解读HashMap的源代码

1.2.1 类信息

在这里插入图片描述

1.2.2 常量属性

在这里插入图片描述

1.2.3 变量属性

在这里插入图片描述

1.2.4 节点信息

在这里插入图片描述

1.2.5 构造方法

1、构造一个具有默认初始容量(16)和默认加载因子(0.75)的空HashMap。

在这里插入图片描述

2、构造一个带指定初始容量和默认加载因子(0.75)的空HashMap。
在这里插入图片描述

3、构造一个带指定初始容量和加载因子的空HashMap。

在这里插入图片描述

4、构造一个映射关系与指定Map相同的新HashMap。

在这里插入图片描述

5、扩展LRU实现方式与思路。

在实现一个基于LRU策略的缓存时,通常会使用一个数据结构来存储缓存中的数据,并且需要记录数据的访问顺序。常见的数据结构是双向链表和哈希表的结合。

基于双向链表和哈希表的实现方式:

  1. 使用双向链表:双向链表可以记录数据的访问顺序,当某个数据被访问时,可以将其移动到链表的头部或尾部。头部表示最近访问的数据尾部表示最久未被访问的数据
  2. 使用哈希表:哈希表用于快速查找缓存中的数据,可以将数据的键(key)映射到对应的链表节点,以实现快速的查找和插入操作。

实现LRU缓存的基本思路如下:

  • 当需要访问缓存中的数据时,首先在哈希表中查找该数据是否存在。
  • 如果存在,则将该数据移动到链表的头部,表示最近被访问过。
  • 如果不存在,需要从后端的存储中加载数据,并插入到链表的头部,同时更新哈希表。
  • 缓存已满时,需要淘汰链表尾部的数据,同时更新哈希表。

通过这种方式,实现了一个基于LRU淘汰策略的缓存系统,可以确保最近最少被使用的数据会被及时地淘汰,从而保持缓存中的数据是最有用的。

1.2.6 put方法

由于put方法代码量偏多,故使用代码+注释形式解读源码。

1.2.6.1 putVal方法
/*** HashMap的put操作源码+注释*/
public V put(K key, V value) {// 先根据hash()方法,计算存放元素在table数组中的下标return putVal(hash(key), key, value, false, true);
}// hash方法
static final int hash(Object key) {int h;// 如果key为null,返回hash=0// 如果key不为null,那么进行key的hashCode值的高低位异或运算,返回结果作为hash值return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}// putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 如果第一次插入,table为空或者length等于0if ((tab = table) == null || (n = tab.length) == 0)// 调用resize方法进行初始化n = (tab = resize()).length;// 通过hash值计算索引位置,将该索引位置的头节点赋值给p且p为空// 实则进行取余操作 == p=tab[hash%length]if ((p = tab[i = (n - 1) & hash]) == null)// 在该索引位置新增一个节点tab[i] = newNode(hash, key, value, null);else {// table表该索引位置不为空,则会发生hash冲突Node<K,V> e; K k;// 当p节点的hash值和key跟传入的相等if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// p节点即为要查找的目标节点,将p节点赋值给e节点e = p;// 当p节点为TreeNodeelse if (p instanceof TreeNode)// 调用红黑树的putTreeVal方法查找目标节点e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {// 判断完,则p节点为普通链表节点//遍历节点操作,使用binCount统计链表的节点数for (int binCount = 0; ; ++binCount) {// p的next节点为空时,代表找不到目标节点if ((e = p.next) == null) {// 新增一个节点并插入链表尾部p.next = newNode(hash, key, value, null);// 当binCount节点数超过8个// TREEIFY_THRESHOLD - 1:由于循环是从p节点的下一个节点开始的if (binCount >= TREEIFY_THRESHOLD - 1)// 调用treeifyBin方法将链表节点转为红黑树节点treeifyBin(tab, hash);break;}// e节点的hash值和key值与传入的相同if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// e节点即为目标节点,跳出循环break;// 将p指向下一个节点,继续往后遍历p = e;  }}// e节点不为空,代表目标节点存在if (e != null) {// 使用传入的value覆盖该节点的value,即保留原来元素的valueV oldValue = e.value;// 当oldValue为空if (!onlyIfAbsent || oldValue == null)// 替换valuee.value = value;// 用于LinkedHashMapafterNodeAccess(e);// 返回一个原有的valuereturn oldValue;}}// 添加操作执行后,对modCount、size做加一操作++modCount;// 插入节点后节点数超过阈值if (++size > threshold)// 调用resize方法进行扩容resize();// 用于LinkedHashMapafterNodeInsertion(evict); return null;
}
1.2.6.2 putTreeVal方法
/*** 当阈值达到触发红黑树的put的源码+注释*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {Class<?> kc = null;boolean searched = false;// TreeNode<K,V>继承于LinkedHashMap.Entry<K,V>// parent节点不等于空,则查找根节点// 即得出索引位置的头节点并不一定为红黑树的根节点TreeNode<K,V> root = (parent != null) ? root() : this;// 将根节点赋值给p节点,开始进行查找for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;// 当传入的hash值小于p节点的hash值if ((ph = p.hash) > h)// 将dir赋值为-1,代表向p的左边查找树dir = -1;// 否则传入的hash值大于p节点的hash值else if (ph < h)// 将dir赋值为1,代表向p的右边查找树dir = 1;// 当传入的key值等于p节点的key值else if ((pk = p.key) == k || (k != null && k.equals(pk)))// 即p节点为目标节点, 返回p节点return p;// 当k所属的类没有实现Comparable接口 或 k和p节点的key相等else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {// 由于局部变量初始为false,即当第一次判断是符合条件的if (!searched) {TreeNode<K,V> q, ch;// 查找过后标记为truesearched = true;// 从p节点的左节点和右节点分别调用find方法进行查找if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||((ch = p.right) != null &&(q = ch.find(h, k, kc)) != null))// 查找到目标节点则返回return q;}// 否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找// dir<0则代表k<pk,则向p左边查找;反之亦然dir = tieBreakOrder(k, pk); }// xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值TreeNode<K,V> xp = p;   // dir<=0则向p左边查找,否则向p右边查找,如果为nullif ((p = (dir <= 0) ? p.left : p.right) == null) {// xp的next节点Node<K,V> xpn = xp.next;    // 创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);// 调整x、xp、xpn之间的属性关系// 如果dir <= 0if (dir <= 0) //  代表x节点为xp的左节点xp.left = x;else        // 如果dir> 0, 则代表x节点为xp的右节点xp.right = x;// 将xp的next节点设置为xxp.next = x;    // 将x的parent和prev节点设置为xpx.parent = x.prev = xp; // 当xpn不为空if (xpn != null)// 将xpn的prev节点设置为x节点,与上文的x节点的next节点对应((TreeNode<K,V>)xpn).prev = x;// 进行红黑树的插入平衡调整moveRootToFront(tab, balanceInsertion(root, x));return null;}}
}
1.2.6.3 tieBreakOrder方法
/*** 使用定义的一套规则来比较k和p节点的key的大小*/
static int tieBreakOrder(Object a, Object b) {  int d;if (a == null || b == null ||(d = a.getClass().getName().compareTo(b.getClass().getName())) == 0)// a < b为-1,a > b为1d = (System.identityHashCode(a) <= System.identityHashCode(b) ?-1 : 1);return d;
}
1.2.6.4 treeifyBin方法
/*** 将链表转为红黑树*/
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 当tab为空或者tab的长度小于64if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// 进行扩容resize();// 根据hash值计算索引值,将该索引位置的节点赋值给e且e不为nullelse if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;// 从e开始遍历该索引位置的链表do {// 将链表节点转红黑树节点TreeNode<K,V> p = replacementTreeNode(e, null);// tl为空代表为第一次循环if (tl == null)	// 将头节点赋值给hdhd = p;else {// 如果不是第一次遍历// 当前节点的prev属性设为上一个节点p.prev = tl;  // 上一个节点的next属性设置为当前节点tl.next = p;    }// 将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)tl = p;} while ((e = e.next) != null);// 将tab该索引位置赋值为新的TreeNode的头节点,如果该节点不为空if ((tab[index] = hd) != null)// 以头节点(hd)为根节点, 构建红黑树hd.treeify(tab);}
}// 将链表节点转红黑树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);
}
1.2.6.5 treeify方法
/*** 将哈希表中的链表结构转换为树形结构,以提高查找效率*/
final void treeify(Node<K,V>[] tab) {TreeNode<K,V> root = null;// 开始一个循环,初始化变量x为当前节点,然后在每次迭代中将x更新为nextfor (TreeNode<K,V> x = this, next; x != null; x = next) {// next赋值为x的下个节点next = (TreeNode<K,V>)x.next;  // 将x的左右节点设置为空x.left = x.right = null;    // 如果还没有根节点if (root == null) {// 根节点没有父节点,设为nullx.parent = null; // 根节点必须为黑色,false为黑色x.red = false; // 在将x设置为根节点root = x;   }// 如果有根节点else {// k赋值为x的keyK k = x.key;// h赋值为x的hash值int h = x.hash;	// 定义一个类型未知的变量kcClass<?> kc = null;// 开始一个无限循环,初始化变量p为root节点,表示对树进行遍历for (TreeNode<K,V> p = root;;) {int dir, ph;K pk = p.key;// 比较当前节点x的哈希值h与节点p的哈希值ph,根据比较结果给变量dir赋值// 如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找if ((ph = p.hash) > h)dir = -1;// 如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找else if (ph < h)dir = 1;// x的hash值和p的hash值相等,则比较key值// 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等else if ((kc == null && (kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)// 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找dir = tieBreakOrder(k, pk);// 将p节点的父节点赋值给xp,中间变量用于下面给x的父节点赋值TreeNode<K,V> xp = p;   // dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置if ((p = (dir <= 0) ? p.left : p.right) == null) {// x的父节点即为最后一次遍历的p节点x.parent = xp; // 如果dir <= 0, 代表x节点为父节点的左节点if (dir <= 0)   xp.left = x;// 如果dir > 0, 代表x节点为父节点的右节点else    xp.right = x;// 进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)root = balanceInsertion(root, x);break;}}}}// 如果root节点不在tab索引位置的头节点, 则将其调整为头节点moveRootToFront(tab, root);
}
1.2.7 get方法
/*** HashMap的get操作源码+注释*/
public V get(Object key) {Node<K,V> e;// 调用hash(key)方法计算键key的哈希值,然后调用getNode方法获取与该键对应的节点,将结果赋给变量e// 如果e为null,则返回null;否则返回e节点的值e.valuereturn (e = getNode(hash(key), key)) == null ? null : e.value;
}// 获取与该键对应的节点
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 对table进行校验:table不为空 && table长度大于0 && table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;// 如果first不是目标节点,并且first的next节点不为空则继续遍历if ((e = first.next) != null) {if (first instanceof TreeNode)// 如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNodereturn ((TreeNode<K,V>)first).getTreeNode(hash, key);// 继续遍历do {// 执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}// 找不到符合的返回空return null;
}
1.2.8 remove方法
/*** HashMap的remove操作源码+注释*/
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) {Node<K,V>[] tab; Node<K,V> p; int n, index;// 如果table不为空 && table长度大于0 && 根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给pif ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;// 如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给nodeif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;// 否则将p.next赋值给e,向下遍历节点else if ((e = p.next) != null) {// 如果p是TreeNode则调用红黑树的方法查找节点if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {// 否则,进行普通链表节点的查找do {// 当节点的hash值和key与传入的相同,则该节点即为目标节点if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {// 赋值给node, 并跳出循环node = e;	break;}// p节点赋值为本次结束的e,在下一次循环中,e为p的next节点p = e;  // e指向下一个节点} while ((e = e.next) != null); }}// 如果node不为空,即根据传入key和hash值查找到目标节点,则进行移除操作if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {// 如果是TreeNode则调用红黑树的移除方法if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);// 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点// “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点else if (node == p)tab[index] = node.next;// 否则将node的上一个节点的next属性设置为node的next节点// 即将node节点移除, 将node的上下节点进行关联(链表的移除)elsep.next = node.next;++modCount;--size;// 供LinkedHashMap使用afterNodeRemoval(node); // 返回被移除的节点return node;}}return null;
}
1.2.9 resize方法
/*** HashMap的resize操作源码+注释*/
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 旧表的容量不为0if (oldCap > 0) {// 判断旧表的容量是否超过最大容量值if (oldCap >= MAXIMUM_CAPACITY) {// 将阈值设置为Integer.MAX_VALUE,并直接返回旧表threshold = Integer.MAX_VALUE;return oldTab;}// 将newCap赋值为oldCap的2倍,如果newCap<最大容量小于最大容量值并且oldCap>=16else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)// 将新阈值设置为原来的两倍newThr = oldThr << 1; // double threshold}// 如果旧表的容量的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为旧表的阈值else if (oldThr > 0)newCap = oldThr;else {// 旧表的容量为0, 旧表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值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);}// 将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表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;// 将索引值为j的旧表头节点赋值给eif ((e = oldTab[j]) != null) {  // 将旧表的节点设置为空, 以便垃圾收集器回收空间oldTab[j] = null; // 如果e.next为空, 则代表旧表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve order// 如果是普通的链表节点,则进行普通的重hash分布// 存储索引位置为:“原索引位置”的节点Node<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置+oldCap”的节点Node<K,V> hiHead = null, hiTail = null; Node<K,V> next;do {next = e.next;// 如果e的hash值与旧表的容量进行与运算为0,则扩容后的索引位置跟旧表的索引位置一样if ((e.hash & oldCap) == 0) {// 如果loTail为空if (loTail == null) // 将loHead赋值为第一个节点loHead = e; else// 否则将节点添加在loTail后面loTail.next = e;  // 并将loTail赋值为新增的节点loTail = e; }else {// 如果hiTail为空, 代表该节点为第一个节点if (hiTail == null)// 将hiHead赋值为第一个节点hiHead = e; else// 否则将节点添加在hiTail后面hiTail.next = e;   // 并将hiTail赋值为新增的节点hiTail = e; }} while ((e = next) != null);// 如果loTail不为空(说明旧表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 如果hiTail不为空(说明旧表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}// 返回新表return newTab;
}

二、 应用与最佳实践

2.1 在实际项目中如何合理使用HashMap

合理使用HashMap的建议:

  1. 缓存数据:可以使用HashMap作为缓存的数据结构,将计算结果或者频繁访问的数据存储在HashMap中,以提高数据的访问速度。
  2. 数据索引:在需要按照某个字段快速查找数据的场景下,可以使用HashMap来构建索引,以便快速定位数据对象。
  3. 配置信息存储:可以使用HashMap来存储应用程序的配置信息,例如键值对形式的配置参数
  4. 数据分组:当需要对数据进行分组时,可以使用HashMap来进行分组存储,以便快速获取特定分组的数据。
  5. 缓存计算结果:在一些需要频繁计算的场景下,可以使用HashMap来缓存计算结果,避免重复计算。
  6. 快速访问和修改:HashMap提供了快速的查找、插入和删除操作,适合于需要频繁进行这些操作的场景。
  7. 代替多层嵌套的条件判断:有时候可以使用HashMap代替多层嵌套的条件判断,提高代码的可读性和可维护性。
  8. 事件处理:可以在事件驱动的系统中使用HashMap来保存事件处理器,根据事件类型快速找到对应的处理器进行处理。
  9. 路由表:在网络相关的应用中,可以使用HashMap来构建路由表,快速查找目标地址对应的路由信息。

2.2 最佳实践和注意事项

最佳实践:

  1. 初始化容量: 在创建HashMap时,尽量提供初始容量,以减少扩容操作的频率。这可以通过构造函数中的参数来完成,如 HashMap(int initialCapacity)

    Map<String, Integer> map = new HashMap<>(16); // 初始化容量为16
    
  2. 负载因子: 负载因子是指哈希表在自动扩容之前可以达到多满的一个度量。默认负载因子是0.75,你可以根据你的需求调整。

    Map<String, Integer> map = new HashMap<>(16, 0.8f); // 指定负载因子为0.8
  3. 选择好的哈希函数: 如果你使用自定义对象作为键,确保实现了合适的hashCode()equals()方法。

  4. 线程安全性: 如果在多线程环境下使用HashMap,考虑使用ConcurrentHashMap,它提供了更好的并发性能。

    Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
  5. 遍历方式: 使用entrySet()来遍历HashMap,而不是分别使用keySet()values()。这可以避免多次计算哈希码。

    for (Map.Entry<String, Integer> entry : map.entrySet()) {// 处理每个键值对
    }

注意事项:

  1. 空指针检查: 在使用get()方法获取值之前,最好先检查键是否存在,以避免空指针异常。

    if (map.containsKey(key)) {// 避免空指针异常Integer value = map.get(key);// 处理值
    }
  2. 键和值的类型: 注意HashMap的键和值的类型,确保它们符合你的预期。使用泛型可以在编译时提供类型检查。

  3. 扩容代价: HashMap在达到一定负载因子时会自动扩容,这可能导致性能损失。在性能敏感的场景中,可以考虑手动调整容量以减少扩容的发生。

  4. 不要过度使用HashMap: 在某些情况下,可能有更适合的数据结构,如LinkedHashMap、TreeMap等,取决于你的需求。

  5. 了解并发限制: 如果在并发环境下使用HashMap,要注意可能出现的并发限制,确保你的代码是线程安全的。

三、 结论

3.1 对HashMap的全面总结

HashMap的全面总结:

  1. 概述:
  • 定义: HashMap是Java集合框架中的一部分,实现了Map接口,用于存储键值对。
  • 特性: 允许存储null键和null值,非同步(不是线程安全的)。
  1. 基本操作:
  • 插入元素: put(key, value)方法用于插入键值对。
  • 获取元素: get(key)方法用于根据键获取值。
  • 删除元素: remove(key)方法用于根据键删除键值对。
  1. 内部实现:
  • 数组+链表/红黑树: HashMap内部使用一个数组来存储桶(bucket),每个桶是一个链表或者红黑树,用于解决哈希冲突。
  • 哈希算法: 通过对键的哈希码进行运算,确定键在数组中的位置。
  1. 哈希冲突:
  • 链表解决冲突: 相同哈希码的键值对以链表形式存储在同一桶中。
  • 红黑树优化: 当链表长度过长时,会将链表转换为红黑树,以提高检索效率。
  1. 扩容和负载因子:
  • 负载因子: 默认为0.75,表示当HashMap中的元素个数达到容量乘以负载因子时,进行扩容操作。
  • 扩容: 当达到负载因子时,HashMap会创建一个新的容量是原容量两倍的数组,将原有元素重新分配到新的数组中。
  1. 性能:
  • 平均时间复杂度: 插入、删除、查找的平均时间复杂度都是O(1),在理想情况下。
  • 注意: 由于哈希冲突和扩容操作,性能可能会有所下降。
  1. 使用注意事项:
  • 线程安全: HashMap不是线程安全的,如果需要在多线程环境中使用,可以考虑使用ConcurrentHashMap
  • equals和hashCode方法: 为了正确存储和检索对象,键的类必须正确实现equalshashCode方法。
  1. JDK版本变化:
  • JDK 8: 引入了红黑树优化,以提高性能。
  • JDK 9: 基于树的桶(bin)中的元素现在是按插入顺序排序。

3.2 鼓励读者深入学习和实践

  • 源码分析: 阅读HashMap的源代码是学习其实现原理的一种有效方式。通过查看Java标准库的HashMap源码,你可以深入了解它是如何处理哈希冲突、计算哈希码、扩容等细节的。
  • 实际应用: 将HashMap应用于实际项目中,观察其在不同场景下的性能表现。理解何时使用HashMap以及如何调整其初始容量等参数是很重要的。
  • 深入学习数据结构和算法: 了解哈希表是如何在计算机科学中工作的,并学习其他数据结构和算法,有助于更好地理解HashMap的优势和局限性。
  • 参与开源项目: 如果可能,参与开源项目,特别是与数据结构和算法相关的项目。通过实际的编码和与其他开发者的交流,你可以深化对HashMap以及其他数据结构的理解。

盈若安好,便是晴天

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/202830.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Less 安装教程

文章目录 前言LESS的系统要求安装LESS例子输出Less编译css工具后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;Sass和Less &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板…

实战Flask+BootstrapTable最简动态表头及数据(ajax方法)

话不多说,有图有源码 1.实现原理:通过ajax从后端获取表头及数据 前端页面内容 <!DOCTYPE html> {% from "common/_macro.html" import static %} <html> <meta charset"utf-8"> <head> <!-- 引入bootstrap样式 --> <l…

【JAVA】学生信息管理系统

目录 前言 一、环境搭建 二、功能实现 1.学生信息类的创建 2.学生信息的添加功能 3.学生信息的删除功能 4.学生信息的修改功能 5.学生信息的查看功能 三、主类的调用 1.界面的搭建 2.学生端和教师端 3.系统和功能的选择 总结 前言 JAVA实现的学生信息管理…

超详细的的PyTorch安装教程,成功率高,适合初学者,亲测可用。

啰嗦几句&#xff1a;网上的教程很多&#xff0c;安装的方法多种多样&#xff0c;操作复杂&#xff0c;成功率还不高。小编在淘宝专门帮助不会安装的小伙伴远程配置环境&#xff0c;这方法都是测试过了&#xff0c;适用大部分人的&#xff0c;完全按照文章来操作&#xff0c;基…

服务化通信OPC实操

实操也是基于视频进行一些笔记&#xff0c;没得写就少写了 准备 Nuget包准备&#xff1a;OPCfoundation 一般都是使用Ua&#xff0c;当然也是有&#xff1a; 客户端链接服务器参数&#xff1a;IP Port 认证 登录用户名 Session 的实例化创建 进行使用&#xff1a; 因为Ses…

【C++】哈希(位图、布隆过滤器)

一、哈希的应用&#xff08;位图和布隆过滤器&#xff09; 1、位图&#xff08;bitset&#xff09; &#xff08;1&#xff09;位图概念 【题目】 给 40亿 个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在这 40亿 个数中。…

STM32 CAN协议讲解以及代码

STM32 CAN 文章目录 STM32 CAN前言一、CAN外设1.主控制寄存器CAN_MCR2.位时序寄存器CAN_BTR3.CAN的发送邮箱4.CAN的接收FIFO5.验收筛选器 二、代码配置1.初始化2.发送数据3.接收数据4.main.c 前言 前面学习了CAN的一些理论知识&#xff0c;他在我们的STM32里面是怎么用的呢 前…

VUE简易购物车程序

目录 效果预览图 完整代码 效果预览图 完整代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>…

【日常总结】Swagger 3.0 + 集成 knife4j ,并设置header入参

一、场景 环境&#xff1a; 二、问题 思路 &#xff1a; 三、解决方案 &#xff08;推荐&#xff09; Stage 1&#xff1a;接入knife4j 依赖 Stage 2&#xff1a;修改 yaml 配置 Stage 3&#xff1a;修改 swagger 3 配置文件 Stage 4&#xff1a;查看效果 Swagger UI …

Java王者荣耀

本设计所开发实现的是基于Java的一个王者荣耀游戏&#xff0c;主要实现键盘控制人物移动&#xff0c;按钮点击事件&#xff0c;英雄动作的改变&#xff0c;鼠标事件&#xff0c;碰撞检测&#xff0c;妲己的三个技能外加普攻&#xff0c;显示生命值&#xff0c;小兵的自动生成和…

HarmonyOS 位置服务开发指南

位置服务开发概述 移动终端设备已经深入人们日常生活的方方面面&#xff0c;如查看所在城市的天气、新闻轶事、出行打车、旅行导航、运动记录。这些习以为常的活动&#xff0c;都离不开定位用户终端设备的位置。 当用户处于这些丰富的使用场景中时&#xff0c;系统的位置能力…

uni-app打包后,打开软件时使其横屏显示

找到page.json文件&#xff0c;在global加入以下代码&#xff1a; 这样就可以横屏显示了。

String类的学习笔记(上):介绍String类及其常用方法的使用

本文介绍了Java中用来描述操作字符串的String类,和其一些常用的基本操作方法,字符串的创建输出,字符串对象的比较,字符串查找,字符串的转化,字符串的替换,字符串拆分,字符串截取,和大小写转换,去除左右空格,子字符串包含,学会使用这些方法,能更方便的使用操作字符串~ String类的…

链表经典面试题

1 回文链表 1.1 判断方法 第一种&#xff08;笔试&#xff09;&#xff1a; 链表从中间分开&#xff0c;把后半部分的节点放到栈中从链表的头结点开始&#xff0c;依次和弹出的节点比较 第二种&#xff08;面试&#xff09;&#xff1a; 反转链表的后半部分&#xff0c;中间节…

蓝桥杯每日一题2023.11.26

题目描述 奖券数目 - 蓝桥云课 (lanqiao.cn) 将每一个数字进行一一枚举&#xff0c;如果检查时不带有数字4则答案可以加1 #include<bits/stdc.h> using namespace std; int ans; bool check(int n) {while(n){if(n % 10 4)return false;n / 10; }return true; } int m…

C语言,通过数组实现循环队列

实现循环队列最难的地方就在于如何判空和判满&#xff0c;只要解决了这两点循环队列的设计就没有问题。接下来我们将会使用数组来实现循环队列。 接下来&#xff0c;为了模拟实现一个容量为4的循环队列&#xff0c;我们创建一个容量为4 1 的数组。 接下来我们将会对这个数组…

pyenv local x.xx.x不生效

我本地原来有个python&#xff0c;之后用pip安装了pyenv&#xff0c;使用pyenv新安装了一个python&#xff0c;设置某个local的时候发现不生效。 这种情况需要检查3个地方。 1.有没有生成这个文件 2.需要重新开一个cmd 3.需要保证pyenv的path环境变量比之前本地的python优先…

机器学习探索计划——数据集划分

文章目录 导包手写数据划分函数使用sklearn内置的划分数据函数stratifyy理解举例 导包 import numpy as np from matplotlib import pyplot as plt from sklearn.datasets import make_blobs手写数据划分函数 x, y make_blobs(n_samples 300,n_features 2,centers 3,clus…

【JavaSE】基础笔记 - 类和对象(上)

目录 1、面向对象的初步认知 1.1、什么是面向对象 1.2、面向对象与面向过程 2. 类定义和使用 2.1、简单认识类 2.2、类的定义格式 2.3、自定义类举例说明 2.3.1、定义一个狗类 2.3.2、定义一个学生类 3、类的实例化 3.1、什么是实例化 3.2、类和对象的说明 1、面向…

【腾讯云云上实验室】用向量数据库—实践相亲社交应用

快速入口 &#x1f449;向量数据库_大模型知识库_向量数据存储_向量数据检索- 腾讯云 (tencent.com) 文章目录 前言1. 向量数据库概念及原理1.1 向量数据库概念1.2 向量数据库核心原理1.3 向量数据库优缺点1.4 向量数据库与传统数据库的区别 2. 腾讯云向量数据库的基本特性及优…