【Java】Map和Set接口初识

前言

TreeMap 和 TreeSet 即是 Java 中利用搜索树实现的 MapSet
实际上用的是红黑树,红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容后续再进行讲解


搜索

  • Map 和 Set 是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关
    • Map 实例化的子类: TreeMapHashMap (哈希) ,两者底层实现是不一样的
      1. TreeMap 的底层是 “红黑树” 实现的,查找的复杂度为 O(logN)
      2. HashMap 的底层是 “哈希桶” 实现的,查找的复杂度为 O(1)
    • Set 实例化 TreeSetHashSet (哈希)也是不一样的

以前常见的搜索方式有:
1. 直接遍历,时间复杂度为 O(N),元素如果比较多效率会非常慢
2. 二分查找,时间复杂度为 O(logN),但搜索前必须要求序列是有序的

上述排序两种搜索比较适合静态类型的查找,即:一般不会对区间进行插入和删除操作了,而现实中的查找比如:
1. 根据姓名查询考试成绩
2. 通讯录:根据姓名查询联系方式
2. 不重复集合,需要先搜索关键字是否已经在集合中

注意: 这种查询一般是需要查一个东西的对应信息,如根据姓名查询考试成绩,它不是只查姓名或成绩,它是查出姓名之后,返回这个姓名对应的成绩。

这种查找可能在会对数据进行一些插入和删除的操作,即动态查找,那上述两种方式就不太合适了,本章介绍的 MapSet 是一种适合动态查找的集合容器


模型

  • 一般把搜索的数据称为 关键字(Key),和关键字(Key)对应的称为 值(Value),将其称之为 Key-Value键值对,所以模型会有两种:
    1. 纯 Key 模型,比如:
      • 有一个英文词典,快速查找一个单词是否在词典中
      • 快速查找某个名字在不在通讯录中
    2. Key-Value 模型,比如:
      • 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>
      • 梁山好汉的江湖绰号:每个好汉都有对应的江湖绰号:<好汉,好汉绰号>
    • Map 中存储的就是 key-value 的键值对,Set 只存储了 key

接口目录

![[Pasted image 20231116153144.png]]


关于 Map 的说明

  • Map 是一个接口类,该类没有继承自 Collection ,该类中存储的是<K,V>结构的键值对,并且 K值 一定是唯一的,不能重复!!!

关于 Map.Entry 的说明

  • Map.Entry<K,V> 是 Map 内部实现的用来存放 <key,value> 键值对映射关系的内部类,该内部类中主要提供了 <key,value> 的获取,value 的设置以及 key 的比较方式
    ![[Pasted image 20231117133633.png]]

注意:

  • Map.Entry<K,V> 并没有提供设置 Key 的方法

Map的常用方法

![[Pasted image 20231116164637.png]]


实例化

  • 以实例化 TreeMap 为例
  • 假设现在查英语单词出现的次数
    • 英语单词对应 String 类型
    • 出现的次数对应 Integer 类型
    • Map 存储的是 key-value 模型,是键值对
public static void main(String[] args) {  
    Map<String,Integer> map = new TreeMap<>();  
}

V put() 方法

  • 设置键值对,keyvalue
// one 这个单词,出现2次;即 one 这个关键码key对应的是 2 这个数  
map.put("one",2); 

// two 这个单词,出现4次;two 这个关键码key对应的是 4 这个数  
map.put("two",4); 

// three 这个单词,出现8次;three 这个关键码key对应的是 8 这个数
map.put("three",8); 

说明:

  • 方法前的 V 说明该方法返回值是 Value 的类型
  • 方法前的 K 说明该方法返回值是 Key 的类型

V get() 方法

  • 返回关键码 key 对应的 value
  • 因为我们上述关键码key 对应的 value 是 Integer类型,所以我们这里用 Integer 类型去接收
Integer integer = map.get("one"); // 返回的是其对应的 value

20231116173931


V getOrDefault() 方法

  • 返回 key 对应的 value,如果这个 keyTreeMap 集合中不存在的话,就返回所设定的默认值
Integer integer = map.getOrDefault("people",789);

![[Pasted image 20231116184125.png]]

  • TreeMap 中不存在 people 这个字符串,我给他设定的默认值为 789,所以接收到的返回值就是 789
  • 有一点 “即时” 的感觉,临时拉来充个数

V remove() 方法

  • 删除 key 对应的映射关系,将 key - value 这个键值对全部删掉了,再去获取这个 key 的对应值时,返回的就是 null,即不存在这个 关键码key
