Thinking in Java之List接口、ArrayList类源码学习

        前言

                  上一篇文章探究了Collection接口的详细设计和相关的方法。文章的链接如下:

             http://blog.csdn.net/kiritor/article/details/8869937,接下来将会探究其子接口List的源码,通过

             源码探究其接口设计和特点。

        List接口设计

                  通过查阅源码,我们知道的是List接口继承至Collection接口,并且实现了集合中元素的有序存

              储。也就是说用户可以对列表中的每个元素的插入位置进行精确的控制,可以根据元素在集合中

              的索引对元素进行访问和遍历(类似数组)。

        List接口源代码解析

                 对于List接口的设计,我们直接看源码和API来的更加实际一些。       

package com.kiritor;

import java.util.Iterator;
import java.util.ListIterator;

/**
 * List源码探究
 * @author Kiritor*/
public interface List<E> extends Collection<E> {
    /**List作为Collection的子接口
     * 提供了Collection接口定义的方法
     * 这些方法在Collection源码学习中
     * 已经分析过了,就不在说明了
     * */
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean addAll(int index, Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    boolean retainAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();
    /**同时List接口定义了一些自己的方法
     * 来实现“有序”这一功能特点*/
    /**
     *返回列表中指定索引的元素
     *return E
     *throws IndexOutofBoundException(index<0||index>=size())
     * */
    E get(int index);
    /**
     *设定某个列表索引的值 
     *throws:
     *UnsupportedOperationException 
      ClassCastException 
      NullPointerException 
      IllegalArgumentException 
      IndexOutOfBoundsException 
     * */
    E set(int index, E element);
    /**
     *在指定位置插入元素,当前元素和后续元素后移
     *这是可选操作,类似于顺序表的插入操作 */
    void add(int index, E element);
    /**
     * 删除指定位置的元素(可选操作)*/
    E remove(int index);
    /**
     * 获得指定对象的最小索引位置,
     * 没有则返回-1*/
    int indexOf(Object o);
    /**获得指定对象的最大索引位置
     * 可以知道的是若集合中无给定元素的重复元素下
     * 其和indexOf返回值是一样的*/
    int lastIndexOf(Object o);
    /**一种更加强大的迭代器,支持向前遍历,向后遍历
     * 插入删除操作,此处不解释*/
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);
    /**
     * 返回索引fromIndex(包括)和toIndex(不包括)
     * 之间的视图。*/
    List<E> subList(int fromIndex, int toIndex);
}

         ArrayList类实现

                    对于ArrayList类的实现笔者就不将代码一一贴出来了,这里只是简要的对一些个方法的

              实现做一些分析。首先我们看看ArrayList类头吧       

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
               可以看出的是他实现了一些额外的接口RandomAccess、Cloneable、Serializable

                RandomAccess:List实现使用的标记接口,主要用于提高连续或随机访问列表时的性能。

                Cloneable:实现该接口的类可以对该类的实例进行克隆(按字段进行复制)

                Serializable:用于标识可序列化(何为序列化?为什么要序列化,有待总结)

              1、底层实现

               通过对其源码的观察,可以发现的是ArrayList的底层是通过数组来实现的。

            private transient Object[] elementData;
            private int size;
               这里解释一下 transient关键字 :
                          Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的

                      对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭

                      serialization,可以在这个域前加上关键字transient。         

             2、构造方法

                     通过查看源码,我们可以知道的是ArrayList提供三种构造方法

      public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }
    public ArrayList() {
        this(10);
    }
     public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        size = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }
                       前两种构造方法很容易理解,根据提供的参数来进行数组空间分配,默认大小为10,

             第三种构造则显得稍微复杂些,它是将给定集合转换成数组(按照集合本身的顺序),如果

             转化成的emlementData不是Object[]类型则调用Arrays.copyOf方法将其转换为Object[]。

                      Tips:需要特别注意前两个构造方法和第三个构造方法的区别,前两者生成的集合是

             无元素的,代码中也没有对size进行修改的地方,第三个构造器对size是有修改的。

          3、存储元素

                       ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、

               addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加

               元素的方法。下面我们一一讲解:

/*
 * 用指定的元素代替集合中指定位置的元素
 * 并返回以前位于该位置的
 */ 
public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

                    set方法会通过rangeCheck()函数检查index的返回,若index>=size则会主动抛出
       IndexOutOfBoundsException异常,不过需要注意的是对于index<0的情况并不会主动抛出
       该异常,且两种情况抛出的异常详细信息不同(如何不同读者可自己尝试),这对于我们以后
       ArrayList下表越界错误的查找也算有一定的帮助吧,助于说为什么JDK对于该方法为什么
       不将index<0情况考虑进去,可能是因为对于数组越界的问题,我们更加关心的是“向前越界”

    // 将指定的元素添加到此列表的尾部。  
    public boolean add(E e) {  
        ensureCapacity(size + 1); //该方法用于调整数组的容量  
        elementData[size++] = e;  
        return true;  
    }  
                ensureCapacity(size+1)这个方法主要用于数组容量的扩容的,至于如何实现,具体后面

           会详细分析。

    // 将指定的元素插入此列表中的指定位置。  
    // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。  
    public void add(int index, E element) {  
        if (index > size || index < 0)  
            throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
        // 如果数组长度不足,将进行扩容。  
        ensureCapacity(size+1);  // Increments modCount!!  
        // 将 elementData中从Index位置开始、长度为size-index的元素,  
        // 拷贝到从下标为index+1位置开始的新的elementData数组中。  
        // 即将当前位于该位置的元素以及所有后续元素右移一个位置。  
        System.arraycopy(elementData, index, elementData, index + 1, size - index);  
        elementData[index] = element;  
        size++;  
    }  
    // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。  
    public boolean addAll(Collection<? extends E> c) {  
        Object[] a = c.toArray();  
        int numNew = a.length;  
        ensureCapacity(size + numNew);  // Increments modCount  
        System.arraycopy(a, 0, elementData, size, numNew);  
        size += numNew;  
        return numNew != 0;  
    }  

    // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。  
    public boolean addAll(int index, Collection<? extends E> c) {  
        if (index > size || index < 0)  
            throw new IndexOutOfBoundsException(  
                "Index: " + index + ", Size: " + size);  
      
        Object[] a = c.toArray();  
        int numNew = a.length;  
        ensureCapacity(size + numNew);  // Increments modCount  
      
        int numMoved = size - index;  
        if (numMoved > 0)  
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);  
      
        System.arraycopy(a, 0, elementData, index, numNew);  
        size += numNew;  
        return numNew != 0;  
    } 

           4、读取元素

    //返回列表中指定位置的元素,需边界检查
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

           5、删除元素

                     ArrayList提供了根据下标或者指定对象两种方式来删除元素代码如下:
    // 移除此列表中指定位置上的元素,并返回,需边界转换  
    public E remove(int index) {  
        RangeCheck(index);  
      
        modCount++;  
        E oldValue = (E) elementData[index];  
      
        int numMoved = size - index - 1;  
        if (numMoved > 0)  
            System.arraycopy(elementData, index+1, elementData, index, numMoved);  
        elementData[--size] = null; // Let gc do its work  
      
        return oldValue;  
    }  
                  
    // 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。
     //该方法通过对象删除,不许进行边界检查
     public boolean remove(Object o) {  
        // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。  
        if (o == null) {  
            for (int index = 0; index < size; index++)  
                if (elementData[index] == null) {  
                    // 类似remove(int index),移除列表中指定位置上的元素。  
                    fastRemove(index);  
                    return true;  
                }  
    } else {  
        for (int index = 0; index < size; index++)  
            if (o.equals(elementData[index])) {  
                fastRemove(index);  
                return true;  
            }  
        }  
        return false;  
    }  
                   第二个方法牵扯到了ArrayList中的一个私有的方法,该方法和remove(int index)方法

          类似,但是并不需要进行边界检查(通过remove(Object o)已经可以index的合理性了),容易

          明白的是该方法不应该直接调用,因此设定为private的,对象引用无法直接调用它,安全性得以

          保障。

   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; // Let gc do its work
    }
                 Tips:从数组中移除一个元素也会导致后面的元素左移。

            6、调整容量

                   很久以前我们就知道ArrayList等集合都是动态的,那么这种动态是如何来实现的?通过

            查阅源码,这种动态性的实现主要有下面两个方法实现的。

    public void ensureCapacity(int minCapacity) {
        if (minCapacity > 0)
            ensureCapacityInternal(minCapacity);
     }

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

               其工作过程以add方法为例

