List源码分析

ArrayList

一.概览

因为 ArrayList 是基于数组实现的,所以支持快速随机访问。

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

在这里插入图片描述

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的
    List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
  • ArrayList 实现 java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。

二.源码分析

1.顶部注释总结

  • 底层:ArrayList是List接口的大小可变数组的实现。
  • 是否允许null:ArrayList允许null元素。
  • 时间复杂度:size、isEmpty、get、set、iterator和listIterator方法都以固定时间运行,时间复杂度为O(1)。add和remove方法需要O(n)时间。与用于LinkedList实现的常数因子相比,此实现的常数因子较低。
  • 容量:ArrayList的容量可以自动增长。
  • 是否同步:ArrayList不是同步的。

2.属性

	/**
	 * 初始化默认容量。
	 */
	private static final int DEFAULT_CAPACITY = 10;
	
	/**
	 * 指定该ArrayList容量为0时,返回该空数组。
	 */
	private static final Object[] EMPTY_ELEMENTDATA = {};
	
	/**
	 * 当调用无参构造方法,返回的是该数组。刚创建一个ArrayList 时,其内数据量为0。
	 * 它与EMPTY_ELEMENTDATA的区别就是:该数组是默认返回的,而后者是在用户指定容量为0时返回。
	 */
	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	
	/**
	 * 保存添加到ArrayList中的元素。
	 * ArrayList的容量就是该数组的长度。
	 * 该值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,当第一次添加元素进入ArrayList中时,数组将扩容值DEFAULT_CAPACITY。
	 * 被标记为transient,在对象被序列化的时候不会被序列化。
	 */
	transient Object[] elementData; // non-private to simplify nested class access
	
	/**
	 * ArrayList的实际大小(数组包含的元素个数)。
	 * @serial
	 */
	private int size;

这里有个问题:elementData被标记为transient,那么它的序列化和反序列化是如何实现的呢?

ArrayList自定义了它的序列化和反序列化方式:
writeObject(java.io.ObjectOutputStream s)readObject(java.io.ObjectOutputStream s) 方法

3.构造方法

ArrayList提供了三种构造方法。

  • ArrayList(int initialCapacity):构造一个指定容量为capacity的空ArrayList。
	/**
     * 带初始容量参数的构造函数。(用户自己指定容量)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
  • ArrayList():构造一个初始容量为 10 的空列表。
	/**
     *默认构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,
     也就是说初始其实是空数组,当添加第一个元素的时候数组容量才变成10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  • ArrayList(Collection<? extends E> c):构造一个包含指定 collection
    的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
    /**
     * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
     */
    public ArrayList(Collection<? extends E> c) {
        //
        elementData = c.toArray();
        //如果指定集合元素个数不为0
        if ((size = elementData.length) != 0) {
            // c.toArray 可能返回的不是Object类型的数组所以加上下面的语句用于判断,
            //这里用到了反射里面的getClass()方法
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 用空数组代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

4.核心方法

方法名时间复杂度
get(int index)O(1)
add(E e)O(1)
add(int index, E element)O(n)
remove(int index)O(n)
set(int index, E element)O(1)
add(E e)
	public boolean add(E e) {
	    //确认list容量,如果不够,容量加1。注意:只加1,保证资源不被浪费
	    ensureCapacityInternal(size + 1);  // Increments modCount!!
	    elementData[size++] = e;
	    return true;
	}
扩容

添加元素时使用 ensureCapacityInternal()方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍

扩容操作需要调用 Arrays.copyOf()把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

/**
* 数组容量检查,不够时则进行扩容,只供类内部使用。
* 
* @param minCapacity    想要的最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
    // 取minCapacity为DEFAULT_CAPACITY和参数minCapacity之间的最大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
/**
* 数组容量检查,不够时则进行扩容,只供类内部使用
* 
* @param minCapacity 想要的最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 确保指定的最小容量 > 数组缓冲区当前的长度  
    if (minCapacity - elementData.length > 0)
        //扩容
        grow(minCapacity);
}

/**
* 扩容,保证ArrayList至少能存储minCapacity个元素
* 第一次扩容,逻辑为newCapacity = oldCapacity + (oldCapacity >> 1);
* 即在原有的容量基础上增加一半。第一次扩容后,如果容量还是小于minCapacity,就将容量扩充为minCapacity。
* 
* @param minCapacity 想要的最小容量
*/
private void grow(int minCapacity) {
    // 获取当前数组的容量
    int oldCapacity = elementData.length;
    // 扩容。新的容量=当前容量+当前容量/2.即将当前容量增加一半。
    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);
}

总结:
1.进行空间检查,决定是否进行扩容,以及确定最少需要的容量
2.如果确定扩容,就执行grow(int minCapacity),minCapacity为最少需要的容量
3.第一次扩容,逻辑为newCapacity = oldCapacity + (oldCapacity >> 1);即在原有的容量基础上增加一半。
4.第一次扩容后,如果容量还是小于minCapacity,就将容量扩充为minCapacity。
5.对扩容后的容量进行判断,如果大于允许的最大容量MAX_ARRAY_SIZE,则将容量再次调整为MAX_ARRAY_SIZE。至此扩容操作结束。

ensureCapacity()扩容

我们在使用Arraylist时,经常要对它进行初始化工作,在使用add()方法增加新的元素时,如果要增加的数据量很大,应该使用ensureCapacity()方法,该方法的作用是预先设置Arraylist的大小,这样可以大大提高初始化速度。

/**
* 增加ArrayList容量。
* 
* @param   minCapacity   想要的最小容量
*/
public void ensureCapacity(int minCapacity) {
    // 如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,最小扩容量为DEFAULT_CAPACITY,否则为0
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)? 0: DEFAULT_CAPACITY;
    //如果想要的最小容量大于最小扩容量,则使用想要的最小容量。
    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}
