【集合】2.List
1. List基本介绍
- List接口是Collection接口的子接口。
- List集合中元素有序(添加顺序和取出顺序一致)并且可重复。
- List集合中每个元素都有其对应的顺序索引,即支持索引。
2. List接口常用方法
- void add(int index, Object ele)
- 在 index 位置插入 ele 元素
- boolean addAll(int index, Collection eles)
- 从 index 位置开始将 eles 中的所有元素添加进来
- Object get(int index)
- 获取指定 index 位置的元素
- int indexOf(Object obj)
- 返回 obj 在集合中首次出现的位置
- int lastIndexOf(Object obj)
- 返回 obj 在当前集合中末次出现的位置
- Object remove(int index)
- 移除指定 index 位置的元素,并返回此元素
- Object set(int index, Object ele)
- 设置指定 index 位置的元素为 ele , 相当于是替换
- List subList(int fromIndex, int toIndex)
- 返回从 fromIndex 到 toIndex 位置的子集合
3. ArrayList源码分析
3.1 ArrayLIst介绍
- ArrayList可以加入null,并且可以加入多个。
- ArrayList是由数组来实现数据存储。
- ArrayList基本等同于Vector,但Vector是线程安全的,而ArrayList是线程不安全的。
3.2 源码分析
3.2.1 构造方法
-
ArrayList中维护了一个Object类型的数组elementData。
transient Object[] elementData; -
当创建ArrayList对象时,如果使用的是无参构造器,则初始的elementData容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容element的1.5倍。
// 默认空数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 无参构造,将elementData指向空数组 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }// 默认容量 private static final int DEFAULT_CAPACITY = 10; -
如果使用的是指定大小的构造器,则初始element容量为指定大小,如果需要扩容,则扩容element的1.5倍。
// 空数组 private static final Object[] EMPTY_ELEMENTDATA = {}; // 有参构造 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } -
如果使用的是指定集合的构造器,则判断传入的集合大小是否为0,为0则初始化为空数组,不为0就将集合元素复制到elementData中,初始大小为传入集合元素的大小。
public ArrayList(Collection<? extends E> c) { Object[] a = c.toArray(); if ((size = a.length) != 0) { if (c.getClass() == ArrayList.class) { elementData = a; } else { elementData = Arrays.copyOf(a, size, Object[].class); } } else { // replace with empty array. elementData = EMPTY_ELEMENTDATA; } }
3.2.2 扩容机制
-
在添加元素时,首先调用ensureCapacityInternal方法确保容量足够。
public boolean add(E e) { // 确定容量 ensureCapacityInternal(size + 1); elementData[size++] = e; return true; } -
之后调用calculateCapacity方法获得需要的容量大小。
private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } -
如果是一个空数组就返回默认大小10和所需最小容量的最大值,否则返回所需最小容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } -
然后将容量大小传入ensureExplicitCapacity方法,如果所需最小容量超过了数组大小,就进行扩容。
private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } -
调用grow方法可进行扩容,扩容大小为原先1.5倍。
private void grow(int minCapacity) { int oldCapacity = elementData.length; // 原大小的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // 防止溢出 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 复制数组 elementData = Arrays.copyOf(elementData, newCapacity); }
4. Vector源码分析
4.1 Vector介绍
-
Vector底层也是一个对象数组。
protected Object[] elementData; -
Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized。
public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); } -
开发中需要线程同步安全是考虑使用Vector,但效率较低。
4.2 源码分析
4.2.1 构造方法
- Vector一共3个构造方法,无参构造方法调用一个参数的构造方法,一个参数的构造方法调用两个参数的构造方法。
- 调用无参构造方法时,默认初始容量为10,增量为0,扩容时会以2倍方式扩容。
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;
}
4.2.2 扩容机制
-
在添加元素时,首先调用ensureCapacityHelper方法确保容量足够。
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } -
如果所需容量大于数组大小就会调用grow方法扩容。
private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } -
在创建Vector时,如果指定增量大小大于0,则采用递增式扩容方法,每次扩容大小为增量大小;如果指定增量大小为0,则采用加倍式扩容方法,每次扩容原数组大小的两倍。
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); }
5. ArrayList和Vector对比
| 底层结构 | 版本 | 线程安全(同步)及效率 | 扩容 | |
|---|---|---|---|---|
| ArrayList | 可变数组 | JDK1.2 | 不安全,效率高 | 如果是有参构造按1.5倍扩容。如果是无参第一次扩容为10,之后按1.5倍扩容 |
| Vector | 可变数组 | JDK1.0 | 安全,效率不高 | 如果是无参或一个参数,按两倍加倍式扩容。如果指定增量大小,按增量大小递增式扩容。 |
6. LinkedList源码分析
6.1 LinkedList介绍
- LinkedList实现了双向链表和双端队列的特点。
- 可以添加任意元素,包括null。可以重复添加。
- 线程不安全,没有实现同步。
6.2 源码分析
-
LinkedList底层维护了一个双向链表。
-
LinkedList中维护了两个属性first和last分别指向头节点和尾节点。
transient Node<E> first;transient Node<E> last; -
节点是内部类Node对象,里面维护了prev、next、item三个属性,prev指向前一个节点,next指向后一个节点。
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
6.2.1 添加元素
使用add方法添加元素时默认添加到链表尾部,会调用linkLast方法。
public boolean add(E e) {
linkLast(e);
return true;
}
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++;
}
6.2.2 删除元素
使用无参remove方法删除元素时,会删除头部元素,会调用removeFirst方法。
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
6.2.3 查找元素
使用get方法查找元素时首先会判断索引是否在链表范围内,如果不在抛出IndexOutOfBoundsException异常。
之后会调用node方法查找元素,先判断索引在前半部分还是后半部分,在前半部分就从前往后遍历;在后半部分就从后往前遍历。
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int 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;
}
}
7. ArrayList和LinkedList对比
| 底层结构 | 增删效率 | 改查效率 | |
|---|---|---|---|
| ArrayList | 可变数组 | 较低,增加需扩容,删除需移动 | 较高 |
| LinkedList | 双向链表 | 较高,通过链表追加 | 较低 |
如何选择ArrayList和LinkedList:
- 如果改查操作多,选择ArrayList。
- 如果增删操作多,选择LinkedList。
- 在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList。
浙公网安备 33010602011771号