java 数据结构-HashMap

一、hashmap特点

        1、HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

        2、HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

        3、HashMap 是无序的,即不会记录插入的顺序。

        4、HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

 HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。

HashMap 中的元素实际上是对象,一些常见的基本类型可以使用它的包装类。

基本类型对应的包装类表如下:

基本类型 引用类型
boolean Boolean
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character

HashMap 类位于 java.util 包中,使用前需要引入它,语法格式如下:

import java.util.HashMap; // 引入 HashMap 类

以下实例我们创建一个 HashMap 对象 Sites, 整型(Integer)的 key 和字符串(String)类型的 value:

HashMap<Integer, String> Sites = new HashMap<Integer, String>();

二、常用方法

 1、添加元素-put()

HashMap 类提供了很多有用的方法,添加键值对(key-value)可以使用 put() 方法:

// 引入 HashMap 类      
import java.util.HashMap;

public class RunoobTest {
    public static void main(String[] args) {
        // 创建 HashMap 对象 Sites
        HashMap<Integer, String> Sites = new HashMap<Integer, String>();
        // 添加键值对
        Sites.put(1, "Google");
        Sites.put(2, "Runoob");
        Sites.put(3, "Taobao");
        Sites.put(4, "Zhihu");
        System.out.println(Sites);
    }
}

 2、访问元素get()

 以下实例创建一个字符串(String)类型的 key 和字符串(String)类型的 value:

// 引入 HashMap 类      
import java.util.HashMap;

public class RunoobTest {
    public static void main(String[] args) {
        // 创建 HashMap 对象 Sites
        HashMap<Integer, String> Sites = new HashMap<Integer, String>();
        // 添加键值对
        Sites.put(1, "Google");
        Sites.put(2, "Runoob");
        Sites.put(3, "Taobao");
        Sites.put(4, "Zhihu");
        System.out.println(Sites.get(3));
    }
}

3、删除元素 remove()

我们可以使用 remove(key) 方法来删除 key 对应的键值对(key-value):

// 引入 HashMap 类      
import java.util.HashMap;

public class RunoobTest {
    public static void main(String[] args) {
        // 创建 HashMap 对象 Sites
        HashMap<Integer, String> Sites = new HashMap<Integer, String>();
        // 添加键值对
        Sites.put(1, "Google");
        Sites.put(2, "Runoob");
        Sites.put(3, "Taobao");
        Sites.put(4, "Zhihu");
        Sites.remove(4);
        System.out.println(Sites);
    }
}

删除所有键值对(key-value)可以使用 clear 方法

// 引入 HashMap 类      
import java.util.HashMap;

public class RunoobTest {
    public static void main(String[] args) {
        // 创建 HashMap 对象 Sites
        HashMap<Integer, String> Sites = new HashMap<Integer, String>();
        // 添加键值对
        Sites.put(1, "Google");
        Sites.put(2, "Runoob");
        Sites.put(3, "Taobao");
        Sites.put(4, "Zhihu");
        Sites.clear();
        System.out.println(Sites);
    }
}

4、计算大小

如果要计算 HashMap 中的元素数量可以使用 size() 方法:

// 引入 HashMap 类      
import java.util.HashMap;

public class RunoobTest {
    public static void main(String[] args) {
        // 创建 HashMap 对象 Sites
        HashMap<Integer, String> Sites = new HashMap<Integer, String>();
        // 添加键值对
        Sites.put(1, "Google");
        Sites.put(2, "Runoob");
        Sites.put(3, "Taobao");
        Sites.put(4, "Zhihu");
        System.out.println(Sites.size());
    }
}

5、迭代 HashMap

可以使用 for-each 来迭代 HashMap 中的元素。

如果你只想获取 key,可以使用 keySet() 方法,然后可以通过 get(key) 获取对应的 value,如果你只想获取 value,可以使用 values() 方法。

// 引入 HashMap 类      
import java.util.HashMap;

public class RunoobTest {
    public static void main(String[] args) {
        // 创建 HashMap 对象 Sites
        HashMap<Integer, String> Sites = new HashMap<Integer, String>();
        // 添加键值对
        Sites.put(1, "Google");
        Sites.put(2, "Runoob");
        Sites.put(3, "Taobao");
        Sites.put(4, "Zhihu");
        // 输出 key 和 value
        for (Integer i : Sites.keySet()) {
            System.out.println("key: " + i + " value: " + Sites.get(i));
        }
        // 返回所有 value 值
        for(String value: Sites.values()) {
          // 输出每一个value
          System.out.print(value + ", ");
        }
    }
}

Java HashMap 方法

描述
clear() 删除 hashMap 中的所有键/值对
clone() 复制一份 hashMap
isEmpty() 判断 hashMap 是否为空
size() 计算 hashMap 中键/值对的数量
put() 将键/值对添加到 hashMap 中
putAll() 将所有键/值对添加到 hashMap 中
putIfAbsent() 如果 hashMap 中不存在指定的键,则将指定的键/值对插入到 hashMap 中。
remove() 删除 hashMap 中指定键 key 的映射关系
containsKey() 检查 hashMap 中是否存在指定的 key 对应的映射关系。
containsValue() 检查 hashMap 中是否存在指定的 value 对应的映射关系。
replace() 替换 hashMap 中是指定的 key 对应的 value。
replaceAll() 将 hashMap 中的所有映射关系替换成给定的函数所执行的结果。
get() 获取指定 key 对应对 value
getOrDefault() 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值
forEach() 对 hashMap 中的每个映射执行指定的操作。
entrySet() 返回 hashMap 中所有映射项的集合集合视图。
keySet() 返回 hashMap 中所有 key 组成的集合视图。
values() 返回 hashMap 中存在的所有 value 值。
merge() 添加键值对到 hashMap 中
compute() 对 hashMap 中指定 key 的值进行重新计算
computeIfAbsent() 对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中
computeIfPresent() 对 hashMap 中指定 key 的值进行重新计算,前提是该 key 存在于 hashMap 中。

 三、HashMap原理

1、什么是HashMap:

(1)HashMap 是基于 Map 接口的非同步实现,线程不安全,是为了快速存取而设计的;它采用 key-value 键值对的形式存放元素(并封装成 Node 对象),允许使用 null 键和 null 值,但只允许存在一个键为 null,并且存放在 Node[0] 的位置,不过允许存在多个 value 为 null 的情况。

(2)在 JDK7 及之前的版本,HashMap 的数据结构可以看成“数组+链表”,在 JDK8 及之后的版本,数据结构可以看成”数组+链表+红黑树”,也就是说 HashMap 底层采用数组实现,数组的每个位置都存储一个单向链表,当链表的长度超过一定的阈值时,就会转换成红黑树。转换的目的是当链表中元素较多时,也能保证HashMap的存取效率(备注:链表转为红黑树只有在数组的长度大于等于64才会触发)

(3)HashMap 有两个影响性能的关键参数:“初始容量”和“加载因子”:

容量 capacity:就是哈希表中数组的数量,默认初始容量是16,容量必须是2的N次幂,这是为了提高计算机的执行效率。
加载因子 loadfactor:在 HashMap 扩容之前,容量可以达到多满的程度,默认值为 0.75
扩容阈值 threshold = capacity * loadfactor

(4)采用 Fail-Fast 机制,底层通过一个 modCount 值记录修改的次数,对 HashMap 的修改操作都会增加这个值。迭代器在初始过程中会将这个值赋给 exceptedModCount ,在迭代的过程中,如果发现 modCount 和 exceptedModCount 的值不一致,代表有其他线程修改了Map,就立刻抛出异常。

2、HashMap 的 put() 方法添加元素的过程:

(1)重新计算 hash 值:

拿到 key 的 hashcode 值之后,调用 hash() 方法重新计算 hash 值,防止质量低下的 hashCode() 函数出现,从而使 hash 值的分布尽量均匀。

JDK8 及之后的版本,对 hash() 方法进行了优化,重新计算 hash 值时,让 hashCode 的高16位参与异或运算,目的是即使 table 数组的长度较小,在计算元素存储位置时,也能让高位也参与运算。 (key == null)? 0 : ( h = key.hashcode()) ^ (h >>> 16)

(2)计算元素存放在数组中的哪个位置:

将重新计算出来的 hash 值与 (tablel.length-1) 进行位与&运算,得出元素应该放入数组的哪个位置。

