Android版数据结构与算法(二):基于数组的实现ArrayList源码彻底分析

版权声明:本文出自汪磊的博客,未经作者允许禁止转载。

本片我们分析基础数组的实现--ArrayList,不会分析整个集合的继承体系,这不是本系列文章重点

源码分析都是基于"安卓版"的源码,和java原生版核心思想都是差不多的。好了,废话依然少说,进入正文。

一、ArrayList中成员变量

源码:

 1     /**
 2      * The minimum amount by which the capacity of an ArrayList will increase.
 3      * This tuning parameter controls a time-space tradeoff. This value (12)
 4      * gives empirically good results and is arguably consistent with the
 5      * RI's specified default initial capacity of 10: instead of 10, we start
 6      * with 0 (sans allocation) and jump to 12.
 7      */
 8     private static final int MIN_CAPACITY_INCREMENT = 12;
 9 
10     /**
11      * The number of elements in this list.
12      */
13     int size;
14 
15     /**
16      * The elements in this list, followed by nulls.
17      */
18     transient Object[] array;

第8行MIN_CAPACITY_INCREMENT的作用通过注释就大体可以知道了,主要就是用来控制内部维护的数组每次扩容时的大小,后面分析具体方法时会多次看到其身影。

第13行size就是内部维护的数组中元素的个数了,没什么多余解释的。

第18行array就是内部盛放数据的数组了,我们加入的数据就放到这里。

二、ArrayList构造方法与初始化

初始化方法有三个,源码如下:

 1 /**
 2      * Constructs a new {@code ArrayList} instance with zero initial capacity.
 3      */
 4     public ArrayList() {
 5         array = EmptyArray.OBJECT;
 6     }
 7 
 8     /**
 9      * Constructs a new instance of {@code ArrayList} with the specified
10      * initial capacity.
11      *
12      * @param capacity
13      *            the initial capacity of this {@code ArrayList}.
14      */
15     public ArrayList(int capacity) {
16         if (capacity < 0) {
17             throw new IllegalArgumentException("capacity < 0: " + capacity);
18         }
19         array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
20     }
21 
22     /**
23      * Constructs a new instance of {@code ArrayList} containing the     elements     of
24      * the specified collection.
25      *
26      * @param collection
27      *            the collection of elements to add.
28      */
29     public ArrayList(Collection<? extends E> collection) {
30         if (collection == null) {
31             throw new NullPointerException("collection == null");
32         }
33 
34         Object[] a = collection.toArray();
35         if (a.getClass() != Object[].class) {
36             Object[] newArray = new Object[a.length];
37             System.arraycopy(a, 0, newArray, 0, a.length);
38             a = newArray;
39         }
40         array = a;
41         size = a.length;
42     }

 4-6行,调用空参数的构造方法就是初始化一个空的array数组。

15-20行,初始化的时候我们可以指定一个大小的数值,如果小于0则会抛出异常,为0就和调用空参数构造方法一样了,大于0,则初始化一个对应大小的array数组。调用此方法我们可以初始化的时候就指定array数组的大小,如果我们事先知道放入元素个数,那么可以调用此方法初始化的时候就指定好内部array数组大小,省去后续不断放入元素扩容的操作。

29-42行,初始化的时候我们同样也可以传入一个集合:

30-32传入集合为null,则报空指针异常。

34行,集合collection转为Object数组。

35行,判断a的字节码类型是否为Object[].class类型,这里注意toArray()方法实际返回的不一定是Object[]类型数组,具体返回由具体的Collcetion子类自己去实现toArray()方法。这里只是用Object[]类型数组接收toArray()返回的数据。

如果返回的字节码类型不是Object[]则进入36-38行逻辑

36行,根据集合大小创建一个同样大小的Object[]类型newArray数组。

37行,就是数组的拷贝了,调用System.arraycopy方法将数组a从0位置开始,拷贝到newArray数组位置同样从0开始,并且拷贝数据长度为a.length。System.arraycopy方法一定要弄明白,后续主要用这个方法对数组进行操作。

经过37行,collection中数据就都拷贝到数组newArray中了。

38行,newArray赋值给数组a。

40-41行,a赋值给array变量,size变量记录数据大小。

以上就是三种创建ArrayList的方式,比较简单。继续往下看

三、ArrayList中add方法源码分析

添加操作主要有四个方法供我们使用,如下:

public boolean add(E object)//添加一个元素

public void add(int index, E object)//向指定位置添加一个元素

public boolean addAll(Collection<? extends E> collection)//添加一个集合

public boolean addAll(int index, Collection<? extends E> collection)//向指定位置添加一个集合

接下来,逐个分析

add(E object)源码:

 1     public boolean add(E object) {
 2         Object[] a = array;
 3         int s = size;
 4         if (s == a.length) {
 5             Object[] newArray = new Object[s +
 6                     (s < (MIN_CAPACITY_INCREMENT / 2) ?
 7                      MIN_CAPACITY_INCREMENT : s >> 1)];
 8             System.arraycopy(a, 0, newArray, 0, s);
 9             array = a = newArray;
10         }
11         a[s] = object;
12         size = s + 1;
13         modCount++;
14         return true;
15     }

这里最核心的就是有个扩容操作,加入元素的时候如果容器已满则进行5-7行扩容逻辑。

扩容方式为:若容量小于6则增加12,否则1.5倍扩容。

关于扩容这里有两种策略,时间换空间和空间换时间,比如我可以一次扩容很大,这样不断加入元素的时候就省去每次扩容所花费的时间了,同样,也可以每次就扩容那么多,放入一个元素我就扩容1,放入10个元素我就扩容10,这样节省了空间但是每次都要扩容从而牺牲了时间。而谷歌工程师在这里考虑了时间空间的平衡,既不扩容很大也照顾扩容次数,个人感觉这里没有一个完美解决方案,个设计者个人喜好也有一定关系。

8行,又是数组的拷贝了,原来数组元素全部拷贝到新数组newArray。

11-12行,元素放入数组以及元素个数加一,没什么要解释的。

13行,记录操作次数。

add(int index, E object)源码:

 1     public void add(int index, E object) {
 2         Object[] a = array;
 3         int s = size;
 4         if (index > s || index < 0) {
 5             throwIndexOutOfBoundsException(index, s);
 6         }
 7 
 8         if (s < a.length) {
 9             System.arraycopy(a, index, a, index + 1, s - index);
10         } else {
11             // assert s == a.length;
12             Object[] newArray = new Object[newCapacity(s)];
13             System.arraycopy(a, 0, newArray, 0, index);
14             System.arraycopy(a, index, newArray, index + 1, s - index);
15             array = a = newArray;
16         }
17         a[index] = object;
18         size = s + 1;
19         modCount++;
20     }

8行,首先判断数组中元素个数是否小于数组长度,其实就是判断数组是否已经填满,没填满,则代表可以直接放入数据,不容扩容。

9行,同样是数组的拷贝,这里拷贝需要自己小小的计算一下了,其实就是将index开始的元素拷贝到index+1开始的位置,把index位置空出来。

如果已经填满数据,则就需要扩容操作了。

12行,创建一个新数组,数组大小为newCapacity(s),自己看一下就可以了,和上面扩容逻辑一样。

13-14行同样是数组的拷贝,将原数组数据拷贝到新数组,同样新数组的index位置会空出来。

17行在index位置上加入新数据。

其余就没什么好解释的了,如果以上两个方法完全明白操作套路,那么其余两个add方法就很容易理解了,这里就不一一分析了。

通过上面分析可以看出添加元素到指定位置需要将其后所有元素都要向后移动一位,所以ArrayList向指定位置添加元素还是比较消耗性能的。

四、ArrayList中remove方法源码分析

删除操作主要有以下两个方法:

remove(int index) //根据索引删除

remove(Object object)//根据元素删除

remove(int index)源码:

 1     public E remove(int index) {
 2         Object[] a = array;
 3         int s = size;
 4         if (index >= s) {
 5             throwIndexOutOfBoundsException(index, s);
 6         }
 7         E result = (E) a[index];
 8         System.arraycopy(a, index + 1, a, index, --s - index);
 9         a[s] = null;  // Prevent memory leak
10         size = s;
11         modCount++;
12         return result;
13     }

最核心就是8-9行代码了

8行,同样也是数组的拷贝操作,从将要删除元素索引index开始将之后元素全部向前移动一位,也就是将index位置元素覆盖掉了。

9行,防止内存泄露,将最后一个元素置null。

其余就没什么好解释得了,关键是第8行一定要理解。

remove(Object object)与remove(int index)主要区别就是有个判断object是否为null操作,其余就基本都差不多了,就不再分析了。

同样,根据上面分析我们可以看出对于删除某一个元素其后所有元素都要向前移动一位,也是比较消耗性能

五、ArrayList中contains方法源码分析

接下来我们看下ArrayList是怎么判断是否包含某一元素的。

contains(Object object)源码如下:

 1     public boolean contains(Object object) {
 2         Object[] a = array;
 3         int s = size;
 4         if (object != null) {
 5             for (int i = 0; i < s; i++) {
 6                 if (object.equals(a[i])) {
 7                     return true;
 8                 }
 9             }
10         } else {
11             for (int i = 0; i < s; i++) {
12                 if (a[i] == null) {
13                     return true;
14                 }
15             }
16         }
17         return false;
18     }

比较简单,无论object是否为null,都需要遍历整个数组比较,所以效率是比较低的。

六、总结

以上就是ArrayList主要方法的分析,整体下来感觉和数组差不多,其优势就是数据的随机访问,对于指定位置插入数据以及删除数据性能都比较"费劲",在初始化完成后如果我们想插入大量数据可以调用ensureCapacity(int minimumCapacity)来提前对ArrayList进行扩容,而不是在不断插入数据的时候自身不断扩容,如果是数据频繁的增加删除LinkedList则是最佳选择。

 好了,本片到此结束,希望对你有用。

声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号

 

posted @ 2018-08-01 09:55 WangLei_ClearHeart 阅读(...) 评论(...) 编辑 收藏