Java集合——Map

news2024/11/27 16:43:36

Map集合

在这里插入图片描述

Map用于保存具有映射关系的数据,以键值对的形式存储,支持通过key来访问value,因此key不能重复。

Map接口下主要有HashtableHashMapLinkedHashMapConcurrentHashMap 四个主要的实现类,实现的基本原理类似,主要详解HashMapConcurrentHashMap

1. HashMap

使用频率最高的键值存储集合,底层采用数组+链表+红黑树存储数据 (JDK1.8) 。访问速度快,线程不安全,最多允许一个key为null ,允许多个value为null

hashCode()对key求哈希值,然后根据哈希值数组长度-1与运算 得到数组下标位置,数组中存储的值存放的是key value组成的结点,如果两个两个key不同但是哈希值相同,就在对应数组位置创建一个链表,如果链表长度超过8,此时数组长度 >= 64 链表转为红黑树,否则扩容数组。(详见哈希冲突和哈希函数)

在JDK1.7中HashMap 底层采用数组+链表实现,且链表采用头插法,在多线程并发环境下rehash扩容容易造成死循环 (详见 Java HashMap的死循环)

如果创建是不指定大小 HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍,如果指定大小

HashMap 会将其扩充为 2 的幂次方大小(为了方便求数组下标)

指定容量和负载因子的构造函数(负载因子是 哈希表中键值对 / 数组容量 0.75是时间和空间的综合考量)

   // 默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

// 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

   //默认负载因子 0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 有参构造函数
public HashMap(int initialCapacity, float loadFactor) {
    // 初始化容量<0 报非法参数异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 大于最大容量就初始化为最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 负载因子<=0 或为NaN 报非法参数异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

tableSizeFor方法保证容量是 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;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

put过程

public V put(K key, V value) {

    return putVal(hash(key), key, value, false, true); 
}

// onlyIfAbsent参数如果是true,那么只有不存在该key时才会进行put操作,默认为false可以覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次put的时,通过resize()创建数组并初始化数组长度
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
     // 寻找数组下标,如果该位置没有值,直接初始化一个结点,将key value存入结点,并把结点放入数组
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 数组该位置已经存在数据
        Node<K,V> e; K k;
        // 首先,判断该位置第一个数据与待put的key是否equals相等,相等就取出这个结点
        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);
                    // TREEIFY_THRESHOLD为8,如果新增元素是第8个尝试转化红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                // 在链表中 找到了equals相等的key,取出这个结点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 结点不为null说明存在equals相等的key,需要进行value覆盖 并返回旧值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果新put的元素导致size超过了阈值,需要扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize() 方法用于初始化数组或数组扩容,如果是扩容,扩容后容量为原来的 2 倍

get()过程分析

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}


final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    
    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<K,V>)first).getTreeNode(hash, key);
            do {
                // 遍历链表查找key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 没有找到就返回null
    return null;
}

2. Hashtable

HashtableHashMap类似,底层采用数组+链表实现。不过Hashtable是线程安全的,通过加synchronized关键字保证,不允许key和value为null,在并发性上不如ConcurrentHashMap

为什么Hashtable效率低

因为synchronized 关键字对整个对象进行加锁,锁住了Hash表

3. ConcurrentHashMap

HashMap 类似 唯一区别在于ConcurrentHashMap 是线程安全的 key和value不能为null

JDK1.7,底层采用分段的数组+链表 ,使用分段锁保证线程安全。

ConcurrentHashMap在对象中保存了一个Segment数组,将整个Hash表划分为多个分段(默认16段);而每个Segment元素,即每个分段则类似于一个Hashtable。在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁而不影响其他Segment。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作Segment 通过继承 ReentrantLock 来进行加锁

JDK1.8,ConcurrentHashMap 底层采用数组+链表+红黑树,加锁采用CASsynchronized实现。锁粒度更细,synchronized 只锁当前链表红黑树的首节点,这样只要 hash值 不冲突,就不会发生并发,不会影响其他 Node结点 的读写,效率大幅提升。

put()过程

public V put(K key, V value) {
    return putVal(key, value, false);
}


