谈看源码大法和JDK的精良设计从ArrayList的toArray的一个细节讲起
一、背景
今天一个小伙伴提出一个细节问题,即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。
- ArrayList源码解析(jdk1.6)
- jdk1.8源码学习与思考--ArrayList
- ArrayList源码分析(基于JDK1.6)
- 开源IOT——一个最小的物联网系统设计方案及源码
- JDK源码解析集合篇--ArrayList全解析
- 给jdk写注释系列之jdk1.6容器(1)-ArrayList源码解析
- JDK 源码设计 时间换空间 &amp; 空间换时间
- ArrayList源码分析(jdk1.8)
- ArrayList源码分析(基于JDK1.6)
- [置顶] 学习JDK源码:编程习惯和设计模式
- OpenJDK源码研究笔记(十五):吐槽JDK中的10个富有争议的设计
- JDK源码学习系列04----ArrayList
- Jdk1.8 Collections Framework源码解析(1)-ArrayList
- [Java]JDK1.8 ArrayList源码剖析(二)
- OpenJDK源码研究笔记(十五):吐槽JDK中的10个富有争议的设计
- 结合JDK源码看设计模式——享元模式
- ArrayList源码分析(基于JDK1.6)
- ArrayList源码阅读笔记(基于JDk1.8)
- JDK 源码 阅读 - 2 - 设计模式 - 创建型模式
- 【集合框架】JDK1.8源码分析之ArrayList(六)