【Java数据结构】基于ArrayList实现一个MyArrayList

实现一个简单的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的基本区别。

功能ArrayListMyArrayList
底层结构数组数组
默认容量105
扩容机制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);
    }

Arrays.copyOf()方法执行过程
写完了要依赖的方法,现在我们实现添加元素的方法。

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,如下图所示:
    删除第一次出现的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。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值