map.remove("one");

![[Pasted image 20231116184536.png]]


Set<K> keySet() 方法

  • 该方法的返回值是 Set<K> 类型
  • 把 Map 里面所有的 Key 放到 Set 里面,即返回所有的 Key
// 这里我们的 Key 是 String 类型的  
Set<String> set = map.keySet();  
System.out.println(set);

![[Pasted image 20231117123930.png]]

注意:

  • 输出的 Key 并不是有序的
  • 因为是通过哈希函数来确定它的哈希地址,所以会和我们插入顺序不同

Collection<V> values() 方法

  • keySet() 方法类似,只不过这是返回所有的 Value
Collection<Integer> collection = map.values();  
System.out.println(collection);

![[Pasted image 20231117124213.png]]

注意:

  • 同理,输出的 Value 并不是有序的
  • 因为是通过哈希函数来确定它的哈希地址,所以会和我们插入顺序不同

Set<Map.Entry<K,V>> 、map.entrySet() 方法(难点)

  • 返回所有的 Key - Value 映射关系

  • Set 在计算机中有 集合 的意思,Entry 在计算机中有 记录 的意思

  • 这样的一个键值对,就代表一个 Entry(记录),将这个键值对看成是一个整体![[Pasted image 20231117134142.png]]

  • Set 就是一个集合,集合里面装满了 Entry(记录)![[Pasted image 20231117134312.png]]

Set<Map.Entry<String,Integer>> entrySet = map.entrySet();  
System.out.println(entrySet);
  • 打印结果:
    ![[Pasted image 20231117134612.png]]

  • 遍历打印

Set<Map.Entry<String,Integer>> entrySet = map.entrySet();  
for(Map.Entry<String,Integer> entry : entrySet) {  
    String key = entry.getKey();  
    Integer value = entry.getValue();  
    System.out.println("key = " + key + "value = " + value);  
}
  • 结果
    ![[Pasted image 20231117134832.png]]

注意事项

  1. Map是一个接口,不能直接实例化对象,如果要实例化对象,只能实例化其实现类 TreeMap 或者 HashMap
  2. Map中存放的键值对中的 key 是唯一的,不能够重复,但 value 是可以重复的
    • 如图,它会覆盖掉旧数据![[Pasted image 20231211144829.png]]
  3. TreeMap 中插入 键值对 时,key 不能为空,否则就会抛 NullPointerException异常,但是 value 可以为空
    • PS:HashMap 中的 keyvalue 都可以为空
  4. Map中的 key 可以全部分离出来,存储到 Set 中来进行访问(因为Key不能重复)
  5. Map中的 value 可以全部分离出来,存储在 Collection 的任何一个子集合中value 可能有重复)
  6. Map 中键值对的 key 不能直接修改,value 可以直接修改,如果要修改 key ,只能先将该 key 删除掉,然后再重新 put 插入

Map底层结构

![[Pasted image 20231211150928.png]]


Set的常用方法

20231211150543


boolean add() 方法

  • 添加元素,但重复元素不会被添加成功
Set<String> set = new TreeSet<>();  
set.add("one");  
set.add("two");  
set.add("three");

![[Pasted image 20231211151117.png]]


迭代器遍历 Iterator<E> 、set.iterator()方法

  • 返回值是一个迭代器
Iterator<String> it = set.iterator();  
while (it.hasNext()) {  
    System.out.println(it.next());  
}

![[Pasted image 20231211151418.png]]


注意事项

  1. Set 是继承自 Collection的一个接口类
  2. Set中只存储了 key,并且要求 key 绝对唯一
  3. TreeSetHashSet 的底层是使用Map的底层来实现的,其使用 keyObject 的一个默认对象作为 “键值对” 插入到 Map 中的
  4. Set最大的功能就是对集合中的元素进行去重
  5. 实现 Set 接口的常用类有 TreeSetHashSet,还有一个 LinkedHashSet,它是在 HashSet 的基础上维护了一个 双向链表 来记录元素的插入次序(当然了,还有一个 LinkedHashMap ,这是在高阶数据结构中要学习到的)
  6. Set 中的 key 不能修改,如果要修改,只能先将原来的删除掉,然后再重新插入
  7. TreeSet 中不能插入 null 的 key,但是 HashSet 可以

Set底层结构

![[Pasted image 20231211152128.png]]


哈希表

  • 上述篇幅中所讲的都是关于:搜索树 -> TreeMap ,虽然提到了 HashMap ,但都是一笔带过,接下来就是讲解哈希表这一数据结构

概念

哈希表是由 数组+链表+红黑树 组成的

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较顺序查找时间复杂度为 O(N),平衡树中是为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数

理想的搜索方法:不经过任何比较,直接一次就能从表中得到要搜索的元素
如果能构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码直接能够建立一一映射的关系,那么在查找时通过该函数就可以非常快地找到该元素

  • 当向该结构中:
    • 插入元素
      • 根据待插入元素的关键码,通过该函数计算出该元素的存储位置,并按此位置进行存放
    • 搜索元素
      • 对元素的关键码进行同样的计算(依旧是通过该函数),把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
  • 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或称为散列表)

例如:
哈希函数设置为:hash(key) = key % capacity,capacity为存储元素底层空间总的大小
![[Pasted image 20231211160059.png]]

  • 简单来讲:
  • 就是将关键码通过该函数运算后,将这个值,直接转成下标(直接观察下标就知道这个值是什么,只不过中间需要一道 哈希函数 工序)

冲突

用该方法进行搜索就不必进行多次关键码的比较,因此搜索的速度很快
但问题是:按照上述哈希方式,向集合中插入元素 44,会出现什么问题?

  • 答:会和元素4的存储位置冲突

  • 所以:不同关键码通过相同哈希函数计算出相同的哈希地址,这种现象称为 “哈希冲突” 或哈希碰撞。

  • 把具有不同关键码,但具有相同哈希地址的数据元素称为 “同义词”


冲突避免

首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键码的数量的,这就导致一个问题,冲突的发生是必然的,我们能够做的就是降低冲突率

  • 即类似物理上的各种不可抗力的因素,如阻力,我们只能尽可能地去降低这一因素带来的问题,而不可能去消除这个因素

冲突避免 – 哈希函数的设计

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

  • 哈希函数设计应包含以下三个原则
    • 哈希函数的定义域必须包括需要存储的全部关键码。而如果散列表允许有m个地址时,其值域必须在0到m-1之间
    • 哈希函数计算出来的地址能均匀分布在整个空间中
    • 哈希函数应该比较简单
  • 常见哈希函数:
    1. 直接定制法–(常用)
      • 取关键字的某个线性函数为散列地址:Hash(Key)= A·Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况
      • 使用场景:适合查找比较小且连续的情况 面试题:字符串中第一个只出现一次字符
    2. 除留余数法–(常用)
      • 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
        之后的了解,看pdf即可

冲突避免 – 负载因子调节(重点)

  • 载荷因子就是用来 量化 哈希冲突的概率,我们要让发生哈希冲突的概率保持在一定水平之下,一般是保持在0.75之下,即 loadFactor = 0.75超过 0.75,就需要对哈希表进行扩容

散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长度

  • 如图:当冲突率达到了一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率
  • 但是!!根据公式来看,降低冲突率可以减少表中元素个数,但这是在现实生活中根本不可能的事情!!(拜托,谁会把数据给删了的)
  • 所以我们能调整的就只有哈希表中的数组的大小,也就是说我们只能对哈希表进行扩容
    ![[Pasted image 20231211210730.png]]

降低冲突

降低哈希冲突 两种常见的方法是:闭散列开散列


闭散列(closed hashing)

闭散列:又称开地址方法(open addressing)。当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的 “下一个” 空位置中去

  • 那如何寻找下一个空位置呢?
  1. 线性探测: 从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
    • 比如上面的场景,现在需要插入142434,先通过哈希函数计算哈希地址,它们三个的哈希地址下标均为4,因此这三个值理论上应该插在下标为4的位置,但是该位置已经放了值为4的元素,即发生了哈希冲突。

    • 通过线性探测,这三个值依次向后探测空位置,按序插入空位置![[Pasted image 20231211212648.png]]

    • 注意! 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,142434这些值查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

  • 线性探测的缺陷是容易把发生冲突的值堆积在一起
  1. 二次探测
    • 二次探测就是为了避免上述堆积在一起的问题,它找下一个空位置的方法是:Hi = (H0 + i^2 )% m, 或者:Hi = (H0 - i^2 )% m

    • 如图:也就是让哈希地址相同的值,能够相隔一定距离来存放,不至于 “挤在一起”

    • ![[Pasted image 20231211213847.png]]
      ![[Pasted image 20231211213944.png]]

    • 研究表明:当表的长度为质数且表负载因子α不超过0.5时,新的值就一点能够插入,而且任何一个位置都不会被探查两次因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须保证表的负载因子α不超过0.5,如果超出必须扩容

  • 因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

开散列(open hashing) – 哈希桶

开散列法又叫开链法,首先对关键码用哈希函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合又称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中

如图:从图中可以看出,开散列中每个桶中放的都是发生哈希冲突的元素,开散列,可以认为是把一个大集合中的搜索问题转化为在小集合中做搜索了
![[Pasted image 20231211221159.png]]


哈希桶的模拟实现

构建构造方法
  • int[] array 这是一个数组,数组里面的空间存放的是 int 类型的
  • Node[] array 这是一个数组,数组里面的空间存放的是 Node 类型的
static class Node {  
    int key;  
    int val;  
    Node next;  
  
    public Node(int key,int val) {  
        this.key = key;  
        this.val = val;  
    }  
  
}  
  
public Node[] array;  
public int usedSize; // 记录有效数据个数  
public static final int DEFAULT_CAPACITY = 10;  // 默认数组大小
  
public MyHashBucket() {  
    array = new Node[DEFAULT_CAPACITY];  
}

put() 方法
  • 设置 keyvalue

步骤:

  1. 构建一个新的结点存放要插入的 keyvalue![[Pasted image 20231212074652.png]]

  2. 通过哈希函数,找到该结点所存放的位置。假设插入位置在数组的1下标![[Pasted image 20231212074814.png]]

  3. 遍历该哈希地址下标的链表,看看 key 值是否已经存在:如果存在,则更新对应 value 值;若不存在,则进行尾插法(注意,数组的每一个下标空间都是链表的头结点)![[Pasted image 20231212075927.png]]

  4. 哈希表插入后,还要检查一下负载因子是否大于0.75!! 若大于,则必须给哈希表扩容!!!

    • 但是但是,哈希表的扩容非常重要!不同于之前简单的 copyOf() 方法,哈希表扩容之后,原来哈希地址冲突的元素,可能要放到新数组的其他位置上去!!!
    • 如,原容量为10的数组的下标 1 地址存放了 1 和 11 两个键值对,扩容之后新数组容量变为20,那么 11 这个值就得放在 下标11 地址
    • 所以就需要 遍历原来 “哈希数组” 的 “每个数组元素”(即链表)![[Pasted image 20231212084959.png]]
  • 代码:
private static final float LoadFactor = 0.75f; // 负载因子

public void put(int key,int val) {  
    // 4.检查负载因子  
    if (checkLoadFactor() > LoadFactor) {  
        // 扩容  
        resize();  
    }  
}

private float checkLoadFactor() {  
    return usedSize * 1.0f / array.length;
}

private void resize() {  
	// 1.创建一个新数组作为哈希表  
	int len = 2 * array.length;  
	Node[] newArr = new Node[len];
	
    // 2.遍历旧哈希数组每个元素(链表)  
    // 将链表上的每一个结点重新一个一个地插入进新数组  
	// 当然这种做法在哈希表很大的时候,一旦扩容,服务器就会突然很卡
    for (int i = 0; i < array.length; i++) {  
        Node cur = array[i];  
        while (cur != null) {  
            int newHashAddress = cur.key % newArray.length;  
            Node curNext = cur.next;  
            // 尾插法  
            if (newArray[newHashAddress] == null) {  
                // 如果新哈希地址为空,头结点即为当前cur结点  
                newArray[newHashAddress] = cur;  
            }else {  
                // 如果不为空,要找到新元素(链表)的最后一个结点  
                Node tmp = newArray[newHashAddress];  
                while (tmp.next != null) {  
                    tmp = tmp.next;  
                }  
                // tmp指向最后一个结点  
                tmp.next = cur;  
            }  
            // 插入完成后,cur继续走到下一个结点  
			// cur.next要置空,newArr[newHashAddress] = cur 是直接把地址存放在这个位置,但这个地址的 .next 可能还跟着数据  
			// 所以 cur 屁股后面的数据也跟到新数组了,需要将这个结点独立开来
            cur.next = null;  
            cur = curNext;  
        }  
    }  
    // 3.重新赋值数组  
    array = newArray;  
}

注意: 这很重要!!

  • resize() 方法中,需要将 newArray[xxx] = cur 中的 cur 结点的 next 域置为空,否则会导致旧哈希表中的元素复制到新哈希表中时,变成循环链表

完整代码:

public void put(int key, int value) {  
    // 1.创建新结点,存放 key 和 value    Node node = new Node(key, value);  
  
    // 2.通过hash算法将结点的 key 转换成 数组下标  
    int hashAddress = key % array.length;  
  
    // 3.遍历该hash地址下标的链表,观察 key 值是否已经存在;  
    // 若存在,则更新 key 对应的 value 值;若不存在,则将该结点进行尾插  
    Node cur = array[hashAddress], curPrev = null;  
    while (cur != null) {  
        if (cur.key == key) {  
            // 说明链表中存在 key 值,更新 key 对应的 value            
            cur.val = value;  
            return;        
        }  
        curPrev = cur;  
        cur = cur.next;  
    }  
  
    // 走到这 cur 为空,说明链表中无该 key,将该结点进行尾插  
    // curPrev定位在 cur 前一个结点,它也可能为 null    
    if (curPrev == null) {  
        // curPrev == null,说明该位置的链表是空的  
        array[hashAddress] = node;  
    } else {  
        curPrev.next = node;  
    }  
    usedSize++;  
  
    // 4.插入完成后,检查是否达到负载因子阈值  
    if (checkLoadFactor() > LoadFactor) {  
        // 若大于,则扩容  
        resize();  
    }  
}

private void resize() {  
    // 1.创建新数组  
    Node[] newArray = new Node[2 * array.length];  
    // 2.遍历旧哈希数组每个元素(链表)  
    for (int i = 0; i < array.length; i++) {  
        Node cur = array[i];  
        while (cur != null) {  
            int newHashAddress = cur.key % newArray.length;  
            Node curNext = cur.next;  
            // 尾插法  
            if (newArray[newHashAddress] == null) {  
                // 如果新哈希地址为空,头结点即为当前cur结点  
                newArray[newHashAddress] = cur;  
            }else {  
                // 如果不为空,要找到新元素(链表)的最后一个结点  
                Node tmp = newArray[newHashAddress];  
                while (tmp.next != null) {  
                    tmp = tmp.next;  
                }  
                // tmp指向最后一个结点  
                tmp.next = cur;  
            }  
            // 链接之后,cur的next域需要置空,否则会导致循环链表
            // 这一步就相当于把所有结点打散,然后重新链接
            cur.next = null;  
            cur = curNext;  
        }  
    }  
    // 3.重新赋值数组  
    array = newArray;  
}  
  
private float checkLoadFactor() {  
    return usedSize * 1.0f / array.length;  
}

get() 方法
  • 获取 键值对 key 对应的 value

步骤:

  • 通过哈希函数,获取 key 的哈希地址
  • 遍历该哈希地址(链表),找到之后返回
  • 找不到则返回 null

代码:

public int get(int key) {  
    // 1.key 通过 hash函数拿到数据对应的下标(即 hash地址)  
    int hashAddress = key % array.length;  
    // 2.遍历该地址下的链表  
    Node cur = array[hashAddress];  
    while (cur != null) {  
        if (cur.key == key) {  
            return cur.val;  
        }  
        cur = cur.next;  
    }  
      
    // 3.按道理没找到应该返回 null,返回值应该是“泛型”  
    // 但因为这只是模拟,返回值设置的是int类型,所以这里返回 -1 便好  
    return -1;  
}

注意事项

Map和Set的哈希桶,在多线程情况下,可能会发生死循环,它是不安全的
多线程情况下所使用的HashMap
![[Pasted image 20231212105456.png]]


冲突严重时的解决办法

  • 哈希桶由 数组 + 链表 + 红黑树 组成
    • (数组长度 >= 64 && 链表长度 >= 8) 以后,就把 链表变成红黑树
  • 不断套娃:
    • 每个桶背后是另一个哈希表
    • 每个桶背后是另一棵搜索树

哈希表的性能分析

虽然哈希表存在有冲突的问题,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数
所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1)


Map和Set与java类集的关系

  1. HashMapHashSet 是 Java 中利用哈希表实现的 Map类 和 Set类
  2. Java 中使用的是 哈希桶 方式解决冲突的
  3. Java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
  4. Java 中计算 哈希值 实际上是 调用的类的 hashCode() 方法,进行 key 的相等性比较是调用 keyequals() 方法。所以如果要用 自定义类 作为 HashMapkey 或者 HashSet 的值,建议必须重写 hashCode()equals() 方法,而且要做到 equals() 比较后,对象相等的,其 hashCode 一定是一致的

对于上述第4点的补充

  • 在之前我们提到过 HashMap 中的 key 是可以赋值任何类型的值的,但是 TreeMap 不行,因为 TreeMap 只能存放可比较的类型
  • 所以我们可以给 HashMap 添加我们 自定义的类对象
    ![[Pasted image 20231212172024.png]]

  • 我们知道,哈希值通过哈希函数来得到哈希地址,那么这个哈希值是怎么来的呢??
  • 答:通过 .hashCode() 方法
    • Object类中有 hashCode() 方法,如果我们不重写这个方法,那么 自定义类 所调用的 hashCode() 方法就是 Object 中的该方法,但这是个 native 方法,我们看不到![[Pasted image 20231212155419.png]]

    • 从下图中可见,如果使用默认的 Object 的 hashCode() 方法,具有同一 id 的两个不同的引用变量,所计算出的哈希值是不一样
      ![[Pasted image 20231212171757.png]]


关于为什么equals()和hashCode()两个方法要捆绑重写 – hashCode方法约定

  • 从源码的注释中我们可以总结出三条:
    1. 如果对象在使用 equals() 方法中进行比较的参数没有修改,那么多次调用一个对象的 hashCode() 返回的哈希值应该是相同的

    2. 如果两个对象通过 equals() 方法比较,是相等的,那么要求这两个对象的 hashCode() 方法返回的哈希值也应该是相等的

    3. 如果两个对象通过 equals() 方法比较是不同的,那么这两个对象所计算出的哈希值也是不相同的

  • 通过上面三条,我们可以知道,hashCode() 方法需要调用到 equals() 方法,是要比较对象的,即引用型变量是否相等
  • 这也是为什么要将两个方法捆绑重写的原因

重写hashCode与equals方法后

  • 从图中可以看见,重写之后,所计算出的哈希值就相同了
  • 根据个人测试,idea帮忙重写的这两个方法,是依据自定义类对象的第一个字符串变量(优先级最高)来计算哈希值的
    ![[Pasted image 20231212174104.png]]

补充
  1. 如果不重写 hashCode() 方法,如图:

    • put 的是 student1get 的是 student2,其获取 value 的结果是 null ,说明在该哈希表中,key 相同的两个对象,其哈希值是不同的!
      ![[Pasted image 20231212175136.png]]
  2. 如果重写 hashCode() 方法,如图:

    • 操作还是一样的,但是其获取 value 的结果是我设置的 2
      ![[Pasted image 20231212175326.png]]
  • 分析:
    • 第1点没重写,获取的是 null:因为两个对象哈希值不一样,存放在哈希表里的位置也会不一样,但是我们没插入 student2,所以获取不到
    • 第2点:重写之后的 hashCode() 通过 equals() 方法,计算出的哈希值是相同的,hashMap.get(student2)get() 方法,通过该哈希值,找到了 student1,所以就能获取到其对应的 value2

泛型哈希表的模拟

  • 在上述我们模拟的只是 <Int,Int> 的键值对,是固定的,不能像Java自带的可以插入任意类型的键值对
  • 接下来就来模拟这种 hash表
  • 注意! 引用变量比较相等,是要求用 equals() 方法!
public class HashBucket2<K,V> {  
  
    static class Node<K,V> {  
        K key;  
        V value;  
        Node<K,V> next;  
  
        Node(K key,V value) {  
            this.key = key;  
            this.value = value;  
        }  
    }  
  
    private Node<K,V>[] array;  
    private int usedSize;  
  
    private static final float Load_Factor = 0.75f;  
  
    public HashBucket2() {  
        array = (Node<K, V>[]) new Node[10];  
    }  
  
    public void put(K key,V val) {  
        int hash = key.hashCode();  
        int hashAddress = hash % array.length;  
        Node<K,V> cur = array[hashAddress];  
        Node<K,V> curPrev = null;  
        while (cur != null) {  
            if (cur.key.equals(key)) {  
                // 说明链表中存在key值,更新key  
                cur.value = val;  
                return;            }  
            curPrev = cur;  
            cur = cur.next;  
        }  
        Node<K,V> node = new Node(key,val);  
        if (curPrev == null) {  
            // 说明该哈希地址链表中无结点,是一个空链表  
            array[hashAddress] = node;  
        }else {  
            curPrev.next = node;  
        }  
        usedSize++;  
        // 后面的resize就不写了  
    }  
  
    public V get(K key) {  
        int hash = key.hashCode();  
        int hashAddress = hash % array.length;  
        Node<K,V> cur = array[hashAddress];  
        while (cur != null) {  
            if (cur.key.equals(key)) {  
                return cur.value;  
            }  
            cur = cur.next;  
        }  
        return null;  
    }  
}
  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行舟Yi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值