Java数据结构——ArrayList简介

ArrayListJava中最基础的数据结构之一,即顺序表。本篇文章将从源码角度简单介绍ArrayList的基本实现原理。

(本文内容中涉及的源码使用JDK1.6版本,在高版JDK中可能源码做了简单调整,但数据结构的实现机制依然是一样的)


顺序表,顾名思义,是一个有序的数组。数据按顺序在内存中存储,这样的数据结构利于快速的查找,但在数组中间插入或删除数据会导致整个数组发生变动。


ArrayList类中,核心变量有两个:

private transient Object[] elementData;

存储数据的数组,ArrayList存储能力取决于此数组总长度(注:transient修饰词指此变量不会被序列化)


private transient Object[] elementData;

ArrayList的真实长度,即其中存放的数据个数


ArrayList共有3个构造方法:

public ArrayList() {
	this(10);
    }

默认构造方法,10为默认的初始长度


public ArrayList(int initialCapacity) {
	super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
	this.elementData = new Object[initialCapacity];
    }
使用此构造方法,可以为 ArrayList 指定一个初始长度。

当你已知数据长度超过10时候,使用此方法可以减少ArrayList扩容次数。

当初始值小于0时候,会抛出异常。


public ArrayList(Collection c) {
	elementData = c.toArray();
	size = elementData.length;
	// c.toArray might (incorrectly) not return Object[] (see 6260652)
	if (elementData.getClass() != Object[].class)
	    elementData = Arrays.copyOf(elementData, size, Object[].class);
    }

当你已经有一个ArrayList时候,使用此方法形成一个新的ArrayList。注释的意思是,此处的toArray方法可能会错误地没有返回一个Object类型数组,因此在下面进行判断,若不是,则转为Object类型数组。



ArrayList中最常用的方法包括:addsetgetremove,这些方法可能有不同的参数而形成了多个重载函数。

add方法:

public boolean add(E e)
public void add(int index, E element)


第一个方法:

public boolean add(E e) {
ensureCapacity(size + 1); 
elementData[size++] = e;
return true;
    }

首先,此方法调用了ensureCapacity方法,它的意思是,如果当数组容量不足的时候,需要扩充容量(后面会介绍)。之后只要简单的将新的数据添加到数组中即可。

注意,此方法一定返回true


第二个方法:

public void add(int index, E element) {
	if (index > size || index < 0)
	    throw new IndexOutOfBoundsException(
		"Index: "+index+", Size: "+size);

	ensureCapacity(size+1); 
	System.arraycopy(elementData, index, elementData, index + 1,
			 size - index);
	elementData[index] = element;
	size++;
    }

此方法中,指定了插入数据的位置。首先对index进行判断,当其值为负或大于当前数组长度时候,会抛出异常。(数组中最后一个数据的位置对应的是size-1)。

index合法时候,判断是否需要扩容,之后将执行arraycopy这个方法,此方法是个native方法(由C语言封装),它的意思是将elementData这个数组中从index开始的数据复制到从index+1开始(相当于从index开始后移一位)。

 

当我执行add(2, 10)的时候,数组会发生如下变动:如图:

原数组

23

24

25

26

 1

 2

13

 

 

位移后

23

24

25

25

26

 1 

 2

13

    

完成

23

24

10

25

26

 1

 2

13

 

由此可见,数组发生了较大的变动,index后面的数据全部平移了1位。因此如果数据大量插入,会耗费一定时间。


set方法:

public E set(int index, E element)

 
  
 
  

set只有一个方法,即修改index的值为element

它的实现:

public E set(int index, E element) {
RangeCheck(index);


E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
    }

RangeCheck方法即监测index是否越界(后面会介绍)。之后只要将对应位置数据修改了即可。此方法会返回oldValue,即原来index位置的值


get方法:

public E get(int index)

get只有一个方法,即获取index位置的值。


public E get(int index) {
	RangeCheck(index);

	return (E) elementData[index];
    }

