iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > JAVA >HashMap详解
  • 407
分享到

HashMap详解

数据结构java链表 2023-09-22 08:09:42 407人浏览 安东尼
摘要

一、HashMap集合简介 HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。

一、HashMap集合简介

  • HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。

  • jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”解决冲突)。jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。

  • 补充:将链表转换成红黑树前会判断,即便阈值大于 8,但是数组长度小于 64,此时并不会将链表变为红黑树,而是选择逬行数组扩容。

    这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要逬行左旋,右旋,变色这些操作来保持平衡。同时数组长度小于64时,搜索时间相对要快些。所以结上所述为了提高性能和减少搜索时间,底层阈值大于8并且数组长度大于64时,链表才转换为红黑树,具体可以参考 treeifyBin() 方法。

    当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于 8 并且数组长度大于 64 时,链表转换为红黑树时,效率也变的更高效。

JDK1.7中的HashMap

采用的是头部插入法:当计算得到的索引值相同时,即Hash碰撞,引入链表结构解决Hash冲突,新加入的值会插入到旧数据的头部。
在这里插入图片描述
链表的时间复杂度是O(n):
当要找的数据是“金莲”,那么在链表上就要遍历3次才能找到,时间复杂度是O(3)
在这里插入图片描述
HashMap中并不是用取模计算索引位置,而是用位运算
位运算的效率 > 取模

为什么HashMap的容量一定要转成2的指数次幂?
因为hash值是随机的值,在与数组长度做与运算时,只有两种结果

(1)h & length
h = 0001 0101 0111 0010 1110
l = 0000 0000 0000 0001 0000
这种情况得到的结果是要么0,要么16

(2)h & (length - 1)
h = 0001 0101 0111 0010 1110
l = 0000 0000 0000 0000 1111
这种情况得到的结果是散列随机的,0-15之间的随机值。从而减少hash碰撞。

扩容机制
(1)达到扩容条件后,则扩容为原数组长度的两倍
(2)将原来老的数据移入到新的数组中,在移植的过程中还要进行一次rehash()运算
(3)

为什么加载因子是0.75?【面试题

这个值是根据空间和时间,通过泊松分布算法得到的一个折中的值。

jdk1.7中HashMap在多线程的情况下是线程安全

会在数据迁移的过程中导致链表形成一个环,导致整个HashMap无法使用。
建议在多线程环境下面,使用ConcurrentHashMap

  • 小结:

    HashMap 特点:

    • 存储无序的。

    • 键和值位置都可以是 null,但是键位置只能存在一个 null。

    • 键位置是唯一的,是底层的数据结构控制的。

    • jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树

    • 阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

二、HashMap 集合底层的数据结构

2.1存储数据的过程

示例代码:

HashMap<String, Integer> map = new HashMap<>();map.put("柳岩", 18);map.put("杨幂", 28);map.put("刘德华", 40);map.put("柳岩", 20);

输出结果:

{杨幂=28, 柳岩=20, 刘德华=40}

分析:

当创建 HashMap 集合对象的时候,HashMap 的构造方法并没有创建数组,而是在第一次调用 put 方法时创建一个长度是16 的数组 Node[] table (jdk1.8 之前是 Entry[] table)用来存储键值对数据。

假设向哈希表中存储 <柳岩,18> 数据,根据柳岩调用 String 类中重写之后的 hashCode() 方法计算出值,然后结合数组长度采用某种算法计算出向 node 数组中存储数据的空间的索引值。如果计算出的索引空间没有数据,则直接将<柳岩,18>存储到数组中。(举例:计算出的索引是 3 )

向哈希表中存储数据 <刘德华,40>,假设算出的 hashCode() 方法结合数祖长度计算出的索引值也是3,那么此时数组空间不是 null,此时底层会比较柳岩和刘德华的 hash 值是否一致,如果不一致,则在空间上划出一个结点来存储键值对数据对 <刘德华,40>,这种方式称为拉链法

假设向哈希表中存储数据 <柳岩,20>,那么首先根据柳岩调用 hashCode() 方法结合数组长度计算出索引肯定是 3,此时比较后存储的数据柳岩和已经存在的数据的 hash 值是否相等,如果 hash 值相等,此时发生哈希碰撞。那么底层会调用柳岩所属类 String 中的 equals() 方法比较两个内容是否相等:

相等:将后添加的数据的 value 覆盖之前的 value。

不相等:继续向下和其他的数据的 key 进行比较,如果都不相等,则划出一个结点存储数据,如果结点长度即链表长度大于阈值 8 并且数组长度大于 64 则将链表变为红黑树。
在这里插入图片描述

在不断的添加数据的过程中,会涉及到扩容问题,当超出阈值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的 2 倍,并将原有的数据复制过来

综上描述,当位于一个表中的元素较多,即 hash 值相等但是内容不相等的元素较多时,通过 key 值依次查找的效率较低。而 jdk1.8 中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阈值)超过8且当前数组的长度大于64时,将链表转换为红黑树,这样大大减少了查找时间

简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。如下图所示:
在这里插入图片描述

8 HashMap结构图:
在这里插入图片描述

jdk1.8 中引入红黑树的进一步原因:

jdk1.8 以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。

针对这种情况,jdk1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

总结
在这里插入图片描述

说明:

  • size 表示 HashMap 中键值对的实时数量,注意这个不等于数组的长度。
  • threshold(临界值)= capacity(容量)* loadFactor(负载因子)。这个值是当前已占用数组长度的最大值。size 超过这个值就重新 resize(扩容),扩容后的 HashMap 容量是之前容量的两倍。默认的临界值是12 = 16 * 0.75

2.2 面试

1、HashMap 中 hash 函数是怎么实现的?还有哪些hash函数的实现方式?
答:对于 key 的 hashCode 做 hash 操作,无符号右移 16 位然后做异或运算。还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。

2、当两个对象的 hashCode 相等时会怎么样?
答:会产生哈希碰撞。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 就转换为红黑树存储。

3、什么是哈希碰撞,如何解决哈希碰撞?
答:只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8之后使用链表 + 红黑树解决哈希碰撞。

4、如果两个键的 hashCode 相同,如何存储键值对?
答:通过 equals 比较内容是否相同。相同:则新的 value 覆盖之前的 value。不相同:则将新的键值对添加到哈希表中。

三、HashMap继承关系

HashMap继承关系如下图所示:

image-20220308153346883

说明:

  • Cloneable 空接口,表示可以克隆。创建并返回 HashMap 对象的一个副本。
  • Serializable 序列化接口。属于标记性接口。HashMap 对象可以被序列化和反序列化。
  • AbstractMap 父类提供了 Map 实现接口。以最大限度地减少实现此接口所需的工作。

四、HashMap集合类的成员

4.1成员变量

4.1.1 serialVersionUID

序列化版本号

private static final long serialVersionUID = 362498820763181265L;
4.1.2 DEFAULT_INITIAL_CAPACITY

集合的初始化容量(必须是 2 的 n 次幂):16

// 默认的初始容量是161 << 4 相当于 1*2的4次方static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
4.1.3 DEFAULT_LOAD_FACTOR

默认的负载因子(默认值 0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.1.4 MAXIMUM_CAPACITY

集合最大容量

static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次幂
4.1.5 TREEIFY_THRESHOLD

当链表的值超过8并且数组长度大于64,则会转为红黑树(jdk1.8新增)

// 当桶(bucket)上的结点数大于这个值时会转为红黑树static final int TREEIFY_THRESHOLD = 8;

问题:为什么 Map 桶中结点个数超过 8 才转为红黑树?

4.1.6 UNTREEIFY_THRESHOLD

当链表的值小于 6 则会从红黑树转回链表

// 当桶(bucket)上的结点数小于这个值,树转为链表 static final int UNTREEIFY_THRESHOLD = 6;
4.1.7 MIN_TREEIFY_CAPACITY

当 Map 里面的数量超过这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于4*TREEIFY_THRESHOLD(8)

// 桶中结构转化为红黑树对应的数组长度最小的值 static final int MIN_TREEIFY_CAPACITY = 64;
4.1.8 table

table 用来初始化(必须是二的n次幂)(重点)

// 存储元素的数组 transient Node<K,V>[] table;

在 jdk1.8 中我们了解到 HashMap 是由数组加链表加红黑树来组成的结构,其中 table 就是 HashMap 中的数组,jdk8 之前数组类型是 Entry 类型。从 jdk1.8 之后是 Node 类型。只是换了个名字,都实现了一样的接口:Map.Entry。负责存储键值对数据的。

4.1.9 entrySet

用来存放缓存

// 存放具体元素的集合transient Set<Map.Entry<K,V>> entrySet;
4.1.10 size

HashMap 中存放元素的个数(重点)

// 存放元素的个数,注意这个不等于数组的长度 transient int size;

size 为 HashMap 中 K-V 的实时数量,不是数组 table 的长度。

4.1.11 modCount

用来记录 HashMap 的修改次数

// 每次扩容和更改 map 结构的计数器 transient int modCount;  
4.1.12 threshold

用来调整大小下一个容量的值计算方式为(容量*负载因子)

// 临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容int threshold;
4.1.13 loadFactor

哈希表的负载因子(重点),默认是0.75f

// 负载因子final float loadFactor;

说明:

  • loadFactor 是用来衡量 HashMap 满的程度,表示HashMap的疏密程度,影响 hash 操作到同一个数组位置的概率,计算 HashMap 的实时负载因子的方法为:size/capacity,而不是占用桶的数量去除以 capacity。capacity 是桶的数量,也就是 table 的长度 length。
  • loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
  • 当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap 太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建 HashMap 集合对象时指定初始容量来尽量避免。
  • 在 HashMap 的构造器中可以定制 loadFactor。
// 构造方法,构造一个带指定初始容量和负载因子的空HashMapHashMap(int initialCapacity, float loadFactor);
  • 为什么负载因子设置为0.75,初始化临界值是12?

    loadFactor 越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
    在这里插入图片描述

如果希望链表尽可能少些,要提前扩容。有的数组空间有可能一直没有存储数据,负载因子尽可能小一些。

举例:

例如:负载因子是0.4。 那么16*0.4--->6 如果数组中满6个空间就扩容会造成数组利用率太低了。 负载因子是0.9。 那么16*0.9--->14 那么这样就会导致链表有点多了,导致查找元素效率低。

所以既兼顾数组利用率又考虑链表不要太多,经过大量测试 0.75 是最佳方案。

  • threshold 计算公式:capacity(数组长度默认16) * loadFactor(负载因子默认0.75)。

这个值是当前已占用数组长度的最大值。当 Size >= threshold 的时候,那么就要考虑对数组的 resize(扩容),也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准扩容后的 HashMap 容量是之前容量的两倍

4.2 构造方法

HashMap 中重要的构造方法,它们分别如下:

4.2.1 HashMap()

构造一个空的HashMap,默认初始容量(16)和默认负载因子(0.75)。

public HashMap() {   this.loadFactor = DEFAULT_LOAD_FACTOR; // 将默认的负载因子0.75赋值给loadFactor,并没有创建数组}
4.2.2 HashMap(int initialCapacity)

构造一个具有指定的初始容量和默认负载因子(0.75)HashMap 。

 // 指定“容量大小”的构造函数public HashMap(int initialCapacity) {    this(initialCapacity, DEFAULT_LOAD_FACTOR);}
4.2.3 HashMap(int initialCapacity, float loadFactor)

构造一个具有指定的初始容量和负载因子的 HashMap。

public HashMap(int initialCapacity, float loadFactor) {    // 判断初始化容量initialCapacity是否小于0        if (initialCapacity < 0)            // 如果小于0,则抛出非法的参数异常IllegalArgumentException            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);    // 判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY        if (initialCapacity > MAXIMUM_CAPACITY)            // 如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacity            initialCapacity = MAXIMUM_CAPACITY;    // 判断负载因子loadFactor是否小于等于0或者是否是一个非数值        if (loadFactor <= 0 || Float.isNaN(loadFactor))            // 如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentException            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);     // 将指定的负载因子赋值给HashMap成员变量的负载因子loadFactor        this.loadFactor = loadFactor;        this.threshold = tableSizeFor(initialCapacity);    }// 最后调用了tableSizeFor,来看一下方法实现:         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;    }

说明:

对于 java this.threshold = tableSizeFor(initialCapacity); 疑问解答:

tableSizeFor(initialCapacity)判断指定的初始化容量是否是2的n次幂,如果不是那么会变为比指定初始化容量大的最小的2的n次幂。但是注意,在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边界值了。有些人会觉得这里是一个bug,应该这样书写:this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。但是请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

4.3 成员方法

4.3.1 增加方法 put()

put方法是比较复杂的,实现步骤大致如下:

​ 1、先通过 hash 值计算出 key 映射到哪个桶;

​ 2、如果桶上没有碰撞冲突,则直接插入;

​ 3、如果出现碰撞冲突了,则需要处理冲突:

​ a 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;

​ b 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;

​ 4、如果桶中存在重复的键,则为该键替换新值 value;

​ 5、如果 size 大于阈值 threshold,则进行扩容;

具体方法如下:

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

说明:

​ 1、HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。 所以我们重点看 putVal 方法。

​ 2、我们可以看到在 putVal 方法中 key 在这里执行了一下 hash 方法,来看一下 hash 方法是如何实现的。

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

从上面可以得知 HashMap 是支持 key 为空的,而 HashTable 是直接用 Key 来获取hashCode 所以 key 为空会抛异常。

解读上述hash方法:

我们先研究下 key 的哈希值是如何计算出来的。key 的哈希值是通过上述方法计算出来的。

这个哈希方法首先计算出 key 的 hashCode 赋值给 h,然后与 h 无符号右移 16 位后的二进制进行按位异或得到最后的 hash 值。计算过程如下所示:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

在 putVal 函数中使用到了上述 hash 函数计算的哈希值:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {...if ((p = tab[i = (n - 1) & hash]) == null) // 这里的n表示数组长度16...}

计算过程如下所示:

说明:

key.hashCode();返回散列值也就是 hashcode,假设随便生成的一个值。

n 表示数组初始化的长度是 16。
3、&(按位与运算):运算规则:相同的二进制数位上,都是 1 的时候,结果为 1,否则为0。

^(按位异或运算):运算规则:相同的二进制数位上,数字相同,结果为 0,不同为 1。
在这里插入图片描述

4.3.2 扩容方法 resize()

扩容机制

​ 1、什么时候才需要扩容

​ 当 HashMap 中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor 的默认值是 0.75。

4.3.3 删除方法 remove()

删除方法就是首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于 6 的时候要转链表。

删除 remove() 方法:

// remove方法的具体实现在removeNode方法中,所以我们重点看下removeNode方法public V remove(Object key) {        Node<K,V> e;        return (e = removeNode(hash(key), key, null, false, true)) == null ?            null : e.value;    }

removeNode() 方法:

final Node<K,V> removeNode(int hash, Object key, Object value,                           boolean matchValue, boolean movable) {    Node<K,V>[] tab; Node<K,V> p; int n, index;// 根据hash找到位置 // 如果当前key映射到的桶不为空    if ((tab = table) != null && (n = tab.length) > 0 &&        (p = tab[index = (n - 1) & hash]) != null) {        Node<K,V> node = null, e; K k; V v;        // 如果桶上的结点就是要找的key,则将node指向该结点        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            node = p;        else if ((e = p.next) != null) {            // 说明结点存在下一个结点            if (p instanceof TreeNode)                // 说明是以红黑树来处理的冲突,则获取红黑树要删除的结点                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);            else {                // 判断是否以链表方式处理hash冲突,是的话则通过遍历链表来寻找要删除的结点                do {                    if (e.hash == hash &&                        ((k = e.key) == key ||                         (key != null && key.equals(k)))) {                        node = e;                        break;                    }                    p = e;                } while ((e = e.next) != null);            }        }        // 比较找到的key的value和要删除的是否匹配        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {            // 通过调用红黑树的方法来删除结点            if (node instanceof TreeNode)                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);            else if (node == p)                // 链表删除                tab[index] = node.next;            else                p.next = node.next;            // 记录修改次数            ++modCount;            // 变动的数量            --size;            afterNodeRemoval(node);            return node;        }    }    return null;}
4.3.5 查找元素方法 get()

查找方法,通过元素的 key 找到 value。

代码如下:

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

get 方法主要调用的是 getNode 方法,代码如下:

final Node<K,V> getNode(int hash, Object key) {    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;    // 如果哈希表不为空并且key对应的桶上不为空    if ((tab = table) != null && (n = tab.length) > 0 &&        (first = tab[(n - 1) & hash]) != null) {                if (first.hash == hash && // always check first node            ((k = first.key) == key || (key != null && key.equals(k))))            return first;        // 如果不是第一个元素,判断是否有后续结点        if ((e = first.next) != null) {            // 判断是否是红黑树,是的话调用红黑树中的getTreeNode方法获取结点            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);        }    }    return null;}

小结:

  1. get 方法实现的步骤:

    a. 通过 hash 值获取该 key 映射到的桶
    b. 桶上的 key 就是要查找的 key,则直接找到并返回
    c. 桶上的 key 不是要找的 key,则查看后续的结点:

    1、如果后续结点是红黑树结点,通过调用红黑树的方法根据 key 获取 value2、如果后续结点是链表结点,则通过循环遍历链表根据 key 获取 value

上述红黑树结点调用的是 getTreeNode 方法通过树形结点的 find 方法进行查找:

 final TreeNode<K,V> getTreeNode(int h, Object k) {            return ((parent != null) ? root() : this).find(h, k, null); }final TreeNode<K,V> find(int h, Object k, Class<?> kc) {    TreeNode<K,V> p = this;    do {        int ph, dir; K pk;        TreeNode<K,V> pl = p.left, pr = p.right, q;        if ((ph = p.hash) > h)            p = pl;        else if (ph < h)            p = pr;        else if ((pk = p.key) == k || (k != null && k.equals(pk)))            return p; // 找到之后直接返回        else if (pl == null)            p = pr;        else if (pr == null)            p = pl;        else if ((kc != null ||                  (kc = comparableClassFor(k)) != null) &&                 (dir = compareComparables(kc, k, pk)) != 0)            p = (dir < 0) ? pl : pr;        // 递归查找        else if ((q = pr.find(h, k, kc)) != null)            return q;        else            p = pl;    } while (p != null);    return null;}

查找红黑树,由于之前添加时已经保证这个树是有序的了,因此查找时基本就是折半查找,效率更高。

这里和插入时一样,如果对比结点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回。不相等就从子树中递归查找。

若为树,则在树中通过key.equals(k)查找,O(logn)。若为链表,则在链表中通过key.equals(k)查找,O(n)。

4.3.6 遍历 HashMap 集合几种方式

分别遍历Key和Value

for (String key : map.keySet()) {System.out.println(key);}for (Object vlaue : map.values() {System.out.println(value);}

使用 Iterator 迭代器迭代(推荐使用)

Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();while (iterator.hasNext()) {    Map.Entry<String, Object> mapEntry = iterator.next();    System.out.println(mapEntry.geTKEy() + "---" + mapEntry.getValue());}

通过 get 方式(不建议使用)

Set<String> keySet = map.keySet();for (String str : keySet) {System.out.println(str + "---" + map.get(str));}

五、设计 HashMap 的初始化容量

5.1 问题描述

如果我们确切的知道我们有多少键值对需要存储,那么我们在初始化 HashMap 的时候就应该指定它的容量,以防止 HashMap 自动扩容,影响使用效率。

默认情况下 HashMap 的容量是 16,但是,如果用户通过构造函数指定了一个数字作为容量,那么 Hash 会选择大于该数字的第一个 2 的幂作为容量(3->4、7->8、9->16)。这点我们在上述已经进行过讲解。

5.2 《阿里巴巴Java开发手册》的建议

在这里插入图片描述

关于设置 HashMap 的初始化容量

我们上面介绍过,HashMap 的扩容机制,就是当达到扩容条件时会进行扩容。HashMap 的扩容条件就是当 HashMap 中的元素个数(size)超过临界值(threshold)时就会自动扩容。所以,如果我们没有设置初始容量大小,随着元素的不断增加,HashMap 会有可能发生多次扩容,而 HashMap 中的扩容机制决定了每次扩容都需要重建 hash 表,是非常影响性能的

但是设置初始化容量,设置的数值不同也会影响性能,那么当我们已知 HashMap 中即将存放的 KV 个数的时候,容量设置成多少为好呢?

关于设置 HashMap 的初始化容量大小

可以认为,当我们明确知道 HashMap 中元素的个数的时候,把默认容量设置成 initialCapacity/ 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。

而 Jdk 并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个 2 的幂。

来源地址:https://blog.csdn.net/java123456111/article/details/123378463

--结束END--

本文标题: HashMap详解

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

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

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

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

下载Word文档
猜你喜欢
  • HashMap详解
    一、HashMap集合简介 HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。...
    99+
    2023-09-22
    数据结构 java 链表
  • Java 中HashMap 详解
    本篇重点: HashMap的存储结构 HashMap的put和get操作过程 HashMap的扩容 关于transient关键字 HashMap的存储结构 HashMap 总体是数组+链表的存储结构, 从JDK1.8开始,当数组的长度大...
    99+
    2023-09-03
    哈希算法 散列表 java
  • Java之HashMap案例详解
    概述 这篇文章,我们打算探索一下Java集合(Collections)框架中Map接口中HashMap的实现。Map虽然是Collctions框架的一部分,但是Map并没有实现Col...
    99+
    2024-04-02
  • rust的vector和hashmap详解
    目录动态数组Vector创建动态数组向数组末尾追加元素重提内存安全所有权系统引用规则hashmap创建hashmap新增键值对根据键查询值根据键删除hashmap的键值对动态数组Ve...
    99+
    2023-03-19
    rust的vector和hashmap rust的vector rust hashmap
  • Java详细分析讲解HashMap
    目录1.HashMap数据结构2.HashMap特点3.HashMap中put方法流程java集合容器类分为Collection和Map两大类,Collection类的子接口有Set...
    99+
    2024-04-02
  • 详解JavaLinkedHashMap与HashMap的使用
    目录HashMap存储自定义类型键值LinkedHashMapMap集合练习JDK9对集合添加的优化HashMap存储自定义类型键值 练习:每位学生(姓名,年龄)都有自己的家庭住址。...
    99+
    2022-11-13
    Java LinkedHashMap HashMap使用 Java LinkedHashMap HashMap Java LinkedHashMap Java HashMap
  • Java 详解Map集合之HashMap和TreeMap
    目录HashMap创建HashMap添加元素访问元素删除元素TreeMap创建TreeMap添加元素访问元素删除元素HashMap、TreeMap区别 Map接口储存一组成对的键-值...
    99+
    2024-04-02
  • Java集合HashMap的知识点详解
    这篇文章主要讲解了“Java集合HashMap的知识点详解”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java集合HashMap的知识点详解”吧!一、什么是哈希表在讨论哈希表之前,我们先大...
    99+
    2023-06-02
  • Java模拟实现HashMap算法流程详解
    目录1、前言2、成员变量的设定3、构造方法4、hash方法以及阈值判断方法5、put方法6、resize 方法7、get 方法1、前言 上期讲解了 HashMap 和 HashSet...
    99+
    2023-02-08
    Java HashMap Java模拟实现HashMap
  • Java中HashMap集合的常用方法详解
    目录public Object clone()总结public Object clone() 返回hashMap集合的副本   其余的方法都是实现Map...
    99+
    2024-04-02
  • Java源码解析之HashMap的put、resize方法详解
    目录一、HashMap 简介二、源码分析2.1 继承和实现2.2 属性2.3 节点类型Node内部类2.4 红黑树的节点三、构造方法3.1 构造器13.2 构造器23.3 构造器33...
    99+
    2024-04-02
  • 通过HashMap原理详解entrySet中的疑问
    目录HashMap底层变量put()方法:2. get(Object key)方法:3. remove(Object key)方法:4.entrySet()方法:EntrySet类代...
    99+
    2022-11-13
    HashMap entrySet HashMap 原理
  • 怎么理解HashMap
    这篇文章主要讲解了“怎么理解HashMap”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么理解HashMap”吧!1、HashMap添加一个键值对的过程是怎么样的这是网上找的一张流程图,可...
    99+
    2023-06-16
  • Java实现HashMap排序方法的示例详解
    目录简介排序已有数据按key排序按value排序按插入顺序存放HashMap不按插入顺序存放LinkedHashMap会按照插入顺序存放简介 本文用示例介绍HashMap排序的方法。...
    99+
    2024-04-02
  • java Map接口子类HashMap遍历与LinkedHashMap详解
    目录一、概述二、Map常用子类三、Map接口中的常用方法四、Map集合遍历键找值方式五、Entry键值对对象六、Map集合遍历键值对方式七、HashMap存储自定义类型键值八、Lin...
    99+
    2024-04-02
  • Java详解HashMap实现原理和源码分析
    目录学习要点:1、什么是HashMap?2、HashMap的特性3、HashMap的数据结构4、HashMap初始化操作4.1、成员变量4.2、 构造方法5、Jdk8中HashMap...
    99+
    2024-04-02
  • JDK8中的HashMap初始化和扩容机制详解
    一、HashMap初始化方法 HashMap() 不带参数,默认初始化大小为16,加载因子为0.75; HashMap(int initialCapacity) 指定初始化大小; H...
    99+
    2024-04-02
  • Rust语言中的String和HashMap使用示例详解
    目录String新建字符串更新字符串使用 + 运算符或 format! 宏拼接字符串索引字符串字符串 slice遍历字符串HashMap新建 HashMapHashMap 和 own...
    99+
    2022-11-13
    Rust String HashMap使用 Rust String HashMap
  • HashMap底层原理全面详解面试绝对不慌
    目录快速入门技术的本质——底层结构结构构成为什么要用链表哈希算法手写HashMapHashMap底层的实现什么情况下用红黑树?快速入门 存储:put 方法 put(key,v...
    99+
    2024-04-02
  • 基于Java快速实现一个简单版的HashMap详解
    目录1.示例图2.分析需求3.代码实现3.运行结果简单实现一个底层数据结构为数组 + 链表的HashMap,不考虑链表长度超过8个时变为红黑树的情况。 1.示例图 2.分析需求 p...
    99+
    2023-02-08
    Java实现HashMap Java HashMap
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作