Java集合-ArrayList源码分析及注意事项

这篇文章给大家带来ArrayList的学习,如果错误希望不吝指出,感谢!平台 jdk1.7,ubuntu 14.02

1  ArrayList介绍

ArrayList是一个数组队列,容量可以动态变化,比java中的数组使用更加方便。

ArrayList继承&实现结构图(查看ArrayList):

      如图所示ArrayList直接继承自AbstractList,间接实现了List接口和继承了AbstractCollection抽象类。

ArrayList继承实现结构源码:

public class ArrayList<E> extends AbstractList<E>

        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

 

(1)ArrayList实现了Inerable接口

public interface Iterable<T>

      实现这个接口操作ArrayList时就可以使用增强的foreach对ArrayList进行遍历访问。 

(2)实现了SeriaLizable接口

public interface Serializable

      Serializable 接口内部没有字段或方法,仅起到标记可序列化的功能。

未实现此接口的类将无法使其任何状态序列化或反序列化,可序列化类的所有子类型本身都是可序列化的。

ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;

当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

(3)实现了Cloneable接口

public interface Cloneable

         实现了 Cloneable 接口,可以利用Object.clone() 方法合法地对该类实例进行按字段复制。 

如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常。 

(4)实现了RandomAccess接口

public interface RandomAccess

RandomAcces接口也是一个标记接口,无具体方法和字段,用来表明其支持快速随机访问。

此接口的主要目的是随机或连续访问列表时能提供良好的性能。 但此实现方法不是线程安全的,如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。

一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法将该列表“包装”起来,此对象的方法就变成了同步操作。

(5)实现List 接口

          此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引访问元素,并搜索列表中的元素。与 set 不同,列表通常允许重复的元素。更确切地讲,列表通常允许 e1.equals(e2)(e1和e2属于同一个ArrayList) 结果为true,并且如果列表本身允许 null 元素的话,通常它们允许多个 null 元素。

2  ArrayList构造函数:

ArrayList提供了3中构造函数:

// 默认空的构造函数构造函数

ArrayList()

// capacity是ArrayList的默认容量大小。当由于增加数据导致容量不足时,容量会添加上一次容量大小的一半。

ArrayList(int capacity)

// 创建一个包含collection的ArrayList

ArrayList(Collection<? extends E> collection)

3 ArrayList源码中常量和成员变量

private static final long serialVersionUID = 8683452581122892189L; private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size;private static final long serialVersionUID = 8683452581122892189L; private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; private int size;

serivalVersionUID用于序列化时作为标记

DEFAULAT_CAPACITY:默认初始化容量大小

EMPTY_ELEMENTDATA:调用空的构造函数,生成的list空实例包含的空数组

DEFAULTCAPACITY_EMPTY_ELEMENTDATA :利用默认容量构造ArrayList实例包含的空数组

elementData:用于存储ArrayList元素的缓存数组,容量可能大于真实存储的数据

Size:ArrayList中真实存在的元素个数

 ArrayList 实际上是通过一个数组elementData去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。当ArrayList容量不足以容纳全部元素时,ArrayList可以动态改变自身容量大小

elementData被关键字transient修饰

      观察源码 transient Object[] elementDataelementData是一个缓存数组,用于存储ArrayList中的数据,但可以看到elementData被关键字transient修饰,看过对象序列化(持久化)的都知道,被关键字transient修饰的字段,不会被序列化,那我们如何反序列化时如何取到ArrayList的数据呢?

elementData它通常会预留一些容量,等容量不足时再扩充容量,假如现在实际有了4个元素,而elementData的大小可能是10,那么在序列化时只需要储存5个元素,数组中的最后五个元素是没有实际意义的,不需要储存。所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。