很简单,只是返回这个值即可。当然,也是要先经过RangeCheck,看index是否越界。



remove方法:

public E remove(int index)
public boolean remove(Object o)

第一个方法:

public E remove(int index) {
	RangeCheck(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; 

	return oldValue;
    }

凡是方法中参数涉及index,都要经过RangeCheck判断是否越界。

此方法中有一个值为modCount,这是父类中的一个值,具体作用下面会介绍。

remove方法核心就是,根据index取得这个原先的值,然后将其删除。删除的本质是通过移动表来实现的,同样使用到了arraycopy这个方法。

 

举例:

当我执行remove(2)的时候,数组会发生如下变动:如图:

原数组

23

24

25

26

 1 

 2

13

     

     

位移后

23

24

26

 1

 2

13

13

 

 

完成

23

24

26

 1

 2

13

置空

 

 

numMoved表示如果我删除的是数组中的最后一个值,直接置空即可,不需要移动数据。

删除同样可能导致数据的移动,因此大量删除操作会耗费一定时间。


第二个方法:

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

另一个remove方法是根据值去删除数据,先遍历查到要删除的值的index,然后调用fastRemove方法删除,可以看到,fastRemove就是remove第一个方法中的一部分,通过移动数组来删除数据,和上面是完全一样的。

 

除了上述的基本方法外,ArrayList还有一些其他常用方法,如:

public boolean addAll(Collection c)
public boolean addAll(int index, Collection c)
public List subList(int fromIndex, int toIndex)

addAll方法主要使用arraycopy实现,arraycopy具体内容上面已有介绍。

subList方法是ArrayList的父类AbstractList中的内部类方法,具体实现并不复杂,因此不再多说,有兴趣可以去看一看。



下面补充一下上面涉及到的一些方法和变量的介绍。

1、ensureCapacity

2、RangeCheck

3、modCount


public void ensureCapacity(int minCapacity) {
	modCount++;
	int oldCapacity = elementData.length;
	if (minCapacity > oldCapacity) {
	    Object oldData[] = elementData;
	    int newCapacity = (oldCapacity * 3)/2 + 1;
    	    if (newCapacity < minCapacity)
		newCapacity = minCapacity;
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
	}
    }

ensureCapacity,从名字上就可以看出,此方法是确认空间是否足够,即是否越界。

首先获取数组长度,之后判断传入的参数minCapacity是否大于数组长度,如果大于的话,将计算新的长度,是old * 3 / 2 + 1;即原来的1.5+1的,如果变化后的值依然较小,即将新长度设置为minCapacity

随后执行copyOf方法生成一个新的组数,此数组长度为newCapacity,并将之前elementData中的值复制到其中,这样来完成扩容。


private void RangeCheck(int index) {
	if (index >= size)
	    throw new IndexOutOfBoundsException(
		"Index: "+index+", Size: "+size);
    }

RangeCheck方法较为简单,只是比较indexsize的值的关系,如果index大于了当前值,那么抛出越界异常。


modCount

这个是一个较为重要的值。它字面意思为modify count,即修改次数。

当数组进行了诸如添加、删除之类的操作,此值会变化。

那么问题是,它到底有什么用?

 

我们知道ArrayList有一种foreach的循环方式:

for (Integer i : list) {

}

如果在迭代过程中,进行对ArrayList的修改操作,如add remove等,那么将报出ConcurrentModificationExceptions

这个异常的意思是同时进行迭代和修改而抛出的异常。它的大致原因是,在迭代时候,修改数据导致ArrayList长度发生了变化,因此在check时候会抛出这个异常。此处暂时不对具体源码进行分析。

 

如果需要在循环中删除某个元素,应当如下写法:

int i = 0;
		while (i < list.size()) {
			if (list.get(i) == 5) {
				list.remove(i);
			} else {
				i++;
			}
		}


你可能感兴趣的:(Java数据结构——ArrayList简介)