🏆作者简介,普修罗双战士,一直追求不断学习和成长,在技术的道路上持续探索和实践。
🏆多年互联网行业从业经验,历任核心研发工程师,项目技术负责人。
🎉欢迎 👍点赞✍评论⭐收藏
文章目录
- 🔎 一、Java高频容器知识文集(1)
- 🍁🍁 01. Java 容器都有哪些?之间有什么区别?
- 🍁🍁 02. Collection 和 Collections 有什么区别?
- 🍁🍁 03. List、Set、Map 之间的区别是什么?举例说明?
- 🍁🍁 04. HashMap 和 Hashtable 有什么区别?
- 🍁🍁 05. 如何决定使用 HashMap 还是 TreeMap?
- 🍁🍁 06. ArrayList 和 LinkedList 的区别是什么?
- 🍁🍁 07. 如何实现数组和 List 之间的转换?
- 🍁🍁 08. ArrayList 和 Vector 的区别是什么?
- 🍁🍁 09. Array 和 ArrayList 有何区别?
- 🍁🍁 10. 在 Queue 中 poll()和 remove()有什么区别?
- 🍁🍁 11. 哪些集合类是线程安全的?
- 🍁🍁 12. 迭代器 Iterator 是什么?
- 🍁🍁 13. Iterator 怎么使用?有什么特点?
- 🍁🍁 14. Iterator 和 ListIterator 有什么区别?
- 🍁🍁 15. 怎么确保一个集合不能被修改?
- 🍁🍁 16. 如何实现数组和 List 之间的转换?
- 从数组转换为 List:
- 从 List 转换为数组:
- 🍁🍁 17. comparable 和 comparator的区别?
- 🍁🍁 18. HashMap是如何解决哈希冲突的?
- 🍁🍁 19. 说一下 HashMap 的实现原理?
- 🍁🍁 20. 说一下 HashSet 的实现原理?
🔎 一、Java高频容器知识文集(1)
🍁🍁 01. Java 容器都有哪些?之间有什么区别?
在 Java 中,容器通常指的是常用的数据结构集合,Java 提供了多种容器来存储、操作和访问数据。主要的 Java 容器包括以下几种:
-
List(列表):
- 以特定顺序存储元素的集合,允许重复元素。常见的实现类有 ArrayList、LinkedList、Vector 等。
- 举例:
List<String> arrayList = new ArrayList<>(); List<Integer> linkedList = new LinkedList<>();
-
Set(集合):
- 不包含重复元素的集合。常见的实现类有 HashSet、LinkedHashSet、TreeSet 等。
- 举例:
Set<String> hashSet = new HashSet<>(); Set<Integer> treeSet = new TreeSet<>();
-
Map(映射):
- 存储键值对的集合,每个键最多只能映射到一个值。常见的实现类有 HashMap、LinkedHashMap、TreeMap 等。
- 举例:
Map<String, Integer> hashMap = new HashMap<>(); Map<String, String> linkedHashMap = new LinkedHashMap<>();
-
Queue(队列):
- 用于存储元素,并以特定顺序提供对元素的访问。常见的实现类有 PriorityQueue、LinkedList 等。
- 举例:
Queue<String> priorityQueue = new PriorityQueue<>(); Queue<Integer> linkedListQueue = new LinkedList<>();
-
Stack(栈):
- 后进先出(LIFO)的数据结构。常见的实现类有 Stack 类。
- 举例:
Stack<String> stack = new Stack<>();
上述是 Java 中常用的容器,它们都提供了不同的特性和适用场景,可以根据需求选择合适的容器来存储和操作数据。
下面是一个以表格形式说明Java中不同容器之间区别的例子:
容器类型 | 是否允许重复元素 | 是否有序 | 是否按键排序 | 是否基于新元素的值进行排序 |
---|---|---|---|---|
List | 允许 | 有序 | N/A | N/A |
Set | 不允许 | 无序 | N/A | N/A |
Map | 允许(值不重复) | 无序 | 键排序 | 值排序 |
Queue | 允许 | 有序 | N/A | N/A |
Stack | 允许 | 有序 | N/A | N/A |
请注意,表中的排序概念只适用于实现了特定排序接口的容器。
- List 允许重复元素,维护插入顺序,并且可以通过索引访问元素。
- Set 不允许重复元素,并且不保证元素的顺序。
- Map 以键值对的形式存储元素,键值对可以有重复的值,但键唯一。可以按照键或值的顺序进行排序。
- Queue 用于存储元素,并且提供对元素的访问顺序。
- Stack 是一种后进先出(LIFO)的数据结构。
根据不同的需求和使用场景,可以选择适当的容器类型来满足需求。
🍁🍁 02. Collection 和 Collections 有什么区别?
Collection
和 Collections
是 Java 中的两个不同的概念:
-
Collection:
Collection
是 Java 集合框架的根接口,它代表一组对象,是集合类的基本接口。Collection
接口派生了 List、Set 和 Queue 等子接口,它定义了对集合元素进行基本操作的方法,比如添加、删除、遍历等。- 举例:
Collection<String> list = new ArrayList<>(); list.add("element1"); list.add("element2");
-
Collections:
Collections
是 Java 实用类库中的一个工具类,包含各种静态方法用于操作集合,比如对集合进行排序、查找最大最小值、反转等操作。它提供了一些实用的静态方法,用于操作各种集合类。- 举例:
List<Integer> numbers = new ArrayList<>(); numbers.add(3); numbers.add(1); numbers.add(2); Collections.sort(numbers);
因此,Collection
是表示集合类的根接口,而 Collections
是包含用于操作集合的静态方法的实用类。它们的区别在于一个是接口用于表示集合操作的基本方法,一个是工具类用于提供集合操作的实用方法。
下面是一个以表格形式说明Collection
和Collections
之间区别的例子:
区别 | Collection | Collections |
---|---|---|
定义 | 根接口,表示一组对象的集合 | 工具类,提供对集合的操作方法 |
功能 | 定义了集合基本操作的方法,如添加、删除等 | 提供了各种集合操作的静态方法 |
包含的接口 | List、Set、Queue 等集合接口的父接口 | 无 |
是否包含实现 | 是 | 否 |
使用方式 | 通常需要具体的实现类实例化对象 | 作为静态工具类直接调用方法 |
请注意,Collection
是一个接口,而 Collections
是一个包含静态方法的工具类。它们的主要区别在于功能和用法。
Collection
是一组对象的集合的根接口,定义了集合的基本操作方法,如添加、删除、遍历等。它是其他集合接口的父接口,需要使用具体的实现类来实例化对象。Collections
是一个工具类,提供了很多静态方法用于操作集合,如排序、查找、反转等操作。它包含一些常用的算法和方法,可以直接调用,而不需要实例化对象。
因此,Collection
和 Collections
在功能和使用方式上存在明显的区别,一个是表示集合类的接口,一个是提供集合操作的工具类。
🍁🍁 03. List、Set、Map 之间的区别是什么?举例说明?
List、Set 和 Map 是 Java 集合框架中常见的三种接口,它们之间有以下主要区别:
-
List:
- List 是一个有序集合,允许存储重复元素。它可以根据索引访问元素,元素在列表中的位置是有意义的。
- 举例:ArrayList 和 LinkedList 都是 List 接口的实现,允许存储重复元素,并且可以按照元素的插入顺序访问元素。
-
Set:
- Set 是一个不允许有重复元素的集合,元素没有特定的顺序。如果试图向 Set 中添加重复元素,添加操作将被忽略。
- 举例:HashSet 和 TreeSet 都是 Set 接口的实现,不允许存储重复元素,并且不保证元素的顺序。
-
Map:
- Map 是一种键值对的集合,每个键只能对应一个值。键是唯一的,值可以重复。Map 不属于 Collection 接口的一部分,它提供了根据键查找值的功能。
- 举例:HashMap 和 TreeMap 都是 Map 接口的实现,用于将键映射到值,并允许通过键来查找对应的值。
综上所述,List 是有序集合、允许存储重复元素,可以根据索引访问元素;Set 是不允许有重复元素的集合,不保证元素顺序;Map 是键值对的集合,键唯一对应值。根据实际需求,选择合适的集合类型可以更好地满足需求。
下面是一个表格,说明 List、Set 和 Map 之间的区别:
List | Set | Map | |
---|---|---|---|
顺序 | 有序 | 无序 | 无序 |
元素重复性 | 允许 | 不允许 | 键不允许重复,值允许重复 |
访问方式 | 通过索引访问 | 无法通过索引访问 | 通过键值对访问 |
接口 | Collection 接口的子接口 | Collection 接口的子接口 | 不属于 Collection 接口的一部分 |
例子 | ArrayList、LinkedList 等 | HashSet、TreeSet 等 | HashMap、TreeMap 等 |
该表格总结了List、Set和Map之间的主要区别,包括顺序特性、元素重复性、访问方式、以及它们所实现的不同接口等方面。选择合适的集合类型可以更好地满足具体的编程需求。
🍁🍁 04. HashMap 和 Hashtable 有什么区别?
HashMap 和 Hashtable 都是用于存储键值对的数据结构,它们之间有以下区别:
-
线程安全性:
- HashMap 是非线程安全的,不支持多线程并发操作,如果需要在多线程环境下使用,需要额外的同步机制来保证线程安全。
- Hashtable 是线程安全的,它的方法都是同步的,可以直接在多线程环境中使用,但也因此在单线程环境下性能会有所下降。
-
空键值(null)的处理:
- HashMap 允许将键和值均设置为 null,即允许存在键或值为 null 的情况。
- Hashtable 不允许键或值为 null,如果存储 null 值,会直接抛出 NullPointerException。
-
继承关系:
- HashMap 继承自 AbstractMap 类,实现了 Map 接口,是 Java Collections Framework 中的一部分。
- Hashtable 继承自 Dictionary 类,实现了 Map 接口,但已经被标记为"过时",推荐使用 ConcurrentHashMap 来替代。
-
性能:
- 由于线程安全机制的不同,HashMap 在非多线程环境下性能通常优于 Hashtable。在多线程环境下,ConcurrentHashMap 是更好的选择。
-
迭代器:
- HashMap 的迭代器是 fail-fast 的,即在迭代过程中若其他线程对其进行结构性修改,会抛出 ConcurrentModificationException 异常。
- Hashtable 的迭代器是 fail-safe 的,允许在迭代过程中对集合进行修改,不会抛出异常。
基于以上区别,通常来说,在新的代码中建议使用 HashMap 而不是 Hashtable。HashMap 提供了更好的性能,在单线程环境下更适用,并且允许存储 null 键和值。在多线程环境下,推荐使用 ConcurrentHashMap 来替代 Hashtable。
以下是一个表格,说明了 HashMap 和 Hashtable 之间的区别:
特性 | HashMap | Hashtable |
---|---|---|
线程安全性 | 非线程安全 | 线程安全 |
空键值(null) | 允许键和值均为 null | 不允许键或值为 null |
继承关系 | 继承自 AbstractMap 类 | 继承自 Dictionary 类 |
替代方案 | 在非多线程环境下通常优于 Hashtable | 已被标记为"过时",推荐使用 ConcurrentHashMap |
性能 | 通常在非多线程环境下性能优于 Hashtable | 在单线程环境下性能有所下降,在多线程环境下相对较低 |
迭代器 | fail-fast | fail-safe |
该表格总结了 HashMap 和 Hashtable 之间的主要区别,可以帮助开发人员根据实际需求选择合适的数据结构。
🍁🍁 05. 如何决定使用 HashMap 还是 TreeMap?
决定使用 HashMap 还是 TreeMap 取决于几个因素:
-
排序需求:如果你需要按键的自然顺序(字典顺序)来进行排序,那么应该使用 TreeMap。TreeMap 内部使用红黑树数据结构维护键的有序性,它会根据键的自然顺序或者自定义的比较器对键进行排序。
-
快速插入与查找:如果你更注重插入和查找操作的速度,而不是键的有序性,那么应该使用 HashMap。HashMap 使用哈希表数据结构,插入和查找操作的平均时间复杂度为 O(1)。
-
内存占用:如果你对内存占用有限制,并希望尽量节省内存空间,那么应该考虑使用 HashMap。TreeMap 会维护键的有序性,需要额外的内存空间来存储排序信息,因此相比之下,HashMap 通常占用更少的内存。
-
自定义排序需求:如果你希望根据自定义的逻辑对键进行排序,那么应该使用 TreeMap。TreeMap 允许通过自定义比较器来指定键的排序方式,可以实现灵活的排序逻辑。
综上所述,如果你需要按照键的自然顺序来排序,或者需要自定义的排序方式,那么应该使用 TreeMap。如果你更关注插入和查找的速度,并且对键的顺序没有要求,那么应该使用 HashMap。另外,如果你对内存占用有限制,也倾向于使用 HashMap。
🍁🍁 06. ArrayList 和 LinkedList 的区别是什么?
ArrayList 和 LinkedList 是 Java 中常用的两种集合类,它们之间的主要区别包括以下几点:
-
底层数据结构:
- ArrayList 使用数组作为底层数据结构来存储元素,支持动态扩容。
- LinkedList 使用双向链表作为底层数据结构来存储元素,每个元素都包含对前一个和后一个元素的引用。
-
随机访问和插入/删除操作的效率:
- ArrayList 支持随机访问,即可以通过索引快速获取元素,时间复杂度为 O(1)。但在中间或开头插入/删除元素时,需要将后续元素进行移动,时间复杂度为 O(n)。
- LinkedList 对于随机访问的效率较低,因为需要从头或尾开始遍历链表以获取指定索引的元素,时间复杂度为 O(n)。但在中间插入/删除元素时,由于只需要修改相邻元素的引用,时间复杂度为 O(1)。
-
内存占用:
- ArrayList 在存储大量元素时会有一定的空间浪费,因为需要预留一定的容量空间,并且在扩容时需要重新分配和复制数组。
- LinkedList 的每个元素都需要额外的内存空间来存储前一个和后一个元素的引用,因此在存储大量元素时会占用更多的内存空间。
-
迭代效率:
- ArrayList 的迭代效率较高,可以通过索引直接访问元素。
- LinkedList 的迭代效率较低,因为需要依次遍历链表来获取元素。
综上所述,如果需要频繁进行随机访问操作,应该选择 ArrayList;如果需要频繁进行插入/删除操作,尤其是在集合的中间位置,应该选择 LinkedList。对于内存占用和迭代效率,需要根据具体的场景进行权衡和选择。
以下是一个表格,说明了 ArrayList 和 LinkedList 之间的区别:
特性 | ArrayList | LinkedList |
---|---|---|
底层数据结构 | 数组 | 双向链表 |
随机访问效率 | 高效(O(1)) | 低效(O(n)) |
插入/删除效率 | 低效(O(n)) | 高效(O(1)) |
内存占用 | 少量额外空间 | 较大额外空间 |
中间插入/删除效率 | 低效(O(n)) | 高效(O(1)) |
迭代效率 | 高效 | 低效 |
该表格总结了 ArrayList 和 LinkedList 之间的主要区别,可以帮助开发人员根据实际需求选择合适的集合类。
🍁🍁 07. 如何实现数组和 List 之间的转换?
要实现数组和 List 之间的转换,可以使用以下方法:
- 从数组转换为 List:
-
使用 Arrays 类的 asList() 方法:可以使用 Arrays.asList() 方法将数组转换为 List。例如:
int[] array = {1, 2, 3, 4};List<Integer> list = Arrays.asList(array);
注意:通过 asList() 转换得到的 List 是一个固定大小的 List,不支持对其进行添加或删除操作。如果需要对 List 进行修改操作,可以使用其他的 List 实现类(如 ArrayList)进行拷贝。例如:
List<Integer> list = new ArrayList<>(Arrays.asList(array));
-
- 从 List 转换为数组:
- 使用 toArray() 方法:可以使用 List 的 toArray() 方法将 List 转换为数组。例如:
List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); Integer[] array = list.toArray(new Integer[list.size()]);
- 使用 toArray() 方法:可以使用 List 的 toArray() 方法将 List 转换为数组。例如:
注意:通过 toArray() 方法转换得到的数组是对象数组,如果希望转换为基本类型数组(如 int[]),需要手动进行转换。
需要注意的是,数组和 List 之间的转换只是将数据从一种数据结构转换为另一种数据结构,并不会进行数据的复制。在进行转换时,对其中一个数据结构的修改会影响到另一个数据结构的内容。
🍁🍁 08. ArrayList 和 Vector 的区别是什么?
ArrayList 和 Vector 有以下几点区别:
-
线程安全性:
- ArrayList 不是线程安全的,它的方法没有进行同步处理,因此在多线程环境下使用时需要进行外部同步控制。
- Vector 是线程安全的,它的方法在内部使用了同步关键字进行同步处理,因此它可以在多线程环境下安全地使用。
-
动态增长:
- ArrayList 和 Vector 都支持动态增长,即当元素数量超过当前容量时,会自动增加容量。
- ArrayList 的容量增加的策略是当前容量不足时增加原容量的一半,Vector 则是增加当前容量的两倍。
-
初始容量的增长:
- ArrayList 的初始容量为 10,当元素数量超过初始容量时,会自动进行容量增长。
- Vector 的初始容量为 10,如果需要更大的初始容量,可以通过构造函数指定。
-
性能:
- ArrayList 在单线程环境下的性能会稍微优于 Vector,因为 ArrayList 不需要进行额外的同步处理。
- Vector 在多线程环境下的性能可能会受到同步的影响稍微低于 ArrayList。
综上所述,ArrayList 和 Vector 最主要的区别在于线程安全性和性能。一般情况下,如果在单线程环境下使用,并且不需要线程安全性保证,可以使用 ArrayList。如果在多线程环境下使用,或者需要线程安全性保证,可以使用 Vector。然而,由于现代 Java 提供了更好的线程安全的集合类(如 CopyOnWriteArrayList),Vector 的使用已经相对较少。
以下是一个表格,说明了 ArrayList 和 Vector 之间的区别:
特性 | ArrayList | Vector |
---|---|---|
线程安全性 | 非线程安全 | 线程安全 |
动态增长 | 支持动态增长 | 支持动态增长 |
初始容量增长 | 初始容量为 10,自动增长 | 初始容量为 10,可通过构造函数指定初始容量 |
性能 | 在单线程环境下性能较好 | 在多线程环境下或需要线程安全性时性能尚可 |
该表格总结了 ArrayList 和 Vector 之间的主要区别,可以帮助开发人员根据需要选择适合的集合类。
🍁🍁 09. Array 和 ArrayList 有何区别?
Array 和 ArrayList 有以下几点区别:
-
大小固定 vs 大小可变:
- Array(数组)的大小是固定的,在创建数组时需要指定大小,并且在后续操作中无法动态改变其大小。
- ArrayList(动态数组)的大小是可变的,它可以根据需要自动增长或缩小。
-
数据类型:
- Array 可以存储任何类型的元素,包括基本类型和引用类型。
- ArrayList 只能存储对象类型(如 Integer、String),不能直接存储基本类型(如 int、char),需要使用对应的包装类。
-
内存管理:
- Array 在内存中是一块连续的存储空间,元素的引用可以直接通过索引进行快速访问和操作。
- ArrayList 在内存中以动态数组的形式存在,元素在内存中不必连续存储,而是通过索引和引用进行访问和操作。
-
自动装箱和拆箱:
- Array 不支持自动装箱和拆箱,对于基本类型需要手动进行转换。
- ArrayList 支持自动装箱和拆箱,可以直接存储和操作基本类型的包装类,会自动进行装箱和拆箱的转换。
-
功能和灵活性:
- Array 相对功能较少,只提供了通过索引访问和操作元素的基本功能。
- ArrayList 提供了丰富的操作方法(如添加、删除、插入、查找等),提供了更高的灵活性和操作便利性。
需要根据具体需求和场景来选择使用 Array 还是 ArrayList。如果大小固定、速度要求较高或直接操作基本类型数据,则可以使用 Array。如果需要动态大小、更多的操作方法和便利性,则可以选择使用 ArrayList。
以下是一个表格,说明了 Array 和 ArrayList 之间的区别:
特性 | Array | ArrayList |
---|---|---|
大小固定 vs 大小可变 | 大小固定,无法动态改变 | 大小可变,可以根据需要自动增长或缩小 |
数据类型 | 可以存储任何类型的元素,包括基本类型和引用类型 | 只能存储对象类型,不能直接存储基本类型,需要使用对应的包装类 |
内存管理 | 连续的存储空间,通过索引快速访问和操作 | 动态数组形式,通过索引和引用访问和操作 |
自动装箱和拆箱 | 不支持自动装箱和拆箱 | 支持自动装箱和拆箱 |
功能和灵活性 | 功能较少,基本的访问和操作功能 | 提供了丰富的操作方法,灵活性和操作便利性较高 |
该表格总结了 Array 和 ArrayList 之间的主要区别,有助于开发人员根据需求选择适合的数据结构。
🍁🍁 10. 在 Queue 中 poll()和 remove()有什么区别?
在 Queue 接口中,poll() 和 remove() 方法都用于从队列中获取并移除头部元素,但它们在处理空队列时有所不同:
-
poll() 方法:
- 当队列为空时,poll() 方法会返回 null 值,表示队列为空,不会抛出异常。
- 如果队列非空,poll() 方法会移除并返回队列的头部元素。
-
remove() 方法:
- 当队列为空时,remove() 方法会抛出 NoSuchElementException 异常,表示队列为空,无法执行移除操作。
- 如果队列非空,remove() 方法会移除并返回队列的头部元素。
因此,主要区别在于处理空队列时的行为:poll() 方法返回 null,而 remove() 方法抛出异常。在使用时,应根据具体需求考虑如何处理可能出现的空队列情况。
以下是一个表格,说明了 Queue 中 poll() 和 remove() 方法的区别:
特性 | poll() | remove() |
---|---|---|
空队列时的处理 | 返回 null 值,表示队列为空 | 抛出 NoSuchElementException 异常,表示队列为空 |
非空队列时的处理 | 移除并返回队列的头部元素 | 移除并返回队列的头部元素 |
返回值 | 返回 null 或队列头部元素 | 队列头部元素 |
该表格总结了 Queue 中 poll() 和 remove() 方法在处理空队列和非空队列时的行为差异,以及它们的返回值。开发人员可以根据具体情况选择适合的方法,进行相应的异常处理或空值检查。
🍁🍁 11. 哪些集合类是线程安全的?
在 Java 中,有一些集合类是线程安全的,也就是说它们可以在多线程环境下安全地进行并发操作。一些常见的线程安全的集合类包括:
-
ConcurrentHashMap:线程安全的哈希表,适合在多线程环境下进行并发读写操作。
-
CopyOnWriteArrayList:线程安全的列表,采用"写时复制"策略,在写操作时复制一份新的数组,适合读操作远远多于写操作的场景。
-
CopyOnWriteArraySet:线程安全的集合,基于 CopyOnWriteArrayList 实现,适合读操作远远多于写操作的场景。
-
ConcurrentLinkedQueue:线程安全的队列,适合在多线程环境下进行并发操作。
-
ConcurrentSkipListMap 和 ConcurrentSkipListSet:基于跳表实现的线程安全的 Map 和 Set。
需要注意的是,虽然这些集合类是线程安全的,但并不意味着可以完全避免同步和并发问题,仍然需要在具体的业务逻辑中考虑并发访问的情况,使用这些线程安全的集合类并结合合适的同步手段来确保并发操作的安全性。
🍁🍁 12. 迭代器 Iterator 是什么?
迭代器(Iterator)是一种用于遍历(遍历访问)集合(如列表、集合、映射等)元素的对象。它提供了一种统一的方式来遍历不同类型的集合,而不需要了解集合的底层结构。
迭代器通常具有以下几个常见的方法:
- hasNext():检查迭代器中是否还有下一个元素。
- next():获取迭代器中的下一个元素。
- remove():从集合中移除迭代器返回的当前元素(可选操作)。
通过使用迭代器,我们可以以一种统一的方式遍历各种类型的集合元素,无需关心集合是数组、链表还是其他数据结构。它提供了一种简洁、安全且高效的方式来访问和操作集合,同时也避免了直接暴露底层集合的结构。
Java 中的集合框架(如 List、Set、Map 等)都实现了迭代器接口(Iterator),通过调用集合对象的 iterator() 方法可以获取对应的迭代器对象,从而进行迭代操作。
🍁🍁 13. Iterator 怎么使用?有什么特点?
使用 Iterator 进行遍历集合的一般步骤如下:
-
通过调用集合对象的 iterator() 方法获取迭代器对象,例如:
Iterator<E> iterator = collection.iterator();
(这里的<E>
表示具体的元素类型)。 -
使用循环(如 while 或 for)结合 hasNext() 和 next() 方法进行迭代遍历,例如:
while (iterator.hasNext()) {E element = iterator.next();// 对元素进行操作 }
-
可选操作:如果需要在迭代过程中删除集合中的某个元素,可以使用 remove() 方法,例如:
iterator.remove();
。
Iterator 的特点包括:
- 统一操作:Iterator 提供了一种统一的方式来遍历不同类型的集合,无需知道集合的具体实现细节,使遍历操作更加简洁、一致且易于使用。
- 安全性:通过 Iterator 遍历集合,可以避免在遍历过程中修改集合,以防止并发修改带来的问题。
- 只读操作:Iterator 接口中的 remove() 方法允许在迭代过程中删除集合元素,但不允许直接修改集合元素的值。
- 快速失败:Iterator 在迭代过程中,如果其他线程对集合进行了结构上的更改(如增加、删除元素),则会抛出 ConcurrentModificationException 异常,以保证在多线程环境下迭代器的安全性。
需要注意的是,Iterator 只能单向遍历,一旦开始遍历,就不能回退或再次遍历,如果需要重新遍历,需要重新获取迭代器对象。
🍁🍁 14. Iterator 和 ListIterator 有什么区别?
Iterator 和 ListIterator 都是 Java 中用于遍历集合的接口,它们有以下区别:
-
遍历方向:
- Iterator:只能向前遍历集合,且只能使用 hasNext() 和 next() 方法。
- ListIterator:可以双向遍历 List,除了具有 Iterator 的功能外,还可以使用 hasPrevious() 和 previous() 方法向前遍历,以及使用 add()、set() 和 remove() 方法修改集合中的元素。
-
List 支持:
- Iterator:可以用于遍历任何类型的集合(如 List、Set、Map),但只能使用基本的遍历能力,无法在遍历过程中修改集合(除非使用 remove())。
- ListIterator:专门用于遍历 List,可以对 List 进行增删改查的操作,具有更强的功能。
-
元素索引:
- Iterator:没有索引的概念,只能判断是否有下一个元素,并逐个访问元素。
- ListIterator:除了 hasNext() 和 next() 方法,还可以获得当前访问元素的索引,通过 previous() 和 hasPrevious() 方法可以向前遍历。
总的来说,Iterator 是最基本且通用的集合遍历接口,适用于任何类型的集合,但功能有限。而 ListIterator 是 Iterator 的子接口,只能用于 List 集合,并且提供了更多的遍历和修改功能。因此,在需要对 List 进行双向遍历或修改操作时,可以使用 ListIterator。
以下是 Iterator 和 ListIterator 的区别的简单表格说明:
特性 | Iterator | ListIterator |
---|---|---|
遍历方向 | 单向遍历,只能向前遍历 | 双向遍历,可以向前和向后遍历 |
支持的集合 | 适用于所有类型的集合(List、Set、Map 等) | 仅适用于 List 接口 |
元素索引 | 无 | 可以获取当前访问元素的索引,可以向前遍历 |
读操作 | hasNext():检查是否有下一个元素 | hasNext():检查是否有下一个元素 |
next():获取下一个元素 | next():获取下一个元素 | |
写操作 | 不支持 | add():在当前元素之前添加元素 |
set():替换当前元素的值 | ||
remove():删除当前元素 | ||
add(E e):在当前元素之后添加元素 | ||
并发修改 | 不支持,如果在迭代过程中修改集合,会抛出 ConcurrentModificationException 异常 | 不支持并发修改,如果在迭代过程中修改集合,可能会导致不确定的行为 |
遍历时删除 | 需要使用 Iterator 的 remove() 方法删除当前元素 | 可以使用 ListIterator 的 remove() 方法删除当前元素 |
遍历时修改 | 不支持 | 可以使用 ListIterator 的 set() 方法修改当前元素的值 |
需要根据具体的需求来选择使用 Iterator 还是 ListIterator。如果只需要简单地遍历集合并访问元素,可以使用 Iterator;如果需要对 List 进行双向遍历或修改操作,可以使用 ListIterator。
🍁🍁 15. 怎么确保一个集合不能被修改?
要确保一个集合不能被修改,可以采取以下措施:
-
使用不可变集合类:Java 中提供了一些不可变的集合类,如 Collections.unmodifiableXXX() 方法可以生成一个不可修改的集合,例如:
List<String> originalList = new ArrayList<>(); List<String> unmodifiableList = Collections.unmodifiableList(originalList);
这样生成的 unmodifiableList 就是一个不可修改的集合了,对其进行修改操作会抛出 UnsupportedOperationException 异常。
-
使用只读接口:将集合对象通过只读接口暴露给外部,只在特定的情况下暴露可修改的集合对象。例如,定义一个只读的方法返回集合:
public List<String> getReadOnlyList() {return Collections.unmodifiableList(internalList); }
-
使用防御性复制:在需要传递集合副本时,不直接传递原始集合对象,而是通过拷贝产生一个新的集合对象进行传递。这样即使外部修改了副本,也不会影响原始集合的数据。例如:
public List<String> getCopyOfList() {// 防御性复制return new ArrayList<>(originalList); }
通过以上方法可以较好地确保一个集合不被修改,从而保护数据的完整性。但需要注意的是,使用不可变集合类或只读接口可以在一定程度上保护集合不被修改,但无法防止原始集合内部数据的修改。因此,在设计数据结构时,也需要考虑数据安全性来更好地保护集合不被修改。
🍁🍁 16. 如何实现数组和 List 之间的转换?
要实现数组和 List 之间的转换,可以使用 Java 中的一些内置方法或者手动遍历来进行转换。
从数组转换为 List:
- 使用 Arrays 类的 asList() 方法:
String[] array = {"a", "b", "c"};
List<String> list = Arrays.asList(array);
- 手动遍历并添加到 List:
String[] array = {"a", "b", "c"};
List<String> list = new ArrayList<>(array.length);
Collections.addAll(list, array);
从 List 转换为数组:
- 使用 List 的 toArray() 方法:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
String[] array = list.toArray(new String[0]);
通过上述方法,就可以比较方便地实现数组和 List 之间的相互转换。需要注意的是,在进行转换时,需要考虑数组和 List 的元素类型是否匹配,以及转换后数据结构的可操作性。
🍁🍁 17. comparable 和 comparator的区别?
Comparable 和 Comparator 是 Java 中用于比较对象的两种接口,它们的区别如下:
- Comparable 接口:
- java.lang.Comparable 接口是在要比较的类的定义内部实现的,被用来指定该类的自然排序顺序。只能在被比较的对象上实现 Comparable 接口。
- 实现 Comparable 接口的类在调用 Collections.sort() 方法或 Arrays.sort() 方法时,会自动按照实现 Comparable 接口中的 compareTo() 方法进行排序。
- compareTo() 方法返回一个整数值,用于描述当前对象和另一个对象的大小关系。如果返回负数,则表示当前对象小于另一个对象;如果返回正数,则表示当前对象大于另一个对象;如果返回 0,则表示两个对象相等。
示例:
public class Person implements Comparable<Person> {private String name;private int age;// 省略其他代码@Overridepublic int compareTo(Person otherPerson) {return this.age - otherPerson.age;}
}
- Comparator 接口:
- java.util.Comparator 接口是在要比较的类的外部单独实现的,用于自定义不同的比较方式。可以为同一个类创建多个不同的比较器,并在需要的时候指定所需的比较器进行排序。
- 实现 Comparator 接口的类在调用排序方法时,需要手动传入相应的比较器对象。
- compare() 方法返回一个整数值,用于描述两个对象的大小关系,同样遵循返回负数、正数、0 的规则。
示例:
public class AgeComparator implements Comparator<Person> {@Overridepublic int compare(Person p1, Person p2) {return p1.getAge() - p2.getAge();}
}// 使用方式示例
List<Person> personList = new ArrayList<>();
// ... 添加元素到 personList 中
AgeComparator ageComparator = new AgeComparator();
Collections.sort(personList, ageComparator);
总体来说,Comparable 是在对象自身定义的一种比较方式,而 Comparator 则是针对某个类的外部实现的一种比较方式,因此可以灵活地定义多个不同的比较器来满足不同的排序需求。
区别 | Comparable | Comparator |
---|---|---|
定义位置 | 在对象的类内部实现 | 在对象的类外部单独实现 |
排序触发 | 当调用 Collections.sort() 或 Arrays.sort() 方法时,会自动按照实现 Comparable 接口中的 compareTo() 方法进行排序 | 需要手动传入相应的 Comparator 对象进行排序 |
作用 | 用于指定类的自然排序顺序 | 用于自定义不同的比较方式 |
可扩展性 | 类只能实现一个 Comparable 接口 | 可为同一个类创建多个不同的 Comparator 实现 |
比较方法 | compareTo() 方法返回一个整数值,用于描述当前对象和另一个对象的大小关系 | compare() 方法返回一个整数值,用于描述两个对象的大小关系 |
以上是 Comparable 和 Comparator 两种比较接口的主要区别。
🍁🍁 18. HashMap是如何解决哈希冲突的?
在Java中,HashMap使用哈希表(Hash Table)作为其底层数据结构来存储键值对。当不同的键被映射到相同的哈希桶(bucket)时,就会发生哈希冲突。HashMap使用了一种开放地址法中的解决冲突的方法,称为"链地址法",具体步骤如下:
- 当发生哈希冲突时,HashMap会将具有相同哈希值的键值对存储在同一个哈希桶中,形成一个链表(或者红黑树,后续会解释)。
- 每个节点(键值对)都包含键、值和一个指向下一个节点的指针。
- 当需要插入或查找一个键值对时,HashMap会计算键的哈希值,然后在对应的哈希桶中搜索键。
- 在哈希桶中,HashMap会遍历链表中的节点,通过键的equals()方法与目标键进行比较,找到匹配的键值对。
- 如果是插入操作,HashMap会将新的键值对添加到链表的末尾。
- 如果是查找操作,HashMap会返回匹配的键值对。
除了链地址法,当链表的长度过长时,为了提高查询效率,HashMap还使用了红黑树(自平衡二叉查找树)优化过程。当链表长度达到一定阈值(默认为8)时,HashMap会将链表转换为红黑树。这样可以将查找操作的时间复杂度从O(n)降低到O(log n)。
总结起来,HashMap解决哈希冲突的方法是在发生冲突时使用链地址法将具有相同哈希值的键值对存储在同一个哈希桶中,并在链表过长时使用红黑树进行优化。这样可以保证HashMap在处理哈希冲突时能够高效地进行插入和查找操作。
🍁🍁 19. 说一下 HashMap 的实现原理?
HashMap 是 Java 中最常用的集合实现之一,它基于哈希表(Hash Table)实现。下面是 HashMap 的实现原理:
-
哈希函数:HashMap 使用对象的 hashCode() 方法生成哈希码,通过这个哈希码进行映射。对不同的对象,哈希函数应该尽量保证生成的哈希码分布均匀,避免产生太多的哈希冲突。
-
哈希桶数组:HashMap 内部使用一个数组来存储数据,这个数组被称为哈希桶数组。桶数组的初始大小是默认的容量(一般为16),随着元素的增加而自动扩容。
-
冲突解决:当不同的键映射到相同的哈希码时,会发生哈希冲突。HashMap使用链地址法来解决冲突。具体而言,相同哈希码的键值对会存储在同一个桶中,并以链表的形式组织起来。
-
红黑树优化:当链表的长度超过一定阈值(默认为8),HashMap会将链表转换为红黑树,以提高查找性能。当红黑树的大小降低到一定程度时(默认为6),又会将红黑树转换回链表。
-
负载因子与扩容:负载因子指的是当前哈希桶数组已使用桶的占比,当负载因子超过一定阈值(默认为 0.75)时,HashMap会触发扩容操作。扩容会创建一个更大的哈希桶数组,并将原有的键值对重新分配到新的桶中,以减少冲突的可能性。
-
桶中的链表或红黑树:每个桶中存储键值对的结构可以是链表,也可以是红黑树。当元素数量较少时,使用链表结构;当元素数量较多时,使用红黑树结构。这样可以保持在不同情况下的高效查找性能。
总结起来,HashMap 的实现原理是使用哈希函数将键映射到哈希码,再根据哈希码在哈希桶数组中存储键值对。当不同键产生哈希冲突时,使用链地址法解决冲突,并通过红黑树优化链表的性能。同时,根据负载因子和扩容机制,保证哈希表的装载因子在一定范围内,以提高效率。
🍁🍁 20. 说一下 HashSet 的实现原理?
HashSet 是 Java 中常用的集合实现之一,它基于哈希表(Hash Table)实现。下面是 HashSet 的实现原理:
-
哈希函数:HashSet 使用对象的 hashCode() 方法生成哈希码,通过这个哈希码进行映射。对不同的对象,哈希函数应该尽量保证生成的哈希码分布均匀,提高哈希表的效率。
-
哈希桶数组:HashSet 内部使用一个哈希桶数组来存储元素。桶数组的初始大小是默认的容量(一般为16),随着元素的增加而自动扩容。
-
冲突解决:当不同的元素映射到相同的哈希码时,会发生哈希冲突。HashSet 使用链地址法(链表或红黑树)来解决冲突。相同哈希码的元素会存储在同一个桶中,并以链表或红黑树的形式组织起来。
-
唯一性保证:HashSet 保证集合中的元素是唯一的。当插入一个新元素时,HashSet 会先检查该元素是否已经存在于集合中。通过元素的哈希码和 equals() 方法来判断元素是否重复。
-
负载因子与扩容:类似于 HashMap,HashSet 也有负载因子和扩容机制。负载因子表示哈希表已使用桶的占比,当负载因子超过一定阈值(默认为 0.75)时,HashSet 会触发扩容操作。扩容会创建一个更大的哈希桶数组,并将原有的元素重新分配到新的桶中,以减少冲突的可能性。
总结起来,HashSet 的实现原理是通过哈希函数将元素映射到哈希码,并根据哈希码在哈希桶数组中存储元素。当不同元素产生哈希冲突时,使用链地址法解决冲突。通过哈希码和 equals() 方法保证集合中的元素唯一性。同时,根据负载因子和扩容机制,保证哈希表的装载因子在一定范围内,以提高效率。
此外,HashSet 也是基于HashMap实现的,它实际上是通过HashMap的键来实现的。在HashSet中,元素的值被存储为HashMap的键,而所有的value都指向同一个Object对象。这样,在HashSet中插入元素实际上是向HashMap中插入键值对,而值指向同一个Object对象。这也解释了为什么HashSet不允许重复元素,因为HashMap不允许重复的键。因此,HashSet的实现依赖于HashMap的实现。
在具体操作上,向HashSet中插入元素时,程序会首先计算元素的哈希值,然后将其放置在对应的哈希桶中。如果元素已经存在于集合中,插入操作将会失败。这样,在进行查找或插入操作时,HashSet能够通过哈希表快速定位元素,并且在理想情况下具有常数时间的性能。
因此,HashSet的实现原理可以总结为:利用HashMap实现,通过哈希函数将元素映射到哈希桶中,使用HashMap中键唯一的特性来保证集合中元素的唯一性,并通过负载因子和扩容机制来保证集合的性能。