private void writeObject(java.io.ObjectOutputStream s)

        throws java.io.IOException{

        // Write out element count, and any hidden stuff

        int expectedModCount = modCount;

        s.defaultWriteObject();

 

        // Write out size as capacity for behavioural compatibility with clone()

        s.writeInt(size);//首先序列化size

        // Write out all elements in the proper order.

        for (int i=0; i<size; i++) {//手动序列化真正存在的元素,elementData中多余的空间不会被写到硬盘

            s.writeObject(elementData[i]);

        }

        if (modCount != expectedModCount) {

            throw new ConcurrentModificationException();

        }

    }

   ArrayList中writeObject方法就是序列化方法,由于在ArrayList中的elementData这个数组的长度是变长的,java在扩容的时候,有一个扩容因子,也就是说这个数组的长度是大于等于ArrayList的长度的,我们不希望在序列化的时候将其中的空元素也序列化到磁盘中去,所以需要手动的序列化数组对象,所以使用了transient来禁止自动序列化这个数组。

4 ArrayList扩容:

调用ArrayList集合的add()方法,可能现有的elementData缓存数组容量不足,下面我们就看看ArrayList扩容的过程:

public boolean add(E e) {

        ensureCapacityInternal(size + 1);  //调用ensureCapacityInternal

        elementData[size++] = e;

        return true;

}   

private void ensureCapacityInternal(int minCapacity) {

        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

        }

        ensureExplicitCapacity(minCapacity);

    }

 

    private void ensureExplicitCapacity(int minCapacity) {

        modCount++;

        // overflow-conscious code

        if (minCapacity - elementData.length > 0)

            grow(minCapacity);//如果剩余容量不足,最终会调用grow函数

    }

 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);//特殊情况,元素超出MAX_ARRAY_SIZE

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

    }

 可以看到最终的扩容方案在grow函数里,首先获取  oldCapacity = elementData.length;然后设置新的newCapacity = oldCapacity + (oldCapacity >> 1);//扩容方案为oldCapacity+oldCapacity>>1。

手动扩容:

public void ensureCapacity(int minCapacity) {

        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)

            // any size if not default element table

            ? 0

            // larger than default for default empty table. It's already

            // supposed to be at default size.

            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {

            ensureExplicitCapacity(minCapacity);

        }

    }

 在添加大量元素前,应用程序可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,最终还是调用ensureExplicitCapacity函数,这可以减少递增式再分配的数量。

扩容耗时分析:

public static void main(String[] args) {  

        Student student1 = null;  

        Student student2= null;  

        long begintime1 = System.currentTimeMillis();  

        //未指定ArrayList长度

        List<Student> list1 = new ArrayList<>();  

        for(int i = 0 ; i < 10000000; i++){  

            student1 = new Student("li"+i,i);  

            list1.add(student1);  

        }  

        long endtime1 = System.currentTimeMillis();  

        System.out.println("time use1:" + (endtime1 - begintime1));  

          

        long begintime2 = System.currentTimeMillis();  

        //指定ArrayList长度

        List<Student> list2 = new ArrayList<>(10000000);  

        for(int i = 0 ; i < 10000000; i++){  

            student2= new Student("li"+i,i);  

            list2.add(student2);  

        }  

        long endtime2 = System.currentTimeMillis();  

        System.out.println("time use2:" + (endtime2 - begintime2));  

} 

 各位可以自己运行上面的程序,可以看到指定集合大小消耗的时间比不指定大小少很多,ArrayList增加元素时,会检测当前容量是否已经到达临界点,如果到达临界点则会扩容,扩容的过程就是生成新数组,拷贝旧数组到新数组,而且如果最终添加的元素数需要多次扩容,这个过程很耗时,所以如果能实现已知集合大小,初始化时就指定集合大小,这样可以给ArrayList带来效率的提升。

5 ArrayList遍历方式

ArrayList支持3种遍历方式(也有说4-5种的,基本的应该就这三种)

(1) 第一种,通过迭代器遍历。即通过Iterator去遍历。

ArrayList<Integer> list=new ArrayList<Integer>();

int value;

//add(.......);

Iterator iter = list.iterator();

while (iter.hasNext()) {

    value = iter.next();

}

(2) 第二种,随机访问,通过索引值去遍历。
由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。

ArrayList<Integer> list=new ArrayList<Integer>();

int value;

//add(.......);

int size = list.size();

for (int i=0; i<size; i++) {

    value = list.get(i);        

}

(03) 第三种ArrayList实现了Inerable接口可以利用增强的for循环。如下:

ArrayList<Integer> list=new ArrayList<Integer>();

int value;

//add(.......);

for (int  intval:list) {

    value = intval;

}

至于三者的效率问题,由于利用数组存储,所以应该是根据下标随机访问更快,但实验结果跟我想的不太一样,希望哪位大神跟我说下怎么设计合理的遍历算法:

代码如下(每次时间不确定,重复3000000次):

long t1,t2;  

ArrayList <String>list=new ArrayList<String>();

for(int i=1;i<3000000;i++){

list.add("lidx"+i);

}

 t1=System.currentTimeMillis();  

        for(String tmp:list)  

        {  

         //  System.out.println(tmp);

        }  

        t2=System.currentTimeMillis();  

        System.out.println("foreach 语句遍历时间:" + (t2 -t1) );  

        t1=System.currentTimeMillis();  

        for(int i = 0; i < list.size(); i++)  

        {  

        //System.out.println(list.get(i) ); 

        String tmp=list.get(i);

        }  

        t2=System.currentTimeMillis();  

        System.out.println("普通for语句遍历时间=:" + (t2 -t1) );  

        Iterator<String> iter = list.iterator();  //ListIterator也可以

        t1=System.currentTimeMillis();  

        while(iter.hasNext())  

        {  

          //  System.out.println( iter.next() ); 

        String tmp=iter.next();

        }  

        t2=System.currentTimeMillis();  

        System.out.println("迭代器遍历时间=:" + (t2 -t1) );  

6 支持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);

        }

}

实例:

ArrayList <String>list1=new ArrayList<String>();

for(int i=1;i<100;i++){

list1.add("lidx"+i);

}

ArrayList list2=list1;

ArrayList list3=(ArrayList) list1.clone();

System.out.println(list1==list2);//true

System.out.println(list1==list3);//false

Clone方法是浅拷贝,只是复制了一份数据,不是同一个对象。

ArrayList与数组转换 
ArrayList提供了一个将List转为数组的一个非常方便的方法toArray。toArray有两个重载的方法: 
1.list.toArray(); 
2.list.toArray(T[] a); 

 public <T> T[] toArray(T[] a) {

        if (a.length < size)

            // Make a new array of a's runtime type, but my contents:

            return (T[]) Arrays.copyOf(elementData, size, a.getClass());

        System.arraycopy(elementData, 0, a, 0, size);

        if (a.length > size)

            a[size] = null;

        return a;

    }

        第一个重载方法,是将list直接转为Object[] 数组,如果想要得到想要的类型,还需要遍历数组,一个元素一个元素的转换不太方便。

第二种方法可以直接将list转化为你所需要类型的数组,如果传输的参数数组类型和定义的泛型不同,无法通过编译。

ArrayList <String>list=new ArrayList<String>();

for(int i=0;i<100;i++){

list.add("lidx"+i);

}

Object []objarray=list.toArray();

String[] strArray1=list.toArray(new String[100]);

String[] strArray2=list.toArray(new String[50]);

String[] strArray3=list.toArray(new String[200]);

String[] strArray4=list.toArray(new String[0]);

System.out.println("size="+objarray.length);//100

System.out.println("size="+strArray1.length);//100

System.out.println("size="+strArray2.length);//100

System.out.println("size="+strArray3.length);//200

System.out.println("size="+strArray4.length);//100

toArray生成数组的容量如实验所示,当传入参数数组长度小于ArrayList时,最终生成数组的长度为ArrayList长度,当作为参数传输数组长度大于ArrayList时,最终生成数组长度为传入参数长度。

7 subList生成的集合问题:

public List<E> subList(int fromIndex, int toIndex) {

            subListRangeCheck(fromIndex, toIndex, size);

            return new SubList(this, offset, fromIndex, toIndex);//调用到内部类

        }

 SubList(AbstractList<E> parent,

                int offset, int fromIndex, int toIndex) {

            this.parent = parent;

            this.parentOffset = fromIndex;

            this.offset = offset + fromIndex;

            this.size = toIndex - fromIndex;

            this.modCount = ArrayList.this.modCount;

        }

