Java并发 - J.U.C并发容器类Map - HashMap简单分析

一个简单的HashMap的使用例子:

package hashmap;

import java.util.HashMap;

/**
 * @Author: Neco
 * @Description: 一个简单的HashMap的使用
 * @Date: create in 2022/6/20 22:51
 */
public class Test {

    public static void main(String[] args) {
        HashMap map = new HashMap<>();
        map.put("necde", "Neco Deng");
        Object o = map.get("necde");
        System.out.println(o);
    }
}

由以上的代码,可以知道HashMap是本质就是一种数据存储的结构类型,本质是一个散列表,它存储的内容是键值对(key-value)映射。

在说明Map的结构之前,我们先了解一下另外的两个数据结构类型:

  • 数组:

【内容来自百度百科】 数组(Array)是有序的元素序列。 [1] 若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。 [1] 这些有序排列的同类数据元素的集合称为数组。
数组是用于储存多个相同类型数据的集合。

用我们的话来说,就是很多个相同类型的数据,按照顺序排列,每个数据都有它自身对应的号码,对应的在这个队伍中的第几位,但是序号是从0开始增加的。

对于数组来说,它的优点是取数据很快,因为只要你知道它的下标(即对应的号码)就可以快速定位到这个数据并取出,但是如果要插入数据,就好比某个人要插队的话,那么他就必须跟后面的人都打一声招呼,让后边的人(数据)都往后挪一下,这样就比较费劲。所以数组的缺点是插入很麻烦。总结来说就是 获取元素特别方便,插入数据非常麻烦

  • 链表

【内容来自百度百科】链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表可以在多种编程语言中实现。像Lisp和Scheme这样的语言的内建数据类型中就包含了链表的存取和操作。程序语言或面向对象语言,如C,C++和Java依靠易变工具来生成链表。

用通俗的话来说,链表就多个同样类型的数据,除了自身携带的数据信息外,它还携带了它的下一个数据的地址信息,这是单向链表,如果它不止知道它的下一个数据的地址信息,还知道它的上一个数据的地址信息,那么这就是双向链表,好比当前数据可以根据下(上)一个数据的地址信息找到这个上(下)一个数据。

对于链表,优点就是插入元素非常方便,只要修改插入位置的前后两个元素的指向即可,但是对于获取元素,则非常麻烦,很有可能需要遍历整个链表,所以缺点是获取元素不方便

链表

HashMap

数组查询快但是插入删除慢;链表插入删除快但是查询慢的问题就引申出了HashMap,HashMap结合了数组和链表的优点(JDK1.8之后还加入了红黑二叉树等相关的内容),所以大致可以得出 HashMap = 数组 + 链表

HashMap的简单示例
  • HashMap 中的put方法 (JDK1.8)
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node 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)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);
                        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;
    }

根据以上内容可以推断出,put方法执行的顺序大致如下:

  1. 通过hash(key)得出对应的hash值,然后再通过一定的算法得出对应的数组的下标
  2. 然后将数据挂在这个数组下标对应的数据下边
  3. 如果发现这个数组下标对应的数据已经挂有对的数据(链表元素)了,则将数据挂载在这个链表的最后边的(JDK1.7)
  4. 为了避免一个hashcode值命中多次,导致链表挂载的数据太多导致性能的问题,所以要引入二叉树(红黑二叉树)

HashMap的一些相关知识点

  • 默认初识长度 16 (二进制 10000)
    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  • 填充因子默认是 0.75
    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

HashMap的总结:

  1. HashMap是数组+链表构成的,JDK1.8之后,加入了红黑树.
  2. HashMap默认数组初始化大小为16,如果瞎设置数字,它会自动调整成2的倍数.
  3. HashMap链表在长度为8之后,会自动转换成红黑树,数组扩容之后,会打散红黑树,重新设置.
  4. HashMap扩容变化因子是0.75,也就是数组的3/4被占用之后,开始扩容。
  5. 在第一次调用PUT方法之前,HashMap是没有数组也没有链表的,在每次put元素之后,开始检查(生成)数组和链表.

更多延伸的内容,请查看

Java - HashMap的扩展知识


如果觉得有收获就点个赞吧,更多知识,请点击关注查看我的主页信息哦~

你可能感兴趣的:(Java并发 - J.U.C并发容器类Map - HashMap简单分析)