一、前言

JAVA数组缺点:

一旦定义了数组,数组的长度不可以更改。

功能少,他没有提供多样的增删改查操作,例如在JavaScript上的push()、pop()、unshift()、shift()等这些接口,但是它能存放任何的数据类型。

由于Java数组有点笨重,操作数组的时候有点不方便,我们不得不自己封装自己数组,当然现在有很多工具类例如ArrayList,它就可以完全代替数组。

下面我们将演示如何对数组进行二次封装,我们将完成以下功能。(图1)

图1

二、准备工作

1. 安装JDK1.8(我自己写的代码在1.8环境测试没有问题)。

2. 安装开发工具(IDEA/Eclipse),推荐IDEA(有很多骚操作)。

三、编写自己的数组类

JDK和开发工具安装完成了,接下来我们就可以开始骚操作了。

我们开始编写自己的数组类。

public class Array {
private int size;
private int[] data;
public Array(){
this(10);
}
public Array(capacity) {
if(capacity < 1) {
throw new IllegalArgumentException("create array failed. this capacity must greater than 0");
}
this.size = 0;
this.data = new int[capacity];
}
}

可以看到Array类有两个私有属性和两个构造方法。两个私有属性分别是int类型的size和int类型的数组data,size存储数组实际长度,data存储数据。两个构造方法,无参构造:默认初始化数组容量为10;有参构造:数组的容量将由调用者决定。

size和capacity的区别

size: 指的是数组已经存放了多少个数据

capacity:指的是数组可以存放多少个元素

四、编写添加操作

push(int e),尾插入其实就是在数组最后一个元素后面插入一个元素,不需要移动数组元素。(图4-1)

图4-1

public void push(int e) {
if(this.size == data.length)
throw new IllegalArgumentException("add failed. this array is full");
this.data[size] = e;
size++;
}

unshift(int e),首插入其实就是在数组第一个元素前面插入一个元素,所以数组所有元素都要向后移一位。(图4-2)

图4-2

public void unshift(int e) {
if(this.size == data.length)
throw new IllegalArgumentException("add failed. this array is full");
for(int i=size-1; i>=0; i--){
this.data[i+1] = data[i];
}
this.data[0] = e;
size++;
}
add(int index,int e), 按指定索引插入其实就是在指定位置上插入一个元素,数组元素是否需要移动取决于索引位置。
public void add(int index,int e) {
if(this.size == data.length)
throw new IllegalArgumentException("add failed. this array is full");
if(index<0 || index>size) {
throw new IllegalArgumentException("add failed. the index illegal");
}
for(int i=size-1; i>=index; i--) {
this.data[i+1] = data[i];
}
this.data[index] = e;
size++;
}

到现在为止已经编写好添加操作的三个方法了。聪明的小伙伴会发现,三个方法都要判断数组是否已经满了,如果满了就直接抛出异常,否则就继续执行。unshift、add方法都要循环遍历移动元素。有没有一种方法,可以把他们相似的代码抽取出来,这样我们就可以减少了代码,并且提高代码的可维护性。现在我们看看下面的代码块。

public void push(int e) {
this.add(size,e);
}
public void unshfit(int e) {
this.add(0,e);
}

重写了push、unshift 方法,就解决了。其实add方法就已经把它们相似的代码块已经封装好了。

毒鸡汤 请相信我,我所说的每句话,都是废话!

五、编写删除操作

remove(int index):int,按指定索引删除,需要是否需要移动取决于索引位置。(图5-1)

图5-1

public int remove(int index) {
if(index<0 || index>=size) {
throw new IllegalArgumentException("remove failed. the index illegal");
}
int res = this.data[index];
for(int i=index+1; i
this.data[i-1] = this.data[i];
}
this.size--;
return res;
}
remove对比add方法,其实也有点相似之处。不管是add方法还是remove方法都要去判断index是否是合法值,它们判断逻辑稍微有一点不一样,贴下代码再来瞧瞧看。
// add
if(index<0 || index>size) {
throw new IllegalArgumentException("add failed. the index illegal");
}
// remove
if(index<0 || index>=size) {
throw new IllegalArgumentException("remove failed. the index illegal");
}

看到这里,可以先上滑看看add和remove时候的图片,思考一下为什么add方法index>size,而remove方法index>=size?

add 方法是index>size,当index==size时,其实就是尾插入,所以index=size是合法的。

remove方法是index>=size,当index==size时,size索引是没有元素的,你还咋删除呢?

shift():int,首移除,数组后面元素全部向前移一位。

public int shift() {
return this.remove(0);
}
pop():int,尾移除,数组实际长度size减少1就可以实现了。
public int pop() {
return this.remove(this.size-1);
}

OVER 删除操作终于结束了。

思考:当成功的删除了一个元素,size的长度也减少了1,但是从图上可以看到size的指向也是有个元素的,我们该如何处理它?

六、编写更改操作

set(int index, int e):int,按指定索引位置重新设置元素,返回原来的元素。

public void set(int index,int e) {
if(index<0 || index >=this.size){
throw new IllegalArgumentException("set failed. the index illegal");
}
int res = this.data[index];
this.data[index] = x;
return res;
}

七、编写查询操作

find(int index):int,按指定索引位置查询元素。

public int find(int index) {
if(index<0 || index >=this.size){
throw new IllegalArgumentException("set failed. the index illegal");
}
return this.data[index]
}
findIndex(int e):int,按指定元素查询元素的值,查询成功返回索引位置,查询失败返回-1。
public int findIndex(int e) {
for(int i=0;i
if(this.data[i] == e) {
return i;
}
}
return -1;
}

八、编写其他操作

isEmpty():boolean,判断数组是否为空。

public boolean isEmpty() {
return this.size == 0;
}
contains(int e):boolean,判断数组是否包含指定元素。
public boolean contains(int e) {
return this.findIndex(e)!=-1 ? true : false
}

因为contains 和 findIndex 这两个方法逻辑基本一致的,只是返回值不一样。

所以这里直接调用findIndex方法,再做判断就行了。

getSize():int, 获取数组实际长度。

public int getSize() {
return this.size;
}
getCapacity():int, 获取数组容量。
public int getCapacity() {
return this.data.length;
}

这里再次提醒一下size和capacity的区别

size: 指的是数组已经存放了多少个数据

capacity:指的是数组可以存放多少个元素

九、重构代码,实现支持所有引用类型

在JAVA里要想实现支持所有引用类型的功能,就要使用泛型,泛型就是一种代码“模板”,可以支持所有的引用类型。

public class Array {
private int size;
private E[] data;
public Array(){
this(10);
}
public Array(capacity) {
if(capacity < 1) {
throw new IllegalArgumentException("create array failed. this capacity must greater than 0");
}
this.size = 0;
this.data = (E[])new Object[capacity];
}
}

使用泛型很简单,在类名后面加上,E是可以任意的,一般取T、E这些名字。然后在需要改动的地方加下说明,就不一一贴上代码了。注意一:泛型在JAVA里是不能通过new关键字实例化对象的,这时候就必须实例化Object再通过强制类型转换来创建初始化data数组;注意二:因为之前数组的元素是基本类型int,现在变成了引用类型了,那么在比较的时候就不能用==了,需要使用equals,如果使用了自定义的数据类型,还需要自己覆写equals方法。其他修改的地方基本一致,把之前int类型的元素,修改为E类型就行了。

十、动态数组

在前言的时候,描述过Java数组的缺点数组的长度不可变,这也算是数组的缺陷吧。没关系,下面将动态的改变数组容量,来实现动态数组。

思考:一直都在强调数组的长度不可变?那又如何改变数组的容量呢?那不是自相矛盾吗?

数组的容量不能改变这个是事实,这里说改变数组容量其实描述的不太对,可以这么理解,data数组指向了另一个容器。如果还不理解,没关系,看一下下面这幅图。

动态数组

从图上可以看到,第一次初始化数组的时候,data指向一个空数组(size==0);当data数组已经满了(size==capacity),数组不得不扩容,那么扩容的原理就是重新实例化一个更大容量的数组(原数组的两倍),把原数组的元素拷贝到新数组里,再让data指向了新数组,这样就实现了动态数组扩容。

相信看完上面的解析,对动态数组有一定的了解了,只不过上面只是动态数组的扩容,有扩充容量自然有缩小容量。

思考:扩充容量是发生在数组已经满的阶段,那么缩小容量该发生哪个阶段呢?

下面看看扩容的实现代码

public void resize(int capacity) {
E newData = (E[])new Objejct[](capacity)
for(int i=0; i
newData[i] = this.data[i];
}
this.data = newData;
}
// 修改add方法如下
public void add(int index,int e) {
if(index<0 || index>size) {
throw new IllegalArgumentException("add failed. the index illegal");
}
if(this.size == this.data.length)
this.resize(2*this.data.length);
for(int i=size-1; i>=index; i--) {
this.data[i+1] = data[i];
}
this.data[index] = e;
size++;
}

注意:还是泛型这个问题,泛型类型是不能实例化的,需要强制类型转换。

十一、总结

到现在为止,这一期的内容基本结束了,虽然完成了思维导航的所有实现,我觉得也有不足的地方,比如时间复杂度的分析并没有提到。

作者大三🐕一枚,虽然之前也接触过Java(已经忘记了),但是从来没有像这样写文章记录自己的学习过程,因为最近在学习一个Java全栈课程,想着以后也想往这方向发展,所以在这里立个flag,坚持每月更新一遍笔记。