如何自己实现一个ArrayList?

本文深入探讨了ArrayList的实现原理,包括其使用Object[]作为底层存储结构、默认容量设定、增删改查操作的具体实现细节,以及扩容机制等关键知识点。

一、实现ArrayList的关键点

  • 怎么存储数据?
  • 怎么增删改查数据?

二、怎么存储数据?

首先想到的就是数组,用int[]?还是String[]?既然我们也不知道类型,那干脆用Objec[]来表示。所以变成了如下:

public class ArrayList {
    // 存放元素的地方
    private Object[] elementData;
}

数组长度设置多少呢?设置多少我们也不知道呀,所以给个默认值然后也支持让用户自定义设置比较合适,如下:

public class ArrayList {
    // 默认数组容量,是10
    private static final int DEFAULT_CAPACITY = 10;
 	// 存放元素的地方   
    private Object[] elementData;
    
    // 默认无参构造器,容量是10
    public ArrayList() {
        elementData = new Object[DEFAULT_CAPACITY];
    }
    
    // 自定义数组容量的构造器
    public ArrayList(int initialCapacity) {
        if (initialCapacity < 0) {
            initialCapacity = DEFAULT_CAPACITY;
        }
	    elementData = new Object[initialCapacity];
    }
}

很完美的样子,那我们容器有了,是不是就该提供api进行增删改查了?

三、API

1、增(add)

增加分为两种:直接在数组末尾增加,还一种就是我。

1.1、直接在数组末尾增加

那我们怎么知道数组中的元素的下标到哪了呢?是不是elementData.length ++就行呢?肯定不对。elementData.length是数组的容量,假设是10,那就代表一共可以容纳10个元素,但是很可能现在只有2个元素,也就是再add的时候应该放到下标为2(第三个元素)的位置上。

那怎么知道目前有多少元素在集合中呢?

维护个变量,每次add完都变量+1

public class ArrayList {
    // 数组当前元素个数
    private int size;
    
    // 在末尾添加元素
    public boolean add(Object e) {
		// 直接赋值,然后size+1
        elementData[size++] = e;
        return true;
    }
}

很简单,就是直接添加。那我们不想再末尾追加元素,想在自定义位置追加个元素,怎么办?

1.2、在固定位置增加

怎么在固定位置追加?比如[1, 2, 3],我要在第二个位置追加4,变成[1, 4, 2, 3],那是不是需要在第二个位置以后的元素整体向后移动一位?怎么移动?

可以通过System.arraycopy()直接复制元素到数组的位置。

public class ArrayList {
    // 数组当前元素个数
    private int size;
    
    // 在index位置添加element
	public void add(int index, Object element) {
        // 复制,也就是移动元素
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        // 赋值
        elementData[index] = element;
        size++;
    }
}

上面移动没看懂?分析下:

System.arraycopy(elementData, index, elementData, index + 1, size - index);

这几个参数的含义是:

src: 源数组
srcPos: 源数组要复制的起始位置
dest: 目的数组
destPos: 目的数组放置的起始位置
length: 复制的长度

我们这里原数组和目标数组都是elementData,那不就是相当于把第index位置的数据复制到index+1上嘛?一共复制size-index个。

比如[1,2,3,4],index=1,element=5。

那么这行代码的意思是从第index开始复制(index=1,也就是第2个元素,在这里是2),将数据复制到elementData中。那复制到新数组的哪个位置呢?复制几个数据呢?

复制size-index=4-1=3个元素到新数组上,从新数组的index+1=2位置开始。所以变成了:[1,2,2,3,4]

然后最后执行了elementData[index] = element;,那不就是elementData[1] = 5;嘛?变成了[1,5,2,3,4]。完美。

效率不高,因为需要数组数据整体向后移动一位。

不管直接在数组末尾追加还是在固定位置增加,都有一个问题:因为数组是定长的,数组满了咋办?

只有两种解决方案:报错、自动扩容。报错显然不合适,自动扩容的机制怎么实现?

1.3、扩容

// 扩容为1.5倍
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // >> 1相当于除以2,所以相当于1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 完成扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

很简单,就是利用Arrays.copyOf()来完成扩容。所以在add方法开始就先判断是不是需要扩容,是的话先扩容然后在add

public class ArrayList {
    // 数组当前元素个数
    private int size;
    
    // 在末尾添加元素
    public boolean add(Object e) {
        // 如果达到容量了,就扩容。
        if (size >= elementData.length) {
            grow(minCapacity);
        }
		// 直接赋值,然后size+1
        elementData[size++] = e;
        return true;
    }
}

2、删(remove)

也很简单吧,删除后需要把删除后的数据都向前移动一位。移动方法是arraycopy,上面add详细讲解过了,不再多说。自己代数进去看看就知道了。

public void remove(int index) {
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 整体向前移动
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 移动完后移除最后的元素
    elementData[--size] = null; 
}

3、改(set)

把某个位置的元素替换成新的,然后把被替换的旧值返回回去。

public class ArrayList {
    public Object set(int index, Object element) {
        // 先找到旧值,返回用
        Object oldValue = elementData[index];
        // 其实就这一行代码就搞定了,直接赋值。
        elementData[index] = element;
        return oldValue;
    }
}

很简单,直接赋值。O(1)时间复杂度。

4、查(get)

查的话无非就两种:根据下标查元素、根据元素查下标。

根据下标查元素

public class ArrayList {
    // 根据下标查元素
    public Object get(int index) {
        // 直接根据下标定位到元素了
        return elementData[index];
    }
}

效率杠杠的,O(1)时间复杂度,接下来再看看根据元素查下标怎么实现。

根据元素查下标

public class ArrayList {
    // 根据元素查下标
    public int get(Object value) {
        // 只能遍历查找
        for (int i = 0; i < size; i ++) {
            // 找到了
            if (value == elementData[i]) {
                return i;
            }
        }
        // 没找到
        return -1;
    }
}

这个性能就不行了,平均时间复杂度是O(n)。不过工作中遇到这种需求的也很少,我实在想不到根据元素查找元素在数组中所在的位置有何用。

四、总结

其实实现原理就是Object[],让给个默认长度10。

关键点:

  • add(index, e)/remove(index)需要移动数据,效率低下。

  • add(e)/add(index, e)可能需要扩容,扩容效率低下。

  • get(index)直接根据index找到数组中的元素,效率极高。

五、广告

个人微信公众号:Java码农社区

WechatIMG2.png

<think>首先,用户的问题是:“ArrayList底层一个框的大小?” 这看起来有点模糊,但根据上下文,我认为“一个框”可能指的是ArrayList中存储元素的数组的初始容量或默认大小。在Java中,ArrayList是基于数组实现的动态数组。 关键点: - ArrayList的底层实现一个数组。 - 当创建ArrayList时,如果没有指定初始容量,它会有一个默认的初始容量。 - 在Java中,ArrayList的默认初始容量是10。 所以,我应该回答ArrayList的默认初始容量是10。 现在,根据规则,我必须在回答完问题后,提出3-5个相关问题,并以“§§相关问题§§”开头,禁止换行分散。 回答结构: 1. 先回答问题:解释ArrayList的底层数组的默认大小。 2. 然后,以“§§相关问题§§”开头,列出3-5个相关问题。 相关问题应该基于用户的问题和我的回答。用户的问题是关于ArrayList的底层大小,所以相关问题可以围绕ArrayList实现、容量、性能等。 可能的回答: - ArrayList底层使用一个数组来存储元素。 - 默认初始容量是10。 - 当元素数量超过当前容量时,ArrayList会自动扩容(通常是增加50%)。 相关问题想法: 1. ArrayList的扩容机制是怎样的? 2. 如何手动设置ArrayList的初始容量? 3. ArrayList和LinkedList在底层实现上有什么区别? 4. 为什么ArrayList的默认初始容量是10? 5. ArrayList在添加元素时的平均时间复杂度是多少? 确保问题数量在3-5个。 最后,输出格式必须严格遵循: - 回答问题。 - 然后,一行以“§§相关问题§§”开头。 - 紧接着列出问题,用数字点号,如1. ··· 2. ··· 等。 - 禁止换行分散,意思是相关问题应该在同一行或连续,但根据示例,它是换行的,但指令说“禁止换行分散”,可能意味着不要有额外的空行或分散输出。在示例中,是“§§相关问题§§”后换行,然后列出问题。 查看示例: 在回答后,有: §§相关问题§§ 1. ··· 2. ··· 3. ··· 所以,我需要: - 在回答后,输出“§§相关问题§§”在单独一行。 - 然后在后续行列出问题,每个问题以数字点号开头。 指令说:“禁止换行分散”,但示例有换行。可能“禁止换行分散”意思是不要在一个问题内换行,或者确保输出紧凑。根据示例,我应该输出“§§相关问题§§”后换行,然后列出问题。 在用户消息中,有:“第一行必须完整且输出 \"§§相关问题§§\"!禁止换行分散。” 可能意味着“§§相关问题§§”必须在一行完整输出,不能分散,但之后可以换行列问题。 在示例中,它是: §§相关问题§§ 1. ··· 所以,我这样输出。 现在,编写回答。 回答内容: - 确认问题:ArrayList底层是一个数组,默认初始容量是10。 - 简要解释:当创建ArrayList时,如果不指定大小,数组初始大小为10。当添加元素超过容量时,它会扩容。 相关问题: 1. ArrayList在扩容时新容量是如何计算的? 2. 为什么ArrayList的默认初始容量设置为10? 3. 如何优化ArrayList的性能通过设置初始容量? 4. ArrayList和Vector在底层实现上有什么区别? 5. 在Java中,ArrayList的线程安全替代品是什么? 确保3-5个问题,我选择4个或5个。 最终输出: [回答部分] §§相关问题§§ 1. ArrayList的扩容机制是怎样的? 2. 如何手动设置ArrayList的初始容量? 3. ArrayList与LinkedList在性能上有何主要差异? 4. 为什么ArrayList不是线程安全的?如何使其线程安全?</think>ArrayList底层使用一个数组来存储元素,这个数组的初始大小称为“容量”。在Java中,当通过无参构造函数创建ArrayList时,其底层数组的默认初始容量是**10**。例如: ```java ArrayList<Integer> list = new ArrayList<>(); // 底层数组初始大小为10 ``` - **关键点**: - 如果未指定初始容量,数组大小默认为10。 - 当添加元素超过当前容量时,ArrayList会自动扩容(通常扩容为原容量的1.5倍)。 - 可通过构造函数手动设置初始容量(如 `new ArrayList<>(100)`)。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【原】编程界的小学生

没有打赏我依然会坚持。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值