List再整理,从代码底层全面解析List(看完后保证收获满满)

前言

本文为对List集合的再一次整理,从父集接口Collection到顶级接口Iterable再到线程不安全实现类:ArrayList、LinkedList,再到线程安全实现类:Vector(被弃用)、CopyOnWriteArrayList。java

List

List集合扩展了Collection接口,它是一个容许重复的集合,即容许有多个元素引用相同的对象。
咱们来看看List接口的源码:git

public interface List<E> extends Collection<E> {

void add(int index,Object ele)// 根据下标添加元素
Boolean addAll(int index,Collection eles) // 在下标index插入另外一个集合的所有元素
Object get(index) // 获取对应下标的元素
int indexOf(Object ele) // ele在集合中首次出现的位置 若是不存在,返回-1
int lastIndexOf(Obj ele) // ele在集合中最后出现的位置 若是不存在,返回-1
Object remove(int index) 移除指定位置索引的元素
Object set(int index,Object ele); // 在下标在下标index插入一个元素

}

能够看到List接口继承了Collection接口,那么接下来看看Collection接口github

Collection

在java类库中,Collection接口是集合类的基本接口,这个接口有两个基本的方法:web

public interface Collection<E> extends Iterable<E>
{
    boolean add(E element);
    Iterator<E> iterator();
    ...
}

add方法用于向集合中添加元素。若是添加元素确实改变了集合就返回true,若是集合没有发生改变就返回false。(其实后面具体的实现类都会对该方法进行重写)列如,向一个集合中添加一个已经存在的对象,这个添加请求就没有效果会返回false,由于Set集合不容许重复对象。从代码中能够看到, Collection接口还包括了一个iterator()方法,返回类型为Iterator接口对象,那么什么Iterator接口?数组

Collection接口中的其它方法:安全

//添加方法:
add(Object o) //添加指定元素
addAll(Collection c) //添加指定集合
//删除方法:
remove(Object o) //删除指定元素
removeAll(Collection c) //输出两个集合的交集
retainAll(Collection c) //保留两个集合的交集
clear() //清空集合
//查询方法:
size() //集合中的有效元素个数
toArray() //将集合中的元素转换成Object类型数组
//判断方法:
isEmpty() //判断是否为空
equals(Object o) //判断是否与指定元素相同
contains(Object o) //判断是否包含指定元素
containsAll(Collection c) //判断是否包含指定集合

咱们还知道有一个Collections,它是一个包装类。它包含有各类有关集合操做的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。数据结构

Iterable

翻看Iterable源码:并发

public interface Iterable<T> {
    /** * Returns an iterator over elements of type {@code T}. * * @return an Iterator. */
    Iterator<T> iterator();
}

能够看到在Iterable接口中定义了一个iterator方法返回类型为Iterator,继续跟进这个Iterator:框架

public interface Iterator<E> {
   
    boolean hasNext();
    E next();
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

很明显的能够看到,这个Iterator接口就像当于链表中的结点,只不过在C语言里结点里的指针在java中变成了对象的引用。那么 咱们就应该知道,经过反复的调用nest()就能够逐个的访问集合中的每一个元素,可是当到达了集合的末尾,nest方法将抛出一个NoSuchElementException。所以,每次都用next方法前都应该调用hasNext方法进行判断。hasNext方法的做用是判断对象是否还有下一个元素,有就返回true,不然返回false。remove方法会删除上次调用next方法时返回的元素。就像是删除一个元素以前先看下它是颇有必要的,remove方法就是按照这个理念设计的。举一个访问集合中全部元素的案例:dom

Collection<String> s = new ArrayList<String>();
        s.add("xiaohong");
        s.add("xionming");
        s.add("wanger");

        Iterator<String> iterator = s.iterator();
        while (iterator.hasNext()) {

            String element = iterator.next();
            System.out.println(element);

在Java SE8版本中,新加入了for each循环遍历,编译器简单地将“for each”循环翻译为带有迭代器的循环。

for (String string : s)
 {
    System.out.println(string);
		
 }

显然,经过"for each"遍历使得代码更加简洁,全部实现了Iterable接口的对象均可以使用"for each"循环。Collection接口扩展了Iterable接口。
好了,到这里咱们就看完了List的父级接口了,接下来咱们看看它的实现类。

AbstractList

若是实现了Collection接口的每个类都要实现它的全部方法,那么将是一件很烦的事情。此时,AbstractList应运而生。它将基础的iterator抽象化,其它的方法给实现了,此时一个具体的集合类就能够扩展AbstractList类,而且只需提供Iterator方法,固然若是不满意AbstractList类实现的方法也能够在子类重写它的方法。

线程不安全实现类

ArrayList

咱们先来看看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; 
	...
	public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
	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. 当经过 new ArrayList()建立对象时,它会分配一个定义好的不能序列化的空的Object数组
  2. 当经过 new ArrayList(int initialCapacity)建立对象时,当initialCapacity大于0时,会返回一个初始大小为10的Object数组。
  3. ArrayList 继承了AbstractList,AbstractList实现了List接口中的大部分方法,提供了相关的添加、删除、修改、遍历等功能。
  4. ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,代表实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,咱们便可以经过元素的序号快速获取元素对象,这就是快速随机访问。
  5. ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
  6. ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能经过序列化去传输。

咱们再看看ArrayList是如何扩容的,咱们跟进add():

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

	private int size;
	 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	
	...

	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) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
	 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);
    }
	private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
}

分析:

  1. 首先咱们看到它调用了ensureCapacityInternal(size + 1)方法,并传了一个最小容量过去(minCapacity),其中size初始化大小为0。接着咱们跟进ensureCapacityInternal方法;
  2. 能够看到ensureCapacityInternal方法中又调用了ensureExplicitCapacity()方法,而且ensureExplicitCapacity()方法的参数为calculateCapacity(elementData, minCapacity)的返回值,那咱们跟进calculateCapacity(elementData, minCapacity)方法
  3. 在calculateCapacity中进行了一个判断,若是数组为null,则返回默认大小10(DEFAULT_CAPACITY),不然返回minCapacity。接着咱们再跟进ensureExplicitCapacity
  4. 能够看到在ensureExplicitCapacity方法中进行了一个判断,当minCapacity比数组的长度大就调用grow(minCapacity)方法扩容,不然什么都不干,咱们跟进grow(minCapacity)
  5. 能够看到新的容量newCapacity为旧的容量oldCapacity加上oldCapacity右移一位,也就是说新的容量是旧的容量的1.5倍,再将新容量和最小容量进行比较,小于就直接将最小容量付给新的容量。若是新的容量大于MAX_ARRAY_SIZE再调用hugeCapacity函数
  6. 能够看到最后的扩容会建立一个新的数组,并将老的数组拷贝过来。

LinkedList

LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操做,另外它实现了Deque接口,使得LinkedList类也具备队列的特性; LinkedList不是线程安全的,若是想使LinkedList变成线程安全的,能够调用静态类Collections类中的synchronizedList方法。
须要提一下的是,LinkedList类中有一个ListIterator listIterator方法,listIterator接口中包含一个add方法:

public interface ListIterator<E> extends Iterator<E> {
 
    boolean hasNext();
    E next();
    boolean hasPrevious();
    void set(E e);
    void add(E e);
}

由于链表是一个有序的集合,每一个对象的位置就显得十分重要。LinkedList中的add方法只能将对象添加到链表尾部,而常常却要将对象添加到链表的中间,迭代器就是用于描述集合中的位置的,因此这种依赖位置的方法就交由迭代器来完成。由于Set集合是无序的,因此在Iterator接口中就没有add方法,而是扩展了一个LinkIterator接口来实现。

值得一提的是:你们都知道,链表是不支持快速随机访问的。若是要查看链表中的第n个元素,就必须从头开始,越过n-1个元素,没有捷径可走,但尽管如此,LinkedList仍是提供了一个用来访问某个特定元素的get方法,固然这个方法的效率并不高,若是在使用这个方法,那么可能对于所要解决的问题使用了错误的数据结构。LinkedList类中get方法所谓的随机访问都是须要从列表的头部开始搜索,效率极低。使用链表的惟一理由是尽量的减小在链表中间插入或删除元素所付出的代价。

LinkedList中特有的方法

//查询方法:
getFirst() //获取集合中的第一个元素
getLast() //获取集合中的最后一个元素
//添加方法:
addFirst(Object o) //在集合的第一个位置添加指定元素
addLast(Object o) //在集合的最后一个位置添加指定元素
//删除方法:
removeFirst() //删除集合中的第一个元素
removeLast() //删除集合中的最后一个元素

下面程序简单的建立了两个链表,将它们合并在一块儿,而后从第二个链表中每隔一个元素删除一个元素,最后测试removeAll()方 法 :

