《Java-SE-第十八章》之HashMap(jdk8)
创始人
2024-04-04 20:09:39

前言

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”

博客主页:KC老衲爱尼姑的博客主页

博主的github,平常所写代码皆在于此

共勉:talk is cheap, show me the code

作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!

文章目录

  • Map
  • HashMap
    • HashMap概述
    • **HashMap属性**
    • 构造方法
    • 核心方法
      • 1.put()
        • 添加逻辑
      • 2.resize()
        • 扩容逻辑
      • 3.get()
      • 遍历

Map

Map是一种以键值对(key-value)进行存储的集合,Map集中的每一个元素都包含一个 键(key) 对象 和 一个值(value)对象。其其特点都是由键来决定的,Map集合的键都是无序,不重复,无索引,Map集合后面重复的键对应的值会覆盖前的重复键的值,并且键和值都允许为空。

HashMap

HashMap概述

HashMap是基于哈希表的Map接口实现的,该类存储的结构的键值对,并且k是唯一的,不能重复,而v作为值可以重复。

数据结构

在这里插入图片描述

HashMap属性

1.序列化ID

    private static final long serialVersionUID = 362498820763181265L;

2.HaahMap对象被创建时,初始的默认容量是16(1<<4)。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

3.HashMap的最大容量是2的30次方。

  static final int MAXIMUM_CAPACITY = 1 << 30;

4.HashMap默认的负载因子为0.75。

 static final float DEFAULT_LOAD_FACTOR = 0.75f;

5.当HashMap底层数组中某一位置的链表长度等于8,该链表便满足了转换成红黑树的条件。

 static final int TREEIFY_THRESHOLD = 8;

6.HashMap底层数组长度等于64时,链表长度等于8时,链表就会转换成红黑树。

static final int MIN_TREEIFY_CAPACITY = 64;

7.当链表已经转换成红黑树时,当树节点少于6便会退化成链表。

 static final int UNTREEIFY_THRESHOLD = 6;

8.HashMap底层是一个链表数组

transient Node[] table;

9.该集合存储的就是Node节点,便于遍历使用

 transient Set> entrySet;

10.记录数组中节点的个数

 transient int size;

11.记录HashMap修改的次数

  transient int modCount;

12.threshold是阈值,为了减少哈希冲突,HashMap底层的数组不是等到存储空间都被利用完之后才扩容,而是根据当前的负载因子和数组长度计算出一个阈值,当超过该阈值就进行扩容。

 int threshold;

13.该属性也是负载因子,仅仅是一个成员变量,便于在方法中使用。

final float loadFactor;

构造方法

使用HashMap的无参构造方法构造HashMap时,其默认的初始化容量是16,负载因子为0.75。

源码:

    public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; //负载因子为0.75}

HashMap还提供了三个带参构造方法,本质上可以视作两个。因为HashMap(int initialCapacity)方法体中实际调用的是HashMap(int initialCapacity, float loadFactor),使用带参构造指定初始化容量时,最终的初始化容量不一定是一开始指定的,初始化容量必须是2的次幂。tableSizeFor(initialCapacity)会对传入的容量进行调整,最终的调整的结果是等于或者大于指定容量的一个最接近2的次幂数。

  //initialCapacity和loadFactor分别为创建HashMap时指定的容量和负载因子public HashMap(int initialCapacity, float loadFactor) {//容量小于0抛出异常if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +  initialCapacity);//容量大于数组最大容量时,将initialCapacity调整为MAXIMUM_CAPACITYif (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;//如果指定的负载因子小于等于0,或者负载因子是非数字则抛出异常if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);//进行负载因子初始化this.loadFactor = loadFactor;//this.threshold = tableSizeFor(initialCapacity);}//创建HashMap指定容量initialCapacitypublic HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的有参构造}

根其它Map集合构造hashMap

如果传入的集合中有元素,在添加元素成功之前就会开辟好内存,如果该集合没有元素,就还是不会开辟内存。

	//创建一个HashMap并将m中的元素存入其中 public HashMap(Map m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}

获得一个大于cap又是最接近cap的2的整数次幂数值

  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;//判断n是否小于0,如果小于0则返回1,否则就继续判断是否大于最大容量,是的话就返回最大容量,不是则返回n+1。return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

综上所述,创建HashMap对象时,只是确定好了初始化容量以及负载因子,底层的数组并没有分配内存。只有当添加元素时才会给数组分配内存。

核心方法

HashMap真正的分配内配内存,是在添加元素时。

1.put()

源码:

 public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}
//该方法根据key计算出该节点应该插入到数组的哪一个下标static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}//插入元素final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//tab为哈希数组,p为节点,n为数组长度,i为数组下标Node[] tab; Node p; int n, i;//判断table的引用是否为空以及table数组的长度,为null或者为0说明需要扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//将扩容之后的数组长度赋值给n//如果数组的i位置没有值,就将传入的key-value插入即可,如果插入成功则返回null。如果插入的节点已经存在则返回这个节点。if ((p = tab[i = (n - 1) & hash]) == null)//p为该索引位置的链表的第一个节点tab[i] = newNode(hash, key, value, null);//插入节点else {//发生哈希冲突的情况//定义临时变量Node e; K k;//如果当前索引位置对应的链表第一个元素的hash与准备添加的key的hash相同//并且满足一下两个条件之一//1.准备加入的key与p.key相同//2.p指向的Node节点的key的equals()与准备加入的key比较后相同(如自定义类型作为key)if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//判断是否是红黑树的节点else if (p instanceof TreeNode)//添加到红黑树,如果该节点在红黑树中存在则返回该节点,不存在则返回nulle = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);else {//hash不为链表首元素,不是红黑树的节点,就是链表中的节点,遍历链表,依次把该元素与链表中的每个元素比较后,都不相同则加到末尾for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//如果遍历到链表末尾说明已经没有重复的节点,此时直接添加到链表的末尾p.next = newNode(hash, key, value, null);//如果链表长度已经达到了8个节点,就会进行树化if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash);break;}//在遍历的过程中找到了重复的节点,直接break,e为重复的节点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//有重复的key()旧节点和插入的节点if (e != null) { V oldValue = e.value;//拿到旧节点的valueif (!onlyIfAbsent || oldValue == null)e.value = value;//将待插入节点的value更新到旧节点afterNodeAccess(e);return oldValue;//返回}}++modCount;if (++size > threshold)//添加成功size首先++,再和阈值进行判断,大于就要扩容resize();afterNodeInsertion(evict);return null;//成功添加}

注意

链表树化需要满足连个添加,第一是链表长度已经到了8,并且数组长度要大于等于64。如果仅仅是链表长度满足了添加,调用 treeifyBin(tab, hash)去树化,实际还是对数组进行了扩容。

源码如下:

在这里插入图片描述

添加逻辑

  1. 首先根据key计算出哈希值
  2. 判断table是否为null或者为长度为0,满足就扩容
  3. 计算出数组的下标,并判断该位置是否为null
  4. 如果table[i]位置为null插入即可
  5. 不为空说明发生了哈希冲突,再判断插入的节点与当前位置节点的key是否相同
  6. 如果相同直接覆盖元素
  7. 如果不是,在判断是否是树节点
  8. 如果是树节点,红黑树插入
  9. 如果不是,直接遍历链表寻找是否存在相同的key
  10. 如果存在直接覆盖节点
  11. 不存在将该节点添加到链表末尾
  12. 接着判断链表长度是否达到了8,达到了尝试树化
  13. 最后对容量判断是否大于了阈值,大于了进行扩容。

2.resize()

final Node[] resize() {//将数组引用赋值该oldTabNode[] oldTab = table;//得到得到当前数组长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧阈值int oldThr = threshold;//newCap为数组新的容量,newThr为新的数组长度,newThr为新的阈值int newCap, newThr = 0;//判断是需要初始化数组还是扩容,如果oldCap小于或者等于0则表示需要初始化数组,大于0 则需要扩容if (oldCap > 0) {//oldCap大于数组的最大容量则需要重置大小if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//将数组扩容到到原来2倍,阈值也扩大原来的2倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//说明调用的是有参构造方法,理由无参构造没有对threshold进行初始化else if (oldThr > 0) newCap = oldThr;else { //无参构造初始化数组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);}//将计算出来的阈值赋值给成员变量 thresholdthreshold = 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) {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 orderNode loHead = null, loTail = null;Node hiHead = null, hiTail = null;Node next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;//返回新数组的引用}

扩容逻辑

  1. 首先为初始化和扩容准备好所需的变量
  2. 根据当前数组的长度(oldCap)进行判断数组是需要扩容还是初始化
  3. 如果oldCap大于0说明是需要扩容,将数组长度以及阈值都扩充到原来的2倍
  4. 如果oldCap小于等于0则说明数组需要初始化,
  5. 根据oldThr判读是调用那个构造方法进行初始化,然后为其分配初始容量以及负载因子
  6. 然后对新计算出来的阈值进行检查,如果为0则需要重新计算
  7. 最后如果是扩容,则将源数组的元素搬运到目标数组去。

3.get()

 public V get(Object key) {Node e;//调用getNode()方法来完成return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node getNode(int hash, Object key) {//table为数组,first头节点,n是数组长度Node[] tab; Node first, e; int n; K k;//判断数组是否null并且已经开辟空间了,同时得到计算出索引位置的节点(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)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;}

查找逻辑

  1. 根据key计算出哈希值
  2. 判断数组是否为null,长度是否为0,如果是就直接返回null
  3. 如果不是,计算出数组的下标,判断该位置的节点的key是否和要查找的key相同,一致直接返回
  4. 不相同,判断第一个节点是否是红黑树节点,如果是则去红黑树里去查找
  5. 如果不是树节点,说明是链表节点,遍历链表即可。找到就返回,还没找到返回null。

遍历

1.键找值的方式的遍历

代码示例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*** @author 929KC* @date 2022/11/5 15:57* @description:*/
public class Demo {public static void main(String[] args) {HashMap map = new HashMap<>();map.put("a",1);map.put("b",2);map.put("c",3);// 1、键找值:第一步:先拿到集合的全部键。Set key = map.keySet();// 2、第二步:遍历每个键,根据键提取值for (String s : key) {int  value = map.get(s);System.out.println(s+" "+value);}}
}//a 1
//b 2
//c 3

2.键值对的方式遍历

代码示例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*** @author 929KC* @date 2022/11/5 15:57* @description:*/
public class Demo {public static void main(String[] args) {HashMap map = new HashMap<>();map.put("a",1);map.put("b",2);map.put("c",3);Set> entries = map.entrySet();for (Map.Entry entry : entries) {Integer value = entry.getValue();String key = entry.getKey();System.out.println(key+" "+value);}}
}
//a 1
//b 2
//c 3

3.forEach遍历

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;/*** @author 929KC* @date 2022/11/5 15:57* @description:*/
public class Demo {public static void main(String[] args) {HashMap map = new HashMap<>();map.put("a",1);map.put("b",2);map.put("c",3);map.forEach(new BiConsumer() {@Overridepublic void accept(String key, Integer value) {System.out.println(key+" "+value);}});}
}
//a 1
//b 2
//c 3


在这里插入图片描述

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...