Java集合--ArrayList知识总结

Java集合–ArrayList知识梳理

概况介绍

ArrayList作为Java集合框架下常用的数据结构,其类的声明如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

作为List接口的大小可变数组的实现。其底层是基于数组实现,相当于容量可变的动态数组,它实现了所有可选列表操作,并允许包括 null 在内的所有元素。
ArrayList还实现了RandomAccess接口,使其支持快速随机访问,即可通过下标序号进行快速访问;实现了Cloneable接口,支持被克隆;实现了Serializable接口,因此支持序列化,能通过序列化进行传输。
ArrayList除了不同步,大致上等同于Vector类。如果出现多个线程同时访问一ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

成员变量

// 默认初始化容量10.
private static final int DEFAULT_CAPACITY = 10;
// 用于有参构造器的空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 无参构造器默认的空数组,用来和上面的空数组进行区分,确定实例的创建方式,以便在添加第一个元素时,确定集合扩展的容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 储存ArrayList元素的数组缓冲区
transient Object[] elementData; 
// ArrayList的大小,即其中包含的元素个数,和容量不是一个概念,每添加一个元素就size++.
private int size;

构造方法

ArrayList类有三个构造方法:

// 1.构造一个初始容量为 10 的空列表。
public ArrayList() {
   /*
    * 因为DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    * 所以此时ArrayList的容量为0,只有当向容器中添加元素时,
    * 即调用add(E e) 时,通过Arrays.copyOf(elementData, newCapacity),
    * 才实现容量为10的数组的创建
    */
         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
     }
// 2.构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
// 3.构造一个具有指定初始容量的空列表。     
public ArrayList(int initialCapacity) {
         if (initialCapacity > 0) {
             this.elementData = new Object[initialCapacity];
         } else if (initialCapacity == 0) {
             this.elementData = EMPTY_ELEMENTDATA;
         } else {
             throw new IllegalArgumentException("Illegal Capacity: "+
                                                initialCapacity);
         }
     }

成员方法

  1. add操作
public boolean add(E e) {
        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) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

private void ensureExplicitCapacity(int minCapacity) {
		// 该变量继承自AbstractList,用于记录操作次数.
        modCount++;

        // elementData.length就是集合的容量,当size+1大于容量时,集合就需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

每次向集合中添加元素前都会先确认size+1capacity的大小,然后size自增并添加该元素到数组缓冲区。第一次向集合中添加元素时,通过calculateCapacity方法判断elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA是否成立,这里就体现了DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA的区别。如果成立,则返回DEFAULT_CAPACITYminCapacity的较大值,然后通过grow(minCapacity)将集合中的数组缓冲区elementData扩容为DEFAULT_CAPACITY

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的扩容是将容量扩大至原来容量的1.5倍,但扩容之后的集合容量也不一定合适,所以才用了下面两个if判断,继续将容量扩展至合适的大小(ArrayList最大的容量为:Integer.MAX_VALUE),然后通过Arrays.copyOf()elementData扩展至newCapacity的大小。到这里也就实现了前面无参构造器默认容量为10的说法。

  1. 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);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

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
    }

删除元素有两种情况,第一种删除指定位置上的元素,另一种删除集合中首次出现的指定元素。
当我们调用remove(int index)时,首先会对index进行校验,然后对判断该位置是否为数组的末尾,如果是数组末尾,就直接将该位置的值设为null;如果不是数组末尾,则调用System.arraycopy()方法,将index位置后面的元素向前移动一个位置,然后将数组末尾的值设为null
调用remove(Object o)时,通过遍历获取首次出现的指定元素的位置,如果不存在直接返回false;如果存在则调用fastRemove(int index)(和remove(int index)方法对比一下,你会发现两个方法基本一致),执行完毕后返回true

  1. set操作
public E set(int index, E element) {
        rangeCheck(index);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
  1. get操作
public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

E elementData(int index) {
        return (E) elementData[index];
    }

从上面的源码可以看出,获取和修改ArrayList中的元素比较简单,所以放在一起介绍。
在根据下标进行数据修改和访问之前,同样会对index进行校验,判断其是否超出了底层数组的边界,然后再进行set和get操作。因为ArrayList的底层是基于数组实现的,实现了RandomAccess接口,所以直接调用数组随机访问即可。

遍历集合

  1. 普通for循环
for(int i = 0 ; i < list.size() ; i++){
	System.out.println(list.get(i));
}
  1. 增强for循环
for(Object obj : list){
	System.out.println(obj);
}
  1. Iterator迭代器遍历
Iterator it = list.iterator();
while(it.hasNext()){
	System.out.println(it.next());
}

以上三种方式都均能实现对AarryList的遍历,其中第二种方法其底层实现也是采用的迭代器模式。
我们在遍历ArrayList时,不能直接调用集合本身的remove方法来删除其中的元素,因为这会改变集合的大小,从而容易造成程序执行的结果不准确或者数组下标越界,甚至抛出ConcurrentModificationException异常。
当我们使用普通for循环时,出现的结果可能和我们预想的不一样,通过代码进行分析:

public class IteratorDemo {
	public static void main(String[] args) {
		ArrayList<Integer> list = new ArrayList<>();
		list.add(1);
		list.add(2);
		list.add(2);
		list.add(3);
		
		for (int i = 0; i < list.size(); i++) {
			if(2==list.get(i)) {
				list.remove(i);
			}
		}
	}
}

程序运行完毕后,我们预期list中只剩下一个元素,即{1,3},通过遍历将list中的元素打印到控制台,却发现还存在两个元素,即{1,2,3}
之所以这样,是因为当我们第一次遍历到2==list.get(i)时,通过list.remove()将该元素删除时,将集合元素依次向前移动了一个位置,将最后的位置赋值为null,执行--size语句,即改变了集合的大小,此时再去执行for语句,i = 2,list.size() = 2,而此时第二个“2”的下标变成了“1”,从而导致第二“2”就没有执行到remove操作。所以我们需要对遍历代码进行稍微修改。

for(int i = 0 ; i < list.size() ; i++){
	if(2 == list.get(i)){
		list.remove(i);
		i--;
	}
}

这样修改后,当执行了remove()操作后,通过i–,继续将下标指向当前位置,再次循环还是从当前位置执行,这样就能确保集合中的重复元素也能给删除。
对于ConcurrentModificationException异常,一般出现在增强for和迭代器中,通过源码来分析异常的原因。

// 该方法直接返回一个Iterator的对象实例
public Iterator<E> iterator() {
        return new Itr();
    }

// ArrayList的内部类
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; // 

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }
        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];
        }
		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();
            }
        }
		final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

从源码中可以知道,ArrayList内部定义了一个Itr的内部类实现了Iterator接口,通过list.iterator()就返回了一个Itr的实例对象。
它的三个成员变量的初始值为:cursor = 0,记录下一个将要遍历的元素的下标;lastRet = -1,记录当前元素的下标;excpectedModCount = modCount,表示预期的集合修改次数。
每次遍历数组,即调用next()方法,都会将cursor+1,并将自增前的cursor赋值给lastRet,然后返回lastRet下标位置的元素。这过程如果顺利进行,直到cursor == size,此时会抛出NoSuchElementException异常,所以我们在遍历之前,增加一个校验:while(it.hasNext()),判断cursor是否指向了集合的末尾,如果是,就跳出循环,结束遍历;如果不是,就继续遍历。
如果我们在通过迭代器对集合进行遍历时,同时采用集合本身的remove()方法,就会出现ConcurrentModificationException异常,代码如下:

public class IteratorDemo{
	public static void main(String[] args){
		ArrayList<Integer> list = new ArrayList<>();
		list.add(1);
		list.add(2);
		list.add(3);
		list.add(4);
		
		// 用于记录当前遍历元素的下标
		int index = 0;
		Iterator<Integer> it = list.iterator();
		while(it.hasNext()){
			if(2 == it.next()){
				list.remove(index);
			}
			index++:
		}
	}
}

执行上面的程序就会出现该异常。
在这里插入图片描述
从上面可以看出异常发生在checkForComodification()方法中,查看该方法,发现当modCount !=expectedModCount时,就会抛出该异常。
以上述代码为例,在遍历list前,我们调用了四次add()方法,我们看代码:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

注意旁边的注释:Increments modCount!!,每次向集合中添加元素,都会执行一次++modCount,当我们调用四次add()方法后,此时modCount = 4。然后开始遍历集合,首先调用iterator(),返回一个new Itr()实例,其中expectedModCount = 4。在遍历的过程中,当2 == i时,调用了集合的remove()方法,执行++modCount,从而导致modCount = 5;继续执行next(),就会调用checkForComodification()方法,校验到expectedModCount != modCount,从而抛出ConcurrentModificationException异常。
如何解决该异常呢?我们可以通过迭代器本身的remove()方法。

while(it.hasNext()){
	if(2 == it.next())
		it.remove();
}

从代码中可知该方法的内部其实也是调用了集合本身的remove()方法,只是它在执行删除操作的同时,将新的modCount赋值给了expectedModCount,同时重新设置了cursorlastRet两个变量,避免了普通for循环的问题。
在使用迭代器的remove()方法时,需要注意如下两点:

1、调用remove()之前,必须先调用next(),因为remove()首先就会对lastRet进行校验,而lastRet的初始值为-1。
2、next()方法之后只能执行一次remove(),因为remove()会将lastRet重新设置为-1,连续调用会触发IllegalStateException异常。

总结

ArrayList的底层是基于数组的实现,其大小可变,相当于动态数组,默认的容量为10。当容量不足时,会首先扩容为原容量大小的1.5倍,如果1.5倍还是太小,则将我们需要的容量赋值给newCapacity,作为新集合的容量,如果1.5倍容量太大或者我们需要的容量太大,就按照newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE。扩容之后通过数组的复制来确保集合内元素的准确性,所以在数据量较大且容量范围大概可知的情况下,应直接给定合适的initialCapacity,避免不断扩容影响程序的效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值