为什么 HashMap 的底层数组长度总是2的n次方幂?因为当 length 为2的n次方时,h & (length – 1) 就相当于对 length 取模,而且速度比直接取模要快得多,二者等价不等效,这是HashMap在性能上的一个优化

(3)将 key-value 添加到数组中:

① 如果计算出的数组位置上为空,那么直接将这个元素插入放到该位置中。

② 如果数组该位置上已经存在链表,则使用 equals() 比较链表上是否存在 key 相同的节点,如果为true,则替换原元素;如果不存在,则在链表的尾部插入新节点(Jdk1.7及以前的版本使用的头插法)

③ 如果插入元素后,如果链表的节点数是否超过8个,则调用 treeifyBin() 将链表节点转为红黑树节点。

④ 最后判断 HashMap 总容量是否超过阈值 threshold,则调用 resize() 方法进行扩容,扩容后数组的长度变成原来的2倍。

 

在 HashMap 中,当发生hash冲突时,解决方式是采用拉链法,也就是将所有哈希值相同的记录都放在同一个链表中,除此之外,解决hash冲突的方式有: 

开放地址法(线性探测再散列、二次探测再散列、伪随机探测再散列):当冲突发生时,在散列表中形成一个探测序列,沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址为止(即该地址单元为空)。如果是插入的情况,在探查到开放的地址,则可将待插入的新结点存入该地址单元,如果是查找的情况,探查到开放的地址则表明表中无待查的关键字,即查找失败。
再哈希法:产生冲突时,使用另外的哈希函数计算出一个新的哈希地址、直到冲突不再发生
建立一个公共溢出区:把冲突的记录都放在另一个存储空间,不放在表里面。

3、HashMap扩容的过程:

(1)重新建立一个新的数组,长度为原数组的两倍;

(2)遍历旧数组的每个数据,重新计算每个元素在新数组中的存储位置。使用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,表示元素在新数组中的位置不变;否则,则在新数组中的位置下标=原位置+原数组长度。

(3)将旧数组上的每个数据使用尾插法逐个转移到新数组中,并重新设置扩容阈值。

问题:为什么扩容时节点重 hash 只可能分布在原索引位置或者 原索引长度+oldCap 位置呢?换句话说,扩容时使用节点的hash值跟oldCap进行位与运算,以此决定将节点分布到原索引位置或者原索引+oldCap位置上的原理是什么呢?

假设老表的容量为16,则新表容量为16*2=32,假设节点1的hash值为 0000 0000 0000 0000 0000 1111 0000 1010,节点2的hash值为 0000 0000 0000 0000 0000 1111 0001 1010。

那么节点1和节点2在老表的索引位置计算如下图计算1,由于老表的长度限制,节点1和节点2的索引位置只取决于节点hash值的最后4位。再看计算2,计算2为元素在新表中的索引计算,可以看出如果两个节点在老表的索引位置相同,则新表的索引位置只取决于节点hash值倒数第5位的值,而此位置的值刚好为老表的容量值16,此时节点在新表的索引位置只有两种情况:原索引位置和 原索引+oldCap位置(在此例中即为10和10+16=26)。由于结果只取决于节点hash值的倒数第5位,而此位置的值刚好为老表的容量值16,因此此时新表的索引位置的计算可以替换为计算3,直接使用节点的hash值与老表的容量16进行位于运算,如果结果为0则该节点在新表的索引位置为原索引位置,否则该节点在新表的索引位置为 原索引+ oldCap 位置。

4、HashMap 链表转换成红黑树:

当数组中某个位置的节点达到8个时,会触发 treeifyBin() 方法将链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点后,链表结构就不存在了。当数组中某个位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发 untreeify() 将红黑树节点转化成链表节点。

HashMap 在进行插入和删除时有可能会触发红黑树的插入平衡调整(balanceInsertion方法)或删除平衡调整(balanceDeletion )方法,调整的方式主要有以下手段:左旋转(rotateLeft方法)、右旋转(rotateRight方法)、改变节点颜色(x.red = false、x.red = true),进行调整的原因是为了维持红黑树的数据结构。

当链表长过长时会转换成红黑树,那能不能使用AVL树替代呢? AVL树是完全平衡二叉树,要求每个结点的左右子树的高度之差的绝对值最多为1,而红黑树通过适当的放低该条件(红黑树限制从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的),以此来减少插入/删除时的平衡调整耗时,从而获取更好的性能,虽然会导致红黑树的查询会比AVL稍慢,但相比插入/删除时获取的时间,这个付出在大多数情况下显然是值得的。

