首页 > 编程语言 >[Java基础]HashMap

[Java基础]HashMap

时间:2024-07-09 19:09:48浏览次数:7  
标签:key hash HashMap 基础 value 线程 哈希 Java

HashMap

HashMap的数据结构

HashMap是:数组+链表/红黑树(JDK1.8增加了红黑树部分)

数据底层具体存储的是什么?

Node<k,v>

这样的存储方式有什么优点呢?

数据结构

// 默认初始容量(数组默认大小):16,2的整数次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
 
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
 
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
装载因子用来衡量HashMap满的程度,表示当map集合中存储的数据达到当前数组大小的75%则需要进行扩容
 
// 链表转红黑树边界
static final int TREEIFY_THRESHOLD = 8;
 
// 红黑树转离链表边界
static final int UNTREEIFY_THRESHOLD = 6;
 
// 哈希桶数组
transient Node<K,V>[] table;
 
// 实际存储的元素个数
transient int size;
 
// 当map里面的数据大于这个threshold就会进行扩容
// 阈值 = table.length * loadFactor
int threshold   
1. `DEFAULT_INITIAL_CAPACITY`: 默认初始容量,即哈希表的数组默认大小,被设置为 `1 << 4`,也就是 2 的 4 次方,即 16。这是因为 HashMap 的数组大小通常是 2 的整数次方,这样有助于在计算哈希索引时更高效。

2. `MAXIMUM_CAPACITY`: 最大容量,被设置为 `1 << 30`,即 2 的 30 次方。这是 HashMap 可以容纳的最大元素数量。

3. `DEFAULT_LOAD_FACTOR`: 默认负载因子,被设置为 0.75f。负载因子是一个衡量 HashMap 满的程度的参数,当存储的数据达到数组大小的 75% 时,会触发扩容操作。

4. `TREEIFY_THRESHOLD`: 链表转红黑树的阈值,被设置为 8。当哈希桶中的链表长度达到 8 时,链表会被转换成红黑树,以提高查询效率。

5. `UNTREEIFY_THRESHOLD`: 红黑树转链表的阈值,被设置为 6。当红黑树中的元素减少到 6 时,红黑树会被转换回链表。

6. `table`: 哈希桶数组,用于存储键值对。`transient` 关键字表示该字段不会被默认的序列化机制序列化。

7. `size`: 实际存储的元素个数,即 HashMap 中键值对的数量。

8. `threshold`: 扩容阈值,计算方式为 `table.length * loadFactor`。当实际存储的元素个数大于等于这个阈值时,触发扩容操作。

Node结构

从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。

static class Node<K,V> implements Map.Entry<K,V> {

    final int hash;//用来定位数组索引位置
    final K key;
          V value;
    Node<K,V> next;//链表的下一个Node节点
 
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
 
    
    public final K getKey() { return key; }
    public final V getValue() { return value; }
    public final String toString() { return key + "=" + value; }
 
 
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
 
 
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
 
 
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
            }
        return false;
    }
}

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。

HashMap的数据存储

  • 1.哈希表来存储

    HashMap采用哈希表来存储数据。

    哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构,只要输入待查找的值即key,即可查找到其对应的值。

    哈希表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。

  • 2.哈希函数

    哈希表中元素存储地址是由哈希函数确定的,将数据元素的关键字Key作为自变量,通过一定的函数关系(称为哈希函数),计算出的值,即为该元素的存储地址。
    表示为:Addr = H(key),如下图所示:

    哈希表中哈希函数的设计是相当重要的,这也是建哈希表过程中的关键问题。

  • 3.核心问题
    建立一个哈希表之前需要解决两个主要问题:

    1)构造一个合适的哈希函数,均匀性 H(key)的值均匀分布在哈希表中

    2)冲突的处理

    冲突:在哈希表中,不同的关键字值对应到同一个存储位置的现象。

  • 4.哈希冲突:

    • 链式哈希表
      哈希表为解决冲突,可以采用地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。

      链地址法,简单来说,就是数组加链表的结合,如下图所示:

      HashMap的哈希函数

      /**
      * 重新计算哈希值
      */
      static final int hash(Object key) {
      
          int h;    
          // h = key.hashCode() 为第一步 取hashCode值
          // h ^ (h >>> 16) 为第二步 高位参与运算
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
      

      //计算数组槽位
      (n - 1) & hash
      n-1的二进制实际上是00000001111的形式,
      与运算的结果是保留hash的后几位如果n=16就是保留后四位,散列到0-15

      对key进行了hashCode运算,得到一个32位的int值h,然后用h 异或 h>>>16位。
      在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)。

      这样做的好处是,可以将hashcode高位和低位的值进行混合做异或运算,而且混合后,低位的信息中加入了高位的信息,这样高位的信息被变相的保留了下来。

      等于说计算下标时把hash的高16位也参与进来了,掺杂的元素多了,那么生成的hash值的随机性会增大,减少了hash碰撞。

      备注:

      ^异或:不同为1,相同为0
      >>>:无符号右移:右边补0
      &运算:两位同时为“1”,结果才为“1,否则为0
      h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方。

为什么槽位数必须使用2^n?

1.为了让哈希后的结果更加均匀

假如槽位数不是16,而是17,则槽位计算公式变成:(17 – 1) & hash
从上文可以看出,计算结果将会大大趋同,hashcode参加&运算后被更多位的0屏蔽,计算结果只剩下两种0和16,这对于hashmap来说是一种灾难。

2.等价于length取模

当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

位运算的运算效率高于算术运算,原因是算术运算还是会被转化为位运算。

最终目的还是为了让哈希后的结果更均匀的分布,减少哈希碰撞,提升hashmap的运行效率。

分析HashMap的put方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 boolean evict) 
 {

    Node<K,V>[] tab; 
    Node<K,V> p; 
    int n, i;

    // 当前对象的数组是null 或者数组长度时0时,则需要初始化数组
    if ((tab = table) == null || (n = tab.length) == 0) 
    {
        n = (tab = resize()).length;
    }

    // 使用hash与数组长度减一的值进行异或得到分散的数组下标,预示着按照计算现在的
    // key会存放到这个位置上,如果这个位置上没有值,那么直接新建k-v节点存放
    // 其中长度n是一个2的幂次数
    if ((p = tab[i = (n - 1) & hash]) == null) {
        tab[i] = newNode(hash, key, value, null);
    }

    // 如果走到else这一步,说明key索引到的数组位置上已经存在内容,即出现了碰撞
    // 这个时候需要更为复杂处理碰撞的方式来处理,如链表和树
    else 
    {
        Node<K,V> e; K k;

        //节点key存在,直接覆盖value
        if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k)))) 
        {
            e = p;
        }
        // 判断该链为红黑树
        else if (p instanceof TreeNode) 
        {
            // 其中this表示当前HashMap, tab为map中的数组
            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
                    // 从0开始的,如果到了7则说明满8了,这个时候就需要转
                    // 重新确定是否是扩容还是转用红黑树了
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                    break;
                }
                // 找到了碰撞节点中,key完全相等的节点,则用新节点替换老节点
                if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
            
        // 此时的e是保存的被碰撞的那个节点,即老节点
        if (e != null) 
        { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent是方法的调用参数,表示是否替换已存在的值,
            // 在默认的put方法中这个值是false,所以这里会用新值替换旧值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // Callbacks to allow LinkedHashMap post-actions
                afterNodeAccess(e);
            return oldValue;
        }
    }
    // map变更性操作计数器
    // 比如map结构化的变更像内容增减或者rehash,这将直接导致外部map的并发
    // 迭代引起fail-fast问题,该值就是比较的基础
    ++modCount;
 
    // size即map中包括k-v数量的多少
    // 超过最大容量 就扩容
    if (++size > threshold)
        resize();
    // Callbacks to allow LinkedHashMap post-actions
    afterNodeInsertion(evict);
    return null;
}

HashMap的put方法执行过程整体如下:

  • 判断键值对数组table[]是否为空或为null,否则执行resize()进行扩容;

  • 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加

  • 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value

  • 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对

  • 遍历table[i],判断链表长度是否大于等于8,大于等于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

  • 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

  • 先判断桶数组是不是空,

    • 如果是,先扩容
    • 如果不是空,
      • 判断hash位置上有没有元素
        • 如果没有,直接插入
        • 如果有,判断该位上的元素是不是和即将插入的元素有相同的key
          • 如果key相同,直接覆盖value
          • 如果key不同,说明拉了链,或者拉了树
            • 如果拉了树,对树进行插入操作
            • 如果拉了链,遍历这个链,查找链的末尾,或者链上有相同的key,覆盖value,
              • 如果插入后链的长度等于8,转为红黑树。

