Java基础(二)Java集合

本文详细解析了Java中的List、Set、Map三种集合类型及其底层数据结构,讨论了线程安全问题,比较了HashSet、LinkedHashSet、TreeSet、HashMap、HashTable和ConcurrentHashMap的异同,并剖析了HashMap的实现细节以及为何长度选择为2的幂次方。
摘要由CSDN通过智能技术生成

1.List、Set、Map三者的区别? 三者底层的数据结构?

List、Set、Map 是 Java 中常用的集合类型,它们在数据结构和用途上有所不同。

List(列表):

  • List 是一个有序的集合,允许重复元素。
  • 可以通过索引来访问列表中的元素,元素的位置是有意义的。
  • 常见的实现类包括 ArrayList、LinkedList、Vector 等。
  • ArrayList 底层基于数组实现,LinkedList 底层基于链表实现,Vector 也是基于数组实现,但线程安全。

Set(集合):

  • Set 是一个不允许重复元素的集合,保证元素的唯一性。
  • Set 不保证元素的顺序,元素的排列顺序可能是不确定的。
  • 常见的实现类包括 HashSet、TreeSet、LinkedHashSet 等。
  • HashSet 基于哈希表实现,TreeSet 基于红黑树实现,LinkedHashSet 继承自 HashSet,但是内部使用 LinkedHashMap 来保存元素,保证了元素的插入顺序。

Map(映射):

  • Map 是一种键值对的集合,每个键都映射到唯一的值。
  • Map 中的键是唯一的,但值可以重复。
  • 常见的实现类包括 HashMap、TreeMap、LinkedHashMap、Hashtable 等。
  • HashMap 基于哈希表实现,TreeMap 基于红黑树实现,LinkedHashMap 继承自 HashMap,但是内部使用双向链表维护元素的插入顺序,Hashtable 是线程安全的,但性能比 HashMap 差。

总结一下底层数据结构:

  • List:ArrayList(数组)、LinkedList(双向链表)、Vector(数组)
  • Set:HashSet(哈希表)、TreeSet(红黑树)、LinkedHashSet(哈希表+链表)
  • Map:HashMap(哈希表)、TreeMap(红黑树)、LinkedHashMap(哈希表+链表)、Hashtable(哈希表)

2.有哪些集合是线程不安全的,怎么解决?

在 Java 中,许多集合类都不是线程安全的。这些线程不安全的集合包括:

  • ArrayList
  • LinkedList
  • HashSet
  • HashMap
  • TreeSet
  • TreeMap
  • LinkedHashSet
  • LinkedHashMap
  1. 使用线程安全的集合类:Java 提供了对应的线程安全集合类,如 Vector、Hashtable、ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet 等。这些集合类在并发访问时能够保证线程安全。
  2. 使用同步集合包装器:通过 Collections.synchronizedXXX() 方法可以将线程不安全的集合转换为线程安全的集合。例如
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<>());
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
  1. 手动同步:在使用线程不安全的集合时,通过手动在操作集合时添加同步块或使用同步方法来保证线程安全。例如:
List<String> list = new ArrayList<>();
synchronized (list) {
    // 对 list 进行操作
}
  1. 使用并发集合类:Java 并发包提供了一系列高效的并发集合类,如 ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet 等。这些集合类针对特定的并发场景进行了优化,能够在高并发情况下提供较好的性能。

3.比较HashSet、LinkedHashSet 和 TreeSet 三者的异同?

HashSet、LinkedHashSet 和 TreeSet 都是 Java 中的集合实现,它们在存储和操作元素时有所不同。

HashSet:

  • HashSet 是基于哈希表实现的集合,不保证元素的顺序,元素存储的顺序可能是不确定的。
  • HashSet 允许存储 null 元素。
  • HashSet 提供了常数时间复杂度的添加、删除、包含等操作。
  • 适用于需要快速查找元素,不关心元素的顺序的场景。

LinkedHashSet:

  • LinkedHashSet 继承自 HashSet,内部使用 LinkedHashMap 来维护元素的插入顺序。
  • LinkedHashSet 保留了元素插入的顺序,因此遍历时元素的顺序是按照插入顺序排列的。
  • LinkedHashSet 允许存储 null 元素。
  • 添加、删除、包含等操作的时间复杂度与 HashSet 相同,但在遍历时会略慢一些。

TreeSet:

  • TreeSet 是基于红黑树(自平衡二叉查找树)实现的有序集合,元素根据自然顺序或者比较器顺序进行排序。
  • TreeSet 保证了元素的有序性,因此遍历时元素是有序的。
  • TreeSet 不允许存储 null 元素。
  • 添加、删除、包含等操作的时间复杂度与树的高度相关,通常情况下是对数时间复杂度,效率较高。

异同点总结:

  • 相同点:都实现了 Set 接口,都不允许存储重复元素。
  • 不同点:在存储顺序和底层实现上有所不同。HashSet 是基于哈希表实现的无序集合;LinkedHashSet 是基于哈希表和链表实现的有序集合;TreeSet 是基于红黑树实现的有序集合。