package listdemo;
 
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
 
public class LinkedListTest {
 
	public static void main(String[] args) {
		
		List<String> a = new LinkedList<>();
		a.add("aaa");
		a.add("bbb");
		a.add("eee");
		
		List<String> b = new LinkedList<>();
		b.add("AAA");
		b.add("BBB");
		b.add("EEE");
		
		ListIterator<String> aIter = a.listIterator();
		ListIterator<String> bIter = b.listIterator();
		
		//a集合合并b集合
		
		while(bIter.hasNext()) {
			
			if(aIter.hasNext())
				 aIter.next();
			aIter.add(bIter.next());
		}
		
		System.out.println(a);
		
		//从b链表中每间隔一个元素删除一个元素
		
		while(bIter.hasNext()) {
			
			bIter.next();//跳过一个元素
			if(bIter.hasNext()) {
				
				bIter.next();
				bIter.remove();//先查后删
			}
		}
		
		System.out.println(b);
		
		//测试删除全部
		a.removeAll(a);
		System.out.println(a);
		
	}
}

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更多的空间(由于要存放直接后继和直接前驱以及数据)。

ArrayList不安全例子

public class ListTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(1,8));
                System.out.println(list);
            },Integer.toString(i)).start();
        }
    }
}

运行结果:报java.util.ConcurrentModificationException异常。
致使缘由:并发争抢修改致使
如何解决?有三种方法:

  1. 使用Collections工具类中的synchronizedList包装ArrayList方法即:Collections.synchronizedList(new ArrayList<>());其底层实现是根据是否实现RandomAccess接口而new两个不一样的内部类(SynchronizedRandomAccessList<>(list),SynchronizedList<>(list)),而后在添加方法subList中使用了synchronized锁。
  2. 使用Vector类(该类由于读写方法都用sychronizd关键字修饰,性能差,基本已弃用)
  3. 使用CopyOnWriteArrayList类

线程安全类

Vector

vector类和ArrayList类的差异就是Vector在每一个方法前都加了个sychronized锁,其它地方和ArrayList基本一致,因为性能太差,基本已被弃用。

CopyOnWriteArrayList

CopyOnWrite的意思:在计算机,若是你想要对一块内存进行修改时,咱们不在原有内存块中进行写操做,而是将内存拷贝一份,在新的内存中进行写操做,写完以后再将指向原来内存的指针指向新的内存,原来的内存就能够等着被GC回收了。
再来看看源码:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	
	final transient ReentrantLock lock = new ReentrantLock();
	private transient volatile Object[] array;
	 
	 ...

	public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
	final void setArray(Object[] a) {
        array = a;
    }
	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();
        }
    }
	
	public E get(int index) {
        return get(getArray(), index);
    }
	final Object[] getArray() {
        return array;
    }
}

经过源码咱们能够看到:

  • CopyOnWriteArrayList一样实现了List, RandomAccess, Cloneable, java.io.Serializable接口。
  • 定义了一个可从入锁lock,在Object数组前加了volatile关键字修饰,使得Object线程可见。(关于volatile关键字能够看个人另外一篇博客:从底层吃透java内存模型(JMM)、volatile、CAS
  • 须要注意的是:CopyOnWriteArrayList只提供了三个构造器:无参构造器、一个参数为List构造器,一个参数为数组构造器。并无提供指定初始容量的构造器,这是由于每次的添加操做都是复制一个新的数组来取代旧的数组的,这也就无需指定初始的数组容量大小了
  • CopyOnWriteArrayList 类的全部可变操做(add,set等等)都是经过建立底层数组的新副原本实现的。当 List 须要被修改的时候,并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完以后,再将修改完的副本替换原来的数据,这样就能够保证写操做不会影响读操做了。
  • 在全部的读取操做时是不加锁的,因此CopyOnWriteArrayList适用于读多写少的场景中,事实上就算是写多的场景CopyOnWriteArrayList在性能上也要好于Vector,因此这就致使着Vector基本被弃用。

最后附上在最开始说的ArrayList线程不安全使用CopyOnWriteArrayList解决的代码:

public class CopyOnWriteArrayListTest {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(1,8));
                System.out.println(list);
            },Integer.toString(i)).start();
        }
    }
}

代码

本文所涉及的全部代码都在个人GitHub上:https://github.com/dave0824/jmm

推荐阅读

从底层吃透java内存模型(JMM)、volatile、CAS