HashMap总结

  • HashMap底层结构?
    基于Map接口的实现,数组+链表的结构,
    JDK 1.8后加入了红黑树,链表长度>=8(链表序号到达7)变红黑树,<6变链表

  • 两个对象的hashcode相同会发生什么?
    Hash冲突,HashMap通过链表来解决hash冲突

  • HashMap 中 equals() 和 hashCode() 有什么作用?
    HashMap 的添加、获取时需要通过 key 的 hashCode() 进行 hash(),然后计算下标 ( n-1 & hash),从而获得要找的同的位置。
    当发生冲突(碰撞)时,利用 key.equals() 方法去链表或树中去查找对应的节点

  • HashMap 何时扩容?
    put的元素达到容量乘负载因子的时候,默认16*0.75

  • hash 的实现
    hash = key.hashCode() ^ (key.hashCode() >>> 16), hashCode 进行无符号右移 16 位,然后进行按位异或,得到这个键的哈希值,由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或

  • HashMap线程安全吗?
    HashMap读写效率较高,但是因为其是非同步的,即读写等操作都是没有锁保护的,所以在多线程场景下是不安全的,容易出现数据不一致的问题,在单线程场景下非常推荐使用

    HashMap 在多线程环境下不是线程安全的。这是因为 HashMap 的实现是基于哈希表的,而哈希表的操作涉及到多个步骤,包括计算哈希码、定位桶位置、插入或检索元素等。在多线程环境下,多个线程同时对 HashMap 进行修改操作可能导致数据不一致或者丢失。

    以下是一些可能导致线程不安全的情况:

    • 竞态条件(Race Condition): 多个线程同时尝试插入或删除元素时,可能导致竞态条件。两个线程可能同时检测到某个位置为空,然后都尝试插入元素,导致其中一个线程的操作被覆盖。

    • 扩容操作: 当 HashMap 需要扩容时,会创建一个新的数组并将旧的元素重新分配到新数组中。在这个过程中,如果有其他线程同时对 HashMap 进行修改,可能会导致元素在扩容过程中丢失或者被重复添加。

    为了在多线程环境下保证线程安全,可以使用 ConcurrentHashMap 类,它提供了一些并发安全的操作。ConcurrentHashMap 使用分段锁的机制,将哈希表分成多个段,每个段上都有一个独立的锁,从而降低了锁的粒度,提高了并发性能。这样,不同的线程可以同时修改不同的段,避免了整个数据结构的锁竞争。

    总的来说,如果需要在多线程环境中使用哈希表,推荐使用 ConcurrentHashMap 而不是 HashMap,以确保线程安全性。

  • 如何做到让HashMap线程安全?
    在Java中,HashMap本身不是线程安全的,但可以通过以下几种方式来实现线程安全的HashMap:

    使用Collections.synchronizedMap方法:

    Map<K, V> synchronizedMap = Collections.synchronizedMap(new HashMap<K, V>());

    这将返回一个线程安全的Map,它在每个方法上都使用同步机制来确保线程安全。但请注意,虽然这确保了每个方法的原子性,但在多个操作之间,仍然可能需要额外的同步。

    使用ConcurrentHashMap: ConcurrentHashMap是Java提供的线程安全的Map实现。它使用分段锁机制,每个段相当于一个小的HashMap,不同的段之间互不影响,这样可以提高并发性能。

    Map<K, V> concurrentMap = new ConcurrentHashMap<K, V>();

    使用Collections.synchronizedMap包装HashMap的迭代器: 如果你使用Collections.synchronizedMap来创建线程安全的HashMap,当你迭代Map时,仍然需要手动同步。你可以通过在迭代器上使用synchronized块来实现:

Map<K, V> synchronizedMap = Collections.synchronizedMap(new HashMap<K, V>());
Set keySet = synchronizedMap.keySet();
synchronized (keySet) {
Iterator iterator = keySet.iterator();
while (iterator.hasNext()) {
K key = iterator.next();
// 在此处执行操作
}
}
如果需要线程安全的HashMap,推荐使用ConcurrentHashMap,因为它在并发场景下性能更好。根据具体的需求,选择适合的方法来保证线程安全。

  • ConcurrentHashMap怎么保证线程安全的?
    ConcurrentHashMap是Java集合框架中的线程安全的Map实现。它采用了一些策略来确保在多线程环境中的安全性:

    • 分段锁(Segmentation): ConcurrentHashMap将整个数据结构分割成多个独立的段(segments),每个段独立地管理一部分数据。每个段都类似于一个小的HashMap,有自己的锁。这样,不同段的数据可以在不同的锁上进行操作,提高了并发度。当一个线程在一个段上进行操作时,其他线程可以同时在其他段上进行操作,减小了竞争范围。

    • 精细化的锁策略: 在ConcurrentHashMap中,只有在读写冲突的时候才会使用锁,而且只锁定与冲突相关的段,而不是整个Map。这种细粒度的锁策略减小了锁的争用,提高了并发性能。

    • 读操作的无锁支持: ConcurrentHashMap对于读操作提供了无锁支持,允许多个线程同时进行读取操作,不会阻塞。只有在写操作发生时才需要加锁,确保写操作的原子性和可见性。