4.HashMap 和 HashTable 的区别? HashMap 和 HashSet 的区别? HashMap 和 TreeMap 的区别?

HashMap 和 HashTable 的区别:

  • **线程安全性:**HashMap 是非线程安全的,而 HashTable 是线程安全的。
  • **Null 键和值:**HashMap 允许一个键为 null,但只允许一个值为 null;而 HashTable 不允许键或值为 null。
  • **迭代器:**HashMap 的迭代器是 fail-fast 的,而 HashTable 的迭代器是 fail-safe 的。
  • **性能:**HashMap 的性能通常比 HashTable 要好,因为 HashMap 不进行同步,可以更快地执行。
  • **扩容机制:**HashMap 的扩容机制是在数组容量超过一定阈值时扩容,而 HashTable 在达到装载因子时会自动进行扩容操作。

HashMap 和 HashSet 的区别

  • **存储结构:**HashMap 是键值对的集合,而 HashSet 是唯一值的集合。
  • **底层实现:**HashMap 基于哈希表实现,HashSet 则是基于 HashMap 实现的,HashSet 中的元素实际上存储为 HashMap 中的键,而值为一个固定的 Object 对象。
  • **元素重复:**HashMap 中键可以重复,但值不能重复;而 HashSet 中的元素都是唯一的。

HashMap 和 TreeMap 的区别:

  • **存储结构:**HashMap 是无序的键值对集合,而 TreeMap 是有序的键值对集合,按照键的自然顺序或者比较器的顺序排序。
  • **性能:**HashMap 的插入、删除、查找等操作的时间复杂度是 O(1),而 TreeMap 的时间复杂度是 O(log n)。
  • **存储方式:**HashMap 是基于哈希表实现的,而 TreeMap 是基于红黑树实现的。
  • **有序性:**HashMap 中的元素是无序的,而 TreeMap 中的元素是有序的。

5.HashMap 的底层实现?

HashMap 的底层实现是基于哈希表(Hash Table)的。在 Java 中,HashMap 是由数组和链表(或红黑树)组成的数据结构,用于存储键值对。下面是 HashMap 的基本结构和实现原理:

数组:

  • HashMap 内部维护了一个数组,称为哈希桶数组(bucket array),用于存储实际的键值对元素。
  • 数组的长度称为容量(capacity),通常会初始化为一个较大的值,并且是 2 的幂次方,如 16、32、64 等。
  • 数组中的每个元素称为桶(bucket),每个桶可以存储一个链表或红黑树,用于解决哈希冲突。

哈希函数:

  • 当添加一个键值对时,首先会对键进行哈希计算,得到键的哈希码。
  • HashMap 使用一个哈希函数将键的哈希码映射到数组的某个位置,从而确定存储位置。
  • 哈希函数通常会对哈希码进行一些处理,如取模运算(hash & (length - 1)),来确保键的分布均匀。

链表或红黑树:

  • 如果发生哈希冲突(即多个键映射到了数组的同一个位置),则会将冲突的键值对存储在同一个桶中。
  • 初始时,通常使用链表来存储冲突的键值对,如果同一个桶中的链表长度超过了阈值(JDK8 中默认为 8),则将链表转换为红黑树,以提高查找效率。

扩容和重新哈希:

  • 当添加元素导致数组中的桶被占满时,HashMap 将会进行扩容操作。
  • 扩容操作会创建一个新的更大的数组,然后将所有元素重新哈希到新数组中。
  • 扩容操作的过程比较耗时,但由于哈希表的性质,扩容操作的平均时间复杂度是 O(1)。

哈希表的性能:

  • HashMap 的添加、删除、查找等操作的时间复杂度是 O(1),即平均情况下是常数时间复杂度。
  • 但在最坏情况下,即所有键映射到了同一个桶,哈希表的性能会退化为 O(n),因此要尽量避免哈希冲突的发生。

6.HashMap 的长度为什么是 2 的幂次方?

HashMap 的长度选择为 2 的幂次方是为了在计算哈希值映射到数组位置时采用位运算,这样可以提高效率。具体来说,长度为 2 的幂次方有以下几个优势:

  • **利用位运算:**当长度为 2 的幂次方时,计算哈希值映射到数组位置可以用位运算来代替取模运算,如 (hash & (length - 1))。而位运算相比取模运算的性能更高,因此可以提高速度。
  • **均匀分布:**哈希值的低几位可以保留原始哈希值的信息,而高位被舍弃,因此,对于不同的哈希值,它们在低几位的概率分布是均匀的。这样可以减少哈希冲突的发生,提高散列均匀性。
  • **更高的性能:**HashMap 在插入、删除和查找操作时,需要计算元素的哈希值并根据长度计算存储位置。采用 2 的幂次方作为长度,可以通过位运算实现快速的哈希值映射,从而提高了性能。
  • **扩容时的处理:**HashMap 在扩容时会将容量扩大到原来的两倍,而容量必须是 2 的幂次方。这样可以保证在扩容时不需要重新计算哈希值的位置,只需将原来位置的元素分配到新数组中即可,提高了扩容时的效率。

因此,选择长度为 2 的幂次方作为 HashMap 的长度是为了利用位运算的高效性和保证均匀分布,从而提高性能和效率。

7.ConcurrentHashMap 和 HashTable 的区别?

线程安全性:

  • ConcurrentHashMap 使用了分段锁(Segment)的技术,将整个哈希表分为多个段,每个段独立加锁,不同的线程可以同时访问不同的段,从而提高了并发性能。
  • HashTable 使用一个全局锁来保证线程安全,即在进行任何修改操作时都需要获得这个全局锁,因此在高并发情况下性能较差。

容量调整:

  • ConcurrentHashMap 在容量不够时可以对其进行动态调整,而不需要锁定整个表,只需锁定一个段。这种分段锁的机制使得 ConcurrentHashMap 在并发环境下扩容的效率更高。
  • HashTable 在进行扩容时需要锁定整个表,这意味着其他线程在这段时间内无法对表进行操作,性能较差。

迭代器:

  • ConcurrentHashMap 的迭代器是弱一致性的(weakly consistent),允许在迭代过程中进行修改操作,但不保证能够看到修改的结果。这种设计可以提高并发性能,但迭代器的行为可能是不确定的。
  • HashTable 的迭代器是强一致性的(fail-fast),在迭代过程中如果有其他线程对表进行了修改,则会抛出 ConcurrentModificationException 异常。

Null 值:

  • ConcurrentHashMap 允许键和值为 null。
  • HashTable 不允许键或值为 null,如果插入了 null 值或 null 键,会抛出 NullPointerException 异常。 性能:
  • ConcurrentHashMap 的性能通常优于 HashTable,在高并发环境下表现更好。
  • ConcurrentHashMap 的分段锁机制使得多个线程可以同时读取和写入不同的段,而 HashTable 在进行修改操作时需要锁定整个表,因此性能较差。

综上所述,ConcurrentHashMap 和 HashTable 在实现和性能上有很大的区别。ConcurrentHashMap 使用了分段锁机制来提高并发性能,并且具有更灵活的容量调整机制和更好的迭代器行为,因此在大多数情况下推荐使用 ConcurrentHashMap 来代替 HashTable。

8.ConcurrentHashMap 线程安全的具体实现方式底层具体实现

ConcurrentHashMap 在实现线程安全性时采用了一种分段锁(Segment Locking)的机制。这种机制将整个哈希表分为多个段(Segment),每个段拥有自己的锁,不同的线程可以同时访问不同的段,从而提高了并发性能。下面是 ConcurrentHashMap 的线程安全实现方式的详细介绍:

分段锁机制:

  • ConcurrentHashMap 内部维护了一个 Segment 数组,Segment 是 ConcurrentHashMap 的内部静态类,它类似于一个小的 HashMap,包含一个哈希桶数组和一些用于控制并发的属性。
  • 每个 Segment 都是一个独立的哈希表,它拥有自己的锁,不同的线程可以同时访问不同的 Segment,从而提高了并发性能。
  • 当需要进行修改操作时,只需要锁定对应的 Segment,而不需要锁定整个 ConcurrentHashMap。

Segment 数组:

  • ConcurrentHashMap 内部维护了一个 Segment 数组,数组的长度通常是 2 的幂次方,可以通过参数指定,默认为 16。
  • 每个 Segment 都是一个独立的哈希表,它包含一个哈希桶数组和一些用于控制并发的属性,如锁、计数器等。

键值对的存储:

  • 当需要插入或查找一个键值对时,首先计算键的哈希值,然后根据哈希值找到对应的 Segment,最后在该 Segment 中进行操作。
  • 不同的键值对可能被存储在不同的 Segment 中,不同的线程可以同时操作不同的 Segment,提高了并发性能。

操作的并发控制:

  • Segment 中的操作通常采用 synchronized 关键字进行并发控制,保证线程安全性。
  • 在 JDK 8 中,对 ConcurrentHashMap 进行了优化,引入了 CAS(Compare and Swap)操作和 synchronized 关键字的混合使用,以提高并发性能。

扩容操作:

  • ConcurrentHashMap 在需要扩容时,只需要扩容对应的 Segment,而不需要扩容整个 ConcurrentHashMap。
  • 扩容操作会将 Segment 的大小扩大为原来的两倍,然后将原来的元素重新分配到新的 Segment 中。

综上所述,ConcurrentHashMap 通过分段锁机制来实现线程安全性,将整个哈希表分为多个段,每个段拥有自己的锁,从而实现了更细粒度的并发控制。这种机制提高了 ConcurrentHashMap 在高并发环境下的性能,并且能够动态调整容量,提高了效率。

  • 29
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值