1. 程式人生 > >一道簡單的 Java 筆試題,但值得很多人反思!

一道簡單的 Java 筆試題,但值得很多人反思!

前言

面試別人,對我來說是一件新奇事,以前都是別人面試我。我清楚地知道,我在的地域與公司,難以吸引到中國的一流軟體人才。所以,我特地調低了期望,很少問什麼深入的技術問題,只問一些廣泛的、基礎的。我只要最終給Leader一句“這個人技術還行/很好/非常好”,就行了。至於其它能力、綜合水平,由別人把關。為此,在挑選唯一的一道筆試題時,我特別地上心。

首先,我不敢用網上那些廣為流傳的,比如Leetcode、《程式設計師面試寶典》裡的題——這些都太難了!正兒八經做,其實很少有人能在1小時內完美做出來,除非之前遇到過。我本人也並非什麼思維敏捷的牛人,不然也不會混得這麼慘。正所謂己所不欲,勿施於人,我也不希望以後別人考我特別麻煩的演算法題,所以自創了一道特別簡單的。

其次,對(Android平臺的)Java程式設計師來說,大多數情況下不需要寫什麼複雜的演算法。相反,Java層主要做的是介面控制、業務邏輯、資料流之類的,更提倡程式碼的簡單和可讀,儘量用既有的公共類庫,不惜損失一些執行效率。拿一道複雜的演算法題,考一個Java程式設計師,多少有點刁難人。

最後,還是那個薪資待遇和人才梯度問題。沒有Google的工資,就別考Google的題;沒有Google的向心力,就別期待有Google級別的人才來面試。

亮題

以下有一個 static method,類外會呼叫它,一個個地插入一些元素進入一個List。可以改變這個List內容的,只有這一個method,要求任何時候這個List都是有序的。比如,依次插入3、2、1、2,我希望List的順序是1、2、2、3。

我會給出15分鐘的時間,而其實往往會再多給10分鐘。(有興趣,你可以停在這試試。相信在看文章這種輕鬆的環境下,理清這道題的思路也就10~30秒。)

(為什麼下限是10秒呢?唉……一不小心暴露了我智商的峰值。我實際問過一些同事,他們通常在理解的同時,就立刻給出了正確的思路,過程不足5秒,其中甚至包括一個硬體工程師,和一個只負責溝通和文件的妹子。)

提示

在過程中,我會逐步給出一些提示,從介面到思路,都會主動提供,其它也基本有問必答。如果單純考演算法,C語言才是最合適的,因為它沒有什麼高階的工具類,什麼複雜點的都得自己寫。而Java,則有一些“基礎”類庫是難以記憶的。比如前面出現的java.util.List,就沒有多少人能在紙上寫出它的常用介面。

我並不想考察什麼死記硬背,在這個時代,斷網後本來就沒幾個程式設計師能正常程式設計。所以我會主動提供一份List的不完全介面列表。

我沒有給出完全的介面,因為給多了無疑是誤導人。真正能用上的介面其實也就3個,但我也總不能只給3個,提示得太明顯,也限制了對方的思路。所以,給出了可能用得上的這幾個。我也沒給出註釋,因為有宣告就已經夠了。而且如果對方問起,我也會給出解釋。

一開始我想,考一個排序算了。但是轉念一想,這也太不負責任了。對面要是背一道氣泡排序的解法上來,達不到考察技術水平的目的,Boss也不會認可。本著“放水不能太明顯”的原則,我想考插入排序,並且把題目弄得沒多少人見過。

排序是一類基本演算法,合格的程式設計師至少會一種。大多數人都只會入門級的氣泡排序,而我更喜歡插入排序,原因……你會明白的。

插入排序,其實就是把陣列或列表在邏輯上分成兩部分,一部分是待排序的,一部分是有序的。一開始,有序的部分只有一個元素(或者一個都沒有),然後從待排序的部分裡一個個抽出來,插入到有序的部分。等元素都插入到了有序的部分,排序過程也就完成了。

你看,也就抽插N次的事。而我這道題,就是隻考插入排序演算法的一半,會插就行。

在面試過程中,我甚至常常親自解釋插入排序是怎麼回事——放水到這個份上,我都不忍心再退步了。

真正的考察點

這是一份Android平臺的開發工作,Boss要求的是能幹活、幹好活。我給出的建議要求是:

熟悉Java。

有良好的溝通、表達能力。

學習能力強,喜歡不斷拓展計算機領域的知識。

有良好的編碼習慣,願意為程式碼的簡潔、優雅而反覆修改。

我建議Boss放棄學歷和工作年限的要求,技術崗位就應該只考察技術(和其它基本能力),不應該考察技術的間接證明。

Java是Android的基本功(我們不玩Kotlin、Scala、React Native等新花樣),這門語言如果不紮實,那至少得帶半年。