实例:

ArrayList<Integer> alist=new ArrayList<Integer>();

        for(int i=0;i<22;i++){

        alist.add(i);

        }

        System.out.println(alist.size());

        List alist2=alist.subList(0, 20);

       // System.out.println(alist2==alist);//false

        alist2.add(20);

        System.out.println(alist.size());

修改生成的alist2,alist的长度也被改变。

官方的解释:

     返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分视图。(如果 fromIndex 和 toIndex 相等,则返回的列表为空)。返回的列表由此列表支持,因此返回列表中的非结构性更改将反映在此列表中,反之亦然。返回的列表支持此列表支持的所有可选列表操作。

     此方法省去了显式范围操作(此操作通常针对数组存在)。通过传递 subList 视图而非整个列表,期望列表的任何操作可用作范围操作。例如,下面的语句从列表中移除了元素的范围:list.subList(from, to).clear();

 注意:这段话原来不太注意,以为subList会生成一个新的对象,但其实不是,只是生成了原集合对象的部分视图,对生成的视图操作依然是对原集合的操作。

8  ArrayList是封装的数组列表,所以存在数组可以转换成列表,看下面的例子:

 List<String> list1 = Arrays.asList("Larry", "Moe", "Curly");

        System.out.println(list1.size());//结果为3

        int [] intarray={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19};

        List list2=Arrays.asList(intarray);

        System.out.println(list2.size());//结果为1

结果有点奇怪,第二个转换后list长度为1.

public static <T> List<T> asList(T… a)

官方解释:返回一个受指定数组支持的固定大小的列表。看到参数为一个可变长度的泛型,所以无法传入基本类型。传入数组,会把数组类型当成类型进行转换,所以长度为1。

9  总结说明:

1)ArrayList内部调用Array 
ArrayList内部封装了一个Object类型的数组,ArrayList内部的很多方法方法,如Index、IndexOf、Contains、Sort等都是在内部数组的基础上直接调用Array的对应方法。 
2)内部的Object类型的影响 
对于引用类型来说,这部分的影响不是很大,但是对于值类型来说,由于ArrayList中只能存放基础类型的包装类,往ArrayList里面添加和修改元素,都会引起装箱和拆箱的操作,频繁的操作可能会影响一部分效率。 
3)数组扩容,尽量初始化时给定固定的容量
     ArrayList本质靠数组实现,虽然我们使用时是可以完成动态扩容的,但扩容过程中依然要进行数组元素的拷贝,构建新数组,当执行Add、AddRange、Insert、InsertRange等添加元素的方法,都会检查内部数组的容量是否不够了,如果容量不够,它就会以当前容量 的两倍来重新构建一个数组,将旧元素Copy到新数组中,然后丢弃旧数组,在这个临界点的扩容操作,应该来说是比较影响效率的。所以初始化ArrayList建立新list时最好能给予确定的容量界限,减少多次扩容带来的损耗。

4)尽可能少的调用IndexOf、Contains等方法
ArrayList是动态数组,但不能实现快速访问,所以类似IndexOf、Contains等方法是执行的简单的循环来查找元素,所以频繁的调用此类方法对性能有较大影响,如果需要随机查找,建议使用Hashtable或SortedList等键值对的集合。  

public boolean add(E e) {

        ensureCapacityInternal(size + 1);  //调用ensureCapacityInternal

        elementData[size++] = e;

        return true;

}   

private void ensureCapacityInternal(int minCapacity) {

        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

        }

        ensureExplicitCapacity(minCapacity);

    }

 

    private void ensureExplicitCapacity(int minCapacity) {

        modCount++;

        // overflow-conscious code

        if (minCapacity - elementData.length > 0)

            grow(minCapacity);//如果剩余容量不足,最终会调用grow函数

    }

 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);//特殊情况,元素超出MAX_ARRAY_SIZE

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

    }

 

    原文作者:java集合源码分析
    原文地址: https://blog.csdn.net/u010126792/article/details/62884069
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