1. Java 8 中与 Collection 相关的功能有哪些?
Java 8 对 Collection API 进行了重大更改。其中一些更改如下:
- 用于集合类的Java Stream API,支持顺序和并行处理。
- Iterable 接口扩展了 forEach()默认方法,我们可以使用它来迭代集合。它在与lambda表达式一起使用时非常有用,因为它的参数 Consumer 是一个函数接口。
Collection API
改进,例如接口forEachRemaining(Consumer action)
中的方法、Map
方法。Iterator
、replaceAll()
、compute()
、merge()
等。
2. 什么是 Java 集合框架?请列出集合框架的一些优点?
集合在每种编程语言中都有使用,最初的 Java 版本包含几个集合类:Vector、Stack、Hashtable、Array。但从更大的范围和使用来看,Java 1.2 提出了集合框架,将所有集合接口、实现和算法分组。Java 集合通过使用泛型和并发集合类实现线程安全操作已经取得了长足的进步。它还包括 Java 并发包中的阻塞接口及其实现。集合框架的一些好处是:
- 通过使用核心集合类而不是实现我们自己的集合类来减少开发工作量。
- 通过使用经过充分测试的集合框架类,代码质量得到提高。
- 通过使用 JDK 附带的集合类减少代码维护工作量。
- 可重用性和互操作性。
3. 集合框架中的泛型有什么好处?
Java 1.5 附带了泛型,所有集合接口和实现都大量使用了泛型。泛型允许我们提供集合可以包含的对象类型,因此如果您尝试添加任何其他类型的元素,则会引发编译时错误。这避免了运行时出现 ClassCastException,因为您将在编译时收到错误。
此外,泛型使代码变得干净,因为我们不需要使用强制类型转换和instanceof运算符。我强烈建议您阅读Java 泛型教程,以更好地理解泛型。
4. Java 集合框架的基本接口有哪些?
Collection是集合层次结构的根。集合表示一组称为其元素的对象。Java 平台不提供此接口的任何直接实现。
Set是不能包含重复元素的集合。此接口模拟数学集合抽象,用于表示集合,例如一副牌。
List是有序集合,可以包含重复元素。您可以从其索引访问任何元素。列表更像是具有动态长度的数组。
Map是将键映射到值的对象。映射不能包含重复的键:每个键最多可以映射到一个值。
其他一些接口是Queue, Dequeue, Iterator, SortedSet, SortedMap 和ListIterator。
5. 为什么 Collection 不扩展 Cloneable 和 Serializable 接口?
Collection 接口指定了一组称为元素的对象。如何维护元素取决于Collection的具体实现。例如,一些Collection实现(如List)允许重复元素,而其他实现(如Set)则不允许。许多Collection实现都有一个公共克隆方法。然而,将其包含在Collection的所有实现中是没有意义的。这是因为Collection是一种抽象表示。重要的是执行。在处理实际实现时,克隆或序列化的语义和含义会发挥作用;因此,具体的实现应该决定如何克隆或序列化它,甚至是否可以克隆或序列化。而且在所有实现中强制克隆和序列化的灵活性较低,限制性更强。具体的实现应该决定它是否可以被克隆或序列化。
6. 为什么 Map 接口不扩展 Collection 接口?
虽然Map接口及其实现是Collections Framework的一部分,但Map不是集合,集合也不是Map。因此,Map扩展Collection或反之亦然是没有意义的。如果Map扩展了Collection接口,那么元素在哪里?该映射包含键值对,并提供了将键或值列表作为集合检索的方法,但它不适合“元素组”范式。
7. 什么是迭代器?
Java迭代器(Iterator)是 Java 集合框架中的一种机制,是一种用于遍历集合(如列表、集合和映射等)的接口。
它提供了一种统一的方式来访问集合中的元素,而不需要了解底层集合的具体实现细节。
Java Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList 和 HashSet 等集合。
Iterator 是 Java 迭代器最简单的接口,ListIterator 是 Collection API 中的接口, 它扩展了 Iterator 接口。
8. Enumeration 和 Iterator 接口有什么区别?
Enumeration 的速度是Iterator 的两倍,并且使用的内存很少。Enumeration是非常基本的,符合基本需求。但与Enumeration 相比,Iterator要安全得多,因为它总是拒绝其他线程修改它正在迭代的集合对象。Iterator在Java集合框架中取代了Enumeration。Iterator允许调用者从底层集合中删除Enumeration无法删除的元素。Iterator方法名称已得到改进,以使其功能清晰。
9. 为什么没有像 Iterator.add() 这样的方法将元素添加到集合中?
语义不清楚,因为Iterator的契约对迭代顺序没有保证。但是请注意,ListIterator确实提供了一个添加操作,因为它确实保证了迭代的顺序。
10. 为什么 Iterator 没有一种不移动游标就能直接获取下一个元素的方法?
它可以在当前的Iterator接口之上实现,但由于它的使用很少,因此将其包含在每个人都必须实现的接口中是没有意义的。
11. Iterator 和 ListIterator 有什么区别?
- 我们可以使用Iterator遍历Set和List集合,而ListIterator只能用于List。
- 迭代器只能正向遍历,而ListIterator可用于双向遍历。
- ListIterator继承自Iterator接口,并具有额外的功能,如添加元素、替换元素、获取前一个和下一个元素的索引位置。
12. 迭代列表有哪些不同的方法?
我们可以用两种不同的方式迭代列表——使用迭代器和for每个循环。
List<String> strList = new ArrayList<>();
//using for-each loop
for(String obj : strList){
System.out.println(obj);
}
//using iterator
Iterator<String> it = strList.iterator();
while(it.hasNext()){
String obj = it.next();
System.out.println(obj);
}
使用迭代器更具线程安全性,因为它确保了如果底层列表元素被修改,它将抛出“ConcurrentModificationException”。
13. 你对迭代器“快速失败”属性有何理解?
每次我们尝试获取下一个元素时,迭代器都会对底层集合结构中的任何修改进行快速属性检查。如果发现任何修改,它将抛出“ConcurrentModificationException”。Collection类中迭代器的所有实现都是设计上快速失败的,除了并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
14. 迭代器快速失败和故障安全之间有什么区别?
迭代器故障安全属性与底层集合的克隆一起工作,因此它不受集合中任何修改的影响。按照设计,java.util.
包中的所有集合类都是快速失败的,而java.util.concurrent
中的集合类是故障安全的。故障快速迭代器抛出ConcurrentModificationException,而故障安全迭代器从不抛出ConcurrentModificationException。
15. 如何在迭代集合时避免 ConcurrentModificationException?
我们可以使用并发集合类来避免在迭代集合时出现“ConcurrentModificationException”,例如CopyOnWriteArrayList而不是ArrayList。
16. 为什么没有 Iterator 接口的具体实现?
迭代器接口声明了迭代集合的方法,但它的实现是collection实现类的责任。每个返回遍历迭代器的集合类都有自己的迭代器实现嵌套类。这允许集合类选择迭代器是快速失败还是安全失败。例如,ArrayList迭代器是故障快速的,而CopyOnWriteArrayList迭代器则是故障安全的。
17. 什么是UnsupportedOperationException?
UnsupportedOperationException是一个异常,用于表示该操作不受支持。它在JDK类和集合框架
Java.util中得到了广泛的应用,UnmodifiableCollection对所有“add”和“remove”操作都抛出此异常。
18. HashMap 在 Java 中如何工作?
HashMap将键值对存储在Map中。入口
静态嵌套类实现。HashMap基于哈希算法工作,在put
和get
方法中使用hashCode()和equals()方法。当我们通过传递键值对来调用put
方法时,HashMap使用key-hashCode()和哈希来找出存储键值对的索引。Entry存储在LinkedList中,因此如果已经存在条目,它会使用equals()方法检查传递的键是否已经存在,如果是,它会覆盖该值,否则会创建一个新条目并存储此键值Entry。当我们通过传递Key调用get
方法时,它再次使用hashCode()在数组中查找索引,然后使用equals()方法查找正确的Entry并返回其值。
关于HashMap,需要知道的其他重要事情是容量、负载因子、阈值大小调整。HashMap的初始默认容量为16,负载系数为0.75。阈值是容量乘以负载因子,每当我们试图添加一个条目时,如果映射大小大于阈值,HashMap会将映射的内容重新散列到一个容量更大的新数组中。容量总是2的幂,所以如果你知道你需要存储大量的键值对,例如在缓存数据库中的数据时,最好用正确的容量和负载因子初始化HashMap。
19. HashMap中的hashCode() 和 equals() 方法的重要性是什么?
HashMap使用Key对象hashCode()和equals()方法来确定放置键值对的索引。当我们试图从HashMap中获取值时,也会使用这些方法。如果这些方法没有正确实现,两个不同的键可能会产生相同的hashCode()和equals()输出,在这种情况下,HashMap将考虑相同的结果并覆盖它们,而不是将其存储在不同的位置。同样,所有不存储重复数据的集合类都使用hashCode()和equals()来查找重复数据,因此正确实现它们非常重要。equals()和hashCode()的实现应该遵循这些规则。
- 如果“o1.equals(o2)”,那么“o1.hashCode()==o2.hashCodes()”应始终为“true”。
- 如果
o1.hashCode()==o2.hashCode
为真,并不意味着o1.equals(o2)
为真。
20. 我们可以使用任何类作为 Map 的键吗?
我们可以使用任何类作为Map Key,但在使用它们之前应该考虑以下几点。
- 如果类重写equals()方法,它也应该重写hashCode()方法。
- 该类应遵循与equals()和hashCode()相关的所有实例的规则。这些规则请参考前面的问题。
- 如果类字段未在equals()中使用,则不应在hashCode()方法中使用它。
- 用户定义键类的最佳实践是使其不可变,以便可以缓存hashCode()值以获得快速性能。此外,不可变类确保hashCode()和equals()在未来不会改变,这将解决任何可变性问题。例如,假设我有一个类“MyKey”,我正在使用它作为HashMap键。
//传递的MyKey name参数用于equals()和hashCode()
MyKey key = new MyKey("Pankaj"); //假设hashCode=1234
myHashMap.put(key, "Value");
//下面的代码将更改键hashCode()和equals()
//但其位置没有改变。
key.setName("Amit");//假设新哈希码=7890
//下面将返回null,因为HashMap将尝试查找键
//在与存储的索引相同的索引中,但由于密钥发生了突变,
//将没有匹配项,它将返回null。
myHashMap.get(new MyKey("Pankaj"));
这就是为什么String和Integer主要用作HashMap键的原因。
21. Map 接口提供了哪些不同的 Collection 视图?
Map 接口提供三个集合视图:
Set<K> keySet()
:返回此映射中包含的键的Set视图。该集合由贴图支持,因此对贴图的更改会反映在集合中,反之亦然。如果在对集合进行迭代的同时修改了映射(通过迭代器的移除操作除外),则迭代的结果是未定义的。该集合支持元素移除,即通过Iterator remove、set.remove、removeAll、retainAll和clear操作从映射中移除相应的映射。它不支持add或addAll操作。Collection<V> values()
:返回此映射中包含的值的Collection视图。集合由映射支持,因此对映射的更改会反映在集合中,反之亦然。如果在对集合进行迭代时修改了映射(通过迭代器的remove操作除外),则迭代的结果是未定义的。该集合支持元素移除,即通过Iterator remove、collection.remove、removeAll、retainAll和clear操作从映射中移除相应的映射。它不支持add或addAll操作。Set<Map.Entry<K, V>> entrySet()
:返回此映射中包含的映射的Set视图。该集合由贴图支持,因此对贴图的更改会反映在集合中,反之亦然。如果在对集合进行迭代的同时修改了映射(除了通过迭代器的remove操作或迭代器返回的映射条目的setValue操作),则迭代的结果是未定义的。该集合支持元素移除,即通过Iterator remove、set.remove、removeAll、retainAll和clear操作从映射中移除相应的映射。它不支持add或addAll操作。
22. HashMap 和 Hashtable 有什么区别?
HashMap
和Hashtable
都实现了Map接口,看起来很相似,但是HashMap
和哈希表之间有以下区别。
HashMap
允许空键和空值,而Hashtable不允许空键或空值。Hashtable
方法是同步的,但HashMap不是。因此,HashMap更适合单线程环境,Hashtable更适合多线程环境。LinkedHashMap
是在Java 1.4
中作为HashMap
的子类引入的,所以如果你想要迭代顺序,你可以很容易地从HashMap切换到LinkedHashMap
,但对于迭代顺序不可预测的Hashtable来说,情况并非如此。HashMap
提供了一组键来迭代,因此它很快就会失败,但Hashtable提供了不支持此功能的键枚举。Hashtable
被认为是遗留类,如果你在迭代时想要修改Map,你应该使用ConcurrentHashMap
。
23. 如何在 HashMap 和 TreeMap 之间做出选择?
对于在Map
中插入、删除和定位元素,HashMap
提供了最佳选择。但是,如果需要按排序顺序遍历键,那么TreeMap
是更好的选择。根据集合的大小,向HashMap
添加元素,然后将映射转换为TreeMap
进行排序键遍历可能会更快。
24. ArrayList 和 Vector 有哪些相同点和不同点?
ArrayList
和Vector
相同点:
- 两者均基于索引,并由内部数组进行备份。
- 两者都保持插入顺序,我们可以按照插入顺序得到元素。
ArrayList
和Vector
的迭代器实现在设计上都是快速失败的。ArrayList
和Vector
都允许空值和使用索引号随机访问元素。
ArrayList
和Vector
之间的区别:
- 矢量已同步,而ArrayList未同步。但是,如果您在迭代时希望修改列表,则应使用CopyOnWriteArrayList。
ArrayList
比Vector
快,因为它没有同步带来的任何开销。ArrayList
更通用,因为我们可以使用Collections
实用程序类轻松地从中获取同步列表或只读列表。
25. Array 和 ArrayList 有什么区别?什么时候会使用 Array 而不是 ArrayList?
数组可以包含基元或对象,而ArrayList
只能包含对象。数组的大小是固定的,而ArrayList
的大小是动态的。数组不提供像ArrayList
这样的很多功能,如addAll
、removeAll
、迭代器等。虽然在处理列表时,ArrayList
是显而易见的选择,但也有一些时候数组是好用的。
- 如果列表的大小是固定的,并且主要用于存储和遍历它们。
- 对于原始数据类型列表,虽然集合使用自动装箱来减少编码工作量,但在处理固定大小的原始数据类型时,它仍然会使它们变慢。
- 如果你正在处理固定的多维情况,使用**[][]**比
List<List<>>
要容易得多。
26. ArrayList 和 LinkedList 有什么区别?
ArrayList
和LinkedList
都实现了List接口,但它们之间存在一些差异。
ArrayList
是由Array
支持的基于索引的数据结构,因此它提供对其元素的随机访问,性能为O(1),但LinkedList
将数据存储为节点列表,其中每个节点都链接到其前一个和下一个节点。因此,尽管有一种使用索引获取元素的方法,但在内部,它从开始遍历到到达索引节点,然后返回元素,因此性能为O(n),比ArrayList
慢。- 与
ArrayList
相比,LinkedList
中插入、添加或删除元素的速度更快,因为在中间添加元素时没有调整数组大小或更新索引的概念。 LinkedList
比ArrayList
消耗更多的内存,因为LinkedList中的每个节点都存储前一个和下一个元素的引用。
27. 哪些集合类提供对其元素的随机访问?
ArrayList
、HashMap
、TreeMap
、Hashtable
和Vector
类提供对其元素的随机访问。
28. 什么是 EnumSet?
java.util.EnumSet
是用于枚举类型的Set实现。枚举集中的所有元素必须来自创建集合时显式或隐式指定的单个枚举类型。EnumSet未同步,不允许使用空元素。它还提供了一些有用的方法,如copyOf(Collection c)
、of(E first,E…rest)
和complementOf(EnumSet)
。
29. 哪些集合类是线程安全的?
Vector
、Hashtable
、Properties
和Stack
是同步类,因此它们是线程安全的,可以在多线程环境中使用。Java 1.5 Concurrent API包含了一些集合类,它们允许在迭代时修改集合,因为它们处理集合的克隆,因此在多线程环境中使用它们是安全的。
30. 什么是并发集合类?
Java 1.5并发包(Java.util.Concurrent
)包含线程安全的集合类,允许在迭代时修改集合。按照设计,java.util.
包中的迭代器实现是快速失败的,并抛出ConcurrentModificationException
。但是java.util.concurrent
包中的迭代器实现是故障安全的,我们可以在迭代时修改集合。其中一些类是“CopyOnWriteArrayList
”、“ConcurrentHashMap
”、“CopyOnWriteArraySet
”。
31. 什么是 BlockingQueue?
java.util.concurrent.BlockingQueue
是一个队列,它支持在检索和删除元素时等待队列变为非空,并在添加元素时等待排队中有可用空间的操作。BlockingQueue
接口是java
集合框架的一部分,主要用于实现生产者-消费者问题。我们不需要担心等待BlockingQueue
中生产者或对象的空间可用,因为它是由BlockingQueue
的实现类处理的。Java提供了多种BlockingQueue
实现,如ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityBlockingQueue
和SynchronousQueue
等。
32. 什么是队列和堆栈,列出它们的区别?
Queue
和Stack
都用于在处理数据之前存储数据。
java.util.Queue
是一个接口,其实现类存在于java并发包中。队列允许按先进先出(FIFO)顺序检索元素,但并非总是如此。还有一个Deque
接口,允许从队列的两端检索元素。堆栈类似于队列,除了它允许按后进先出(LIFO)顺序检索元素。Stack
是一个扩展Vector
的类,而Queue
是一个接口。
33. 什么是 Collections 类?
java.util.Collections
是一个实用程序类,它只由对集合进行操作或返回集合的静态方法组成。它包含对集合进行操作的多态算法、返回由指定集合支持的新集合的“包装器”以及其他一些零碎的东西。此类包含集合框架算法的方法,如二分查找、排序、洗牌、反转等。
34. 什么是 Comparable 和 Comparator 接口?
Java提供了一个Comparable
接口,如果我们想使用数组或集合排序方法,任何自定义类都应该实现该接口。Comparable 接口有一个compareTo(T obj)
方法,排序方法使用该方法。我们应该重写此方法,使其在this
对象小于、等于或大于作为参数传递的对象时返回负整数、零或正整数。
但是,在大多数现实生活中,我们希望根据不同的参数进行排序。例如,作为一名首席执行官,我想根据工资对员工进行排序,人力资源部想根据年龄对他们进行排序。在这种情况下,我们需要使用Comparator
接口,因为Comparable.compareTo(Object o)
方法实现只能基于一个字段进行排序,我们无法选择要对对象进行排序的字段。比较器接口compare(Object o1,Object o2)
方法需要实现,该方法接受两个Object参数,如果第一个参数小于第二个参数,则应实现为返回负int
,如果它们相等,则返回零,如果第一次参数大于第二次参数,则返回正int
。
35. Comparable 和 Comparator 接口有什么区别?
Comparable
和Comparator
接口用于对对象的集合或数组进行排序。Comparable 接口用于提供对象的自然排序,我们可以使用它来提供基于单一逻辑的排序。比较器接口用于提供不同的排序算法,我们可以选择要用于对给定对象集合进行排序的比较器。
36. 我们如何对对象列表进行排序?
如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()
。如果我们需要对对象列表进行排序,我们可以使用Collections.sort()
。这两个类都重载了sort()
方法,用于自然排序(使用Comparable
)或基于条件排序(使用Comparator
)。集合内部使用Arrays
排序方法,因此两者具有相同的性能,除了集合需要一些时间将列表转换为数组。
37. 当将 Collection 作为参数传递给函数时,我们如何确保该函数不能修改它?
在将其作为参数传递之前,我们可以使用Collections.unmodifiableCollection(collection c)
方法创建只读集合,这将确保任何更改集合的操作都会抛出UnsupportedOperationException
。
38. 我们如何从给定的集合创建一个同步集合?
我们可以使用Collections.synchronizedCollection(Collection c)
来获取由指定集合支持的同步(线程安全)集合。
39. 集合框架中实现了哪些常见算法?
Java集合框架提供了常用的算法实现,如排序和搜索。集合类包含这些方法实现。这些算法大多适用于List,但其中一些适用于各种集合。其中一些是排序、搜索、洗牌、最小最大值。
40. 什么是 Big-O 符号?请举几个例子?
Big-O
符号根据数据结构中的元素数量描述算法的性能。由于Collection
类是数据结构,我们通常倾向于使用Big-O
符号来根据时间、内存和性能选择要使用的集合实现。示例1:ArrayList
的get(index i)
是一个常数时间操作,不依赖于列表中的元素数量。因此,它在Big-O
符号中的性能是O(1)。示例2:对数组或列表性能的线性搜索是O(n),因为我们需要搜索整个元素列表才能找到元素。
41. 与 Java 集合框架相关的最佳实践是什么?
-根据需要选择正确的集合类型,例如,如果大小是固定的,我们可能希望使用Array
而非ArrayList。如果我们必须按插入顺序迭代Map,我们需要使用LinkedHashMap
。如果我们不想要重复,我们应该使用Set。
- 一些集合类允许指定初始容量,因此,如果我们对将存储的元素数量有一个估计,我们可以使用它来避免重新散列或调整大小。
- 根据接口而不是实现来编写程序,它允许我们在以后的时间点轻松更改实现。
- 始终使用泛型来保证类型安全,并在运行时避免ClassCastException。
- 使用JDK提供的不可变类作为Map中的键,以避免为我们的自定义类实现
hashCode()
和equals()
。 - 尽可能多地使用
Collections
实用程序类进行算法或获取只读、同步或空集合,而不是编写自己的实现。它将增强代码重用,具有更高的稳定性和更低的可维护成本。
42. 什么是 Java 优先级队列?
PriorityQueue
是一个基于优先级堆的无界队列,元素按其自然顺序排序,或者我们可以在创建时提供Comparator
进行排序。PriorityQueue
不允许空值,我们不能添加任何不提供自然排序的对象,或者我们没有任何用于排序的比较器。Java PriorityQueue是非线程安全的,并且为查询和取消查询操作提供了O(log(n))的时间。
43. 为什么我们不能像这样写代码List numbers = new ArrayList();?
泛型不支持子类型,因为它会在实现类型安全方面造成问题。这就是为什么List<T>
不被视为List<S>
的子类型,其中s是T的超类型。为了理解为什么不允许使用它,让我们看看如果支持它会发生什么。
List<Long> listLong = new ArrayList<Long>();
listLong.add(Long.valueOf(10));
List<Number> listNumbers = listLong; // compiler error
listNumbers.add(Double.valueOf(1.23));
正如您从上面的代码中看到的,IF泛型将支持子类型,我们本可以很容易地在Long列表中添加一个Double,这将在运行时遍历Long列表时导致ClassCastException
。
44. 为什么我们不能创建通用数组?或者编写如下代码List[] array = new ArrayList[10];
我们不允许创建泛型数组,因为数组在运行时携带其元素的类型信息。如果元素类型与定义的类型不匹配,则在运行时使用此信息抛出ArrayStoreException
。由于泛型类型信息在编译时被类型擦除,因此数组存储检查本应在失败的地方通过。
让我们用一个简单的示例代码来理解这一点:
List<Integer>[] intList = new List<Integer>[5]; // compile error
Object[] objArray = intList;
List<Double> doubleList = new ArrayList<Double>();
doubleList.add(Double.valueOf(1.23));
objArray[0] = doubleList; // 这应该会失败,但会通过,因为在运行时intList和doubleList都只是List
数组本质上是协变的,也就是说,只要S是T的一个子类型,S[]就是T的一个子类型,但泛型不支持协变或子类型,正如我们在上一个问题中看到的那样。因此,如果我们被允许创建泛型数组,由于类型擦除,即使这两种类型不相关,我们也不会得到数组存储异常。
总结
以上就是我整理的关于Java集合的44个经典面试提。当我发现更多有关 Java 集合框架的问题时,我会继续添加它们,如果您发现它有用,请与其他人分享,这会激励我写更多类似的文章。如果我遗漏了任何重要问题,请告诉我,我会将其列入清单。