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

前言

面試別人,對我來講是一件新奇事,之前都是別人面試我。 我清楚地知道,我在的地域與公司,難以吸引到中國的一流軟件人才。因此,我特意調低了指望,不多問什麼深刻的技術問題,只問一些普遍的、基礎的。我只要最終給Leader一句「這我的技術還行/很好/很是好」,就好了。至於其它能力、綜合水平,由別人把關。爲此,在挑選惟一的一道筆試題時,我特別地上心。java

首先,我不敢用網上那些廣爲流傳的,好比Leetcode、《程序員面試寶典》裏的題——這些都太難了!正兒八經作,其實不多有人能在1小時內完美作出來,除非以前遇到過。我本人也並不是什麼思惟敏捷的牛人,否則也不會混得這麼慘。正所謂己所不欲,勿施於人,我也不但願之後別人考我特別麻煩的算法題,因此自創了一道特別簡單的。程序員

其次,對(Android平臺的)Java程序員來講,大多數狀況下不須要寫什麼複雜的算法。相反,Java層主要作的是界面控制、業務邏輯、數據流之類的,更提倡代碼的簡單和可讀,儘可能用既有的公共類庫,不惜損失一些運行效率。拿一道複雜的算法題,考一個Java程序員,多少有點刁難人。面試

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

亮題

如下有一個static method,類外會調用它,一個個地插入一些元素進入一個List。能夠改變這個List內容的,只有這一個method,要求任什麼時候候這個List都是有序的。 好比,依次插入三、二、一、2,我但願List的順序是一、二、二、3。編程

class Solution {
    private static List<Integer> sSorted = new LinkedList<>();
    public static void addElement(int e) {
        // TODO: Insert e to sSorted and make sure sSorted is always sorted.
    }
}
複製代碼

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

(爲何下限是10秒呢?唉……一不當心暴露了我智商的峯值。我實際問過一些同事,他們一般在理解的同時,就馬上給出了正確的思路,過程不足5秒,其中甚至包括一個硬件工程師,和一個只負責溝通和文檔的妹子。)bash

提示

在過程當中,我會逐步給出一些提示,從接口到思路,都會主動提供,其它也基本有問必答。若是單純考算法,C語言纔是最合適的,由於它沒有什麼高級的工具類,什麼複雜點的都得本身寫。而Java,則有一些「基礎」類庫是難以記憶的。好比前面出現的java.util.List,就沒有多少人能在紙上寫出它的經常使用接口。多線程

我並不想考察什麼死記硬背,在這個時代,斷網後原本就沒幾個程序員能正常編程。因此我會主動提供一份List的不徹底接口列表。ide

public interface List<E> extends Collection<E> {
    public void add(int location, E object);
    public void add(E object);
    public void clear();
    public boolean contains(Object object);
    public boolean equals(Object object);
    public E get(int location);
    public int indexOf(Object object);
    public boolean isEmpty();
    public E remove(int location);
    public E set(int location, E object);
    public int size();
}
複製代碼

我沒有給出徹底的接口,由於給多了無疑是誤導人。真正能用上的接口其實也就3個,但我也總不能只給3個,提示得太明顯,也限制了對方的思路。因此,給出了可能用得上的這幾個。我也沒給出註釋,由於有聲明就已經夠了。並且若是對方問起,我也會給出解釋。工具

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

排序是一類基本算法,合格的程序員至少會一種。大多數人都只會入門級的冒泡排序,而我更喜歡插入排序,緣由……你會明白的。

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

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

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

真正的考察點

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

  1. 熟悉Java。
  2. 有良好的溝通、表達能力。
  3. 學習能力強,喜歡不斷拓展計算機領域的知識。
  4. 有良好的編碼習慣,願意爲代碼的簡潔、優雅而反覆修改。

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

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

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

**溝通是職場基本功。**若是話都說不清,那麼會顯著下降團隊的溝通效率。並且,我我的認爲,話說不清的人,代碼必定寫很差。語言條理清晰,邏輯井井有條,體現到代碼上,就是簡潔、明朗。

**學習能力、求知慾,是做爲一個程序員的基本素養。**由於,大部分人的工做,相似於在一堆按鈕中,找到合適的那個按下去;而程序員的工做,每每是閉着眼睛這麼幹。開發工程師一般是在一堆未知(沒讀過的代碼、不知道的接口)中,把一小部分變成已知(讀懂了的代碼或接口),進行一些增刪改,最後達成外界(產品經理、設計師、測試工程師)賦予的業務目標。

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

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

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

可是,編碼習慣做爲程序員的軟技能,仍是能夠必定程度上看出其技術素養、代碼質量的。至於優雅什麼的,我其實沒有真的敢這麼期待。

因此,我這道題實際上是考察這四點。

  1. 能寫出來,而且無明顯問題,表明Java基本功紮實。
  2. 理解我對題目的描述,和我確認清楚題目的細節,這是看溝通能力。
  3. List接口不知道,我給你啊;插入排序不會,我教你啊;其它還有什麼不會,你問啊——這是在考察學習能力。
  4. 代碼的字裏行間,能夠明顯看出編碼習慣。

