今天一个小伙伴提出一个细节问题,即ArrayList的toArray(T[] a)中的最后一个判断没有必要。
由于出于对官方JDK代码的莫名的权威性的信任,以及曾经隐约看过注释有点印象,决心排查一下。
这个问题虽然看似难度不大,但是本文将介绍一个学习源码的法宝,另外我们看看JDK的API编写者的良苦用心,最后总结一下这种思想。
先看源码
java.util.ArrayList#toArray(T[]) JDK8版本
public 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;
}
我们先写个测试类
@Slf4j
public class ArrayListTest {
public static void main(String[] args) {
ArrayList 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呢??
我们看另外一个例子
@Slf4j
public class ArrayListTest {
public static void main(String[] args) {
ArrayList 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
能不能明白点什么呢??
如果不能我们再改造一下
@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 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元素,这可以帮助你判断集合元素的长度。
因此如果我们是上游,这个集合并不在我们这里,我们借助这个细节来判断数据的大小。
代码如下:
@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 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,以便让垃圾回收器及时回收这个对象),后续新增的时候会覆盖这个位置。
能够在读源码或者看文章的时候有各种疑问对学习很有帮助。
但是当我们怀疑一个非常成熟的框架时,尽量先去源码中看看注释是不是另有深意,然后核实自己写的代码是不是姿势不对。
另外我们通过一个细节,看到了JDK的一些类的作者写代码的时候都不是瞎写的,很多细节的处理都是很用心的。
另外我们的看源码(注释)大法,本地Demo大法一定要掌握。
还有我们随着学的知识越来越多,我们应该尝试把知识串起来,这样找到知识的共性,理解起来就更容易了,更容易从思想层面去掌握知识而不是仅仅停留在用法,学习新的东西也会更快。
我们要尝试掌握思想,而不仅仅是某个具体的技术点,才更容易融会贯通,学以致用。