02动态数组

1、动态数组是什么

动态数组就是可以自动扩容的数组,元素可以自由地被添加/插入到相应的位置,也可以很方便地移除某个位置上的元素。在Java中的ArrayList集合就是一个动态数组。

2、为什么要使用动态数组

  • 普通数组的缺点
    1. 数组一旦创建出来,它的容量就确定了,无法再改变。
    2. 数组中只能存储一种数据类型。
    3. 数据的添加、插入、移除都不方便
  • 动态数组的优点
    1. 可以自动扩容
    2. 可以自定义存储的数据类型
    3. 可以自由地添加、插入、移除数组中的某个元素

3、通过封装一个集合类来实现动态数组

public class ArrayList<E>

类名起为ArrayList,与JDK自带的集合同名,可以帮助我们更好地理解ArrayList集合,这里使用到了泛型,目的是可以自定义集合中存储的数据类型。

3.1、属性(字段)

 /** * 集合中元素的数量 */
    private int size;

    /** * 集合的默认初始容量 */
    private static final int DEFAULT_CAPACITY = 10;

    /** * 如果没有找到该元素,则返回该值 */
    private static final int INDEX_NOT_FOUND = -1;

    /** * 集合中存放元素的数组 */
    private E[] elements;

3.2、构造器

	public ArrayList(){ 
        //调用有参构造方法
        this(DEFAULT_CAPACITY);
    }

    /** * 有参构造方法,可以自定义数组容量 * @param capacity */
    public ArrayList(int capacity){ 
        //先判断是否比初始化容量大,如果没用初始化容量大则还是使用初始化容量
        capacity = capacity < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : capacity;
        //给集合的初始化容
        elements = (E[]) new Object[capacity];
    }

这里我们设置有参和无参构造方法。

  • 对于无参构造,默认调用有参构造方法,并且将默认的初始容量的常量作为参数传入。
  • 对于有参构造,首先判断传入的参数是否比我们默认的常量小,如果是,新建的数组容量为默认的常量,如果不是,则采用传入的参数。在初始化数组时,要使用强制类型转换,因为所有类都基础了Object类,所以这里使用Object作为一个共用的数据类型,以保证泛型可以被正常使用。**

3.3、方法

3.3.1、获取元素的个数
    /** * 获取数组中元素的个数 * @return */
    public int size(){ 
        return size;
    }

直接返回成员变量size即可。

3.3.2、添加元素
/** * 往集合中添加元素,添加到集合的末尾 * @param element */
    public void add(E element){ 
        //直接调用另一个add方法即可,这样可以减少代码的变动
        add(size, element);
    }

    /** * 往集合中的某个位置添加元素 * @param index * @param element */
    public void add(int index, E element){ 
        //检查索引范围是否正确
        rangeCheckForAdd(index);
        //要保证数组的容量够用
        ensureCapacity(size);
        for (int i = size; i > index; i--){ 
    		elements[i] = elements[i - 1];
		}
        elements[index] = element;
        size++;
    }

这里使用了方法重载,我们重点来看有两个参数的add方法

首先调用了rangCheckForAdd方法来检查传入的index是否正确,在后面的方法中,只要参数中有index,我们都需要进行判断,来确定这个index是否正确,因此在这里我们将判断的代码抽取出来写成了一个方法,这样的好处是可以让每个方法只关注自己核心的功能。

抽取出来检查index是否正确的方法:

private void indexOutOfBoundCheck(int index){ 
        if (index >= size || index < 0){ 
            throw new IndexOutOfBoundsException("index is " + index + " but size is " 			+ size);
        }
    }

    private void rangeCheckForAdd(int index){ 
        if (index > size || index < 0){ 
            throw new IndexOutOfBoundsException("index is " + index + " but size is " 			+ size);
        }
    }

这两者唯一的区别就是if语句中index>size和index>=size的区别,rangeCheckForAdd方法只用于检查add方法中传入的index是否正确。

  • indexOutOfBoundCheck这个方法用于根据索引获取、移除、改变元素,因此index的值不能>=size,否则就会产生数组下标越界异常。
  • rangeCheckForAdd这个方法的目的是插入一个元素到数组的任意位置,因此也可以插入到数组的末尾,也就是size的位置,因此可以等于size。

对于第一个add方法,我们的目的是通过该方法向元素的末尾添加元素,因此只需要让该add方法调用第二个add方法即可,我们需要做的就是将索引index设置为size,表示往最后添加元素。

  • ensureCapacity这个方法是用来确保容量的,也就是动态数组的关键——可以自动扩容
	private void ensureCapacity(int oldCapacity) { 
        if (oldCapacity < elements.length) return;
        //当size = 数组的长度时,表示应该进行扩容,这里使用位运算符,扩容之后的容量为之前的1.5倍
        int newCapacity = elements.length + (elements.length >> 1);
        //调用copyOf进行数组拷贝
        elements = Arrays.copyOf(elements, newCapacity);
    }