5、HashMap 在 JDK7 和 JDK8 有哪些区别?

① 数据结构:在 JDK7 及之前的版本,HashMap 的数据结构可以看成“数组+链表”,在 JDK8 及之后的版本,数据结构可以看成”数组+链表+红黑树”,当链表的长度超过8时,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率

② 对数据重哈希:JDK8 及之后的版本,对 hash() 方法进行了优化,重新计算 hash 值时,让 hashCode 的高16位参与异或运算,目的是在 table 的 length较小的时候,在进行计算元素存储位置时,也让高位也参与运算。

③ 在 JDK7 及之前的版本,在添加元素的时候,采用头插法,所以在扩容的时候,会导致之前元素相对位置倒置了,在多线程环境下扩容可能造成环形链表而导致死循环的问题。DK1.8之后使用的是尾插法,扩容是不会改变元素的相对位置

④ 扩容时重新计算元素的存储位置的方式:JDK7 及之前的版本重新计算存储位置是直接使用 hash & (table.length-1);JDK8 使用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,表示元素在新数组中的位置不变;否则,则在新数组中的位置下标=原位置+原数组长度。

⑤ JDK7 是先扩容后插入,这就导致无论这次插入是否发生hash冲突都需要进行扩容,但如果这次插入并没有发生Hash冲突的话,那么就会造成一次无效扩容;JDK8是先插入再扩容的,优点是减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容

6、线程不安全的体现?如何变成线程安全:

无论在 JDK7 还是 JDK8 的版本中,HashMap 都是线程不安全的,HashMap 的线程不安全主要体现在以下两个方面:

在JDK7及以前的版本,表现为在多线程环境下进行扩容,由于采用头插法,位于同一索引位置的节点顺序会反掉,导致可能出现死循环的情况
在JDK8及以后的版本,表现为在多线程环境下添加元素,可能会出现数据丢失的情况

如果想使用线程安全的 Map 容器,可以使用以下几种方式:

(1)使用线程安全的 Hashtable,它底层的每个方法都使用了 synchronized 保证线程同步,所以每次都锁住整张表,在性能方面会相对比较低。

除了线程安全性方面,Hashtable 和 HashMap 的不同之处还有:

继承的父类:两者都实现了 Map 接口,但 HashMap 继承自 AbstractMap 类,而 Hashtable 继承自 Dictionary 类
遍历方式:HashMap 仅支持 Iterator 的遍历方式,但 Hashtable 实现了 Enumeration 接口,所以支持Iterator和Enumeration两种遍历方式
使用方式:HashMap 允许 null 键和 null 值,Hashtable 不允许 null 键和 null 值
数据结构:HashMap 底层使用“数组+链表+红黑树”,Hashtable 底层使用“数组+链表”
初始容量及扩容方式:HashMap 的默认初始容量为16,每次扩容为原来的2倍;Hashtable 默认初始容量为11,每次扩容为原来的2倍+1。
元素的hash值:HashMap的hash值是重新计算过的,Hashtable直接使用Object的hashCode;

之所以会出现初始容量以及元素hash值计算方式的不同,是因为 HashMap 和 Hashtable 设计时的侧重点不同。Hashtable 的侧重点是哈希结果更加均匀,使得哈希冲突减少,当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而 HashMap 则更加已关注哈希的计算效率问题,在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。 有关 Hashtable 的内容,可以详细阅读:Hashtable原理详解(JDK1.8)

(2)使用Collections.synchronizedMap()方法来获取一个线程安全的集合,底层原理是使用synchronized来保证线程同步。

(3)使用 ConcurrentHashMap 集合。

四、

4、HashMap 链表转换成红黑树:

当数组中某个位置的节点达到8个时,会触发 treeifyBin() 方法将链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点后,链表结构就不存在了。当数组中某个位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发 untreeify() 将红黑树节点转化成链表节点。