面試結果

整體來講,我很傷心。

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

private static List<Integer> sSorted = new LinkedList<>();
public static void addElement(int e) {
    if (null == sorted) {
        sorted.add(e);
    }
    // I couldn't read more! 複製代碼

第一行就編譯不過。若是他對Java的一些命名規範有必定的瞭解,就毫不會把sSorted寫成sorted。(固然,sSorted也許並非合適的命名方式,由於s和m這類前綴有些冗餘。我一般遵照Android源碼的通用規範,它是有這類前綴的。) 第二行必然拋出NullPointerException,而不知道是該慶幸仍是悲傷的是,它永遠執行不到。根據我已經給出的一個接口addElement,和能夠猜到或者問出來的讀取接口,都是不會把sSorted變成null的。這體現了溝通、理解能力的一點問題。 此外,即便sSorted由於什麼bug而變成null,這裏也不該該作處理,而是任其拋出NullPointerException,或者轉義一下,主動拋出IllegalStateException。不然,此處將變成一個不會crash的隱藏bug。不能用正常處理,代替異常處理;固然,也不能用異常處理,代替流程控制。

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

for (int i = 0; i < sSorted.size(); i++) {
    if (e == sSorted.get(i)) {
        sSorted.add(i, e);
    }
}
複製代碼

我問他,若是這個元素不在這個List裏存在怎麼辦?若是這個List是空的怎麼辦?他頓時一囧,我也一塊兒囧,心想本身是否是太壞了。

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

if (sSorted.size == 0) {
    sSorted.add(e);
    return;
}
if (e >= sSorted.get(sSorted.size - 1)) {
    sSorted.add(e);
    return;
}
if (e <= sSorted.get(0)) {
    sSorted.add(0, e);
    return;
}
if (sSorted.contains(e)) {
    sSorted.add(sSorted.indexOf(e), e);
    return;
}
// more...
複製代碼

他想幹什麼呢?也許是優化性能吧,只能這麼幫腔了。另外,他對size的理解,和數組的length相同。 這位算是經驗比較豐富(30歲),對Java的理解比較深刻的了。他說排序不須要手寫,Java裏有現成的接口。我說,是這樣沒錯,但接口我沒給出,若是你記得,那就寫出來吧。 因而他在剛纔那一大段「優化」的後面,這麼寫了:

sSorted.add(e);
sSorted.sort(new Comp...able() {
    public boolean ?(left, right) {
        return right >= left;
    }
});
複製代碼

思路上,插入後再排序,我先不吐槽。我明明說了「記得」再寫,這Comparable及其接口int compareTo(T another)若是記不清,我就當看lambda表達式了。但是,他這個?分明是Comparator的int compare(T lhs, T rhs)接口呀! 不過,其實這些我均可以捏着鼻子認了,由於我也手寫不出來。但List是沒有sort方法的呀! Arrays和Collections纔有各自的sort方法,它倆算是銀彈型工具類,而Array和Collection是沒有的。這個細節,誰用誰知道,知道了就毫不會記錯,儘管就差一個s。

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

sSorted.add(e);
for (int i = 0, sSorted.size(i) > sSorted.get(e), i++) {
    temp = sSorted.get(e);
    sSorted.get(e) = sSorted.size(i);
    sSorted.size(i) = temp;
}
複製代碼
  1. 你沒看錯,for()裏面是,分隔的。
  2. 你沒看錯,temp是從石頭縫裏蹦出來的。
  3. 你沒看錯,List.get(e)是能夠對其賦值的。
  4. 你沒看錯,List.size(i)是能夠傳參數進去的。

還有兩位,直接交白卷放棄了。 其中一位還比較認真,思考了一下子,說「我不想浪費時間」。 我沒亂用詞,他確實「比較認真」。另外一位在我遞過去後,直接看兩眼就遞回來,「排序我不會」,而後看手機去了。

參考答案

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

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

class Solution {
    private static List<Integer> sSorted = new LinkedList<>();

    public static void addElement(int e) {
        int i;
        for (i = 0; i < sSorted.size(); ++i) {
            if (e <= sSorted.get(i)) {
                break;
            }
        }
        sSorted.add(i, e);
    }
}
複製代碼

這是我本身在紙上寫的答案。(若是有興趣,能夠停在此處,考慮下這是不是最優算法。)

@Override
    public E get(int location) {
        if (location >= 0 && location < size) {
            Link<E> link = voidLink;
            if (location < (size / 2)) {
                for (int i = 0; i <= location; i++) {
                    link = link.next;
                }
            } else {
                for (int i = size; i > location; i--) {
                    link = link.previous;
                }
            }
            return link.data;
        }
        throw new IndexOutOfBoundsException();
    }
複製代碼

這是 java.util.LinkedList在Android(API 23)上的實現,而反編譯Oracle JDK 1.8的實現也大同小異。也就是說,我寫的答案雖然看似簡潔,但其最壞時間複雜度與先插入再排序也沒太大區別,都是O(n2)。 終日打燕,反而被燕啄了眼!(暴露了真實水平。) 我後來又寫了一個參考答案,算是勉強在臉上摸了些防曬霜。(你們有興趣能夠想一想爲何這是一個改進。固然,必定還有更好的方案。)

class Solution {
    private static List<Integer> sSorted = new LinkedList<>();

    public static void addElement(int e) {
        int i = 0;
        for (int j : sSorted) {
            if (e <= j) {
                break;
            }
            ++i;
        }
        sSorted.add(i, e);
    }
}
複製代碼

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

隱藏的殺手鐗

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

爲了不讓人以爲這家公司考題太簡單、工做內容太無趣、裏面的員工(我)水平過低,我還準備了一些後續問題,由淺入深,做爲殺手鐗。

  1. 爲何LinkedList能夠賦值給List? 考察多態(polymorphism)。
  2. 爲何List要寫<>內的內容,而LinkedList<>()能夠不寫? 考察泛型(generic)。
  3. 爲何List裏面是Integer,但放進去和拿出來的都是int? (此處有坑,其實拿出來的仍是Integer。) 考察基本數據類型的自動裝箱、拆箱(auto boxing/unboxing)。
  4. 如何在外面有多線程調用時,保證這個惟一的List的正確性? 考察synchronized和volatile。
  5. 如何在多線程狀態下的每個線程,各保持一個獨立的List? 考察ThreadLocal。

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

我真心是沒想考算法,因此連算法複雜度的評估都沒打算問。實際狀況是,我每每沒有機會問這些問題,由於沒幾我的寫出來。

吐槽與建議

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

有兩位是某App的開發者。我把他們的App下載下來,發現了一堆bug後,原本想忍忍、就當沒看見、碼農何苦爲難碼農,而後手機發熱、卡頓、滅屏後幾乎點亮不了(內存泄露吃光了RAM,致使系統進程沒有內存可用)。過了一陣最終好了,我查看耗電排行,運行10分鐘就高居榜首,耗了17%的電——我嚇得馬上卸載了。一個第三方App能把系統給卡成這樣,通常人還真作不到。

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

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

如今,不少碼農都戲稱本身是在「搬磚」、複製粘貼,但實際上程序員的工做不可能僅止於此。使用別人寫好的基本算法,參考別人的實現代碼,只是爲了集中精力去解決抽象層次更高的業務問題。

「咱們不寫代碼,咱們只作代碼的搬運工。」——萬萬不可把這句話當作信條。

還有不少人,在沒有Demo的狀況下,不管給多麼詳細的API或其它資料,仍然沒法寫代碼。他們只能在既有的基礎上,修修補補,沒法憑空創做。

我推薦三本Java的基礎書:

  • 《Java編程思想》(Think in Java) 這本是最合適的Java語言入門書。其它不少語法書都是從C/C++的角度來說Java的變化,或者從C++的思路來討論Java怎麼用,而這本書的英文名則直接告訴你,請用Java來思考、解決問題。

  • 《Effective Java》 Java中有不少坑,Java中也有不少糖。若是沒有看過這本書,那麼不知不覺就會犯不少大忌。

  • 《代碼整潔之道》(Clean Code) 我是在獨立寫一個小項目的時候開始看的。看到一半時項目也寫到一半,頓時連代碼都不會寫了!天天都在寫本身看不下去的代碼,而不知道怎麼寫能看得過眼的。加速看完後,從新開始會寫代碼。最終,項目後半部分的代碼,和前半部分徹底不一樣,不像一我的寫的,我後來又重構了一遍。

(爲何我很少推薦點書呢?一來,是我本人也沒看過多少,囧;二來,三本是極限,根據個人經驗,推薦三本可讓人看一本,推薦三本以上,受衆一本也不會看。)

我有一個朋友,也是一個前同事,好學如好色。他週末都在找一個大學教室看書,甚至有時請年假去教室看書。一本《Java編程思想》,逐行精讀三遍以上。工做經驗不足兩年,跳槽三次,如今在一家百億級上市公司,年薪三十萬,統率十人。

究其緣由,無非基礎知識紮實,口水噴死麪試官爾

後記

我做爲一個面試別人的初哥,一心只想着本身喜歡的抽插……呃不,插入排序算法,給別人形成了沒必要要的麻煩,只能說抱歉了。

和我一塊兒面試的,還有一個負責文檔和規範的妹子(前面提到過的那個)。她也是有一票否決權的,並且用得比我更頻繁!

我最多隻給交白卷的,和那個耗電超恐怖的App,這幾我的直接否決。而這個妹子,考察對方的其它綜合能力。

在實際工做中,她是需求的接口人,咱們須要和她溝通,實現各方對咱們團隊的需求。因此,只要她說一句「我和這我的難以溝通」,那麼Boss基本上就直接拒絕了。

下次我仍是換一道更簡單的吧,碼農何苦爲難碼農。

相關文章
相關標籤/搜索