Java——基于JDK1.8的ArrayList源码分析

1,前言

很久没有写博客了,很想念大家,18年都快过完了,才开始写第一篇,争取后面每周写点,权当是记录,因为最近在看JDK的Collection,而且ArrayList源码这一块也经常被面试官问道,所以今天也就和大家一起来总结一下

2,源码解读

当我们一般提到ArrayList的话都会脱口而出它的几个特点:有序、可重复、查找速度快,但是插入和删除比较慢,线程不安全,那么现在阿呆哥哥就会有这些疑问:为什么说是有序的?怎么有序?为什么又说插入和删除比较慢?为什么慢?还有线程为什么不安全?所以带着这些问题,我们一一的来源码中来找找答案。

一般对于一个陌生的类,我们想使用它,都会先看它构造方法,再看它的属性和方法,那么我们也按照这种方式来读读ArrayList这个类

2.1构造方法

1

2

ArrayList<String> arrayList = new ArrayList();

ArrayList<String> arrayList1 = new ArrayList(2);

一般来说我们常见使用ArrayList的创建方式是上面的这两种

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

private static final Object[] EMPTY_ELEMENTDATA = {};

transient Object[] elementData;

private int size;

public ArrayList() {

this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

}

public ArrayList(int initialCapacity) {

if (initialCapacity > 0) {

this.elementData = new Object[initialCapacity];

} else if (initialCapacity == 0) {

this.elementData = EMPTY_ELEMENTDATA;

} else {

throw new IllegalArgumentException("Illegal Capacity: "+

initialCapacity);

}

}

上面是我们两个构造方法和我们类中基本的属性,从上面的代码上来看,在创建构造基本上都没有做,且定义了两个默认的空数组,默认容器的大小DEFAULT_CAPACITY为10,还有我们真正存储元素的地方elementData数组,所以这就是为什么说ArrayList存储集合元素的底层时是使用数组来实现,OK,上面的代码除了一个transient 修饰符之外我们同学们可能有点陌生之外,其余的应该都能看的懂,transient 有什么作用还有为什么用它修饰elementData字段,这个需要看完整个源码之后,我再来给大家解释的话比较合适,这里只需要留心一下。

还有一个不常用的构造方法

1

2

3

4

5

6

7

8

9

10

11

public ArrayList(Collection<? extends E> c) {

elementData = c.toArray();

if ((size = elementData.length) != 0) {

// c.toArray might (incorrectly) not return Object[] (see 6260652)

if (elementData.getClass() != Object[].class)

elementData = Arrays.copyOf(elementData, size, Object[].class);

} else {

// replace with empty array.

this.elementData = EMPTY_ELEMENTDATA;

}

}

第2行:利用Collection.toArray()方法得到一个对象数组,并赋值给elementData

第3行:size代表集合的大小,当通过别的集合来构造ArrayList的时候,需要赋值size

第5-6行:判断 c.toArray()是否出错返回的结果是否出错,如果出错了就利用Arrays.copyOf 来复制集合c中的元素到elementData数组中

第9行:如果c中元素数量为空,则将EMPTY_ELEMENTDATA空数组赋值给elementData

上面就是所有的构造函数的代码了,这里我们可以看到,当构造函数走完之后,会创建出数组elementData和初始化size,Collection.toArray()则是将Collection中所有元素赋值到一个数组,Arrays.copyOf()则是根据Class类型来决定是new还是反射来创造对象并放置到新的数组中,源码如下:

1

2

3

4

5

6

7

8

9

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {

@SuppressWarnings("unchecked")

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;

}

这里面System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 这个方法在我们的后面会的代码中会出现,就先讲了,定义是:将数组src从下标为srcPos开始拷贝,一直拷贝length个元素到dest数组中,在dest数组中从destPos开始加入先的srcPos数组元素。相当于将src集合中的[srcPos,srcPos+length]这些元素添加到集合dest中去,起始位置为destPos

2.2 增加元素方法

一般经常使用的是下面三种方法

