【JAVA】【集合7】Java中的ArrayList
- 【集合】ArrayList
一、ArrayList定义
ArrayList在java.util.ArrayList中定义。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
...
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
}
二、ArrayList的构造方法
(1)创建空的ArrayList
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
指定初始创建的ArryList存储长度:
public ArrayList(int initialCapacity)
(2)通过集合创建ArrayList
public ArrayList(Collection<? extends E> c)
样例:
List<String> list = new ArrayList<String>(Arrays.asList(args));
三、遍历ArrayList元素
1. 通过普通for循环遍历
List<String> array = new ArrayList<String>(Arrays.asList("10","11","12","13","14"));
for(int i = 0; i < array.size(); i++) {
System.out.println(array.get(i));
}
2. 通过foreach循环遍历
List<String> array = new ArrayList<String>(Arrays.asList("10","11","12","13","14"));
for(String str: array) {
System.out.println(str);
}
3. 通过迭代器遍历
List<String> array = new ArrayList<String>(Arrays.asList("10","11","12","13","14"));
Iterator strIteator = array.iterator();
while(strIteator.hasNext()) {
System.out.println(strIteator.next());
}
四、几种遍历访问方式的效率
参考:
https://blog.csdn.net/qq_28605513/article/details/84981338
测试方法:
创建包含100000个元素的ArryList和LinkedList结合,分别采用如上三种方式遍历10遍,其效率如下:
-
ArrayList集合的遍历效率
for循环100次时间:15 ms foreach循环100次时间:25 ms 迭代器循环100次时间:20 ms
-
LinkedList集合的遍历效率
for循环100次时间:59729 ms foreach循环100次时间:18 ms 迭代器循环100次时间:14 ms
分析原因之前,先解答几个概念:
(1)随机访问 和 迭代访问
- 随机访问:就是指定位置即可随机的定位到集合中的需要操作的元素。不用把集合遍历一遍来定位需要操作的元素。
- 迭代访问:只能依次访问集合中的每个元素,这种方式叫迭代。如果需要定位一个元素,就是需要迭代+提交判断来定位。
(2)如何判断一个集合能随机访问
我们一般期望List具备高校的随机访问能力。但是,不是所有列表都能高效地随机访问任意索引上的元素。哪些List的实现类具备随机访问能力呢?
提供高效随机访问的类都实现了标记接口 RandomAccess。因此,判断一个集合是否能随机访问,可以使用 instanceof 运算符测试是否实现了这个接口。如下:
// 随便创建一个列表,供后面的代码处理
List<?> list = ...;
// 测试能否高效随机访问
// 如果不能,先使用副本构造方法创建一个支持随机访问的副本,然后再处理
if (!(list instanceof RandomAccess)) {
l = new ArrayList<?>(list);
}
(3)如何判断一个集合能迭代访问(迭代器)
为了深入理解遍历循环处理集合的方式,我们要了解两个接口:java.util.Iterator 和 java.lang.Iterable:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
Iterator 接口定义了一种迭代集合或其他数据结构中元素的方式。迭代的过程是这样的:
- 只要集合中还有更多的元素(hasNext() 方法返回 true),就调用 next() 方法获取集合中的下一个元素。
- 有序集合(例如列表)的迭代器一般能保证按照顺序返回元素。
- 无序集合 (例如 Set)只能保证不断调用 next() 方法返回集中的所有元素,没有遗漏也没有重复,不过没有特定的顺序。
(4)如上3种遍历效率解析
- ArrayList实现了RandomAccess接口,支持随机访问。ArrayList存放的内容用的是transient Object[],在内存中是连续的,通过get(i)本质上是通过[ ]访问,相当于直接操作内存地址,所以随机访问的效率较高。使用普通for循环 比 forEach循环、迭代器效率高。
- 而LinkedList是一个双向链表,链表只能顺序访问,LinkedList中的get(i) 方法是按照顺序从列表的一端开始检查,直到找到要找的地址。所以,遍历LinkedList使用forEach、迭代器的效率高,使用普通for循环会每次都从头开始遍历、效率较差。
五、ArrayList的扩容
通过add()等方法向ArrayList集合中添加元素时,如果空间不够,ArrayList会自动扩容。如add()方法的调用关系如下:
add()
|--ensureCapacityInternal(size + 1)
|--ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
|--ensureExplicitCapacity(int minCapacity)
|--grow(minCapacity);
最终调用到grow(int minCapacity) 方法,扩容公式是:int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity为偶数就是1.5倍,否则是1.5倍左右)。 奇偶不同,比如 :10+10/2 = 15, 33+33/2=49。如果是奇数的话会丢掉小数。l
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);
}
六、ArrayList遍历中remove()错误用法
参考:
https://www.jianshu.com/p/2c3c4bb1eca0
https://www.jb51.net/article/177791.htm
E remove(int index);
ArrayList常见如下几种遍历删除:
- for循环,通过index删除
- foreach指定对象删除
- 通过迭代器删除
- 通过removeif()方法删除
我们以如下ArrayList为例,看每种操作方式的删除效果:
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
(1)for循环,通过index删除(错误用法)
private static void forRemove(List<String> list) {
for (int i = 0; i < list.size(); i++) {
if ("b".equals(list.get(i)) || "c".equals(list.get(i))) {
list.remove(i);
}
}
}
如上代码,期望删除ArrayList中内容为“b”、“c”的元素。
我们查看下remove(int index)方法的源码,可以看出其实现就是将给定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;
}
接下来,我们分析下上面删除元素的代码:
- 当i=1时,我们删除了元素b,这时list变成了[a,c,d,e],size变成了4。
- for循环继续往下进行,i=2,for循环找到了第三个元素d,发现不匹配我们的条件,没有进行删除。
这样就跳过了我们想删除的c。
所以,第此种方式最后结果是[a, c, d, e],并没有达到我们的程序预期。
(2)foreach遍历删除(错误用法)
private static void foreachRemove(List<String> list) {
for (String s : list) {
if ("b".equals(s) || "c".equals(s)) {
list.remove(s);
}
}
}
foreach循环,编译器编译后,也是一种迭代器的方式循环,我们看一下编译后什么样子:
private static void foreachRemove(List<String> list) {
Iterator var1 = list.iterator();
while(true) {
String s;
do {
if (!var1.hasNext()) {
return;
}
s = (String)var1.next();
} while(!"b".equals(s) && !"c".equals(s));
list.remove(s);
}
}
接下来我们看一下这里调的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;
}
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; // clear to let GC do its work
}
fastRemove(int index)对比之前的remove(int index)方法,两个方法操作其实一样,除了fastRemove(int index)没有返回原来的值。
这里还有一个重要的点,就是modCount++,这个modCount是干嘛用的呢?我们继续往下看。
上面编译器编译后的文件中,我们看到获取元素是通过迭代器的next()方法去获取的,我们来看下迭代器的几个关键方法:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
....
}
我们看到迭代器其实就是维护一个游标cursor,不断往下遍历集合。然后我们注意到next()方法首先会调用checkForComodification(),检查集合是否被修改过。
我们看checkForComodification()方法,就是判断modCount是否等于expectedModCount,这里的expectedModCount就是我们迭代器初始化的时候赋值的(expectedModCount = modCount),回到刚才我们提到的fastRemove(index)方法,里面有一个modCount++,所以集合每次删除元素,这个modCount值就会发生变化,下次再调用next()方法,就会抛出ConcurrentModificationException异常 。
这里抛出ConcurrentModificationException异常,是一种快速失败(fail-fast)机制,就是两个线程一起遍历操作集合时,如果修改了集合数据,那么另一个地方再次操作集合时,直接抛出异常。(当然多线程操作集合也不建议使用线程不安全的ArrayList)
所以,foreach方式遍历删除,结果是抛出ConcurrentModificationException。
(3)迭代器删除(正确用法)
private static void iteRemove(List<String> list){
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
if ("b".equals(next) || "c".equals(next)) {
iterator.remove();
}
}
}
这种方式和第二种方式编译后类似,不同的地方是:这里用的迭代器的删除方法iterator.remove()。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
删除当前元素,并且把游标回到当前位置,这样就避免了第一种方式出现的跳过一个元素的结果。
- lastRet = -1:如果连着两次调用remove()则会抛出非法参数异常(lastRet会在调用next()方法时被赋值为cursor的值,可以看上面贴的next源码)。
所以,此种方式遍历删除ArrayList中元素是可行的。
(4)通过removeIf()方法删除(正确用法)
在JDK1.8中,Collection以及其子类新加入了removeIf()方法,作用是按照一定规则过滤集合中的元素。如:
list.removeIf(item -> "1".equals(item))
ArrayLsit中对removeIf()的重写如下:
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
removeIf()的入参是一个过滤条件,用来判断需要移除的元素是否满足条件。方法中设置了一个removeSet,把满足条件的元素索引坐标都放入removeSet,然后统一对removeSet中的索引进行移除。是安全的方法。
(5)把需删除内容加入临时ArrayList,然后通过removeAll()方法删除 (正确的方法)
这种方法思路是for循环内使用一个集合存放所有满足移除条件的元素,for循环结束后直接使用removeAll()方法进行移除。
List<Long> removeList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
if (i % 2 == 0) {
removeList.add(list.get(i));
}
}
list.removeAll(removeList);
removeAll源码如下:
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, 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) {
System.arraycopy(elementData, r, elementData, w, size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
定义了两个数组指针r和w,初始都指向列表第一个元素。循环遍历列表,r指向当前元素,若当前元素没有满足移除条件,将数组[r]元素赋值给数组[w],w指针向后移动一位。这样就完成了整个数组中,没有被移除的元素向前移动。遍历完列表后,将w后面的元素都置空,并减少数组长度。至此完成removeAll移除操作。
(6) for循环,通过index删除变种:从后向前(正确用法)
同1,也是for循环,为啥从后往前遍历就是正确的呢?
因为每次调用remove(int index),index后面的元素会往前移动,如果是从后往前遍历,index后面的元素发生移动,跟index前面的元素无关,我们循环只去和前面的元素做判断,因此就没有影响。如:
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).longValue() == 2) {
list.remove(i);
}
}
五、子类的ArrayList和父类的ArrayList之间是否存在继承关系?
样例:
ArrayList<String> arrayList1 = new ArrayList<String>();
arrayList1.add(new String());
ArrayList<Object> arrayList2 = arrayList1; //编译错误
解析:ArrayList