ArrayList可以称得上是集合框架方面最常用的类了,可以和HashMap一教高下。
从名字就可以看出来,ArrayList实现了List接口,并且是基于数组实现的。
数组的大小是固定的,一旦创建的时候指定了大小,就不能再调整了。也就是说,如果数组满了,就不能再添加任何元素了。ArrayList在数组的基础上实现了自动扩容,并且提供了比数组更丰富的预定义方法(各种增删改查),非常灵活。
Java这门编程语言和C语言的不同之处就在这里,C语言的话,你必须得手动实现自己的ArrayList,原生的库函数里面是没有的。
01、创建ArrayList
ArrayList<String> arrayList = new ArrayList<String>();
可以通过上面的语句来创建一个字符串类型的ArrayList(通过尖括号来限定ArrayList中元素的类型,如果尝试添加其他类型的元素,将会产生编译错误),更简单的写法如下。
List<String> arrayList = new ArrayList<>();
由于ArrayList实现了List接口,所以arrayList变量的类型可以是List类型;new关键字声明后的尖括号中可以不再指定元素的类型,编译器可以通过前面尖括号中的类型进行只能判断。
此时会调用无参构造函数创建一个空的数组,常量 DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值为{}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
如果非常确定ArrayList中元素的个数,在创建时候还可以指定初始大小。
List<String> arrayList = new ArrayList<>(20);
这样做的好处是,可以有效地避免在添加新的元素时候进行不必要的扩容。
02、向ArrayList中添加元素
可以通过add()方法向ArrayList中添加一个元素。
arrayList.add("董李阳");
我们先来看看源码,看看add方法到底执行了哪些操作。在这个过程中,可以学习如何优雅的写代码。
先给结论:
堆栈过程图示:
add(element)
└── if (size == elementData.length) // 判断是否需要扩容
├── grow(minCapacity) // 扩容
│ └── newCapacity = oldCapacity + (oldCapacity >> 1) // 计算新的数组容量
│ └── Arrays.copyOf(elementData, newCapacity) // 创建新的数组
├── elementData[size++] = element; // 添加新元素
└── return true; // 添加成功
来具体看一下,add()方法的源码
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
/**
* This helper method split out from add(E) to keep method
* bytecode size under 35 (the -XX:MaxInlineSize default value),
* which helps when add(E) is called in a C1-compiled loop.
*/
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow() {
return grow(size + 1);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
* @throws OutOfMemoryError if minCapacity is less than zero
*/
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//如果elementData不是默认的空数组并且,大于零。
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
//这里的首选增长和最小增长。里面的逻辑是谁大,就选谁扩容后大小=原有的+两个中最大的。
} else {
//这里是elementData是默认的空数组,那就new一个出来(默认容量和传进的容量进行比对)
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
翻译下:
elementData:存储ArrayList元素的数组缓冲区, ArrayList的容量是此数组缓冲区的长度。当添加第一个元素时,任何具有elementData == DEFAULTCAPCITY_empty_elementData的空ArrayList都将扩展为DEFAULT_CAPACITY.
非私有简化嵌套类访问
size:ArrayList的大小(它包含的元素数量)。
将指定的元素附加到此列表的末尾。
参数:是要附加到此列表的元素e。
返回值:操作成功与否的boolean。
此辅助方法从add(E)中分离出来,将方法字节码大小保持在35以下(-XX:MaxInlineSize 默认值),这有助于在C1编译循环中调用add(E)
增加容量,以确保它至少可以容纳由最小容量参数指定的*个元素。
参数e为要添加的元素,此时的值为“董李阳”,size为ArrayList的长度,此时为0.
当第一次执行的时候,会从DEFAULT_CAPACITY和1作比较。然后长度是10;
然后将元素放入elementData[0] ;然后size+1,变为1;
当进行第二次添加的时候,因为elementData.length长度为10,然后现在size为1,代表还有9个空,那么就不扩容,然后直接将元素放到elementData[1];然后size+1,变为2;
可以判断,当下一次进行扩容的时候,是添加第11个元素时,大家可以尝试分析以下这个过程。
03、右移操作符
在ArrayList操作中,扩容操作里面 oldCapacity>>1。
这里扩容时候,会 oldCapacity+max(minCapacity-oldCapacity,oldCapacity>>1)
在add操作中,等添加11元素时,10 + max(11-10,10/2)--->也就是15变为1.5倍。
并且是固定的,来进行分析,因为这里的minCapacity = size + 1,当需要扩容时候,size = oldCapacity。也就是说add这个方法,
左侧这里的总是1,右侧是原本值除2.
简单说 公式会变为 oldCapacity + oldCapacity/2 =1.5倍oldCapacity(因为可以验证右边这个值一定是比1大的,add这个方法,左侧插值只能是1)。
可以在ArrayList中添加第11个元素debug验证下。
04、向ArrayList的指定位置添加元素
除了 add(E e)
方法,还可以通过 add(int index, E element)
方法把元素添加到 ArrayList 的指定位置:
arrayList.add(0, "董李阳");
add(int index, E element)
方法的源码如下:
public void add(int index, E element) {
rangeCheckForAdd(index); //检查索引是否越界
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
//这里是当前的数组容量 == 已经填充的数组容量,也就是满了。
//并且当第一次(数组容量是0,并且填充也是0,也满足情况)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element; //这里是将元素插入到指定位置。
size = s + 1;
}
两个流程是很相似的,这个是需要越界检查,然后扩容的机制是一样的,其余的都一样。
这里用到了一个非常重要的本地方法:System.arraycopy(); 他会对数组进行复制(要插入位置上的元素往后复制--头插法)。
来品味下:
这是arraycopy()的语法:
@IntrinsicCandidate
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
在ArrayList.add(int index, E element)方法中,具体用法如下:
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData:表示要复制的源数组,即ArrayList中的元素数组。
index:表示源数组中要复制的起始位置,即需要将index及后面的元素向后移动以为
elementData:表示要复制到的目标数组,即ArrayList中的元素数组。
index + 1:表示目标数组中复制的起始位置,即将 index 及其后面的元素向后移动一位后,应该插入到的位置。
size - index:表示要复制的元素个数,即需要将 index 及其后面的元素向后移动一位,需要移动的元素个数为 size - index。
05、更新ArrayList中的元素
可以使用set()方法来更改ArrayList中的元素,需要提供下标和新元素。
arrayList.set(0,"董李阳");
假设原本0位置上的元素为"董董李阳“,现在可以将其更新为”董李阳“。
来看一下set()方法的源码:
public E set(int index, E element) {
Objects.checkIndex(index, size);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
该方法先检查下标,看是否越界,然后替换新值,并返回旧值(有时候可以借助这个来完成一些操作)。
06、删除ArrayList中的元素
remove(int index)方法用于删除指定下标位置上的元素,remove(Object o)方法用于删除指定值的元素。
alist.remove(1);
alist.remove("董李阳");
来看看remove(int index)方法的源码:
public E remove(int index) {
Objects.checkIndex(index, size);//检查索引是否越界
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];//获取要删除的元素
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
//这里是判断,需要删除的元素,是不是在当前的末尾。
//这里会优化需要删除的元素在末尾的情况.
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
需要注意的是,在ArrayList中,删除元素时,需要将删除位置后面的元素向前移动一位,以填充删除位置留下的空隙。如果需要移动元素,则需要使用System.arraycopy方法将删除位置后面的元素向前移动一位。最后,将数组末尾的元素置为null,以便让垃圾回收机制回收该元素占用的空间。
再来看remove(Object o)方法的源码:
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
found: {
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
return false;
}
fastRemove(es, i);
return true;
}
该方法通过遍历的方式找到要删除的元素,null的使用使用==操作符判断,非null的时候使用equals()方法,然后调用fastRemove()方法。
注意:
有相同元素时,只会删除第一个(因为break)。
判断两个元素是否相等。
这里是数组的复制和移动。
07、查找ArrayList中的元素
如果要正序查找一个元素,可以使用indexOf()方法;如果要倒序查找一个元素,可以使用lastIndexOf()方法。
arrayList.indexOf("董李阳");
arrayList.lastIndexOf("董李阳");
来看看indexOf()方法的源码:
public int indexOf(Object o) {
return indexOfRange(o, 0, size);
}
int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
if (o == null) { //如果要查找的元素是null
for (int i = start; i < end; i++) { //遍历列表
if (es[i] == null) {//如果找到了null元素
return i;//返回索引
}
}
} else {//如果要查找到元素不是null
for (int i = start; i < end; i++) {//遍历列表
if (o.equals(es[i])) {//如果找到了要查找的元素
return i;//返回索引
}
}
}
return -1; //如果找不到要查找的元素,则返回 -1;
}
如果元素为null的时候使用"=="操作符,否则使用equal()方法。
lastIndexOf()方法和indexOf()方法类似,不过遍历的时候,从最后开始。
public int lastIndexOf(Object o) {
return lastIndexOfRange(o, 0, size);
}
int lastIndexOfRange(Object o, int start, int end) {
Object[] es = elementData;
if (o == null) {
for (int i = end - 1; i >= start; i--) {
if (es[i] == null) {
return i;
}
}
} else {
for (int i = end - 1; i >= start; i--) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
contains()方法可以判断ArrayList中是否包含某个元素,其内部就是通过indexOf()方法实现的。
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
这里前面的-1,和找到了的返回值就形成对比,借助这个差异,来判断有没有包含某个元素。
08、二分查找法
如果ArrayList中的元素是经过排序的,就可以使用二分查找法,效率更快。
Collections 类的sort()方法可以对ArrayList进行排序,该方法会按照字母顺序对String类型的列表进行排序。如果是自定义类型的列表,还可以指定Comparator进行排序。
List<String> copy = new ArrayList<>(alist);
copy.add("a");
copy.add("c");
copy.add("b");
copy.add("d");
Collections.sort(copy);
System.out.println(copy);
输出结果如下所示:
[a, b, c, d]
排序后就可以使用二分查找法了:
int index = Collections.binarySearch(copy, "b");
09、ArrayList增删改查的时间复杂度
最后,来简单总结一下ArrayList的时间复杂度吧,方便后面学习LinkedList时候对比。
1)查询
时间复杂度O(1),因为ArrayList内部使用数组来存储元素,所以可以直接根据索引来访问。
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
2)插入
添加一个元素(调用add()方法时)的时间复杂度最好情况为O(1),最坏情况为O(n)。
如果在列表末尾添加元素,时间复杂度为O(1)。
如果要在列表的中间或开头插入元素,则需要将插入位置之后的元素全部向后移动一位,时间复杂度为O(n).
3)删除
删除一个元素(调用remove(Object)方法时)的时间复杂度最好情况O(1),最坏情况O(n)。
如果要删除列表末尾的元素,时间复杂度为O(1)。
如果要删除列表中间或开头的元素,则需要将删除位置之后的元素全部向前移动一位,时间复杂度为O(n)。
4)修改
修改一个元素(调用set()方法时)于查询操作类似,可以直接根据索引来访问,时间按复杂度为O(1)。
public E set(int index, E element) {
Objects.checkIndex(index, size);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
10、总结
ArrayList,如果有个中文名的话,可以称作动态数组(可自动调整大小)。动态数组克服了静态数组的限制,更符合实际的开发需求。
需要掌握扩容机制,必须得读源码,这是你精进的机会。