Java 集合框架面经

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

Java1.5 引入了泛型,所有的集合接口和实现都大量地使用它。泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现 ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和 instanceOf 操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。

2 为何 Collection 不继承 Cloneable 和 Serializable 接口?

Collection 接口指定一组对象,对象即为它的元素。如何维护这些元素由 Collection 的具体实现决定。例如,一些如 List 的 Collection 实现允许重复的元素,而其它的如 Set 就不允许。很多 Collection 实现有一个公有的 clone 方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为 Collection 是一个抽象表现。重要的是实现。当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。

3 为何 Map 接口不继承 Collection 接口?

尽管 Map 接口和它的实现也是集合框架的一部分,但 Map 不是集合,集合也不是 Map。因此, Map 继承 Collection 毫无意义,反之亦然。如果 Map 继承 Collection 接口,那么元素去哪儿?Map 包含 key-value 对,它提供抽取 key 或 value 列表集合的方法,但是它不适合“一组对象”规范。

4 Enumeration 和 Iterator 接口的区别?

Enumeration 的速度是 Iterator 的两倍,也使用更少的内存。Enumeration 是非常基础的,也满足了基础的需要。但是,与 Enumeration 相比,Iterator 更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。

迭代器取代了 Java 集合框架中的 Enumeration。迭代器允许调用者从集合中移除元素,而 Enumeration 不能做到。为了使它的功能更加清晰,迭代器方法名已经经过改善。

5 Iterater 和 ListIterator 之间有什么区别?

  1. 我们可以使用 Iterator 来遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  2. Iterator 只可以向前遍历,而 LIstIterator 可以双向遍历。
  3. ListIterator 从 Iterator 接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

6 遍历一个 List 有哪些不同的方式?

可以使用 for-each 循环或者iterator 遍历一个 List,其中使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出 ConcurrentModificationException。

7 通过迭代器 fail-fast 属性,你明白了什么?

每次我们尝试获取下一个元素的时候,Iterator fail-fast 属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出 ConcurrentModificationException。Collection 中所有 Iterator 的实现都是按 fail-fast 来设计的(ConcurrentHashMap 和 CopyOnWriteArrayList 这类并发集合类除外)。

8 在迭代一个集合的时候,如何避免ConcurrentModificationException?

在遍历一个集合的时候,我们可以使用并发集合类来避免 ConcurrentModificationException,比如使用 CopyOnWriteArrayList,而不是 ArrayList。

9 为何 Iterator 接口没有具体的实现?

Iterator 接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的 Iterator 的集合类都有它自己的 Iterator 实现内部类。这就允许集合类去选择迭代器是 fail-fast 还是 fail-safe 的。比如,ArrayList 迭代器是 fail-fast 的,而 CopyOnWriteArrayList 迭代器是 fail-safe 的。

10 UnsupportedOperationException 是什么?

UnsupportedOperationException 是用于表明操作不支持的异常。在 JDK 类中已被大量运用,在集合框架 java.util.Collections.UnmodifiableCollection 将会在所有 add 和 remove 操作中抛出这个异常。

11 在 Java 中,HashMap 是如何工作的?

HashMap 在 Map.Entry 静态内部类实现中存储 key-value 对。HashMap 使用哈希算法,在 put 和 get 方法中,它使用 hashCode() 和 equals() 方法。当我们通过传递 key-value 对调用 put 方法的时候,HashMap 使用 Key hashCode() 和哈希算法来找出存储 key-value 对的索引。Entry 存储在 LinkedList 中,所以如果存在 entry,它使用 equals() 方法来检查传递的 key 是否已经存在,如果存在,它会覆盖 value,如果不存在,它会创建一个新的 entry 然后保存。当我们通过传递 key 调用 get 方法时,它再次使用 hashCode() 来找到数组中的索引,然后使用 equals() 方法找出正确的 Entry,然后返回它的值。其它关于 HashMap 比较重要的问题是容量、负荷系数和阀值调整。HashMap 默认的初始容量是16,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个 entry,如果 map 的大小比阀值大的时候,HashMap 会对 map 的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的 key-value 对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对 HashMap 进行初始化是个不错的做法。

12 hashCode() 和 equals() 方法有何重要性?

HashMap 使用 Key 对象的 hashCode() 和 equals() 方法去决定 key-value 对的索引。当我们试着从 HashMap 中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同 Key 也许会产生相同的 hashCode() 和 equals() 输出,HashMap 将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用 hashCode() 和 equals() 去查找重复,所以正确实现它们非常重要。equals() 和 hashCode() 的实现应该遵循以下规则:

  1. 如果 o1.equals(o2),那么 o1.hashCode() == o2.hashCode() 总是为 true 的。
  2. 如果 o1.hashCode() == o2.hashCode(),并不意味着 o1.equals(o2) 会为 true。

13 我们能否使用任何类作为 Map 的 key?

我们可以使用任何类作为 Map 的 key,然而在使用它们之前,需要考虑以下几点:

  1. 如果类重写了 equals() 方法,它也应该重写 hashCode() 方法。
  2. 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。请参考之前提到的这些规则。
  3. 如果一个类没有使用 equals(),你不应该在 hashCode() 中使用它。
  4. 用户自定义 key 类的最佳实践是使之为不可变的,这样,hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

比如,我有一个类 MyKey,在 HashMap 中使用它。

//传递给MyKey的name参数被用于equals()和hashCode()中
MyKey key = new MyKey('Pankaj'); //assume hashCode=1234
myHashMap.put(key, 'Value');
// 以下的代码会改变key的hashCode()和equals()值
key.setName('Amit'); //assume new hashCode=7890
//下面会返回null,因为HashMap会尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null
myHashMap.get(new MyKey('Pankaj'));

那就是为何 String 和 Integer 被作为 HashMap 的 key 大量使用。

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

Map接口提供三个集合视图:

  1. Set keyset():返回 map 中包含的所有 key 的一个 Set 视图。集合是受 map 支持的,map 的变化会在集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若 map 被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过 Iterator 的 Remove、Set.remove、removeAll、retainAll 和 clear 操作进行元素移除,从 map 中移除对应的映射。它不支持 add 和 addAll 操作。
  2. Collection values():返回一个 map 中包含的所有 value 的一个 Collection 视图。这个 collection 受 map 支持的,map 的变化会在 collection 中反映出来,反之亦然。当一个迭代器正在遍历一个 collection 时,若 map 被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过 Iterator 的 Remove、Set.remove、removeAll、retainAll 和 clear 操作进行元素移除,从 map 中移除对应的映射。它不支持 add 和 addAll 操作。
  3. Set>entrySet():返回一个 map 钟包含的所有映射的一个集合视图。这个集合受 map 支持的, map 的变化会在 collection 中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若 map 被修改了(除迭代器自身的移除操作,以及对迭代器返回的 entry 进行 setValue 外),迭代器的结果会变为未定义。集合支持通过 Iterator 的 Remove、Set.remove、removeAll、retainAll 和 clear 操作进行元素移除,从 map 中移除对应的映射。它不支持 add 和 addAll 操作。

15 HashMap 和 HashTable 有何不同?

  1. HashMap 允许 key 和 value 为null,而 HashTable 不允许。
  2. HashTable 是同步的,而 HashMap 不是。所以 HashMap 适合单线程环境,HashTable 适合多线程环境。
  3. 在 Java1.4 中引入了 LinkedHashMap,HashMap 的一个子类,假如你想要遍历顺序,你很容易从 HashMap 转向 LinkedHashMap,但是 HashTable 不是这样的,它的顺序是不可预知的。
  4. HashMap 提供对 key 的 Set 进行遍历,因此它是 fail-fast 的,但 HashTable 提供对 key 的 Enumeration 进行遍历,它不支持 fail-fast。
  5. HashTable 被认为是个遗留的类,如果你寻求在迭代的时候修改 Map,你应该使用 CocurrentHashMap。
  6. 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。
  7. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n-1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。

16 如何决定选用 HashMap 还是 TreeMap?

对于在 Map 中插入、删除和定位元素这类操作,HashMap 是更好的选择。然而,假如你需要对一个有序的 key 集合进行遍历,TreeMap 是更好的选择。

17 ArrayList 和 Vector 有何异同点?

ArrayList 和 Vector 相似的地方:

  1. 两者都是基于索引的,内部由一个数组支持。
  2. 两者维护插入的顺序,我们可以根据插入顺序来获取元素。
  3. ArrayList 和 Vector 的迭代器实现都是 fail-fast 的。
  4. ArrayList 和 Vector 两者允许 null 值,也可以使用索引值对元素进行随机访问。

ArrayList 和 Vector 不同的地方:

  1. Vector 是同步的,而 ArrayList 不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用 CopyOnWriteArrayList。
  2. ArrayList 比 Vector 快。
  3. ArrayList 更加通用,因为我们可以使用 Collections 工具类轻易地获取同步列表和只读列表。

18 Array 和 ArrayList 有何区别?

  1. Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象。
  2. Array 是指定大小的,而 ArrayList 大小不是固定的。
  3. Array 没有提供 ArrayList 那么多功能,比如 addAll、removeAll 和 iterator 等。尽管 ArrayList 明显是更好的选择,但也许有些时候 Array 更好用。

19 什么时候更适合用 Array?

  1. 列表的大小已经指定,大部分情况下是存储和遍历它们。
  2. 对于遍历基本数据类型,尽管 Collections 使用自动装箱来减轻编码任务,但在指定大小的基本类型的列表上工作也会变得很慢。
  3. 如果你要使用多维数组,使用 [][] 比 List> 更容易。

20 ArrayList 和 LinkedList 有何区别?

ArrayList 和 LinkedList 两者都实现了 List 接口,但是它们之间有些不同。

  1. ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全。
  2. Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是双向链表。
  3. ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响;LinkedList 采用链表存储,所以对于 add(E e) 方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话 (add(int index, E element) 时间复杂度近似为 O(n)) ,因为需要先移动到指定位置再插入。
  4. LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) 方法)。
  5. ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

21 BlockingQueue 是什么?

Java.util.concurrent.BlockingQueue 是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue 接口是 Java 集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在 BlockingQueue 的实现类中被处理了。Java 提供了集中 BlockingQueue 的实现,比如 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue 等。

22 Collections 类是什么?

Java.util.Collections 是一个工具类仅包含静态方法。它包含操作集合的多态算法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜索、排序、混编和逆序等。

23 Comparable 和 Comparator 的区别?

Java 两个比较器 Comparable 和 Comparator 的区别

24 我们如何对一组对象进行排序?

如果我们需要对一个对象数组进行排序,我们可以使用 Arrays.sort() 方法。如果我们需要排序一个对象列表,我们可以使用 Collection.sort() 方法。两个类都有用于自然排序(使用 Comparable)或基于标准的排序(使用 Comparator)的重载方法 sort()。Collections 内部使用数组排序方法,所有它们两者都有相同的性能,只是 Collections 需要花时间将列表转换为数组。

25 当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?

在作为参数传递之前,我们可以使用 Collections.unmodifiableCollection(Collection c) 方法创建一个只读集合,这将确保改变集合的任何操作都会抛出 UnsupportedOperationException。

26 我们如何从给定集合那里创建一个 synchronized 的集合?

我们可以使用 Collections.synchronizedCollection(Collection c) 根据指定集合来获取一个 synchronized(线程安全的)集合。

27 集合框架里实现的通用算法有哪些?

Java 集合框架提供常用的算法实现,比如排序和搜索。Collections 类包含这些方法实现。大部分算法是操作 List 的,但一部分对所有类型的集合都是可用的。部分算法有排序、搜索、混编、最大最小值。

28 List,Set,Map 三者的区别?

  1. List:List 接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
  2. Set:不允许重复的集合。不会有多个元素引用相同的对象。
  3. Map:使用键值对存储。Map 会维护与 Key 有关联的值。两个 Key 可以引用相同的对象,但 Key 不能重复,典型的 Key 是 String 类型,但也可以是任何对象。

29 RandomAccess

RandomAccess 接口内容如下:

public interface RandomAccess {
}

查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。

在 binarySearch 方法中,它要判断传入的 list 是否 RamdomAccess 的实例,如果是,调用indexedBinarySearch 方法,如果不是,那么调用 iteratorBinarySearch 方法。

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!

30 list 的遍历方式选择?

  1. 实现了 RandomAccess 接口的 list,优先选择普通 for 循环 ,其次 foreach
  2. 未实现 RandomAccess 接口的 list,优先选择 iterator 遍历(foreach 遍历底层也是通过 iterator 实现的,),大 size 的数据,千万不要使用普通 for 循环

31 HashMap 和 HashSet 的区别?

  1. HashSet 底层就是基于 HashMap 实现的
  2. HashSet 实现了 Set 接口,HashMap 实现了 Map 接口
  3. HashSet 仅仅存储对象,HashMap 用于存储键值对
  4. HashSet 使用 add 方法添加元素,HashMap 使用 put 方法添加元素

32 HashSet 如何检查重复

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

33 == 与 equals 的区别

== 判断两个变量或实例是不是指向同一个内存空间, equals 是判断两个变量或实例所指向的内存空间的值是不是相同

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

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是 (n - 1) & hash(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1) 的前提是 length 是2的 n 次方)。并且采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

35 集合框架底层数据结构总结

List
  • Arraylist: Object 数组
  • Vector: Object 数组
  • LinkedList: 双向链表( JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)
Map
  • HashMap: JDK1.8 之前 HashMap 由数组和链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • Hashtable: 数组 链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

36 fail-fast 和 fail-safe 的区别?

面试题思考:java中快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

37 CopyOnWrite 容器详解?

CopyOnWrite 容器即写时复制的容器。通俗的理解是当往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWrite 并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

CopyOnWrite 的缺点如下:

  1. 内存占用问题:因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象。如果这些对象占用的内存比较大,那么这个时候很有可能造成频繁的 Yong GC 和 Full GC。
  2. 数据一致性问题。CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器。

参考:Java集合框架是什么?说出一些集合框架的优点?
全网阅读过20k的Java集合框架常见面试题总结!

你可能感兴趣的:(Java面经,集合框架,面经)