Java集合(1):ArrayList
Java容器类的用途是“保存对象”,分为两类:Map——存储“键值对”组成的对象;Collection——存储独立元素。Collection又可以分为List和Set两大块。List保持元素的顺序(有序可重复),而Set不能有重复的元素(无序唯一)。
我们从List中最常用的ArrayList展开对Java集合容器的介绍。
一.ArrayList介绍
ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
1.ArrayList的继承关系
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}
2.ArrayList与Collection关系如下图
- ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
- ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。
- ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
- ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
- 和Vector不同,ArrayList中的操作不是线程安全的。所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。
二.ArrayList源码解析
1.私有属性
ArrayList定义只定义类两个私有属性:
1 /** 2 * The array buffer into which the elements of the ArrayList are stored. 3 * The capacity of the ArrayList is the length of this array buffer. 4 */ 5 private transient Object[] elementData; 6 7 /** 8 * The size of the ArrayList (the number of elements it contains). 9 * 10 * @serial 11 */ 12 private int size;
elementData存储ArrayList内的元素,size表示它包含的元素的数量。
2.ArrayList构造方法
ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。
1 // ArrayList带容量大小的构造函数。 2 public ArrayList(int initialCapacity) { 3 super(); 4 if (initialCapacity < 0) 5 throw new IllegalArgumentException("Illegal Capacity: "+ 6 initialCapacity); 7 // 新建一个数组 8 this.elementData = new Object[initialCapacity]; 9 } 10 11 // ArrayList无参构造函数。默认容量是10。 12 public ArrayList() { 13 this(10); 14 } 15 16 // 创建一个包含collection的ArrayList 17 public ArrayList(Collection<? extends E> c) { 18 elementData = c.toArray(); 19 size = elementData.length; 20 if (elementData.getClass() != Object[].class) 21 elementData = Arrays.copyOf(elementData, size, Object[].class); 22 }
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)这些添加元素的方法。
1 // 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。 2 public E set(int index, E element) { 3 RangeCheck(index); 4 5 E oldValue = (E) elementData[index]; 6 elementData[index] = element; 7 return oldValue; 8 } 9 // 将指定的元素添加到此列表的尾部。 10 public boolean add(E e) { 11 ensureCapacity(size + 1); 12 elementData[size++] = e; 13 return true; 14 } 15 // 将指定的元素插入此列表中的指定位置。 16 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。 17 public void add(int index, E element) { 18 if (index > size || index < 0) 19 throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size); 20 // 如果数组长度不足,将进行扩容。 21 ensureCapacity(size+1); // Increments modCount!! 22 // 将 elementData中从Index位置开始、长度为size-index的元素, 23 // 拷贝到从下标为index+1位置开始的新的elementData数组中。 24 // 即将当前位于该位置的元素以及所有后续元素右移一个位置。 25 System.arraycopy(elementData, index, elementData, index + 1, size - index); 26 elementData[index] = element; 27 size++; 28 } 29 // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。 30 public boolean addAll(Collection<? extends E> c) { 31 Object[] a = c.toArray(); 32 int numNew = a.length; 33 ensureCapacity(size + numNew); // Increments modCount 34 System.arraycopy(a, 0, elementData, size, numNew); 35 size += numNew; 36 return numNew != 0; 37 } 38 // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。 39 public boolean addAll(int index, Collection<? extends E> c) { 40 if (index > size || index < 0) 41 throw new IndexOutOfBoundsException( 42 "Index: " + index + ", Size: " + size); 43 44 Object[] a = c.toArray(); 45 int numNew = a.length; 46 ensureCapacity(size + numNew); // Increments modCount 47 48 int numMoved = size - index; 49 if (numMoved > 0) 50 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); 51 52 System.arraycopy(a, 0, elementData, index, numNew); 53 size += numNew; 54 return numNew != 0; 55 }
4.元素读取
1 // 返回此列表中指定位置上的元素。 2 public E get(int index) { 3 RangeCheck(index); 4 5 return (E) elementData[index]; 6 }
5.元素删除
ArrayList提供了根据下标或者指定对象两种方式的删除功能。
(1).remove(int index):
1 // 移除此列表中指定位置上的元素。 2 public E remove(int index) { 3 RangeCheck(index); 4 5 modCount++; 6 E oldValue = (E) elementData[index]; 7 8 int numMoved = size - index - 1; 9 if (numMoved > 0) 10 System.arraycopy(elementData, index+1, elementData, index, numMoved); 11 elementData[--size] = null; // Let gc do its work 12 13 return oldValue; 14 }
首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将list末尾元素置空(null),返回被移除的元素。
(2).remove(Object o)
1 // 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。 2 public boolean remove(Object o) { 3 // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。 4 if (o == null) { 5 for (int index = 0; index < size; index++) 6 if (elementData[index] == null) { 7 // 类似remove(int index),移除列表中指定位置上的元素。 8 fastRemove(index); 9 return true; 10 } 11 } else { 12 for (int index = 0; index < size; index++) 13 if (o.equals(elementData[index])) { 14 fastRemove(index); 15 return true; 16 } 17 } 18 return false; 19 } 20 } 21 22 private void fastRemove(int index) { 23 modCount++; 24 int numMoved = size - index - 1; 25 if (numMoved > 0) 26 System.arraycopy(elementData, index+1, elementData, index, 27 numMoved); 28 elementData[--size] = null; // Let gc do its work 29 }
首先通过代码可以看到,当移除成功后返回true,否则返回false。remove(Object o)中通过遍历element寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了index,不通过remove(index)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了index不会超过边界,而且fastRemove并不返回被移除的元素。fastRemove基本和remove(index)一致。
6.调整数组容量
(1).ensureCapacity
从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。
1 public void ensureCapacity(int minCapacity) { 2 modCount++; 3 int oldCapacity = elementData.length; 4 if (minCapacity > oldCapacity) { 5 Object oldData[] = elementData; 6 int newCapacity = (oldCapacity * 3)/2 + 1; //增加50%+1 7 if (newCapacity < minCapacity) 8 newCapacity = minCapacity; 9 // minCapacity is usually close to size, so this is a win: 10 elementData = Arrays.copyOf(elementData, newCapacity); 11 } 12 }
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
关于ArrayList和Vector区别如下:
- ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。
- Vector提供indexOf(obj, start)接口,ArrayList没有。
- Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销。
(2).trimToSize
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。
1 public void trimToSize() { 2 modCount++; 3 int oldCapacity = elementData.length; 4 if (size < oldCapacity) { 5 elementData = Arrays.copyOf(elementData, size); 6 } 7 }
由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间。
7.转为静态数组toArray
注意ArrayList的两个转化为静态数组的toArray方法。
(1). 调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回。
public Object[] toArray() { return Arrays.copyOf(elementData, size); }
(2).如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。
1 public <T> T[] toArray(T[] a) { 2 if (a.length < size) 3 // Make a new array of a's runtime type, but my contents: 4 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); 5 System.arraycopy(elementData, 0, a, 0, size); 6 if (a.length > size) 7 a[size] = null; 8 return a; 9 }
8.Fail-Fast机制
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
9.ArrayList遍历方式
ArrayList支持3种遍历方式
1.第一种,通过迭代器遍历。即通过Iterator去遍历。
1 Integer value = null; 2 Iterator iter = list.iterator(); 3 while (iter.hasNext()) { 4 value = (Integer)iter.next(); 5 }
2.第二种,随机访问,通过索引值去遍历。
由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
1 Integer value = null; 2 int size = list.size(); 3 for (int i=0; i<size; i++) { 4 value = (Integer)list.get(i); 5 }
3.第三种,for循环遍历。
Integer value = null; for (Integer integ:list) { value = integ; }
遍历ArrayList时,使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低!
10.ArrayList示例
介绍 ArrayList 的常用API
1 import java.util.*; 2 /* 3 * @desc ArrayList常用API的测试程序 4 * @author skywang 5 * @email kuiwu-wang@163.com 6 */ 7 public class ArrayListTest { 8 public static void main(String[] args) { 9 10 // 创建ArrayList 11 ArrayList list = new ArrayList(); 12 // 将“” 13 list.add("1"); 14 list.add("2"); 15 list.add("3"); 16 list.add("4"); 17 // 将下面的元素添加到第1个位置 18 list.add(0, "5"); 19 // 获取第1个元素 20 System.out.println("the first element is: "+ list.get(0)); 21 // 删除“3” 22 list.remove("3"); 23 // 获取ArrayList的大小 24 System.out.println("Arraylist size=: "+ list.size()); 25 // 判断list中是否包含"3" 26 System.out.println("ArrayList contains 3 is: "+ list.contains(3)); 27 // 设置第2个元素为10 28 list.set(1, "10"); 29 // 通过Iterator遍历ArrayList 30 for(Iterator iter = list.iterator(); iter.hasNext(); ) { 31 System.out.println("next is: "+ iter.next()); 32 } 33 // 将ArrayList转换为数组 34 String[] arr = (String[])list.toArray(new String[0]); 35 for (String str:arr) 36 System.out.println("str: "+ str); 37 // 清空ArrayList 38 list.clear(); 39 // 判断ArrayList是否为空 40 System.out.println("ArrayList is empty: "+ list.isEmpty()); 41 } 42 }
参考: