1Java集合框架是什么?说出集合框架的一些优点?

Java集合框架(Collection Framework)是一组用于存储和操作对象的类和接口的集合
集合框架的应用场景非常广泛,具体有List、Set、Map接口及其实现类:

1.1List集合

适用于需要保持元素插入顺序的场景,如学生的成绩列表,或者任何需要有序且允许重复元素的集合。

1.2Set集合

适用于需要存储唯一元素的场景,如学生学号的集合,确保每个学生都有一个唯一的标识。

1.3Map集合

适用于需要通过键快速查找值的场景,如学生名册,其中学号作为键,姓名作为值,可以快速检索每个学生的信息。

1.4优点:




2集合接口的常见实现类

2.1List接口的实现类

2.1.1背记

List接口的实现类有以下几个:

2.1.2理解

2.1.2.1ArrayList:

底层实现:基于动态数组实现。
特点:查询效率高,因为支持快速随机访问;增删效率相对较低,尤其是在列表中间插入或删除元素时,因为需要移动大量元素。
适用场景:适合随机访问元素较多的场景。

2.1.2.2LinkedList:

底层实现:基于双向链表实现。
特点:在列表的两端添加或删除元素的效率高,因为只需要改变链接;查询效率较低,因为需要从头或尾遍历链表。
适用场景:适合频繁在列表两端进行插入和删除操作的场景。

2.1.2.3Vector:

底层实现:基于数组实现,与ArrayList类似,但Vector是线程安全的。
特点:由于同步开销,性能比ArrayList低;提供了线程安全的列表操作。
适用场景:在多线程环境中需要保证线程安全的场景。

2.1.2.4选择

如果需要频繁访问列表中的某个元素,那么ArrayList可能是最佳选择;
如果需要在列表的两端频繁添加或删除元素,那么LinkedList可能更合适。
如果需要在多线程环境中使用List,可以考虑使用Vector,因为它线程安全,尽管它的性能可能不如其他实现类。
如果在多线程环境中需要兼顾性能和安全,那么就需要使用CopyOnWriteArrayList

2.2Set接口的实现类

2.2.1背记

Set接口的实现类有以下几个:

2.2.2理解

Set接口的实现类及其特点具体如下:

2.2.2.1HashSet

基于HashMap实现,由哈希表支持。
无序性:不保证集合的迭代顺序,特别是它不保证该顺序恒久不变。
不允许重复元素:利用hashCode()方法和equals()方法来确保集合中不会有重复元素。
性能优异:因为基于哈希表,所以具有很快的添加、删除和查找速度。
使用示例:当需要创建一个不含重复元素的集合时,通常会选择HashSet。

2.2.2.2LinkedHashSet

继承自HashSet,内部使用链表维护插入顺序。
有序性:元素迭代顺序为它们被插入的顺序,或者它们最后一次被访问的顺序。
不允许重复:与HashSet类似,通过hashCode()和equals()方法确保元素唯一性。
适用场景:在需要保持插入顺序的场景下使用。

2.2.2.3TreeSet

基于红黑树数据结构实现。
有序性:可以确保集合元素处于排序状态,它实现了SortedSet接口。
不允许重复:同样利用hashCode()方法和equals()方法确保无重复元素。
可自定义排序规则:允许用户自定义排序规则,或要求集合内的元素类型实现Comparable接口。
使用示例:当需要对集合元素进行排序时,或需要有序且不重复的元素集合时,应使用TreeSet。

以上是Set接口的三个主要实现类及其特点。
在实际开发中,根据需要选择最适合的实现类非常重要。
例如,如果需要一个能够快速访问的无序集合,则HashSet是一个不错的选择;
如果还需要保留插入顺序,则应该考虑使用LinkedHashSet;
而如果需要集合中的元素始终保持排序状态,则TreeSet是正确的选择。

2.3Map接口的实现类

2.3.1背记

Map接口的实现类主要有以下几个:

2.3.2理解

Map接口的各个实现类在不同维度上的特点如下:

2.3.2.1HashMap

存储结构:基于散列表(哈希表)实现。
排序:无序,不保证映射的迭代顺序。
同步:非线程安全,高效的单线程访问。
性能:通常具有较高的存取效率,但在大量元素时可能出现哈希冲突。
允许空值:允许一个键对应null值,但不允许多个键相同。
典型用途:在没有特殊排序或同步需求时使用。

2.3.2.2LinkedHashMap

存储结构:基于哈希表和双向链表实现。
排序:保持插入顺序或访问顺序(取决于构造参数)。
同步:非线程安全。
性能:相对于HashMap,插入和迭代速度略慢,但仍具有较好的存取效率。
允许空值:允许一个键对应null值,但不允许多个键相同。
典型用途:需要保持元素插入顺序的场景。

2.3.2.3TreeMap

存储结构:基于红黑树(一种自平衡二叉搜索树)实现。
排序:根据键的自然顺序或者自定义比较器进行排序。
同步:非线程安全。
性能:插入、删除和查找操作的时间复杂度为O(log n)。
允许空值:允许一个键对应null值,但不允许多个键相同。
典型用途:需要对键进行排序或有序遍历的场景。

2.3.2.4Hashtable

存储结构:基于散列表实现。
排序:无序,不保证映射的迭代顺序。
同步:线程安全,所有公共操作都是同步的。
性能:由于同步,多线程环境下表现良好,但单线程效率较低。
允许空值:不允许键和值为null。
典型用途:需要线程安全的Map实现。

2.3.2.5ConcurrentHashMap

存储结构:基于分段锁的散列表实现。
排序:无序,不保证映射的迭代顺序。
同步:线程安全,高效并发访问。
性能:在高并发环境下提供比Hashtable更好的性能。
允许空值:允许一个键对应null值,但不允许多个键相同。
典型用途:高并发环境下需要高性能的场景。


3ListMap区别?

3.1背记

3.1.1存储结构

List是单列数据集合,用于存储一系列有序的对象,而Map是双列数据集合,通过键值对(Key-Value)来存储数据。

3.1.2元素唯一性

List允许元素重复,即可以有多个相同的元素存在于列表中。
Map中的键(Key)必须是唯一的,不允许有重复的键存在,但值(Value)可以重复。

3.1.3顺序性

List中的元素是有序的,元素的插入顺序会被保留。
Map中的元素是无序的,它不保证键值对的存储顺序

3.2理解

ListMapJava集合框架中是两种非常常用的数据结构,它们在存储结构、元素唯一性、排序特点和使用场景方面存在区别。
具体分析如下

3.2.1存储结构

List是单列数据集合,用于存储一系列有序的对象。它允许存储重复的元素,并且可以通过索引直接访问元素。
Map是一种关联数组结构,它通过键值对(Key-Value)来存储数据,其中键是唯一的,不能重复,但值可以重复。

3.2.2元素唯一性

List中,元素是可以重复的,即可以有多个相同的元素存在于列表中。
而在Map中,键(Key)必须是唯一的,不允许有重复的键存在。

3.2.3排序特点

List中的元素是有序的,元素的插入顺序会被保留。
Map中的元素是无序的,它不保证键值对的存储顺序。

3.2.4使用场景

List通常用于存储一组相关的对象,如名单、待办事项列表等,需要按照特定的顺序进行访问和处理。
Map则适用于需要通过一个特定键来快速查找、插入或删除值的场景,如字典、数据库表等。

总的来说,List以线性方式存储数据,强调的是元素的顺序和重复性;
Map通过键值对的方式存储数据,强调的是键的唯一性和快速检索。
在实际开发中,选择List还是Map取决于具体的数据结构和应用场景。


4List、Map、Set三个接口,存取元素时,各有什么特点?

4.1背记

4.1.1List


4.1.2Map


4.1.3Set


4.2理解

4.2.1List 接口


4.2.2Set 接口


4.2.3Map 接口


总的来说,List 适合有序存储、允许重复元素的场景;Set 适合保证元素唯一性的场景;Map 适合
键值对存储、根据键快速查找值的场景。开发人员在选择使用时应根据实际需求和数据特点来灵活
选取适合的集合类型。


5为什么Map接口不继承Collection接口?

5.1背记

5.1.1数据结构不同


5.1.2操作方式不同


5.1.3违反接口分离原则

接口的设计应该遵循最小化,不应该把用户不使用的方法塞进同一个接口里,每个接口应该只包含一个职责,即一组相关的操作

5.2理解

5.2.1设计原则

根据接口分离原则,接口应该细化并只提供客户端所需的方法
如果一个接口提供了许多方法,而这些方法对于某些客户端来说是不必要的,那么这些客户端就不得不实现它们不需要的方法

5.2.2设计实践

在设计时,采用多个与特定客户类有关的接口比采用一个通用的接口要好
这是因为不同的客户端可能需要不同的方法集合,通过分离接口可以确保每个客户端只依赖于它实际需要的方法

5.2.3设计问题

如果Map继承了Collection接口,那么所有实现了Map接口的类也将隐式地继承Collection接口的所有方法
这会导致一些问题,比如类的接口定义暴露了过多的行为,内聚程度不够好


6IteratorListIterator之间有什么区别?

6.1背记

IteratorListIterator都是Java集合框架中用于遍历集合的接口

6.1.1使用范围


6.1.2功能


6.1.3操作

ListIterator可以在遍历过程中进行安全的修改操作,如添加、设置和获取列表中的元素。Iterator则没有这些额外的操作。

6.2理解

IteratorListIterator都是用于遍历集合的接口,但它们在遍历方向、修改元素、使用范围以及索引定位方面存在差异。

6.2.1遍历方向


6.2.2修改元素


6.2.3使用范围


6.2.4索引定位

ListIterator可以定位当前索引的位置,通过nextIndex()和previousIndex()方法可以实现。
Iterator没有此功能。

总结来说,ListIterator提供了比Iterator更丰富的功能,尤其是在遍历List时,它能够提供更多的操作和灵活性。
如果只是需要对集合进行简单的前向遍历,Iterator就足够了。
而在处理List集合且需要更多控制时,应当使用ListIterator。
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("3");
list.add("2");
list.add("4");
list.add("5");
System.out.println("正向遍历结果:");

ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
    System.out.println(listIterator.next());

}
System.out.println("反向遍历结果:");

while (listIterator.hasPrevious()) {
    System.out.println(listIterator.previous());
}

System.out.println("插入元素:");

ListIterator<String> listIterator1 = list.listIterator();
listIterator1.add("7");
while (listIterator1.hasPrevious()) {
    System.out.println(listIterator1.previous());

}
System.out.println(list);
System.out.println();

ListIterator<String> listIterator2 = list.listIterator();

System.out.println("替换元素:");

while (listIterator2.hasNext()) {
    String next = listIterator2.next();
    if ("5".equals(next)) {
        listIterator2.set("155");
    }
}
System.out.println(list); 示例代码



7集合框架中的泛型有什么优点?

7.1背记

7.1.1类型安全

通过使用泛型,可以在编译时检查类型,确保只有正确类型的对象被添加到集合中。
这避免了在运行时可能发生的类型转换错误。

7.1.2消除类型转换

在使用泛型之前,从集合中取出的对象需要进行类型转换。
使用泛型后,由于类型已经在编译时得到了确认,因此不再需要进行类型转换。

7.1.3代码重用性

泛型允许你编写灵活且可重用的代码。你可以创建泛型方法或类,它们可以在不同的数据类型上操作,而不必为每种数据类型都编写一个新的版本。

7.1.4优化设计

泛型提供了一种更优雅的方式来处理集合中的元素,使得代码更加清晰和易于理解。
它们还有助于减少潜在的错误,因为你不能将错误的数据类型添加到集合中。

7.2理解

Java中的泛型是在JDK 5中引入的,它的主要目的是提供编译时类型检查和消除类型转换,使得代码更加安全、清晰和易于维护。
在没有泛型之前,Java程序员使用Object类型来处理集合中的元素,这导致了代码中充斥着大量的类型转换操作,而且容易出错。

例如,如果你将一个错误类型的对象添加到集合中,那么在运行时可能会引发ClassCastException异常。
为了解决这个问题,Java引入了泛型。通过使用泛型,你可以在编译时指定集合中元素的类型,从而避免了类型转换错误。

此外,泛型还提供了代码重用性和优化设计的好处。
总的来说,Java中的泛型是一个重要的特性,它通过提供编译时的类型检查和消除类型转换,使得代码更加安全、清晰和易于维护。


8Map接口提供了哪些不同的集合视图?

8.1背记

Map 接口提供了三种不同的集合视图


视图名称

Set view of keys

Collection view of values

Set view of entries

入口方法

keySet()

values()

entrySet()

方法功能说明

获得一个包含所有键(keys)的Set视图

得到一个包含所有值(values)的 Collection视图

获取一个包含所有映射(entries)的Set 视图。每个映射都由一个键和一个值组成

8.2理解

8.2.1Set keyset()

返回Map中包含的所有key的一个Set视图。
Set集合是受Map支持的,Map增删改会在该集合中反映出来。
反过来此Set集合的删除操作也会在原Map集合中体现出来。
当时一个迭代器正在遍历此Set集合,若Map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为不确定。
Set集合支持元素查找和删除,从此Set中删除元素会从map中移除对应的映射,它不支持add()和addAll()添加操作。

8.2.2Collection values()

