【DS】 HashMap 详解

在这里插入图片描述

当年面试问的最多的那批 Java 集合框架现在问的还多么? 编程十年(手动狗头) , 我把这些集合框架做成盲盒, 无论你多难, 我都想去了解你, 今天来看-----pia~,HashMap ,基于哈希表实现的键值对存储容器。它允许快速的插入、查找和删除操作,通过哈希函数将键映射到唯一的索引。提供常数时间的基本操作,但不保证元素顺序。允许null键和值,但非线程安全。常用于快速数据检索、缓存实现等场景。

在了解 HashMap 之前可以先了解一下哈希表, HashMap 的底层就是基于 哈希表的。 哈希表

一. Map

1. 概念

Map 和 set 是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。

以前常见的搜索方式有:

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

上述查找比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:

  • 根据姓名查询考试成绩
  • 通讯录,即根据姓名查询联系方式
  • 不重复集合,即需要先搜索关键字是否已经在集合中

可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,本文介绍的 Map 是一种适合动态查找的集合容器。

2. 模型

一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为 Key-value 的键值对,所以模型会有两种:

  1. 纯 key 模型,比如:
    有一个英文词典,快速查找一个单词是否在词典中快速查找某个名字在不在通讯录中
  2. Key-Value 模型,比如:
    统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数 <单词,单词出现的次数>
    梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号

3. Map 的使用

Map 的官方文档

在这里插入图片描述

4. 关于Map的说明

Map是一个接口类,该类没有继承自 Collection,从而想要遍历 Map 中的元素, 只能先把元素重新放到 Set 中, 然后再遍历, 该类中存储的是<K,V> 结构的键值对,并且K一定是唯一的,不能重复。

5. 关于Map.Entry<K, V>的说明

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

在这里插入图片描述

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

在这里插入图片描述

6. Map 的常用方法说明

在这里插入图片描述

在这里插入图片描述

注意:

  1. Map 是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类 TreeMap 或者 HashMap
  2. Map 中存放键值对的Key是唯一的,value 是可以重复的
  3. 在 TreeMap 中插入键值对时,key 不能为空,否则就会抛 NullPointerException 异常,value 可以为空。key 不能为空是因为 需要用 key 进行比较, 空 无法比较, 但是HashMap 的 key 和 value 都可以为空, HashMap 中的 key 不涉及比较。
  4. Map 中的 Key 可以全部分离出来,存储到 Set 中来进行访问(因为 Key 不能重复)。
  5. Map 中的 value 可以全部分离出来,存储在 Collection 的任何一个子集合中( value 可能有重复)。
  6. Map 中键值对的 Key 不能直接修改,value 可以修改,如果要修改 key,只能先将该 key删除掉,然后再来进行重新插入。
  7. TreeMap 和 HashMap的区别

在这里插入图片描述

二. HashMap

1. HashMap 的部分源码

属性:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

构造函数:

    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;
        this.threshold = tableSizeFor(initialCapacity);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

由构造函数可以看出, 构造时并没有开辟空间
在这里插入图片描述
从这段代码可以看出, 如果初始化的值不是 2 的次幂的话, 修正为 2 的次幂

得到哈希值的方法:

在这里插入图片描述
从这段代码可以看出, 对 key == null 进行了单独处理, 所以 HashMap 中的 key 可以为 null

put 方法:

在这里插入图片描述

get 方法:
在这里插入图片描述

2. 对于源码的一些问题讲解

下面将对一些问题进行讲解:

  1. 如果 new HashMap(19) 那么 开辟的空间大小是多大 ?
    是32, >= 19 的最接近的 2 的 次幂 ?
  2. 为什么容量一定要是 2 的次幂 ?
    解释这个问题之前需要先解决另一个问题,
  • 元素放置的位置是怎么得到的 ?
    (n-1) & hash,得到位置, n 是数组的长度, hash 就是得到的 hash 值, 为什么要这么算 ?
    其实 这个函数等价于 hash % n, 而 & 的效率比 % 更高, 所以 使用 (n-1) & hash
    那么使用 & 了, 就可能带来一个问题, 我们都知道 & 的性质, 如果 双方出现一个 0, 的话, 该位置就是 0

那么如果 容量是 2 的次幂的话, (n-1) 一定是奇数, 这样最后一位一定是 1, 从而 (n-1) & hash 的结果才可能出现奇数或偶数, 但是如果 n 是 奇数的话, (n-1) 是偶数, 最后一位 是 0, 那么 (n-1) & hash 的结果一定是 偶数, 所以元素都放到了 偶数位置, 极大的提高了 哈希碰撞的概率, 所以, 容量一定是 2 的 次幂, 这样元素存放的位置更随机, 降低了哈希碰撞的概率。

  1. 得到 hash 值的方法为什么是 hash ^ (hash >>> 16)
    为了混合高位和低位, 高位还是原本的高位, 低位变成了 高位与低位 异或的结果, 增加了低位的随机性同时也保持了高位的特性, 从而 使得 hash 值更加随机, 进一步降低哈希冲突的概率

  2. HashMap 什么时候开辟数组的空间
    第一次 put 元素的时候, 且默认大小为 16

  3. HashMap 什么时候扩容 ?
    超过负载因子时, 扩容是 2 倍扩容, 因为 一直要保持 是 2 的次幂

  4. 当两个对象 hash 值相同时会发生什么?
    哈希冲突, 两个对象存放在一条链上或者一颗红黑树上

  5. 若两个对象的 hash 值相同, 如何获取等值对象。(equals 的对象)
    遍历与 hash 值相等时相连的链表或者 红黑树, 并用 equals 进行比较, 直到 equals 结果为 true 或者 没找到

  6. 重新调整 HashMap 的大小时, 会发生什么 ?
    两倍扩容, 然后将之前 数组里面的内容重新 哈希到 新的数组空间中

  7. HashMap
    最大容量: 1 << 30
    默认容量: 16
    默认负载因子: 0.75
    树化条件: 散列表(数组) 长度 >= 64, 链表长度 >= 8
    链表还原阈值: 即红黑树变为 链表的阈值, 在扩容时, 树中节点总数 <= 6 时, 红黑树转为 链表

  8. 由上面源码可知, 自定义的类型一定要重写 hashCode() 和 equals() 方法, 才能往 HashMap 里面存放

  9. HashMap 的负载因子为 1 时, 再往里面放元素, 不一定冲突, 注意 负载因子的定义是: (已放的元素个数) / 散列表的长度。

  10. 由于负载因子的存在, 以及当链表的长度到达一定阈值时转为红黑树, 所以链表的长度不会很长, 所以我们认为 HashMap 的插入、删除、查找元素的时间复杂度为 O(1)

  11. 因为存放的位置是由 hash 函数得出来的, 所以存放的顺序是随机的, 不会按照插入时的顺序存放,下面代码中的结果也可以体现。

3. HashMap 的使用

HashMap 的常用方法就是实现从 Map 接口的几个常用方法

在这里插入图片描述

    public static void main(String[] args) {
        Map<String, Integer> hashMap = new HashMap<>();

        // 放置元素
        hashMap.put("hello", 3);
        hashMap.put("abc", 3);
        hashMap.put("the", 3);
        System.out.println(hashMap);

        // 获取元素
        Integer val1 = hashMap.get("hello");
        System.out.println("hello: " + val1);
        Integer val2 = hashMap.getOrDefault("hello2", 100);
        System.out.println("hello2: " + val2);

        // 判断 key 值是否存在
        System.out.println(hashMap.containsKey("hello"));
        System.out.println(hashMap.containsKey("hello2"));

        // 获取所有 key
        Set<String> keySet = hashMap.keySet();
        System.out.println(keySet);

        // 获取 Map.Entry<>
        Set<Map.Entry<String, Integer>> set = hashMap.entrySet();
        for (Map.Entry<String, Integer> entry: set) {
            System.out.print("key: " + entry.getKey() + " " + "val:" + entry.getValue() + " ");
        }
    }

三. TreeMap

TreeMap 与 HashMap 使用都差不多, 只不过 TreeMap 底层是一颗红黑树(一种特殊的二叉搜索树), 是 关于 key 有序的, 所以存放到 TreeMap 里面的 key 必须是能够比较的。所以如果 key 是自定义类型的话, 需要实现 Comparable 接口, 或者 实现一个比较器。(Comparable、比较器)

    public static void main(String[] args) {
        Map<String, Integer> treeMap = new TreeMap<>();

        // 放置元素
        treeMap.put("hello", 3);
        treeMap.put("abc", 3);
        treeMap.put("the", 3);
        System.out.println(treeMap);

        // 获取元素
        Integer val1 = treeMap.get("hello");
        System.out.println("hello: " + val1);
        Integer val2 = treeMap.getOrDefault("hello2", 100);
        System.out.println("hello2: " + val2);

        // 判断 key 值是否存在
        System.out.println(treeMap.containsKey("hello"));
        System.out.println(treeMap.containsKey("hello2"));

        // 获取所有 key
        Set<String> keySet = treeMap.keySet();
        System.out.println(keySet);

        // 获取 Map.Entry<>
        Set<Map.Entry<String, Integer>> set = treeMap.entrySet();
        for (Map.Entry<String, Integer> entry: set) {
            System.out.print("key: " + entry.getKey() + " " + "val:" + entry.getValue() + " ");
        }
    }

同样的代码, 只是一个是用 HashMap 存储, 一个用 TreeMap 存储

在这里插入图片描述

在这里插入图片描述

上图只是简单的模拟了一下怎么存储, 实际情况是很复杂的。
因为是二叉搜索树, 所以它的中序遍历就是有序的。

好啦,以上就是对 HashMap 的讲解, 希望能帮到你 !
评论区欢迎指正 !

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值