1

2

3

arrayList.add( E element);

arrayList.add(int index, E element);

arrayList.addAll(Collection<? extends E> c);

让我们一个个来看看

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

public boolean add(E e) {

ensureCapacityInternal(size + 1); // Increments modCount!!

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);

}

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);

// 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;

}

Integer. MAX_VALUE = 0x7fffffff;

MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8

第2行:调用ensureCapacityInternal()函数

第8-9行:判断当前是否是使用默认的构造函数初始化,如果是设置最小的容量为默认容量10,即默认的elementData的大小为10(这里是有一个容器的概念,当前容器的大小一般是大于当前ArrayList的元素个数大小的)

第16行:modCount字段是用来记录修改过扩容的次数,调用ensureExplicitCapacity()方法意味着确定修改容器的大小,即确认扩容

第26-30、35-44行:一般默认是扩容1.5倍,当时当发现还是不能满足的话,则使用size+1之后的元素个数,如果发现扩容之后的值大于我们规定的最大值,则判断size+1的值是否大于MAX_ARRAY_SIZE的值,大于则取值MAX_VALUE,反之则MAX_ARRAY_SIZE,也就数说容器最大的数量为MAX_VALUE

第32行:就是拷贝之前的数据,扩大数组,且构建出一个新的数组

第3行:这时候数组扩容完毕,就是要将需要添加的元素加入到数组中了

1

2

3

4

5

6

7

8

9

10

public void add(int index, E element) {

if (index > size || index < 0)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

ensureCapacityInternal(size + 1); // Increments modCount!!

System.arraycopy(elementData, index, elementData, index + 1,

size - index);

elementData[index] = element;

size++;

}

第2-3行:判断插入的下标是否越界

第5行:和上面的一样,判断是否扩容

第6行:System.arraycopy这个方法的api在上面已经讲过了,这里的话则是将数组elementData从index开始的数据向后移动一位

第8-9行:则是赋值index位置的数据,数组大小加一

1

2

3

4

5

6

7

8

public boolean addAll(Collection<? extends E> c) {

Object[] a = c.toArray();

int numNew = a.length;

ensureCapacityInternal(size + numNew); // Increments modCount

System.arraycopy(a, 0, elementData, size, numNew);

size += numNew;

return numNew != 0;

}

第2行:将集合转成数组,这时候源码没有对c空很奇怪,如果传入的Collection为空就直接空指针了

第3-7行:获取数组a的长度,进行扩容判断,再将新传入的数组复制到elementData数组中去

所以对增加数据的话主要调用add、addAll方法,判断是否下标越界,是否需要扩容,扩容的原理是每次扩容1.5倍,如果不够的话就是用size+1为容器值,容器扩充后modCount的值对应修改一次

2.3 删除元素方法

常用删除方法有以下三种,我们一个个来看看

1

2

3

arrayList.remove(Object o);

arrayList.remove(int index)

arrayList.removeAll(Collection<?> c)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public boolean remove(Object o) {

if (o == null) {

for (int index = 0; index < size; index++)

if (elementData[index] == null) {

fastRemove(index);

return true;

}

} else {

for (int index = 0; index < size; index++)

if (o.equals(elementData[index])) {

fastRemove(index);

return true;

}

}

return false;

}

private void fastRemove(int index) {

modCount++;

int numMoved = size - index - 1;

if (numMoved > 0)

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[--size] = null; // clear to let GC do its work

}

从上面源码可以看出,如果要移除的元素为null和不为空,都是通过for循环找到要被移除元素的第一个下标,所以这里我们就会思考,当我们的集合中有多个null的话,是不是调用remove(null)这个方法只会移除第一个出现的null元素呢?这个需要同学们下去验证一下。然后通过System.arraycopy函数,来重新组合elementData中的值,且elementData[size]置空原尾部数据 不再强引用, 可以GC掉。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public E remove(int index) {

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

modCount++;

E oldValue = (E) elementData[index];

int numMoved = size - index - 1;

if (numMoved > 0)

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[--size] = null; // clear to let GC do its work

return oldValue;

}

