在网上找了半天也没有很好的 JAVA 集合框架相关的教程,虽然平时经常使用,但是总感觉有些混乱,也不知晓各个集合或者说容器的特点和使用场景。接下来这一系列的博客希望能将 JAVA 集合框架中的知识做一个总结,也给后来者提供更加清晰的思路。
概述
我们通过这张接口图来了解 JAVA 到底定义了哪些容器。后面我们还会对需要重点掌握的容器的使用和特点以及原理做单独的讲解。
根据各个容器接口的特点,总结下来分为四大类,Set
,List
,Queue
,Map
。后面的章节我们会对他们的几个重要的实现类做具体的讲解。
List
List 可以理解为列表,并且 List 接口是有序的,即用户放入的元素的顺序与在 List 中存放的顺序相同。List 接口我们主要用到两个实现类 ArrayList 和 LinkedList.
ArrayList
ArrayList 实现了 List 接口,所以是有序的,底层通过Object 数组实现。允许放入 null 元素。
特点:
- 容量不固定,可以进行动态扩容。
- 有序集合(输入等于输出)。
- 插入的元素可以为 null。
- 可以随机访问元素(底层为数组)。
- 改和查效率比较高(相对与 LinkedList 来说,增和删需要移动数组元素的位置)。
- 为了追求效率,ArrayList 没有实现同步。如果想变为同步容器可以使用 Collections.synchronized 或者使用Vector(Vector 就是在 ArrayList 的方法上加了 Synchronized 关键字)。
方法分析:
增加和删除我们就不分析了,太过简单就是对数组元素的操作和移动位置。我们来讲一讲扩容。
ArrayList 有一个默认的大小或者叫容量(capacity)。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
我们不断的往容器中添加新元素,这可能会导致数组的大小不够,因此在每次添加之前都要对剩余空间进行检查,如果需要则是自动扩容的,扩容操作是通过 grow 方法完成的,最终扩容为原来的 1.5 倍。
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//原来的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);//创建新的 List,并将引用指向新的 List
}
最后一个问题,为什么 ArrayList 的底层数组对象要使用 transient 来修饰哪?
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable {
//实现Serializable接口,生成的序列版本号:
private static final long serialVersionUID = 8683452581122892189L;
//ArrayList初始容量大小:在无参构造中不使用了
private static final int DEFAULT_CAPACITY = 10;
//空数组对象:初始化中默认赋值给elementData
private static final Object[] EMPTY_ELEMENTDATA = {};
//ArrayList中实际存储元素的数组:
private transient Object[] elementData;
//集合实际存储元素长度:
private int size;
}
transient 的作用是当我们序列化一个对象时,如果我们不想对某个属性进行序列化,在这个属性前面添加 transient 修饰符即可。
比如我们创建了 new Object[10] 这样的数组对象,但是只添加了一个元素,而生于9个位置并没有添加元素。如果没有 transient 关键字,我们会对整个数组进行序列化,这样会浪费磁盘空间。
我们通过 writeObject 和 readObject 来看序列化时是怎么做的。
//序列化写入:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
int expectedModCount = modCount;
s.defaultWriteObject();
s.writeInt(size);
for (int i=0; i 0) {
ensureCapacityInternal(size);
Object[] a = elementData;
for (int i=0; i
序列化和反序列化的时候直接写入了数组中的元素,而不是整个数组。
LinkedList
LinkedList 实现了 List 接口,所以是有序的。它又实现了 Deque 接口,所以也可以看作一个队列(Queue)或者栈(Deque 是双端队列,可以当作栈来使用,Java 官方声明已经不建议用 Stack 这个类了)。
LinkedList 的底层实现是双向链表。在 1.7 之前是环形链表,1.7之后变为了直线型链表,当然这并不影响我们理解 LinkedList。
特点:
- 实现了 List,有序的,输入即输出。
- 实现了 Deque(双端队列) 接口,可以先入先出,也可以先入后出,既可以当作队列使用,也可以当作栈使用。
- 插入删除的效率较高(直接更改引用即可,查询和更改需要遍历)。
- 因为是链表结构,所以不存在扩容机制。
- 没有实现同步 Synchronized。
方法分析:
先来看节点类 Node。
//节点的数据结构,包含前后节点的引用和当前节点
private static class Node {
//结点元素:
E item;
//结点后指针
java.util.LinkedList.Node next;
//结点前指针
java.util.LinkedList.Node prev;
Node(java.util.LinkedList.Node prev, E element, java.util.LinkedList.Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList 通过 first 和 last 分别指向链表的第一个和最后一个元素,当链表为空的时候,first 和 last 都为 null。LinkedList 所有跟下标有关的操作都需要线性时间,在首尾添加元素只需要常数时间。
LinkedList 的操作方法的源码也比较简单,这里就不做分析了。
List 总结
ArrayList 和 LinkedList 都不是线程安全的,如果想要使用线性同步容器,可以使用 Vector,或者使用 Collections.synchronized 方法使其变为同步容器。线性的并发容器我们稍后再讲。