提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
为了更好地理解ArrayList底层运行原理,特此模仿ArrayList集合,自己手动写一个简易版的MyArrayList。
一、思路
首先了解java中ArrayList的基本功能的书写特点,模仿它的底层容器数组、自动扩容功能和其他基本功能。
二、具体功能点如下
1、MyArrayList需要支持泛型,内部使用数组作为容器。
2、在MyArrayList中开发add方法,用于添加数据的,需要遵循ArrayList的扩容机制(自行设计代码,不需要与ArrayList的源代码一样,思想一致即可)
3、在MyArrayList中开发根据索引查询数据的get方法。
4、在MyArrayList中开发根据索引删除数据的remove方法。
5、在MyArrayList中开发一个获取集合大小的size()方法。
6、能够在MyArrayList集合中开发一个forEach方法,这个方法支持使用Lambda进行遍历,至于函数式接口叫什么名称无所谓。
7、编写测试用例对自己编写的MyArrayList集合进行功能正确性测试。
三.代码实现
MyArrayList类
public class MyArrayList<E> {
//首先需要定义泛型,来给用户自定义数据类型
//需要定义数组成员变量,因为这个数组的真实容量是等于用户获取的数组长度的,所以这里进行私有化
private Object[] elementDate = {};
//在Java中,由于泛型类型的类型擦除机制,无法在运行时创建泛型数组。因此,尝试创建泛型数组会导致编译器报错
//定义数组长度size,用于给用户查询当前集合的长度,这个长度不应该被外界随意修改,所以进行私有化
private int size;
// 这里定义的数组的默认初始容量为10,是模范的java中ArrayList的写法
private int DEFAULT_CAPACITY = 10;
//add 添加方法
public boolean add(E e) {
//如果集合的长度等于数组的真实长度,这时候数组应该进行扩容操作
if (size == elementDate.length) {
grow();
}
//当第一次添加的时候,size初始值是0,所以将数据添加到索引0的位置,顺便将size向后移动一位,用来记录数组大小和添加下一个元素
elementDate[size++] = e;
return true;
}
//get 查询方法 根据索引查询数组元素
public E get(int index) {
//涉及到索引需要定义一个检查索引越界的方法
checkIndex(index);
return (E) elementDate[index];
}
public E remove(int index) {
E e = (E) elementDate[index];
//删除数据应该考虑是否需要进行元素移位操作。
//0 1 2 3 4 5
//0 1 2 4 5 0
//这里使用size减1得到最后一个索引的位置再减去index就得到index后面的元素个数,也就是需要进行移动的元素个数,将它定义为移动标记
int moveFlag = size - index - 1;
//如果移动标记等于0,说明输入的索引是0数组也只有一个元素,这个时候不需要进行移位,直接跳过下面的if判断将数组的唯一元素赋值为空即可
if (moveFlag != 0) {
//如果移动标记不等于0,说明需要进行移位,这里使用的是System.arraycopy方法,参数依次为:原数组、原数组要复制的起始位置、
// 目标数组、目标数组的起始位置和要复制的元素个数
System.arraycopy(elementDate, index + 1, elementDate, index, moveFlag);
}
//将数组最后一个元素赋空,并将数组长度-1
elementDate[--size] = null;
return e;
}
//forEach的遍历方法
//因为forEach方法是基于迭代器实现的,而迭代器只能返回 Object 类型的元素。所以forEach方法会有一个接口函数的调用,函数的功能
// 是将数组的每个值进行类型转换处理再返回的一个方法回调
//这里我们模仿java中的Consumer 写一个MyConsumer来使用
public void forEach(MyConsumer<E>action){
//在遍历之前加一个判断传入的元素非空
Objects.requireNonNull(action);
for (int i = 0; i < size; i++) {
action.accept((E) elementDate[i]);
}
}
//返回集合长度
public int size() {
return size;
}
private void checkIndex(int index) {
//应为size记录的永远是数组最后一个元素的下一个,所以索引等于size也会越界
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException(index + " out of max length " + size);
}
}
private void grow() {
//size等于0 说明是第一次添加数组,应该创建一个容量为10的初始数组
if (size == 0) {
elementDate = new Object[DEFAULT_CAPACITY];
} else {
//走到这里说明数组的容量不够用了,模仿java中ArrayList的写法应该将数组的容量扩大1.5倍
//这里因为加法的优先级高于移位算法,所以后面用小括号括起来
elementDate = Arrays.copyOf(elementDate, elementDate.length + (elementDate.length >> 1));
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < size; i++) {
sb.append(i == size-1 ? elementDate[i] + "]": elementDate[i] + ", ");
}
return sb.toString();
}
}
MyConsumer类
public interface MyConsumer<E> {
abstract void accept(E e);
}
四.测试用例
测试类
public class MyArrayListTest {
public static void main(String[] args) {
MyArrayList<String>list = new MyArrayList<>();
//查看集合初始长度
System.out.println(list.size());
//添加元素
list.add("张三");
list.add("李四");
//查看数组第一次添加数据后的长度
System.out.println(list.size());
//打印数组
System.out.println(list);
//查找方法
System.out.println(list.get(1));
//删除方法
System.out.println(list.remove(1));
System.out.println(list);
//forEach方法
list.forEach(s -> System.out.println(s));
}
}
总结
难点是删除方法 和 forEach方法,自己手动写一遍基本理解