面試官:你背了幾道面試題就敢說熟悉Java源碼?對不起,我們不招連源碼都不會看的人

如果你不會看源碼,請耐心看下去

一、我的真實經歷

標題是我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) {//起始座標小於0
            throw 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 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<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.其他的源碼的閱讀都可以按照這個套路來分析

四、總結

作者=萌新,如有錯誤,歡迎指出

閱讀源碼絕對是每個程序員都需要的技能,即使剛開始很難讀懂,也要慢慢去習慣

如果喜歡,歡迎點贊、評論、收藏、關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章