您的位置:首页 > 编程语言 > Java开发

谈看源码大法和JDK的精良设计从ArrayList的toArray的一个细节讲起

w605283073 2019-06-15 17:09 155 查看 https://blog.csdn.net/w6052830

一、背景

今天一个小伙伴提出一个细节问题,即ArrayList的toArray(T[] a)中的最后一个判断没有必要。

由于出于对官方JDK代码的莫名的权威性的信任,以及曾经隐约看过注释有点印象,决心排查一下。

这个问题虽然看似难度不大,但是本文将介绍一个学习源码的法宝,另外我们看看JDK的API编写者的良苦用心,最后总结一下这种思想。

 

二、源码

先看源码

java.util.ArrayList#toArray(T[])  JDK8版本

[code]    public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

我们先写个测试类

[code]@Slf4j
public class ArrayListTest {

public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 6; i++) {
arrayList.add(i);
}

Integer[] integers = new Integer[8];

Integer[] integers1 = arrayList.toArray(integers);
log.debug(JSON.toJSONString(integers1));
}

}

我们在源码中打断点

运行结果:

16:19:07.342 [main] DEBUG com.chujianyun.common.list.ArrayListTest - [0,1,2,3,4,5,null,null]

那问题来了,既然这个数组是8个元素的空数组,为啥这个还要将集合元素放到数组里后面还专门设置null呢??

我们看另外一个例子

[code]@Slf4j
public class ArrayListTest {

public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 6; i++) {
arrayList.add(i);
}

Integer[] integers = new Integer[8];

Integer[] integers1 = arrayList.toArray(integers);
log.debug("第一次打印" + JSON.toJSONString(integers1));

arrayList.clear();
for (int i = 0; i < 3; i++) {
arrayList.add(i);
}
integers1 = arrayList.toArray(integers);
log.debug("第一次打印" + JSON.toJSONString(integers1));
}

}

我们清空集合元素后,添加3个元素,然后复用之前的数组看看效果,第二次转成数组时,index=3的元素被置为了null(not showing null elements,表示为null的就不展示了)

最终的结果:

16:26:08.226 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第一次打印[0,1,2,3,4,5,null,null]
16:27:50.602 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第一次打印[0,1,2,null,4,5,null,null]

能不能明白点什么呢??

如果不能我们再改造一下

[code]@Slf4j
public class ArrayListTest {

private static final int MAX_LENGTH = 10;

public static void main(String[] args) {

Integer[] integers = new Integer[MAX_LENGTH];
for (int i = 0; i < 5; i++) {
log.debug("第:{}轮的结果:{}", i, JSON.toJSONString(nextStage(integers)));
}

}

/**
* 下游接口使用了集合转数组,且保证前面集合元素都不为null
*/
private static Integer[] nextStage(Integer[] integers) {
ArrayList<Integer> arrayList = new ArrayList<>();
int random = RandomUtils.nextInt(3, MAX_LENGTH);
log.debug("随机数-->{}", random);
for (int i = 0; i < random; i++) {
arrayList.add(i);
}
return arrayList.toArray(integers);
}
}

我们看下输出

16:38:59.836 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->4
16:38:59.917 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第:0轮的结果:[0,1,2,3,null,null,null,null,null,null]
16:38:59.917 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->6
16:38:59.918 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第:1轮的结果:[0,1,2,3,4,5,null,null,null,null]
16:38:59.918 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->9
16:38:59.918 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第:2轮的结果:[0,1,2,3,4,5,6,7,8,null]
16:38:59.918 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->5
16:38:59.918 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第:3轮的结果:[0,1,2,3,4,null,6,7,8,null]
16:38:59.918 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->3
16:38:59.918 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第:4轮的结果:[0,1,2,null,4,null,6,7,8,null]

如果我们保证返回的集合里都没null,如果我们复用数组的话,会发现第一个null就是我们想要的数据的分界线。

我们是不是可猜测到可以用这个null来判断下游数据的边界?

三、探究

开启我们的看源码(注释)大法

大概翻译一下:

如果传入的数组长度大于集合的长度,那么集合最后一个元素后将设置一个null元素。

如果你的集合元素不含null元素,这可以帮助你判断集合元素的长度。

因此如果我们是上游,这个集合并不在我们这里,我们借助这个细节来判断数据的大小。

代码如下:

[code]@Slf4j
public class ArrayListTest {

private static final int MAX_LENGTH = 10;

public static void main(String[] args) {

Integer[] integers = new Integer[MAX_LENGTH];
for (int i = 0; i < 5; i++) {
Integer[] nextStage = nextStage(integers);
log.debug("第:{}轮的结果:{}", i, buildArrayInfo(nextStage));
}
}

/**
* 用第一个null作为数据的边界判断
*/
private static String buildArrayInfo(Integer[] nextStage) {

if (nextStage == null || nextStage.length == 0) {
return "";
}
StringJoiner stringJoiner = new StringJoiner(",");
for (Integer integer : nextStage) {
if (integer == null) {
break;
}
stringJoiner.add(integer.toString());
}
return stringJoiner.toString();
}

/**
* 下游接口使用了集合转数组,且保证前面集合元素都不为null
*/
private static Integer[] nextStage(Integer[] integers) {
ArrayList<Integer> arrayList = new ArrayList<>();
int random = RandomUtils.nextInt(3, MAX_LENGTH);
log.debug("随机数-->{}", random);
for (int i = 0; i < random; i++) {
arrayList.add(i);
}
return arrayList.toArray(integers);
}
}

结果:

16:49:37.962 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->3
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第0轮的结果:0,1,2
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->7
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第1轮的结果:0,1,2,3,4,5,6
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->6
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第2轮的结果:0,1,2,3,4,5
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->5
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第3轮的结果:0,1,2,3,4
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 随机数-->5
16:49:37.971 [main] DEBUG com.chujianyun.common.list.ArrayListTest - 第4轮的结果:0,1,2,3,4

那么问题又来了,如果没有那个null的覆盖,我们每次都new一个数组传进去不就好了?

设计API总不能强制你必须传一个空数组吧?如果你想复用数组参数,第二次的结果比第一次的少,边界怎么判断?

很多人会说我用集合的长度啊,看上面的场景,如果集合在下游你怎么获得集合的长度??

那还有一个问题,为啥不把后面的都置空呢?

答案是没必要,给你一个边界,你知道前面的都是你要的就好了,后面的置空没有意义,浪费时间。

这点和windows系统删除文件很像,它的删除是标记删除,标记这个文件的区域已经删除了,新的文件直接覆写这个区域就好了,完全没必要将这个区域都置空,避免了一些不必要的工作,节省了时间,这也侧面也为数据的恢复提供了可能性。

另外《开发方向校招准备的正确姿势,机会留给有准备的人》所推荐的《数据结构实用教程(Java语言描述)》一书中关于ArrayList的实现那里移除一个元素后,size-1后并没有删除最后一个元素(JDK的ArrayList源码的remove函数,将这个元素置为null,以便让垃圾回收器及时回收这个对象),后续新增的时候会覆盖这个位置。

四、Learn More

能够在读源码或者看文章的时候有各种疑问对学习很有帮助。

但是当我们怀疑一个非常成熟的框架时,尽量先去源码中看看注释是不是另有深意,然后核实自己写的代码是不是姿势不对。

另外我们通过一个细节,看到了JDK的一些类的作者写代码的时候都不是瞎写的,很多细节的处理都是很用心的。

另外我们的看源码(注释)大法,本地Demo大法一定要掌握。

还有我们随着学的知识越来越多,我们应该尝试把知识串起来,这样找到知识的共性,理解起来就更容易了,更容易从思想层面去掌握知识而不是仅仅停留在用法,学习新的东西也会更快。

我们要尝试掌握思想,而不仅仅是某个具体的技术点,才更容易融会贯通,学以致用。

五、相关参考

《如何高效学习和阅读源码?》

《几个本地搭建练习项目来学习的小技巧分享》

《开发方向校招准备的正确姿势,机会留给有准备的人》

 

创作不易,如果觉得本文对你有帮助,欢迎点赞,欢迎关注我,如果有补充欢迎评论交流,我将努力创作更多更好的文章。

另外欢迎加入我的知识星球,知识星球ID:15165241 一起交流学习。

https://t.zsxq.com/Z3bAiea  申请时标注来自CSDN。

 

 

标签: