面试官:你背了几道面试题就敢说熟悉Java源码?对不起,我们不招连源码都不会看的人
如果你不會看源碼,請耐心看下去
一、我的真實經(jīng)歷
標題是我2019.6.28在深圳某500強公司面試時候面試官跟我說的話,即使是現(xiàn)在想起來,也是覺得無盡的羞愧,因為自己的愚鈍、懶惰和自大,我到深圳的第一場面試便栽了大跟頭。
我確信我這一生不會忘記那個燥熱的上午,在頭一天我收到了K公司的面試通知,這是我來深圳的第一個面試邀約。收到信息后,我激動得好像已經(jīng)收到了K公司的offer,我上網(wǎng)專門查了下K公司的面經(jīng),發(fā)現(xiàn)很多人都說他們很注重源碼閱讀能力,幾乎每次都會問到一些關(guān)于源碼的經(jīng)典問題,因此我去網(wǎng)上找了幾篇關(guān)于String、HashMap等的文章,了解到了很多關(guān)于Java源碼的內(nèi)容。看完后我非常的自信,心想著明天的所有問題我肯定都可以回答上來,心滿意足的睡覺。
面試的那天上午,我9點鐘到了K公司樓下,然后就是打電話聯(lián)系人帶我上去,在等待室等待面試,大概9:30的時候,前臺小姐姐叫到了我的名字,我跟著她一起進入到了一個小房間,里面做了兩個人,看樣子都是做技術(shù)的(因為都有點禿),一開始都很順利,然后問道了一個問題“你簡歷上說你熟悉Java源碼,那我問你個問題,String類可以被繼承么”,當(dāng)然是不可以繼承的,文章上都寫了,String是用final修飾的,是無法被繼承的,然后我又說了一些面試題上的內(nèi)容,面試官接著又問了一個問題
“請你簡單說一下substring的實現(xiàn)過程”
是的,我沒有看過這一題,平時使用的時候,也不會去看這個方法的源碼,我支支吾吾的回答不上來,我能感覺到我的臉紅到發(fā)燙。他好像看出了我的窘迫,于是接著說“你真的看過源碼么?substring是一個很簡單的方法,如果你真的看過,不可能不知道”,到這個地步,我也只好坦白,我沒有看過源碼,是的我其實連簡單的substring怎么實現(xiàn)的都不知道,我甚至都找不到String類的源碼。
面試官說了標題上的那句話,然后我面試失敗了。
我要感謝這次失敗的經(jīng)歷,讓我打開了新世界,我開始嘗試去看源碼,從jdk源碼到Spring,再到SpringBoot源碼,看得越多我越敬佩那些寫出這優(yōu)秀框架的大佬,他們的思路、代碼邏輯、設(shè)計模式,是那么的優(yōu)秀與恰當(dāng)。不僅如此,我也開始逐漸嘗試自己去寫一些框架,第一個練手框架是“手寫簡版Spring框架--YzSpring”,花了我一周時間,每天夜里下班之后都要在家敲上一兩個小時,寫完YzSpring之后,我感覺我才真正了解Spring,之前看網(wǎng)上的資料時總覺得是隔靴搔癢,只有真正去自己手寫一遍才能明白Spring的工作原理。
再后來,我手上的“IPayment”項目的合作伙伴一直抱怨我們接口反饋速度慢,我著手優(yōu)化代碼,將一些數(shù)據(jù)緩存到Redis中,速度果然是快了起來,但是每添加一個緩存數(shù)據(jù)都要兩三行代碼來進行配套,緩存數(shù)據(jù)少倒無所謂,但是隨著越來越多的數(shù)據(jù)需要寫入緩存,代碼變得無比臃腫。有天我看到@Autowired的注入功能,我忽然想到,為什么我不能自己寫一個實用框架來將這些需要緩存的數(shù)據(jù)用注解標注,然后用框架處理呢?說干就干,連續(xù)加班一周,我完成了“基于Redis的快速數(shù)據(jù)緩存組件”,引入項目之后,需要緩存的數(shù)據(jù)只需要用@BFastCache修飾即可,可選的操作還有:對數(shù)據(jù)進行操作、選擇數(shù)據(jù)源、更新數(shù)據(jù)源、設(shè)置/修改Key等,大大提高了工作效率。第一次自寫輪子,而且效果這么好,得到了老大哥的肯定,真的很開心。
那么現(xiàn)在我要問你三個問題:
你看源碼么?
你會看源碼么?
你從源碼中有收獲么?
二、看源碼可以獲得什么
1.快速查錯、減少出錯
在編碼時,我們一般都發(fā)現(xiàn)不了RuntimeException,就比如String的substring方法,可能有時候我們傳入的endIndex大于字符串的長度,這樣運行時就會有個錯誤
String index out of range: 100有時候稀里糊涂把代碼改正確了,但是卻不知道為什么發(fā)生這個異常,下次編寫的時候又發(fā)生同樣的問題。如果我們看過源碼,我們就可以知道這個異常發(fā)生的原因
public String substring(int beginIndex, int endIndex) {if (beginIndex < 0) {//起始坐標小于0throw new StringIndexOutOfBoundsException(beginIndex);}if (endIndex > value.length) {//結(jié)束坐標大于字符串長度throw new StringIndexOutOfBoundsException(endIndex);}int subLen = endIndex - beginIndex;if (subLen < 0) {//起始坐標大于結(jié)束坐標throw new StringIndexOutOfBoundsException(subLen);}return ((beginIndex == 0) && (endIndex == value.length)) ? this: new String(value, beginIndex, subLen);}源碼中給出了三個可能拋出上面異常的情景,那我們就可以根據(jù)這三種情景去檢查我們的代碼,也以后在編碼的時候注意這些問題。
2.學(xué)習(xí)編程習(xí)慣
還是說上面的substring源碼,請注意他的return,如果是你,你會怎么寫?如果沒有看過源碼,我肯定會寫成下面
if ((beginIndex == 0) && (endIndex == value.length)) return this;return new String(value, beginIndex, subLen);雖然功能是一樣的,但是運用三元運算可以用一行代碼解決問題,而且又不用寫if語句,現(xiàn)在我已迷上了三元運算符,真的很好用。
3.學(xué)習(xí)設(shè)計模式(針對新手)
好吧!我攤牌了,作為一個半路出家的程序員,我沒有接受過系統(tǒng)化的教學(xué),所有的都是自學(xué),在之前我完全不了解設(shè)計模式,只知道有23種設(shè)計模式,最多知道單例模式。
不了解設(shè)計模式最主要的原因是當(dāng)時沒有實戰(zhàn)經(jīng)驗,自己寫的項目都是比賽項目,完全不用不上設(shè)計模式,基本上是能跑就行。我第一次接觸設(shè)計模式是在log4j的工廠模式,當(dāng)時是完全不懂工廠模式該怎么用,就是看著log4j的源碼一步步學(xué)會了,然后自己做項目的時候就會有意無意的開始運用設(shè)計模式,下面是我項目中使用單例模式獲取配置類的代碼
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.小總結(jié)
你們可能很多人都會覺得上面的東西很簡單,請不要被我誤導(dǎo),因為上面都是最簡單的例子,源碼中值得學(xué)習(xí)的地方非常多,只有你自己去看,才能明白。
三、閱讀源碼的正確姿勢
我們這里以一個熱度非常高的類HashMap來舉例,同時我非常建議你使用IDEA來閱讀編碼,其自帶反編譯器,可以讓我們快速方便的看到源碼,還有眾多快捷鍵操作,讓我們的操作爽到飛起。
1.定位源碼
其實定位的時候也有多種情況
Ctrl+左鍵
像這種情況,我們要進入只屬于HashMap類的方法,我們可以直接Ctrl+左鍵就可以定位到源碼位置了
Ctrl+Alt+B
HashMap的put方法是重寫了Map的方法,如果我們用Ctrl+左鍵,會直接跳到Map接口的put方法上,這不是我們想要的結(jié)果,此時我們應(yīng)該把鼠標光標放到put上,然后按下Ctrl+Alt+B,然后就出現(xiàn)了很多重寫過put方法的類
找到我們需要查看的類,左鍵點擊就可以定位到put方法了
2.查看繼承關(guān)系
一個類的繼承關(guān)系很重要,特別是繼承的抽象類,因為抽象類中的方法在子類中是可以使用的。
上一步中我們已經(jīng)定位到了HashMap源碼上,現(xiàn)在拉到最上面,我們可以看到類定義的時候是有一下繼承關(guān)系
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable當(dāng)然,如果想更直觀更詳細的話,在IDEA中有個提供展示繼承關(guān)系的功能,可以把鼠標放在要查看的類上,然后Ctrl+Alt+Shift+U,或者右鍵=》Diagrams=》Show Diagram,然后我們就可以看到繼承關(guān)系
?然后大致查看下AbstractMap抽象類,因為有可能等下會用到。
3.查看類常量
我們進到HashMap構(gòu)造函數(shù)時,發(fā)現(xiàn)了以下代碼
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}我們只知道initialCapacity是我們傳入的初始容量,但完全不知道這個DEFAULT_LOAD_FACTOR是什么、等于多少,我們可以先大致看一下這個類所擁有的的常量,留個印象就好,有利于等下閱讀源碼,Ctrl+左鍵定位到這個量的位置,然后發(fā)現(xiàn)還有好幾個常量,常量上面有注釋,我們看一下,這有助于我們理解這些常量
//序列號private static final long serialVersionUID = 362498820763181265L;/*** 初始容量,必須是2的冪數(shù)* 1 << 4 = 10000 = 16*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始默認值二進制1左移四位 = 16/*** 最大容量* 必須是2的冪數(shù) <= 1<<30.*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** 加載因子,構(gòu)造函數(shù)中沒有指定時會被使用*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 從鏈表轉(zhuǎn)到樹的時機*/static final int TREEIFY_THRESHOLD = 8;/*** 從樹轉(zhuǎn)到鏈表的時機*/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.查看構(gòu)造函數(shù)
我們一般看一個類,首先得看這個類是如何構(gòu)建的,也就是構(gòu)造方法的實現(xiàn)
/*** 構(gòu)造一個空的,帶有初始值和初始加載因子的HashMap* @param initialCapacity the initial capacity.* @throws IllegalArgumentException if the initial capacity is negative.*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}很明顯,上面的構(gòu)造函數(shù)指向了另一個構(gòu)造函數(shù),那么我們點進去看看
/**** @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);}這里就是我們構(gòu)造函數(shù)實現(xiàn)的地方了,我們來一行一行的去分析:
1.我們的initialCapacity參數(shù)是我們一開始傳進來的16,loadFactor是上一步中用的默認參數(shù)0.75f
2.判斷初始容量是否小于0,小于0就拋出異常,不小于0進行下一步
3.判斷初始容量是否大于最大容量(1 << 30),如果大于,就取最大容量
4.判斷加載因子是否小于等于0,或者是否為數(shù)字,拋出異常或下一步
5.初始化這個HashMap的加載因子
6.最后一行是HashMap的擴容機制,根據(jù)我們給的容量大小來確定實際的容量,我們來看一下該方法的源碼
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;}這一步其實就是為了求大于我們設(shè)定的容量的最小2的冪數(shù),以這個值作為真正的初始容量,而不是我們設(shè)定的值,這是為了隨后的位運算的。現(xiàn)在我們解釋一下上面的運算:
以cap=13為例,那么n初始=12,n的二進制數(shù)為00001100,隨后一次右移一位并進行一次與n的或運算,以第一次為例,首先|=右邊運算為無符號右移1位,那么右邊的值為00000110,與n進行或運算值為00001110,反復(fù)運算到最后一步的時候,n=00001111,然后在return的時候便返回了n+1,也就是16.
至此,我們完成了一個空HashMap的初始化,現(xiàn)在這個HashMap已經(jīng)可以操作了。
5.查看方法邏輯
我們一般使用HashMap的時候,put方法用的比較多,而且他涉及的內(nèi)容也比較多,現(xiàn)在來定位到HashMap的put方法
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}put方法又調(diào)用了putVal方法,并且將參數(shù)分解了,key和value沒什么好說的,我們來先看一下hash(key)這個方法干了什么
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}如果當(dāng)前key是null,那么直接返回哈希值0,如果不是null,那就獲取當(dāng)前key的hash值賦值給h,并且返回一個當(dāng)前key哈希值的高16位與低16位的按位異或值,這樣讓高位與低位都參與運算的方法可以大大減少哈希沖突的概率。
OK!多出來的三個參數(shù),其中hash值的內(nèi)容我們已經(jīng)知道了,但是三個值都不知道有什么用,不要急,我們進入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;}?看這上面一堆代碼,是不是又開始頭疼了,不要怕他,我們一行一行分解他,就會變得很容易了。
第一步還是要看注釋,注釋已經(jīng)翻譯好了,請享用
/*** 繼承于 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 返回當(dāng)前值, 當(dāng)前值不存在返回null*/然后來看內(nèi)容
1.創(chuàng)建了幾個變量,其中Node是HashMap的底層數(shù)據(jù)結(jié)構(gòu),其大致屬性如下
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.判斷當(dāng)前table是否為空,或者table的長度是否為0,同時給tab和n賦值,如果條件成立(當(dāng)前的HashMap是空的),那就進行resize,并將resize的值賦予tab,把tab數(shù)組的長度賦予n,由于篇幅原因,這里不詳細解說resize()方法,這個方法內(nèi)容比較多,在其他文章中也說了很多,今天的重點是說明如何去讀源碼,而不是HashMap。
3.判斷底層數(shù)組中當(dāng)前key值元素的hash值對應(yīng)的位置有沒有元素,如果沒有,直接將當(dāng)前元素放進去即可
4.接上一步,如果底層數(shù)組對應(yīng)位置中已經(jīng)有值,那就進行其他的一些列操作把數(shù)據(jù)寫入,并返回oldValue。
我們走完整個流程后,總結(jié)幾個需要注意的點,比如HashMap.put方法里要注意的就是resize,尾插,樹與列表之間的轉(zhuǎn)換。
由于篇幅問題,這個方法里的內(nèi)容,我只是簡略的說一下,具體的查看源碼的方式和之前大同小異,一步步分析即可。
6.小總結(jié)
查看源碼的幾個技巧
1.Ctrl+左鍵或Ctrl+Alt+B定位到正確的源碼位置
2.查看類里面一些量,有個大概的認識
3.查看構(gòu)造函數(shù)看實例的初始化狀況
4.如果代碼比較復(fù)雜,分解代碼,步步為營
5.其他的源碼的閱讀都可以按照這個套路來分析
四、總結(jié)
作者=萌新,如有錯誤,歡迎指出
閱讀源碼絕對是每個程序員都需要的技能,即使剛開始很難讀懂,也要慢慢去習(xí)慣
如果喜歡,歡迎點贊、評論、收藏、關(guān)注
總結(jié)
以上是生活随笔為你收集整理的面试官:你背了几道面试题就敢说熟悉Java源码?对不起,我们不招连源码都不会看的人的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php session超时设置,php如
- 下一篇: 第四十九期:大牛总结的MySQL锁优化,