前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >揭秘Java中的瑞士军刀——ArrayList源码解析

揭秘Java中的瑞士军刀——ArrayList源码解析

作者头像
灬沙师弟
发布2023-11-20 16:24:55
1690
发布2023-11-20 16:24:55
举报
文章被收录于专栏:Java面试教程Java面试教程

前言

在编程的世界里,我们常常被一些基础的、看似简单的工具所困扰。

比如在Java中,我们经常使用的ArrayList类。

它为我们提供了一种方便的方式来管理和操作一个动态数组,但是你是否曾经停下来3思考过它是如何工作的呢?它的内部机制是什么?

如果你对这些问题感到好奇,那么让我们一起深入到Java的核心库中去,探索ArrayList的源代码,揭开它的神秘面纱。

小白 ArrayList 简单使用技巧

首先是 ArrayList 的简单使用技巧,主要是介绍 ArrayList 的几个常见方法:

代码语言:javascript
复制
/**
 * 编写一个ArrayList的简单实用demo
 * ArrayList 的常见方法包括:
 *     add(element):添加元素
 *     get(index):获取下标元素
 *     remove(index):移除下标对应元素
 *     set(index,element):将index处的元素修改为element
 */
public static void main(String[] args) {
    // 创建 ArrayList 的对象
    ArrayList data = new ArrayList();
    // 添加元素
    data.add("Java面试教程");
    // 构造随机数并进行添加
    Random rnd = new Random();

    System.out.println("随机20个数字添加进入data集合");
    for (int i = 0; i < 20; i++) {
        data.add(rnd.nextInt(1000));
    }
    // 取出ArrayList里的元素进行打印
    System.out.println("取出ArrayList里的元素进行打印");
    for (int i = 0; i < data.size(); i++) {
        System.out.print(data.get(i) + "  ");
    }
    // 修改1号index成的元素为了不起
    System.out.println();
    data.set(1, "了不起");
    System.out.println("修改1号index成的元素为了不起:data.set(1, \"了不起\")");
    for (int i = 0; i < data.size(); i++) {
        System.out.print(data.get(i) + "  ");
    }
    System.out.println("");
    // 移除“了不起”元素
    System.out.println("移除“了不起”元素:data.remove(1)");
    data.remove(1);
    for (int i = 0; i < data.size(); i++) {
        System.out.print(data.get(i) + "  ");
    }
    System.out.println("");
}

这段代码是一个简单的Java程序,主要演示了如何使用ArrayList类来存储、添加、修改和移除元素。

下面是对这段代码的详细解释:

  1. 首先,创建一个名为data的ArrayList对象。ArrayList是一种动态数组,可以根据需要自动调整大小。
  2. 然后,向data中添加一个字符串元素"Java面试教程"。
  3. 接下来,创建一个Random对象rnd,用于生成随机数。
  4. 使用for循环,向data中添加20个随机整数(范围在0到999之间)。
  5. 使用另一个for循环,遍历data中的所有元素并打印它们。
  6. 修改data中索引为1的元素为字符串"了不起"。
  7. 再次使用for循环,打印修改后的data中的所有元素。
  8. 最后,使用remove方法从data中移除索引为1的元素(即"了不起"),并打印移除元素后的data。

这段代码展示了ArrayList的基本操作,包括添加元素、获取元素、修改元素和移除元素。

源码分析

接下来,我们来看看关于ArrayList源码:

1.ArrayList初始化

代码语言:javascript
复制
// ArrayList 初始化时默认大小为10
private static final int DEFAULT_CAPACITY = 10;

// 用于空实例的共享空数组实例。其实就是直接初始化的话一个空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

//构造一个具有指定初始容量的空列表,初始化ArrayList,传入初始化时的大小
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);
    }
}
//构造一个初始容量为10的空列表。
//如果不传入大小的话就默认大小是10,那么这里就有一个问题:
//我们上面插入的元素超过了10,继续插入元素就会进行拷贝扩容,性能不是特别高。
//所以我们一般情况下初始化时给定一个比较靠谱的数组大小,避免到时候导致元素不断拷贝
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

当我们创建ArrayList对象时,如果没有指定大小参数,那么默认情况下,它会被初始化为一个包含10个元素的数组。

这意味着,每当我们尝试插入超过10个元素时,ArrayList会进行数组拷贝和扩容操作。

这种频繁的数组拷贝和扩容会导致性能消耗较大。

因此,为了优化性能,建议在初始化ArrayList时,为其指定一个相对较大的容量大小。

2.ArrayList的add方法

一、public boolean add(E e) 方法:

这个方法用于向ArrayList中添加一个元素。

首先,它调用ensureCapacityInternal(size + 1)来确保ArrayList的容量足够容纳新元素。

然后,将新元素添加到ArrayList的末尾,并将数组的大小加1。

最后,返回true表示添加成功。

代码语言:javascript
复制
public boolean add(E e) {
    // 增量modCount
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}
//这个方法用于确保ArrayList的容量足够容纳新元素。它首先调用ensureExplicitCapacity(calculateCapacity(elementData, minCapacity))来确定所需的最小容量
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//这个方法用于计算ArrayList的容量。如果传入的elementData是默认的空元素数组,则返回默认容量和最小容量中的较大值;否则,返回传入的最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
}
//这个方法用于确保ArrayList的容量足够容纳新元素。它首先增加modCount(用于记录集合修改次数),然后检查是否需要扩容。如果需要的最小容量减去当前数组长度大于0,就调用grow(minCapacity)方法进行扩容操作。
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}

二、public void add(int index, E element) 方法:

这个方法用于在指定索引位置插入一个元素。

首先,它调用rangeCheckForAdd(index)来检查索引是否在有效范围内。

然后,再次调用ensureCapacityInternal(size + 1)来确保ArrayList的容量足够容纳新元素。

接下来,使用System.arraycopy()方法将指定索引位置之后的所有元素向后移动一个位置,为新元素腾出空间。

然后将新元素插入到指定索引位置,并将数组的大小加1。

代码语言:javascript
复制
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);  
    System.arraycopy(elementData, index, elementData, index + 1,
            size - index);
    elementData[index] = element;
    size++;
}
//这个方法用于检查给定的索引是否在有效范围内。如果索引大于当前列表的大小或小于0,就会抛出IndexOutOfBoundsException异常。
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//这个方法用于确保ArrayList的容量足够容纳新元素。它首先调用ensureExplicitCapacity(calculateCapacity(elementData, minCapacity))来确定所需的最小容量
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//这个方法用于计算ArrayList的容量。如果传入的elementData是默认的空元素数组,则返回默认容量和最小容量中的较大值;否则,返回传入的最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
}
//这个方法用于确保ArrayList的容量足够容纳新元素。它首先增加modCount(用于记录集合修改次数),然后检查是否需要扩容。如果需要的最小容量减去当前数组长度大于0,就调用grow(minCapacity)方法进行扩容操作。
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}

三、public void add(E e) 方法(带并发修改检查):

这个方法用于在没有锁的情况下向ArrayList中添加一个元素。

首先,它调用checkForComodification()来检查ArrayList是否被其他线程修改过。

然后,尝试在当前游标位置插入新元素,并更新游标。

如果发生IndexOutOfBoundsException异常,说明当前线程试图修改一个不存在的元素,此时会抛出ConcurrentModificationException异常。

代码语言:javascript
复制
public void add(E e) {
    checkForComodification();
    try {
        int i = cursor;
        ArrayList.this.add(i, e);
        cursor = i + 1;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
//它用于检查对ArrayList的修改是否被其他线程同时进行
final void checkForComodification() {
    //通过比较modCount(表示ArrayList在当前方法调用之前被修改的次数)和expectedModCount(表示ArrayList在进入该方法时期望的修改次数)来判断是否有并发修改发生
    //果两者不相等,说明在当前方法调用期间有其他线程对ArrayList进行了修改,而当前线程没有获取到正确的数据状态,因此抛出ConcurrentModificationException异常。
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

3.ArrayList的set方法

一、public E set(int index, E element)

这个方法用于在ArrayList的指定索引位置设置一个新的元素。

首先,它会调用rangeCheck(index)来检查索引是否在有效范围内。

然后,它会获取该索引位置的旧值,并将新元素设置到该位置。

最后,它返回旧值。

代码语言:javascript
复制
public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
//这个方法用于检查给定的索引是否在有效范围内。如果索引小于0或大于等于ArrayList的大小,将抛出IndexOutOfBoundsException异常。
private void rangeCheck(int index) {
    if (index < 0 || index >= this.size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

二、public void set(E e)

这个方法用于在ArrayList的最后一个元素位置设置一个新的元素。

首先,它会检查lastRet是否小于0,如果是,那么它会抛出一个IllegalStateException异常。

然后,它会调用checkForComodification()来检查ArrayList是否被并发修改。

接着,它会尝试调用ArrayList.this.set(lastRet, e)来设置新元素。

如果发生IndexOutOfBoundsException异常,那么它会抛出一个ConcurrentModificationException异常。

代码语言:javascript
复制
public void set(E e) {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.set(lastRet, e);
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

4.ArrayList的get方法

public E get(int index)

这是一个公开方法,返回类型为E,参数为一个整数index,表示要获取元素的索引位置。

代码语言:javascript
复制
public E get(int index) {
    rangeCheck(index);
    checkForComodification();
    return ArrayList.this.elementData(offset + index);
}
//这个方法用于检查给定的索引是否在有效范围内。如果索引超出范围,它将抛出一个IndexOutOfBoundsException异常。
private void rangeCheck(int index) {
    if (index < 0 || index >= this.size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//这个方法用于检查ArrayList是否被并发修改。如果在调用此方法时ArrayList正在被修改(例如,其他线程正在添加或删除元素),那么它将抛出一个ConcurrentModificationException异常。
private void checkForComodification() {
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
}
//这是获取指定索引位置元素的主要逻辑。首先,它会计算偏移量(offset)与索引的和,得到实际的数组下标。然后,它会调用ArrayList的内部方法elementData()来获取该下标位置的元素,并将其返回。
E elementData(int index) {
    return (E) elementData[index];
}

get(int index)的方法,用于获取ArrayList中指定索引位置的元素。

在获取元素之前,它会调用rangeCheck(index)来检查索引是否在有效范围内,然后调用checkForComodification()来检查ArrayList是否被并发修改。

如果一切正常,它会计算实际的数组下标,并调用ArrayList的内部方法elementData()来获取该下标位置的元素,并将其返回。

5.ArrayList 的 remove 方法

一、public void remove()

这个方法用于移除ArrayList的最后一个元素。

首先,它会检查lastRet是否小于0,如果是,那么它会抛出一个IllegalStateException异常。

然后,它会调用checkForComodification()来检查ArrayList是否被并发修改。

接着,它会尝试调用ArrayList的内部方法remove(lastRet)来移除最后一个元素。

如果发生IndexOutOfBoundsException异常,那么它会抛出一个ConcurrentModificationException异常。

代码语言:javascript
复制
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

二、public E remove(int index)

这个方法用于移除ArrayList中指定索引位置的元素。

首先,它会调用rangeCheck(index)来检查索引是否在有效范围内。

然后,它会调用checkForComodification()来检查ArrayList是否被并发修改。

接着,它会调用父类(即ArrayList)的remove(parentOffset + index)方法来移除指定索引位置的元素,并将返回的元素保存在result变量中。

最后,它会更新ArrayList的修改计数器和大小,并返回移除的元素。

代码语言:javascript
复制
public E remove(int index) {
    // 进行index是否越界的判断
    rangeCheck(index);
    checkForComodification();
    E result = parent.remove(parentOffset + index);
    this.modCount = parent.modCount;
    this.size--;
    return result;
}

三、public E remove(E element)

这个方法用于移除ArrayList中第一个出现的指定元素。

首先,它会调用rangeCheck(index)来检查元素是否在有效范围内。

然后,它会调用checkForComodification()来检查ArrayList是否被并发修改。

接着,它会获取指定元素的旧值,并计算需要移动的元素数量。如果需要移动的元素数量大于0,那么它会使用System.arraycopy()方法将后面的元素向前移动一位。

最后,它会将ArrayList的大小减1,并将最后一个元素设置为null,然后返回移除的元素。

代码语言:javascript
复制
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;
    return oldValue;
}

//这个方法用于检查给定的索引是否在有效范围内。如果索引超出范围,它将抛出一个IndexOutOfBoundsException异常。
private void rangeCheck(int index) {
    if (index < 0 || index >= this.size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//这是获取指定索引位置元素的主要逻辑。首先,它会计算偏移量(offset)与索引的和,得到实际的数组下标。然后,它会调用ArrayList的内部方法elementData()来获取该下标位置的元素,并将其返回。
E elementData(int index) {
    return (E) elementData[index];
}

动态扩容和拷贝

代码语言:javascript
复制
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 这里做了位运算,相当于数组扩容了1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 随后进行元素拷贝
    elementData = Arrays.copyOf(elementData, newCapacity);
}

这里会涉及三个方法:ensureCapacityInternal(int minCapacity), ensureExplicitCapacity(int minCapacity)grow(int minCapacity)

这三个方法都是用于管理ArrayList的容量:

  1. ensureCapacityInternal(int minCapacity):这个方法用于确保ArrayList的最小容量至少为minCapacity。首先,它会调用ensureExplicitCapacity(int minCapacity)方法来确保ArrayList的显式容量至少为minCapacity
  2. ensureExplicitCapacity(int minCapacity):这个方法用于确保ArrayList的显式容量至少为minCapacity。首先,它会将modCount(表示ArrayList被修改的次数)加1。然后,如果minCapacity - elementData.length > 0(即当前的容量小于需要的最小容量),它会调用grow(int minCapacity)方法来扩容。
  3. grow(int minCapacity):这个方法用于扩容ArrayList。首先,它会获取当前的元素数组的长度,并将其赋值给oldCapacity。然后,它会通过位运算将数组长度扩大1.5倍,并将结果赋值给newCapacity。接着,如果newCapacity小于minCapacity,它会将newCapacity设置为minCapacity。然后,如果newCapacity大于MAX_ARRAY_SIZE(即ArrayList的最大容量),它会调用hugeCapacity(int minCapacity)方法来获取一个更大的容量。最后,它会使用Arrays.copyOf(elementData, newCapacity)方法来创建一个新的元素数组,并将原数组的元素复制到新数组中。

结束语

这次的学习到此告一段落,代码已经粘贴出来了,感兴趣的可以去深入学习一下,下次我们再来看看HashMap的源码吧。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java面试教程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 小白 ArrayList 简单使用技巧
    • 源码分析
      • 1.ArrayList初始化
        • 2.ArrayList的add方法
          • 3.ArrayList的set方法
            • 4.ArrayList的get方法
              • 5.ArrayList 的 remove 方法
                • 动态扩容和拷贝
                  • 结束语
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档