iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >Java集合框架中如何掌握Map和Set 的使用
  • 490
分享到

Java集合框架中如何掌握Map和Set 的使用

2023-06-22 03:06:06 490人浏览 薄情痞子
摘要

这篇文章将为大家详细讲解有关Java集合框架中如何掌握Map和Set 的使用,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。1. 搜索1.1 场景引入在学习编程时,我们常见的搜索方式

这篇文章将为大家详细讲解有关Java集合框架中如何掌握Map和Set 的使用,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

    1. 搜索

    1.1 场景引入

    在学习编程时,我们常见的搜索方式有:

    • 直接遍历:时间复杂度为 O(N),元素如果比较多效率会非常慢

    • 二分查找:时间复杂度为 O(logN),搜索前必须要求序列有序

    但是上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作。

    而现实中的查找如:

    • 根据姓名查询考试成绩

    • 通讯录(根据姓名查询联系方式)

    可能在查找时需要进行一些插入和删除操作,即动态查找。因此上述排序的方式就不太适合。

    1.2 模型

    一般会把要搜索的数据称为关键字(Key),和关键字对应的称为值(Value),这两者又能组合成为 Key-Value 的键值对。

    因此动态查找的模型有下面两种:

    纯 Key 模型: Set 的存储模式,比如:

    • 有一个英文词典,快速查找一个单词是否在词典中

    • 快速查找某个名字在不在通讯录中

    Key-Value 模型: Map 的存储模式,比如:

    • 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词, 单词出现的次数>

    • 梁山好汉的江湖绰号,每个好汉都有自己的江湖绰号:<好汉, 江湖绰号>

    2. Map

    2.1 关于 Map 的介绍

    简单介绍:

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

    注意:

    • Map 接口位于 java.util 包中,使用前需要引入它

    • Map 是一个接口类,故不能直接实例化对象。如果要实例化对象只能实例化其实现类 TreeMap 或 HashMap

    • Map 中存放的键值对的 Key 是唯一的,Value 是可以重复的

    文档信息:

    Java集合框架中如何掌握Map和Set 的使用

    2.2 关于 Map.Entry<K, V> 的介绍

    简单介绍:

    Map.Entry<K, V> 是 Map 内部实现的用来存放 <Key, Value> 键值对映射关系的内部类。该内部类中主要提供了 <Key, Value> 的获取,Value 的设置以及 Key 的比较方式

    常用方法:

    方法说明
    K geTKEy()返回 entry 中的 key
    V getValue()返回 entry 中的 value
    V setValue(V value)将键值对中的 value 替换为指定的 value

    注意:

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

    文档信息:

    Java集合框架中如何掌握Map和Set 的使用

    2.3 Map 的常用方法说明

    Java集合框架中如何掌握Map和Set 的使用

    注意:

    • 往 Map 中存储数据时,会先根据 key 做出一系列的运算,再存储到 Map 中

    • 如果 key 一样,那么新插入的 key 的 value,会覆盖原来 key 的 value

    • 在 Map 中插入键值对时,key 和 value 都可以为 null

    • Map 中的 key 可以全部分离出来,存储到 Set 中来进行访问(因为 key 是不能重复的)

    • Map 中的 value 可以全部分离出来,存储到 Collection 的任何一个子集中(注意 value 可能有重复)

    • Map 中的键值对的 key 不能直接修改,value 可以修改。如果要修改 key,只能先将 key 删掉,然后再进行重新插入(或者直接覆盖)

    2.4 关于 HashMap 的介绍

    简单介绍:

    • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射

    • HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步

    • HashMap 是无序的,即不会记录插入的顺序

    • HashMap 继承于 AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口

    注意:

    HashMap 类位于 java.util 包中,使用前需要引入它

    文档信息:

    Java集合框架中如何掌握Map和Set 的使用

    2.5 关于 TreeMap 的介绍

    简单介绍:

    • TreeMap 是一个能比较元素大小的 Map 集合,会对传入的 key 进行大小排序。其中可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。

    • 不同于 HashMap 的哈希映射,TreeMap 底层实现了树形结构,即红黑树的结构。

    注意:

    TreeMap 类位于 java.util 包中,使用前需要引入它

    文档信息:

    Java集合框架中如何掌握Map和Set 的使用

    2.6 HashMap 和 TreeMap 的区别

    Java集合框架中如何掌握Map和Set 的使用

    2.7 Map 使用示例代码

    注意:Map 是一个接口类,故不能直接实例化对象,以下以 HashMap 作为实现类为例

    示例一: 创建一个 Map 实例

    Map<String,String> map=new HashMap<>();

    示例二: 插入 key 及其对应值为 value

    map.put("惠城","法师");map.put("晓","刺客");map.put("喵班长","盗剑客");System.out.println(map);// 结果为:{惠城=法师, 晓=刺客, 喵班长=盗剑客}

    示例三: 返回 key 的对应 value

    String str1=map.get("晓");System.out.println(str1);// 结果为:刺客

    示例四: 返回 key 对应的 value,若 key 不存在,则返回默认值 defaultValue

    String str2=map.getOrDefault("弥惠","冒险者");System.out.println(str2);// 结果为:冒险者

    示例五: 删除 key 对应的映射关系

    String str3=map.remove("喵班长");System.out.println(str3);System.out.println(map);// 结果为:盗剑客 和 {惠城=法师, 晓=刺客}

    示例六: 除了上述直接打印 map,也可以通过 Set<Map.Entry<K, V>> entrySet() 这个方法进行遍历打印

    Set<Map.Entry<String,String>> set=map.entrySet();for(Map.Entry<String,String> str: set){    System.out.println("Key="+str.getKey()+" Value="+str.getValue());}

    示例七: 判断是否包含 key

    System.out.println(map.containsKey("惠城"));// 结果为:true

    3. Set

    3.1 关于 Set 的介绍

    简单介绍:

    Set 是一个继承于 Collection 的接口,是一个不允许出现重复元素,并且无序的集合,主要有 HashSet 和 TreeSet 两大实现类。

    注意:

    Set 接口位于 java.util 包中,使用前需要引入它

    文档信息:

    Java集合框架中如何掌握Map和Set 的使用

    3.1 Set 的常用方法说明

    Java集合框架中如何掌握Map和Set 的使用

    注意:

    Set 中只继承了 Key,并且要求 Key 唯一
    Set 的底层是使用 Map 来实现的,其使用 Key 与 Object 的一个默认对象作为键值对插入插入到 Map 中
    Set 的最大功能就是对集合中的元素进行去重
    实现 Set 接口的常用类有 TreeSet HashSet,还有一个 LinkedHashSetLinkedHashSet 是在 HashSet 的基础上维护了一个双向链表来记录元素的插入次序
    Set 中的 Key 不能修改,如果修改,要先将原来的删除
    Set 中可以插入 null

    3.3 关于 TreeSet 的介绍

    简单介绍:

    TreeSet 实现类 Set 接口,基于 Map 实现,其底层结构为红黑树

    注意:

    TreeSet 类位于 java.util 包中,使用前需要引入它

    文档信息:

    Java集合框架中如何掌握Map和Set 的使用

    3.4 关于 HashSet 的介绍

    简单介绍:

    • HashSet 实现了 Set 接口,底层由 HashMap 实现,是一个哈希表结构

    • 新增元素时,新增的元素,相当于 HashMap 的 key,value 则默认为一个固定的 Object

    注意:

    HashSet 类位于 java.util 包中,使用前需要引入它

    文档信息:

    Java集合框架中如何掌握Map和Set 的使用

    3.5 TreeSet 和 HashSet 的区别

    Java集合框架中如何掌握Map和Set 的使用

    3.6 Set 使用示例代码

    注意:Set 是一个接口类,故不能直接实例化对象,以下以 HashSet 作为实现类为例

    示例一: 创建一个 Set 实例

    Set<Integer> set=new HashSet<>();

    示例二: 添加元素(重复元素无法添加成功)

    set.add(1);set.add(2);set.add(3);set.add(4);set.add(5);System.out.println(set);// 结果为:[1, 2, 3, 4, 5]

    示例三: 判断 1 是否在集合中

    System.out.println(set.contains(1));// 结果为:true

    示例四: 删除集合中的元素

    System.out.println(set.remove(3));// 结果为:true

    示例五: 返回 set 中集合的个数

    System.out.println(set.size());// 结果为:4

    示例六: 检测 set 是否为空

    System.out.println(set.isEmpty());// 结果为:false

    示例七: 返回迭代器,进行遍历

    Iterator<Integer> it=set.iterator();while(it.hasNext()){    System.out.println(it.next());}// 结果为:1 2 4 5

    hashNext() 方法是判断当前元素是否还有下一个元素,next() 是获取当前元素,并且指向下一个元素

    示例八: 清空集合

    set.clear();System.out.println(set);// 结果为:[]

    4. 编程练习题

    4.1 找出第一个重复数据

    题目:

    从一些数据中,打印出第一个重复数据

    代码:

    public static void findNum(int[] array){    Set<Integer> set=new HashSet<>();    for(int i=0;i<array.length;i++){        if(set.contains(array[i])){            System.out.println(array[i]);            break;        }        set.add(array[i]);    }}

    4.2 去除重复数据

    题目:

    去除一些数据当中重复的数据

    代码:

    public static int[] removeSample(int[] array){    Set<Integer> set=new HashSet<>();    for(int i=0;i<array.length;i++){        set.add(array[i]);    }    Object[] arr=set.toArray();    return array;}

    4.3 统计重复数据的出现次数

    题目:

    统计重复的数据出现的次数

    代码:

    public static Map count(int[] array){    Map<Integer,Integer> map=new HashMap<>();    for(int i=0;i<array.length;i++){        if(map.containsKey(array[i])){            int val=map.get(array[i]);            map.remove(array[i]);            map.put(array[i],val+1);        }else {            map.put(array[i], 1);        }    }    return map;}

    4.4 只出现一次的数字

    题目(OJ 链接):

    给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素

    代码1: 异或法

    public int singleNumber(int[] nums) {    int sum=0;    for(int i=0;i<nums.length;i++){        sum=sum^nums[i];    }    return sum;}

    代码2: 使用 HashSet

    public int singleNumber(int[] nums) {    Set<Integer> set=new HashSet<>();    for(int val: nums){        if(set.contains(val)){            set.remove(val);        }else{            set.add(val);        }    }    for(int val: nums){        if(set.contains(val)){            return val;        }    }    return -1;}

    4.5 复制带随机指针的链表

    题目(OJ 链接):

    给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

    代码:

    public static node copyRandomList(Node head) {    Map<Node,Node> map=new HashMap<>();    Node cur=head;    while(cur!=null){        Node node=new Node(cur.val);        map.put(cur,node);        cur=cur.next;    }    cur=head;    while(cur!=null){        Node node=map.get(cur);        node.next=map.get(cur.next);        node.random=map.get(cur.random);        cur=cur.next;    }    return map.get(head);}

    4.6 宝石与石头

    题目(OJ 链接):

    给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。 stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。

    字母区分大小写,因此 “a” 和 “A” 是不同类型的石头。

    代码:

    public int numJewelsInStones(String jewels, String stones) {        Set<Character> set=new HashSet<>();        for(int i=0;i<jewels.length();i++){            set.add(jewels.charAt(i));        }        int count=0;        for(int i=0;i<stones.length();i++){            if(set.contains(stones.charAt(i))){                count++;            }        }        return count;    }

    4.7 旧键盘

    题目(OJ 链接):

    旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出
    肯定坏掉的那些键。

    代码:

    import java.util.*;public class Main{    public static void main(String[] args){        Set<Character> set=new HashSet<>();        List<Character> list=new ArrayList<>();        Scanner scanner=new Scanner(System.in);        String str1=scanner.next();        String str2=scanner.next();        char[] s1=str1.toUpperCase().toCharArray();        char[] s2=str2.toUpperCase().toCharArray();        for(int i=0;i<s2.length;i++){            set.add(s2[i]);        }        for(int i=0;i<s1.length;i++){            if(!set.contains(s1[i])){                set.add(s1[i]);                System.out.print(s1[i]);            }        }    }}

    4.8 前 K 个高频单词

    题目(OJ 链接):

    给一非空的单词列表,返回前 k 个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。

    代码:

    public List<String> topKFrequent(String[] Words, int k) {    Map<String,Integer> map=new HashMap<>();    for(String s: words){        if(map.containsKey(s)){            map.put(s,map.get(s)+1);        }else{            map.put(s,1);        }    }    PriorityQueue<Map.Entry<String,Integer>> minHeap=new PriorityQueue<>(new Comparator<Map.Entry<String,Integer>>(){        @Override        public int compare(Map.Entry<String,Integer> s1, Map.Entry<String,Integer> s2){            if(s1.getValue().compareTo(s2.getValue())==0){                return s2.getKey().compareTo(s1.getKey());            }            return s1.getValue()-s2.getValue();        }    });    for(Map.Entry<String,Integer> entry: map.entrySet()){        if(minHeap.size()<k){            minHeap.add(entry);        }else{            if(entry.getValue().compareTo(minHeap.peek().getValue())>0){                minHeap.poll();                minHeap.offer(entry);            }else if(entry.getValue().compareTo(minHeap.peek().getValue())==0){                if(entry.getKey().compareTo(minHeap.peek().getKey())<0){                    minHeap.poll();                    minHeap.offer(entry);                }            }        }    }    List<String> list=new ArrayList<>();    for(int i=0;i<k;i++){        Map.Entry<String,Integer> top=minHeap.poll();        list.add(top.getKey());    }    Collections.reverse(list);    return list;}

    5. 哈希表

    5.1 概念

    简单介绍:

    哈希表(也叫做散列表),是根据关键码(Key Value)而直接进行访问的数据结构。它通过把关键码映射到表中的一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数(也叫做散列函数),存放记录的数组叫做哈希表(也叫散列表)

    在之前讲解的数据结构中,元素关键码及其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较,搜索的效率取决于搜索过程中元素的比较次数。例如:

    • 顺序查找的时间复杂度为:O(N)

    • 二分查找的时间复杂度为:O(log(N))

    我们希望有一种理想的搜索方法,它可以不经过任何比较,能直接从表中得到要搜索的元素。因此就有了哈希表,它的原理就是:通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,例如:

    插入元素:

    根据待插入元素的关键码,以某个函数计算出该元素的存储位置,并按此位置进行存放

    搜索元素:

    用该函数对元素的关键码进行计算,把求得的函数值当作元素的存储位置,在此结构中按此位置取元素与要搜索的元素进行比较,若关键码相等,则搜索成功

    上述的方法就叫做哈希方法(也叫散列方法),其中哈希方法中使用的转换函数称为哈希函数(也叫做散列函数),构造出来的结构称为哈希表(也叫散列表)

    示例: 当我们将哈希函数设置为:hash(key) = key % capacity 时,我们往一个数组中存放以下几个元素,存放形式如下

    Java集合框架中如何掌握Map和Set 的使用

    但是使用哈希方法会出现一个问题:哈希冲突

    5.2 哈希冲突——概念

    简单介绍:

    哈希冲突(也叫做哈希碰撞),是指对于两个数据元素的关键字 ki 和 kj (i != j),虽然 ki != kj,但是存在:Hash(ki) == Hash(kj),即不同关键字通过相同哈希函数,可能会计算出相同的哈希地址

    示例: 当我们将哈希函数设置为:hash(key) = key % capacity 时,元素 3 和 13 通过该哈希函数计算出的存放位置是一样的

    Java集合框架中如何掌握Map和Set 的使用

    由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致,哈希冲突的发生是必然的。并且哈希冲突不能根本上消除,为此我们就要

    • 在哈希冲突发生前:尽量避免

    • 在哈希冲突发生后:重点解决

    5.3 哈希冲突——避免

    5.3.1 哈希函数设计

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

    哈希函数的设计原则:

    • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到 m-1 之间

    • 哈希函数计算出来的地址能够均匀分布在整个空间中

    • 哈希函数应该比较简单

    常见哈希函数:

    直接定制法(常用)

    • 思路:取关键字的某个线性函数为散列地址:Hash(Key) = A * Key + B

    • 优点:简单、均匀

    • 缺点:需要事先知道关键字的分布情况

    • 使用场景:适合查找比较小且连续的情况

    除留余数法(常用)

    • 思路:设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(Key) = Key % p (p<=m) 将关键码转换成哈希地址

    平方取中法(了解)

    • 思路:假设关键字为1234,对它去平方就是1522756,抽取中间的3位数227作为哈希地址。再比如关键字为3241,它的平方就是18671041,抽取中间的3位671作为哈希地址

    • 使用场景:不知道关键字的分布,而位数又不是很大的情况

    折叠法(了解)

    • 思路:将关键字从左到右分割成位数相等的及部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址

    • 使用场景:事先不需要知道关键字的分布,且关键字位数比较多的情况

    随机数法(了解)

    • 思路:选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 Hash(Key) = random(Key) (random 为随机函数)

    • 使用场景:通常应用于关键字长度不等时的情况

    数学分析法(了解)

    • 思路:设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀的只有某几种符号。可跟据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址

    • 使用场景:适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均与的情况

    5.3.2 负载因子调节

    负载因子定义:

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

    α 是散列表装满程度的标志因子。由于表长是定值,α 与填入表中的元素个数成正比,所以 α 越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之 α 越小,表名填入表中的元素越少,产生冲突的可能性就越小

    一些采用开放定址法的 hash 库,如 Java 的系统库限制了载荷因子为0.75,超过此值将 resize 散列表

    负载因子和冲突率的关系粗略演示图:

    Java集合框架中如何掌握Map和Set 的使用

    通过演示图我们可以很直观的了解,当冲突率越大时,我们需要通过降低负载因子来变相降低冲突率。而填入表中的元素个数已成定局,我们不能够修改,因此需要调整哈希表中的数组大小,即调整散列表长度

    5.4 哈希冲突——解决

    解决哈希冲突两种常见的方法:

    • 闭散列

    • 开散列

    5.4.1 闭散列

    简单介绍:

    闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明哈希表中必然还有空位置,那么可以把 Key 存放到冲突位置的下一个空位置中去

    寻找冲突位置下一个空位置的方法:

    线性探测

    • 方法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止,如果后面的位置都满了,则会从头开始寻找

    • 缺点:会将所有冲突元素都堆积在一起

    二次探测

    • 方法:Hi = (H0 + i2) % m 或者 Hi = (H0 - i2) % m(i = 1,2,3…),H0 是通过散列函数 Hash(x) 对元素的关键码 key 进行计算得到的位置,m是表的大小,Hi 是寻找到的空位置

    • 缺点:空间利用率较低,这也是哈希表的缺陷

    5.4.2 开散列/哈希桶

    简单介绍:

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

    补充:

    HashMap 的底层就是一个开散列

    可以认为开散列是把一个在大集合中的搜索问题转化为在小集合中做搜索

    由于会尽量避免冲突,所以冲突率其实会有保障,因此当在单链表中做搜索时,其实很快,时间复杂度接近:O(1)

    但是当冲突很严重时,那么在单链表这个小集合中做搜索其实性能就会大大降低,因此单链表就不太适合做小集合的结构

    冲突严重时的解决办法:

    • 将哈希桶的结构替换成另一个哈希表

    • 将哈希桶的结构替换成搜索树

    5.5 相关习题

    补充: Hash表的平均查找长度(ASL)包括查找成功时的平均查找长度和查找失败时的平均查找长度

    • 查找成功的平均查找长度=表中每个元素查找成功时的比较次数之和/表中元素个数

    • 查找不成功的平均查找长度=该位置如果查找一个元素失败的次数/表的长度

    题目:

    设哈希表长度为11,哈希函数 H(K) = (K的第一个字母在字母表中的序号) % 11,若输入顺序为(D、BA、TN、M、CI、I、K、X、TA),采用闭散列处理冲突方法为线性探测,要求构造哈希表,在等概率的情况下查找成功的平均查找长度为( ),查找不成功的平均查找长度为( )

    查找成功的平均查找长度为:20/9

    Java集合框架中如何掌握Map和Set 的使用

    查找不成功的平均查找长度为:58/11

    Java集合框架中如何掌握Map和Set 的使用

    5.6 性能分析

    虽然哈希表一直在和冲突做斗争,但在实际使用过程中,哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数。

    因此,通常意义下我们认为哈希表的插入、删除、查找时间复杂度为:O(1)

    6. 手动实现 HashMap(提及 hashCode 和 equals 的作用)

    • HashMap 底层是:数组+单链表

    • 我们可以先定义一个最基础的哈希表,让他可以实现整形的存值、取值等(哈希函数设定为:Hash(Key) = Key % capacity)

    class HashBuck{    static class Node{        public int key;        public int value;        public Node next;        public Node(int key, int value) {            this.key = key;            this.value = value;        }    }    public Node[] array;    public int usedSize;    public HashBuck(){        this.array=new Node[8];    }    // 获取 key 对应的 value    public int get(int key){        int index=key%this.array.length;        Node cur=this.array[index];        while(cur!=null){            if(cur.key==key){                return cur.value;            }            cur=cur.next;        }        return -1;  // 这里先用 -1 返回,也可以抛异常    }    // 新增元素    public void put(int key, int val){        int index=key%this.array.length;        Node cur=this.array[index];        Node node=new Node(key,val);        if(cur==null){            this.array[index]=node;        }else{            while(cur.next!=null){                if(cur.key==key){                    cur.value=val;                    break;                }                cur=cur.next;            }            cur.next=node;        }        this.usedSize++;        if(loadFactor()>=0.75){            resize();        }    }    // 计算负载因子    public double loadFactor(){        return this.usedSize*1.0/this.array.length;    }    // 扩容后,重新构造哈希    public void resize(){        Node[] oldArray=this.array;        this.array=new Node[2*oldArray.length];        this.usedSize=0;        for(int i=0;i<oldArray.length;i++){            Node cur=oldArray[i];            while(cur!=null){                put(cur.key,cur.value);                cur=cur.next;            }        }    }}

     当我们实现了对整形的部分哈希表的实现后,如果要实现其它类型是有问题的,因为我们设计的哈希函数为:Hash(Key) = Key % capacity,而其它类型都不能取模,因此我们需要能够对 Key 进行数字转换的方法。而在 Object 类有一个 hashCode() 方法,它能将传过来的对象,转换成一个合法的数字,以此来找到合适的位置,因此使用它就能解决我们上述的麻烦。除此之外,上述代码中的一些==需要修改为 equals() 方法,因为 equals 它能够判断传入的对象的实例是否相等

    通过上面一部分的分析,最终可以得到如下代码:

    class HashBuck<K,V>{    static class Node<K,V>{        public K key;        public V val;        public Node<K,V> next;        public Node(K key,V val){            this.key=key;            this.val=val;        }    }    public Node<K,V>[] array=new Node[8];    public int usedSize;    public void put(K key,V val){        int hash=key.hashCode();        int index=hash%this.array.length;        Node<K,V> cur=this.array[index];        Node<K,V> node=new Node<K,V>(key,val);        if(cur==null){            this.array[index]=node;        }else{            while(cur.next!=null){                if(cur.key.equals(key)){                    cur.val=val;                    break;                }                cur=cur.next;            }            cur.next=node;        }        this.usedSize++;        if(loadFactor()>=0.75){            resize();        }    }    public V get(K key){        int hash=key.hashCode();        int index=hash%this.array.length;        Node<K,V> cur=this.array[index];        while(cur!=null){            if(cur.key.equals(key)){                return cur.val;            }            cur=cur.next;        }        return null;    }    // 计算负载因子    public double loadFactor(){        return this.usedSize*1.0/this.array.length;    }    // 重新哈希    public void resize(){        Node<K,V>[] oldArray=this.array;        this.array=(Node<K, V>[]) new Node[2*oldArray.length];        this.usedSize=0;        for(int i=0;i<oldArray.length;i++){            Node<K,V> cur=oldArray[i];            while(cur!=null){                put(cur.key,cur.val);                cur=cur.next;            }        }    }}

    7. hashCode 和 equals 的关系

    如果 hashCode 一样,那么 equals 会一样吗?

    • equals 不一定一样,因为 hashCode 是确定在数组中存储的位置是不是一样,但不能确定在哈希桶中,存放的 key 是不是一样,即不能确定 equals 的结果是不是一样

    如果 equals 一样,那么 hashCode 一样吗?

    • hashCode 一定一样,因为 equals 一样了,那么在数组中存储的位置是肯定一样的

    8. 哈希和 Java 类集的关系

    • HashMap HashSet 即 Java 中利用哈希表实现的 Map 和 Set

    • Java 中会使用哈希桶解决哈希冲突

    • Java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)。具体数值为当前链表的长度超过8且当前桶的个数大于等于64时,就会将当前长度超过8的链表变为红黑树

    • Java 中计算哈希值实际上是调用 Object 类的 hashCode 方法,进行 Key 的相等性比较是调用的 equals 方法

    • 自定义类如果需要作为 HashMap 的 Key 或者 HashSet 的值,必须重写 hashCode equals 方法

    9. HashMap 部分源码解读

    HashMap 的默认容量是:16

    Java集合框架中如何掌握Map和Set 的使用

    HashMap 的最大容量为:1 << 30(必须为2的倍数)

    Java集合框架中如何掌握Map和Set 的使用

    HashMap 默认负载因子为:0.75

    Java集合框架中如何掌握Map和Set 的使用

    HashMap 中的单链表变为红黑树的条件,该值将在 putVal 方法中出现

    Java集合框架中如何掌握Map和Set 的使用

    HashMap 有四种构造方法

    方法一: 没有参数,则哈希表容量为:0,负载因子为:0.75

    Java集合框架中如何掌握Map和Set 的使用

    方法二: 传入一个 Map 参数,则哈希表负载因子为:0.75

    Java集合框架中如何掌握Map和Set 的使用

    方法三: 传一个容量参数 initialCapacity,则哈希表容量为:initialCapacity,负载因子为:0.75

    Java集合框架中如何掌握Map和Set 的使用

    方法四: 传两个参数,容量参数 initialCapacity,负载因子参数 loadFactor,则哈希表负载因子为:loadFactor

    Java集合框架中如何掌握Map和Set 的使用

    但是哈希表容量在这里不能确定,它有一个 tableSizeFor 的方法,为此我们转到它的源码

    Java集合框架中如何掌握Map和Set 的使用

    这个方法的意思是返回一个最近接传入的 initialCapacity 参数的向上取整的2的次方的数字,例如传入的参数为18,那么哈希表的容量就为32,传入的参数为1000,那么哈希表的容量就为1024

    put 方法的实现

    Java集合框架中如何掌握Map和Set 的使用

    hash 方法的实现,由于 hashCode 被 native 修饰,故无法看到具体源码

    Java集合框架中如何掌握Map和Set 的使用

    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;    // 当哈希表的大小为 0 时,进行 resize 调整    if ((tab = table) == null || (n = tab.length) == 0)        n = (tab = resize()).length;    // ((n - 1) & hash) 其实等价于 (n % 数组长度) 但是前提条件是,n是2的次幂    // 如果为 null 就是第一次插入    if ((p = tab[i = (n - 1) & hash]) == null)        tab[i] = newNode(hash, key, value, null);    // 如果不为 null,就进行尾插法    else {        Node<K,V> e; K k;        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            e = p;        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;                }                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    break;                p = e;            }        }        if (e != null) { // existing mapping for key            V oldValue = e.value;            if (!onlyIfAbsent || oldValue == null)                e.value = value;            afterNodeAccess(e);            return oldValue;        }    }    ++modCount;    if (++size > threshold)        resize();    afterNodeInsertion(evict);    return null;}

    resize 方法的实现(以2倍的方式进行扩容)

    final Node<K,V>[] resize() {    Node<K,V>[] oldTab = table;    int oldCap = (oldTab == null) ? 0 : oldTab.length;    int oldThr = threshold;    int newCap, newThr = 0;    if (oldCap > 0) {        if (oldCap >= MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return oldTab;        }        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;}

    10. HashMap 常考问题

    问题一:如果 new HashMap(19),那么 bucket 数组有多大?

    32,原因在第9大节的构造方法四中

    问题二:HashMap 什么时候开辟 bucket 数组占用内存?

    第一次 put 的时候

    问题三:HashMap 何时扩容?

    当填入的元素/容量大于负载因子的时候,进行扩容,jdk1.8 负载因子默认为0.75

    问题四:当两个对象的 hashCode 相同时会发生什么?

    会发生哈希冲突

    问题五:如果两个键的 hashCode 相同,你如何获取值对象?

    遍历当前位置的链表,通过 equals 返回和要查询的 Key 值一样的 Key 的 Value

    问题六:你了解重新调整 HashMap 大小存在什么问题吗?

    重新调整大小一定要进行重新哈希

    关于Java集合框架中如何掌握Map和Set 的使用就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

    --结束END--

    本文标题: Java集合框架中如何掌握Map和Set 的使用

    本文链接: https://www.lsjlt.com/news/302054.html(转载时请注明来源链接)

    有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

    本篇文章演示代码以及资料文档资料下载

    下载Word文档到电脑,方便收藏和打印~

    下载Word文档
    猜你喜欢
    • Java集合框架中如何掌握Map和Set 的使用
      这篇文章将为大家详细讲解有关Java集合框架中如何掌握Map和Set 的使用,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。1. 搜索1.1 场景引入在学习编程时,我们常见的搜索方式...
      99+
      2023-06-22
    • Java 集合框架掌握 Map 和 Set 的使用(内含哈希表源码解读及面试常考题)
      目录1. 搜索1.1 场景引入1.2 模型2. Map2.1 关于 Map 的介绍2.2 关于 Map.Entry<K, V> 的介绍2.3 Map 的常用方法说明2.4...
      99+
      2022-11-12
    • javascript ES6中set集合、map集合如何使用
      本文小编为大家详细介绍“javascript ES6中set集合、map集合如何使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“javascript ES6中set集合、map集合如何使用”文章能帮助大家解决疑惑,下...
      99+
      2023-07-04
    • ES6中如何使用Map与Set集合
      本篇内容主要讲解“ES6中如何使用Map与Set集合”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“ES6中如何使用Map与Set集合”吧!集合的概念以及和数组的区别其实数组也是集合, 只不过数组...
      99+
      2023-06-17
    • 如何解读Java三大集合中map list set的用法
      如何解读Java三大集合中map list set的用法,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。Map接口和Collection接口是所有集合框架的父接口...
      99+
      2023-06-25
    • 一文掌握Java中List和Set接口的基本使用
      目录集合的概念List接口泛型Set接口List和set的区别基本概念的区别使用场景集合的概念 是一个工具类,作用为存储多个数据,通常用于替代数组 集合的特点 只能存放Object对...
      99+
      2022-11-13
    • Spring框架中Java函数的秘密:如何掌握它们?
      Spring框架是一个非常流行的Java应用程序开发框架,它提供了丰富的功能和工具,可以帮助开发人员更快地构建高质量的应用程序。其中,Java函数是Spring框架中最重要的部分之一,因为它们允许开发人员创建可重用的代码块,以便在整个应用...
      99+
      2023-09-16
      函数 spring 框架
    • Java中Set集合的使用和底层原理解析
      目录Set系列集合介绍Set集合概述HashSet无序原理Set集合对象去重LinkedHashSetTreeSet排序规则Set系列集合介绍 Set集合概述 Set系列集合特点: ...
      99+
      2022-12-10
      Java中Set集合的使用 Java中Set集合
    • Windows系统下,Java框架和关键字的使用技巧,你掌握了吗?
      Java作为一门广泛应用于企业级开发的编程语言,拥有庞大的开发者群体和丰富的生态系统,其中框架和关键字是Java开发中不可或缺的一部分。本文将介绍在Windows系统下,Java框架和关键字的使用技巧,帮助读者更好地掌握Java开发。 一...
      99+
      2023-06-25
      框架 windows 关键字
    • JAVA集合框架中的常用集合及其特点和实现原理简介
      本篇内容介绍了“JAVA集合框架中的常用集合及其特点和实现原理简介”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Java提供的众多集合类由两...
      99+
      2023-06-19
    • Java中Map集合体系的基本使用和常用API是什么
      这篇文章主要讲解了“Java中Map集合体系的基本使用和常用API是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java中Map集合体系的基本使用和常用API是什么”吧!Map集合概述...
      99+
      2023-07-05
    • PHP开发必须掌握的技能?如何在Linux系统中使用Laravel框架?
      PHP是一种广泛应用于Web开发领域的编程语言。在Web应用程序的开发过程中,开发者需要掌握一些基本技能,包括基本语法、变量、数据类型、运算符、流程控制、函数、数组等。但是,对于PHP开发者而言,仅掌握这些基本技能还不足以开发出高质量的W...
      99+
      2023-08-15
      linux 框架 laravel
    • 您是否知道Java和Unix如何结合使用框架和索引?
      Java和Unix结合使用框架和索引是一种非常常见的解决方案,这种方案在大数据处理方面有着广泛的应用。在本文中,我们将介绍Java和Unix如何结合使用框架和索引,并提供演示代码。 Java和Unix结合使用框架 Java是一种面向对象...
      99+
      2023-06-16
      unix 框架 索引
    • Java和NumPy框架API:如何在你的项目中集成它们?
      Java和NumPy框架都是广泛使用的编程工具,它们都有着各自独特的优势和应用场景。Java是一种非常流行的编程语言,广泛应用于企业级应用程序的开发。而NumPy框架是Python语言中最广泛使用的数值计算库,它提供了许多数学函数和数组操...
      99+
      2023-07-26
      numpy 框架 api
    • 如何在Bash和Java开发中使用Laravel框架?
      Laravel框架是一个流行的PHP框架,它提供了许多有用的功能和工具,使得开发人员可以更加高效地进行Web应用程序的开发。虽然Laravel框架主要用于PHP开发,但是它也可以在Bash和Java开发中使用。在本文中,我们将探讨如何在Ba...
      99+
      2023-06-03
      bash 开发技术 laravel
    • 如何在 Java 中使用最新的 NPM 框架?
      随着前端技术的不断发展,NPM(Node Package Manager)已经成为了前端开发不可或缺的工具。NPM 是一个包管理器,可以通过它方便地安装和管理 JavaScript 的包。Java 开发者也可以通过 NPM 来管理自己的包...
      99+
      2023-07-08
      容器 npm 框架
    • Python中的集合和字典如何使用
      这篇文章主要讲解了“Python中的集合和字典如何使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Python中的集合和字典如何使用”吧!1 集合集合可以使用大括号({})或者set()函...
      99+
      2023-06-29
    • Spring框架中的Java函数:如何使用它们?
      Spring框架是一个很流行的Java框架,它提供了许多有用的功能和组件,以帮助开发人员构建高质量的Java应用程序。其中一个重要的组件是Java函数,它们可以帮助我们轻松地编写复杂的业务逻辑。在本文中,我们将深入探讨Spring框架中的J...
      99+
      2023-09-16
      函数 spring 框架
    • 如何在 Java 框架中优化数据类型和接口的使用?
      Java 框架是目前应用最广泛的编程语言之一。在开发 Java 应用程序时,优化数据类型和接口的使用可以提高程序的性能和可读性。本文将介绍一些在 Java 框架中优化数据类型和接口的使用的技巧,同时提供一些演示代码。 1. 使用适当的数据类...
      99+
      2023-10-13
      框架 数据类型 接口
    • Python和Unix的完美结合:如何使用框架索引您的数据
      Python和Unix都是非常强大的工具,它们各自都有着独特的优势。Python是一种高级编程语言,具有易读易写的特点,可以让用户快速地编写脚本和应用程序。而Unix则是一种操作系统,具有强大的命令行工具和管道机制,可以让用户快速地处理文...
      99+
      2023-11-05
      索引 unix 框架
    软考高级职称资格查询
    编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
    • 官方手机版

    • 微信公众号

    • 商务合作