Java笔试面试题AI答之集合(3)
文章目录
- 13. 简述ArrayList与LinkedList的区别?
- 1. 内部实现
- 2. 性能特性
- 3. 适用场景
- 14. Hashtable与HashMap有什么不同之处?
- 1. 继承的父类
- 2. 线程安全性
- 3. 键和值的null处理
- 4. 初始化容量和扩容机制
- 5. 遍历方式
- 6. 迭代器的行为
- 7. 性能
- 8. 其他方法
- 15. Java中的HashSet,内部是如何工作的?
- HashMap 的工作原理
- HashSet 的工作原理
- 16. ArrayList和HashMap的默认大小 ?
- ArrayList 的默认大小
- HashMap 的默认大小
- 总结
- 17. 说明Java中使用 Collections的最佳实践?
- 18. 简述Java用哪两种方式来实现集合的排序?
- 1. 使用`Collections.sort()`方法
- 2. 使用`TreeSet`或`TreeMap`等自动排序的集合类
- 总结
13. 简述ArrayList与LinkedList的区别?
ArrayList 和 LinkedList 都是 Java 集合框架(Java Collections Framework)中提供的用于存储元素的集合类,它们实现了 List 接口,因此具有列表的共有特性,如元素的有序性、可重复性和动态数组的大小。然而,它们在内部实现、性能特性以及适用场景上存在显著的差异。
1. 内部实现
-
ArrayList:基于动态数组的数据结构。当元素被添加到
ArrayList时,如果数组的大小不足以容纳所有元素,那么会创建一个新的数组来存放这些元素,新数组的大小通常是旧数组大小的1.5倍(具体实现可能因JVM实现而异),然后将旧数组的元素复制到新数组中。因此,ArrayList在添加或删除元素时(尤其是在列表的开头或中间位置)可能需要移动大量元素,这可能导致较高的性能开销。 -
LinkedList:基于双向链表的数据结构。每个元素都包含数据部分和两个引用,分别指向前一个元素和后一个元素。这种结构使得
LinkedList在添加或删除元素时(尤其是在列表的开头或中间位置)效率较高,因为只需要修改相关元素的引用即可,而不需要移动其他元素。
2. 性能特性
-
访问性能:由于
ArrayList是基于数组实现的,因此它可以通过索引快速访问元素(时间复杂度为 O(1))。而LinkedList需要从头节点或尾节点开始遍历链表来访问元素(时间复杂度为 O(n))。 -
插入和删除性能:在
ArrayList的末尾添加或删除元素时效率较高(时间复杂度为 O(1)),但在列表的开头或中间位置进行这些操作时效率较低(时间复杂度为 O(n))。LinkedList在任何位置添加或删除元素时效率都相对较高(时间复杂度为 O(1)),但需要注意的是,频繁地在链表的两端进行添加或删除操作可能会导致内存碎片。
3. 适用场景
-
ArrayList:适用于需要频繁访问元素,但不太频繁地插入和删除元素的情况。
-
LinkedList:适用于需要频繁插入和删除元素,但不经常通过索引访问元素的情况。此外,
LinkedList还提供了额外的操作,如addFirst(),addLast(),getFirst(),getLast(),removeFirst(),removeLast()等,这些操作对于实现栈、队列等数据结构非常有用。
总的来说,选择 ArrayList 还是 LinkedList 取决于你的具体需求,包括元素访问频率、插入和删除操作的频率以及是否需要额外的链表操作等。
14. Hashtable与HashMap有什么不同之处?
Hashtable与HashMap在Java中都是常用的映射表实现,但它们在多个方面存在显著的不同。以下是它们之间的主要区别:
1. 继承的父类
- Hashtable:继承自
java.util.Dictionary类,实现了Map、Cloneable、Serializable接口。 - HashMap:继承自
java.util.AbstractMap类,同样实现了Map、Cloneable、Serializable接口。
2. 线程安全性
- Hashtable:是线程安全的,因为它的方法(如put和get)被
synchronized关键字修饰,支持在多线程环境下使用。 - HashMap:不是线程安全的,如果在多线程环境下使用,需要外部同步或使用
Collections.synchronizedMap等方法进行包装。
3. 键和值的null处理
- Hashtable:不允许键(key)和值(value)为null。
- HashMap:允许键为null,但这样的键只能有一个,并且键为null的键值对总是被放在哈希表的第一位。HashMap的值可以为null。
4. 初始化容量和扩容机制
- Hashtable:默认的初始容量为11,扩容时容量为原来的2倍加1(即newCapacity = oldCapacity * 2 + 1)。
- HashMap:默认的初始容量为16(在Java 8及以后版本中),扩容时容量为原来的2倍(即newCapacity = oldCapacity * 2)。
5. 遍历方式
- Hashtable:支持通过
Enumeration和Iterator两种方式遍历。 - HashMap:只支持通过
Iterator遍历。
6. 迭代器的行为
- HashMap:迭代器是fail-fast的,如果在迭代过程中有其他线程对HashMap的结构进行了修改(非通过迭代器自身的remove方法),则会抛出
ConcurrentModificationException异常。 - Hashtable:其枚举器(Enumerator)不是fail-fast的,不会抛出
ConcurrentModificationException。
7. 性能
- Hashtable:由于它的线程安全性,其性能通常比HashMap低,尤其是在单线程环境下。
- HashMap:在多线程环境下需要外部同步,但其在单线程环境下的性能通常优于Hashtable。
8. 其他方法
- Hashtable:提供了
elements()和contains(Object value)等方法,这些方法在HashMap中不可用或表现不同。 - HashMap:没有直接提供类似
elements()的方法,其containsValue(Object value)方法用于检查值是否存在。
综上所述,Hashtable和HashMap在继承关系、线程安全性、null处理、初始化容量和扩容机制、遍历方式、迭代器行为、性能以及提供的方法等方面存在显著差异。在选择使用哪一个时,需要根据具体的应用场景和需求来决定。
15. Java中的HashSet,内部是如何工作的?
HashSet 是 Java 集合框架中的一个重要类,它实现了 Set 接口。HashSet 不保证集合的迭代顺序,并且允许存储 null 元素。其内部主要基于 HashMap 来实现。理解 HashSet 的工作原理,首先需要了解 HashMap 的工作原理。
HashMap 的工作原理
HashMap 是基于哈希表的 Map 接口的非同步实现,允许使用 null 值和 null 键。它通过将键的哈希码映射到表的索引来快速访问元素。HashMap 实际上是一个数组(通常称为表)和链表(在 Java 8 及更高版本中,当链表长度超过某个阈值时,会转换为红黑树以提高性能)的组合结构。
- 哈希函数:
HashMap使用键的哈希码来找到存储键值对的索引。通过调用键对象的hashCode()方法来获取哈希码,然后使用某种算法(通常是哈希码对数组长度取模)来确定数组中的索引位置。 - 碰撞解决:由于不同的键可能会产生相同的哈希码(即哈希冲突),
HashMap使用链表(或红黑树)来存储所有哈希码相同的元素。这些元素被链接到同一个索引位置。 - 扩容:当哈希表中的条目数超过了负载因子(默认值为 0.75)与当前容量(即数组大小)的乘积时,
HashMap会自动扩容,通常是当前容量的两倍。扩容后,所有的键值对都需要重新计算哈希值并重新定位。
HashSet 的工作原理
HashSet 内部实际上是通过一个 HashMap 实现的,但它只使用了 HashMap 的键(key)部分,而忽略了值(value)部分。在 HashSet 中,所有的键都是集合的元素,而它们的值则是一个固定的对象(在 Java 8 中,这个固定对象是一个名为 PRESENT 的静态常量,其类型为 Object)。
- 添加元素:当向
HashSet添加一个元素时,实际上是将该元素作为键添加到其内部的HashMap中,值被设置为PRESENT。 - 查找元素:查找元素时,也是通过
HashMap的get()方法来完成的,但检查的是HashMap中是否存在该键,而不是它的值。 - 删除元素:删除元素则是从
HashMap中移除对应的键值对。
由于 HashSet 依赖于 HashMap 的哈希机制来处理元素的存储和查找,因此 HashSet 也继承了 HashMap 的优点和缺点,包括高效的存取性能和可能的哈希冲突问题。
16. ArrayList和HashMap的默认大小 ?
ArrayList 和 HashMap 的默认大小在Java中是不同的,具体如下:
ArrayList 的默认大小
- 默认初始容量:ArrayList的底层数组默认初始化大小为10。然而,需要注意的是,在使用无参构造函数创建ArrayList时,并不会立即分配一个大小为10的数组。实际上,无参构造函数会将一个空数组(
DEFAULTCAPACITY_EMPTY_ELEMENTDATA)赋值给底层数组。当第一次添加元素时,才会分配一个大小为10的数组。 - 扩容机制:当ArrayList中的元素数量超过当前数组大小时,会自动进行扩容。扩容时,通常会创建一个新的数组,其大小为原数组大小的1.5倍(向下取整),然后将原数组的元素复制到新数组中。不过,扩容的临界值是
Integer.MAX_VALUE - 8,这是为了防止数组大小超过Java中整型数的最大值。
HashMap 的默认大小
- 默认初始容量:HashMap的默认初始容量是16。这个大小是HashMap内部实现中用于存储键值对的数组的大小。
- 扩容机制:当HashMap中的元素数量达到容量的75%(默认负载因子为0.75)时,HashMap会自动进行扩容。扩容时,通常会创建一个新的数组,其大小为原数组大小的2倍,然后重新计算每个键的哈希值,并将其放入新的数组中的适当位置。
总结
| 集合类型 | 默认大小 | 扩容机制 |
|---|---|---|
| ArrayList | 底层数组默认初始化大小为10(但无参构造函数初始化为空数组,首次添加元素时分配大小为10的数组) | 扩容时,通常会创建一个新的数组,其大小为原数组大小的1.5倍(向下取整),但最大不超过Integer.MAX_VALUE - 8 |
| HashMap | 16 | 扩容时,通常会创建一个新的数组,其大小为原数组大小的2倍,并重新计算哈希值 |
这些默认大小和扩容机制是Java集合框架中为了优化性能和内存使用而设计的。在实际应用中,如果已知将要存储的元素数量,可以通过构造函数指定初始容量来避免不必要的扩容操作,从而提高性能。
17. 说明Java中使用 Collections的最佳实践?
在Java中使用Collections框架时,遵循一些最佳实践可以帮助你编写更高效、更可维护、更易于理解的代码。以下是一些关键的最佳实践:
-
选择合适的集合类型:
- 根据你的需求(如是否需要排序、是否允许重复元素等)选择最合适的集合类型。例如,如果需要保持元素的插入顺序,则使用
ArrayList;如果需要频繁地插入和删除元素,则考虑LinkedList;如果需要自动排序,则使用TreeSet或TreeMap。
- 根据你的需求(如是否需要排序、是否允许重复元素等)选择最合适的集合类型。例如,如果需要保持元素的插入顺序,则使用
-
使用不可变集合:
- 当集合在创建后不需要修改时,使用
Collections.unmodifiableList(),Collections.unmodifiableSet(),Collections.unmodifiableMap()等方法来创建不可变集合。这可以防止集合被意外修改,提高代码的安全性。
- 当集合在创建后不需要修改时,使用
-
利用Collections工具类:
Collections框架提供了许多静态方法,如sort(),shuffle(),reverse(),binarySearch(),max(),min(),fill(),copy()等,这些方法可以简化集合的操作。
-
避免使用原始类型的集合:
- 总是使用泛型集合,避免使用原始类型的集合(如
List,Set,Map等),因为泛型可以提供编译时类型检查,减少运行时错误。
- 总是使用泛型集合,避免使用原始类型的集合(如
-
合理控制集合的大小:
- 在可能的情况下,预估并控制集合的大小,以避免不必要的扩容操作。例如,在创建
ArrayList时,如果知道大概的元素数量,可以使用带有初始容量的构造函数。
- 在可能的情况下,预估并控制集合的大小,以避免不必要的扩容操作。例如,在创建
-
使用并发集合:
- 如果你的应用是多线程的,并且需要并发访问集合,那么应该使用
java.util.concurrent包下的并发集合,如ConcurrentHashMap,CopyOnWriteArrayList等。这些集合提供了比同步包装器(如Collections.synchronizedList())更好的并发性能。
- 如果你的应用是多线程的,并且需要并发访问集合,那么应该使用
-
避免在循环中修改集合:
- 如果需要在遍历集合的同时修改集合(如添加或删除元素),应该使用迭代器提供的
remove()方法(如果适用),或者使用Java 8引入的removeIf()方法(对于List和Set)。直接在循环中修改集合(如使用for-each循环和remove()方法)可能会导致ConcurrentModificationException异常。
- 如果需要在遍历集合的同时修改集合(如添加或删除元素),应该使用迭代器提供的
-
使用Stream API进行复杂操作:
- Java 8引入的Stream API提供了一种高效且表达力强的方式来处理集合。对于复杂的集合操作(如过滤、映射、归约等),使用Stream API可以使代码更加简洁和易于理解。
-
注意集合的线程安全性:
- 大多数Java集合都不是线程安全的。如果你需要在多线程环境中使用集合,请确保你了解并采取了适当的线程安全措施(如使用并发集合、同步代码块或锁等)。
-
文档和注释:
- 对于复杂的集合操作或自定义的集合类,提供清晰的文档和注释是非常重要的。这有助于其他开发者理解你的代码,并减少潜在的错误。
18. 简述Java用哪两种方式来实现集合的排序?
Java中实现集合排序主要有两种方式:
1. 使用Collections.sort()方法
Collections.sort()是Java集合框架中提供的一个静态方法,用于对List集合进行排序。它支持自然排序和自定义排序。
- 自然排序:当集合中的元素实现了
Comparable接口,并且重写了compareTo()方法时,可以使用Collections.sort()方法按照元素的自然顺序进行排序。对于可比较大小的数据类型(如Integer、Double等),其自然顺序通常是从小到大;对于字符串(String),其自然顺序是按照字典序从小到大。 - 自定义排序:如果需要对集合进行自定义排序,即按照程序员指定的顺序进行排序,可以让集合中的元素实现
Comparator接口,并重写compare()方法。然后,将自定义的Comparator对象作为第二个参数传递给Collections.sort()方法。
2. 使用TreeSet或TreeMap等自动排序的集合类
- TreeSet:
TreeSet是基于红黑树实现的,它可以保证集合元素处于排序状态。默认情况下,TreeSet会按照元素的自然顺序进行排序。如果元素没有实现Comparable接口,那么在创建TreeSet时需要传入一个自定义的Comparator对象来指定排序规则。 - TreeMap:与
TreeSet类似,TreeMap也是基于红黑树实现的,但它存储的是键值对。TreeMap可以保证所有的键处于排序状态。默认情况下,键会按照其自然顺序进行排序,但同样可以通过传入自定义的Comparator对象来改变排序规则。
总结
Java中实现集合排序的两种方式各有特点:
- 使用
Collections.sort()方法可以直接对List集合进行排序,支持自然排序和自定义排序,操作灵活。 - 使用
TreeSet或TreeMap等自动排序的集合类,则可以在添加元素时自动保持集合的排序状态,无需手动调用排序方法。这种方式适用于需要保持元素有序,且不需要频繁进行排序操作的场景。
在实际开发中,可以根据具体需求选择合适的排序方式。
答案来自文心一言,仅供参考