CAS(Compare and Swap)操作: ConcurrentHashMap使用CAS操作来确保对数据的原子更新。CAS是一种无锁算法,它比传统的锁机制更轻量级。通过CAS,ConcurrentHashMap可以在不加锁的情况下完成一些简单的操作。

适应性自动调整: ConcurrentHashMap在运行时会根据负载因子、并发度等参数进行自动调整。这使得它在不同的负载和并发情况下都能够保持高效。

ConcurrentHashMap通过使用分段锁、细粒度的锁策略、无锁的读操作和CAS操作等技术,以及适应性自动调整,来保证在多线程环境中的高并发性能和线程安全。这些特性使得ConcurrentHashMap成为处理高并发情况下Map操作的理想选择。

HashTable

线程安全,但是效率太低了,synchronized修饰方法,只允许一个线程访问

标签:key,hash,HashMap,基础,value,线程,哈希,Java
From: https://www.cnblogs.com/DCFV/p/18292583

相关文章

  • java day1
    简单了解了java,他最大的特点就是,稳定,安全,可以解决高并发的访问,最大的优势.java分es,ee,me.java的强势之处在于javaee,我是从javase开始学的.2.安装JDk,我装的是JDK17,据说当前市场大多数用的都是JDK8,17在未来可能会占据更大市场.17也是长期支持版.安装步骤(1)访问甲骨文......
  • [Java并发]Synchronized
    publicclassAtomicTest01{publicstaticinti=0;publicstaticvoidmain(String[]args){Runnabletask=newRunnable(){@Overridepublicvoidrun(){synchronized(this){tr......
  • Gradle基础:从入门到掌握
    人不走空                                          ......
  • Java爬虫翻页
    编写一个Java爬虫以进行翻页通常涉及到使用HTTP客户端(如ApacheHttpClient或OkHttp)来发送请求,解析HTML页面(如使用Jsoup库),以及处理分页逻辑(如通过URL参数或页面内的链接进行翻页)。1.使用Jsoup和ApacheHttpClient的Java爬虫示例以下是一个使用Jsoup和ApacheHttpClient的Java爬......
  • 对零基础想转行网络安全同学的一点建议
    最近有同学在后台留言,0基础怎么学网络安全?0基础可以转行做网络安全吗?以前也碰到过类似的问题,想了想,今天简单写一下。我的回答是先了解,再入行。具体怎么做呢?首先,你要确定学习方向。网络安全是一个很大的概念,包含的东西也很多,比如web安全,系统安全,二进制安全,无线安全、数......
  • 网络安全工程师需要学什么?零基础怎么从入门到精通,看这一篇就够了
    前言我发现关于网络安全的学习路线网上有非常多看似高大上却无任何参考意义的回答。大多数的路线都是给了一个大概的框架,告诉你那些东西要考,以及建议了一个学习顺序。但是这对于小白来说是远远不够的,有的可能还会有误导性!比如说很多的学习路线会说要从语言开始学起,于是很......
  • 【2024最新】零基础如何学习挖漏洞?看这篇就够了(超详细)
    文章目录前言什么是漏洞挖掘学习漏洞挖掘的正确顺序漏洞挖掘需要具备的知识漏洞挖掘需要做什么有关漏洞挖掘的其他想法漏洞的复杂性团队工作写在最后==如何入门学习网络安全【黑客】==【----帮助网安学习,以下所有学习资料文末免费领取!----】大纲学习教程面试刷题资料......
  • Java volatile 深度解析
    简介被volatile修饰的变量有两大特点:当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值立即刷新回主内存中。当读一个volatile变量时,JMM会把线程对应的本地内存设置为无效,需要工作线程重新回到主内存中读取最新共享变量。所以volatile的写的内存语......
  • 【转】-Java反射
    Java反射由浅入深|进阶必备原文链接本博文主要记录我学习Java反射(reflect)的一点心得,在了解反射之前,你应该先了解Java中的Class类,如果你不是很了解,可以先简单了解下。一、Java反射机制参考了许多博文,总结了以下个人观点,若有不妥还望指正:Java反射机制在程序运行时,......
  • 教你了解八大排序(含代码注释示例java)
    目录1.冒泡排序(BubbleSort)2.选择排序(SelectSort)3.插入排序(InsertionSort)4.希尔排序(ShellSort)5.归并排序(MergeSort)6.快速排序(QuickSort)7.堆排序(HeapSort)8.基数排序(RadixSort)1.冒泡排序(BubbleSort)这是最简单的排序算法之一。它......