您的位置:首页 > 理论基础 > 数据结构算法

Java数据结构——ArrayList简介

2015-11-30 13:45 411 查看
ArrayList是Java中最基础的数据结构之一,即顺序表。本篇文章将从源码角度简单介绍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<? extends E> 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中最常用的方法包括:add、set、get、remove,这些方法可能有不同的参数而形成了多个重载函数。

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<? extends E> c)
public boolean addAll(int index, Collection<? extends E> c)
public List<E> 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方法较为简单,只是比较index和size的值的关系,如果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++;
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: