1 容器架构图
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、 List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是 collection的子接口。
Collection集合主要有List和Set两大接口
- List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重 复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
- Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素, 只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、 LinkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、 ConcurrentHashMap
2 容器底层数据结构
2.1 List
- Arraylist: Object数组
- Vector: Object数组
- LinkedList: 双向循环链表
2.2 Set
- HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基 于 Hashmap 实现一样,不过还是有一点点区别的。
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
2.3 Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主 体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是 基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面 结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。 同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为 了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
3 List
3.1 ArrayList
3.1.1 简介
ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess(随机访问), Cloneable(克隆), java.io.Serializable(可序列化)这些接口。
- ArrayList 继承了
AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。 - ArrayList 实现了
RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。 - ArrayList 实现了
Cloneable接口,即覆盖了函数clone(),能被克隆。 - ArrayList 实现
java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输 - 和Vector不同,ArrayList中的操作
不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
3.1.2 源码解读
// 默认容量是10
private static final int DEFAULT_CAPACITY = 10;
// 如果容量为0的时候,就返回这个数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 使用默认容量10时,返回这个数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素存放的数组
transient Object[] elementData;
// 元素的个数
private int size;
// 记录被修改的次数
protected transient int modCount = 0;
// 数组的最大值
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
-
如果不传入初始容量,就使用默认容量,并设置
elementData为DEFAULTCAPACITY_EMPTY_ELEMENTDATA -
如果传入初始容量,会判断这个传入的值,如果大于0,就new一个新的Object数组,如果等于0,就直接设置
elementData为EMPTY_ELEMENTDATA。 -
如果传入一个Collection,则会调用
toArray()方法把它变成一个数组并赋值给elementData。同样会判断它的长度是否为0,如果为0,设置elementData为EMPTY_ELEMENTDATA。
3.1.3 扩容
// 扩容一个
private Object[] grow() {
return grow(size + 1);
}
// 保证扩容到期望容量minCapacity及以上
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
// 根据期望容量minCapacity计算实际需要扩容的容量
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; // 得到旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 设置新容量为旧容量的1.5倍
if (newCapacity - minCapacity <= 0) { // 如果新容量仍然小于期望容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // 如果是使用的默认容量
return Math.max(DEFAULT_CAPACITY, minCapacity); // 取默认容量和期望容量较大值返回
if (minCapacity < 0) // overflow // 检查期望容量是否越界(int 的范围)
throw new OutOfMemoryError();
return minCapacity; // 返回期望容量
}
// 如果新容量大于期望容量,判断一下新容量是否越界
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
3.1.4 缩容
ArrayList没有缩容。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。
3.1.5 遍历
- 第一种,通过迭代器遍历。即通过
Iterator去遍历。
Integer value = null;
Iterator iter = list.iterator();
while (iter.hasNext()) {
value = (Integer)iter.next();
}
复制代码
- 第二种,
随机访问index,通过索引值去遍历。 由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
Integer value = null;
int size = list.size();
for (int i=0; i<size; i++) {
value = (Integer)list.get(i);
}
复制代码
- 第三种,
增强for循环遍历。如下:
Integer value = null;
for (Integer integ:list) {
value = integ;
}
复制代码
遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低
3.2 LinkedList
3.2.1 简介
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList的本质是双向链表。
- LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。
- LinkedList包含两个重要的成员:header 和 size。
header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量:previous, next, element。(其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。) size是双向链表中节点的个数。- LinkedList 实现
List接口,能对它进行数组操作。 - LinkedList 实现
Deque接口,即能将LinkedList当作双端队列使用。 - LinkedList 实现了
Cloneable接口,即覆盖了函数clone(),能克隆。 - LinkedList 实现
java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。 - LinkedList 是
非同步的。(若要实现同步List list = Collections.synchronizedList(new LinkedList(...));)
3.2.2 分析
- 访问性 LinkedList实际上是通过
双向链表去实现的。既然是双向链表,那么它的顺序访问会非常高效,而随机访问效率比较低。 - 根据索引值操作 既然LinkedList是通过双向链表的,但是它也实现了List接口,也就是说,它实现了
get(int index)、remove(int index)等根据索引值来获取、删除节点的函数。 - LinkedList是如何实现List的这些接口的,如何将双向链表和索引值联系起来的?其实,它是通过一个
计数索引值来实现的。例如,当程序调用get(int index)方法时,首先会比较location和双向链表长度的1/2;如果前者大,则从链表头开始向后查找,直到location位置;否则,从链表末尾开始向前查找,直到location位置。
3.2.3 遍历
支持多种遍历方式。建议不要采用随机访问的方式去遍历LinkedList,而采用逐个遍历的方式。
- 第一种,通过迭代器遍历。即通过
Iterator去遍历。
for(Iterator iter = list.iterator(); iter.hasNext();)
iter.next();
复制代码
- 通过
快速随机index访问遍历LinkedList
int size = list.size();
for (int i=0; i<size; i++) {
list.get(i);
}
复制代码
- 通过另外一种
增强版for循环来遍历LinkedList
for (Integer ele: list) {
}
复制代码
- 通过
pollFirst()来遍历LinkedList,获取并移除此列表的第一个元素;如果此列表为空,则返回 null
while(list.pollFirst() != null){
}
复制代码
- 通过
pollLast()来遍历LinkedList,获取并移除此列表的最后一个元素;如果此列表为空,则返回 null。
while(list.pollLast() != null) {
}
复制代码
- 通过
removeFirst()来遍历LinkedList,移除并返回此列表的第一个元素。 NoSuchElementException - 如果此列表为空。
try {
while(list.removeFirst() != null) {
}
} catch (NoSuchElementException e) {
}
复制代码
- 通过
removeLast()来遍历LinkedList,移除并返回此列表的最后一个元素。NoSuchElementException - 如果此列表为空。
try {
while(list.removeLast() != null) {
}
} catch (NoSuchElementException e) {
}
3.3 CopyOnWriteArrayList
3.3.1 Copy-On-Write
顾名思义,在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉嘛!
3.3.2 简介
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。
3.3.3 源码解读
add()方法源码:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;//重入锁
lock.lock();//加锁啦
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组
newElements[len] = e;
setArray(newElements);//将引用指向新数组 1
return true;
} finally {
lock.unlock();//解锁啦
}
}
原来add()在添加集合的时候加上了锁,保证了同步,避免了多线程写的时候会Copy出N个副本出来。CopyOnWriteArrayList是怎么解决线程安全问题的?答案就是----写时复制,加锁
3.3.4 优缺点
缺点:
- 1、耗内存(集合复制)
- 2、实时性不高 优点:
- 1、数据一致性完整,为什么?因为加锁了,并发数据不会乱
- 2、解决了
像ArrayList、Vector这种集合多线程遍历迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题完全没有解决!
3.3.5 适应场景
- 1、读多写少(白名单,黑名单,商品类目的访问和更新场景),为什么?因为写的时候会复制新集合
- 2、集合不大,为什么?因为写的时候会复制新集合
- 实时性要求不高,为什么,因为有可能会读取到旧的集合数据