一、List接口
简介
List:有序集合(也称为序列 )。 用户使用该接口可以精确控制List中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。
List是Collection的子接口,其最大的特点是允许保存有重复数据元素,该接口的定义如下:
public interface List<E> extends Collection<E> {...}
但是需要清楚的是List子接口对Collection接口进行了方法的扩充,最显而易见的是在方法中有了索引index
重要的扩充方法
方法名 | 说明 |
---|---|
E get(int index) | 获取指定索引的元素 |
E set(int index, E element) | 设置(替换)指定索引的元素 |
void add(int index, E element) | 在指定索引新增(插入)元素 |
E remove(int index) | 移除指定索引的元素 |
int indexOf(Object o) | 返回查到的第一个指定元素的索引 |
int lastIndexOf(Object o); | 返回查到的最后一个指定元素的索引 |
ListIterator< E> listIterator(); | 返回该List的LiistIterator迭代器 |
但是List本身依然属于一个接口,那么对于接口要想使用则一定要使用子类来完成定义
三个重要子类
ArrayList、LinkedList、Vector
继承图解
二、ArrayLIst(JDK1.8)
1.继承关系
ArrayList的类定义
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输
- 实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问
- 实现了Cloneable接口,能被克隆
2.核心属性
//默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
//用于指定初始化容量为0时的数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于调用默认无参构造方法时的数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储ArrayList元素的数组缓冲区。
transient Object[] elementData;
//ArrayList内的元素个数
private int size;
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别是:前者是在指定初始化ArrayList容量为0时用到的空数组实例,而后者是在未指定初始化ArrayList容量时,即调用无参构造方法时用到的空数组实例,这些空数组实例最终都会由emementData所引用,具体下面会讲到
3.构造器
无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
当调用无参构造方法时,缓存数组elementData将会指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,虽然此时是一个空Object型数组,但在第一次调用add()方法时,数组的容量将会设置为默认容量DEFAULT_CAPACITY,即10,这里在后面分析add()方法时会讲到
有参构造
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);
}
}
- 当指定初始化容量initialCapacity小于0时会抛出异常
- 当指定初始化容量initialCapacity等于0时,会将elementData指向EMPTY_ELEMENTDATA,一个空数组对象,这里在第一次调用add()的时候,容量不会设置为10,这就是和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的一个区别
- 当指定初始化容量initialCapacity大于于0时,会初始化一个新的容量为initialCapacity的Object型数组,并将elementData指向它
4.add方法(自动扩容分析)
add(E e) 方法源码
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
add()方法会在ArrayList末尾添加指定元素,首先进行容量判断,如果容量足够,则将指定元素赋值给size的索引位置(因为索引从0开始),再将size数加1,并将最后返回添加成功标志
后面来分析一下容量判断:
ensureCapacityInternal(int minCapacity) 方法源码
其中的参数minCapacity就是上一步的size+1,即我们需要的最小容量
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
可以看到这个方法内部会调用两个方法:ensureExplicitCapacity()、calculateCapacity(),我们一个一个分析:
calculateCapacity(Object[] elementData, int minCapacity) 方法源码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
这个方法其实很简单,就是计算我们需要的容量,它会判断我们是不是调用的无参构造方法
- 如果是的话,那么elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA会指向同一块内存空间,所以进行==运算为true,返回的是默认容量DEFAULT_CAPACITY和传过来的minCapacity中的最大值,第一次调用肯定是默认容量最大,所以我们第一次调用add()方法会将我们所需的容量置为默认容量10(后面的真正扩容方法grow会将生成的新容量也置为10,即第一次调用add方法后,数组扩容后的容量即为10,可以自行验证),第二次及以后调用add()方法这里的判断条件就为false了,因为后面的扩容会生成新的数组
- 如果不是的话,会直接返回传过来的参数minCapacity
接下来这个方法就是判断是否要扩容:
ensureExplicitCapacity(int minCapacity)源码
其中的参数minCapacity还是我们需要的最小容量
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- 如果minCapacity大于了elementData数组的长度,那么就需要扩容,调用grow()方法进行扩容
- 否则,容量足够,不进行扩容处理
关于modCount:这是AbstractList类中的一个属性,modCount表示了当前列表结构被修改的次数,在调用迭代器操作时,则会检查这个值,如果发现已更改,抛出异常(即一边迭代一边修改可能会抛出异常),具体参考:https://www.cnblogs.com/NextLight/p/9592069.html
一个扩容需要的函数是真的多,不过貌似JDK1.8之后源码做了简化,实际就是多个方法合在了一起,扯远了,接下来看一下真正的扩容处理:
grow(int minCapacity)方法源码
其中的参数minCapacity是我们需要的容量
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);
}
- 首先获得原来的容量oldCapacity:elementData.length
- 然后获得新容量newCapacity:原来的容量+原来容量÷2(移位运算效率更高)
- 新容量可能还是没有满足需要的容量,也有可能超过了Integer.MAX_VALUE,所以要和需要的容量进行比较,取较大的那一个赋值给newCapacity(超过了Integer.MAX_VALUE的会是负数)
- 再将newCapacity和MAX_ARRAY_SIZE比较,看是否超过了最大数组容量,其中MAX_ARRAY_SIZE的值为Integer.MAX_VALUE-8,如果大于,那么会执行hugeCapacity()方法,这个方法后面介绍
- 最后便确定了新容量newCapacity是多少,然后将原来的数组(elementData)中的数据调用Arrays.copyOf()方法拷贝到一个新的数组中,新的数组长度为newCapacity(生成新数组的操作在Arrays.copyOf()方法内部,源码就不贴了,感兴趣的可以看看)
接下来看看hugeCapacity()方法,实际上走到这一步的很少,但还是分析一波:
hugeCapacity(int minCapacity)方法源码
其中参数minCapacity是我们需要的容量
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
这个方法作用就是返回最大的容量
- 如果minCapacity大于MAX_ARRAY_SIZE九返回Integer.MAX_VALUE
- 否则直接返回MAX_ARRAY_SIZE
至此,add()方法分析完毕,果然是又臭又长,可该分析的还是得分析,梳理下来会发觉还是挺简单的,由于是自己对源码的解读,有可能不正确,特别是对于数据溢出的部分,仅供参考
总结一下:
- 如果是调用的默认无参构造方法,那么第一次调用add()方法后,缓存数组elementData的容量将扩容为10
- 首先进行扩容判断:传入我们需要的最小容量–>将调用无参构造所生成的默认数组容量置为10(只有第一次调用add的时候会走这一步)–>将所需的最小容量和数组的当前长度比较来判断是否要扩容–>如果要扩容,则将新容量置为原来容量的3/2–>再次比较所需容量和新容量的大小–>如果新容量大于所需容量,并且小于数组所规定的最大长度,则进行数组拷贝,相当于将数组容量改为新容量–>扩容成功
- 扩容成功后,将指定的元素赋值在size索引处,而后size加1
此外,还有add的重载方法,它加入了索引,简单介绍一下
add(int index, E element)方法源码
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++;
}
跟上面的add(E e)方法不同的是,它的形参多了个index索引,相当于在指定索引添加指定元素,与其说是添加不如说是插入
源码方面,大同小异:
- 先调用rangeCheckForAdd(index)检查索引是否合法,不合法抛出异常
- 接着进行扩容判断,跟上面的毫无差别
- 扩容判断完成后,然后便是插入,这里是用了数组的拷贝,实现的效果是将index索引开始的元素向后移一位,然后将element元素赋值在index位置上
5.其他常用方法
1)public E remove(int index)
删除指定索引位置的元素
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
- rangeCheck(index)检查索引是否超过了size,超过则抛出异常,注意这里与上面的rangeCheckForAdd(index)方法不同的是,这里不会检查index为负的情况,因为后面elementData(index)会在index为负时抛出数组越界异常
- 将要删除的值存储在oldValue中
- 计算需要移动的位数
- 如果位数大于0,则调用数组拷贝,将index+1位置及后面的元素前移一位
- 由于删除了一个元素,所以ArrayList的size减1,并且由于整体向前移了1位,所以最后一位元素为垃圾元素(如果删除的是最后一个,那么不会前移,但最后一位还是垃圾元素),因此将最后一个元素置为null,利于垃圾回收
- 最后返回已经删除的元素
2)public boolean remove(Object o)
删除指定元素
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
- 这里的删除是删除线性查找所找到的第一个指定元素
- 可以删除为null的对象
- 注意用的是Object的equals方法,即比较对象的地址,如果有需求,可以重写equals方法,以此来改变remove的作用
- fastRemove(index)方法的内容和上面的1)中的remove(index)方法大致相同,就是少了索引检查,也不会返回oldValue
- 如果要删除的元素存在,则返回true,否则返回false
3)public void clear()
清空ArrayList中的元素
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
源码很简单,就是将所有元素置为null,size置为0
4)public boolean addAll(Collection<? extends E> c)
将Collection对象中的元素全部添加到ArrayList中
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
- 先将Collection转化为数组,得到数组的长度
- 扩容分析
- 数组拷贝
- size增加数组的长度
- 返回最终结果是否影响了ArrayList的标志
5)public E get(int index)
得到指定索引的元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
6)public E set(int index, E element)
设置指定索引处的元素值
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
7)public boolean contains(Object o)
判断ArrayList中是否存在某个元素
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
没作分析的都是一眼可以看懂的,就不浪费篇幅了
6.特点
- ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低
- ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长
- ArrayList不是线程安全的,确保线程安全可以用Vector,这也是两者的重要区别(除此之外,Vector和ArrayList除了扩容的机制有些许不同外,其他几乎没有什么区别)
- 通过remove和contains方法可以看出,ArrayList的元素可以存储null
— —事常与人违,事总在人为