前言
写过一些程序,搞过一些项目我们或许会发现,数组其实还是很牛逼的,Java中的集合框架就是用到了数组的封装,比如Arraylist使用到了Object数组作为底层进行了封装,再比如String这个类底层使用了char数组进行了封装,这些东西在我们开发中可谓是屡见不鲜。俗话说万丈高楼平地起,数组作为这些东西的基石我们有必要来探索下。(以下都是围绕自定义数组来搞事的)
探究内容
科普
数组基础性的东西我们就不在这唠叨了,今天我们就看一些数组的一些重要的概念:
1 length(capacity)
length我们都知道就是数组的长度,代表数组当前最大容量。他也是基本类型数组中的唯一一个成员变量,其实在一些封装的集合框架中使用capacity(容量),他俩都是一个概念,都代表当前数组的容量。
2 size(自定义数组中)
- 数组的大小,代表当前容器中存的真实元素数。
- 代表最后一个有效元素下一位置。
size为什么可以代表最后一个有效元素下一位置?
如上文的科普中的图片中,数组中存在2个元素,则第一个元素为data[0],最后一个有效元素为data[1],最后一个有效元素下一位置元素为data[size]
3 数组的语义
- 如上文假如数组具备语义data数组存的就是学生分数时,想知道成绩时data[索引] 就获得了当前索引的成绩。如此我们知道数组具有查询快的优点(可以与链表比较,后续会探究原因)
数组有语义那我们使用就简单了,通过索引查询,通过索引修改,简直是so easy。
- 数组没语义时就产生一些问题了,还是上图,data[2],data[3]…是没有意义的,我们是不允许操作这些位置的(面向程序端的程序员来说)。这些索引的删除、修改数据是不允许的。
我们的动态数组就是研究数组的没有语义情况
基本数组的封装
基本数组我们就以int 类型为例封装一个可以存取int类型的数组,来探讨一下数组底层是如何封装的。同时本部分也是动态数组,通用动态数组的基础。
1 类的设计
package array;
/**
* Create by SunnyDay on 2019/01/29
*/
public class Array {
private int[] data; // 数组
private int size;// 数组大小
// capacity (数组的容量) 与size的区别: size代表数组内存储元素的数量,capacity 能存储元素的最大量,
// capacity 作用等同length
/**
* @param capacity 根据传入的容量声明数组大小
*/
public Array(int capacity) {
data = new int[capacity];
}
/**
* 默认数组大小
*/
public Array() {
this(10);
}
}
我们自定义的具有int功能的数组就叫Array,中间有两个成员。可以看出我们首先设计了一个参数的构造,当你声明对象时是必须传入一个int型的容量的,然而当你不传参数(使用无参数构造)时我们默认的有数值的。这些都是常识接下来我们看看一些方法的设计。
2 增删改查
容器的操作无非是增删改查,接下来我们就具体探索增删改查的设计
2.1 增的api设计
- addLast
- add
- addFirst
/**
* 向数组的最后一个元素之后添加元素
*/
public void addLast(int e) {
if (size == data.length) {
throw new IllegalArgumentException("addLast element fail ,Array is full.");
}
data[size] = e;
size++;
}
addLast 往数组末尾添加元素,添加前我们首先判断下数组是否满,满了抛异常,反之添加元素,数组大小增加。
/**
* 数组任意位置添加元素
*
* @param index 要插入的索引
* @param e 要插入的元素
*/
public void add(int index, int e) {
// 首先还是检查数组是否满了
if (size == data.length) {
throw new IllegalArgumentException("add element fail ,Array is full.");
}
// 1、上面判断了size==arr.length的状况,要想插入元素位置合法,能够插入元素,size<arr.length即可
// 2、最不理想的状况就是还能插入一个元素,此时size=arr.length -1
// 3、这时index==size时就是插入末尾(size代表当前有效元素下一位的索引)
// 4、index>size就是插入位置不合法了,超出了数组的界限。
if (index < 0 || index > size) {
throw new IllegalArgumentException("add element fail ,require index>0 or index<=size");
}
// 数据先向后挪,再添加。 否则会发生数据覆盖
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
add往指定位置添加数据,添加数据时我们还是要考虑意外因素的,比如数组是否满了,添加时索引是否合法。这些处理过后我们就看添加的思路了:
1 遍历要插入索引位置元素及其以后的有效元素
2 插入位置元素及后面元素向后移动一位
ps:移动顺序从后向前否则数据会覆盖丢失
我们遍历的元素为6,5 而6表示为data[size-1],5为插入位置index,表示为data[index],故循环条件便出来了,然后再进行移位。
有了插入指定位置等方法我们就可以复用其代码了,修改addLast。
size代表末尾索引,也就是当前索引,正好应用使代码简洁多了。
public void addLast(int e) {
add(size, e);
}
addFirst和addLast类似复用就行(参考附录源代码)
2.2查、修,的设计
/**
* 根据索引查询元素
* @param index 查询的索引
* @return data[index] 直接返回元素
* 静态数组必须开辟一定空间 没使用的不让访问,这就是直接返回元素,而不是返回静态数组的原因
* 好处:用户永远只能查询已经使用空间上的数据
* */
public int get(int index){
if (index<0||index>size){
throw new IllegalArgumentException("get failed index is illegal");
}
return data[index];
}
/**
* 元素的修改
* @param index 修改的索引
* @param e 修改为的元素
*
* */
public void set(int index,int e){
if (index<0||index>size){
throw new IllegalArgumentException("set failed index is illegal");
}
data[index] = e;
}
参看代码不在讲解,你懂得哈哈!!!
2.3 删 的api设计
- remove
- removeFirst
- removeLast
- removeElement
/**
* 按照索引删除元素
* @param index 索引
* @return int type 返回删除的元素
* */
public int remove(int index){
if (index<0||index>=size){
throw new IllegalArgumentException("remove failed index is illegal");
}
int tempElement = data[index];
// index+1<=size
for (int i=index+1;i<size;i++){
data[i-1] = data[i];
}
size--;
return tempElement;
}
删除的核心思路:
如图我们要删除索引为index(2)的元素,此时我们要遍历索引为index以及他后面有效的元素,让他们逐个向前移即可。
data[index]后一个元素为data[index+1],当循环到size位置终止,所以遍历的条件就出来了。
ps:删除时元素要从前开始移动哦
removeFirst,removeLast参考源码嘿嘿!!!
以上是根据索引删除元素的,当然我们也可以根据元素名删除元素,这就是removeElement的功能,其实其内部也是根据索引删除的(参考源码)
通用数组
有了基本数组的设计思路,我们再运行泛型就设计出了通用数组(全部源码)
package array;
/**
* Create by SunnyDay on 2019/02/01
*/
public class UseGeneric <E>{
private E[] data; // 数组
private int size;// 数组大小
/**
* @param capacity 根据传入的容量声明数组大小
*/
public UseGeneric(int capacity) {
// 由于java 不支持泛型数组 我们可以使用Obj 在强转
data = (E[]) new Object[capacity];
}
/**
* 默认数组大小
*/
public UseGeneric() {
this(10);
}
/**
* 返回数组大小
*/
public int getSize() {
return size;
}
/**
* 返回数组容量
* length java数组的唯一属性数组长度
*/
public int getCapacity() {
return data.length;
}
/**
* 数组判断空
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 向数组的最后一个元素之后添加元素
*/
public void addLast(E e) {
add(size, e);
}
/**
* 数组任意位置添加元素
*
* @param index 要插入的索引
* @param e 要插入的元素
*/
public void add(int index, E e) {
// 首先还是检查数组是否满了
if (size == data.length) {
throw new IllegalArgumentException("add element fail ,Array is full.");
}
// 判断插入数据的合法性(数组中数据是紧密排列,索引大于零)
if (index < 0 || index > size) {
throw new IllegalArgumentException("add element fail ,require index>0 or index<=size");
}
// 数据先向后挪,再添加。 否则会发生数据覆盖
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
/**
* 数组的头部添加元素(复用上面代码)
*/
public void addFirst(E e) {
add(0, e);
}
/**
* 根据索引查询元素
* @param index 查询的索引
* @return data[index] 直接返回元素
* 静态数组必须开辟一定空间 没使用的不让访问,这就是直接返回元素,而不是返回静态数组的原因
* 好处:用户永远只能查询已经使用空间上的数据
* */
public E get(int index){
if (index<0||index>size){
throw new IllegalArgumentException("get failed index is illegal");
}
return data[index];
}
/**
* 元素的修改
* @param index 修改的索引
* @param e 修改为的元素
*
* */
public void set(int index,E e){
if (index<0||index>size){
throw new IllegalArgumentException("set failed index is illegal");
}
data[index] = e;
}
/**
* 数组中是否包含此元素
* @param e 元素
* @return boolean 是否包含
* */
public boolean contain(E e){
for (int i = 0; i < size; i++) {
if (e.equals(data[i])){
return true;
}
}
return false;
}
/**
* 查找元素所在的索引(第一次出现的索引)
* @param e 元素
* @return i 索引 ,-1 异常退出(没有元素)
* */
public int findIndex(E e){
for (int i = 0; i < size; i++) {
if (e.equals(data[i])){
return i;
}
}
return -1;
}
/**
* 按照索引删除元素
* @param index 索引
* @return int type 返回删除的元素
* */
public E remove(int index){
if (index<0||index>=size){
throw new IllegalArgumentException("remove failed index is illegal");
}
E tempElement = data[index];
for (int i=index+1;i<size;i++){
data[i-1] = data[i];
}
size--;
data[size] = null;
return tempElement;
}
/**
* 删除第一个元素
* */
public E removeFirst(){
return remove(0);
}
/**
* 删除最后个元素
* */
public E removeLast(){
return remove(size-1);
}
/**
* 删除指定的元素
*
* 复用查找索引的方法
* 本质通过索引删除元素
*
* 只删除一个元素 存在相同时(可以自定义接口在实现)
* */
public void removeElement( E e){
int index = findIndex(e);
if (index!=-1){
remove(index);
}
}
// 加注解的好处 防止覆盖出错,当父类没有此方法时报错
@Override
public String toString() {
// System.out.println(Arrays.toString(data)); 默认会把没有的元素默认为0
// 自定义 封装
StringBuilder sb = new StringBuilder();
sb.append(String.format("Array size = %d ,capacity = %d\n", size, getCapacity()));
sb.append("[");
// 数字中的有效元素遍历
for (int i = 0; i < size; i++) {
sb.append(data[i]);
// 不是最后一个元素时拼接
if (i != size - 1) {
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
注意点:
1 泛型不支持泛型数组(boject的替换)
2 contain 等方法中equals的替换(tip:客户端程序员自定义对象时要自己实现equals)
3 remove中引用的回收 (基本类型不考虑,但是引用类型要了考虑)
通用动态数组组
我们都知道普通的数组都是静态的,一旦我们初始化之后,数组容量就定下了,这就产生了弊端,假如我们申请了10容量的数组,我们使用了1个,后面不使用的就造成了浪费。假如我们申请了容量为100的数组,删除了90个这些删除的空间不再使用同样造成了浪费。这时我们就设计动态数组。
扩容思路:我们再设计个大的数组,拷贝当前的数组,再添加即可。
*/
public void add(int index, E e) {
// 首先还是检查数组是否满了
// 判断插入数据的合法性(数组中数据是紧密排列,索引大于零)
if (index < 0 || index > size) {
throw new IllegalArgumentException("add element fail ,require index>0 or index<=size");
}
if (size == data.length) {
// todo 动态扩充 参考ArrayList的扩容倍数
resize((int) (1.5 * data.length));
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
当数组满时我们扩充
/**
* 重置
*/
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
// 复制
newData[i] = data[i];
}
// 更改引用
data = newData;
}
缩容的原理和扩容一样,删除到一定个数时缩小容器容量。
/**
* 按照索引删除元素
*
* @param index 索引
* @return int type 返回删除的元素
*/
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("remove failed index is illegal");
}
E tempElement = data[index];
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;
data[size] = null; // 引用制空
// 当数组元素减少过多时达到容量一半时 开始动态缩小容量
if (size==data.length/2){
resize(data.length/2);
}
return tempElement;
}
源码下载:https://github.com/sunnnydaydev/DataStructure
小结:
通过一段短途的数组之旅,相信我们对数组的设计有所了解了,本文中只是简单地进行了一下探讨,诸如删除你会发现只能删除第一个元素(按照元素删除,数组中存在多个相同的时),查找元素索引,只能查找第一次出现的,这些弊端我们想要实现解决只需自己添加方法即可。
The end!!!
书山有路勤为径,学海无涯苦作舟。