返回一个Map中包含的所有value的一个Collection视图。
这个Collection集合受Map支持,Map增删改会在Collection中反映出来。
反过来此Collection集合的删除操作也会在原Map集合中体现出来。
当一个迭代器正在遍历此Collection时,若Map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为不确定。
Collection集合支持元素查找和删除,从此Collection中删除元素会从Map中移除对应的映射,它不支持add()和addAll()添加操作。

8.2.3Set entrySet()

返回一个Map中包含的所有映射的一个Set集合视图。
这个Set集合受Map支持的,Map增删改会在Collection中反映出来。
反过来此Set集合的删除操作也会在原Map集合中体现出来。
当一个迭代器正在遍历此Set集合时,若Map被修改了(除迭代器自身的移除操作,以及对迭代器返回的entry进行setValue()外),迭代器的结果会变为未定义。
Set集合支持元素查找和删除,从此Set中删除元素会从Map中移除对应的映射,它不支持add()和addAll()添加操作。


9JDK1.7版本中HashMap的内部结构

JDK1.7HashMap的存储结构是数组加链表组合而成的。
每个元素都被存储为一个Entry对象,这些Entry对象被分布到一个数组中,每个数组的元素都是一个单向链表,链表中的每个节点都包含键值对。
这种设计允许HashMap在解决哈希冲突时通过链表来保存具有相同哈希值的不同键值对。
当链表的长度变得过长时,查询效率会受到影响,因为需要遍历链表来查找对应的键值对。
JDK1.7中,HashMapput()流程涉及几个关键步骤:

9.1计算Hash值与数组下标

根据keyhashCode计算出数组的下标,计算过程是先计算hashCode值,然后用hashCode值对数组长度取模,取模的结果就是数组下标。

9.2判断是否需要扩容

如果当前HashMap的大小已达到阈值(默认容量的0.75倍),则需要进行扩容操作,这个过程涉及到rehash和数据转移。

9.3插入新元素

在不需要扩容的情况下,直接在对应下标处的链表插入新的Entry节点。
如果链表长度过长,在JDK1.8之后的版本中会将链表转换为红黑树以提高查询效率。

9.4处理哈希冲突

如果存在相同的hash值导致下标冲突,且key调用equals()方法确定不相等,则采用头插法将新元素插入到链表中,确保新加入的元素位于链表的最前端。
总之,整个put()方法的逻辑保证了HashMap能够在保证键值对高效存取的同时,通过链表来解决哈希冲突的问题。
需要注意的是,在多线程环境下,HashMap并不是线程安全的,因此可能会遇到并发问题如链表循环等。

9.5put()方法流程梳理



10HashMap中哪些情况会导致哈希冲突?

10.1哈希冲突

哈希冲突是指:经过一系列计算后,Entry存入数组的下标相同,所以导致哈希冲突的原因就是看哪些情况会导致计算得到的数组下标相同

10.2冲突原因


10.3hash()方法

10.3.1hash()方法的位置


10.3.2hash()方法的作用

通过对hashCode值进行『异或』操作、『位移』操作把原来的hashCode值打乱,尽可能降低哈希冲突的概率

11JDK1.8版本中HashMap的内部结构

JDK1.8HashMap的存储结构是数组加链表/红黑树
JDK1.8中,HashMap的底层实现发生了变化,主要为了解决当链表过长时导致的性能问题。
在这个版本中,当一个链表的长度超过一定阈值(默认为8)且数组已经历过两次扩容,该链表会被转换为红黑树,以提高搜索效率。
具体来说JDK1.8HashMapput()流程如下:

11.1计算Hash值与数组下标

首先根据keyhashCode方法计算出一个hash值,然后根据这个hash值通过位运算得到数组的下标。

11.2判断是否需要扩容

如果当前HashMap的大小已达到阈值(默认容量的0.75倍),则需要进行扩容操作,这个过程涉及到rehash和数据转移。

11.3插入新元素

在不需要扩容的情况下,根据得到的下标位置,找到对应的节点。如果该位置没有节点,则直接创建一个新的节点并放入。
如果有节点,则判断key是否相等。

11.4处理哈希冲突

如果存在相同的hash值导致下标冲突,则采用头插法将新元素插入到链表中,确保新加入的元素位于链表的最前端。
如果链表长度超过阈值,将链表转换为红黑树。
总的来说,整个put()方法的逻辑保证了HashMap能够在保证键值对高效存取的同时,通过链表或红黑树来解决哈希冲突的问题。
需要注意的是,在多线程环境下,HashMap并不是线程安全的,因此可能会遇到并发问题如链表循环等。

11.5put()方法流程

11.6特点总结

将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树。而是选择进行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。
所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。
具体可以参考 treeifyBin方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。


12JDK8中的HashMap为什么要使用红黑树?

12.1背记

JDK8中的HashMap使用红黑树是为了提高查找效率和维持数据结构的平衡。
链表的查找时间复杂度为O(n),当链表长度过长时会导致性能下降。
而红黑树作为一种自平衡的二叉查找树,可以在数据量增大过程中保持较好的查找性能,其查找时间复杂度为O(log n)。
保证数据结构平衡的同时,提高查找效率,避免因链表过长导致的性能问题,同时也减少了内存开销,并简化了实现的复杂性

12.2理解

使用红黑树的几个原因:

12.2.1提高查找效率

HashMap中某个桶(bucket)内的元素数量超过一定阈值(默认为8个),并且这些元素都位于同一条链表上时,将链表转换为红黑树可以提高查找效率。因为红黑树的查找效率要高于链表。

12.2.2自平衡性

红黑树是一种自平衡的二叉查找树,它能够在插入和删除操作后自动调整结构,以保持树的平衡。
这样可以确保即使在极端情况下,树的高度也不会过高,从而维持了较好的查找性能。

12.2.3避免过度哈希化

在早期版本的HashMap中,如果出现大量冲突,链表会变得非常长,导致性能急剧下降。
引入红黑树后,即使有大量冲突,也能通过红黑树的结构来保持较高的查找效率。

12.2.4减少内存开销

相比于B树等其他平衡树结构,红黑树的节点利用率更高,因为它不需要存储额外的信息(如兄弟节点指针),这有助于减少内存开销。

12.2.5实现简单

红黑树的实现相对简单,它只有五种基本的旋转操作来维护平衡,这使得它在HashMap中的实现更加高效和可靠。

13JDK8中的HashMap什么时候将链表转化为红黑树?

13.1背记

JDK8HashMap将链表转化为红黑树需要同时满足两个条件:

13.2理解

之所以需要同时满足这两个条件,是因为HashMap的设计者认为:频繁的出现哈希碰撞可能是因为数组长度不够。
如果确实是因为数组长度不够而导致了哈希碰撞,那么此时首先要做的就不是把链表转为红黑树,而是对数组扩容。

14为什么HashMap长度大于等于8且容量大于等于64才转化为红黑树?

14.1背记

链表的长度达到一定阈值(默认为8)时,表明该链表已经足够长,以至于搜索性能可能开始受到影响。
HashMap的容量达到(默认为64)时,表示HashMap已经足够大,足以容纳更多的元素而不需要频繁扩容,在这个时机进行树化主要是保证性能的同时,避免不必要的数据结构转换开销。

14.2理解

HashMap在决定是否将链表转化为红黑树时,考虑了两个条件:链表的长度和HashMap的容量。
这两个条件共同确保了转化操作的性能收益大于其带来的开销。
具体来说,以下是为什么HashMap会设定这些条件的详细解释:

14.2.1链表长度

链表的长度达到一定阈值(默认为8)时,表明该链表已经足够长,以至于搜索性能可能开始受到影响。
这个阈值是基于性能测试和经验得出的,它代表了在一般情况下,链表转化为红黑树的性能收益开始显现的点。

14.2.2HashMap容量

HashMap的容量达到MIN_TREEIFY_CAPACITY(默认为64)时,表示HashMap已经足够大,足以容纳更多的元素而不需要频繁扩容。
此时,将链表转化为红黑树的操作更有可能带来性能上的提升,因为树结构可以提供更好的搜索性能。
此外,如果HashMap的容量小于64,即使链表长度超过8,也不会进行树化操作。
这是为了确保在较小的HashMap中,不会因为过早的树化而导致额外的开销。
同时,如果红黑树的节点数量减少到小于或等于6个,HashMap会将其恢复为链表形态,因为在这种情况下,维护树结构的开销不再划
算。
总的来说,HashMap中的这一设计是为了在保证性能的同时,避免不必要的数据结构转换开销。

15JDK7JDK8HashMap的不同点?

15.1背记

JDK7JDK8HashMap的不同点主要体现在数据结构和扩容机制上:

15.1.1数据结构

15.1.2扩容机制

JDK7中当HashMap中的元素数量达到容量的0.75倍时,扩大到原来的两倍,并重新散列所有的元素。
JDK8中除了扩大数组大小,还会根据情况将红黑树转换回链表或保持红黑树结构。

15.2理解


16JDK 1.7 ConcurrentHashMap

16.1两层嵌套数组

JDK 1.7 ConcurrentHashMap底层是由两层嵌套数组来实现的:

16.2put流程

16.2.1segments[j]

当调用ConcurrentHashMapput()方法时,先根据key计算出对应的Segment[]的数组下标j,确定好当前key、value应该插入到哪个Segment对象中,如果segments[j] 为空,则利用自旋锁的方式在j位置生成一个Segment对象。

16.2.2调用Segment对象的put()方法

Segment对象的put()方法会先加锁,然后根据key计算出对应的HashEntry[]的数组下标i,然后将key、value封装为HashEntry对象放入该位置,此过程和JDK7HashMapput()方法一样,然后解锁。
在加锁的过程中逻辑比较复杂,先通过自旋加锁,如果超过一定次数就会直接阻塞


17JDK 1.8 ConcurrentHashMap

17.1总体介绍

JDK1.8中的ConcurrentHashMap的底层结构是Node数组+链表+红黑树:

17.1.1Node数组

这是ConcurrentHashMap存储元素的基础数据结构,它类似于HashMap中的桶
(bucket)数组。Node是一个简单的静态内部类,它包含一个键、一个值以及指向下一个节
点的指针。

17.1.2链表

Node数组的同一索引处,如果发生了哈希冲突,那么新的节点将以链表的形式连接在一起。

17.1.3红黑树

当链表的长度超过一定阈值时,链表会被转换为红黑树以提高搜索效率。
这是因为红黑树可以提供更好的查找性能,尤其是在大量数据的情况下。

此外,与JDK1.7相比JDK1.8ConcurrentHashMap不再使用分段锁,而是采用了更细粒度的同步机制,如synchronized关键字和CAS操作。

总的来说,JDK1.8ConcurrentHashMap的底层结构设计旨在提高并发性能,同时保持数据的线程安全性。
它通过结合多种数据结构和同步机制来实现这一目标。

17.2put流程

当向ConcurrentHashMapput一个key、value

18JDK 1.7 ConcurrentHashMap如何保证并发

18.1背记

JDK1.7中的ConcurrentHashMap通过分段锁机制来保证并发性:

18.1.1分段锁技术

ConcurrentHashMap将内部结构分为多个段(Segment),每个段都有自己的锁。
这样,当多个线程尝试访问不同的段时,它们可以同时进行,不会被其他线程持有的锁阻塞。
这种细粒度的锁定机制提高了并发度。

18.1.2高效的读操作

由于HashEntry对象的不变性和volatile变量的使用,读操作可以在没有锁的情况下安全地进行。
即使有写操作正在进行,读操作也能够看到最新的内存状态,这是通过内存可见性保证的。

18.1.3跨段操作的安全性

对于跨段的读操作ConcurrentHashMap提供了两种方案:不加锁和加锁。
不加锁的方案适用于读多写少的场景,而加锁方案则用于写操作较多的场景,以确保操作的安全性。

18.2理解

主要利用Unsafe操作+ ReentrantLock +分段思想。
主要使用了Unsafe操作中的:
分段思想是为了提高ConcurrentHashMap的并发量,分段数越高则支持的最大并发量越高,程序员可以通过concurrencyLevel参数来指定并发量。
ConcurrentHashMap的内部类Segment就是用来表示某一个段的。
每个Segment就是一个小型的HashMap,当调用ConcurrentHashMapput()方法时,最终会调用到Segmentput()方法,而Segment类继承了ReentrantLock,所以Segment自带可重入锁,当调用到Segmentput方法时,会先利用可重入锁加锁,加锁成功后再将待插入的key、value插入到小型HashMap中,插入完成后解锁。

19JDK 1.8 ConcurrentHashMap如何保证并发

JDK1.8中的ConcurrentHashMap通过以下几个方面的细节来保证并发性:

19.1数据结构

使用数组+链表+红黑树的混合结构,其中数组用于存储桶(bucket),链表用于解决哈希冲突,当链表长度超过一定阈值时会转化为红黑树以提升性能。

19.2无锁操作

ConcurrentHashMap在大部分读操作中不需要加锁,而是采用无锁的方式来进行。
这是通过使用 volatile 关键字和CAS(Compare And Swap)操作来实现的。

19.3CAS操作

在更新操作中ConcurrentHashMap使用了CAS操作来减少同步的开销,并确保原子性操作。

19.4扩容机制

当元素数量达到阈值时,ConcurrentHashMap会触发扩容操作,即创建一个新的数组并将旧数组中的元素重新散列到新数组中。

