简介
java中的线性表容器,是一种顺序表的结构,核心是用一个数组来对数据进行存储,同时也是一个容器,可以实现数据的动态存储。ArrayList中使用了泛型的定义,所以在实际的存储中存储的对象不能是基本类型(int,double等)。
基本操作
先从一些基本设计来看:
transient Object[] elementData;//实际用于存储的数组
private static final int DEFAULT_CAPACITY = 10;//默认的容器容量
private int size;//实际的数量
这里有一个区分一个是数据的数量,表述已经装入了这么多数量的数据,一个是容器的容量表示目前开辟的内存空间可以装这么多的数据。既然是一个顺序表的结构我们再看看其基本的操作。
初始化(构造函数)
这个类提供了三个构造函数。
默认生成空的线性表:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这里的空的表是初始化时将表置空,还有一个空表是在之后操作时表中没有数据了,为了区分这两个逻辑使用了两个引用加以区分。
private static final Object[] EMPTY_ELEMENTDATA = {};//空的表
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//初始化时的空表,这时的容器容量为默认容量。
自定义容器大小的初始化,在初始化的时候不使用默认的容量初始化数组:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//判断传入的大小是否合法
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//如果是0就是建立空表
this.elementData = EMPTY_ELEMENTDATA;
} else {//异常处理
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
最后一个构造函数是以一个原有容器中的数据为基础创建顺序表:
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();//类型转换
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
//转换之后的类型可能不是Object数组,再使用Arrays.copyOf()方法转化。
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {//如果传入的是个空的容器就设置一个空表
this.elementData = EMPTY_ELEMENTDATA;
}
}
Collection这个接口为所有容器提供了一些共有的操作定义
ArrayList实现了List接口,而Collection是所有相关容器接口的父接口。
所以使用Collection可以代指所有与容器相关的类。
toArray方法是Collection接口中定义的,可以将相关的数据转换为数组。当然ArrayList也实现了这个方法。但是实际的实现对象是Arrays类。
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
再到Arrays中看看:
//保持默认类型的转换
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
//指定类型的转换
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
//根据出传入的类型和大小size来初始化一个数组
//如果不是Object类型就获取其默认类型(反射),并完成数组的空间开辟
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//复制原先数组指定数量的项到新的数组中。Math.min()求出了具体复制的次数
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
arraycopy是System的native方法实现了数组的复制,参数是原数组的引用和起始位置,目的数组的引用和起始位置,还有要复制的项数。
按索引的取值与赋值
//检查索引范围
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//处理索引越界的异常信息,size为表长
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
//按索引取值
E elementData(int index) {
return (E) elementData[index];
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
//按索引赋值
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;//返回旧值
}
就是一般的对数组的操作。
按值的查找
从头查找
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;
}
就是一个简单的判断根据是否为null来进行遍历查找,同理从后面查找有
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
如果找到就返回相应的索引,如果没有找到就返回-1,同理可以判断是否在数组中含有某个元素。
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
容量扩充
插入操作会改变数据的数量大小。除此之外还要考虑容器的容量大小。还有容器的容量扩充。
//当容器容量过大时的处理
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)//容量小于0,int越界
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
//容量在int的范围内则返回最大值MAX_ARRAY_SIZE
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//容器容量的最大值
//容器扩充操作
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);
}
//判断容量是否够用,如果不够就进行扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//记录数据变化的次数
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//判断容量是否够用,先将需要的大小与默认大小进行比较,保证容量最小保持在默认限度内
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
这样就实现了容量的判读机制与自动扩容机制,当插入后的容量不够后就可以自动的申请内存了,但是线性表的最大数据容量在一个int能表示的最大范围内。
容量裁剪
有容量的扩充也有容量的裁剪,trimToSize函数可以将当前的容器容量减到与数据容量相同。
public void trimToSize() {
modCount++;//数据修改计数
if (size < elementData.length) {//数据数量小于容量
elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size);
//根据实际存储的数据大小来修改数组的容量大小
}
}
这里Arrays.copyOf()函数是将elementData数组的大小控制到size的大小。
插入数据
基本的插入操作分为在对应的位置插入和默认的插入表尾。
//默认的插入表尾
public boolean add(E e) {
//先查看容器的大小是否够用,不够就自动扩容
ensureCapacityInternal(size + 1);
elementData[size++] = e;//将数据插到表尾,表的长度加一
return true;
}
//对应的位置插入
public void add(int index, E element) {
rangeCheckForAdd(index);//越界判断
ensureCapacityInternal(size + 1);//查看容器的大小是否够用,不够就自动扩容
//将对应位置之后的元素向后移动一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element; //数据赋值
size++;//表的长度加一
}
//判断索引是否越界
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
除了插入单个的数据之外,还有一次性插入多个数据:
//全部加到表尾
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();//先转换为数组
int numNew = a.length;
ensureCapacityInternal(size + numNew); // 计算并判断容量是否足够
System.arraycopy(a, 0, elementData, size, numNew);//移位并插入表尾
size += numNew;//修改计数
return numNew != 0;//插入的数据数量要大于0
}
//指定位置插入
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);//检查位置是否越界
Object[] a = c.toArray();//容器数据转换为数组
int numNew = a.length;
ensureCapacityInternal(size + numNew); // 计算并判断容量是否足够
int numMoved = size - index;// 计算插入的位置后面有多少数据要移位
if (numMoved > 0)//进行移位操作为插入的数据腾出空间
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);//将数据插入
size += numNew;//修改计数
return numNew != 0;//插入的数据数量要大于0
}
移除数据
移除数据的操作也分为按索引移除和按值移除:
//按索引移除并返回移除的值
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; //将移除的表尾置null,由gc回收
return oldValue;//返回旧值
}
//按索引移除不返回移除的值
private void fastRemove(int index) {
modCount++;//数据修改计数
int numMoved = size - index - 1;//计算在移除的位置后面有多少数据需要移动
if (numMoved > 0)//数据移动
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; //将移除的表尾置null,由gc回收
}
//按数值移除
public boolean remove(Object o) {
if (o == null) {//如果是移除null
for (int index = 0; index < size; index++)
if (elementData[index] == null) {//找到在表中的第一个null值
fastRemove(index);//移除
return true;
}
} else {//移除的值不为null
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {//找到在表中的第一个相关数值
fastRemove(index);//移除
return true;
}
}
return false;
}
和插入一样还有按范围删除的:
protected void removeRange(int fromIndex, int toIndex) {
modCount++;//数据修改计数
int numMoved = size - toIndex;//计算在移除的位置后面有多少数据需要移动
System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);//数据移动
int newSize = size - (toIndex-fromIndex);//统计计数
for (int i = newSize; i < size; i++) {
elementData[i] = null;//批量置null,由gc回收
}
size = newSize;//修改计数
}
还有批量删除:
//complement用于表示是保留数据(true),删除数据(false)
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;//用于标记是否进行修改
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
//根据对传入容器的数据进行判断保留需要的数据到表头
elementData[w++] = elementData[r];
} finally {
if (r != size) {//保持与AbstractCollection的兼容性,contains可能会抛出异常
System.arraycopy(elementData, r, elementData, w, size - r);//将中断后的数据加到表头
w += size - r;//最终数据的大小
}
if (w != size) {//如果判断后的数据数量比之前小(需要移除)
for (int i = w; i < size; i++)
elementData[i] = null;//将之后的数据置null
modCount += size - w;//数据修改计数
size = w;//修改后的数据数量
modified = true;
}
}
return modified;
}
//保留容器含有中的数据,其余的数据清空
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);//检查空指针
return batchRemove(c, true);
}
//删除容器中含有的数据
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);//检查空指针
return batchRemove(c, false);
}
还有比较极端的清除操作,清除全部:
public void clear() {
modCount++;//数据修改计数
for (int i = 0; i < size; i++)
elementData[i] = null;//清除所有数据,置null由gc回收
size = 0;
}
表长
//空表判断
public boolean isEmpty() {
return size == 0;
}
//返回表长
public int size() {
return size;
}