add( int index, E element)

需要先对元素进行移动,然后完成插入操作,也就意味着该方法有着线性的时间复杂度,即O(n)。

	/**
	 * 在制定位置插入元素。当前位置的元素和index之后的元素向后移一位
	 *
	 * @param index 即将插入元素的位置
	 * @param element 即将插入的元素
	 * @throws IndexOutOfBoundsException 如果索引超出size
	 */
	public void add(int index, E element) {
	    //越界检查
	    rangeCheckForAdd(index);
	    //确认list容量,如果不够,容量加1。注意:只加1,保证资源不被浪费
	    ensureCapacityInternal(size + 1);  // Increments modCount!!
	    // 对数组进行复制处理,目的就是空出index的位置插入element,并将index后的元素位移一个位置
	    System.arraycopy(elementData, index, elementData, index + 1,size - index);
	    //将指定的index位置赋值为element
	    elementData[index] = element;
	    //实际容量+1
	    size++;
	}

总结:
1.越界检查
2.空间检查,如果有需要进行扩容
3.插入元素

remove( int index)

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。

/**
 * 删除list中位置为指定索引index的元素
 * 索引之后的元素向左移一位
 *
 * @param index 被删除元素的索引
 * @return 被删除的元素
 * @throws IndexOutOfBoundsException 如果参数指定索引index>=size,抛出一个越界异常
 */
public E remove(int index) {
    //检查索引是否越界。如果参数指定索引index>=size,抛出一个越界异常
    rangeCheck(index);
    //结构性修改次数+1
    modCount++;
    //记录索引为inde处的元素
    E oldValue = elementData(index);

    // 删除指定元素后,需要左移的元素个数
    int numMoved = size - index - 1;
    //如果有需要左移的元素,就移动(移动后,该删除的元素就已经被覆盖了)
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // size减一,然后将索引为size-1处的元素置为null。为了让GC起作用,必须显式的为最后一个位置赋null值
    elementData[--size] = null; // clear to let GC do its work

    //返回被删除的元素
    return oldValue;
}

总结:
1.检查索引是否越界。如果参数指定索引index>=size,抛出一个越界异常
2.将索引大于index的元素左移一位(左移后,该删除的元素就被覆盖了,相当于被删除了)。
3.将索引为size-1处的元素置为null(为了让GC起作用)。

Vector

1. 同步

它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。

	public synchronized boolean add(E e) {
	    modCount++;
	    ensureCapacityHelper(elementCount + 1);
	    elementData[elementCount++] = e;
	    return true;
	}
	
	public synchronized E get(int index) {
	    if (index >= elementCount)
	        throw new ArrayIndexOutOfBoundsException(index);
	
	    return elementData(index);
	}

2. 与 ArrayList 的比较

  • Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
  • Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。

3.替代方案

可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

	List<String> list = new ArrayList<>();
	List<String> synList = Collections.synchronizedList(list);

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

	List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList

读写分离

  • 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
  • 写操作需要加锁,防止并发写入时导致写入数据丢失。
  • 写操作结束之后需要把原始数组指向新的复制数组。
	public boolean add(E e) {
	    final ReentrantLock lock = this.lock;
	    lock.lock();
	    try {
	        Object[] elements = getArray();
	        int len = elements.length;
	        Object[] newElements = Arrays.copyOf(elements, len + 1);
	        newElements[len] = e;
	        setArray(newElements);
	        return true;
	    } finally {
	        lock.unlock();
	    }
	}
	
	final void setArray(Object[] a) {
	    array = a;
	}
	@SuppressWarnings("unchecked")
	private E get(Object[] a, int index) {
	    return (E) a[index];
	}

适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

但是 CopyOnWriteArrayList 有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景

LinkedList

1.概览

LinkedList是一个实现了List接口Deque接口的双端链表。
LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;
LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法

	List list=Collections.synchronizedList(new LinkedList(...));

基于双向链表实现,使用 Node 存储链表节点信息。

	private static class Node<E> {
	    E item;
	    Node<E> next;
	    Node<E> prev;
	}

每个链表存储了 first 和 last 指针:

	transient Node<E> first;
	transient Node<E> last;

在这里插入图片描述

2.源码

add(E e)

add(E e) 方法:将元素添加到链表尾部

	public boolean add(E e) {
        linkLast(e);//这里就只调用了这一个方法
        return true;
    }
   /**
     * 链接使e作为最后一个元素。
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;//新建节点
        if (l == null)
            first = newNode;
        else
            l.next = newNode;//指向后继元素也就是指向下一个元素
        size++;
        modCount++;
    }

add(int index, E element)

	public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

get(int index)

get(int index): 根据指定索引返回数据

	public E get(int index) {
        //检查index范围是否在size之内
        checkElementIndex(index);
        //调用Node(index)去找到index对应的node然后返回它的值
        return node(index).item;
    }

根据下标和中点位置决定从前往后遍历还是从后往前遍历,这样可以加快查找。

	/**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

remove(Object o) & remove(int index)

remove() ,removeFirst(), pop(): 删除头节点
removeLast(), pollLast(): 删除尾节点
removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null。

remove(Object o): 删除指定元素

	public boolean remove(Object o) {
        //如果删除对象为null
        if (o == null) {
            //从头开始遍历
            for (Node<E> x = first; x != null; x = x.next) {
                //找到元素
                if (x.item == null) {
                   //从链表中移除找到的元素
                    unlink(x);
                    return true;
                }
            }
        } else {
            //从头开始遍历
            for (Node<E> x = first; x != null; x = x.next) {
                //找到元素
                if (o.equals(x.item)) {
                    //从链表中移除找到的元素
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

当删除指定对象时,只需调用remove(Object o)即可,不过该方法一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。

unlink(Node x) 方法:

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;//得到后继节点
        final Node<E> prev = x.prev;//得到前驱节点

        //删除前驱指针
        if (prev == null) {
            first = next;//如果删除的节点是头节点,令头节点指向该节点的后继节点
        } else {
            prev.next = next;//将前驱节点的后继节点指向后继节点
            x.prev = null;
        }

        //删除后继指针
        if (next == null) {
            last = prev;//如果删除的节点是尾节点,令尾节点指向该节点的前驱节点
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

remove(int index):删除指定位置的元素

	public E remove(int index) {
        //检查index范围
        checkElementIndex(index);
        //将节点删除
        return unlink(node(index));
    }

3. 与 ArrayList 的比较

  • ArrayList 基于动态数组实现,LinkedList 基于双向链表实现;
  • ArrayList 支持随机访问,LinkedList 不支持;
  • LinkedList 在任意位置添加删除元素更快。

面试题

1.Arraylist 与 LinkedList 区别?

  1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

  2. 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)

  3. 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。

  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

  5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

双向链表和双向循环链表
双向链表: 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。
双向循环链表: 最后一个节点的next 指向head,而 head 的prev指向最后一个节点,构成一个环。

2.ArrayList 和 Vector 有何异同点?

相同点:
(1)两者都是基于索引的,都是基于数组的。
(2)两者都维护插入顺序,我们可以根据插入顺序来获取元素。
(3)ArrayList 和 Vector 的迭代器实现都是 fail-fast 的。
(4)ArrayList 和 Vector 两者允许 null 值,也可以使用索引值对元素进行随机访问。
不同点:
(1)Vector 是同步,线程安全,而 ArrayList 非同步,线程不安全。对于 ArrayList,如果迭代时改变列表,应该使用 CopyOnWriteArrayList。
(2)但是,ArrayList 比 Vector 要快,它因为有同步,不会过载。
(3)在使用上,ArrayList 更加通用,因为 Collections 工具类容易获取同步列表和只读列表。

3.说一说 ArrayList 的扩容机制

  • 看上面源码

4.数组(Array) 和列表(ArrayList) 有什么区别?什么时候应该使用 Array 而不是ArrayList?

答:不同点:
定义上:Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
容量上:Array 大小固定,ArrayList 的大小是动态变化的。操作上:ArrayList 提供更多的方法和特性,如:addAll(),removeAll(),iterator()等等。

使用基本数据类型或者知道数据元素数量的时候可以考虑 Array;
ArrayList 处理固定数量的基本类型数据类型时会自动装箱来减少编码工作量,但是相对较慢。

5.快速失败(fail-fast) 和安全失败(fail-safe)

一:快速失败(fail—fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。

场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

二:安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

总结:
快速失败和安全失败是对迭代器而言的。
快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出 ConcurrentModification 异常,java.util 下都是快速失败。
安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在 java.util.concurrent 下都是安全失败

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值