在Java的集合框架中,Map为双列集合,在Map中的元素是成对以HashMap
、Hashtable
、TreeMap
、LinkedHashMap
和ConcurrentHashMap
。
HashMap 是 Java 中一个非常常用的集合类,可以通过key的 HashCode 值来快速访问值,具有很快的访问速度,并且由于存储的键值对是无序的,插入的顺序并不会影响到查询的结果。
在 HashMap 中,允许存储的键为 null,value也可以为null,但只能有一个key 为 null。键值对是通过键的 HashCode 值来存储的,如果多个键的 HashCode 值相同,它们就会被存储在同一个桶中,也就是所谓的哈希桶。每个桶是一个单向链表或双向链表,存储了所有哈希值相同的键值对。当我们向 HashMap 中添加一个键值对时,它会根据键的 HashCode 值计算出一个哈希桶的索引,然后将键值对存储在对应的哈希桶中,并将哈希桶中的链表或双向链表更新为当前节点后面的节点。
HashMap 继承自 AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。
Java 中的 HashMap 底层数据结构主要由数组
、链表
、红黑树
三部分组成,jdk 1.8之前采用数组+链表
,jdk 1.8 采用数组+链表+红黑树的方式。
哈希码(Hash Code)是用于计算键的哈希值,以便在存储和查询时使用的。哈希码的计算方式是通过将键的哈希值存储在数组中的一个位置上,这个位置称为哈希码值。当我们向 HashMap 中添加键值对时,HashMap 会使用键的哈希值来计算哈希码值,并将键值对存储在对应的哈希码值的位置上。当我们查询 HashMap 中的键值对时,HashMap 会使用键的哈希码值来计算哈希码值的位置,并在该位置上遍历所有的键值对,直到找到对应的键值对。
哈希码的重要性在于,它可以帮助我们快速地定位键值对在数组中的位置,从而提高 HashMap 的存储和查询效率。如果两个键的哈希码不同,它们会被分配到不同的桶中;如果哈希码相同,就会发生哈希冲突,需要进一步处理。
为了解决哈希冲突,HashMap 使用了链地址法,即将哈希桶转换成一个链表或红黑树,存储所有哈希值相同的键值对。当多个键的哈希码值相同时,HashMap 会将它们存储在同一个桶中,并将它们转换成链表或红黑树的形式。当我们查询键值对时,HashMap 会遍历该桶中的所有键值对,直到找到对应的键值对。如果该桶中没有对应的键值对,则说明该键值对不存在,HashMap 会返回 null。
HashMap 内部使用了数组和链表(或红黑树)来存储键值对,当多个线程同时访问 HashMap 时,可能会出现以下问题:
多线程修改元素出现的不安全
:线程 A 和线程 B 同时修改同一个 HashMap 中的键值对,由于 HashMap 是线程不安全的,当线程 A 修改键值对时,线程 B 也可能在同时修改键值对,导致数据不一致或丢失。多线程并发扩容出现的不安全
:HashMap在达到阈值时需要进行扩容,扩容时需要重新计算Hash值,重新分配存储位置。如果在扩容过程中有多个线程同时进行插入或删除操作,就可能会导致数据结构混乱,还可能会导致Map中链表的尾结点指向头结点造成死循环。在创建 HashMap 对象时,需要传入一个整数参数,表示 HashMap 中桶的数量,默认情况下,该参数为 16。初始化容量的大小会直接影响到 HashMap 的空间利用率和性能。如果初始化容量太小,可能会导致频繁的扩容操作,从而降低性能;如果初始化容量太大,则会导致空间浪费,并且当需要进行扩容时,需要重新计算数组大小,并重新分配内存等,也会消耗一定的系统资源。
为了提高 HashMap 的查询效率。当桶中链表或红黑树的长度超过一定阈值时,该阈值默认为初始容量的两倍加上负载因子的值,即 2 * initialCapacity + 0.5f。超过这个阈值会自动进行扩容到之前容量的2倍,阈值也为原来的2倍。扩容时,HashMap 会重新分配一个新的桶数组,并将原来的键值对数据复制到新的桶数组中。新的桶数组的大小通常是原来的两倍,以便容纳更多的键值对数据。由于 HashMap 的扩容操作需要重新计算哈希值,因此在扩容时会导致性能下降。为了尽可能减少扩容带来的性能损失,建议在创建 HashMap 实例时,根据实际需要设置合适的容量大小。
在 Java 中的 HashMap 中,加载因子是一个重要的参数,它决定了 HashMap 在初始化时所分配的桶的数量。加载因子的取值范围是 0.75 到 1.0 之间,通常情况下,默认的加载因子是 0.75。
加载因子的作用是控制 HashMap 中桶的数量,当桶的数量达到了加载因子所指定的最大值时,就需要进行扩容操作。加载因子的取值会直接影响到 HashMap 的空间利用率和性能,如果加载因子的取值过小,则会导致频繁的扩容操作,从而降低性能;如果加载因子的取值过大,则会导致空间浪费,并且当需要进行扩容时,需要重新计算数组大小,并重新分配内存等,也会消耗一定的系统资源。
当存储键值对时,先对key的hashCode值进行比较,如果不同,则认为两个key不相等,会直接插入集合中。如果两个key的hashCode值相同,则会调用equals()方法进行比较,如果equals()方法返回true,则认为两个key相等,则会覆盖掉原来的键值对;否则认为两个key不相等,会两个key会放入到同一个哈希桶中。
在 Java 中,HashMap 类使用键的哈希值和键对象的equals方法来实现键的唯一性。equals方法用于比较两个对象是否相等,而hashCode方法用于生成键对象的哈希码。当两个对象通过equals方法比较相等时,它们的hashCode方法应该返回相同的值。如果两个对象的equals()方法返回true,但是它们的hashCode()方法返回的哈希码不同,那么它们就会被存在HashMap的不同桶里,导致HashMap无法正确获取对象。
在 Java 中的 HashMap 类中,键可以是任何实现了 Comparable 接口的对象。最常用的键类型是字符串。这是因为字符串类型的键可以方便地进行比较和排序,并且可以轻松地进行字符串转换。另外,字符串类型的键还可以方便地进行序列化和反序列化操作,从而方便进行数据传输和存储。其他比较常用的类型如:Integer、Long、String、Object等都可以作为key。
Hashtable 是 Java 中的一个同步的键值对存储容器,它实现了 Map 接口。与 HashMap 不同,Hashtable 是线程安全的,它使用 synchronized 关键字将整张散列表加锁的方式来保证多线程访问时的线程安全性,因此它的运行效率非常低。
Hashtable 的key和value都不能为 null,否则会抛出NullPointerException。
HashTable 内部的数据结构主要包括以下几个部分:
TreeMap 是 Java 中的一个基于红黑树的排序 Map 实现,它实现了 NavigableMap 接口,用于存储有序的键值对。由于红黑树的查找效率比链表要慢一些,因此在键值对数量较多的情况下,使用 TreeMap 可能会影响性能。但是,由于 TreeMap 可以保证键值对的有序性,因此在一些需要按照键进行排序的场景下,使用 TreeMap 是比较合适的。
TreeMap的key不能为 null,value可以为null。
TreeMap 内部的数据结构主要包括以下几个部分:
LinkedHashMap 是 Java 中的一个基于双向链表的 Map 实现,它实现了 Map 接口,用于存储键值对,并支持按照插入顺序和访问顺序进行访问。由于 LinkedHashMap 是基于双向链表实现的,因此它的插入、删除、查找等操作的时间复杂度都是 O(1),性能比较优秀。但是,由于双向链表的数据结构比较简单,因此 LinkedHashMap 的可扩展性比一些基于哈希表实现的集合要好。LinkedHashMap 不支持并发访问,如果需要在多线程环境下使用,需要进行同步控制。
LinkedHashMap 的key可以为 null,value可以为null。
LinkedHashMap 内部的数据结构主要包括以下几个部分:
ConcurrentHashMap 是 Java 中常用的一种并发安全的Map实现,它实现了 Map 接口,内部和 HashMap 一样,都是采用了数组 + 链表 + 红黑树的方式来实现。与hashtable不同,该类不依赖于synchronization去保证线程操作的安全,而是利用分段锁(CAS 锁)来保证数据的安全,支持的并发量相对更高。ConcurrentHashMap 插入、删除、查找等操作的时间复杂度都是 O(1),性能也是比较优秀。但是,由于哈希表的数据结构比较复杂,因此 ConcurrentHashMap 的可扩展性相对一些基于链表的实现集合要差。
ConcurrentHashMap 的key和value都不可以为null。
ConcurrentHashMap 内部的数据结构主要包括以下几个部分: