作者|Baldwin_KeepMind
责编|伍杏玲
出品|CSDN博客
我的真实经历
标题是我2019.6.28在深圳某500强公司面试时候面试官跟我说的话,即使是现在想起来,也是觉得无尽的羞愧,因为自己的愚钝、懒惰和自大,我到深圳的第一场面试便栽了大跟头。
我确信我这一生不会忘记那个燥热的上午,在头一天我收到了K公司的面试通知,这是我来深圳的第一个面试邀约。收到信息后,我激动得好像已经收到了K公司的Offer,我上网专门查了下K公司的面经,发现很多人都说他们很注重源码阅读能力,几乎每次都会问到一些关于源码的经典问题,因此我去网上找了几篇关于String、HashMap等的文章,了解到了很多关于Java源码的内容。看完后我非常的自信,心想着明天的所有问题我肯定都可以回答上来,心满意足的睡觉。
面试的那天上午,我9点钟到了K公司楼下,然后就是打电话联系人带我上去,在等待室等待面试,大概9:30的时候,前台小姐姐叫到了我的名字,我跟着她一起进入到了一个小房间,里面做了两个人,看样子都是做技术的(因为都有点秃),一开始都很顺利,然后问道了一个问题“你简历上说你熟悉Java源码,那我问你个问题,String类可以被继承么”,当然是不可以继承的,文章上都写了,String是用final修饰的,是无法被继承的,然后我又说了一些面试题上的内容,面试官接着又问了一个问题:
“请你简单说一下substring的实现过程”
是的,我没有看过这一题,平时使用的时候,也不会去看这个方法的源码,我支支吾吾的回答不上来,我能感觉到我的脸红到发烫。他好像看出了我的窘迫,于是接着说“你真的看过源码么?substring是一个很简单的方法,如果你真的看过,不可能不知道”,到这个地步,我也只好坦白,我没有看过源码,是的我其实连简单的substring怎么实现的都不知道,我甚至都找不到String类的源码。
面试官说了标题上的那句话,然后我面试失败了。
我要感谢这次失败的经历,让我打开了新世界,我开始尝试去看源码,从jdk源码到Spring,再到SpringBoot源码,看得越多我越敬佩那些写出这优秀框架的大佬,他们的思路、代码逻辑、设计模式,是那么的优秀与恰当。不仅如此,我也开始逐渐尝试自己去写一些框架,第一个练手框架是“手写简版Spring框架--YzSpring”,花了我一周时间,每天夜里下班之后都要在家敲上一两个小时,写完YzSpring之后,我感觉我才真正了解Spring,之前看网上的资料时总觉得是隔靴搔痒,只有真正去自己手写一遍才能明白Spring的工作原理。
再后来,我手上的“IPayment”项目的合作伙伴一直抱怨我们接口反馈速度慢,我着手优化代码,将一些数据缓存到Redis中,速度果然是快了起来,但是每添加一个缓存数据都要两三行代码来进行配套,缓存数据少倒无所谓,但是随着越来越多的数据需要写入缓存,代码变得无比臃肿。有天我看到@Autowired的注入功能,我忽然想到,为什么我不能自己写一个实用框架来将这些需要缓存的数据用注解标注,然后用框架处理呢?说干就干,连续加班一周,我完成了“基于Redis的快速数据缓存组件”,引入项目之后,需要缓存的数据只需要用@BFastCache修饰即可,可选的操作还有:对数据进行操作、选择数据源、更新数据源、设置/修改Key等,大大提高了工作效率。第一次自写轮子,而且效果这么好,得到了老大哥的肯定,真的很开心。
那么现在我要问你三个问题:
你看源码么?
你会看源码么?
你从源码中有收获么?
看源码可以获得什么
1.快速查错、减少出错
在编码时,我们一般都发现不了RuntimeException,就比如String的substring方法,可能有时候我们传入的endIndex大于字符串的长度,这样运行时就会有个错误:
String index out of range: 100
有时候稀里糊涂把代码改正确了,但是却不知道为什么发生这个异常,下次编写的时候又发生同样的问题。如果我们看过源码,我们就可以知道这个异常发生的原因:
public String substring(int beginIndex, int endIndex) {
if (beginIndex value.length) {//结束坐标大于字符串长度
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen 8;
n |= n >>> 16;
return (n = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这一步其实就是为了求大于我们设定的容量的最小2的幂数,以这个值作为真正的初始容量,而不是我们设定的值,这是为了随后的位运算的。现在我们解释一下上面的运算:
以cap=13为例,那么n初始=12,n的二进制数为00001100,随后一次右移一位并进行一次与n的或运算,以第一次为例,首先|=右边运算为无符号右移1位,那么右边的值为00000110,与n进行或运算值为00001110,反复运算到最后一步的时候,n=00001111,然后在return的时候便返回了n+1,也就是16.
至此,我们完成了一个空HashMap的初始化,现在这个HashMap已经可以操作了。
5.查看方法逻辑
我们一般使用HashMap的时候,put方法用的比较多,而且他涉及的内容也比较多,现在来定位到HashMap的put方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put方法又调用了putVal方法,并且将参数分解了,key和value没什么好说的,我们来先看一下hash(key)这个方法干了什么。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如果当前key是null,那么直接返回哈希值0,如果不是null,那就获取当前key的hash值赋值给h,并且返回一个当前key哈希值的高16位与低16位的按位异或值,这样让高位与低位都参与运算的方法可以大大减少哈希冲突的概率。
OK!多出来的三个参数,其中hash值的内容我们已经知道了,但是三个值都不知道有什么用,不要急,我们进入putVal方法。
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
看这上面一堆代码,是不是又开始头疼了,不要怕他,我们一行一行分解他,就会变得很容易了。
第一步还是要看注释,注释已经翻译好了,请享用。
/**
* 继承于 Map.put.
*
* @param hash key的hash值
* @param key key
* @param value 要输入的值
* @param onlyIfAbsent 如果是 true, 不改变存在的值
* @param evict if false, the table is in creation mode.
* @return 返回当前值, 当前值不存在返回null
*/
然后来看内容
1.创建了几个变量,其中Node是HashMap的底层数据结构,其大致属性如下:
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
2.判断当前table是否为空,或者table的长度是否为0,同时给tab和n赋值,如果条件成立(当前的HashMap是空的),那就进行resize,并将resize的值赋予tab,把tab数组的长度赋予n,由于篇幅原因,这里不详细解说resize()方法,这个方法内容比较多,在其他文章中也说了很多,今天的重点是说明如何去读源码,而不是HashMap。
3.判断底层数组中当前key值元素的hash值对应的位置有没有元素,如果没有,直接将当前元素放进去即可。
4.接上一步,如果底层数组对应位置中已经有值,那就进行其他的一些列操作把数据写入,并返回oldValue。
我们走完整个流程后,总结几个需要注意的点,比如HashMap.put方法里要注意的就是resize,尾插,树与列表之间的转换。
由于篇幅问题,这个方法里的内容,我只是简略的说一下,具体的查看源码的方式和之前大同小异,一步步分析即可。
6.小总结
查看源码的几个技巧:
1.Ctrl+左键或Ctrl+Alt+B定位到正确的源码位置
2.查看类里面一些量,有个大概的认识
3.查看构造函数看实例的初始化状况
4.如果代码比较复杂,分解代码,步步为营
5.其他的源码的阅读都可以按照这个套路来分析
作者=萌新,如有错误,欢迎指出。
阅读源码绝对是每个程序员都需要的技能,即使刚开始很难读懂,也要慢慢去习惯。
如果喜欢,欢迎点赞、评论、收藏、关注。
版权声明:本文为CSDN博主「Baldwin_KeepMind」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/shouchenchuan5253/article/details/105196154
更多精彩推荐
☞数字化转型太太太难?AI、IoT 重拳出击!
☞堪称奇迹!8天诞生一个产品,这家创业公司做到了
☞快速搭建对话机器人,就用这一招!
☞“抗疫”新战术:世卫组织联合IBM、甲骨文、微软构建了一个开放数据的区块链项目!
☞详Kubernetes在边缘计算领域的发展
☞原来疫情发生后,全球加密社区为了抗击冠状病毒做了这么多事情!
☞据说,这是当代极客们的【技术风向标】...
你点的每个“在看”,我都认真当成了喜欢