// 将指定的元素添加到此列表的尾部。  
    public boolean add(E e) {  
        ensureCapacity(size + 1); //该方法用于调整数组的容量  
        elementData[size++] = e;  
        return true;  
    }  

                 在进行元素尾部插入的时候,首先确保插入元素后集合的size也就是minCapacity必须<=

           集合的容量,也就是elementData.lentgh,如果大于集合的容量之后则需通过grow方法

           进行扩容。阅读下面方法很容易知道ArrayList是按照自己的50%扩容的。

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进行扩容的时候,会将就数据中的元素重新拷贝一份到新的数组中去,这种
              代价是挺高的,因此我们应该尽量避免数组的扩容。而且需注意一点的是ensureCapacity()方法
              是public的,我们可以通过它手动的增加容量。
              而且ArrayList提供了一个有趣的方法,它可以将底层数组的容量调整为集合中实际元素的数量
  public void trimToSize() {  
        modCount++;  
        int oldCapacity = elementData.length;  
        if (size < oldCapacity) {  
            elementData = Arrays.copyOf(elementData, size);  
        }  
    }  

            7、Fail-Fast(快速失败)机制

                        仔细观察上述的各个方法,我们在源码中就会发现一个特别的属性modCount,API解释如下:
                 The number of times this list has been structurally modified. Structural modifications are those
                 that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress
                 may yield incorrect results.
                      记录修改此列表的次数:包括改变列表的结构,改变列表的大小,打乱列表的顺序等使正在进行
                 迭代产生错误的结果。Tips:仅仅设置元素的值并不是结构的修改
                     我们知道的是ArrayList是线程不安全的,如果在使用迭代器的过程中有其他的线程修改了List就会
                  抛出ConcurrentModificationException这就是Fail-Fast机制。   
                     那么快速失败究竟是个什么意思呢?
                 在ArrayList类创建迭代器之后,除非通过迭代器自身remove或add对列表结构进行修改,否则在其他
                 线程中以任何形式对列表进行修改,迭代器马上会抛出异常,快速失败。    
  

           8、迭代器遍历

                    我们知道Collection接口都有产生迭代器的接口,作为List接口的具体实现类ArrayList来说肯定是

               少不了的,而且ArrayList同时还提供一个ListIterator实现更为灵活的遍历,这里我们就Iterator做一些

               探究吧。看看产生Iterator的方法 

       /*返回一个迭代器接口的具体实现类*/
 public Iterator<E> iterator() {
        return new Itr();
 }

    /**
     * An optimized version of AbstractList.Itr
     */
    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;

        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();
        }
    }
                 通过阅读源码,很容易就知道了,通过迭代器我们并不需要知道集合的底层实现,我们只需

           生成一个迭代器对象,通过迭代器对象提供的方法,实现集合的遍历等操作。

                留意到的是checkForComodification可以知道,当多个线程同时操作一个ArrayList造成

           modeCount不一致时,会抛出ConcurrentModificationException异常,仔细观察又可以发现的是

           迭代器每一个方法执行都需要调用checkForComodification()方法,这样就实现了上面所说的

           Fail-Fast(快速失败),相较于迭代完成之后抛出异常更合理些。 

            9、一个小"陷阱"

                  通过阅读源码笔者还发现了一个有意思的问题,那就是subList(index1,index2)方法,返回一个

             从index1(包括)开始到index2(不包括)的子列表。通过观察可发现的是这里子列表的幕后还是原

             列表,对其的修改也会造成原列表的修改。

                如果不想出现这种情况,很简单 new ArrayList(list.subList(index1,index2));就ok了。

                好了,对于List、ArrayList的整理就到这个地方了,对于一些细节的东西笔者难免有些遗漏的 

            地方,希望给予指出与交流!  

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值