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 +
'}';
}
}