HashMap 在进行插入和删除时有可能会触发红黑树的插入平衡调整(balanceInsertion方法)或删除平衡调整(balanceDeletion )方法,调整的方式主要有以下手段:左旋转(rotateLeft方法)、右旋转(rotateRight方法)、改变节点颜色(x.red = false、x.red = true),进行调整的原因是为了维持红黑树的数据结构。

当链表长过长时会转换成红黑树,那能不能使用AVL树替代呢? AVL树是完全平衡二叉树,要求每个结点的左右子树的高度之差的绝对值最多为1,而红黑树通过适当的放低该条件(红黑树限制从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的),以此来减少插入/删除时的平衡调整耗时,从而获取更好的性能,虽然会导致红黑树的查询会比AVL稍慢,但相比插入/删除时获取的时间,这个付出在大多数情况下显然是值得的。

5、HashMap 在 JDK7 和 JDK8 有哪些区别?

① 数据结构:在 JDK7 及之前的版本,HashMap 的数据结构可以看成“数组+链表”,在 JDK8 及之后的版本,数据结构可以看成”数组+链表+红黑树”,当链表的长度超过8时,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率

② 对数据重哈希:JDK8 及之后的版本,对 hash() 方法进行了优化,重新计算 hash 值时,让 hashCode 的高16位参与异或运算,目的是在 table 的 length较小的时候,在进行计算元素存储位置时,也让高位也参与运算。

③ 在 JDK7 及之前的版本,在添加元素的时候,采用头插法,所以在扩容的时候,会导致之前元素相对位置倒置了,在多线程环境下扩容可能造成环形链表而导致死循环的问题。DK1.8之后使用的是尾插法,扩容是不会改变元素的相对位置

④ 扩容时重新计算元素的存储位置的方式:JDK7 及之前的版本重新计算存储位置是直接使用 hash & (table.length-1);JDK8 使用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,表示元素在新数组中的位置不变;否则,则在新数组中的位置下标=原位置+原数组长度。

⑤ JDK7 是先扩容后插入,这就导致无论这次插入是否发生hash冲突都需要进行扩容,但如果这次插入并没有发生Hash冲突的话,那么就会造成一次无效扩容;JDK8是先插入再扩容的,优点是减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容

6、线程不安全的体现?如何变成线程安全:

无论在 JDK7 还是 JDK8 的版本中,HashMap 都是线程不安全的,HashMap 的线程不安全主要体现在以下两个方面:

在JDK7及以前的版本,表现为在多线程环境下进行扩容,由于采用头插法,位于同一索引位置的节点顺序会反掉,导致可能出现死循环的情况
在JDK8及以后的版本,表现为在多线程环境下添加元素,可能会出现数据丢失的情况

如果想使用线程安全的 Map 容器,可以使用以下几种方式:

(1)使用线程安全的 Hashtable,它底层的每个方法都使用了 synchronized 保证线程同步,所以每次都锁住整张表,在性能方面会相对比较低。

除了线程安全性方面,Hashtable 和 HashMap 的不同之处还有:

继承的父类:两者都实现了 Map 接口,但 HashMap 继承自 AbstractMap 类,而 Hashtable 继承自 Dictionary 类
遍历方式:HashMap 仅支持 Iterator 的遍历方式,但 Hashtable 实现了 Enumeration 接口,所以支持Iterator和Enumeration两种遍历方式
使用方式:HashMap 允许 null 键和 null 值,Hashtable 不允许 null 键和 null 值
数据结构:HashMap 底层使用“数组+链表+红黑树”,Hashtable 底层使用“数组+链表”
初始容量及扩容方式:HashMap 的默认初始容量为16,每次扩容为原来的2倍;Hashtable 默认初始容量为11,每次扩容为原来的2倍+1。
元素的hash值:HashMap的hash值是重新计算过的,Hashtable直接使用Object的hashCode;

之所以会出现初始容量以及元素hash值计算方式的不同,是因为 HashMap 和 Hashtable 设计时的侧重点不同。Hashtable 的侧重点是哈希结果更加均匀,使得哈希冲突减少,当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而 HashMap 则更加已关注哈希的计算效率问题,在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。 有关 Hashtable 的内容,可以详细阅读:Hashtable原理详解(JDK1.8)

(2)使用Collections.synchronizedMap()方法来获取一个线程安全的集合,底层原理是使用synchronized来保证线程同步。

(3)使用 ConcurrentHashMap 集合。

四、

4、HashMap 链表转换成红黑树:

当数组中某个位置的节点达到8个时,会触发 treeifyBin() 方法将链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点后,链表结构就不存在了。当数组中某个位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发 untreeify() 将红黑树节点转化成链表节点。

HashMap 在进行插入和删除时有可能会触发红黑树的插入平衡调整(balanceInsertion方法)或删除平衡调整(balanceDeletion )方法,调整的方式主要有以下手段:左旋转(rotateLeft方法)、右旋转(rotateRight方法)、改变节点颜色(x.red = false、x.red = true),进行调整的原因是为了维持红黑树的数据结构。

当链表长过长时会转换成红黑树,那能不能使用AVL树替代呢? AVL树是完全平衡二叉树,要求每个结点的左右子树的高度之差的绝对值最多为1,而红黑树通过适当的放低该条件(红黑树限制从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的),以此来减少插入/删除时的平衡调整耗时,从而获取更好的性能,虽然会导致红黑树的查询会比AVL稍慢,但相比插入/删除时获取的时间,这个付出在大多数情况下显然是值得的。

5、HashMap 在 JDK7 和 JDK8 有哪些区别?

① 数据结构:在 JDK7 及之前的版本,HashMap 的数据结构可以看成“数组+链表”,在 JDK8 及之后的版本,数据结构可以看成”数组+链表+红黑树”,当链表的长度超过8时,链表就会转换成红黑树,从而降低时间复杂度(由O(n) 变成了 O(logN)),提高了效率

② 对数据重哈希:JDK8 及之后的版本,对 hash() 方法进行了优化,重新计算 hash 值时,让 hashCode 的高16位参与异或运算,目的是在 table 的 length较小的时候,在进行计算元素存储位置时,也让高位也参与运算。

③ 在 JDK7 及之前的版本,在添加元素的时候,采用头插法,所以在扩容的时候,会导致之前元素相对位置倒置了,在多线程环境下扩容可能造成环形链表而导致死循环的问题。DK1.8之后使用的是尾插法,扩容是不会改变元素的相对位置

④ 扩容时重新计算元素的存储位置的方式:JDK7 及之前的版本重新计算存储位置是直接使用 hash & (table.length-1);JDK8 使用节点的hash值与旧数组长度进行位与运算,如果运算结果为0,表示元素在新数组中的位置不变;否则,则在新数组中的位置下标=原位置+原数组长度。

⑤ JDK7 是先扩容后插入,这就导致无论这次插入是否发生hash冲突都需要进行扩容,但如果这次插入并没有发生Hash冲突的话,那么就会造成一次无效扩容;JDK8是先插入再扩容的,优点是减少这一次无效的扩容,原因就是如果这次插入没有发生Hash冲突的话,那么其实就不会造成扩容

6、线程不安全的体现?如何变成线程安全:

无论在 JDK7 还是 JDK8 的版本中,HashMap 都是线程不安全的,HashMap 的线程不安全主要体现在以下两个方面:

在JDK7及以前的版本,表现为在多线程环境下进行扩容,由于采用头插法,位于同一索引位置的节点顺序会反掉,导致可能出现死循环的情况
在JDK8及以后的版本,表现为在多线程环境下添加元素,可能会出现数据丢失的情况

如果想使用线程安全的 Map 容器,可以使用以下几种方式:

(1)使用线程安全的 Hashtable,它底层的每个方法都使用了 synchronized 保证线程同步,所以每次都锁住整张表,在性能方面会相对比较低。

除了线程安全性方面,Hashtable 和 HashMap 的不同之处还有:

继承的父类:两者都实现了 Map 接口,但 HashMap 继承自 AbstractMap 类,而 Hashtable 继承自 Dictionary 类
遍历方式:HashMap 仅支持 Iterator 的遍历方式,但 Hashtable 实现了 Enumeration 接口,所以支持Iterator和Enumeration两种遍历方式
使用方式:HashMap 允许 null 键和 null 值,Hashtable 不允许 null 键和 null 值
数据结构:HashMap 底层使用“数组+链表+红黑树”,Hashtable 底层使用“数组+链表”
初始容量及扩容方式:HashMap 的默认初始容量为16,每次扩容为原来的2倍;Hashtable 默认初始容量为11,每次扩容为原来的2倍+1。
元素的hash值:HashMap的hash值是重新计算过的,Hashtable直接使用Object的hashCode;