我沒有在Android崗明確地要求考察Android,是因為Android的那些東西相對來說容易學習。即便是毫無經驗的新手,要搞清楚什麼“四大元件”“五大布局”,也就一兩天的事。而如果Java不夠紮實,各種肉眼可見的大小bug就會層出不窮,知識盲點一兩年都補不完。

溝通是職場基本功。如果話都說不清,那麼會顯著降低團隊的溝通效率。而且,我個人認為,話說不清的人,程式碼一定寫不好。語言條理清晰,邏輯層次分明,體現到程式碼上,就是簡潔、明朗。

學習能力、求知慾,是作為一個程式設計師的基本素養。因為,大部分人的工作,類似於在一堆按鈕中,找到合適的那個按下去;而程式設計師的工作,往往是閉著眼睛這麼幹。開發工程師通常是在一堆未知(沒讀過的程式碼、不知道的介面)中,把一小部分變成已知(讀懂了的程式碼或介面),進行一些增刪改,最後達成外界(產品經理、設計師、測試工程師)賦予的業務目標。

一些職業賣口水,一些職業賣口才。一些職業賣青春,一些職業賣肉體(咳咳,我說的是空姐和搬磚,想歪的去面壁)。一些職業賣知識,一些職業賣能力。

程式設計師,或者說軟體開發工程師,賣的是學習能力(其實也包括青春和肉體),快速學會各種知識,找到那些藏在螢幕外的按鈕,並且正確的按下去。比如,像Bash這類Command line工具,就是自己敲命令出來執行,而不是去介面上找功能對應的按鈕;而程式設計、實現,就是去發現、或者創造一種解決問題的辦法,然後用程式碼表達出來——你看,都是在幹一些反UI、UX設計的事。唯有不斷地學習,才能提高效率,把自己從加班中解脫出來,把專案從bug中拯救出來。所以,厭學的人當不了好程式設計師,也幹不長。

編碼習慣,相對次之。部分觀點認為,這東西伴隨一生,如果一開始沒有好習慣,這輩子都沒辦法改了。Boss就是這麼認為的,我倒是不這麼認為。我相信編碼習慣的可塑性是很高的——你不按規範寫,我不給你merge,改不改?

但是,編碼習慣作為程式設計師的軟技能,還是可以一定程度上看出其技術素養、程式碼質量的。至於優雅什麼的,我其實沒有真的敢這麼期待。

所以,我這道題其實是考察這四點。

能寫出來,並且無明顯問題,代表Java基本功紮實。

理解我對題目的描述,和我確認清楚題目的細節,這是看溝通能力。

List介面不知道,我給你啊;插入排序不會,我教你啊;其它還有什麼不會,你問啊——這是在考察學習能力。

程式碼的字裡行間,可以明顯看出編碼習慣。

面試結果

總體來說,我很傷心。

第一位就讓我很傷心,當我看了他前兩行程式碼,就不忍心接著往下看:

第一行就編譯不過。如果他對Java的一些命名規範有一定的瞭解,就絕不會把sSorted寫成sorted。(當然,sSorted也許並不是合適的命名方式,因為s和m這類字首有些冗餘。我通常遵守Android原始碼的通用規範,它是有這類字首的。)

第二行必然丟擲NullPointerException,而不知道是該慶幸還是悲傷的是,它永遠執行不到。根據我已經給出的一個介面addElement,和可以猜到或者問出來的讀取介面,都是不會把sSorted變成null的。這體現了溝通、理解能力的一點問題。

此外,即使sSorted因為什麼bug而變成null,這裡也不應該做處理,而是任其丟擲NullPointerException,或者轉義一下,主動丟擲IllegalStateException。否則,此處將變成一個不會crash的隱藏bug。不能用正常處理,代替異常處理;當然,也不能用異常處理,代替流程控制。

另外,更令我失望的是,有一位是這麼寫的:

我問他,如果這個元素不在這個List裡存在怎麼辦?如果這個List是空的怎麼辦?他頓時一囧,我也一起囧,心想自己是不是太壞了。

還有一位,彷彿聽見了我這幾個問題,他竟然一一作答:

他想幹什麼呢?也許是優化效能吧,只能這麼幫腔了。另外,他對size的理解,和陣列的length相同。

這位算是經驗比較豐富(30歲),對Java的理解比較深入的了。他說排序不需要手寫,Java裡有現成的介面。我說,是這樣沒錯,但介面我沒給出,如果你記得,那就寫出來吧。

於是他在剛才那一大段“優化”的後面,這麼寫了:

思路上,插入後再排序,我先不吐槽。我明明說了“記得”再寫,這Comparable及其介面int compareTo(T another)如果記不清,我就當看lambda表示式了。可是,他這個?分明是Comparator的int compare(T lhs, T rhs)介面呀!

不過,其實這些我都可以捏著鼻子認了,因為我也手寫不出來。但List是沒有sort方法的呀!

Arrays和Collections才有各自的sort方法,它倆算是銀彈型工具類,而Array和Collection是沒有的。這個細節,誰用誰知道,知道了就絕不會記錯,儘管就差一個s。

還有一位,他先插入、再氣泡排序,是這麼寫的:

你沒看錯,for()裡面是,分隔的。

你沒看錯,temp是從石頭縫裡蹦出來的。

你沒看錯,List.get(e)是可以對其賦值的。

你沒看錯,List.size(i)是可以傳引數進去的。

還有兩位,直接交白卷放棄了。

其中一位還比較認真,思考了一會兒,說“我不想浪費時間”。

我沒亂用詞,他確實“比較認真”。另一位在我遞過去後,直接看兩眼就遞回來,“排序我不會”,然後看手機去了。

o(╯□╰)o

參考答案

我自己在紙上寫的時候,花了大概5分鐘去思考細節,再花5分鐘寫出來。(唉……一不小心,又暴露了自己奇慢無比的思維,以及奇慢無比的寫字速度。)這比我此前預計的時間多了好幾倍!

不過,以我給的15~25分鐘,應該不算太難為人……吧?

這是我自己在紙上寫的答案。(如果有興趣,可以停在此處,考慮下這是否是最優演算法。)

這是 java.util.LinkedList在Android(API 23)上的實現,而反編譯Oracle JDK 1.8的實現也大同小異。也就是說,我寫的答案雖然看似簡潔,但其最壞時間複雜度與先插入再排序也沒太大區別,都是O(n2)。

終日打燕,反而被燕啄了眼!(暴露了真實水平。)

我後來又寫了一個參考答案,算是勉強在臉上摸了些防晒霜。(大家有興趣可以想想為什麼這是一個改進。當然,一定還有更好的方案。)

(我沒有在提示列表中給出迭代器,結果自己也被晃過去了。)

隱藏的殺手鐗

面試官在出題考察應聘者時,應聘者也在通過這道題考察這家公司。

為了避免讓人覺得這家公司考題太簡單、工作內容太無趣、裡面的員工(我)水平太低,我還準備了一些後續問題,由淺入深,作為殺手鐗。

為什麼LinkedList可以賦值給List?

考察多型(polymorphism)。

為什麼List<Integer>要寫<>內的內容,而LinkedList<>()可以不寫?

考察泛型(generic)。

為什麼List裡面是Integer,但放進去和拿出來的都是int?

(此處有坑,其實拿出來的還是Integer。)

考察基本資料型別的自動裝箱、拆箱(auto boxing/unboxing)。

如何在外面有多執行緒呼叫時,保證這個唯一的List的正確性?

考察synchronized和volatile。

如何在多執行緒狀態下的每一個執行緒,各保持一個獨立的List?

考察ThreadLocal。

(當然,還有一些和Android相關的問題。)

我真心是沒想考演算法,所以連演算法複雜度的評估都沒打算問。實際情況是,我往往沒有機會問這些問題,因為沒幾個人寫出來。

吐槽與建議

首先,噴一下大學擴招……算了,不扯這麼遠了。那兩位放棄做題的,一個是計算機學院的,一個是軟體工程學院的。排序寫不出來,竟然也是能畢業的!

有兩位是某App的開發者。我把他們的App下載下來,發現了一堆bug後,本來想忍忍、就當沒看見、碼農何苦為難碼農,然後手機發熱、卡頓、滅屏後幾乎點亮不了(記憶體洩露吃光了RAM,導致系統程序沒有記憶體可用)。過了一陣最終好了,我檢視耗電排行,執行10分鐘就高居榜首,耗了17%的電——我嚇得立刻解除安裝了。一個第三方App能把系統給卡成這樣,一般人還真做不到。

還有兩位是“相關專業”的,非計算機、軟體工程專業,反而表現最佳,雖然還是沒寫出來。

他們無一例外,都是在大學以外,又參加過某些Java、Android培訓的。這些培訓班的水平,可見一斑。問題倒不一定是培訓班的教學質量,而是這種大規模提供人才轉型服務的形式本身——這個世界上,本來就不是誰,都能當一個好碼農,哪怕工作要求只是複製貼上。

現在,很多碼農都戲稱自己是在“搬磚”、複製貼上,但實際上程式設計師的工作不可能僅止於此。使用別人寫好的基本演算法,參考別人的實現程式碼,只是為了集中精力去解決抽象層次更高的業務問題。

“我們不寫程式碼,我們只做程式碼的搬運工。”——萬萬不可把這句話當做信條。

還有很多人,在沒有Demo的情況下,無論給多麼詳細的API或其它資料,仍然無法寫程式碼。他們只能在既有的基礎上,修修補補,無法憑空創作。


歡迎工作一到五年的Java工程師朋友們加入Java填坑之路:8  6  0  1  1  3  4  8  1
裡面提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!