可以看到remove(int index)更简单了,都不需要通过for循环将要删除的元素下边确认下来,整体的逻辑和上面通过元素删除的没什么区别,再来看看批量删除

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

public boolean removeAll(Collection<?> c) {

Objects.requireNonNull(c);

return batchRemove(c, false);

}

public static <T> T requireNonNull(T obj) {

if (obj == null)

throw new NullPointerException();

return obj;

}

private boolean batchRemove(Collection<?> c, boolean complement) {

final Object[] elementData = this.elementData;

int r = 0, w = 0;

boolean modified = false;

try {

for (; r < size; r++)

if (c.contains(elementData[r]) == complement)

elementData[w++] = elementData[r];

} finally {

// Preserve behavioral compatibility with AbstractCollection,

// even if c.contains() throws.

if (r != size) {

System.arraycopy(elementData, r,

elementData, w,

size - r);

w += size - r;

}

if (w != size) {

// clear to let GC do its work

for (int i = w; i < size; i++)

elementData[i] = null;

modCount += size - w;

size = w;

modified = true;

}

}

return modified;

}

第2、6-10行:对传入集合c进行判空处理

第13-15行:定义局部变量elementData、r、w、modified elementData用来重新指向成员变量elementData,用来存储最终过滤后的元素,w用来纪录过滤之后集合中元素的个数,modified用来返回这次是否有修改集合中的元素

第17-19行:for循环遍历原有的elementData数组,发现如果不是要移除的元素,则重新存储在elementData,且w自增

第23-28行:如果出现异常,则会导致 r !=size , 则将出现异常处后面的数据全部复制覆盖到数组里。

第29-36行:判断如果w!=size,则表明原先elementData数组中有元素被移除了,然后将数组尾端size-w个元素置空,等待gc回收。再修改modCount的值,在修改当前数组大小size的值

2.3 修改元素方法

1

arrayList.set(int index, E element)

常见的方法也就是上面这一种,我们来看看它的实现的源码

1

2

3

4

5

6

7

8

public E set(int index, E element) {

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

E oldValue = (E) elementData[index];

elementData[index] = element;

return oldValue;

}

源码很简单,首先去判断是否越界,如果没有越界则将index下表的元素重新赋值element新值,将老值oldValue返回回去

2.4 查询元素方法

1

arrayList.get(int index);

让我们看看源码

1

2

3

4

5

6

public E get(int index) {

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

return (E) elementData[index];

}

源码也炒鸡简单,首先去判断是否越界,如果没有越界则将index下的元素从elementData数组中取出返回

 2.5 清空元素方法

1

arrayList.clear();

常见清空也就这一个方法

1

2

3

4

5

6

7

8

9

public void clear() {

modCount++;

// clear to let GC do its work

for (int i = 0; i < size; i++)

elementData[i] = null;

size = 0;

}

源码也很简单,for循环重置每一个elementData数组为空,修改size的值,修改modCount值

 2.6 判断是否存在某个元素

1

2

arrayList.contains(Object o);

arrayList.lastIndexOf(Object o);

常见的一般是contains方法,不过我这里像把lastIndexOf方法一起讲了,源码都差不多

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public boolean contains(Object o) {

return indexOf(o) >= 0;

}

public int indexOf(Object o) {

if (o == null) {

for (int i = 0; i < size; i++)

if (elementData[i]==null)

return i;

} else {

for (int i = 0; i < size; i++)

if (o.equals(elementData[i]))

return i;

}

return -1;

}

public int lastIndexOf(Object o) {

if (o == null) {

for (int i = size-1; i >= 0; i--)

if (elementData[i]==null)

return i;

} else {

for (int i = size-1; i >= 0; i--)

if (o.equals(elementData[i]))

return i;

}

return -1;

}

