在Java中,集合是我们经常要使用的内容,并且集合也是面试的考点之一,掌握集合帮助我们了解更多的内部构造。
List
list集合是代表的是一个元素有序的,可重复的集合。
虽然List中有很多子类的实现,但我们经常用的还是那几个,ArrayList,LinkedList,Vector等内容。
ArrayList
ArrayList 是底层由数组构成的集合,但是ArrayList有哪些优点呢?
- 能够做到动态扩容,不再局限于设置的数组大小。
- 继承于List,有集合的操作方式,方便快速的操作书库,添加,删除,修改,遍历等内容。
缺点
- 不是线程安全的集合,在操作多线程的时候需要采用别的集合例如Vector或者CopyOnWriteArrayList方式。
ArrayList 源码解析
基本元素
// 默认的List 集合大小,在创建ArrayList 的时候没有制定大小 默认是10
private static final int DEFAULT_CAPACITY = 10;
// 默认的空对象
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认对象内容是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//当前数据对象存放的地方
transient Object[] elementData; // non-private to simplify nested class access
//当前数组的长度
private int size;
// 数组最大的长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 改变list 大小的次数,进行增删除数据都涉及到此数值
protected transient int modCount = 0;
复制代码
方法介绍
既然是数组集合,就需要涉及到数组的扩容与缩容,在原先我们学习数组的时候就了解,数组的扩容与缩容都涉及到数组内的数据的迁移问题。
既然ArrayList 底层是数组,想当然的也需要涉及到这部分内容。
add方法
add 方法中涉及到增加单个元素,增加单个元素到指定位置,增加一个集合元素,增加一个集合元素到指定位置四个不同类型的方法,但是基本内容是相同的.
public boolean add(E e) {
元素增加 ,在现在的大小上增加1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 计算是否需要进行扩容 ,需要就进行扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 默认的空对象 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 判断 现在list 列表中的元素 是否是空对象。 空对象 返回 最大的值 。不是空对象 返回minCapacity
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 扩容结构进行加1
modCount++;
// 进行扩容
// 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 + (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);
}
// 计算容量 选择
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
复制代码
从以上代码来看库容有以下流程来判断
- 如果是新建立的集合list 加入数据
- 首先new出来的是集合,不给定参数的情况下,是没有进行任何容量的初始化的。
- 在执行插入add的时候,会进行容量的初始化10.
- 已经存在的数据量的集合或者指定集合数量
- 在创建的时候指定数量,那么会初始化这么数组空内容。
- 执行插入的时候会进行直接插入。
以上内容新建后的流程,有内容后,就涉及到扩容的问题。
- List 数组扩容。
- 判断现在list里面内容的大小是否超过设置的容量大小。
- 不超过不执行扩容
- 超过执行库容
- 扩容首先扩大1.5倍的大小容量
- 如果该容量还是不足以放置新增的数据,会直接扩容到最小要求的容量。
- 新的容量大小与最大值进行比较
- 存在负值情况小于0 直接超出内容容量的大小。
- 大于现在最大值直接返回Integer的最大值。
- 说明list不是无限大小的,最大是Integer的最大值。
- 进行数据的copy进行数组的扩容。 代码解释请看上面
public void add(int index, E element) {
// 指定位置增加数据,需要检查该位置是否已经被安置数据如果没有那么执行失败
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
// 进行数据的copy 工作,将该位置的数据往后面进行复制
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
然后修改该值
elementData[index] = element;
size++;
}
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;
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
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;
}
复制代码
总体添加流程简单来说是这样的:
- 判断是否是新初始化的空集合或者指定了容量的集合
- 进行添加数据判断是否需要扩容,或者先判断指定位置数据是否存在
- 扩容后的数据迁移。
ArrayList 集合容量扩容会导致性能问题,Java中复制是需要消耗内容空间,创建同样数量的对象大小,特别是大批量数据进行库容容易导致性能下降。
set get
set get 方法没有需要多说的,根据下标进行数据的读取与插入,小标注意不要超过集合大小。
remove
remove 方法在数组中会导致数组的结构的破坏,删除数据也需要进行数据的迁移。
// 移除指定元素
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);
//最后的一位置位null 让gc来处理
elementData[--size] = null; // clear to let GC do its work
返回移除内容
return oldValue;
}
复制代码
// 批量进行删除
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++)
// 判断不被 删除的元素是否在集合中 集合是 0 1 2 3 4 5 删除 1 3 complement = true
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws. 1 3 2 3 4 5 异常执行复制copy 这里主要是存在可能删除的元素不在集合中
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work 0 1 size = 6 w = 2
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
复制代码
删除代码中主要涉及到
- 删除指定元素,都涉及到数组内容的拷贝。
循环
- for循环,根据指针进行循环,比较快速
- foreach与迭代器循环数据比较适合链表式的集合数据
- 删除集合中的数据使用迭代器,如果使用for指针循环删除数据容易出现异常。