掌握这些,ArrayList就不用再学了(上)

北城以北 提交于 2021-02-20 11:47:30

ps:一不小心又写万把字了,没办法,怕你们看不下去,分成了上下两部分!

关于ArrayList的学习

ArrayList属于Java基础知识,面试中会经常问到,所以作为一个Java从业者,它是你不得不掌握的一个知识点。😎

可能很多人也不知道自己学过多少遍ArrayList,以及看过多少相关的文章了,但是大部分人都是当时觉得自己会了,过不了多久又忘了,真的到了面试的时候,自己回答的支支吾吾,自己都不满意😥

为什么会这样?对于ArrayList这样的知识点的学习,不要靠死记硬背,你要做的是真的理解它!😁

我这里建议,如果你真的想清楚的理解ArrayList的话,可以从它的构造函数开始,一步步的读源码,最起码你要搞清楚add这个操作,记住,是源码😄

一个问题看看你对ArrayList掌握多少

很多人已经学习过ArrayList了,读过源码的也不少,这里给出一个问题,大家可以看看,以便测试下自己对ArrayLIst是否真的掌握:

请问在ArrayList源码中DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA是什么?它们有什么区别?

怎么样?如果你能很轻松的回答上来,那么你掌握的不错,不想再看本篇文章可以直接出门右拐(我也不知道到哪),如果你觉得不是很清楚,那就跟着我继续往下,咱们再来把ArrayList中那些重点过一遍!😎

你觉得ArrayList的重点是啥?

在我看来,ArrayList的一个相当重要的点就是数组扩容技术,我们之前学习过数组,想一下数组是个什么玩意以及它有啥特点。

随机访问,连续内存分布等等,这些学过的都知道,这里说一个似乎很容易被忽略的点,那就是数组的删除,想一下,数组怎么做删除?😏

关于数组删除的一些思考

关于数组的删除,我之前也是有疑惑,后来也花时间思考了一番,算是比较通透了,这里就提一点,数组并没有提供删除元素的方法,我们都是怎么做删除的?

比如我们要删除中间的一个元素,怎么操作,首先我们可以把这个元素置为null,也就把这个元素删除掉了,此时数组上就空出了一个位置,这样行吗?

当我们再次遍历这个数组的时候是不是还是会遍历到这个位置,那么就会报空指针异常,怎么办?是的我们可以先判断,但是这样的做法不好,怎么办呢?

那就是我们可以把这个元素后面的所有元素统一的向前复制,有的地方这里会说移动,我觉得不够合理,为啥?

复制是把一个元素拷贝一份放到其他位置,原来位置元素还存在,而移动呢?区别就是移动了,原本的元素就不存在了,而数组这里是复制,把元素统一的各自向前复制,最终结果就是倒数第一和第二位置上的元素是相同的。

此时的删除的本质实际上是要删除的这个元素的后一个元素把要删除的这个元素给覆盖了,后面依次都是这样的操作,可能有点绕,自己想一下。

所以就引出了数组的删除操作是要进行数组元素的复制操作,也就导致数组删除操作最坏的时间复杂度是0(n)。

为什么说这个?因为对理解数组扩容技术很有帮助!

数组扩容技术

上面我们谈到了关于数组的删除操作,我们只是分析了该如何去删除,但是数组并未提供这样的方法,如果我们要搞个数组,这个删除操作还是要我们自己写代码去实现的。

不过好在已经有实现了,谁嘞,就是我们今天的主角ArrayList,其实ArrayList就可以看作是数组的一个升级版,ArrayList底层也是使用数组来实现,然后加上了很多操作数组的方法,比如我们上面分析的删除操作,当然除此之外,还实现了一些其他的方法,然后这就形成了一个新的物种,这就是ArrayList。

本质上ArrayList就是一个普通的类,对数组进行的封装,扩展其功能

对于数组,我们还了解一点那就是数组一旦确定就不能再被改变,而这个ArrayList却可以实现自动扩容,有木有觉得很高级,其实也没啥,因为数组本身特性决定,ArrayList所谓的自动扩容其实也是新创建一个数组而已,因为ArrayList底层就是使用的数组。

我们的重点需要关注的是这个自动扩容的过程,就是怎么创建一个新的数组,创建完成之后又是怎么做的,这才是我们关注的重点。

接下来我们看两种数组扩容方式。

Arrays.copyof

不知道你使用过没,我们直接看代码:

public static void main(String[] args) {
int[] a1 = new int[]{1, 2};
for (int a : a1) {
System.out.println(a);
}
System.out.println("-------------拷贝------------");
int[] b1 = Arrays.copyOf(a1, 10);
for (int b : b1) {
System.out.println(b);
}
}

代码不多,很简单,看看输出结果你就明白了

ok,是不是很简单,知道这个简单用法就ok了,接下来看另外一种

System.arraycopy()

这个方法我们看看是个啥:

public static native void arraycopy(Object src,  int  srcPos,
Object dest, int destPos,
int length)
;

看见没,native修饰的,一般是使用c/c++写的,性能很高,我们看看这里面的这几个参数都是啥意思:

src:要拷贝的数组 srcPos:要拷贝的数组的起始位置 dest:目标数组 destPos:目标数组的起始位置 length:你要拷贝多少个数据

怎么样,知道这几个参数什么意思了,那使用就简单了,我这里就不显示了。

ps:以后复制数组别再傻傻的遍历了,用这个多香😄

以上两个方法都是进行数组拷贝的,这个对理解数组扩容技术很重要,而且在ArrayList中也有应用,我们等会会详细说。

下面咱们开始看看ArrayList的一些源码,加深我们对ArrayList的理解!

源码中的ArrayList

一般我们是怎么用ArrayList的呢?看下面这些代码:

 ArrayList arrayList = new ArrayList();
arrayList.add("hello");
arrayList.add(1);

ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("hello");

简单,都会吧,就是new一个出来,不过上面的代码我还想说明一个问题,当你不指定具体类型的时候是可以存储任意类型的数据的,指定的话就只能存储特定类型,为啥不指定可以存储任意类型?

这个问题不做解释,等会看源码你就明白了。

看看ArrayList的无参构造函数

一般我们看ArrayList的源码,都是从它的无参构造函数开始看起的,也就是这个:

new ArrayList();

好啦,走进去看看这个new ArrayList();构造函数长啥样吧。

    /**
* Constructs an empty list with an initial capacity of ten.
*/

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

咋一看,代码不多,简单,里面就是个赋值操作啊,有两个新东西elementData和DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这是啥?🤪

不着急,我们点进去看看

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access

这不就是Object数组嘛,好像还真是的,那transient啥意思?它啊,你就记住被它修饰序列化的时候会被忽略掉。

好了,除此之外,就是个数组,对Object类型的。

不好像有点区别啊,DEFAULTCAPACITY_EMPTY_ELEMENTDATA已经指定是个空数组了,而elementData只是声明,在new一个ArrayList的时候进行了赋值,也就是这样:

 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

咋样?明白了吧,之前不就说了嘛,ArrayList底层就是一个数组的,这里你看,new之后不就给你弄个空数组出来嘛,也就是说啊,你要使用ArrayList,一开始先new一下,然后给你搞个空数组出来。

啥?空数组?空数组怎么行呢?毕竟我们还需要用它存数据嘞,所以啊,重点来了,我们看它的add,也就是添加数据的操作。

看看ArrayList的add

  public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

就是这个啦,ArrayList不就是使用add来添加数据嘛,我们看看是怎么操作的,咋一看这段代码,让我们感到比较陌生的就是这个方法了

ensureCapacityInternal(size + 1);

这是啥玩意,翻译一下😂

确保内部容量?什么鬼,这里还有个size,我们看看是啥?

 private int size;

就是一个变量啊,我们再看看这段代码

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

尤其是

elementData[size++] = e;

知道了嘛?我们之前不是已经创建了一个空数组,不就是elementData嘛,这好像是在往数组里面放数据啊,不过不对啊,不是空数组嘛?咋能放数据,这不是前面还有这一步嘛

 ensureCapacityInternal(size + 1);

是不是有想法了,这一步应该就是把数组的容量给确定下来的,赶紧进去看看

   private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

就是这个了,这一步很重要:

 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

也好理解吧,就是先判断下现在这个ArrayList的底层数组elementData 是不是刚创建的的空数组,这里肯定是啊,然后开始执行

minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

留一个疑问,你们知道minCapacity 到底是个啥嘛?这可是很重要的,我们下回再说!

未完待续!

本文分享自微信公众号 - 编码之外(ithuangqing)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!