jdk源码分析(四)——ArrayList


ArrayList可以说是我们在java中最经常使用的数据结构之一了,它为我们提供了方便灵活的可变长度列表实现,并可以高效的存取。因此,作为一个如此亲密的朋友,我们自然是要走近它,一探究竟了。

一.类定义

ArrayList位于java.util包,其定义如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

可以看到ArrayList继承自AbstractList类,并实现了List接口、RandomAccess接口、Cloneable接口、Serializable接口。继承关系如下图所示:

位于继承关系最顶端的是Iterable接口,该接口只有一个方法:

// 获取一个可以应用于一组元素的迭代器
Iterator<T> iterator();

通过迭代器来遍历元素是很多容器类的共有特性,其他的还有Set系列的类。

Collection接口声明了很多容器操作方法,例如addcontainsremove等。

Collection的概念还是比较宽泛的,像Set家族的很多类、Queue家族的很多类都实现了该方法。而List就更具体了,它表示列表,可以按下标进行存取。

除了List接口外,ArrayList还实现了Serializable接口、RandomAccess接口、Cloneable接口,这三个接口本身都没有任何方法,只起到标识作用。Serializable接口使对象能够被序列化和反序列化;RandomAccess声明了ArrayList类的实例可以被随机访问,也就是可以通过下标的方式来访问列表中的元素;Cloneable接口为了实现Object类中的clone方法,从而获得对象的副本,有关clone方法我们在jdk源码分析(一)中已有提到,此处不再赘述。

二.存储结构

既然ArrayList类中的元素可以被随机访问,在java中,能够实现这一点的基础数据结构也只有数组了。我们查看源代码,也可以发现:ArrayList中的所有元素保存在一个对象数组中:

// 存储列表元素
private transient Object[] elementData;
// 记录列表中元素个数
private int size;

三.常用方法

1.构造方法

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);
}
// 复制另一个容器类中的元素到ArrayList
public ArrayList(Collection<? extends E> c) {
    // c.toArray将会产生一个新的数组,因此elementData是一份拷贝
    elementData = c.toArray();
    size = elementData.length;
    // elementData的类型可能不是Object[]
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

这个方法值得我们注意:在对elementData和size赋值后,又检查了elementData的类型,我们看Collection接口的文档,看到toArray方法返回的是Object[]类型,而我们声明的elementData也是Object[]类型的,这不是正好匹配吗?为什么还要判断类型呢?

这是由于java中广泛存在类的继承关系,父类中的方法常常会被子类重写,有时候toString方法返回的未必是Object[]类型,我们看下面的例子:

class A {}

public class MyList<A> extends ArrayList<A> {

    public MyList() {
        super();
    }

    // 重写toArray方法
    @Override
    public String[] toArray() {
        String[] array = new String[super.size()];
        for (int i = 0; i < super.size(); i++) {
            array[i] = super.get(i).toString();
        }
        return array;
    }
}

上面代码中,我们实现了一个特殊的ArrayList类,并重写toArray方法,然后我们执行以下操作:

List<A> list = new MyList<A>();
list.add(new A());
Object[] array = list.toArray();
// 输出false
System.out.println(array.getClass() == Object[].class);
// 输出class [Ljava.lang.String;
System.out.println(array.getClass());
// throw java.lang.ArrayStoreException
// array[0] = new A();
Object[] objectArray = Arrays.copyOf(array, array.length, Object[].class);
// 输出true
System.out.println(objectArray.getClass() == Object[].class);

我们我们调用list.toArray()方法,虽然可以得到Object数组,但其实得到的是String数组,这可以通过打印array.getClass()看出。如果此时,我们将一个A类型的对象放入数组,将会抛出ArrayStoreException异常。执行完Arrays.copyOf的操作后,我们才算将String数组转换为Object数组。

此时,我们明白了,ArrayLis的这个构造方法进行类型检查是完全有必要的,可以确保类型安全。

2.add方法

在完成了列表实例的初始化后,就可以往列表中添加元素了,这就要用到add方法,我们这里只看最简单的add()方法:

public boolean add(E e) {
    // 确保对象数组中有足够的空间可以容纳新的列表元素
    ensureCapacity(size + 1); 
    elementData[size++] = e;
    return true;
}

public void ensureCapacity(int minCapacity) {
    // 记录列表发生结构性变化的次数
    modCount++;
    int oldCapacity = elementData.length;
    // 需要进行扩容
    if (minCapacity > oldCapacity) {
        // oldData复制后并没有被用到,不知道是不是无用代码?
        Object oldData[] = elementData;
        // 按照固定的公式计算扩容后的大小
        int newCapacity = (oldCapacity * 3)/2 + 1;
        // 扩容后依然小于所需要的大小,将新的数组大小设置为所需大小
        if (newCapacity < minCapacity)
            newCapacity = minCapacity;
        // 将旧有元素拷贝到新的数组中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
}

在执行添加操作前,会首先检查数组大小是否足以容纳新的元素,如果不够,就进行扩容,扩容的公式是:新的数组大小=(老的数组大小*3)/2 + 1,例如初始时数组大小为10,第一次扩容后,数组大小就为16,再扩容一次变为25。

值得注意的是,在操作过程中有一个变量似乎与添加元素的操作并无关系:modCount。但其实这个变量也十分重要,它主要用于记录列表发生结构性变化的次数,例如列表的长度发生变化,modCount就会递增。其作用主要体现在多线程当中,可能我们在一个线程中正在读ArrayList中的元素,但另一个线程却插入了一个元素,那么我们就可以及时发现,从而做一些安全处理。modCount字段定义在父类AbstractList中:

protected transient int modCount = 0;
3.get方法

在向列表中添加完元素后,后续可能需要取出,ArrayList支持按照下标来读取列表元素。

public E get(int index) {
    RangeCheck(index);
    return (E) elementData[index];
}

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

get方法比较简单,首先进行一次边界检查,判断所请求的下标是否越界,如果越界,抛出IndexOutOfBoundsException异常,如果不越界,则取出数组元素,转换为相应元素类型,返回。

4.indexOf方法

有时,我们会需要检查列表中是否包括某个元素,那么就可以使用indexOf方法,该方法将返回被检查元素在列表中的下标,如果未找到该元素,则返回-1。

// 查找列表中是否存在元素o
public int indexOf(Object o) {
    // 如果o为null
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else { // 如果o不为null
        for (int i = 0; i < size; i++)
            // 是否equals进行比较
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

代码逻辑清晰明了,不再赘述,但有以下两点值得注意:
(1)保存到列表中的元素可以为null;
(2)在进行元素比较时,使用的是equals方法,也就是内容比较。

5.remove方法

我们可以通过使用remove方法将指定下标的元素删除:

public E remove(int index) {
    // 边界检查
    RangeCheck(index);

    // 删除元素必然要改变列表结构,因此modCount递增
    modCount++;
    // 记录待删除的元素
    E oldValue = (E) elementData[index];

    // 需要被移动的元素
    int numMoved = size - index - 1;
    // 如果有元素需要被移动,则将需要被移动的元素顺次往前移动一个位置
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                numMoved);
    // 移动后,最后一个位置已经没有用了,置为null,等待被垃圾回收
    elementData[--size] = null; // Let gc do its work

    return oldValue;
}

元素的删除操作效果如下:

四.相关类

除了ArrayList外,Java中还有几个容器类拥有线性存储结构:

  • LinkedList:链式存储结构,以链表的形式存储列表元素;
  • Vector:向量,方法与ArrayList非常类似,但列表操作都是同步的,线程安全。

LinkedListArrayList的主要区别如下:
(1)存储结构不同。LinkedList以链表的形式存储元素,ArrayList以数组的形式存储元素;
(2)使用场景不同。LindedList随机访问性能差,但插入和删除效果较高;ArrayList随机访问性能高,但插入和删除性能较差,两者正好互补。

VectorArrayList的主要区别如下:
(1)Vector是线程安全的,ArrayList不是线程安全的;
(2)扩容方式不同。ArrayList按照新的数组大小=(老的数组大小*3)/2 + 1的公式进行扩容;Vector则可以指定每次扩容的增量,若不指定,则每次扩为为原有大小的2倍。

本文已迁移至我的博客:http://ipenge.com/25632.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339

推荐阅读更多精彩内容

  • 一、基本数据类型 注释 单行注释:// 区域注释:/* */ 文档注释:/** */ 数值 对于byte类型而言...
    龙猫小爷阅读 4,251评论 0 16
  • 一.线性表 定义:零个或者多个元素的有限序列。也就是说它得满足以下几个条件:  ①该序列的数据元素是有限的。  ②...
    Geeks_Liu阅读 2,679评论 1 12
  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,480评论 0 3
  • 有一首歌《董小姐》很出名,歌词写得挺好的,当然旋律也很不错。其实,我身边也有一位董小姐,她是我的同事兼好...
    可桐阅读 1,622评论 45 62
  • 逆位倒吊人第一眼感觉:又是逆位,海王星照耀着的倒吊人,给我带来怎样的心情
    塔罗师阅读 4,904评论 0 0