集合类进阶

一、典型面试例题及思路分析

问题 1:“Java 集合的快速失败(fail-fast)和安全失败(fail-safe)的差别是什么?”

快速失败和安全失败都是 java 集合(Collection)的一种错误机制。单线程情况下,遍历集合时去执行增删等改变集合结构的操作;或者多线程情况下,一个线程遍历集合,另一个线程执行增删等改变集合结构的操作。

快速失败,是指失败 / 异常时立即报错,通常会抛出 ConcurrentModificationException 异常,像 java.util 包下面的集合类就是使用这种机制;
安全失败,是指失败 / 异常时直接忽略,java.util.concurrent 包下面的集合类都是使用这种机制。
点评:

​快速失败的原因在于,每当迭代器在进行增删等操作时,会使用 hashNext () /next () 进行元素遍历,而元素遍历之前都会检测 modCount 变量是否为 expectedmodCount 的值,是的话就返回遍历,否则抛出异常 ConcurrentModificationException,终止遍历。 以 ArrayList 为例,相关代码如下:

/**
* 迭代器构造类
*/
public Iterator<E> iterator() {
return new Itr();
}

public E remove(int index) {
rangeCheck(index);
// 删除元素时修改元素个数
modCount++;
/**省略后面的代码**/
}

private class Itr implements Iterator<E> {
... ...
// 初始化时设置元素个数的个数
int expectedModCount = modCount;

public E next() {
// 1、每次迭代前检测元素个数是否被修改,被修改则抛出ConcurrentModificationException()
 checkForComodification();
 /**省略后面的代码**/
}

/**
    * 迭代器中安全地删除元素
*/
public void remove() {
/**省略前置检测代码*/
  ArrayList.this.remove(lastRet);
  cursor = lastRet;
  lastRet = -1;
  // 对象删除后修改元素个数
  expectedModCount = modCount;
  /**省略后面的代码**/
}

final void checkForComodification() {
 if (modCount != expectedModCount)
   throw new ConcurrentModificationException();
}

​安全失败的处理方式则有两种:

一是 CopyOnWriteArrayList/CopyOnWriteArraySet 这类集合,底层增删时会复制数组,如果增删操作前遍历数组,则会遍历复制前的老视图,二者并不冲突(参见第 8 章节 List 与 Set 类面试题);

二是 ConcurrentHashMap 这些并发集合,这些集合不存在 expectedmodCount,Iterator 也不会做相应的检查。

​目前快速失败 / 安全失败已经不仅限于 JDK 里面的集合操作了,而是作为一种思想在 Java 很多地方都有用到,比如说 RPC 调用框架的不同策略、Web 容器启动等。广义地讲,快速失败是指代码逻辑在碰到潜在问题的时候,尽快返回错误而不是尝试容忍或者解决问题;而安全失败则是采用兜底逻辑来忽略此问题,以便流程可以继续推进。关于这部分的应用可以参见扩展阅读中 Martin Fowler 的论述,这里不再展开。

问题 2:“HashMap 和 ConcurrentHashMap 的区别?”

基础特性不同:
HashMap 的 key 和 value 可以为 null,ConcurrentHashMap 的 key 和 value 不能为 null。
内部数据结构不同:
HashMap 在 JDK1.7 中采用的数据结构是数组 + 链表,在 JDK1.8 中采用的数据结构是数组 + 链表 / 红黑二叉树;
ConcurrentHashMap 在 JDK1.7 中采用的数据结构是分段的数组 + 链表,JDK1.8 的内部数据结构采用的数据结构是数组 + 链表 / 红黑二叉树(同 HashMap 一致)。
在这里插入图片描述

图 1 JDK1.7 中 ConcurrentHashMap 的结构
在这里插入图片描述

​图 2 JDK1.8 中 ConcurrentHashMap 的结构

线程安全不同:
HashMap 是非线程安全的;
ConcurrentHashMap 是线程安全的;

点评:

​这是一个 ConcurrentHashMap 的典型面试题,它也可能有很多变种:

为什么 ConcurrentHashMap 可以在高并发的情况下比 HashMap 更为高效?
ConcurrentHashMap 的 put 的过程是什么样的?
你一般在什么场景下使用 ConcurrentHashMap?

这类题目考察的重点都在 ConcurrentHashMap,特别是其中如何实现线程安全细节部分。

JDK1.7 中,ConcurrentHashMap 采用 HashEntry+Segment 的结构,ConcurrentHashMap 里一共 16 个 Segment,Segment 是可重入锁 ReentrantLock 的子类,每个 Segment 对应一个 HashEntry 键值对数组。当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁,因此,多线程访问容器里不同 Segment 的数据,就不会存在锁竞争,从而提升并发性能。

​JDK1.8 中则摒弃了 Segment 的概念,并发控制使用 synchronized 和 CAS 来操作,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。来看看核心的 put 方法。
图片描述可以看到 JDK1.8 中的 ConcurrentHashMap 主要通过小范围的加锁 (synchronizded) 以及大量的 CAS 操作来实现 put 方法的线程安全;而 get () 方法则没有加锁。其他主要方法不再展开,相关细节可以参见下面的脑图:
在这里插入图片描述

二、总结


集合进阶的内容也比较繁杂,通常来讲包括两方面的内容:一是掌握 java.util.concurrent 包下面的并发集合(源码层面的掌握,多问问为什么);二是要结合工程实践作一些尝试(比如说典型的面试题如何实现 LRU,LinkedHashMap 可用于何种场景等)。

​ 本章节虽然只列举了两个面试题,但其实是代表了上述两个方向:ConcurrentHashMap 是并发集合中的一个典型高频题,而且不同的 JDK 版本中实现并不相同,这部分内容需要重点掌握;而安全失败 / 快速失败现在不仅仅是集合的一种机制,也是很多应用广泛的的框架大量使用的一种思想。

三、扩展阅读

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

每种编程语言中都有集合。集合框架的部分优点如下:
(1)使用核心集合类降低开发成本,而非实现我们自己的集合类。
(2)随着使用经过严格测试的集合框架类,代码质量会得到提高。
(3)通过使用JDK附带的集合类,可以降低代码维护成本。
(4)复用性和可操作性。

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

Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。

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

问:Java集合框架的基础接口有哪些?

Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接的实现。

Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副牌。

List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换的数组。

Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。

一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。

问:为何Collection不从Cloneable和Serializable接口继承?

克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。

问:Iterator是什么?

Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。

问:迭代器的优点

如果用的是for循环,就用集合自带的remove(),而这样就改变了集合的Size()循环的时候会出错。但如果把集合放入迭代器,既iterator迭代可以遍历并选择集合中的每个对象而不改变集合的结构,而把集合放入迭代器,用迭代器的remove()就不会出现问题

问:Enumeration和Iterator接口的区别?

Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。

问:Iterater和ListIterator之间有什么区别?

(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。

(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。

(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

问:哪些集合类是线程安全的?

Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。

问:并发集合类是什么?

Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。

问:Collections类是什么?

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

问:如何保证线程安全又效率高?

Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

ConcurrentHashMap将整个Map分为N个segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认N为16。

问:怎么确保一个集合不能被修改?

(1)Java中提供final关键字,对基本类型进行修饰,当第一次初始化后,该变量就不可被修改
(2)Collections`工具类中的UnmodifiableList(不可修改的List、Map、Set等)

问:Comparator和Comparable的区别?

相同点:
都是用于比较两个对象“顺序”的接口
都可以使用Collections.sort()方法来对对象集合进行排序

不同点:
Comparable位于java.lang包下,而Comparator则位于java.util包下
Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序

总结
使用Comparable接口来实现对象之间的比较时,可以使这个类型(设为A)实现Comparable接口,并可以使用Collections.sort()方法来对A类型的List进行排序,之后可以通过a1.comparaTo(a2)来比较两个对象;

当使用Comparator接口来实现对象之间的比较时,只需要创建一个实现Comparator接口的比较器(设为AComparator),并将其传给Collections.sort()方法即可对A类型的List进行排序,之后也可以通过调用比较器AComparator.compare(a1, a2)来比较两个对象。

可以说一个是自己完成比较,一个是外部程序实现比较的差别而已。

用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象(strategy object)来改变它的行为。

比如:你想对整数采用绝对值大小来排序,Integer 是不符合要求的,你不需要去修改 Integer 类(实际上你也不能这么做)去改变它的排序行为,这时候只要(也只有)使用一个实现了 Comparator 接口的对象来实现控制它的排序就行了。

两种方式,各有各的特点:使用Comparable方式比较时,我们将比较的规则写入了比较的类型中,其特点是高内聚。但如果哪天这个规则需要修改,那么我们必须修改这个类型的源代码。如果使用Comparator方式比较,那么我们不需要修改比较的类,其特点是易维护,但需要自定义一个比较器,后续比较规则的修改,仅仅是改这个比较器中的代码即可。

问:哪些集合类提供对元素的随机访问?

ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。

问:与Java集合框架相关的有哪些最好的实践?

(1)根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们想根据插入顺序遍历一个Map,我们需要使用LinkedHashMap。如果我们不想重复,我们应该使用Set。

(2)一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免了重新哈希或大小调整。

(3)基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。

(4)总是使用类型安全的泛型,避免在运行时出现ClassCastException。

(5)使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。

(6)尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提供代码重用性,它有着更好的稳定性和可维护性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值