Java基础系列--集合之ArrayList

Posted 唯一浩哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础系列--集合之ArrayList相关的知识,希望对你有一定的参考价值。

 原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/8494618.html

一、概述

  ArrayList是Java集合体系中最常使用,也是最简单的集合类,是以数组实现的线性表。

  数组在内存中是以一段连续的内存来进行存放的,同样,ArrayList也是如此,初始化时可以指定初始容量,也可以以默认容量(10)创建底层数组,由于ArrayList属于可变长列表,采用可变数组实现,数组本身是不变的,一旦定义就无法变长,可变数组使用创建新数组拷贝旧数据的方式间接实现可变长,习惯称为扩容。

  ArrayList底层数组的扩容算法依据的是一个扩容算法来计算新的数组长度,扩容的条件是当前底层数组不足以容纳新的元素。

二、继承结构 

  ArrayList的类结构如下所示:

三、底层实现

3.1 初始化

  如前所述,ArrayList底层采用的是数组结构。

1     private transient Object[] elementData;

  elementData就是定义在ArrayList底层的数组,而数组就是一连串连续的内存,其逻辑结构如下:

  上图表示初始容量为10的ArrayList的底层数组,默认的初始容量为10,而列表长度用size来定义:

1     private int size;

  这个表示的是列表中元素的数量,与数组的长度不同,size默认为0,表示列表为空。

  1.7版本:

  ArrayList的初始化由两个基本的构造器实现:

 1     public ArrayList() {
 2         this(10);
 3     }
 4 
 5     public ArrayList(int initialCapacity) {
 6         super();
 7         if (initialCapacity < 0)
 8             throw new IllegalArgumentException("Illegal Capacity: "+
 9                                                initialCapacity);
10         this.elementData = new Object[initialCapacity];
11     }

  第一个构造器用于定义一个默认的ArrayList,这个列表拥有默认的10长度的底层数组,第二个构造器用于自行定制指定初始容量的ArrayList。其中第一个构造器调用第二个构造器来实现的。

  在第二个构造器中,只进行了容量值不为负值的简单校验。

  1.8版本:

  1.8版本中对这部分进行了优化,在1.8中定义了两个空数组:

1     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
2     private static final Object[] EMPTY_ELEMENTDATA = {};
3     private static final int DEFAULT_CAPACITY = 10;

  其中:EMPTY_ELEMENTDATA代表一个简单的0容量的空数组,用于表示一个空列表,DEFAULTCAPACITY_EMPTY_ELEMENTDATA同样表示一个空列表,但是它含有一层区别意味,意指默认容量的空列表(虽然它本身任然是0容量的)。

  由于这些因素,其初始化逻辑也做了优化:

 1     public ArrayList() {
 2         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 3     }
 4 
 5     public ArrayList(int initialCapacity) {
 6         if (initialCapacity > 0) {
 7             this.elementData = new Object[initialCapacity];
 8         } else if (initialCapacity == 0) {
 9             this.elementData = EMPTY_ELEMENTDATA;
10         } else {
11             throw new IllegalArgumentException("Illegal Capacity: "+
12                                                initialCapacity);
13         }
14     }

  第一个构造器构建的是一个默认容量的列表,但实质上只是一个容量为0的空数组(空列表),它的容量指定是在第一个元素被添加进来之前指定的,容量为10(默认容量),有效的节省了内存空间,这个列表在不使用之前,几乎不占任何堆内存。

  第二个构造器,在1.7版本的基础上加了一个initialCapacity==0的判断情况,这种情况将列表定义为一个容量为0 的空数组EMPTY_ELEMENTDATA。为什么不直接使用new创建呢?因为定义为指定的EMPTY_ELEMENTDATA可以极大的节省内存空间,因为EMPTY_ELEMENTDATA是私有静态不变的。

3.2 添加元素

  初始化的ArrayList的底层数组是没有元素的,即数组的各位均为null(在1.8版本中底层数组是0长度的数组)。使用add方法我们可以为列表添加元素,ArrayList中的添加单个元素有两种方式,一种是直接添加,另一种是定位添加。还有添加一组元素的两种方法,一种是定组直接添加,一种是定位定组添加。

3.2.1 直接添加

  所谓直接添加就是将新元素添加到列表末尾,其实现逻辑如下:

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

  上面的逻辑很简单,首先进行列表容量检测(容后详述),然后直接将新元素放置到底层数组中即可。

  图中显示添加新元素到列表中,添加之后size的值会增加,这个size即指向数组最新空位的下标,有代表数组中元素的个数。

3.2.2 定位添加

  所谓定位添加,就是我们将新元素,添加到列表指定下标处。

1     public void add(int index, E element) {
2         rangeCheckForAdd(index);
3 
4         ensureCapacityInternal(size + 1);  // Increments modCount!!
5         System.arraycopy(elementData, index, elementData, index + 1,
6                          size - index);
7         elementData[index] = element;
8         size++;
9     }

  首先进行index参数校验,校验通过后进行列表容量检测(容后详述),然后将指定下标处开始到结尾的所以元素整体后移以为,下标处空出来后填充新添加的元素。这个添加操作涉及到一部分元素的整体移动,较为耗时,具体视实际移动的元素数量而定。

  实例:原始列表中由e1-e5共5个元素,现在执行add(2,e6),表示在下标2处添加元素e6,执行步骤如下:

  注意:add(int,E)方法底层数组元素的后移操作采用的是System.arraycopy()方法实现的,不仅此处,后面还会多次使用这个方法来实现数组元素的拷贝。

3.2.3 定组直接添加

  定组直接添加方法为:addAll(Collection<? extends E>),直接将给定的集合中的元素依次添加到当前列表的后面。

1     public boolean addAll(Collection<? extends E> c) {
2         Object[] a = c.toArray();
3         int numNew = a.length;
4         ensureCapacityInternal(size + numNew);  // Increments modCount
5         System.arraycopy(a, 0, elementData, size, numNew);
6         size += numNew;
7         return numNew != 0;
8     }

  首先进行集合转化,将其转化为数组,获取其长度(元素个数),进行列表容量检测(容后详述),将转化好的数组元素复制到当前列表的底层数组后面,计算size。

  明显类似于直接添加,只是添加的数量不同而已,做个简单的图示:

  只是这里讲给定的集合简化为数组形式,其实在源码中我们也能发现,在第2行对集合进行了数组转化,便于操作。元素的添加还是使用数组拷贝的形式实现。

3.2.4 定位定组添加

   定位定组添加类似于定位添加,同样只是添加的元素个数不同。

 1     public boolean addAll(int index, Collection<? extends E> c) {
 2         rangeCheckForAdd(index);
 3 
 4         Object[] a = c.toArray();
 5         int numNew = a.length;
 6         ensureCapacityInternal(size + numNew);  // Increments modCount
 7 
 8         int numMoved = size - index;
 9         if (numMoved > 0)
10             System.arraycopy(elementData, index, elementData, index + numNew,
11                              numMoved);
12 
13         System.arraycopy(a, 0, elementData, index, numNew);
14         size += numNew;
15         return numNew != 0;
16     }

  首先进行index参数校验,然后将集合转化为数组,获取其中元素个数numNew,再进行列表容量检测,获取需要后移的元素的个数,使用数组复制的方式将这些元素后移,再将转化的数组元素复制迁移到空出的空位处。计算size。

  参照下方实例,原始列表有两个元素:e1、e2,现在给定集合包含3个元素,e3、e4、e5,现在执行add(1,Collection<? extends E>)

  也就是将给定集合的元素嵌入到当前列表中。

  总结:我们可以看到在添加元素之前,我们都需要对列表的容量进行校验,以确定已有的空余容量能否容纳新添加的元素,如果检查发现容量不足,就必须进行扩容,见3.7。

 3.3 获取元素

  获取指定下标处的元素,复杂度O(1),只要获取数组指定下标处的元素就可以。

1     public E get(int index) {
2         rangeCheck(index);
3 
4         return elementData(index);
5     }

  首先进行index参数校验,然后获取返回数组执行下标的元素。

3.4 修改元素

  修改元素需要提供修改下标和新值,直接用新值替换旧值即可。

1     public E set(int index, E element) {
2         rangeCheck(index);
3 
4         E oldValue = elementData(index);
5         elementData[index] = element;
6         return oldValue;
7     }

  首先进行index参数校验,然后保存指定下标的旧值,替换为新值,将旧值返回。

3.5 删除元素

  删除元素的操作挺多的,针对不同的情况:

3.5.1 定位删除

  定位删除,即删除指定下标的元素,需要提供删除的元素的下标。

 1     public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5         E oldValue = elementData(index);
 6 
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)
 9             System.arraycopy(elementData, index+1, elementData, index,
10                              numMoved);
11         elementData[--size] = null; // Let gc do its work  
12 
13         return oldValue;
14     }

  首先进行index参数校验,modCount自增1,保存指定下标处的旧值,然后将指定下标的下一个元素到结尾的元素整体前移一位,后面元素覆盖前面元素,指定下标处的旧值被删除,然后将原来的末尾元素置空,size自减1,最后将旧值返回。

  同样数组元素的移动采用数组复制的方式实现。

  实例:原始列表拥有5个元素,e1-e5,现移除下标2处的元素:remove(2)

 

 3.5.2 定元素删除

  指定要删除的元素的值,在列表中查询该值,删除查询到的第一个。即该方法只会删除符合条件的首个元素(即使列表中存在多个符合条件的元素)。

 1     public boolean remove(Object o) {
 2         if (o == null) {
 3             for (int index = 0; index < size; index++)
 4                 if (elementData[index] == null) {
 5                     fastRemove(index);
 6                     return true;
 7                 }
 8         } else {
 9             for (int index = 0; index < size; index++)
10                 if (o.equals(elementData[index])) {
11                     fastRemove(index);
12                     return true;
13                 }
14         }
15         return false;
16     }
17 
18     private void fastRemove(int index) {
19         modCount++;
20         int numMoved = size - index - 1;
21         if (numMoved > 0)
22             System.arraycopy(elementData, index+1, elementData, index,
23                              numMoved);
24         elementData[--size] = null; // Let gc do its work
25     }

  指定元素进行删除的情况,较为复杂,需要针对元素的情况进行分析,如果指定元素为null,则删除第一个null元素,若指定元素非null,则查询首个符合条件的元素进行删除。

  欲删除元素,必须先找到要删除元素的下标,这个过程由循环实现(第3行和第9行),找到下标之后,调用内部方法fastRemove(int),进行指定下标元素的删除,即定位删除,然后返回true,表示删除成功。

  还有一种情况那就是指定的元素在列表中查询不到,这是直接返回false即可。

  这种删除底层使用的仍然是定位删除。不在画图举例了。

 3.5.3 定组删除

  所谓定组删除,就是删除当前列表中所以与给定集合中元素相同的元素,该操作需要制定一个欲要删除的元素的集合(Collection)。

 1     public boolean removeAll(Collection<?> c) {
 2         return batchRemove(c, false);
 3     }
 4 
 5     private boolean batchRemove(Collection<?> c, boolean complement) {
 6         final Object[] elementData = this.elementData;
 7         int r = 0, w = 0;
 8         boolean modified = false;
 9         try {
10             for (; r < size; r++)
11                 if (c.contains(elementData[r]) == complement)
12                     elementData[w++] = elementData[r];
13         } finally {
14             // Preserve behavioral compatibility with AbstractCollection,
15             // even if c.contains() throws.
16             if (r != size) {
17                 System.arraycopy(elementData, r,
18                                  elementData, w,
19                                  size - r);
20                 w += size - r;
21             }
22             if (w != size) {
23                 for (int i = w; i < size; i++)
24                     elementData[i] = null;
25                 modCount += size - w;
26                 size = w;
27                 modified = true;
28             }
29         }
30         return modified;
31     }

  定组删除的complement传值为false,用于第11行比较,表示如果给定集合中不包含当前列表的当前下标的元素的情况下,执行内部语句块,将这个元素保留下来(亦即若包含该元素则不保留该元素,最后查遗补漏时,会将其消失【finally块逻辑】)

实例:当前列表是包含5个元素e1-e5,给定集合元素包括e2,e3,e5三个元素,则定位删除的图示为:

  从第二步开始循环,每次循环结束,r就会自增1,而w表示的是下一个保留元素的下标或者保留元素的个数。循环在第七步r自增到5,不满足循环条件时结束,最后r=5,w=2,亦即共删除3个元素,modCount需要自增(r-w=5-2=3)次。

  最后finally中执行第二个if块,将多余的元素位置置null,再计算modCount和size。

3.5.4 定组保留删除

  定组保留删除逻辑与定组删除正好相反,需要将给定集合中包含的当前列表的元素保留下来,将不包含的删除。

1     public boolean retainAll(Collection<?> c) {
2         return batchRemove(c, true);
3     }

 

  实例,同上,图示如下:

  执行过程同上。

3.5.5 范围删除

  ArrayList中还有一个范围删除方法:removeRange(int,int),根据给定的两个下标,删除下标范围之内的所有元素。该方法是protected修饰的,那么很明显,这个方法并不是面向ArrayList使用者的,而是面向JDK实现者的,这里只做简单介绍。

 1     protected void removeRange(int fromIndex, int toIndex) {
 2         modCount++;
 3         int numMoved = size - toIndex;
 4         System.arraycopy(elementData, toIndex, elementData, fromIndex,
 5                          numMoved);
 6 
 7         // Let gc do its work
 8         int newSize = size - (toIndex-fromIndex);
 9         while (size != newSize)
10             elementData[--size] = null;
11     }

  很简单,将toIndex到结尾的元素复制到fromIndex,空出的位置全部置空即可。