通过上面的源码,大家可以看到,不管是contains方法还是lastIndexOf方法,其实就是进行for循环,如果找到该元素则记录下当前元素下标,如果没找到则返回-1,很简单

2.7 遍历ArrayList中的对象(迭代器)

1

2

3

4

Iterator<String> it = arrayList.iterator();

while (it.hasNext()) {

System.out.println(it.next());

}

我们遍历集合中的元素方法挺多的,这里我们就不讲for循环遍历,我们来看看专属于集合的iterator遍历方法吧

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

public Iterator<E> iterator() {

return new Itr();

}

private class Itr implements Iterator<E> {

// Android-changed: Add "limit" field to detect end of iteration.

// The "limit" of this iterator. This is the size of the list at the time the

// iterator was created. Adding & removing elements will invalidate the iteration

// anyway (and cause next() to throw) so saving this value will guarantee that the

// value of hasNext() remains stable and won't flap between true and false when elements

// are added and removed from the list.

protected int limit = ArrayList.this.size;

int cursor; // index of next element to return

int lastRet = -1; // index of last element returned; -1 if no such

int expectedModCount = modCount;

public boolean hasNext() {

return cursor < limit;

}

@SuppressWarnings("unchecked")

public E next() {

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

int i = cursor;

if (i >= limit)

throw new NoSuchElementException();

Object[] elementData = ArrayList.this.elementData;

if (i >= elementData.length)

throw new ConcurrentModificationException();

cursor = i + 1;

return (E) elementData[lastRet = i];

}

public void remove() {

if (lastRet < 0)

throw new IllegalStateException();

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

try {

ArrayList.this.remove(lastRet);

cursor = lastRet;

lastRet = -1;

expectedModCount = modCount;

limit--;

} catch (IndexOutOfBoundsException ex) {

throw new ConcurrentModificationException();

}

}

第1-3行:在获取集合的迭代器的时候,去new了一个Itr对象,而Itr实现了Iterator接口,我们主要重点关注Iterator接口的hasNext、next方法

第12-16行:定义变量,limit:用来记录当前集合的大小值;cursor:游标,默认为0,用来记录下一个元素的下标;lastRet:上一次返回元素的下标

第18-20行:判断当前游标cursor的值是否超过当前集合大小zise,如果没有则说明后面还有元素

第24-31行:在这里面做了不少线程安全的判断,在这里如果我们异步的操作了集合就会触发这些异常,然后获取到集合中存储元素的elemenData数组

第32-33行:游标cursor+1,然后返回元素 ,并设置这次次返回的元素的下标赋值给lastRet

3,看源码之前问题的反思

ok,上面的话基本上把我们ArrayList常用的方法的源码给看完了。这时候,我们需要来对之前的问题来一一进行总结了

①有序、可重复是什么概念?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public static void main(String[] args){

ArrayList arrayList = new ArrayList();

arrayList.add("1");

arrayList.add("1");

arrayList.add("2");

arrayList.add("3");

arrayList.add("1");

Iterator<String> it = arrayList.iterator();

while (it.hasNext()) {

System.out.println(it.next());

}

}

输出结果

1

1

2

3

1

可重复是指加入的元素可以重复,有序是指的加入元素的顺序和取出来的时候顺序相同,一般这个特点是List相对于Set和Map来比较出来的,后面我们把Set、Map的源码看了之后会更加理解这两个特点

② 为什么说查找查找元素比较快,但添加和删除元素比较慢呢?

我们从上面的源码得到,当增加元素的时候是有可能会触发扩容机制的,而扩容机制会导致数组复制;删除和批量删除会导致找出两个集合的交集,以及数组复制操作;而查询直接调用return (E) elementData[index]; 所以说增、删都相对低效 而查找是很高效的操作。

③ 为什么说ArrayList线程是不安全

从上面的代码我们都知道,现在add()方法为例

1

2

3

4

5

6

public boolean add(E e) {

//确定是否扩容,这里可以忽略

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

这里我们主要看两点,第一点add()方法前面没有synchronized字段、第二点 elementData[size++] = e;这段代码可以拆开为下面两部分代码

1

2

elementData[size] = e;

size++

也就是说整个add()方法可以拆为两步,第一步在elementData[Size] 的位置存放此元素,第二步增大 Size 的值。我们都知道我们的CUP是切换进程运行的,在单线程中这样是没有问题的,但是一般在我们项目中很多情况是在多线程中使用ArrayList的,这时候比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 ,所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。这样就会得到元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这样就造成了我们的线程不安全了。

大家可以写一个线程搞两个线程来试试,看看size是不是有问题,这里就不带大家一起写了。

④ transient 关键字有什么用?

唉,这个就有点意思了,这个是我们之前读源码读出来的遗留问题,那源码现在读完了,是时候来解决这个问题了,我们来看看transient官方给的解释是什么

1

当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。

然后我们看一下ArrayList的源码中是实现了java.io.Serializable序列化了的,也就是transient Object[] elementData; 这行代码的意思是不希望elementData被序列化,那这时候我们就有一个疑问了,为什么elementData不进行序列化?这时候我去网上找了一下答案,觉得这个解释是最合理且易懂的

1

在ArrayList中的elementData这个数组的长度是变长的,java在扩容的时候,有一个扩容因子,也就是说这个数组的长度是大于等于ArrayList的长度的,我们不希望在序列化的时候将其中的空元素也序列化到磁盘中去,所以需要手动的序列化数组对象,所以使用了transient来禁止自动序列化这个数组

这时候我们是懂了为什么不给elementData进行序列化了,那当我们要使用序列化对象的时候,elementData里面的数据是不是不能使用了?这里ArrayList的源码提供了下面方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

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);

// Write out all elements in the proper order.

for (int i=0; i<size; i++) {

s.writeObject(elementData[i]);

}

if (modCount != expectedModCount) {

throw new ConcurrentModificationException();

}

}

