这篇文章给大家带来ArrayList的学习,如果错误希望不吝指出,感谢!平台 jdk1.7,ubuntu 14.02
ArrayList是一个数组队列,容量可以动态变化,比java中的数组使用更加方便。
ArrayList继承&实现结构图(查看ArrayList):
如图所示ArrayList直接继承自AbstractList,间接实现了List接口和继承了AbstractCollection抽象类。
ArrayList继承实现结构源码:
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
public interface Iterable
实现这个接口操作ArrayList时就可以使用增强的foreach对ArrayList进行遍历访问。
public interface Serializable
Serializable 接口内部没有字段或方法,仅起到标记可序列化的功能。
未实现此接口的类将无法使其任何状态序列化或反序列化,可序列化类的所有子类型本身都是可序列化的。
ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;
当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
public interface Cloneable
实现了 Cloneable 接口,可以利用Object.clone() 方法合法地对该类实例进行按字段复制。
如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常。
public interface RandomAccess
RandomAcces接口也是一个标记接口,无具体方法和字段,用来表明其支持快速随机访问。
此接口的主要目的是随机或连续访问列表时能提供良好的性能。 但此实现方法不是线程安全的,如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。
一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法将该列表“包装”起来,此对象的方法就变成了同步操作。
此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引访问元素,并搜索列表中的元素。与 set 不同,列表通常允许重复的元素。更确切地讲,列表通常允许 e1.equals(e2)(e1和e2属于同一个ArrayList) 结果为true,并且如果列表本身允许 null 元素的话,通常它们允许多个 null 元素。
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[] elementData; elementData是一个缓存数组,用于存储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
ArrayList中writeObject方法就是序列化方法,由于在ArrayList中的elementData这个数组的长度是变长的,java在扩容的时候,有一个扩容因子,也就是说这个数组的长度是大于等于ArrayList的长度的,我们不希望在序列化的时候将其中的空元素也序列化到磁盘中去,所以需要手动的序列化数组对象,所以使用了transient来禁止自动序列化这个数组。
调用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 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 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带来效率的提升。
ArrayList支持3种遍历方式(也有说4-5种的,基本的应该就这三种)
(1) 第一种,通过迭代器遍历。即通过Iterator去遍历。
ArrayList list=new ArrayList();
int value;
//add(.......);
Iterator iter = list.iterator();
while (iter.hasNext()) {
value = iter.next();
}
(2) 第二种,随机访问,通过索引值去遍历。
由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
ArrayList list=new ArrayList();
int value;
//add(.......);
int size = list.size();
for (int i=0; i
(03) 第三种ArrayList实现了Inerable接口可以利用增强的for循环。如下:
ArrayList list=new ArrayList();
int value;
//add(.......);
for (int intval:list) {
value = intval;
}
至于三者的效率问题,由于利用数组存储,所以应该是根据下标随机访问更快,但实验结果跟我想的不太一样,希望哪位大神跟我说下怎么设计合理的遍历算法:
代码如下(每次时间不确定,重复3000000次):
long t1,t2;
ArrayList list=new ArrayList();
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 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 list1=new ArrayList();
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[] 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 list=new ArrayList();
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时,最终生成数组长度为传入参数长度。
public List subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, offset, fromIndex, toIndex);//调用到内部类
}
SubList(AbstractList 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 alist=new ArrayList();
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会生成一个新的对象,但其实不是,只是生成了原集合对象的部分视图,对生成的视图操作依然是对原集合的操作。
List 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
官方解释:返回一个受指定数组支持的固定大小的列表。看到参数为一个可变长度的泛型,所以无法传入基本类型。传入数组,会把数组类型当成类型进行转换,所以长度为1。
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;
}