如果你不会看源码,请耐心看下去

一、我的真实经历

标题是我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 < 0) {//起始坐标小于0throw new StringIndexOutOfBoundsException(beginIndex);}if (endIndex > value.length) {//结束坐标大于字符串长度throw new StringIndexOutOfBoundsException(endIndex);}int subLen = endIndex - beginIndex;if (subLen < 0) {//起始坐标大于结束坐标throw new StringIndexOutOfBoundsException(subLen);}return ((beginIndex == 0) && (endIndex == value.length)) ? this: new String(value, beginIndex, subLen);}

源码中给出了三个可能抛出上面异常的情景,那我们就可以根据这三种情景去检查我们的代码,也以后在编码的时候注意这些问题。

2.学习编程习惯

还是说上面的substring源码,请注意他的return,如果是你,你会怎么写?如果没有看过源码,我肯定会写成下面

        if ((beginIndex == 0) && (endIndex == value.length)) return this;return new String(value, beginIndex, subLen);

虽然功能是一样的,但是运用三元运算可以用一行代码解决问题,而且又不用写if语句,现在我已迷上了三元运算符,真的很好用。

3.学习设计模式(针对新手)

好吧!我摊牌了,作为一个半路出家的程序员,我没有接受过系统化的教学,所有的都是自学,在之前我完全不了解设计模式,只知道有23种设计模式,最多知道单例模式。

不了解设计模式最主要的原因是当时没有实战经验,自己写的项目都是比赛项目,完全不用不上设计模式,基本上是能跑就行。我第一次接触设计模式是在log4j的工厂模式,当时是完全不懂工厂模式该怎么用,就是看着log4j的源码一步步学会了,然后自己做项目的时候就会有意无意的开始运用设计模式,下面是我项目中使用单例模式获取配置类的代码

import java.util.ResourceBundle;public class Configration {private static Object lock              = new Object();private static Configration config     = null;private static ResourceBundle rb        = null;private Configration(String filename) {rb = ResourceBundle.getBundle(filename);}public static Configration getInstance(String filename) {synchronized(lock) {if(null == config) {config = new Configration(filename);}}return (config);}public String getValue(String key) {String ret = "";if(rb.containsKey(key)){ret = rb.getString(key);}return ret;}
}

3.小总结

你们可能很多人都会觉得上面的东西很简单,请不要被我误导,因为上面都是最简单的例子,源码中值得学习的地方非常多,只有你自己去看,才能明白。

三、阅读源码的正确姿势

我们这里以一个热度非常高的类HashMap来举例,同时我非常建议你使用IDEA来阅读编码,其自带反编译器,可以让我们快速方便的看到源码,还有众多快捷键操作,让我们的操作爽到飞起。

1.定位源码

其实定位的时候也有多种情况

Ctrl+左键

像这种情况,我们要进入只属于HashMap类的方法,我们可以直接Ctrl+左键就可以定位到源码位置了

Ctrl+Alt+B

HashMap的put方法是重写了Map的方法,如果我们用Ctrl+左键,会直接跳到Map接口的put方法上,这不是我们想要的结果,此时我们应该把鼠标光标放到put上,然后按下Ctrl+Alt+B,然后就出现了很多重写过put方法的类

找到我们需要查看的类,左键点击就可以定位到put方法了

2.查看继承关系

一个类的继承关系很重要,特别是继承的抽象类,因为抽象类中的方法在子类中是可以使用的。

上一步中我们已经定位到了HashMap源码上,现在拉到最上面,我们可以看到类定义的时候是有一下继承关系

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable 

当然,如果想更直观更详细的话,在IDEA中有个提供展示继承关系的功能,可以把鼠标放在要查看的类上,然后Ctrl+Alt+Shift+U,或者右键=》Diagrams=》Show Diagram,然后我们就可以看到继承关系

然后大致查看下AbstractMap抽象类,因为有可能等下会用到。

3.查看类常量

我们进到HashMap构造函数时,发现了以下代码

public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}

我们只知道initialCapacity是我们传入的初始容量,但完全不知道这个DEFAULT_LOAD_FACTOR是什么、等于多少,我们可以先大致看一下这个类所拥有的的常量,留个印象就好,有利于等下阅读源码,Ctrl+左键定位到这个量的位置,然后发现还有好几个常量,常量上面有注释,我们看一下,这有助于我们理解这些常量

    //序列号private static final long serialVersionUID = 362498820763181265L;/*** 初始容量,必须是2的幂数* 1 << 4 = 10000 = 16*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始默认值二进制1左移四位 = 16/*** 最大容量* 必须是2的幂数 <= 1<<30.*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** 加载因子,构造函数中没有指定时会被使用*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 从链表转到树的时机*/static final int TREEIFY_THRESHOLD = 8;/*** 从树转到链表的时机*/static final int UNTREEIFY_THRESHOLD = 6;/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.*/static final int MIN_TREEIFY_CAPACITY = 64;

这样,我们就对HashMap中常量的作用和意义有所理解了

4.查看构造函数

我们一般看一个类,首先得看这个类是如何构建的,也就是构造方法的实现

    /*** 构造一个空的,带有初始值和初始加载因子的HashMap* @param  initialCapacity the initial capacity.* @throws IllegalArgumentException if the initial capacity is negative.*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}

很明显,上面的构造函数指向了另一个构造函数,那么我们点进去看看

    /**** @param  initialCapacity the initial capacity* @param  loadFactor      the load factor* @throws IllegalArgumentException if the initial capacity is negative*         or the load factor is nonpositive*/public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);}

这里就是我们构造函数实现的地方了,我们来一行一行的去分析:

1.我们的initialCapacity参数是我们一开始传进来的16,loadFactor是上一步中用的默认参数0.75f

2.判断初始容量是否小于0,小于0就抛出异常,不小于0进行下一步

3.判断初始容量是否大于最大容量(1 << 30),如果大于,就取最大容量

4.判断加载因子是否小于等于0,或者是否为数字,抛出异常或下一步

5.初始化这个HashMap的加载因子