之所以会出现初始容量以及元素hash值计算方式的不同,是因为 HashMap 和 Hashtable 设计时的侧重点不同。Hashtable 的侧重点是哈希结果更加均匀,使得哈希冲突减少,当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。而 HashMap 则更加已关注哈希的计算效率问题,在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。 有关 Hashtable 的内容,可以详细阅读:Hashtable原理详解(JDK1.8)

(2)使用Collections.synchronizedMap()方法来获取一个线程安全的集合,底层原理是使用synchronized来保证线程同步。

(3)使用 ConcurrentHashMap 集合。

四、HashMap的应用场景、优点与缺点

业务场景:学生课程成绩管理

假设我们正在开发一个学生成绩管理系统,其中需要存储每个学生的Name、ID和其所选修的课程及对应的成绩。这时,使用HashMap将会很适合来解决这个问题。

import java.util.HashMap;

public class StudentGradesManagement {
    public static void main(String[] args) {
        // 创建学生课程成绩Map
        HashMap<Integer, HashMap<String, Double>> studentsGrades = new HashMap<>();

        // 添加学生1的课程成绩
        HashMap<String, Double> student1Grades = new HashMap<>();
        student1Grades.put("Math", 87.5);
        student1Grades.put("Science", 92.0);
        student1Grades.put("History", 78.5);
        studentsGrades.put(10001, student1Grades);

        // 添加学生2的课程成绩
        HashMap<String, Double> student2Grades = new HashMap<>();
        student2Grades.put("Math", 92.0);
        student2Grades.put("Science", 88.5);
        student2Grades.put("History", 85.0);
        studentsGrades.put(10002, student2Grades);

        // 获取学生1的成绩信息
        HashMap<String, Double> gradesOfStudent1 = studentsGrades.get(10001);
        System.out.println("Grades of Student1: " + gradesOfStudent1);

        // 获取学生1的数学成绩
        double mathGradeOfStudent1 = gradesOfStudent1.get("Math");
        System.out.println("Math grade of Student1: " + mathGradeOfStudent1);
    }
}

在上述代码中,我们创建了一个HashMap并添加了三个键值对。然后,我们通过键”Apple”获取对应的值,并输出结果。接下来,我们添加了一个新的键值对”Grapes”,并打印更新后的HashMap。

灵活性与扩展性:HashMap能够根据需要自动调整内部存储容量的大小。即使数据量增长,它也能够自动扩展以容纳更多的键值对,同时还可以自动收缩以节省内存空间。

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 创建HashMap并添加键值对
        HashMap<String, Integer> hashMap = new HashMap<>();

        // 添加100个键值对
        for (int i = 0; i < 100; i++) {
            hashMap.put("Key" + i, i);
        }

        System.out.println("HashMap Size: " + hashMap.size());
    }
}

 

在上述代码中,我们创建了一个空的HashMap。然后,使用循环添加了100个键值对。由于HashMap具有自动扩展的能力,即使添加了大量的键值对,它也能根据需要调整内部存储容量的大小。

支持多种数据类型:HashMap可以存储各种类型的键和值。这使得它非常适合用于存储特定对象与相关信息之间的映射关系。

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 创建HashMap并添加不同类型的键值对
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("Name", "John Doe");
        hashMap.put("Age", 25);
        hashMap.put("IsStudent", true);

        System.out.println("HashMap: " + hashMap);
    }
}

 

在上述代码中,我们创建了一个HashMap来存储不同类型的键值对。其中,”Name”键对应一个字符串,”Age”键对应一个整数,”IsStudent”键对应一个布尔值。HashMap的灵活性使其适用于存储各种数据类型。

灵活的数据操作:HashMap提供了用于添加、删除和更新键值对的方法,非常方便实用。

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 创建HashMap并添加键值对
        HashMap<String, Integer> hashMap = new HashMap<>();
        hashMap.put("Apple", 50);
        hashMap.put("Banana", 30);
        
        // 更新键为"Apple"的值
        hashMap.put("Apple", 100);
        System.out.println("Updated HashMap: " + hashMap);

        // 删除键为"Banana"的键值对
        hashMap.remove("Banana");
        System.out.println("Final HashMap: " + hashMap);
    }
}

 

