1 / 1
HashMap 的底层数据结构
1 / 1
HashMap 的底层数据结构是一个数组和链表或红黑树。具体来说,HashMap 内部维护了一个数组 table,数组中的每个元素称为桶(bucket),每个桶里存放着一个链表或红黑树。
当我们往 HashMap 中添加一个键值对时,首先会根据键的哈希值计算出该键值对在数组中的索引位置。如果该索引位置上已经有链表或红黑树存在,则遍历链表或红黑树,查找是否已经存在相同的键,如果存在则更新对应的值,否则将该键值对插入到链表或红黑树的头部。如果该索引位置上没有链表或红黑树存在,则创建一个新的链表或红黑树,并将键值对插入到头部。
当我们从 HashMap 中获取一个键的值时,首先也会根据键的哈希值计算出该键值对在数组中的索引位置,然后遍历链表或红黑树,查找是否存在该键,如果存在则返回对应的值,否则返回 null。
HashMap 内部使用链表或红黑树的数据结构是为了解决哈希冲突的问题。当哈希冲突比较少时,使用链表就可以满足需求;当哈希冲突比较多时,使用红黑树可以提高查询效率。
1 / 1
为什么使用“数组+链表”
1 / 1
使用"数组+链表"的主要原因是为了解决哈希冲突的问题。哈希函数将键映射到桶的位置时,不同的键可能会映射到同一个桶的位置,这就是哈希冲突。
当哈希冲突发生时,HashMap 使用链表将多个键值对存储在同一个桶中,可以避免将每个键值对都存储在不同的桶中,从而节省空间。同时,使用链表可以在桶中快速插入和删除键值对。
但是,当链表长度过长时,遍历链表的时间复杂度会变高,因此,为了提高性能,JDK1.8 中引入了红黑树。当链表长度达到一定阈值时,将链表转化为红黑树,可以大大提高查找键值对的效率。同时,当红黑树节点数量过少时,又会将红黑树转化为链表,避免过多的空间浪费。
因此,使用"数组+链表(或红黑树)"的数据结构,可以兼顾空间效率和时间效率,有效地解决了哈希冲突的问题。
1 / 1
为什么要改成“数组+链表+红黑树”
1 / 1
在 JDK 1.8 之前的版本中,HashMap 使用的是"数组+链表"的数据结构来实现。但是,在某些情况下,当哈希冲突较为严重时,使用链表会导致查询效率变低。这时候,如果能够将链表转化为平衡二叉树,可以大大提高查询效率。因此,在 JDK 1.8 中,HashMap 的实现被改进,采用了"数组+链表+红黑树"的数据结构来实现。
当桶中的元素较多时,HashMap 会将链表转化为红黑树。这样,当需要查找元素时,就可以采用红黑树的高效查找方式,而不是简单地遍历整个链表。由于红黑树的时间复杂度为 O(log n),所以在一些场景下,使用"数组+链表+红黑树"的数据结构可以提高 HashMap 的查询效率。
总的来说,"数组+链表+红黑树"的数据结构可以更好地平衡空间效率和时间效率,并且能够应对哈希冲突的各种情况,使 HashMap 的性能更加稳定和高效。
1 / 1
那在什么时候用链表?什么时候用红黑树?
1 / 1
在 HashMap 中,元素存储的数据结构是链表和红黑树。具体使用哪种数据结构取决于桶中元素的数量。
当桶中的元素较少时,HashMap 会使用链表存储元素。因为当元素数量较少时,遍历链表的时间复杂度是比较低的,这时使用链表能够保证性能和空间的平衡。
当桶中的元素较多时,HashMap 会使用红黑树存储元素。红黑树的时间复杂度为 O(log n),在查找元素时效率更高。而且当元素数量很大时,使用链表会使桶中的元素过于分散,导致遍历时间变长,因此使用红黑树可以更好地平衡时间和空间效率。
在 JDK 1.8 中,HashMap 会将链表转化为红黑树的阈值为 8,也就是说,当一个桶中的元素数量超过 8 时,会将链表转化为红黑树。当元素数量少于等于 8 时,HashMap 会使用链表存储元素,以保证查询效率和空间利用率的平衡。
1 / 1
为什么链表转红黑树的阈值是8?
1 / 1
在 HashMap 中,链表转红黑树的阈值是 8,是经过多次实验得出的一个最优值。具体来说,阈值的设定需要考虑以下两个方面的因素:
-
时间复杂度:当桶中元素数量很少时,使用链表进行元素存储和查找的时间复杂度是很低的,而转化为红黑树的时间复杂度会稍微高一些。因此,如果阈值设置得太小,就会导致过多的链表转化为红黑树,从而增加查找和插入的时间复杂度。
-
空间复杂度:当桶中元素数量较多时,使用链表进行元素存储的空间复杂度会相对较高,而使用红黑树可以降低空间的使用率。因此,如果阈值设置得太大,就会导致较多的元素使用链表进行存储,从而浪费较多的空间。
综合考虑时间复杂度和空间复杂度的因素,经过实验测试,阈值设置为 8 是一个比较优秀的选择,可以在保证时间复杂度和空间复杂度的平衡的同时,提高查询和插入的效率。
1 / 1
那为什么转回链表节点是用的6而不是复用8?
1 / 1
当哈希表中元素的数量减少时,如果桶中的元素数量小于等于 6,HashMap 会将红黑树转化为链表。这个阈值 6 是在 JDK 1.8 中对 HashMap 进行优化时设置的。
有人可能会觉得阈值 6 不应该和阈值 8 不一样,因为它们都是红黑树转链表的阈值。但是,在哈希表中,当元素数量变小时,由于空间占用变小,链表存储元素的效率就会变高。而转化为红黑树的主要目的是为了提高查找效率,当元素数量减少时,恢复为链表可以更好地平衡时间和空间的利用。
此外,由于红黑树在内存中占用的空间比链表更大,因此,当元素数量减少时,将红黑树转化为链表可以节省内存空间。如果阈值设置得太大,就会导致过多的元素使用红黑树进行存储,从而浪费较多的内存空间。
因此,为了更好地平衡时间和空间效率,阈值 6 在实际测试中被证明是一个较为合适的值。
1 / 1
HashMap 有哪些重要属性?分别用于做什么的?
1 / 1
在 Java 中,HashMap 是一个非常重要的集合类,用于存储键值对(key-value)的映射关系。以下是 HashMap 中的一些重要属性:
-
table:HashMap 的底层数据结构,存储了所有的键值对。table 是一个数组,每个数组元素称为“桶”,一个桶可以存放多个键值对,如果多个键值对在哈希计算后的索引位置相同,则会被存储在同一个桶中。
-
threshold:表示 HashMap 中的键值对数量达到多少时需要进行扩容操作。默认值为初始容量(capacity)乘以负载因子(load factor)。
-
loadFactor:表示 HashMap 的负载因子,用于计算 threshold 的值。当 HashMap 中的键值对数量超过 threshold 时,就需要对 HashMap 进行扩容。负载因子默认为 0.75,可以通过构造函数进行指定。
-
size:表示 HashMap 中键值对的数量。
-
modCount:用于实现 HashMap 的迭代器(Iterator),表示 HashMap 结构发生变化的次数。每次进行增删操作,modCount 的值就会增加 1。
这些属性在 HashMap 的实现中都扮演了重要的角色,其中,table 用于存储数据,threshold 和 loadFactor 用于控制 HashMap 的扩容,size 用于记录 HashMap 中键值对的数量,modCount 则用于实现 HashMap 的迭代器。了解这些属性可以帮助我们更好地理解 HashMap 的实现原理。
1 / 1
HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?
1 / 1
HashMap 的默认初始容量是 16,这个值是在 JDK 1.8 中设置的。在 JDK 1.7 及之前的版本中,HashMap 的默认初始容量是 16,但负载因子的默认值为 0.75。
HashMap 的容量没有明确的限制,但由于它是基于数组实现的,因此实际上是受到数组长度的限制。在 JDK 1.8 中,HashMap 采用了自适应的方式进行扩容,即在当前容量(capacity)超过了阈值(threshold)时,会自动进行扩容操作。在扩容时,HashMap 会将数组长度翻倍,即 capacity *= 2,并重新计算 threshold 的值。
需要注意的是,如果在构造 HashMap 时指定了初始容量,那么实际的初始容量将会被调整为大于等于这个值的最小的 2 的幂。这是为了保证 HashMap 的容量始终是 2 的幂次方,这样可以提高哈希算法的效率,减少哈希冲突的发生概率。
总之,HashMap 的容量没有明确的限制,但需要注意的是,如果容量过大,会占用过多的内存空间,如果容量过小,则会导致哈希冲突的发生概率增大,从而降低 HashMap 的效率。因此,在使用 HashMap 时,需要根据实际情况进行合理的容量设置。
1 / 1
HashMap 是线程安全的吗?
1 / 1
HashMap 不是线程安全的,它是一种非线程安全的数据结构。多个线程同时对 HashMap 进行读写操作时,可能会导致数据的不一致性或者异常。
例如,在一个线程对 HashMap 进行扩容的过程中,另一个线程进行读取操作,可能会读取到一个不一致的状态。此外,当多个线程同时进行写操作时,可能会导致链表或红黑树的结构被破坏,从而出现链表成环或者链表断裂等情况,进而导致数据的不一致性或者异常。
为了保证线程安全,可以使用 Java 提供的 ConcurrentHashMap 类,它是一种线程安全的哈希表实现,可以支持高并发的读写操作。ConcurrentHashMap 在内部使用了锁分段技术,将整个哈希表分成了多个段(Segment),每个段都拥有自己的锁,不同的段之间可以同时进行读写操作,从而提高了并发性能。
除此之外,也可以使用 Collections.synchronizedMap() 方法对 HashMap 进行包装,得到一个线程安全的 Map 实例。但需要注意的是,使用该方法包装 HashMap 会对所有的读写操作进行同步,可能会导致性能的下降,因此建议在多线程环境下使用 ConcurrentHashMap。
1 / 1
LinkedHashMap 和 TreeMap 排序的区别
1 / 1
LinkedHashMap 和 TreeMap 都是基于红黑树实现的有序 Map,它们都可以按照键的自然顺序或者自定义顺序进行排序,但它们的实现方式略有不同。
LinkedHashMap 是基于哈希表和双向链表实现的有序 Map。它内部维护了一个双向链表,用于维护插入顺序或者访问顺序。在插入元素时,LinkedHashMap 会将元素插入到链表的尾部,从而保证元素的插入顺序。在访问元素时,LinkedHashMap 会将被访问的元素移动到链表的尾部,从而保证元素的访问顺序。由于 LinkedHashMap 同时使用了哈希表和链表,因此它既能够快速查找元素,又能够保持元素的插入顺序或者访问顺序。
TreeMap 则是基于红黑树实现的有序 Map。它内部维护了一棵红黑树,用于维护元素的顺序。在插入元素时,TreeMap 会根据键的比较结果,将元素插入到合适的位置上,从而保证元素的排序。在查询元素时,TreeMap 会利用红黑树的特性,快速地定位到目标元素的位置,从而实现快速的查找操作。
LinkedHashMap 和 TreeMap 的主要区别在于它们维护元素顺序的方式不同。LinkedHashMap 通过双向链表维护插入顺序或者访问顺序,而 TreeMap 则是通过红黑树维护元素的排序。由于红黑树的高度始终保持在一个较小的范围内,因此 TreeMap 的查找操作通常比 LinkedHashMap 更快。但 LinkedHashMap 可以更好地支持 LRU 缓存等场景,因为它可以很方便地维护元素的访问顺序。
1 / 1
HashMap 和 Hashtable 的区别?
1 / 1
HashMap 和 Hashtable 都是 Java 中的 Map 实现,它们都可以用于存储键值对,但它们之间也存在一些区别。
-
线程安全性:Hashtable 是线程安全的,而 HashMap 不是。Hashtable 的所有公共方法都是同步的,因此多个线程可以安全地同时访问 Hashtable。而 HashMap 不是线程安全的,如果多个线程同时访问 HashMap,可能会导致数据不一致或者出现其他错误。
-
Null 值:Hashtable 不允许键或值为 null,否则会抛出 NullPointerException。而 HashMap 允许键或值为 null,但是需要注意的是,如果键为 null,它只能被映射到一个桶中,因此 HashMap 中只能有一个键为 null 的键值对。
-
迭代顺序:Hashtable 迭代元素的顺序是随机的,而 HashMap 迭代元素的顺序是不确定的,它是基于哈希算法来确定的。
-
继承关系:Hashtable 是 Dictionary 类的子类,而 HashMap 是 AbstractMap 类的子类。
-
性能:HashMap 的性能通常要比 Hashtable 好,因为 HashMap 不是线程安全的,因此不需要进行同步操作,而 Hashtable 需要进行同步操作,因此性能上会有一些损失。
综上所述,如果需要线程安全的 Map,可以使用 Hashtable;如果不需要线程安全的 Map,可以使用 HashMap。另外,由于 HashMap 的性能通常要比 Hashtable 好,因此在大多数情况下,建议使用 HashMap。
1 / 1
ArrayList 和 Vector 的区别。
1 / 1
ArrayList 和 Vector 都是 Java 中的 List 实现,它们都可以用于存储一组对象,并且可以根据索引值快速访问其中的元素。它们之间的区别如下:
-
线程安全性:Vector 是线程安全的,而 ArrayList 不是。Vector 的所有公共方法都是同步的,因此多个线程可以安全地同时访问 Vector。而 ArrayList 不是线程安全的,如果多个线程同时访问 ArrayList,可能会导致数据不一致或者出现其他错误。
-
扩容方式:Vector 扩容时默认增加一倍,而 ArrayList 扩容时默认增加一半。Vector 在扩容时会申请一块新的内存,并将原有的元素全部复制到新的内存空间中,这样可能会导致一些性能损失。而 ArrayList 在扩容时也会申请一块新的内存,但它只会复制一半的元素到新的内存空间中,因此可能会有更好的性能表现。
-
初始大小:Vector 的默认初始大小为 10,而 ArrayList 的默认初始大小为 0。在实际使用中,ArrayList 会根据实际需要动态地增加其容量,因此初始大小不是非常重要。
-
继承关系:Vector 是 AbstractList 类的子类,而 ArrayList 是 AbstractList 类的子类。
综上所述,如果需要线程安全的 List,可以使用 Vector;如果不需要线程安全的 List,可以使用 ArrayList。由于 ArrayList 的性能通常要比 Vector 好,因此在大多数情况下,建议使用 ArrayList。
1 / 1
ArrayList 和 LinkedList 的区别?
1 / 1
ArrayList 和 LinkedList 都是 Java 中的 List 实现,它们都可以用于存储一组对象,并且可以根据索引值快速访问其中的元素,但它们之间也存在一些区别。
-
内部数据结构:ArrayList 是基于数组实现的,而 LinkedList 是基于双向链表实现的。因此,在随机访问时,ArrayList 的性能更好,而在插入或删除元素时,LinkedList 的性能更好。
-
插入和删除操作的性能:在插入或删除元素时,LinkedList 的性能比 ArrayList 更好,因为在 LinkedList 中只需要修改指针即可,而在 ArrayList 中需要移动元素。
-
随机访问的性能:在随机访问时,ArrayList 的性能比 LinkedList 更好,因为 ArrayList 中的元素是连续存储的,可以通过索引值快速访问,而 LinkedList 中的元素是通过指针连接的,需要遍历链表才能访问。
-
内存占用:在存储相同数量的元素时,LinkedList 占用的内存比 ArrayList 更多,因为 LinkedList 需要存储指向前后节点的指针。
-
迭代器的性能:在迭代元素时,LinkedList 的性能比 ArrayList 更好,因为在 LinkedList 中,通过迭代器遍历元素比通过索引值访问元素更快。
综上所述,如果需要频繁地插入或删除元素,可以使用 LinkedList;如果需要频繁地随机访问元素,可以使用 ArrayList。另外,由于 ArrayList 的内存占用更少,因此在存储大量数据时,建议使用 ArrayList。
1 / 1
Collection 与 Collections的区别
1 / 1
Collection 和 Collections 是两个不同的概念。
Collection 是 Java 集合框架的根接口,用于表示一组对象,提供了一些通用的方法,如添加、删除、遍历等。
Collections 是一个包装类,提供了一系列静态方法,用于操作集合,如排序、查找、复制等。它提供的方法都是静态的,可以直接通过类名调用,例如 Collections.sort(list)。
因此,Collection 表示一组对象的接口,而 Collections 是对集合操作的工具类。它们之间的关系是 Collection 是所有集合类的顶级接口,而 Collections 是对 Collection 接口的实现提供了一些算法方法的辅助类。