Java 集合学习笔记:ArrayList

Posted 笑虾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合学习笔记:ArrayList相关的知识,希望对你有一定的参考价值。

Java 集合学习笔记:ArrayList

UML


从类图可见ArrayList 并不是直接实现 List
ArrayList 是继承 AbstractList 再进行扩展的。
至于实现List接口的行为,听说是失误,反正删不删也不影响,所以就一直没动。

简介

ArrayList 是 List 接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于 Vector 类,除了此类是非线程安全的。)

List数组实现。

  1. 因为底层数据解构是数组,所以特点是连续内存空间,根据索引操作。
  2. 优势在于,频繁写操作相对吃亏。有两个性能消耗点:
    2.1. 插入/删除时,当前元素之后的所有元素需要整体平移。
    2.2. 空间不足时的反复自动扩容。

阅读源码

方法说明
boolean add(E e)在列表末尾添加新元素。
void add(int index, E element)在指定位置添加元素,原 index 到末尾的所有元素,整体后移一位。
boolean addAll(Collection<? extends E> c)将指定集合添加到当前列表的末尾
boolean addAll(int index, Collection<? extends E> c)将指定集合插入当前列表的指定位置
  • public boolean add(E element)
    public boolean add(E e) 
    	// 当前长度 + 1 = 所需长度。检测并按需扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 在末尾插入新元素,并更新 size
        elementData[size++] = e;
        // 成功返回 true
        return true;
    
  • public void add(int index, E element)
    public void add(int index, E element) 
		// 检查 index 有没有超出合理范围,插入的合理索引范围是: 0 到 size
        rangeCheckForAdd(index);
		// 当前长度 + 新增长度1 = 所需长度。检测并按需扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // elementData 是当前 list 底层的数组容器。
        // 将 index 后的所有元素整体向右移一位,让出空间,插入新元素。
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        // 更新 index 位置元素的值
        elementData[index] = element;
        // 更新当前 list 大小
        size++;
    
  • public boolean addAll(Collection<? extends E> c)
    public boolean addAll(Collection<? extends E> c) 
	    // 取出 c 的底层数组
        Object[] a = c.toArray();
        // 获取将会新增多少个元素(也就是 a 的长度)
        int numNew = a.length;
        // 当前长度 + 新增长度 = 所需长度。检测并按需扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
        // 从数组 a 的第 0 个索引开始,复制 numNew 个元素,到 elementData 的末尾(size)。
        System.arraycopy(a, 0, elementData, size, numNew);
        // 更新当前 list 的大小(+上新增个数)
        size += numNew;
        // 如果新增个数不为 0 表示插入成功,返回 true 
        return numNew != 0;
    
  • public boolean addAll(int index, Collection<? extends E> c)
	// 在当前 list 的 index 处插入集合 c (c保持其迭代器的顺序,整个拷进来)
    public boolean addAll(int index, Collection<? extends E> c) 
    	// 检查 index 有没有超出合理范围,插入的合理索引范围是: 0 到 size
        rangeCheckForAdd(index); 
		// 取出 c 的底层数组
        Object[] a = c.toArray();
        // 获取将会新增多少个元素(也就是 a 的长度)
        int numNew = a.length;
        // 当前长度 + 新增长度 = 所需长度。检测并按需扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
		// numMoved 表示:在当前 list 中的 index 处插入 c 时 index 后有多少个元素要向右平稳让出空位。
        int numMoved = size - index;
        // numMoved 大于 0 表示不在末尾,肯定有元素需要右移。
        // 调用 native 方法,利用复制实现平稳。(原来的位置数据还在,但不用管,插入新元素会覆盖它们)
        // elementData 是当前 list 底层的数组容器。
        // 从 index 位置开始,复制 numMoved 个元素到 index + numNew 处。
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
		// 从数组 a 的第 0 个索引开始,复制 numNew 个元素,到 elementData 的 index 处。
        System.arraycopy(a, 0, elementData, index, numNew);
        // 更新当前 list 的大小(+上新增个数)
        size += numNew;
        // 如果新增个数不为 0 表示插入成功,返回 true 
        return numNew != 0;
    

方法说明
void clear()清空列表。
E remove(int index)移除指定索引上的元素。右侧所有元素整体向左平移一位。
boolean remove(Object o)移除指定元素。如果有多个,每次只会移除第一个。
boolean removeAll(Collection<?> c)删除指定集合中包含的元素。
boolean removeIf(Predicate<? super E> filter)删除所有复合条件的元素。(filter 是一个返回布尔型的 Lambda)
boolean retainAll(Collection<?> c)保留当前列表目标集合都存在的元素。也就是,从当前列表中删除指定集合中不存在的所有元素。
  • public boolean removeAll(Collection<?> c)
    public boolean removeAll(Collection<?> c) 
    	// 如果要删除的集合 c 为 null 直接抛锅。
        Objects.requireNonNull(c);
        // 否则调用批量删除方法。(注意这里给的值 false)
        return batchRemove(c, false);
    
	// c :给定的集合
	// complement : true ? 跳过 : 删除 (以下注释以 false 为例)
    private boolean batchRemove(Collection<?> c, boolean complement) 
    	// 取当前 list 底层数组容器
        final Object[] elementData = this.elementData;
        // 双指针删除算法。(本质是把不需要删除的元素从 r 复制到 w 保留下来)
        // 可以把 r 想象成原数组中的指针,把 w 想象成结果数组中的指针。
        int r = 0, w = 0;
        // 设定修改标记,表示未修改。
        boolean modified = false;
        try 
            // 本质就是从原数组向结果数据复制元素,遇到要删除的就跳过。
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
         finally 
            // 保持与AbstractCollection的行为兼容性,即使c.contains()抛出。
            // r != size 表示遍历没走完,中途抛异常了。
            if (r != size) 
            	// 把 r 后所有元素,向前移到 w 位置。(后续元素虽然没处理,但也不能弄丢了)
                System.arraycopy(elementData, r, elementData, w, size - r);
                // 更新结果指针的值
                w += size - r;
            
            // 如果结果指针 w 与 size 不一样,说明有元素被删除了。
            if (w != size) 
                // 把 w 之后的所有索引上的内容都清理掉。(这些元素都已经被移走了,这里是残留的垃圾)
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w; // 更新修改计数
                size = w; // 更新 size
                modified = true; // 更新修改标记
            
        
        return modified;  // 返回修改标记
    
  • 双指针删除算法 JS 演示

本质就是从原数组向结果数据复制元素,遇到要删除的就跳过。结果数组自然就没有要删除的元素了。
分开声明 arr1arr2 是为了便于理解,懂了之后不需要arr2全都用arr1,效果是一样的。

var arr1 = [1,2,3,4,5]; // 原数组
var arr2 = arr1;		// 结果数组
var removeArr = [2, 4];	// 要删除的元素
var r = 0; // 指向原数组
var w = 0; // 指向结果数组
for(;r < arr1.length; r++)
	// 该元素不需要删除,我们就执行:从原数组拷贝到结果数组
	if (removeArr.includes(arr1[r]) == false)
    	arr2[w++] = arr1[r];

// 抛弃末尾的无效元素
arr2.length = arr1.length - removeArr.length;
console.info(arr2);

方法说明
E set(int index, E element)替换指定位置的元素
void replaceAll(UnaryOperator operator)遍历列表执行特定的操作,实现替换。list.replaceAll(x -> x * 2);
void sort(Comparator<? super E> c)传入一个比较器。list.sort((a,b) -> b-a);

方法说明
size()获取列表大小。
boolean isEmpty()判断列表是否为空。
E get(int index) 按索引获取元素。
boolean contains(Object o)判断列表是否包含指定元素。
int indexOf(Object o)返回指定元素在列表中的索引位置 。
int lastIndexOf(Object o)返回指定元素最后一次出现的位置。

手动扩容/缩容

方法说明
void ensureCapacity(int minCapacity) 如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。
void trimToSize()将此 ArrayList 实例的容量调整为列表的当前大小。

迭代

