原来你是这样的数组,终于学会了

Posted 庆哥Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原来你是这样的数组,终于学会了相关的知识,希望对你有一定的参考价值。

数组,非常常见的一种数据类型,在很多编程语言中,都有数组的实现,也都是非常重要的一种复合数据类型,不同的编程语言对数组的实现略有不同,比如C/C++对数组的实现就和Java有所不同!

一个问题

其实我一直觉得,要想把Java学的比较有意思,学的更好,最好学学C/C++,最不济,也要懂C语言啊,不过C语言属于上大学时候的事了,虽然大家都说语言一通百通,但是没有经过系统化的学习,我还是觉得自己并不会它!

于是乎,这周准备把C语言系统化的学习一遍,然后上架到咱们的Java自学网站上继续为大家补充Java知识体系!


今天主要想和大家了一个关于数组是否新建的问题,在前几天的数据结构直播训练营中有人提出了这么一个问题:

大概意思就是,我现在有一个数组,我删除了一个元素,删除这个元素之后,后面的元素要全部往前,也就是左移一位,那么这些操作是在同一个数组中完成的,还是在不同的数组中完成的呢?

大家怎么看?

数组不可变?

我们当时的回答是新开辟一个数组,也就是在不同的数组中完成的,但是这样真的正确吗?有没有什么问题?

对于数组,我们都了解的一点,那就是 一旦创建完成,就不可改变 🆗,这就是我们需要注意的点,我们需要搞清楚,这个不可变到底是怎么回事?不可变的到底是啥?

比较String

我们知道在Java中有个比较特殊的引用数据类型,就是Java字符串String,对于它而言,可以算的上是真正的不可变,你只要创建一个String出来,那它就是不可变的,而且这个不可变是那种你没法改动的,你要是想进行任何的改动,都需要新创建一个String出来,你是没法对之前的做操作,对String字符串对象的任何改变都是建立在一个新的字符串对象之上!

比如我们这里新建一个字符串“hello”:

String str = "hello";

这个时候就产生了一个字符串对象hello,对应字符串引用str,此时,你说,我想对这个hello做点啥,比如这样:

String str = str + "java"; 

此时这个字符串引用str就指向了新的字符串对象:

代码就是这样的:

在上述的操作中,本身是想着在字符串hello后面追加一个java,实际上是新建了一个字符串对象hellojava而不是在原有的hello后面进行追加,因为字符串对象是不可变的,无法修改!

而Java中的数组则不一样,Java中的数组,我们也说是不可变,但是指的是数组的长度是固定不变的,这个时候并不是说,我们不能对该数组进行操作,一旦操作,就会创建一个新的数组出来,并不是这样,数组的不可变和字符串String不可变是完全不同的!

划重点:数组的不可变是长度不可变

这点要理解清楚了,也就是对数组而言,我们声明一个数组的时候,要声明数组的长度,比如我这里声明一个长度为5的整型数组

int[] array = new int[5];

这里声明的时候就要指定数组长度,也就是5,此时也就是在内存中占据着这么一个5个int大小的空间,如下:

此时数据默认赋值为0,总共占据五个int大小块,此时数组声明完成,大小已经固定下来了,也就是说此时的这个数组的固定不变指的是这个长度固定不变:

也就是此时创建的一个大小为5的整型数组已经在内存中分配好空间了,总长度是5,那我们可以对这个数组进行相应的一些操作吗?比如删除其中的一个元素,或者插入一个元素?

数组的操作

不能插入数据

我们先看插入操作,首先要理解这个插入,比如我们要插入一个新的数据,如下所示:

这个时候你看,能不能插入,比如上述,可以把新数据6插入到array[2]array[3]两个数据之间吗?

显然是不可以的,因为数组的长度是固定不变的,一旦初始化完成,长度将不可改变!

因为如果你插入一个元素的话,那数组的长度就会增加1,这就改变了数组的长度,这就不行了,所以对于数组,你没法插入数据,不允许,因为这个会改变数组大小!

可能你会疑问了?不能插入数据?不对吧,那对于像ArrayList这样的集合容器,底层不就是数组,不也可以add数据?

可以覆盖数据

这里就要分清楚了,数组肯定是不能插入数据的,因为这个会把数组的长度给改变,但是我们添加数据的另一种方式是啥,可以覆盖,也就是这样的:

