1、动态数组是什么
动态数组就是可以自动扩容的数组,元素可以自由地被添加/插入到相应的位置,也可以很方便地移除某个位置上的元素。在Java中的ArrayList集合就是一个动态数组。
2、为什么要使用动态数组
- 普通数组的缺点
- 数组一旦创建出来,它的容量就确定了,无法再改变。
- 数组中只能存储一种数据类型。
- 数据的添加、插入、移除都不方便
- 动态数组的优点
- 可以自动扩容
- 可以自定义存储的数据类型
- 可以自由地添加、插入、移除数组中的某个元素
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来判断。