3.6 查找元素

  查找元素包括3个方法:

    contains(Object)  检查当前列表是否包含某个元素

    indexOf(Object)  检查给定元素在当前列表中首次出现的下标

    lastIndexOf(Object)  检查给定元素在当前列表中最后出现的下标

3.6.1 包含方法

1     public boolean contains(Object o) {
2         return indexOf(o) >= 0;
3     }

  好家伙,自己啥也不干,就靠后面了...

3.6.2 前序查找

  前序查找就是从头开始查找元素,返回首次出现的下标。

 1     public int indexOf(Object o) {
 2         if (o == null) {
 3             for (int i = 0; i < size; i++)
 4                 if (elementData[i]==null)
 5                     return i;
 6         } else {
 7             for (int i = 0; i < size; i++)
 8                 if (o.equals(elementData[i]))
 9                     return i;
10         }
11         return -1;
12     }

  源码显示,需要分两种情况考虑,如果给定元素为null,则查找首个null元素的下标并返回,如果给定元素非null,则查找首次出现的下标并返回,如果列表中不包含该元素,则返回-1。

3.6.3 后序查找

   后序查找就是前序查找的反向查找方式:

 1     public int lastIndexOf(Object o) {
 2         if (o == null) {
 3             for (int i = size-1; i >= 0; i--)
 4                 if (elementData[i]==null)
 5                     return i;
 6         } else {
 7             for (int i = size-1; i >= 0; i--)
 8                 if (o.equals(elementData[i]))
 9                     return i;
10         }
11         return -1;
12     }

  参考前后查找的源码不难发现,模式一致,只是查找的方向不同而已。

3.7 列表扩容

  列表的扩容是底层自动进行的,对列表的使用者是完全透明的,因此其方法都是私有的。扩容的条件与算法并不复杂:

  1.7版本:

 1     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
 2 
 3     private void ensureCapacityInternal(int minCapacity) {
 4         modCount++;
 5         // overflow-conscious code
 6         if (minCapacity - elementData.length > 0)
 7             grow(minCapacity);
 8     }
 9 
10     private void grow(int minCapacity) {
11         // overflow-conscious code
12         int oldCapacity = elementData.length;
13         int newCapacity = oldCapacity + (oldCapacity >> 1);
14         if (newCapacity - minCapacity < 0)
15             newCapacity = minCapacity;
16         if (newCapacity - MAX_ARRAY_SIZE > 0)
17             newCapacity = hugeCapacity(minCapacity);
18         // minCapacity is usually close to size, so this is a win:
19         elementData = Arrays.copyOf(elementData, newCapacity);
20     }
21 
22     private static int hugeCapacity(int minCapacity) {
23         if (minCapacity < 0) // overflow
24             throw new OutOfMemoryError();
25         return (minCapacity > MAX_ARRAY_SIZE) ?
26             Integer.MAX_VALUE :
27             MAX_ARRAY_SIZE;
28     }

  1.8版本:

 1     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
 2     private void ensureCapacityInternal(int minCapacity) {
 3         ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
 4     }
 5 
 6     private static int calculateCapacity(Object[] elementData, int minCapacity) {
 7         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
 8             return Math.max(DEFAULT_CAPACITY, minCapacity);
 9         }