方法说明
void forEach(Consumer<? super E> action)遍历列表执行消费者。
Iterator iterator()返回迭代器
Spliterator spliterator()返回可拆分迭代器。
ListIterator listIterator()返回双向迭代器。
ListIterator listIterator(int index)从指定的位置开始,返回双向迭代器。
List subList(int fromIndex, int toIndex)返回指定范围的subList
Object[] toArray()返回 Object 数组
<T> T[] toArray(T[] a)返回指定类型数组。 list.toArray(new Integer[0]);

内部类

说明
class Itr implements Iterator实现了 Iterator 接口,iterator() 返回的就是它。
class ListItr extends Itr implements ListIterator双向迭代器。listIterator() 返回的就是它。
private class SubList extends AbstractList implements RandomAccess私有内部类。subList() 返回的就是它。

Itr

定义迭代器,用于在调用 iterator() 时,返回当前列表的迭代器

private class Itr implements Iterator<E> 
	int cursor;       // 光标指向:下一次(要返回的)元素的索引。 int 类型默认值 0
	int lastRet = -1; // 最近一次(返回的)元素的索引; 调用 remove 后会重置为-1,表示没有指向。
	int expectedModCount = modCount; // 预期的结构修改的次数 = 此列表被结构修改的次数

	Itr() 

	// 如果下一次(要返回的)元素,不等于最后一个。返回 true 
	public boolean hasNext() 
		return cursor != size;
	

	// 
	@SuppressWarnings("unchecked")
	public E next() 
		// 检测并发冲突
		checkForComodification();
		// 申明变量 i 缓存下一个(要返回的)元素的索引。
		int i = cursor;
		// 如果 i 比最后一个元素(size-1)还大,则抛锅。
		if (i >= size)
			throw new NoSuchElementException();
		// 取列表底层数组容器。如果 i 大于底层数组的长度。抛锅。
		Object[] elementData = ArrayList.this.elementData;
		// i 大于等于数组长度,说明数组被别人动了,抛并发异常锅。
		if (i >= elementData.length)
			throw new ConcurrentModificationException();
		else
			// 否则更新下一个(要返回的)元素的索引 +1
			cursor = i + 1;
		
		// 然后从底层数组容器中获取索引 i 对应的元素。
		// 同时更新最后一次操作的元素 lastRet 等于这个返回的元素索引。(下面 remove 就要用到 lastRet )
		return (E) elementData[lastRet = i];
	
	
	// 删除 next() 返回的索引,每次 next() 后才可以 remove 一次。
	// Itr 只有这一个方法会修改列表。
	// 最后一次(操作的)元素的索引小于 0,等于 -1 时表示没有。抛锅。
	// 检查并发冲突。
	// 执行删除操作
	public void remove() 
		if (lastRet < 0)
			throw new IllegalStateException();
		checkForComodification();

		try 
			// 上一次 next() 的最后更新了 lastRet,移除它。
			ArrayList.this.remove(lastRet);
			// 更新下一个(要返回的)元素的索引等于 lastRet。
			// 比如 lastRet = 0 时 cursor = 1。当上面执行删除索引后,后面所有元素向前移到,next() 应该返回的还是索引 0 对应的元素。
			cursor = lastRet;
			// 上一次操作的元素已经删除。重置 lastRet 为 -1 实现一次 next() 对应一个 remove()
			lastRet = -1;
			// 同步预期修改次数
			expectedModCount = modCount;
		 catch (IndexOutOfBoundsException ex) 
			throw new ConcurrentModificationException();
		
	

	@Override
	@SuppressWarnings("unchecked")
	/** 遍历余下的所有元素,执行消费者 */
	public void forEachRemaining(Consumer<? super E> consumer) 
		// 非空检测,为空抛锅。
		Objects.requireNonNull(consumer);
		// 取当前列表的 size
		final int size = ArrayList.this.size;
		// 申明变量 i 缓存下一个(要返回的)元素的索引。
		// 如果 i 大于列表尺寸,抛锅。
		int i = cursor;
		if (i >= size)  return; 
		// 取列表底层数组容器。如果 i 大于底层数组的长度。抛锅。
		final Object[] elementData = ArrayList.this.elementData;
		// i 大于等于数组长度,说明数组被别人动了,抛并发异常锅。
		if (i >= elementData.length) 
			throw new ConcurrentModificationException();
		
		// 从下一个(要返回的)元素开始,一直到最后一个元素。逐个执行消费者。
		// 每次都要同步 modCount == expectedModCount
		while (i != size && modCount == expectedModCount) 
			consumer.accept((E) elementData[i++]);
		
		// 下一个(要返回的)元素索引更新为 i(最后一个元素的后一位)
		cursor = i;
		// 更新最后一次(操作作的)元素的索引
		lastRet = i - 1;
		// 检查并发冲突。
		checkForComodification();
	
	
	// 检查并发冲突。Itr 的 remove 会同步 expectedModCount = modCount;
	// 如果这两个变量不相等,说明当前列表被其他未知的神秘力量修改过了。
	final void checkForComodification() 
		if (modCount != expectedModCount)
			throw new ConcurrentModificationException();
	

