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