10         return minCapacity;
11     }
12 
13     private void ensureExplicitCapacity(int minCapacity) {
14         modCount++;
15 
16         // overflow-conscious code
17         if (minCapacity - elementData.length > 0)
18             grow(minCapacity);
19     }
20     
21     private void grow(int minCapacity) {
22         // overflow-conscious code
23         int oldCapacity = elementData.length;
24         int newCapacity = oldCapacity + (oldCapacity >> 1);
25         if (newCapacity - minCapacity < 0)
26             newCapacity = minCapacity;
27         if (newCapacity - MAX_ARRAY_SIZE > 0)
28             newCapacity = hugeCapacity(minCapacity);
29         // minCapacity is usually close to size, so this is a win:
30         elementData = Arrays.copyOf(elementData, newCapacity);
31     }
32 
33     private static int hugeCapacity(int minCapacity) {
34         if (minCapacity < 0) // overflow
35             throw new OutOfMemoryError();
36         return (minCapacity > MAX_ARRAY_SIZE) ?
37             Integer.MAX_VALUE :
38             MAX_ARRAY_SIZE;
39     }

  可见1.8版本对容量校验的部分进行了再封装,这一部分主要就是之前3.1中讲述的关于首次添加元素时进行容量指定的内容,重点在calculateCapacity方法中,指定要校验的容量minCapacity和初始容量作比较,如果初始容量大则使用初始容量,否则使用指定容量,这些也只会在首次添加元素时进行,之后就不会存在初始容量大的情况了。

  扩容条件:拿给定的容量(长度值)minCapacity与当前列表的底层数组的容量elementData.length进行比较,如果前者大,则说明容量不足,需要扩容,调用grow(minCapacity)进行扩容。

  扩容算法:扩容时存在三种情况,第一种就是普通的自动扩容,按照oldCapacity + (oldCapacity >> 1)算法进行扩容,上式计算得出的即为新的数组容量,一般针对的是单个元素添加的情况,即直接添加和定位添加的情况,这种情况下,每次只添加一个元素,不会出现第14行成功的可能,但是如果是定组直接添加和定位定组添加的时候,由于添加的集合元素数量未知,一旦给定的minCapacity比计算得出的新容量要大,说明计算得出的容量不足以容纳所有的元素,这是直接使用minCapacity作为新容量进行扩容。即优先使用算法计算的容量进行扩容,一旦计算容量还不足以容纳新元素,则使用给定的容量进行扩容。

  还有一种特殊情况,当本次扩容时,计算得到的容量,或者给定的容量大于MAX_ARRAY_SIZE(=Integer.MAX_VALUE - 8)的情况下,需要调用hugeCapacity(minCapacity)方法进行人为限制容量超限,将容量限制在整形的最大值之内。

  最后进行数组扩容,创建新数组,拷贝数据。

3.8 迭代器

  列表的迭代必不可少,而且这里还会用到一个出现很久的变量modCount,此前我们对它一无所知。

  modCount记录的是列表结构发生变化的次数,所谓结构变化包括:新增元素,删除元素,清空元素,扩容等。

  ArrayList的迭代器有两种,ListIterator和Iterator。二者虽然都是迭代器,但是还是有些不同的。

  Iterator迭代器拥有前序遍历列表的功能,和删除元素的功能,这些删除会实时体现在列表中。

  ListIterator迭代器是Iterator的子类,拥有Iterator的全部功能,并且进行了简单扩展,新增了修改元素和添加新元素的功能,同样会实时提现到列表中。

  这些扩展的功能一般都是依附遍历而存在的,不能脱离遍历使用。比如remove操作,直接进行remove是会报错的,因为迭代器的指针指向的是首个元素之前的位置,这是删除是无法进行删除的,只有执行了next()方法,指针跳过若干元素之后,remove操作会将刚跳过的元素删除。

  add操作的作用是在指针刚刚跳过的元素后面与下一个元素之间插入新元素,修改元素则是针对指针刚刚跳过的元素进行修改。

 

 1 import java.util.ArrayList;
 2 import java.util.ListIterator;
 3 
 4 public class ListTest {
 5     public static void main(String[] args){
 6         ArrayList<String> list = new ArrayList<>();
 7         list.add("1");list.add("2");list.add("3");list.add("4");list.add("5");list.add("6");
 8         System.out.println("初始列表为:"+list);
 9         ListIterator<String> lit = list.listIterator();
10         lit.add("10");
11         System.out.println("迭代器指针移动前直接添加元素列表为:"+list);
12         String s = lit.next();
13         lit.add("100");
14         System.out.println("迭代器指针移动后再次添加元素后:"+list);
15         s = lit.next();
16         lit.set("200");
17         System.out.println("修改元素:"+list);
18         s = lit.next();
19         lit.remove();
20         System.out.println("删除元素为:"+list);
21     }
22 }

 

  执行结果为:

初始列表为:[1, 2, 3, 4, 5, 6]
迭代器指针移动前直接添加元素列表为:[10, 1, 2, 3, 4, 5, 6]
迭代器指针移动后再次添加元素后:[10, 1, 100, 2, 3, 4, 5, 6]
修改元素:[10, 1, 100, 200, 3, 4, 5, 6]
删除元素为:[10, 1, 100, 200, 4, 5, 6]
View Code

3.9 Spliterator迭代器

  Spliterator是1.8新增的迭代器,属于并行迭代器,可以将迭代任务分割交由多个线程来进行。详情以后单立一篇进行讲述。

 

以上是关于Java基础系列--集合之ArrayList的主要内容,如果未能解决你的问题,请参考以下文章

Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)

Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例

Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例

Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例

扒一扒系列之开发中常用的Java集合类(ArrayList篇 jdk 1.7)

Java 集合系列目录(Category)