3. 老生常谈List

452 阅读11分钟

从这篇开始写关于Java容器相关的,主要分为三类:List,Set,Map.目前来看只会涉及主要的实现,但也不排除穿插'老家伙'的情况.

1.Collection

既然要写容器,那肯定先从Collection入手,CollectionListSet的父接口.Map则是单独的接口.在这里提下Collection,后面就不再赘述了.

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

Collection继承Iterable接口,实现了Iterable就表示允许该对象成为for-each的目标对象,者也是Java的一个语法糖.该类就包含一个个返回Iterator的方法和两个接口默认方法,具体可以自行查看.

Collection作为List,Set接口的根接口,并没有什么实质性的内容,只是定义了些通用方法用户在不同类型中传参,具体的内部细节有子类去掌控,如是否允许为空之类的.而且JDK也没有提供该接口的具体实现.

2. List

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

以上便是List接口的定义信息,从官方文档注释来看,List代表的是有序的集合,可以精确的控制每个元素的插入位置,可以通过整数索引获取指定位置的元素.并且允许重复,可以有多个null值或不为null,取决于具体的实现(大部分都允许包括null值).

官方还提示了一点,在你不知道List的具体类型的情况下,迭代列表比通过索引获取更加可取.同时需要慎用通过索引获取元素,这将执行高代价的线性搜索.

3.ArrayLsit

1. 概览

ArrayList可以说是老大哥的存在,绝大部分情况下List实现首选,先看下结构:

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

先看下ArrayList所继承的类:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

AbstractList提供了List接口的基本实现,以减少采用随机访问(数组)实现Lsit接口所需的工作.

上一层

public abstract class AbstractCollection<E> implements Collection<E> {

AbstractCollection提供了Collection的基本实现,以减少实现该接口所需要的工作.如果你只需要实现一个不可修改的集合,那么扩展该类即可,只需重写iterator()size()即可,要实现可修改的,那么你还需要重写add()方法,因为该add()方法默认抛出异常.

AsbtractCollectionAbstractList的区别就是一个是针对需要实现Collection服务的,一个是针对使用数组实现List服务的.

再回到ArrayList,实现了RandomAccess接口,该接口只是一个标记接口,表示该类支持随机访问,在选择对应算法时就可以通过该接口选择合适的算法.Cloneable也是一个标记接口,表示该类支持使用clone()方法,如果没有实现Cloneable而去调用clone()则会产生CloneNotSupportedException异常.最后一个Serializable就不说了,相信大家都知道.

2.方法

上面提了一下ArrayList的类定义信息,这里我们先粗略的看一下基本熟悉:

	/**
     * 默认初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * 慢性膨胀,和下面这个主要用去区分第一次膨胀的大小
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 第一次膨胀为DEFAULT_CAPACITY
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     *	实际存储元素数组 transient不被序列化
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 数组包含的元素数量
     * @serial
     */
    private int size;

刚开始令我疑惑的就是上面的EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA属性,为什么需要两个默认的空数组,后续讲到扩容的时候便会体现出这个区别,这里先透露一下: 使用带了指定长度的构造函数使用的是EMPTY_ELEMENTDATA,而默认的空参构造方法使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA.

构造函数这里就略过了,只是一些初始化操作.下面看点常用方法

indexOf()

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

indexOf()用于查找元素第一次出现的索引,挺简单的.contains()复用了这个方法,与之相识的还有lastIndexOf(),实现也很简单,只是方向遍历查找值.

clone()

public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }

ArrayListclone()只是浅拷贝.并没有去处理对象值的情况.

add()

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

这个方法算是比较重要的方法,涉及到扩容之类的,上面便是add()的定义,比较重要的还是ensureCapacityInternale().我们就着重讲一下该方法,我们假设第一次调用,也就是参数为1调用该方法.

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

elementData也就是我们正在存储数据的地方,还记得开头说的elementData具体的是EMPTY_ELEMENTDATA还是DEFAULTCAPACITY_EMPTY_ELEMENTDATA吗?我们把这两种情况都带上:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

从上面方法功能看出,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA则我们这个传递的1就被淘汰了,换来的便是默认的DEFAULT_CAPACITY,也就是10,如果不是则还是我们的1,那要是一开始参数就大于默认的10,则还是以参数的为准.

 private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

上面判断是否需要扩容,默认第一次初始化为1-0>0true,所以,只有在初始化和容器满时才需要扩容.同时modCount++记录操作,用于快速失败.

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);
}

把参数带进这个方法便得出扩容结果是多少, 如果是EMPTY_ELEMENTDATA,则第一次为1, 是DEFAULTCAPACITY_EMPTY_ELEMENTDATA则第一次为10, 多带入几次, 就会发现如果一开始初始化时如果指定了长度,则使用指定长度的. 如果是指定长度是0, 也是会有自动增长的, 但是增长都是从低处开始1,2,3,4,6.....相对于使用无参数的构造方式,扩容的大小不是那么大,但前期每次都会试探性的扩容,逐渐越来越大.

所以,尽量能确定所需要使用的空间,避免扩容发生数组复制.如不能确定,也可以估算一个相对大一点的值.

还有一个add()方法,就是在指定位置添加,尽量少使用该方法,因为每次都会进行数组复制.

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;
}

remove()没什么好说的,就是把后面的往前移动一个位置. 这也看出每次复制都会进行数组复制.还有一个remove()的重载方法就是删除指定的元素,这个效率更低了,因为还多了层遍历操作.