/**

* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,

* deserialize it).

*/

private void readObject(java.io.ObjectInputStream s)

throws java.io.IOException, ClassNotFoundException {

elementData = EMPTY_ELEMENTDATA;

// Read in size, and any hidden stuff

s.defaultReadObject();

// Read in capacity

s.readInt(); // ignored

if (size > 0) {

// be like clone(), allocate array based upon size not capacity

ensureCapacityInternal(size);

Object[] a = elementData;

// Read in all elements in the proper order.

for (int i=0; i<size; i++) {

a[i] = s.readObject();

}

}

}

通过writeObject方法将数据非null数据写入到对象流中,再使用readObject读取数据

4,总结

上面我们写了这么一大篇,是时候该来总结总结一下了

①查询高效、但增删低效,增加元素如果导致扩容,则会修改modCount,删出元素一定会修改。 改和查一定不会修改modCount。增加和删除操作会导致元素复制,因此,增删都相对低效。而在我们常见的Android场景中,ArrayList多用于存储列表的数据,列表滑动时需要展示每一个Item(element)的数组,所以查询操作是最高频的,且增加操作只有在列表加载更多时才会用到 ,而且是在列表尾部插入,所以也不需要移动数据的操作。而删操作则更低频。 故选用ArrayList作为保存数据的结构

②线程不安全,这个特点一般会和Vector做比较,Vector的源码,内部也是数组做的,区别在于Vector在API上都加了synchronized所以它是线程安全的,以及Vector扩容时,是翻倍size,而ArrayList是扩容50%。Vector的源码大家可以在后面闲下来的时候看看,这里给大家留一个思考题:既然Vector是安全的,那为什么我们在日常开发Android中基本上没有用到Vector呢?大家可以闲下来的时候来寻找一下这个问题的答案

最后再啰嗦一句,写完全篇后发现 ,感觉好久没写博客手很生了,在写的过程总发现大体框架不对也在一点点的修复,后面争取坚持写下来,加油!!!

Java——基于JDK1.8的ArrayList源码分析

卓尔山,大地的肌肤

相关推荐