实现java动态数组

本文详细解析了Java中ArrayList的实现机制,包括动态数组的概念、扩容与缩容策略,以及添加、删除、查找等基本操作。通过一个自定义的Array类展示了动态数组如何在内存中管理和调整容量,深入理解ArrayList的内部工作原理。
摘要由CSDN通过智能技术生成

java自身的数组属于静态数组,即无法自动伸缩容量。动态数组则是在静态数据的基础上,使用代码逻辑实现数组容量的自动伸缩,如下代码。java.util.ArrayList的实现原理与下面代码类似。

import java.util.Arrays;

/**
 * 数组有一个前置条件,一个挨着一个存储。
 */
public class Array<E> {
    /**
     * 步骤一  定义类变量
     */
    private E[] data;
    //size具有实际计数意义,类似指针。表示data当前最后一个存值位置的后面一位(即第一个不存值的位置)的索引,即实际大小
    // data的length属性是从1开始计数,其值表示数组data的总长度。而数组索引是从0开始计数,所以其范围是 0~ length-1.
    //再由于size是”逻辑游标“,因此其有效的最大值是length-1。即size在0~length-1之间时,data[size]可以取到有效值。
    //注意区分 数组索引的0~lenght-1时是客观存在的,一旦定义一个数组,其索引就已确定。而size是在使用数组时,定义的一个游标。
    // 当size=length-1时,即游标指向了数组的最有一个位置,表示data数组只只能再添加一个元素
    //当size = length时,表示游标指向了数组外,即data数组已满,没有空位置了,再添加元素需要扩容。
    private int size;

    /**
     * 步骤二 定义构造参数
     * @param capacity
     */
    public Array(int capacity) {
        //java不支持new创建泛型数组 data = new E[10];
        data = (E[])new Object[capacity];
        size = 0;
    }

    public Array() {
        this(10);
    }

    /**
     * 步骤三 定义外部调用方法
     *
     * @return
     */
    public boolean isEmpty(){
        return size == 0;
    }

    /**
     * 获取数组的容量
     * @return
     */
    public int getCapacity(){
        return  data.length;
    }

    /**
     * 向数组任一位置添加元素
     * @param index 目标位置
     * @param e 插入元素
     */
    public void add(int index,E e){
        //数组容量校验
        if(size == data.length){
            //扩容 * 2  新容量和老容量再同一个数量级,而不是常数
            resize(data.length * 2);
        }
        //插入位置校验
        //数组有一个前置条件,一个挨着一个存储。而size下标对应数组当前该插入的位置。
        // 若index > size,则会出现数组没有连续存储,存储位置发生离散,不符合数组这种数据结构的基本定义。
        if(index < 0 || index > size){
            throw new IllegalArgumentException("数组越界");
        }
        //边界
        for(int i = size - 1;i >= index; i--  ){
            data[i + 1] = data[i];
        }
        data[index] = e;
        size++;
    }

    /**
     * 首位插入
     * @param e
     */
    public void addFirst(E e){
        add(0,e);

    }

    /**
     * 数组尾部插入数据
     * @param e
     */
    public void addLast(E e){
        //注意此处的尾部是指数组连续有值的尾部,而不是数组的容量。
        //譬如数组的容量是10,而当前存储了5个值(索引0~4已有数据,此时size=5),那么此处的尾部指索引为5的位置,即size指向的位置。
        //因此参数使用size,而不是data.length。
        add(size,e);
    }


    /**
     * 按照索引获取元素
     * @param num
     * @return
     */
    public E getElement(int num) {
        return data[num];
    }

    public int getSize() {
        return size;
    }

    /**
     * 获取数组元素
     * 数组元素不用移动,直接从静态数据data获取元素
     * @param index
     * @return
     */
    public E  get(int index){
        //此处
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("参数非法");
        }
        return data[index];
    }

    /**
     * 更新元素
     * 数组元素不用移动,直接更新静态数据data
     * @param index
     * @param e
     */
    public void set(int index,E e){
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("参数非法");
        }
        data[index] = e;
    }

    /**
     * 查找元素e的第一个索引
     * @param e
     * @return
     */
    public int find(E e){
        for (int i = 0; i < size; i++) {
            if(data[i] == e){
                return i;
            }
        }
        return -1;
    }

    /**
     * 是否存在元素e
     * @return
     */
    public boolean isExist(E e){
        for (int i = 0; i < size; i++) {
            if(data[i] == e){
                return true;
            }
        }
        return false;
    }

    /**
     * 删除指定位置元素,返回删除的元素
     * @param index
     */
    public E remove(int index){
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("数组越界,无法删除");
        }
        E temp = data[index];
        /*for (int i = index; i < size-1; i++) {
            data[i] = data[i + 1];
        }
        该循环i的初始值是index,则对应的终点值是 size-2,因此边界条件是 i < size -1
        下面的循环i的初始值是index + 1,则对应的终点值是size-1,因此边界条件是 i < size.
        这两个循环的效果一样,但下面的更贴合当前的需求。因为data[index]的值不需要动,第一个要动的是data[index + 1],因此i初始化为index+1.
        在使用for循环时,关键在于初始值,边界。初始化的位置一般选择第一个要发生变化的值;而边界值的选择受到 结合业务场景 和 初始值 的制约。
        for循环的步长会影响时间复杂度。
        */
        for (int i = index + 1; i < size; i++) {
            data[i-1] = data[i];
        }
        size--;
        //在不是泛型的情况下,不需要手动置空。
        //在泛型情况下,由于泛型中装载的是对象,当删除数组元素时,尾部的size指针左移一位后,指向的是一个无用的对象【loitering objects】。
        //但这个无用的对象不代表内存泄露,因为数组只要再被使用,这个对象就可能被覆盖。
        //不过,若数组不再被使用,这个对象就不会被释放,无法被GC。因此,手动置空可让GC回收这个对象,以提高性能。
        //置空与否不影响删除逻辑。
        data[size] = null;
        //此处size边界设置为1/4,是为了避免复杂度震荡
        if(size == data.length/4 || data.length/2 != 0){
            resize(data.length/2);
        }
        return temp;
    }

    /**
     * 删除全部指定元素
     * @param e
     */
    public void removeAllElement(E e){
        for (int i = 0; i < size; i++) {
            if(data[i] == e){
                remove(i);
            }
        }
    }


    /**
     * 删除第一个指定元素
     * @param e
     */
    public void removeElement(E e){
        int index = find(e);
        if(index != -1){
            remove(index);
        }
    }

    /**
     * 动态扩容、缩容
     * 内部逻辑,不暴露给用户 因此使用private
     * @param newCapacity
     */
    private void resize(int newCapacity){
        E[] newArr = (E[])new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newArr[i] = data[i];
        }
        data = newArr;
    }
    
    @Override
    public String toString() {
        return "Array{" +
                "data=" + Arrays.toString(data) +
                ", size=" + size +
                '}';
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值