一、集合类关系图
上述类图中,实线边框的是类,而点线边框的是接口。
二、ArrayList
继承关系、实现接口
//继承关系
java.lang.Object
java.util.AbstractCollection<E>
java.util.AbstractList<E>
java.util.ArrayList<E>
//实现接口
List<E>, Iterable<E>, Collection<E>, Serializable, Cloneable, RandomAccess
底层依赖
/**
* 存储ArrayList元素的数组缓冲区。
* ArrayList的容量是此数组缓冲区的长度。
*/
private transient Object[] elementData;
ArrayList底层依赖数组。从源码中可以看出,它封装了一个Object[]类型的数组。而数组的优点就是方便查询。
构造方法
public ArrayList() {
this(10);
}
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList(Collection<? extends E> c) {
......
}
第一个无参构造:调用第二个构造方法,所传参数就是默认初始化集合容量,大小为10;
第二个带参构造:自定义集合容量;
第三个带参构造:这里不进行说明。
添加方法
//将指定的元素追加到此集合的末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//将指定元素插入此集合的指定位置
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
这里只讲解两个常用添加方法。
第一个添加方法:在集合末尾添加。
- 首先,进行扩容检测 ensureCapacityInternal(size + 1);
- 若需要扩容,先进行扩容,再进行添加(扩容后面讲解);
- 若不需要扩容,直接添加即可。
第二个添加方法:在集合指定位置添加。
- 首先,检测添加位置是否在集合容量范围内,若不在,抛出IndexOutOfBoundsException;
- 然后,跟上一个添加方法一样,进行扩容检测;
- 调用System.arraycopy方法将数组内位置为 index 到 (size-1)的元素往后移动一位(删除集合中的元素也是调用此方法,由此可见,该集合对于元素的增删效率较低)。
- 继而,将元素添加到index处。
查找方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
首先,检测index是否在集合范围内,若不在,抛出IndexOutOfBoundsException;
然后直接根据index查找数组中的对应的值,并返回。
扩容操作
通过上面的add方法可知,每次在添加元素之前都会进行扩容检测。下面根据源码来分析ArrayList集合具体是怎么进行扩容的。
扩容操作的第一步:
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- 在添加元素的时候,会调用ensureCapacityInternal方法来判断是否需要扩容;
- 当minCapacity(也就是add方法中的(size+1))大于数组长度时,将调用grow方法真正的进行扩容操作;
- 其中,源码中的modCount++表示记录集合修改次数。
扩容操作的第二步:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 源码中的int newCapacity = oldCapacity + (oldCapacity >> 1)就是扩容的重点。其中(oldCapacity >> 1)表示右移一位,可以看作(oldCapacity/2)。即扩容1.5倍
- 后面的if语句是对容量的一系列判断,ArrayList最大容量为Integer.MAX_VALUE
- 最后,将旧数组复制元素到新数组完成扩容操作。
ArrayList总结
ArrayList底层依赖数组来实现,查询效率较高,增删效率较低
ArrayList中的元素有序、可重复、允许null值
ArrayList会自动进行扩容1.5倍。初始化时尽量指定初始容量,可避免频繁扩容,影响程序执行效率
线程不安全,适用于单线程环境。
三、Vector
继承关系、实现接口
//继承关系
java.lang.Object
java.util.AbstractCollection<E>
java.util.AbstractList<E>
java.util.Vector<E>
//实现接口
List<E>, Collection<E>, Iterable<E>, Serializable, Cloneable, RandomAccess
底层依赖
protected Object[] elementData;
Vector的底层实现与ArrayLIst一样,也是依赖数组。
构造方法
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(Collection<? extends E> c) {
......
}
第一个无参构造:调用第二个构造方法,继而调用第三个构造方法,所传参数就是默认初始化集合容量,大小为10;
第二个带参构造:调用第三个构造方法,自定义集合容量;
第三个带参构造:自定义集合容量,并设置容量增长量,如果增量大于0,则在扩容后的容量为原容量+增量。
第四个带参构造:这里不进行说明。
添加方法
//将指定的元素追加到此Vector的末尾。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
Vector只有在集合末尾添加单个元素的方法(当然,它有在指定位置添加一个集合的方法,这里不作讲述),与ArrayList的add()方法类似。
有一点区别就是Vector的扩容检测方法是ensureCapacityHelper(elementCount + 1)。
查找方法
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
同ArrayList的get方法类似。
扩容操作
Vector的扩容与ArrayList有一些不同,主要是多了一个成员变量capacityIncrement,下面重点讲与ArrayList不同的地方。
protected int capacityIncrement;
扩容操作的第一步:同ArrayList类似
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
扩容操作的第二步:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
从源码中第三行代码可以看出Vector具体的扩容方法:
-
在对集合进行初始化的时候,若没有通过构造方法对capacityIncrement赋值(默认为0),则集合扩容后的容量为原来的2倍;
否则,集合扩容后的容量为原容量+增量
Vector总结
Vector底层依赖数组来实现,查询效率较高,增删效率较低
Vector中的元素有序、可重复、允许null值,添加单个元素的话,只能添加到集合末尾
Vector会自动进行扩容。扩容后的容量由增量来决定,(2倍 or 原容量+增量)
大多数方法用关键字synchronized修饰,线程安全。
四、LinkedList
继承关系、实现接口
//继承关系
java.lang.Object
java.util.AbstractCollection<E>
java.util.AbstractList<E>
java.util.AbstractSequentialList<E>
java.util.LinkedList<E>
//接口实现
List<E>, Queue<E>, Deque<E>, Collection<E>, Iterable<E>, Serializable, Cloneable
底层依赖
transient Node<E> first;
transient Node<E> last;
LinkedList底层依赖链表。从源码中可以看出,它封装了头结点、尾节点。而链表的优点就是方便增删节点。
若面试官细问,可以回答LinkedList底层依赖双向循环链表。因为双向链表包含两个指针,pre指向前一个节点,next指向后一个节点。 由下面源码可知,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,形成一个“环”。
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
构造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
第一个无参构造:LinkedList集合容量默认为空,没有扩容的概念。
第二个带参构造:这里不进行说明。
添加方法
//将指定的元素追加到此列表的末尾。
public boolean add(E e) {
linkLast(e);
return true;
}
//将指定元素插入此列表中的指定位置。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
查找方法
//返回此列表中指定位置的元素。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
首先,进行异常检测,若有异常,抛出IndexOutOfBoundsException;
因为LinkedList无法随机访问,只能通过遍历的方式找到相应的节点;
从源码中可以看出,为了提高效率,当前位置首先和元素数量的中间位置开始判断,小于中间位置,从头节点开始遍历,大于中间位置从尾节点开始遍历。
LinkedList总结
LinkedList底层依赖双向循环链表实现,增删效率较高,查询效率较低
LinkedList中的元素有序、可重复、允许null值
线程不安全,适用于单线程环境。
五、ArrayList、Vector、LinkedList总结
1.底层数据结构
ArrayList、Vector底层依赖数组,查询效率较高,增删效率较低(因为Vector是线程安全的,整体效率比ArrayList低)
LinkedList底层依赖双向循环链表,增删效率较高,查询效率较低
2.存储元素方面
ArrayList、Vector、LinkedList中的元素有序、可重复、允许null值
3.扩容方面
ArrayList一次扩容1.5倍
Vector根据增量扩容,增量为0,扩容2倍;否则原容量+增量
LinkedList没有扩容
4.线程安全方面
ArrayList、LinkedList线程不安全(如果有多个线程需要同时访问List集合中的元素,可以考虑使用Collections将集合包装成线程安全的集合)
Vector线程安全