在上述代码中,我们创建了一个HashMap并添加了两个键值对。然后,我们通过将键”Apple”的值更新为100来更新HashMap,并输出结果。接下来,我们使用remove()方法删除了键为”Banana”的键值对,并打印最终的HashMap。

HashMap的缺点

除了优点之外,HashMap也存在一些缺点需要注意:

无序性:HashMap不保证元素的顺序,即插入顺序与遍历顺序可能不一致。如果需要有序性,可以选择使用LinkedHashMap类。

import java.util.HashMap;
import java.util.LinkedHashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 使用HashMap存储并打印键值对
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(3, "C");
        hashMap.put(2, "B");
        hashMap.put(1, "A");

        System.out.println("HashMap: " + hashMap);

        // 使用LinkedHashMap存储并打印键值对(保持插入顺序)
        LinkedHashMap<Integer, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put(3, "C");
        linkedHashMap.put(2, "B");
        linkedHashMap.put(1, "A");

        System.out.println("LinkedHashMap: " + linkedHashMap);
    }
}

 

分析:

这段代码演示了使用HashMap和LinkedHashMap来存储键值对并打印它们的结果。

首先,我们创建了一个HashMap,并使用put()方法向其中添加三个键值对,键分别为3、2和1,对应的值分别为”C”、“B”和”A”。由于HashMap不保证顺序,输出时键值对的顺序可能与插入顺序不同。在当前代码片段中,HashMap打印的结果是:{1=A, 2=B, 3=C}

然后,我们创建了一个LinkedHashMap,并使用put()方法添加相同的三个键值对,这里的顺序与上述HashMap相同。不同的是,LinkedHashMap可以保持插入顺序。因此,当打印LinkedHashMap时,键值对的顺序与插入顺序完全一致:{3=C, 2=B, 1=A}

通过这个示例,我们可以观察到HashMap的输出是无序的,而LinkedHashMap保持了元素的插入顺序。如果需要保持有序性,可以选择使用LinkedHashMap。

不适合频繁删除和插入操作:在大量元素的情况下,频繁进行删除和插入操作会引发HashMap的重新散列过程,从而导致性能下降。

 

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 创建HashMap存储命令和对应的执行结果
        HashMap<String, String> commandResultMap = new HashMap<>();
        commandResultMap.put("command1", "result1");
        commandResultMap.put("command2", "result2");
        commandResultMap.put("command3", "result3");

        System.out.println("初始HashMap:" + commandResultMap);

        // 删除对应的命令和执行结果
        commandResultMap.remove("command2");
        System.out.println("删除command2之后的HashMap:" + commandResultMap);

        // 添加新的命令和执行结果
        commandResultMap.put("command4", "result4");
        System.out.println("添加command4之后的HashMap:" + commandResultMap);
    }
}

在上述代码中,我们创建了一个HashMap来存储命令和对应的执行结果。首先,我们删除了”command2″键及其对应的值,并输出删除后的HashMap。然后,我们添加了新的”command4″键及其对应的值,并输出添加之后的HashMap。可以观察到,在频繁进行删除和插入操作时,HashMap会经历多次重新散列的过程。

较高的内存消耗:由于存储了桶数组和链表结构来解决散列冲突,HashMap需要分配额外的内存空间。因此,在内存资源受限的情况下,需要注意HashMap的内存开销。

import java.util.HashMap;

public class HashMapExample {
    public static void main(String[] args) {
        // 创建一个较大的HashMap
        HashMap<Integer, String> hashMap = new HashMap<>();
        for (int i = 0; i < 1000000; i++) {
            hashMap.put(i, "Value" + i);
        }

        System.out.println("HashMap 已存储了1000000个键值对");
    }
}

 

在上述代码中,我们创建了一个包含1000000个键值对的HashMap。由于集合较大,会消耗大量的内存空间,特别是对于内存有限的环境来说,可能会导致内存溢出的问题。

综上所述,除了HashMap的许多优点之外,我们也需要注意其无序性、不适合频繁删除和插入操作以及较高的内存消耗。为了解决其中的问题,我们可以选择使用LinkedHashMap保持插入顺序,或者考虑其他数据结构来满足特定业务需求。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容