6.最后一行是HashMap的扩容机制,根据我们给的容量大小来确定实际的容量,我们来看一下该方法的源码

    static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (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<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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 1sttreeifyBin(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 keyV 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<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> 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.其他的源码的阅读都可以按照这个套路来分析

四、总结

作者=萌新,如有错误,欢迎指出

阅读源码绝对是每个程序员都需要的技能,即使刚开始很难读懂,也要慢慢去习惯

如果喜欢,欢迎点赞、评论、收藏、关注

面试官:你背了几道面试题就敢说熟悉Java源码?对不起,我们不招连源码都不会看的人相关推荐

  1. 面试官:背了几道面试题就敢说熟悉Java源码?我们不招连源码都不会看的人|原力计划...

    作者|Baldwin_KeepMind 责编|伍杏玲 出品|CSDN博客 我的真实经历 标题是我2019.6.28在深圳某500强公司面试时候面试官跟我说的话,即使是现在想起来,也是觉得无尽的羞愧,因 ...

  2. 面试官问了我几道Java基础没答上来

    面试官问了我几道Java基础没答上来 文章目录 面试官问了我几道Java基础没答上来 1.面向对象的三大特性,分别解释下? 2.说到多态,再来说下方法重载和重写的区别? 3.Java是面向对象的语言, ...

  3. 2022春招、金三银四,面试官必问的1000道Java面试题及答案整理

    前言 不论是校招还是社招都避免不了各种面试,如何去准备面试就显得格外重要. 这不马上金三银四了,相信有很多小伙伴为社招或跳槽做准备,最近小编也常常在刷面试题,发现网上很多Java面试题都没有答案,所以 ...

  4. python 实数如何取整_从面试官角度提问:15道硬核Python面试题,论面霸是如何炼成的...

    见过面试题也不少了,总之了一句话:面试前备好功课,面试中临危不乱,面试后谦虚有礼!这只是我本人总结的一些面试三要素,需要的可以参考参考,话不多了,今天为大家找了比较硬核的15道面试题,希望能够对各位有 ...

  5. 我发现不少培训班的就业辅导老师,简直是面试官的卧底——再论培训班学员的就业方式(java方向)

    我最近在帮一些朋友做java方面的就业辅导,其中有些朋友是经过培训班加持后入行java的.由于我本人做过一些大厂和外企的java技术面试官,我发现其中一些朋友的简历甚至根本没法通过小公司的筛选,而且不 ...

  6. 面试官100%会问的软件测试面试题!!(必看)

    目录 一.问题预测 让简单介绍下自己(每次面试开场) 让说下自己会的内容 看了哪些书籍(有问到) 了解过哪些技术博客/论坛(有问到) 是否了解软件测试需要掌握哪些知识(问到类似问题) 之前面试过,觉得 ...

  7. 北京六天十次面试,已拿到offer,面试题和总结分享(Java岗)

    前言: 最近一朋友离了职,在北京前前后后面试了6天,一共10家公司,我找他分享出面试过程及经验,希望能对各位正在面试中或者准备面试的童鞋有所帮助,以下面试企业为了保密不说实际名称.以下为第一人称. 第 ...

  8. c++编写算法判断二叉树是否为完全二叉树_字节面试官:连这90道LeetCode算法题都不会也来面试?...

    面试大厂必刷:LeetCode算法90题 1. 买股票的最佳时机 难度级别:简单 题目: 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. 如果你最多只允许完成一笔交易(即买入和卖出一 ...

  9. hashmap put过程_阿里面试官:HashMap数据结构之道

    问题1:HashMap的数据结构是什么样的? 同学1:嗯...数组+链表 同学2:数组+链表... 同学3:数组+链表... 同学4:数组+链表+红黑树... 同学n:..... 为什么答案会有两种? ...

  10. 46道面试题带你了解中高级Java面试,Java开发必看书籍

    前言 分布式事务主要解决分布式一致性的问题.说到底就是数据的分布式操作导致仅依靠本地事务无法保证原子性.与单机版的事务不同的是,单机是把多个命令打包成一个统一处理,分布式事务是将多个机器上执行的命令打 ...

最新文章

  1. RabbitMQ —— 延迟队列
  2. linux中chmod与chown两个命令详解
  3. 解读容器的 2020:寻找云原生的下一站
  4. 关于主机的思维导图_几张思维导图,让你清楚的知道ip地址怎么回事?
  5. GetCursorPos/WindowFromPoint/SendMessage
  6. Android之SlidingMenu使用和总结
  7. 二叉树层次遍历python_根据二叉树层序遍历顺序(数组),将其转换为二叉树(Python)...
  8. IDC:物联网计划对IT基础设施影响重大
  9. Java 算法 阿尔法乘积
  10. 自动化如何帮助全球共抗疫情?
  11. Android 资讯类App项目实战 第一章 滑动顶部导航栏
  12. 上采样卷积转置的deconvolution方法实现双线性插值,代码实现,结果不一样
  13. Monkey学习笔记三:Monkey脚本编写
  14. jquery实现返回顶部的效果
  15. Velocity基本常用语法
  16. 转:多玩网总裁李学凌:在腾讯阴影下
  17. c语言中until的用法,until的用法总结
  18. 使用Zbar进行二维码识别 中文字符解码 RawBytes
  19. 麦吉尔大学 计算机科学和阿尔伯特,加拿大低龄留学-阿尔伯特学院-Albert College...
  20. 【C语言初级】 在有序数组中在折半查找数值并打印下标

热门文章

  1. 【Scratch案例实操】Scratch萌宠运动会 scratch编程案例教学 scratch创意编程 少儿编程教案
  2. UA OPTI512R 傅立叶光学导论14 卷积定理
  3. Oracle如何查看表空间的大小及使用情况
  4. mysql alter table 速度慢_mysql问题: alter导致速度慢 | 学步园
  5. mysql数据比较工具_mysql主从数据对比工具简介
  6. sap 服务采购订单研究
  7. dell自带的测试软件,Dell System Detect
  8. 调洪演算双辅助线法计算程序(带石门坎水电站算例)
  9. 软件使用,Microsoft Visual C++运行库合集包
  10. 第七章软件项目资源管理