探索ArrayList自动改变size真相
ArrayList的列表对象实质上是存储在一个引用型数组里的,有人认为该数组有“自动增长机制”可以自动改变size大小。正式地说,该数组是无法改变
大小的,实际上它只是改变了该引用型数组的指向而已。下面,让我们来看看java是怎样实现ArrayList类的。
一、ArrayList类的实质
ArrayList底层采用Object类型的数组实现,当使用不带参数的构造方法生成ArrayList对象时,
实际上会在底层生成一个长度为
10
的Object类型数组。
首先,ArrayList定义了一个私有的未被序列化的数组elementData,用来存储ArrayList的对象列表(注意只定义未初始):
private
transient
Object[] elementData;
其次,以指定初始容量(Capacity)或把指定的Collection转换为引用型数组后实例化elementData数组;如果没有指定,则预置初始容量为
10
进行
实例化。把私有数组预先实例化,然后通过copyOf方法覆盖原数组,是实现自动改变ArrayList的大小(size)的关键。有人说ArrayList是复杂的数组,我
认为不如说ArrayList是关于数组的系统的方法组合。
ArrayList的构造方法源码如下:
// 用指定的初始容量构造一个空列表。
public
ArrayList(
int
initialCapacity) {
super
();
if
(initialCapacity <
0
)
throw
new
IllegalArgumentException(
"Illegal Capacity: "
+initialCapacity);
this
.elementData =
new
Object[initialCapacity];
//属性指向新建长度为初始容量的临时数组
}
// 使用初始容量10构造一个空列表
public
ArrayList() {
this
(
10
);
}
/ *构造包含利用collection的迭代器按顺序返回的指定collection元素的列表
*
@param
c 集合,它的元素被用来放入列表t
*
@throws
NullPointerException 如果指定集合为
null
*/
public
ArrayList(Collection<?
extends
E> c) {
elementData = c.toArray();
//用Collection初始化数组elementData
size = elementData.length;
if
(elementData.getClass() != Object[].
class
)
elementData = Arrays.copyOf(elementData, size, Object[].
class
);
}
二、ArrayList实现自动改变size机制
为了实现这一机制,java引进了Capacity和size概念,以区别数组的length。为了保证用户增加新的列表对象,java设置了最小容量(minCapacity)
,通常情况上,它大于列表对象的数目,所以Capactiy虽然就是底层数组的长度(length),但是对于最终用户来讲,它是无意义的。而size存储着列表
对象的数量,才是最终用户所需要的。为了防止用户错误修改,这一属性被设置为privae的,不过可以通过size()获取。
下面,对ArrayList的初始以及其列表对象的增加和删除等三种情况下的size自动改变机制进行分析。
1
、初始Capacity和size值。
从上面给出的ArrayList构造方法源码中,我们不难看出Capacity初始值(initialCapacity)可以由用户直接指定或由用户指定的Collection集合存
储的对象数目确定,如果没有指定,系统默认为
10
。而size的被声明为
int
型变量,默认为
0
,当用户指定Collection创建ArrayList时,size值等于
initialCapacity。
2
、add()方法
该方法的源码如下:
public
boolean
add(E e) {
ensureCapacityInternal(size +
1
);
elementData[size++] = e;
//添加对象时,自增size
return
true
;
}
方法中调用的ensureCapacityInternal主要用来调整容量,修改elementData数组的指向。其中涉及到
3
个方法的调用,其核心在于grow方法:
private
void
ensureCapacityInternal(
int
minCapacity) {
modCount++;
//定义于ArrayList的父类AbstractList,用于存储结构修改次数
// overflow-conscious code
if
(minCapacity - elementData.length >
0
)
grow(minCapacity);
}
private
void
grow(
int
minCapacity) {
// overflow-conscious code
int
oldCapacity = elementData.length;
int
newCapacity = oldCapacity + (oldCapacity >>
1
);
//新容量扩大到原容量的1.5倍,右移一位相关于原数值除以2。
if
(newCapacity - minCapacity <
0
)
newCapacity = minCapacity;
if
(newCapacity - MAX_ARRAY_SIZE >
0
)
newCapacity = hugeCapacity(minCapacity);
// 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;
//MAX_ARRAY_SIZE和Integer.MAX_VALUE为常量,详细请参阅下面的注解
}
通过以上代码,我们可知java自动增加ArrayList大小的思路是:向ArrayList添加对象时,原对象数目加
1
如果大于原底层数组长度,则以适当长度新
建一个原数组的拷贝,并修改原数组,指向这个新建数组。原数组自动抛弃(java垃圾回收机制会自动回收)。size则在向数组添加对象,自增
1
。
注解:
//定义于该类的常量,用来分配数组的size最大值。一些 vms在数组里保留字头,试图分配更大数组时可能导致OutOfMemoryError:被请求数组的
size超出vm界限。
private
static
final
int
MAX_ARRAY_SIZE = Integer.MAX_VALUE -
8
;
//在java.lang.Integer类中常量MIN_VALUE、MAX_VALUE如下:
public
static
final
int
MIN_VALUE =
0x80000000
;
//整型取值区间下界:-2147483648
public
static
final
int
MAX_VALUE =
0x7fffffff
;
//整型取值区间上界:2147483647
//在java.util.AbstractList中modCount定义如下:
protected
transient
int
modCount =
0
;
3
、remove()方法
该重构方法其一源码如下(其它的就不累述了):
public
E remove(
int
index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int
numMoved = size - index -
1
;
if
(numMoved >
0
)
System.arraycopy(elementData, index+
1
, elementData, index,
numMoved);
//将后面的列表对象前移
elementData[--size] =
null
;
// 数组前移一位,size自减,空出来的位置置null,具体的对象的销毁由Junk收集器负责
return
oldValue;
}
private
void
rangeCheck(
int
index) {
//边界检查
if
(index <
0
|| index >=
this
.size)
throw
new
IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(
int
index) {
//获取指定index所在位置的对象
return
(E) elementData[index];
}
通过remove()源码的学习,我们不难看出,其改变ArrayList大小的核心与add()方法相似,都是同数组拷贝。
另外,如果确有必要,用户也可以指定ArrayList实例的容量,可以有效的降低时间成本。它是通过调用ensureCapacityInternal来实现的,源代码
如下:
public
void
ensureCapacity(
int
minCapacity) {
if
(minCapacity >
0
)
ensureCapacityInternal(minCapacity);
}
因为size为
private
的,java给出方法来访问它:
public
int
size() {
checkForComodification();
return
this
.size;
}
综上所述,在用户向ArrayList追加对象时,Java总是要先计算容量(Capacity)是否适当,若容量不足则把原数组拷贝到以指定容量为长度创建的
新数组内,并对原数组变量重新赋值,指向新数组。在这同时,size进行自增
1
。在删除对象时,先使用拷贝方法把指定index后面的对象前移
1
位(如果
有的话),然后把空出来的位置置
null
,交给Junk收集器销毁,size自减
1
,即完成了。
ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。
ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。 每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。
对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析ArrayList的源代码: ArrayList定义只定义类两个私有属性:
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer.
*/
private transient Object[] elementData;
/**
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;
很容易理解,elementData存储ArrayList内的元素,size表示它包含的元素的数量。 有个关键字需要解释:transient。 Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。 有点抽象,看个例子应该能明白。 public class UserInfo implements Serializable {
private static final long serialVersionUID = 996890129747019948L;
private String name;
private transient String psw;
public UserInfo(String name, String psw) {
this.name = name;
this.psw = psw;
}
public String toString() {
return "name=" + name + ", psw=" + psw;
}
}
public class TestTransient {
public static void main(String[] args) {
UserInfo userInfo = new UserInfo("张三", "123456");
System.out.println(userInfo);
try {
// 序列化,被设置为transient的属性没有被序列化
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
"UserInfo.out"));
o.writeObject(userInfo);
o.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
try {
// 重新读取内容
ObjectInputStream in = new ObjectInputStream(new FileInputStream(
"UserInfo.out"));
UserInfo readUserInfo = (UserInfo) in.readObject();
//读取后psw的内容为null
System.out.println(readUserInfo.toString());
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
被标记为transient的属性在对象被序列化的时候不会被保存。 接着回到ArrayList的分析中...... 2) 构造方法:
// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 新建一个数组
this.elementData = new Object[initialCapacity];
}
// ArrayList无参构造函数。默认容量是10。
public ArrayList() {
this(10);
}
// 创建一个包含collection的ArrayList
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
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)这些添加元素的方法。下面我们一一讲解:
20 // 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
21 public E set(int index, E element) {
22 RangeCheck(index);
23
24 E oldValue = (E) elementData[index];
25 elementData[index] = element;
26 return oldValue;
27 }
28 // 将指定的元素添加到此列表的尾部。
29 public boolean add(E e) {
30 ensureCapacity(size + 1);
31 elementData[size++] = e;
32 return true;
33 }
34 // 将指定的元素插入此列表中的指定位置。
35 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。
36 public void add(int index, E element) {
37 if (index > size || index < 0)
38 throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
39 // 如果数组长度不足,将进行扩容。
40 ensureCapacity(size+1); // Increments modCount!!
41 // 将 elementData中从Index位置开始、长度为size-index的元素,
42 // 拷贝到从下标为index+1位置开始的新的elementData数组中。
43 // 即将当前位于该位置的元素以及所有后续元素右移一个位置。
44 System.arraycopy(elementData, index, elementData, index + 1, size - index);
45 elementData[index] = element;
46 size++;
47 }
48 // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。
49 public boolean addAll(Collection<? extends E> c) {
50 Object[] a = c.toArray();
51 int numNew = a.length;
52 ensureCapacity(size + numNew); // Increments modCount
53 System.arraycopy(a, 0, elementData, size, numNew);
54 size += numNew;
55 return numNew != 0;
56 }
57 // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。
58 public boolean addAll(int index, Collection<? extends E> c) {
59 if (index > size || index < 0)
60 throw new IndexOutOfBoundsException(
61 "Index: " + index + ", Size: " + size);
62
63 Object[] a = c.toArray();
64 int numNew = a.length;
65 ensureCapacity(size + numNew); // Increments modCount
66
67 int numMoved = size - index;
68 if (numMoved > 0)
69 System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
70
71 System.arraycopy(a, 0, elementData, index, numNew);
72 size += numNew;
73 return numNew != 0;
}
书上都说ArrayList是基于数组实现的,属性中也看到了数组,具体是怎么实现的呢?比如就这个添加元素的方法,如果数组大,则在将某个位置的值设置为指定元素即可,如果数组容量不够了呢? 看到add(E e)中先调用了ensureCapacity(size+1)方法,之后将元素的索引赋给elementData[size],而后size自增。例如初次添加时,size为0,add将elementData[0]赋值为e,然后size设置为1(类似执行以下两条语句elementData[0]=e;size=1)。将元素的索引赋给elementData[size]不是会出现数组越界的情况吗?这里关键就在ensureCapacity(size+1)中了。
// 返回此列表中指定位置上的元素。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
ArrayList提供了根据下标或者指定对象两种方式的删除功能。如下: romove(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),返回被移除的元素。 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 }
首先通过代码可以看到,当移除成功后返回true,否则返回false。remove(Object o)中通过遍历element寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了index,不通过remove(index)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了index不会超过边界,而且fastRemove并不返回被移除的元素。下面是fastRemove的代码,基本和remove(index)一致。 1 private void fastRemove(int index) {
2 modCount++;
3 int numMoved = size - index - 1;
4 if (numMoved > 0)
5 System.arraycopy(elementData, index+1, elementData, index,
6 numMoved);
7 elementData[--size] = null; // Let gc do its work
8 }
removeRange(int fromIndex,int toIndex) 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 }
执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。 这个方法是protected,及受保护的方法,为什么这个方法被定义为protected呢? 这是一个解释,但是可能不容易看明白。http://stackoverflow.com/questions/2289183/why-is-javas-abstractlists-removerange-method-protected 先看下面这个例子
ArrayList<Integer> ints = new ArrayList<Integer>(Arrays.asList(0, 1, 2,
3, 4, 5, 6));
// fromIndex low endpoint (inclusive) of the subList
// toIndex high endpoint (exclusive) of the subList
ints.subList(2, 4).clear();
System.out.println(ints);
输出结果是[0, 1, 4, 5, 6],结果是不是像调用了removeRange(int fromIndex,int toIndex)!哈哈哈,就是这样的。但是为什么效果相同呢?是不是调用了removeRange(int fromIndex,int toIndex)呢?
从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1; //增加50%+1
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。 Object oldData[] = elementData;//为什么要用到oldData[]
关于ArrayList和Vector区别如下:
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:
127 public void trimToSize() {
128 modCount++;
129 int oldCapacity = elementData.length;
130 if (size < oldCapacity) {
131 elementData = Arrays.copyOf(elementData, size);
132 }
}
由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间。
4、注意ArrayList的两个转化为静态数组的toArray方法。 第一个, 调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回。
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
第二个,如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。
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;
}
Fail-Fast机制:
关于ArrayList的源码,给出几点比较重要的总结: 1、注意其三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10,带有Collection参数的构造方法,将Collection转化为数组赋给ArrayList的实现数组elementData。 2、注意扩充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组(详见下面的第3点)。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。 3、ArrayList的实现中大量地调用了Arrays.copyof()和System.arraycopy()方法。我们有必要对这两个方法的实现做下深入的了解。 首先来看Arrays.copyof()方法。它有很多个重载的方法,但实现思路都是一样的,我们来看泛型版本的源码: public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
很明显调用了另一个copyof方法,该方法有三个参数,最后一个参数指明要转换的数据的类型,其源码如下: public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
这里可以很明显地看出,该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。 下面来看System.arraycopy()方法。该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。
4、ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。 5、在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。 |
探索ArrayList自动改变size真相、使用总结
最新推荐文章于 2022-07-14 16:12:01 发布