也就是把array[2]这个位置的数据给覆盖了,成了这样:

也就是说,相对数组操作,增加数据?当然可以,但是要注意,你可以覆盖原有位置的数据,但是不能插入改变我数组原有的长度!

所以对于数组的添加数据,实际上是覆盖操作

那比如我现在要往这个数组中添加数据,如下:

int[] array = new int[5];
array[0] = 1;
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;

那本质上是啥?是不是就是覆盖掉数组原位置上默认初始化的0值,最后也就成了这样:

那你说,我再覆盖一个行不:

array[5] = 6;

当然不行,为啥?因为数组长度是5,索引最大就是array[4],不存在的你赋值,那不是赋了个寂寞,那你说,我现在的确还有数据需要存储啊,但是数组长度就是5,不够了啊,咋办,没办法,只能新建一个更大的数组,然后将原来的数组数据拷贝过来,然后再继续赋值,比如上述数组大小已经不够用了,我们需要创建一个新的更大的数组:

int[] newArray = new int[10];

这样我们就创建了一个更大的数组,接下来需要把我们之前的数组里面的数据都给拷贝过来,比如这样:

public static void main(String[] args) 
        int[] array = new int[5];
        array[0] = 1;
        array[1] = 2;
        array[2] = 3;
        array[3] = 4;
        array[4] = 5;
        System.out.println("原数组array-------------------");
        for (int i : array) 
            System.out.println(i);
        
        System.out.println("新数组newArray-------------------");
        int[] newArray = Arrays.copyOf(array, 10);
        for (int i : newArray) 
            System.out.println(i);
        
    

结果如下:

此时也就是将原来长度为5的数组中的全部数据复制到这个新的长度为10的数组中了,我们此时还可以进行如下的操作:

也就是将新创建的长度为10的数组对象引用赋值给原先的array,这样一来,原先长度为5的数组对象就会丢失引用,最终会被GC以达到释放空间的目的!

扩容新建数组

以上其实就是属于一种扩容,就是原先的数组大小不够用了,重新生成一个更加大的数组来存放原先的数据,将原先的数据全部赋值过来,让原先的数组丢失引用从而释放内存!

一旦数组发生扩容,就会创建新的数组对象来承接数据,以上我们演示的代码中使用了如下的方法进行数组拷贝:

Arrays.copyOf

这个就是数组工具类Arrays提供的一个拷贝方法,查看源码:

发现该方法实际上就是新建了一个指定大小的数组来承接原先数组的数据,所以,一旦数组大小不够了,唯一办法就是新建一个更大的数组,这样的操作肯定是在不同的数组上!

删除数据不新建数组

接下来我们看删除操作,也就是将数组中的元素删除,比如下面这样:

可以直接删除吗?当然是不行的,因为数组长度是不变的,如果你直接将该元素删除的话,数组长度就遭到了破坏,这是不行的,那该怎么操作去执行删除的命令?

其实还是覆盖:

对数组的操作,主要就是读取和更新,读取就是利用下标,而更新的本质其实就是覆盖,无论添加数据还是删除数据!

那这里是如何通过覆盖达到删除的呢?想必大家都清楚,就是把要删除的元素后面的元素统一向前挪动一位,也就是变成了这个样子:

这里要清楚这个向前挪动一位是怎么回事,其实就相当于4覆盖了原来的3达到了删除的目的,然后后面的5覆盖了前面的4,整体看着是向前挪动,但是要注意的是,末尾的这个下标为array[4]的值还是5,也就是说,删除操作,这种通过向前挪动覆盖的效果,最终会导致数组最后两位的数值是一样的!

**那这种代码该怎么实现呢?**新建一个比原先数组长度少一的新数组去承接?这个肯定是不行的,要不,每删除一个元素都要新建一个数组,这种做法太影响性能和浪费空间了!

正确做法就是在原有数组上去操作,如下代码实现:

 int[] array = new int[5];
        array[0] = 1;
        array[1] = 2;
        array[2] = 3;
        array[3] = 4;
        array[4] = 5;
        System.out.println("原数组array-------------------");
        for (int i : array) 
            System.out.println(i);
        
        System.out.println("删除索引为2的数据后数组----------");
        System.arraycopy(array,3,array,2,2);
        for (int i : array) 
            System.out.println(i);
        

这里的主要操作就是:

System.arraycopy(array,3,array,2,2);

这个数组拷贝的方法神奇之处就是它没有新建数组,而是在同一数组上进行数据拷贝,从而达到删除的效果,结果如下:

但是这样还没有实现真正的删除,这一点我们可以看集合容器ArrayList的巧妙实现!

ArrayList的巧妙实现

对于数组,它是不能进行数据的插入和删除操作的,说白了就是对于数组而言,你只可以读取,不能修改,而这里所说的修改主要指的就是数据的插入和删除,因为这两个操作会破坏数组的长度,而数组的长度一但确定就是不能更改的!

所以你看,数组的功能很简单,简单到啥都没有,所以插入啊,删除什么的方法想都别想,但是由于数组的特性,我们需要将其作为数据结构去存储数据,该怎么做?答案就是把数组封装起来,作为底层存储数据的结构,这里的经典例子就是java中的集合容器!

我们以Java集合中的ArrayList为例去举例说明,主要看看在其中它是如何实现数组的删除操作的,我们在之前通过自己写代码去删除数组中元素的时候,实际上是通过移动数组数据覆盖之前的数据来达到删除的目的,比如得出的是如下的效果:

但是我们想要的怎么样的呢?

比如说现在有个长度为5的数组,我们删除一个元素,我们希望得到的是啥?是不是长度由原来的5变成了4,因为我们删除了一个元素啊,长度理应减少1才对,也就是说,我原来的数组数据是1,2,3,4,5,现在我删除索引为2的数据,理应得到的应该是1,2,4,5才对,但是我们上述操作得到的却是1,2,4,5,5这明显不是我们所想的啊!

这个该怎么做?我们看下ArrayList是如何实现的,首先我们创建一个ArrayList:

那这个时候我们删除索引为2的数据,如下:

然后我们再看打印输出的效果:

这个是不是就是我们想要的效果,完美啊,那它这个是怎么实现的呢?我们查看remove的源码:

我们发现它也是使用这个方法进行的数据拷贝,也就是在原有数组上通过数据覆盖达到删除的目的,不过在ArrayList中能够完美的实现这种删除操作,还得益于它的巧妙设计!

因为本身ArrayList就是一个对数组做高度封装,数组的长度在ArrayList中成了size:

我们可以通过这个size获取数组的长度,而ArrayList的巧妙之处就在这里:

也就是说,每当删除一个元素,我就主动让size减少1,这样你得到的数组长度就会少一,你自然就会觉得数组中的元素被删除了**,但是实际上呢?这里只是这个size数值少一,而ArrayList的底层和这个数组的长度可并没有减少,还是原来,只不过给你的size的确是减少了,让你误以为数组减少了!**

而底层的数组长度还是原来的长度,相当于还是5,只不过现在你能拿到的size是4,此时对你来说这个数组的长度就是4,而你能拿到的ArrayList中的最后一个数据,其实是底层数组中的倒数第二个数据,也就是你能拿到的是这个数据:

而实际上数组的最后一个元素,也就是这个数据:

因为已经通过size减少1的方式放你拿到的数组长度是少一的,所以这个数据你是获取不到的,那获取不到怎么办呢?这里将其置为null:

也就是说,一但删除,底层数组,最后一个位置的数据就会空出来,而整体的size通过减一的方式让你看到,元素确实被删除了,而且数组长度也减少了,实际上,底层数组长度并没有减少,只是通过这种方式,让你误以为减少,而且你还真的拿不到最后一个元素,既然拿不到,那就用不到了,于是乎把它置为null,让GC回收,从而释放内存!

这个就是ArrayList对数组删除的一个巧妙设计,理解了这点,就可以加深我们对数组的理解学习!

ok,本文就分享到这里,旨在为大家理清数组的插入和删除操作,让大家对数组理解的更加深刻,如果有不同意见,欢迎评论留言讨论,一起进步!

以上是关于原来你是这样的数组,终于学会了的主要内容,如果未能解决你的问题,请参考以下文章

原来你是这样的数组,终于学会了

原来你是这样的数组,终于学会了

原来你是这样的PaaS!

英伟达:从图像中抽象出概念再生成新的图像,网友:人类幼崽这个技能AI终于学会了...

折腾一两天,终于学会使用grunt压缩合并混淆JS脚本,小激动,特意记录一下+spm一点意外收获

原来你是这样的setTimeout