19.5分段锁技术

尽管JDK1.8ConcurrentHashMap不再依赖分段锁,但在一些写操作中仍然使用了锁来保证线程安全。

总的来说,JDK1.8中的ConcurrentHashMap通过这些细节设计实现了高效的并发操作,同时保持了数据的线程安全性。

20HashSet的去重原理

20.1背记

HashSet的去重原理主要基于HashMapkey不重复特性。
HashSet底层使用了一个HashMap来存储元素,所有放入HashSet中的元素实际上都被存储在HashMapkey中,而此时HashMapvalue则是一个常量对象。

20.2理解

详细来说,HashSet底层使用了一个HashMap来存储元素,所有放入HashSet中的元素实际上都被存储在HashMapkey中,而HashMapvalue则是一个常量对象。

下面是HashSet实现去重的具体步骤:

此外,对于基本数据类型或者String类型的集合,HashSet能够成功实现去重是因为这些类型已经正确地覆盖了hashCode()和equals() 方法。

对于自定义类,如果想要在HashSet中正确去重,也必须重写这两个方法,确保相等的对象有相同的哈希码,这样HashSet才能识别它们为相同的元素并去除重复项。

总的来说,HashSet利用HashMapkey不能重复的特性来实现去重,通过覆写hashCode()和equals()方法来确保唯一性

21集合与数组的区别

集合与数组在Java中是两种不同的数据结构,它们在元素类型、大小可变性、存储类型以及成员方法上有显著的区别。
具体分析如下:

21.1元素类型


21.2大小可变性


21.3存储类型


21.4成员方法


总的来说,集合提供了比数组更高级的功能,包括动态调整大小、存储不同类型的对象以及提供多样化的成员方法。
而数组则在声明时需要确定大小和类型,但在处理大量同类型数据时可能更高效。

22TreeSetHashSet的区别

22.1背记

TreeSetHashSetJava中都是用于存储不重复元素的集合类。

22.1.1底层数据结构


22.1.2元素顺序


22.1.3null元素


22.1.4性能特点

在大多数情况下,HashSet的性能要优于TreeSet,尤其是在添加和检索元素时,因为哈希表的操作通常具有常数时间复杂度。
但是,如果需要对集合进行排序操作,TreeSet会更适合,因为它内部维持了元素的有序状态。

22.2理解

TreeSetHashSet的底层原理要点涉及它们各自的数据结构实现和操作机制。
具体来看:

22.2.1二叉树与二叉查找树

TreeSet中,元素的存储是基于二叉查找树(BST)的数据结构。
二叉查找树是一种特殊的二叉树,其中左子节点的值总是小于其父节点,右子节点的值总是大于其父节点。
这种结构保证了元素会自动按照排序顺序存储。

22.2.2哈希表

HashSet的底层实现基于HashMap,它使用数组加链表的方式来存储元素。
当一个对象被加入HashSet时,首先会计算对象的哈希值,然后通过移位运算确定该对象在哈希表中的位置。
如果该位置没有其他元素,则直接存储;如果有,则以链表的形式存储在这个位置上。

22.2.3hashCode()和equals()方法的重要性

对于HashMapHashSet来说,hashCode()方法和equals()方法至关重要。
hashCode()方法用于计算对象的哈希值,而equals()方法用于比较两个对象是否相等。
这两个方法的正确实现对于集合类的性能和准确性至关重要。

总的来说,TreeSetHashSet的底层原理主要区别在于它们采用的数据结构不同,这导致了它们在性能和功能上的不同。
TreeSet基于二叉查找树,保证了元素的有序性和范围查询的能力;
HashSet基于HashMap,提供了快速的插入和查询操作,但不保证元素的顺序。

23如何遍历一个Map集合?

23.1背记

遍历Map一共有三种方式:

23.2理解

Java中,Map集合的三种常见遍历方式包括键找值的方式遍历、键值对的方式遍历以及使用Lambda表达式遍历。具体思路如下:

23.2.1键找值的方式

Map<String, String> map = new HashMap<>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");

Set<String> keySet = map.keySet();
for (String key : keySet) {
    String value = map.get(key);
    System.out.println(key + " -> " + value);
} 示例代码

23.2.2键值对的方式

Map<String, String> map = new HashMap<>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");

Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
    String key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + " -> " + value);
} 示例代码

23.2.3Lambda表达式遍历(JDK 1.8及以上版本)

Map<String, String> map = new HashMap<>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");

map.forEach((key, value) -> System.out.println(key + " -> " + value)); 示例代码

23.2.4补充:仅遍历所有值

Map<String, String> map = new HashMap<>();
map.put("key01", "value01");
map.put("key02", "value02");
map.put("key03", "value03");

Collection<String> values = map.values();
for (String value : values) {
    System.out.println("value = " + value);
} 示例代码


24HashMapHashTable 的区别

24.1背记

HashMapHashTable在继承关系、线程安全性和使用null作为键值方面存在差异

24.1.1继承关系

HashMap继承自AbstractMap类,而HashTable继承自Dictionary抽象类

24.1.2线程安全性

HashTable的方法是同步的,这意味着它是线程安全的,可以直接在多线程环境中使用
HashMap是非同步的,如果在多线程环境下使用,需要外部同步或者通过Collections.synchronizedMap() 来获取一个线程安全的集合

24.1.3使用null作为键值

HashMap允许使用null作为键和值,虽然可以存储null键,但建议避免这样做,因为这样会导致所有的null键都存储在table数组的第一个节点上
HashTable不允许null作为键或值,这是为了避免潜在的NullPointerException问题

24.2理解

HashMapHashTable在底层实现上主要基于散列表,但它们的存储结构、线程安全性和使用限制等方面存在差异

24.2.1存储结构

HashMap的底层实现是数组加链表(JDK1.8之前),在JDK1.8及以后版本中,为了提高性能,当链表长度超过一定阈值时,链表会转换为红黑树。这种结构既保证了查询效率,也便于元素的增加和删除。
Hashtable的底层也是基于散列表,但它只使用链表来解决冲突,不支持JDK1.8HashMap的红黑树优化。

24.2.2线程安全性

HashMap是非线程安全的,这意味着在多线程环境下直接使用可能会出现并发问题
如果需要在多线程环境中使用,可以考虑使用 Collections.synchronizedMap() 方法来包装HashMap,或者使用ConcurrentHashMap作为替代方案
HashTable是线程安全的,因为它的方法是同步的
这确保了在多线程环境下的安全性,但也因此牺牲了一定的性能,因为所有线程在执行方法时都需要获得对象的锁

24.2.3使用限制

HashMap允许null值和null键,虽然可以使用null作为键,但是这样会导致所有的null键都存储在table数组的第一个节点上,可能会引起性能问题
Hashtable不允许null作为键或值,这是为了避免潜在的NullPointerException问题

24.2.4初始容量和加载因子

HashMap的初始容量为16,加载因子为0.75。
加载因子越大,就是启用扩容的阈值越大,这表明HashMap所占的内存越少,但是某一数组上挂载的链表长度越长,所以搜索速度越慢。
Hashtable的初始默认容量为11个,负载因子也是0.75。

总的来说,HashMapHashTable虽然都是基于散列表的实现,但它们在存储结构、线程安全性、使用限制以及初始容量和加载因子上有所不同。
这些差异决定了它们在不同场景下的适用性和性能表现。
在实际开发中,选择合适的数据结构应根据具体的应用场景和需求来决定。


25ArrayListLinkedList的区别

25.1背记

25.1.1内部实现


25.1.2随机访问


25.1.3内存占用


25.1.4插入和删除操作

对于频繁的插入和删除操作,LinkedList通常比ArrayList更高效,因为LinkedList只需要改变指针指向,而不需要移动其他元素的位置。
ArrayList在进行插入和删除操作时,可能需要移动大量元素以保持数组的连续性,这会导致性能下降。

25.2理解

ArrayList的底层实现是基于动态数组,而LinkedList的底层实现是基于双向链表。

25.2.1ArrayList底层实现

ArrayList在底层使用一个 Object 类型的数组来存储元素。
这个数组在实例化时会被初始化为一个固定的长度,这个长度会随着元素的增加而动态扩展。
当 ArrayList 需要扩展容量时,它会创建一个新的数组,长度为原数组长度的1.5倍加1,然后将原数组中的元素复制到新数组中。
这个过程称为动态扩容。

25.2.2LinkedList底层实现

LinkedList在底层使用双向链表来实现。每个元素都被封装在一个节点对象中,每个节点包含指向前一个和后一个节点的引用。
这种结构使得在链表中添加或删除节点时,只需要改变相应的指针指向即可。
由于链表的特性,LinkedList在插入和删除操作上具有优势,因为这些操作不需要移动大量元素,只需要调整相邻节点的指针即可。
总的来说,如果应用场景中随机访问操作较多,则 ArrayList 可能更适合;如果应用场景中插入和删除操作较多,则 LinkedList 可能更合适。