小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

面試官:背了幾道面試題就敢說熟悉Java源碼?我們不招連源碼都不會(huì)看的人

 板橋胡同37號(hào) 2020-04-12
作者|Baldwin_KeepMind
責(zé)編|伍杏玲
出品|CSDN博客
我的真實(shí)經(jīng)歷

標(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等,大大提高了工作效率。第一次自寫輪子,而且效果這么好,得到了老大哥的肯定,真的很開心。
那么現(xiàn)在我要問你三個(gè)問題:
  1. 你看源碼么?
  2. 你會(huì)看源碼么?
  3. 你從源碼中有收獲么?

看源碼可以獲得什么
1.快速查錯(cuò)、減少出錯(cuò)
在編碼時(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í)候注意這些問題。
2.學(xué)習(xí)編程習(xí)慣
還是說上面的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;
    }
}
3.小總結(jié)
你們可能很多人都會(huì)覺得上面的東西很簡(jiǎn)單,請(qǐng)不要被我誤導(dǎo),因?yàn)樯厦娑际亲詈?jiǎn)單的例子,源碼中值得學(xué)習(xí)的地方非常多,只有你自己去看,才能明白。

閱讀源碼的正確姿勢(shì)

我們這里以一個(gè)熱度非常高的類HashMap來舉例,同時(shí)我非常建議你使用IDEA來閱讀編碼,其自帶反編譯器,可以讓我們快速方便的看到源碼,還有眾多快捷鍵操作,讓我們的操作爽到飛起。
1.定位源碼
其實(shí)定位的時(shí)候也有多種情況:
Ctrl+左鍵

像這種情況,我們要進(jìn)入只屬于HashMap類的方法,我們可以直接Ctrl+左鍵就可以定位到源碼位置了。
Ctrl+Alt+B
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方法了。
2.查看繼承關(guān)系
一個(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ì)用到。
3.查看類常量
我們進(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中常量的作用和意義有所理解了

4.查看構(gòu)造函數(shù)
我們一般看一個(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ù)字,拋出異?;蛳乱徊?。
5.初始化這個(gè)HashMap的加載因子。
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)可以操作了。
5.查看方法邏輯
我們一般使用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
     */
然后來看內(nèi)容
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)略的說一下,具體的查看源碼的方式和之前大同小異,一步步分析即可。
6.小總結(jié)
查看源碼的幾個(gè)技巧:
1.Ctrl+左鍵或Ctrl+Alt+B定位到正確的源碼位置
2.查看類里面一些量,有個(gè)大概的認(rèn)識(shí)
3.查看構(gòu)造函數(shù)看實(shí)例的初始化狀況
4.如果代碼比較復(fù)雜,分解代碼,步步為營(yíng)
5.其他的源碼的閱讀都可以按照這個(gè)套路來分析
作者=萌新,如有錯(cuò)誤,歡迎指出。
閱讀源碼絕對(duì)是每個(gè)程序員都需要的技能,即使剛開始很難讀懂,也要慢慢去習(xí)慣。
版權(quán)聲明:本文為CSDN博主「Baldwin_KeepMind」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多