导言:
在实际应用开发的过程中,对于数据的操作我们常常考虑这样的问题:需要快速搜索成千上万个有序序列吗?需要快速插入删除有序序列吗?需要建立键值之间的关联吗?当非常关注性能时,选择不同的数据结构会带来很大的差异。
ArrayList在java集合中的位置
ArrayList是List(有序)接口的动态数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。可存储重复元素。
ArrayList的动态性体现在于随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。
此外注意,ArrayList此实现不是同步的(Vector同步)。ArrayList采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
先看源码(jdk 1.8)
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
源码
transient Object[] elementData;
//内部是用到了一个数组来进行元素的存储(transient 可避免序列化此字段)
private int size;
//size表示实际元素的个数
protected transient int modCount = 0;
//表示数组操作数,监测并发修改
另外可以使用size来和数组的大小进行比较进行释放数组多余的空间,节约资源,接口函数为trimToSize
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
本部分个人觉得只需说一下ArrayList增 查(遍历)
增 add /addAll
add和addAll各有两个重载,add可选择直接在数组尾部增加元素,也可选择在指定的位置增加元素。
public boolean add(E e) {
/*确定当前数组容量在增加n个元素是否充足,
充足继续执行,否则最终通过grow函数扩充容量
*/
ensureCapacityInternal(size + 1);
elementData[size++] = e;
//存入数组
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
//判断index是否是越界访问。
ensureCapacityInternal(size + 1);
// Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
addAll
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
//判断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;
}
说明:addAll函数是增加参数集合中的全部元素,index参数指明具体在调用者哪一个位置开始加入元素。
遍历
在ArrayList中含有实现了Iterator和listIterator两个迭代器接口。
Iterator实现类的三个属性
int cursor; //当前元素位置
int lastRet = -1; //访问的上一个元素位置
int expectedModCount = modCount;
//用于处理并行访问迭代器时的快速失败策略
Iterator接口中含有下面四个方法
boolean hasNext();
//如果迭代器对象还有多个可以访问的对象,则返回true
E next();
//通过反复调用next()可以访问集合中的元素
default void remove();
//删除上一个访问的元素
default void forEachRemaining(Consumer<? super E> action) ;
//可以使用该方法结合一个lambda表达式进行对每一个元素进行操作
使用例子:
ArrayList<Integer> arrayList=new ArrayList<>();
for(int i=0;i<10;i++)
arrayList.add(i);
Iterator<Integer> iterator=arrayList.iterator();
//随机访问
for(Integer i:arrayList)
{
System.out.println(i);
}
//迭代器遍历
while(iterator.hasNext())
{
System.out.println(iterator.next());
}
//forEachRemaining方法
iterator.forEachRemaining(e-> System.out.println(e));
说明:Iterator和listIterator两个迭代器接口有什么区别呢?(说一下前者的优点)
listIterator可以说是Iterator的加强版,前者可以定位当前索引的位置,有nextIndex()和previousIndex(),有hasPrevious()和previous()方法,可以实现向前遍历,有add(e)方法,可以向List中添加对象,有set(e)方法,可以对List中的对象进行修改。
每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
源码这里介绍扩容函数grow()
private void grow(int minCapacity) {
// 获取数组的长度
int oldCapacity = elementData.length;
//扩大至1.5倍的容量 >>1相当于除以2
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);
}
ArrayList也采用了快速失败的机制,通过记录modCount参数(操作数)来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
`
因为数组是一个有界集合,如果程序中收集的数据没有上限就需要我们使用链表来实现。
ArrayList对于频繁数据增删的场景不适用
为追求效率,ArrayList没有实现同步。(synchronized),如果需要多个线程并发访问,用户可以手动同步,也可使用Vector替代。
欢迎朋友讨论指正哈~ ~