ListItr

定义双向迭代器,用于在调用 listIterator() 时,返回当前列表的双向迭代器

/** 扩展 Itr 实现 ListIterator 接口定义的方法。*/
private class ListItr extends Itr implements ListIterator<E> 
	// 传入光标指向的索引。初始化双向迭代器。
	ListItr(int index) 
		super();
		cursor = index;
	
	// 如果下一个(要返回的)元素的索引不等于 0 返回 true
	public boolean hasPrevious() 
		return cursor != 0;
	
	// 返回下一个(要返回的)元素的索引
	public int nextIndex() 
		return cursor;
	
	// 返回上一个(要返回的)元素的索引。
	public int previousIndex() 
		return cursor - 1;
	

	
	/** 返回上一个元素。*/
	@SuppressWarnings("unchecked")
	public E previous() 
		// 检查并发冲突。
		checkForComodification();
		
		// 取上一个元素索引。小于 0 就抛锅。
		int i = previousIndex();
		if (i < 0) 
			throw new NoSuchElementException();
			
		// 取列表底层数组容器。如果 i 大于底层数组的长度。抛锅。
		Object[] elementData = ArrayList.this.elementData;
		// i 大于等于数组长度,说明数组被别人动了,抛并发异常锅。
		if (i >= elementData.length)
			throw new ConcurrentModificationException();
		
		// 更新光标指向上一个元素
		cursor = i;
		// 更新最后一次操作的元素的索引
		// 返回这个元素
		return (E) elementData[lastRet = i];
	

	/** 用指定元素替换 next 或 previous 返回的最后一个元素(可选操作)。*/
	public void set(E e) 
		// 最后一次(操作的)元素的索引小于 0,等于 -1 时表示没有。抛锅。
		// 检查并发冲突。
		if (lastRet < 0)
			throw new IllegalStateException();
		checkForComodification();

		try 
			// 更新最后一次操作的元素。
			ArrayList.this.set(lastRet, e);
		 catch (IndexOutOfBoundsException ex) 
			throw new ConcurrentModificationException();
		
	
	
	/** 将指定的元素插入列表(可选操作)。 */
	public void add(E e) 
		// 检查并发冲突
		checkForComodification();

		try 
			int i = cursor;
			ArrayList.this.add(i, e);
			
			// 光标指向,向后+1
			cursor = i + 1;
			
			// 清空最后一个操作元素的指针。
			lastRet = -1;
			
			// 每次都要同步 modCount == expectedModCount
			expectedModCount = modCount;
		 catch (IndexOutOfBoundsException ex) 
			throw new ConcurrentModificationException();
		
	

静态内部类

说明
static final class ArrayListSpliterator<E> implements Spliterator<E>实现了可拆分迭代器接口。

自动扩容逻辑

以下方法会触发自动扩容。(从JDK8中数的)

// 手动扩容
public void ensureCapacity(int minCapacity)
// 末尾追加
public boolean add(E e)
// 插入指定位置(ListItr、SubList 会间接调用此方法)
Java 集合学习笔记:ArrayList

Java 集合学习笔记:ArrayList

java学习笔记--类ArrayList和LinkedList的实现

Java集合框架学习笔记

java学习笔记

Java学习笔记集合转数组---toArray()