还有一个比较重要的就是iterator()方法,这一块涉及到fail-fast机制:

iterator()

iterator()方法属于iterable接口定义的,上面也说了,实现iterable接口就可以成为for-each的目标对象,也就是诸如下面这种形式:

for(String str:list){}

如果没有实现iterable是不能成为该目标的,我们可以自己尝试下:

// 编译通过
public class Demo implements Iterable<String>{
    public static void main(String[] args) {
        Demo demo = new Demo();
        for (String s : demo) {}
    }
    @Override
    public Iterator<String> iterator() {
        return null;
    }
}
// 编译失败
public class Demo{
    public static void main(String[] args) {
        Demo demo = new Demo();
        for (String s : demo) {}
    }
}

而这种形式也就是Java的语法糖,除去语法糖就是下面这种形式:

Iterator var2 = list.iterator();
while(var2.hasNext()) {
     String s = (String)var2.next();
     list.remove(s);
}

Iterator 就是集合的一个迭代器,从Doc注释来看,这个迭代器替换了 Enumeration, 比Enumeration有两个不同之处:

  1. 允许在迭代的时候删除元素.
  2. 改进的方法名称.

接下类看下ArrayList的实现:

private class Itr implements Iterator<E> {
        int cursor;       // 判断是否有下一个元素
        int lastRet = -1; // 返回最后一个元素的索引,没有则返回-1
        int expectedModCount = modCount; // 用于快速失败

        Itr() {}

        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];
        }

        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使用一个内部类来实现Iterator,其实所谓的快速失败,就是通过modCount来实现的,在每次使用ArrayList进行修改操作时,都会执行modCount++,而同时使用迭代器进行遍历时,next()会校验modCount的值是否发生变化,如果不同,则会抛出异常.

ArrayList内容大致就以上几点.

4.LinkedList

1.概览

public class LinkedList<E> extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{

LinkedList所继承的类:

public abstract class AbstractSequentialList<E> extends AbstractList<E> {

AbstractSequentialListArrayListAbstractList相似.AbstractSequentialList提供了采用顺序访问(如链表)实现List接口的工作. AbstractList就不多说了,上面已经提过了.

回到LinkedList中,它实现了Deque接口,它所定义的就是一个双端队列,可以在头部或尾部插入,其他实现就不多介绍了,前面都有提及.

从类定义可以看出,LinkedList支持双端队列的操作,同时又有List的操作能力.所以,在使用LinkedList时,最好使用本类去引用的方式,而不要使用父类,不然有的方法会无法使用.LinkedList也允许元素为null

关于LinkedList的双向链表结构就不提了,也比较简单,网上也很多例子.

2. 方法

由于是双向链表,这里只讲一些相对重要的方法.

 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;
        }
    }

上面这个方法是LinkedList中通用的通过位置检索元素的方法,他在内部采取了一个半分的思想:如果要检索的位置比总数量的一半还要大那么从尾部开始检索,否则从头检索.

在使用get(index)之类的方法时,都用到了上述定义的方法.

LinkedList带给我们的其实不多,因为是链表的结构,也不涉及扩容之类的操作.需要注意的也就是些元素读取.

5.CopyOnWriteArrayList

1. 概述

根据官方的定义,该类时ArrayList的线程安全的变体,所有操作都是创建底层数组的新副本进行的,类的定义和ArrayList类似,这里就不多说了.

2.方法

获取方法和ArrayList相似,主要不同的是修改操作.

add()

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();
        }
    }

可以看到,add操作时先上锁,如果复制一个新数组,在新数组上操作,然后再把原有的替换为新数组,该类其实再性能上并不好,因为每次修改操作都会上锁,复制数组,消耗较大.但是可以避免并发修改异常,能避免的原因就是迭代器不支持修改操作,并且迭代器所持有的数据只有获取迭代器时之前的数据,因为在原数组上添加新元素,是复制的一个新数组,而不是原来的数组.

虽然性能不好,但是你在不想使用外部同步时,可以考虑这种方式.我认为迭代器不支持修改的主要原因是: 为了保持写时复制数组这以特性,不然在迭代器里面进行写操作时,也需要加上同一把锁进行同步,防止出现并发修改异常.这带来的效率非常低.

其主要场景就是在你不想使用外部同步,但遍历操作远远大于修改操作时,或者修改操作少时可以考虑使用.

6.Vector

1.概述

老大哥本来不太想写的,但其扩容机制有点特殊,和市面上提的二倍有点差异,这才写上一点.

2.方法

就直接进入核心方法吧, 前提须知: Vector有一个增量属性,该属性会影响扩容,默认不传为0,并且Vector没有延迟初始化的功能,不传指定长度,直接为10.

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

直接上核心代码了,这个capacityIncrement就是前面说的增量,可以看到,如果我们设置了增量,则扩容的大小为原来的大小加上我们指定的增量大小,没有指定则是旧的加上旧的,也就是两倍.

7.杂谈

List慢慢看还是挺容易理解的,无非就是继承关系容易看懵.多品品就好了.还有一点就是在使用Listiterator()时,尽量看看能否使用ListIterator去接受,该接口继承了Iterator接口,并提供了更多特性.但是如果使用Iterator去接受就享受不了该特性了.

在网上还有众多的关于List实现如何选择的文章,这里就不说了,毕竟也得根据业务来做具体选择.差距较大的还是体现在LinkedList按索引获取,ArrayList删除操作和头插入之类的操作中.

List就目前来说就先写以上几点,后续有什么新发现会更上的.