简单源码解读之猜想验证

简介: 简单源码解读之猜想验证

一、背景

最近有一个朋友问,为啥 ArrayList 空参构造方法和有参构造方法的参数为 0 所用的空元素数组不用同一个。

空参构造方法:

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

带初始容量的构造方法:

/**
 * Constructs an empty list with the specified initial capacity.
 *
 * @param  initialCapacity  the initial capacity of the list
 * @throws IllegalArgumentException if the specified initial capacity
 *         is negative
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

从源码中我们可以清晰地看到,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和 EMPTY_ELEMENTDATA 的为空数组。

那么为什么要这么写?

本文不仅仅解答这个问题,因为讲得再好你也只能知道一个具体问题,帮助是非常有限的q。

更重要的是分享几点好的看源码习惯,这才是学习的能力,可以解决未来更多问题。

二、分析

看源码的技巧有很多,主要分为两类,一类是思想类,一类是方法类。

所谓思想,其中比较重要的一点是:先猜想后验证。所谓方法,其中比较重要的几点是:看源码注释、看调用栈等。

当然还有很多其他不错的思想和方法,感兴趣可以通过本人的专栏学习。

2.1 猜想后验证

既然不一样,我们猜测他们可能并不是简单的作为空数组元素,可能会用来判断构造的来源(是空参构造函数还是带初始容量构造的)。

2.2 源码注释

既然两者不一样,那么构造函数上的注释或者两个数组上的注释应该有线索:

/**
 * Shared empty array instance used for empty instances.
 * 空 ArrayList 对象使用的共享的空数组实例
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 * 默认长度的空ArrayList 对象使用的共享空数组。
 * 我们将此数组和 EMPTY_ELEMENTDATA 分开是为了第一次添加元素时判断要扩容多少
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

通过注释我们就可以了解到为什么空参构造方法不使用 EMPTY_ELEMENTDATA 数组的原因。

那么第一次添加元素时真的用它来判断了吗?空参构造方法第一次添加元素时容量又是怎样变化的呢?

2.2 核实&验证

如果没提到第一次添加元素时用到,我们可以在 IDEA中找到用到该变量的地方,一一排查。

提到了我们可以直接找到添加元素的函数代码:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

寻找底层调用

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

调用

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

此时

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

因此我们知道,如果是添加第一个元素,则默认容量为 10。

还可以找到用到的地方在对应函数中打断点,写 DEMO 调用空参构造函数,添加一个元素查看调用栈即可。

import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo {

    public static void main(String[] args) {
        List<String> data = new ArrayList<>();
        data.add("a");
        System.out.println(data.toString());
    }
}

在 data.add("a") 处和打印语句处分别打断点,然后在调试窗口中执行表达式:

((ArrayList)data).DEFAULTCAPACITY_EMPTY_ELEMENTDATA == ((ArrayList) data).elementData

验证了此时的确为默认容量的空元素数据。

执行到打印语句处打断点,然后在调试窗口中执行表达式:

((ArrayList) data).elementData

可以看到添加第一个元素后,底层的 elementData 容量的确是 10.
在这里插入图片描述

当然也可以单步进入 add 函数研究更多细节。

三、思考

3.1 猜想验证

看源码建议一定要先猜想后验证。

就像看着答案做题一样,看着很对,看着很简单,其实并不会。

如果你直接看源码解读的文章或者源码,你以为自己都会,其实并没有真正理解。

先猜想后验证才能明确知道你在看源码之前理解的对不对,才能知道自己的想法和源码差距在哪里,才能对源码理解的更加深刻。

3.2 看注释

很多人不喜欢看注释,认为不重要。但是恰恰相反,真正源码看得好的同学很大一部分会非常重视注释。

尤其是代码非常复杂时,注释对我们理解代码有极大的帮助。

一般注释写的好,会将代码的意图,以及注意事项写的非常清楚。

有些人会说自己英语不好,然而,搞计算机的普遍英语都不是很突出,这些英语的词汇难度其实并不高。

另外有很多翻译软件,为啥不能去用呢?

如果你说你懒,你说这样比较麻烦,那啥也别说了,学技术一点苦都不想吃,一点麻烦都能接受还想学好,想想就好了。

3.3 DEMO & 调试

写DEMO 对代码进行调试是学习源码的一个重要手段。

四、总结

很多人读源码忽视注释,看源码侧重于记忆而不是理解。

很多人看源码能发现问题却不主动去研究问题,希望大家带着方法去练习看源码,练习分析问题,在才是提高学习能力,记住某个具体知识点用处并不大。

看会了很快就忘记本来就是人之常情,其次看着博客,看着源码理解本身就不可靠,容易遗忘,记住也很难迁移,先猜想后验证才能学到更多。

当然读源码还有很多技巧,可以通过本人的专栏学习(参见PC版左侧自定义模块部分)。

相关文章
|
7月前
1005 继续(3n+1)猜想
1005 继续(3n+1)猜想
49 0
|
7月前
|
测试技术
面试题8: 如何确定测试需求的关键场景和细节?
面试题8: 如何确定测试需求的关键场景和细节?
|
7月前
|
前端开发 测试技术
谈谈测试的专业性
谈谈测试的专业性
46 0
|
7月前
|
敏捷开发 测试技术
面试题9:常见测试模型有哪些?
面试题9:常见测试模型有哪些?
|
7月前
|
安全 前端开发 Java
针对 SpringSecurity 鉴权流程做了一个详细分析,让你明白它是如何执行的!
针对 SpringSecurity 鉴权流程做了一个详细分析,让你明白它是如何执行的!
283 0
|
机器学习/深度学习 人工智能 自然语言处理
此「错」并非真的错:从四篇经典论文入手,理解Transformer架构图「错」在何处
此「错」并非真的错:从四篇经典论文入手,理解Transformer架构图「错」在何处
119 0
|
测试技术
测试中进行断言是什么意思?底层原理是什么?
测试中进行断言是什么意思?底层原理是什么?
1411 0
|
JavaScript 前端开发
技术汇总:第七章:三种验证方式
技术汇总:第七章:三种验证方式
134 0
技术汇总:第七章:三种验证方式
|
前端开发
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
搞明白axios 源码,探究配置、拦截器、适配器等核心功能具体的执行过程(二)
|
前端开发 计算机视觉 Python
代码报错还好说,源码报错才难搞!分享自己源码报错的解决过程!
代码报错还好说,源码报错才难搞!分享自己源码报错的解决过程!
139 0
代码报错还好说,源码报错才难搞!分享自己源码报错的解决过程!