实现一个简单的ArrayList
ArrayList是基于顺序表设计的一个能够动态存储的容器。对于数组长度不可变的限制,ArrayList提供了动态数组的功能,这对程序员编写代码提供了很大的便利。为了对ArrayList有一个更加深刻的认识,接下来我们自己实现一个简易版的Array List。
1. 需求分析
为什么要实现一个MyArrayList?
先来看一个场景,某校有100个学生(Student),现在需要对学生信息进行统一管理,我们可以申请一个长度为100的Student数组,对学生信息进行存储,但是当有新生来学校时,由于数组的长度是不可变的,那么对于新生的信息又该如何存储?
Java中为我们提供了集合类来解决数组长度不可变的问题,其中一个典型的集合类ArrayList,底层是基于数组进行设计的,在存入大量元素时,如果数组存不下,就会触发扩容机制,因此也解决了一些元素个数不确定的场景。
俗话说:“授人以鱼不如授人以渔”。为了方便我们对ArrayList有更深刻的认识,在学习使用它的同时,更要了解其底层是怎样设计的、代码是怎样实现的,因此我们自己实现一个MyArrayList也是很有必要的。
2. 系统设计
在实现MyArrayList之前,我们先去看一下ArrayList的源码,类比ArrayList然后实现MyArrayList。
2.1 功能设计
接下来通过一个表来对比一下我们将要实现的MyArrayList和Java类库ArrayList的基本区别。
功能 | ArrayList | MyArrayList |
---|---|---|
底层结构 | 数组 | 数组 |
默认容量 | 10 | 5 |
扩容机制 | 1.5倍扩容 | 2倍扩容 |
存储数据类型 | 任意类型 | Integer |
ArrayList使用了泛型的设计思想,容器中可以存储任意类型的元素,同时在实例化时可以指定元素类型,对使用时元素类型又做了限制,使其只能存储相同的数据类型。
我们将MyArrayList元素类型设置成Integer类型,忽略类型对的处理,更能体现其中的设计思想。
MyArrayList将提供以下方法:增加元素、删除元素、修改元素、获取元素位置、判断是否存在该元素、获取元素个数等。
2.2 框架设计
MyArrayList基于数组存储元素,默认长度为5,存储元素的类型是整型,基于这些条件我们可以设计出代码的整体框架。
public class MyArrayList {
//因为存储的数据类型为Integer,因此使用int类型的数组存储即可
private int[] elem = new int[DEFAULT_CAPACITY];
private int size;//元素个数
//由于每一个对象默认值都为5,因此将默认值设置为静态的static
private static final int DEFAULT_CAPACITY = 5;//默认长度
// 新增元素,默认尾插
public void add(int data) { }
// 在 index 位置新增元素
public void add(int index, int data) { }
//删除 index 位置的元素并返回该元素
public int remove(int index) { return 0; }
//删除第一次出现的元素 data ,并返回是否删除成功
public boolean remove(Object data) { return true; }
// 判定是否包含 data 元素
public boolean contains(int data) { return true; }
// 获取 index 位置的元素
public int get(int index) { return -1; }
// 将 index 位置的元素设为 value
public void set(int index, int value) { }
//返回第一次出现 data 的位置
public int indexOf(int data) { return 0; }
//返回最后一次出现 data 的位置
public int lastIndexOf(int data) { return 0; }
// 获取集合长度
public int size() { return 0; }
// 清空元素
public void clear() { }
}
3. 代码编写
代码的整体框架已经形成,接下来就是对每个方法进行具体实现了。
3.1 添加元素
添加元素之前需要先判断数组是否已满,如果满了则要进行扩容。
3.1.1 判断数组是否已满
这个方法只提供给类内别的方法调用,因此将其访问级别设置为private,数组满的条件就是数组长度等于集合内元素的个数。
//判断数组是否已满
private boolean isEmpty() {
return elem.length == size;
}
3.1.2 扩容方法
进行2倍扩容,当数组元素已满,还要向其中添加元素时,就会进行扩容,如下图所示,集合中已存在五个元素,向其中添加6时就会触发扩容。
实际上你又会产生疑问,数组可以发生扩容吗?答案是不能,那么它又是如何扩容的呢?
这一切都来源于 Arrays.copyOf(int[] original, int newLength) 这个方法,数组扩容只是表象,实际上是数组进行了拷贝,然后我们将新的数组有赋值给了原数组,由于现在原数组未被对象引用,也会被GC回收器回收。
//扩容方法
private void grow() {
elem = Arrays.copyOf(elem,2 * elem.length);
}
写完了要依赖的方法,现在我们实现添加元素的方法。
3.1.3 尾插元素
- 先判断是否需要扩容
- 再向末尾添加元素
- 元素个数+1
// 新增元素,默认尾插
public void add(int data) {
//数组满了则要扩容
if (this.isEmpty()) {
this.grow();
}
elem[size] = data;//将元素尾插到数组
size++;//集合元素个数+1
}
3.1.4 指定位置插入元素
- 先要判断插入位置是否合法
- 不合法就抛异常
- 合法再判断是否需要扩容
- 然后再添加元素
- 元素个数+1
定义一个异常类继承自RuntimeException
public class IndexOfException extends RuntimeException {
//构造方法
public IndexOfException(String message) {
super(message);//调用父类构造,传入异常时打印的信息
}
}
检查index的合法性,数组中有两个元素1和2,要向其中添加一个元素,数组小于0的下标不存在,可以添加的有效位置如下图粉色部分所示:
//检查添加元素时 index是否合法
private void checkAdd(int index) {
if (index <= -1 || index > size) {
throw new IndexOfException("添加元素时位置不合法!");
}
}
数组中有三个元素1、2、3,要向1下标添加一个元素6,则先要将元素向后移,将下标1位置腾出来,方便新元素插入。
// 在 index 位置新增元素
public void add(int index, int data) {
//检查下标合法性
checkAdd(index);
//数组满了则要扩容
if (this.isEmpty()) {
this.grow();
}
//移动元素给index位置腾地方,插入新元素
for (int i = size; i > index; i--) {
elem[i] = elem[i - 1];
}
//将新元素加入index位置
elem[index] = data;
size++;//记得元素个数+1
}
3.2 删除元素
3.2.1 删除某下标的元素
- 先检查位置的合法性,不合法直接抛异常
- 删除元素:删除元素时只要将待删除位置的元素覆盖了就可以
- 元素个数-1
删除时位置一定要是有效的,数组下表没有小于0的,大于等于size下标的也不合法,如图粉色部分为合法的。
//检查删除时下标是否合法
private void checkRemove(int index) {
//注意size位置也不合法
if (index <= -1 || index >= size) {
throw new IndexOfException("删除元素时位置不合法!");
}
}
集合中有三个元素1、2、3,删除0位置的元素如图所示:
//删除 index 位置的元素并返回该元素
public int remove(int index) {
//检查删除位置的合法性
checkRemove(index);
//记录待删除的值
int value = elem[index];
//覆盖index位置元素
for (int i = index; i < size; i++) {
elem[i] = elem[i + 1];
}
size--;//元素个数-1
return value;
}
3.2.2 删除第一次出现的data元素
- 从数组0下标开始访问,与data元素比较
- 相等则覆盖该元素,然后返回true
- 不相等则继续寻找,有效元素遍历完还未找到则返回false
数组中存在四个元素1、2、3、2,删除第一个出现的2,如下图所示:
此图可以看出3下标的值还为2,但是有效长度已变为3,在逻辑上删除了3下标的值,实际上3下标的值还存在。
//删除第一次出现的元素 data ,并返回是否删除成功
public boolean remove(Object data) {
int i = 0;
boolean flag = false;
//设置一个标志位,false未找到元素,表示删除失败
//true找到元素,表示删除成功
for (; i < size; i++) {
if (data.equals(elem[i])) {
//找到元素
flag = true;
break;//找到第一个data的位置
}
}
//可以直接调用方法删除i位置的值
remove(i);
return true;
}
3.2.3 清空元素
清空集合的元素很简单,只需要将元素个数设置为0就可以,该删除为逻辑上的删除,实际上数组上还有值,但是这些数都是无效的了。比如集合中存在元素1、2、3,将其清空,如下图所示:
// 清空元素
public void clear() {
size = 0;
}
3.3 获取元素
3.3.1 获取指定位置的元素
- 判断位置合法性,此合法性和删除时的合法性一致
- 通过下标直接返回
//检查获取元素时下标是否合法
private void checkGet(int index) {
//注意size位置也不合法
if (index <= -1 || index >= size) {
throw new IndexOfException("获取元素时位置不合法!");
}
}
// 获取 index 位置的元素
public int get(int index) {
checkGet(index);
return elem[index];
}
3.4 修改元素
3.4.1 修改指定位置的元素
- 检查修改位置的合法性
- 通过下标直接修改元素
//修改元素时下标是否合法
private void checkSet(int index) {
//注意size位置也不合法
if (index <= -1 || index >= size) {
throw new IndexOfException("修改元素时位置不合法!");
}
}
// 将 index 位置的元素设为 value
public void set(int index, int value) {
//检查合法性
checkSet(index);
//通过下标直接修改元素值
elem[index] = value;
}
3.5 其他方法
3.5.1 获取集合长度
// 获取集合长度
public int size() {
return size;
}
3.5.2 集合中是否存在否元素
// 判定是否包含 data 元素
public boolean contains(int data) {
//遍历集合找到直接返回true
for (int i = 0; i < size; i++) {
if (elem[i] == data) {
return true;
}
}
return false;
}
3.5.3 返回第一次出现data的位置
//返回第一次出现 data 的位置
public int indexOf(int data) {
//正向遍历集合,找到返回,为找到返回-1
for (int i = 0; i < size; i++) {
if (elem[i] == data) {
return i;
}
}
return -1;
}
3.5.4 返回最后一次出现data的位置
//返回最后一次出现 data 的位置
public int lastIndexOf(int data) {
//返向遍历集合,找到返回,为找到返回-1
for (int i = size; i > 0; i--) {
if (elem[i] == data) {
return i;
}
}
return -1;
}
4. 功能测试
测试每一个方法的正确性,和预期值比较:
public static void main(String[] args) {
MyArrayList list = new MyArrayList();
//添加元素
//list.add(2,1);//IndexOfException 添加元素时位置不合法!
list.add(1);
list.add(2);
list.add(3);
list.add(1,6);
System.out.println(list.toString());//[1, 6, 2, 3]
//删除元素
//list.remove(-1);//IndexOfException 删除元素时位置不合法!
//list.remove(4);//IndexOfException 删除元素时位置不合法!
list.remove(1);
System.out.println(list.toString());// [1, 2, 3]
list.add(2);
System.out.println(list.remove(new Integer(2)));//true
System.out.println(list.toString());// [1, 3, 2]
System.out.println(list.contains(3));//true
System.out.println(list.contains(5));//false
list.add(1);
list.add(2);
list.add(3);
System.out.println(list.indexOf(3));//1
System.out.println(list.lastIndexOf(2));//4
System.out.println(list.size());//6
list.clear();
System.out.println(list.toString());//[]
}
注释为预期结果,运行结果如下,和预期结果一致。
5. 小结
本编文章实现了一个简易版的MyArrayList,对于ArrayList也有了更加深刻的认识,了解了底层的实现原理,以后在编码中也就能够灵活的运用ArrayList。
- 获取源码: MyArrayList集合实现