带着问题来阅读

1、Java有哪些集合

2、不同集合的应用场景分别是哪些

3、哪些实现类是线程安全的

4、为什么Java集合不能存放基本类型

5、集合的fail-fast和fail-safe是什么

Java集合概览

Java通过Java Collections Framework(JCF)为开发者提供了一系列集合接口和实现,所谓集合,就是多个Java对象的聚集。

学习过数据结构的同学们对各类集合的定义肯定不陌生,Java通过提供一系列的内置数据结构实现,为开发者提高了开发的便利性,提升了程序的兼容性,降低了编程的复杂性。

图片出自https://www.pdai.tech/md/java/collection/java-collection-all.html

Java集合包含两个顶层接口:Collection和Map,Collection是主要保存对象的集合,Map是保存键值对的集合。

下面我们对两类集合的实现做简单介绍。

Collection

Collection是Jdk1.2版本引入的接口,用于描述保存对象的集合,它的扩展接口有List、Queue、Set。

List

List以列表形式顺序存取元素,保证元素的插入顺序和存储顺序一致。

实现

线程安全

ArrayList

数组

LinkedList

双向链表

CopyOnWriteArrayList

数组

是,使用CopyOnWrite保证线程安全

Vector

数组

是,使用Synchronized保证线程安全

Stack

数组

是,继承Vector

Vector是Jdk1.0就引入的线程安全的列表实现,早于Collection接口设计,采用直接在方法上添加Synchronized来保证线程安全,Stack是继承Vector实现的栈结构,由于其线程安全的低效性,目前在实际环境均不再推荐使用。

Queue

Queue是先进先出的结构,从队尾加入元素,队头弹出元素。其子接口Deque为双端队列,即两头都可以进出。

实现

线程安全

ArrayDeque

循环数组

LinkedList

链表

PriorityQueue

BlockingQueue

BlockingQueue为阻塞队列的扩展接口

由于BlockingQueue略为复杂,更涉及到一些进阶应用场景,留待后续讲解。

Set

Set是不重复元素集合,元素中不包含相同元素,且通常情况不保持元素插入顺序

实现

线程安全

HashSet

哈希表

否,基于HashMap实现

TreeSet

红黑树

否,基于TreeMap实现

LinkedHashSet

哈希表+链表

否,基于LinkedHashMap实现

CopyOnWriteArraySet

数组

是,CopyOnWrite保证

ConcurrentSkipListSet

跳表

是,基于ConcurrentSkipListMap实现

Map

Map用于存储键值对。

实现

线程安全

HashMap

哈希表+红黑树

TreeMap

红黑树

LinkedHashMap

哈希表+链表+红黑树

WeakHashMap

哈希表

ConcurrentHashMap

哈希表+红黑树

是,基于节点CAS和Synchronized实现

ConcurrentSkipListMap

跳表

为什么Java集合不能存放基本类型

Java在1.2版本引入JCF框架,Java范型是在1.5版本引入,因此在泛型引入之前集合默认以Object作为存储类型。

以List为例

List list = new ArrayList();

list.add(123); // 自动boxing

list.add("123");

int num = (int) list.get(0);

String str = (String) list.get(1);

显而易见该方式存在缺陷,集合内可以放入任何以Object为基类的元素,而元素的获取方无法确定元素的具体类型,容易出现类型转换错误。在1.5版本引入范型以后,对集合接口进行规范,添加了范型参数,Java的泛型机制本质上还是将具体类型擦除为Object,因此泛型集合在初始化时,无法将参数指定为非Object派生的基本类型。

什么是fail-fast和fail-safe

List list = new ArrayList();

list.add("123");

list.add("456");

//(1) throw ConcurrentModificationException

for (String s : list) {

list.remove(s);

}

//(2) 正常移除

Iterator it = list.iterator();

while (it.hasNext()) {

it.next();

it.remove();

}

//(3) throw ConcurrentModificationException

new Thread(() -> {

for (String s : list) {

System.out.println(s);

try {

TimeUnit.SECONDS.sleep(1);

} catch (InterruptedException ignore)

{

}

}

}).start();

new Thread(() -> {list.add("789");}).start();

上面这段代码,(1) (3) 会抛出ConcurrentModificationException,(2)可以正常移除所有元素。首先了解一下ConcurrentModificationException,当对一个对象做出的并发修改不被允许时,将抛出这个异常。

This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.

那么(1)是单线程执行,(3)并发添加元素也并未对已有元素做修改,为什么也会触发该异常呢。

ArrayList var1 = new ArrayList();

var1.add("123");

var1.add("456");

Iterator var2 = var1.iterator();

while(var2.hasNext()) {

String var3 = (String)var2.next();

var1.remove(var3);

}

对代码(1)的class文件反编译查看,发现foreach实际上是通过Iterator做的迭代,迭代过程中删除是直接调用list.remove。我们再进入到list.iterator方法探个究竟。

/** Returns an iterator over the elements in this list in proper sequence.

* The returned iterator is fail-fast. */

public Iterator iterator() {

return new Itr();

}

private class Itr implements Iterator {

....

int expectedModCount = modCount;

....

final void checkForComodification() {

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

}

}

iterator方法会创建一个Itr对象,在其创时会复制modCount到expectedModCount,每进行迭代时都会判断两个值是否相同,如果不同则抛出ConcurrentModificationException。

再来看modCount是一个继承自AbstractList的成员变量,用于记录list被修改的次数,每当调用add/remove时,modCount都会加1。

// The number of times this list has been structurally modified.

protected transient int modCount = 0;

那么问题就很明显了,每当对list进行修改modCount都会改变,而foreach的iterator记录的是迭代对象创建时刻的modCount值,接下来的迭代过程中,由于调用了list的修改方法,改变了其中modCount的值,导致modCount != expectedModCount,于是就抛出了异常。(3)代码是相同的问题,不再进行赘述。

*

* The iterators returned by this class's {@link #iterator() iterator} and

* {@link #listIterator(int) listIterator} methods are fail-fast:

* if the list is structurally modified at any time after the iterator is

* created, in any way except through the iterator's own

* {@link ListIterator#remove() remove} or

* {@link ListIterator#add(Object) add} methods, the iterator will throw a

* {@link ConcurrentModificationException}. Thus, in the face of

* concurrent modification, the iterator fails quickly and cleanly, rather

* than risking arbitrary, non-deterministic behavior at an undetermined

* time in the future.

在所有Java集合类中,直接位于java.util下除Vector、Stack、HashTable外,所有的集合都是fail-fast的,而在java.util.concurrent下的集合都是fail-safe的,即可以并发的遍历和修改集合,具体实现由各自的线程安全机制保证。

为什么需要fail-fast

fail-fast意为快速失败,在非线程安全的集合应用场景中,并发对集合做的添加/删除,可能导致另一个正在遍历集合的线程出现未知的错误如数组越界。因此非线程安全的集合实现引入fail-fast以此来快速中断线程,避免引发未知的连锁问题。

参考

Java集合框架详解

快速失败(fail-fast)与安全失败(fail-safe)