final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key和value不能为null
    if (key == null || value == null) throw new NullPointerException();
    // 计算hash值
    int hash = spread(key.hashCode());
    // 记录链表的长度
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 数组为null或空 就初始化数组
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        
        // 找到hash值对应的数组下标,下标的第一个结点f
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 如果数组该位置为空,通过一次CAS操作将这个值放入 结束
          // 如果CAS失败,说明有并发,循环进行下一次CAS操作
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                  
        }
        // 需要扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        
        // f为该位置的头结点,且不为null
        else {
            V oldVal = null;
            // 获取头结点的监视器锁
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 头结点的hash值>=0 说明是链表
                    if (fh >= 0) {
                        //累计链表的长度
                        binCount = 1;
                        // 遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果有equals相等的key,判断是否进行值覆盖 随后break
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 链表中不存在该key,插入链表尾部
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 红黑树
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        // 往红黑树中插入新节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                // 判断是否将链表转化为红黑树,临界值为8
                // 如果数组长度小于64 选择扩容数组而不转红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

get()过程

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 计算哈希值
    int h = spread(key.hashCode());
    // 找数组对应位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 如果头结点的 hash值 < 0 说明 正在扩容/该位置为红黑树
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 遍历链表查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    // 不存在 返回null
    return null;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/92060.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【图像处理】Hough变换检测直线与圆的原理

霍夫变换的基本原理 霍夫变换(Hough Transform)可以理解为图像处理中的一种特征提取技术&#xff0c;通过投票算法检测具有特定形状的物体。霍夫变换运用两个坐标空间之间的变换将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间中的一个点形成峰值&#xff0c;从而…

菜菜学paddle第七篇:目标检测的基本概念

一、什么是目标检测&#xff1f; 在前面的几篇中&#xff0c;我们学习了使用卷积神经网络进行图像分类&#xff0c;比如手写数字识别是用来识别0~9这十个数字。与图像分类处理单个物体的识别不同&#xff0c;目标检测它识别的不仅是物体&#xff0c;还是多个物体&#xff0c;…

[附源码]Python计算机毕业设计高校教务管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

Spring之IOC

IoC&#xff08;Inverse of Control:控制反转&#xff09;是一种设计思想&#xff0c;就是 将原本在程序中手动创建对象的控制权&#xff0c;交由Spring框架来管理。 IoC 在其他语言中也有应用&#xff0c;并非 Spirng 特有。 IoC 容器是 Spring 用来实现 IoC 的载体&#xff0…

服务攻防-应用协议RsyncSSHRDPFTP漏洞批扫口令猜解

目录 测试思路 &#xff08;一&#xff09;口令猜解——SSH&RDP&FTP Demo: &#xff08;二&#xff09;配置不当-未授权访问-Rsync 文件备份 尝试读取src文件 &#xff08;三&#xff09;协议漏洞-应用软件-FTP&Proftpd 搭建 &#xff08;四&#xff09;协议漏…

音视频- iOS使用metal渲染图像(一)

概要 本文主要总结一下Metal的基本使用&#xff0c;用来渲染拍照的到的图像&#xff0c;其中涉及到的有UIKit中的MTKView&#xff0c;Metal中负责渲染的几个类&#xff0c;使用MSL&#xff08;Metal Shading Language&#xff09;编写着色器&#xff0c;最终将图片渲染出来…

Python 绘制极坐标图(玫瑰花图优化)

风速的玫瑰花图可见 Matlab 绘制风速、风向统计玫瑰花图 在今天的这边博文,选用Python工具,绘制玫瑰花图,并对图进行优化。 example 1 【不规则宽度】N = 20 theta = np.linspace(0.0,2*np.pi,N,endpoint=False)

Jmeter(十五):jmeter场景的运行方式(GUI运行和命令行运行)命令行相关参数

jmeter场景的运行方式(GUI运行和命令行运行) 运行方式&#xff1a; GUI运行&#xff1a;通过图形界面方式运行&#xff0c;该运行方式的可视化界面及监听器动态展示 结果都比较消耗负载机资源&#xff0c;建议大并发时不用&#xff0c;一般进行脚本调试&#xff1b; 命令行运…

【PTA-训练day17】L2-029 特立独行的幸福 + L1-071 前世档案

L2-029 特立独行的幸福 - 递归/模拟 判断素数 PTA | 程序设计类实验辅助教学平台 思路&#xff1a; 第一层循环是边界循环 for(int il;i<r;i)枚举每一个i 第二层循环是判断这个i是不是幸福数st数组 判定 i 这个数在迭代过程中是否出现循环 如果出现循环及时跳出比如&…

【TypeScript系列】【一篇就够】TypeScript知识点总结(二)

12 面向对象简介 简而言之&#xff0c;面向对象就是程序之中所有的操作都需要通过对象来完成。 举例来说&#xff0c; 操作浏览器要使用windows对象&#xff1b;操作网页要使用document对象&#xff1b;操作控制台要使用console对象。一切操作都要通过对象&#xff0c;也就是…

nacos--扩展--03--系统参数

nacos–扩展–03–系统参数 1、Nacos Server 配置参数位置&#xff1a;{nacos.home}/conf/application.properties里注意&#xff1a;如果参数名后标注了(-D)的&#xff0c;则表示是 JVM 的参数&#xff0c;需要在{nacos.home}/bin/startup.sh里进行相应的设置。 案例&#xf…

测试用例等级怎么划分?别再傻傻的一脸懵逼

我们都知道测试工程师最基本的能力便是编写测试用例&#xff0c;可是看似简单的用例&#xff0c;后面其实蕴含这个很多人忽略的细节&#xff0c;今天就来说测试里面所蕴含的很多细节。 很多时候不只是测试和测试用例息息相关&#xff0c;开发&#xff0c;产品也有的时候对于测试…

C/C++程序的断点调试 - CodeBlocks

本文以CodeBlocks为例&#xff0c;简述C/C程序断点调试的基本方法和过程。其它的IDE环境&#xff0c;大同小异。 本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载&#xff0c;但需要注明原作者"海洋饼干叔 叔"&#xff1b;本文不允许…

单链表翻转-链表篇

leetcode206单链表的翻转 题目&#xff1a; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出…

如何指定标签在页面中显示的位置

如何指定标签在页面中显示的位置 在HTML页面设计中常常需要调整标签&#xff08;元素&#xff09;的位置&#xff0c;那么&#xff0c;如何指定标签在页面中显示的位置呢&#xff1f; 使用标签的align属性指定标签在页面中显示的位置&#xff0c;如align"left|right|cen…

02加锁源码分析-ReentrantReadWriteLock原理-AQS-并发编程(Java)

文章目录3.1 加锁3.1.1 读锁加锁3.1.1.1 tryAcquireShared()3.1.1.2 readerShouldBlock()3.1.1.3 fullTryAcquireShared()3.1.1.4 doAcquireShared()3.1.2 写锁加锁3.1.2.1 tryAcquire()3.1.2.2 acquireQueued()3.2 加锁示意图3.2.1 先写锁在读锁3.2.2 先读锁在写锁后记3.1 加锁…

全网惟一面向软件测试人员的Python基础教程-Python数据类型中有那些故事呢?

全网惟一面向软件测试人员的Python基础教程 起点&#xff1a;《python软件测试实战宝典》介绍 第一章 为什么软件测试人员要学习Python 第二章 学Python之前要搞懂的道理 第三章 你知道Python代码是怎样运行的吗&#xff1f; 第四章 Python数据类型中有那些故事呢&#xff1f;…

PHP+Laravel框架RabbitMQ简单使用

RabbitMQ安装教程请转到&#xff1a;RabbitMQ安装教程&#xff08;超详细&#xff09; 1、创建生产者 在app/Http/Controllers里创建一个php控制器文件&#xff0c; namespace App\Http\Controllers;use App\Http\Controllers\Controller; //引入amqp扩展 use PhpAmqpLib\Co…

铁路轨道交通智慧管理系统

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&…

sklearn基础篇(九)-- 主成分分析(PCA)

1 引言 降维是对数据高维度特征的一种预处理方法。降维是将高维度的数据保留下最重要的一些特征&#xff0c;去除噪声和不重要的特征&#xff0c;从而实现提升数据处理速度的目的。在实际的生产和应用中&#xff0c;降维在一定的信息损失范围内&#xff0c;可以为我们节省大量的…