在进行添加之前,我们应该使用ensureCapacity方法来确保数组的容量足够。

  • 首先先进行判断,如果集合中元素的大小要小于数组的容量,我们不做任何操作。

  • 当集合的大小与数组容量相同时,应该进行扩容,这里采用位运算符来进行运算。

    效率高,左移一位表示*2,右移一位表示/2,扩容后的容量为之前的1.5倍,JDK中自带的ArrayList也是如此。

  • 这里采用Arrays类的copyOf方法来进行数组拷贝,给elements成员变量重新指定一个新的容量的数组。

下面来看看这个for循环的思路

for (int i = size; i > index; i--){ 
    elements[i] = elements[i - 1];
}

往数组中一个位置添加元素,那么该位置以后的元素都要向后移动一位,因此将该位置后面的元素都要赋值给后一位元素,而且要从最后一位元素开始赋值。将前一位置元素的值赋给后面的元素,依次往前,直到空出要插入元素的位置,将传入的元素赋值给该位置。

elements[index] = element;

(如果从插入的位置开始往后类推赋值,则会出现将一个值重复给后面的元素赋值,比如将a赋给b,再将b赋给c,将c赋给d,这里的b已经成为了a,因此全部的值都会变为a,所以我们要采用从最后一位往前开始赋值)

3.3.4、移除元素
	public E remove(int index){ 
        indexOutOfBoundCheck(index);
        E element = elements[index];
        for(int i = index + 1; i < size; i++){ 
            //从index位置开始,将每个元素都向前移动一位
            elements[i - 1] = elements[i];
        }
        //将先前最后一个元素的位置设置为null
        elements[--size] = null;
        return element;
    }

这里的道理与添加元素类似,刚好相反,是将后面的元素赋值给前一个元素,并且是从前往后赋值。

3.3.5、改变某个元素的值
    public E set(int index, E element){ 
        indexOutOfBoundCheck(index);
        E oldElement = elements[index];
        elements[index] = element;
        return oldElement;
    }

直接赋值即可。

3.3.6、获取根据索引某个元素
	public E get(int index){ 
        indexOutOfBoundCheck(index);
        return elements[index];
    }

直接返回该位置的元素即可。

3.3.6、判断集合是否为空
	public boolean isEmpty(){ 
        return size == 0;
    }

当size为零时即可认定集合为空。

3.3.7、清除集合中的元素
	public void clear(){ 
        //将数组中每一个元素设置为null
        for (int i = 0; i < size; i++){ 
            elements[i] = null;
        }
        size = 0;
    }

要将数组中的每一个位置都设置为null,这也可以使每个位置指向的对象都被垃圾回收,而不会造成内存浪费。

要注意的是不能将elements设置为null,这样相当于重新创建了一个数组。

3.3.8、查找元素的索引
    public int indexOf(E element){ 
        //这里进行空值判断,如果输入的参数为null,就不能使用equals进行比较
        //此时我们要便利集合返回第一个为null的值
        if (element == null){ 
            for(int i = 0; i < size; i++){ 
                if (elements[i] == null) return i;
            }
            return INDEX_NOT_FOUND;
        }else { 
            for(int i = 0; i < size; i++){ 
                if (element.equals(elements[i])) return i;
            }
            return INDEX_NOT_FOUND;
        }
    }

  • 如果传入的值为null,则返回第一个null所在的位置。
  • 如果传入的值不为null,那么可以调用传入值的equals方法与数组中的元素一一比对,直到找到该元素。
  • 如果没有找到相对应的元素那么就返回我们提前设置好的常量。
3.3.9、判断是否包含某个元素
    public boolean contains(E element){ 
        return indexOf(element) != INDEX_NOT_FOUND;
    }

这里可以借助indexOf这个方法来进行判断,如果该方法返回的值与INDEX_NOT_FOUND相等,则说明不包含。

4、感受

  • 一定要养成简化代码的习惯,能优化的地方一定要优化,比如说for循环中就有许多指的优化的地方,一个小改动可能就会减少许多复杂度。
  • 要学会对经常使用的代码片段进行抽离,方便重复调用。(以常量或者私有方法的形式)
  • 要会对方法进行重复使用,如在无参构造方法中调用有参构造方法,在第一个add方法中调用第二个方法,在contains方法中调用indexOf来判断。
    原文作者:cp1094397606
    原文地址: https://blog.csdn.net/cp1094397606/article/details/106950012
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