1. 程式人生 > >面試話癆(二)C:JAVA String,別以為你穿個馬甲我就不認識你了

面試話癆(二)C:JAVA String,別以為你穿個馬甲我就不認識你了

  面試話癆系列是從技術廣度的角度去回答面試官提的問題,適合萌新觀看!

   面試官,別再問我火箭怎麼造了,我知道螺絲的四種擰法,你想聽嗎?


  String相關的題目,是面試中經常考察的點,當面試中遇到了String相關的問題,我們可以這麼聊:

一:String底層結構

  從底層結構上來說,jdk1.8的String,底層是char[]。我在工作中幾乎很少用到char[],因為List太好用了,我寧願用List<Character>也不想用char[],因為以前學C時體會過用char[]的痛苦,長度必須事先設定好,也沒有豐富的API去處理資料。所以剛開始用java時,覺得String這個類簡直太好用了(直視面試官的眼睛,露出有點不好意思的笑)。後來才知道,原來是原始碼在替我們負重前行。

  比如我們都知道String的拼接,大部分情況下都是新建一個空間(這裡可以用稍微緩慢不確定的語氣說這句話,稍有心的面試官就會問你什麼情況下不會新建空間,你就可以回答:1.連續相加時jdk會自動優化成一次空間的新建,2.兩個字串常量相加的值如果是已經存在的字串常量,那麼會直接指向這個已存在的字元常量),新建的原因就是因為底層是一個char[],無法直接擴容。而新建空間的花費很多,所以對於有多次拼接需求的情況,我會酌情選擇StringBuffer和StringBuilder,主要是看是否存在多執行緒的情況,存在的話就用StringBuffer(執行緒安全的方式,涉及的知識點很多,可以專門寫一章,先留個傳送門:面試話癆(三)我會鎖的三種配法,您配嗎?)。

 

  另外,我有次看jdk11裡面String的底層已經變成了byte[],順手查了一下,說是用byte[]存的話,可以減少大概一半的記憶體佔用,再具體的怎麼實現怎麼優化的沒太仔細瞭解。(對於自己不太懂的知識,介紹完自己懂得部分以後,一定要接一句:再往下自己就不清楚了之類的話。這樣面試官就不會再接著問你這個問題)。

  題外話:字元之間可以直接使用‘+’和‘-’運算子,實際計算的是字元對應的ASCII碼的位置的加減,如 ‘1’ - ‘0’ = (int)1,用這個方法可以快速的完成char到int的強轉,很多String類的題目都需要用到這個方法。

 

二:final修飾符

  因為char[]本身就是一個不能擴容的陣列,所以用不可變的常量去修飾字符串就很適合。另外,final的特性也能為String帶來很多的好處:

  1. 安全方面來說,密碼、個人資訊等基本都是以String為載體來進行儲存的,final修飾的String類不可以被繼承,建立的物件也不可以被改變,可以保證關鍵資料的安全性。

  2. 效能方面來說,final修飾後,String就被放入了常量池,常量池中有專門的字串常量池,JVM可以將多個一樣的String指向同一個地址,其中有任意一個String改變時,因為final的特性都會去重新建一個地址(或者指向另外一個值恰好相同的地址),不會影響原來的值;另外,String的不可變性讓它的hashcode是固定的,可以被快取的,用來做Map的key運算更快捷;還有,final修飾後,也不會存在多執行緒安全的問題。

  (本來想接著說一下常量的,但是一想到常量,腦子裡就蹦出了方法區,堆,棧,JDK版本更替、GC方式,類載入,JVM記憶體模型,Java記憶體模型,可見性,volatile,自旋鎖,太多了。先留個坑吧。面試話癆(四)常量在哪裡呀,常量在哪裡)。

  與final長得像的,還有finally和finalize。finally就是跟在try或者catch後面的一個關鍵字,以前我們學的是,try之後必定會執行finally,但其實這個是有前提的,就是程式不崩潰或者不被強制結束,try中加一句 System.exit(0); ,finally就不會被執行。

  至於finalize,這個方法幾乎沒人用了,它是Object類中就自帶的方法,學名解構函式,聽說是在java剛出生時,為了迎合C程式設計人員的習慣新增的方法。在物件快要被回收時呼叫且只會被呼叫一次。如果finalize中的程式碼將另外一個指標指向了該物件,那麼JVM就會放棄該物件的回收。等到下一次該物件又不可達了,JVM就會直接回收,不會再呼叫finalize方法。因此方法存在不確定性,很少被使用。

 

三:equals

  在Obejct中,equals和==是一樣的,都是直接比較資料的存放地址是否一致,而在String中,equals方法被重寫成三個步驟的判斷。

  

  HashMap中的equals大致也是使用了這三個步驟的判斷:地址是否相等 --> size是否相等 --> 每一個key是否有equals的key,對應的value是否equals。

  不同的類對於equals的實現方式不一樣,但他們都遵循若hashcode不相等,則equals也不相等的原則。這樣做的目的主要是為了讓Hash類集合插入值時的重複判定更合理:

   

  試想一下,假設兩個身份證物件的hashcode不相等,equals卻相等,那麼兩個相同身份證資訊就會被放入HashSet中了!這會對我們的編碼造成很大的困擾,所以對於需要被用於Hash的key值的物件(HashSet的值插入相當於是將值放入了key中,再插入了一個固定value值的HashMap),我們需要滿足若hashcode不相等,則equals也不相等的原則。

  HashSet中的元素重複判定,優先判定hashcode的原因是:hashcode通過與HashSet的大小取餘以後,可以快速的定位到可能相等的元素的位置,這時再對該位置上存在的元素的進行equals就行,只要hash分佈的足夠均勻,該操作的時間複雜度就接近於O(1),而若去掉hashcode定址,直接使用equals對n個數進行對比的話,時間複雜度就是O(n)。

  說到這裡還可以提一個經常被問到的問題,HashMap的查詢時間複雜度是多少?

 

  如我上面的分析一樣,時間複雜度是接近於O(1)的,當然也會有O(n)的情況。比如新建了一個類,hashcode值全部返回1,這樣所有的值都會接到一個連結串列上,陣列+連結串列的結構就退化成純連結串列,時間複雜度就變成了O(n)。jdk1.8以後,連結串列長度超過8,並且陣列長度大於64時,連結串列會變成紅黑樹,紅黑樹的遍歷時間複雜度為O(log(n))。(紅黑樹、B樹、索引、資料庫、圖、廣度搜索、深度搜索相關的資料結構及演算法後面再說。面試話癆(N)還沒有頭緒不知道什麼時間寫)。

 


  最後,送大家一道我特別喜歡的面試題。 

    請將一段字串轉換成整數,不要使用parseInt。

 

    public int MyParseInt(String str) throws Exception{
        /*
            1.資料校驗
            (不管面試過程中遇到什麼題,都可以先優雅的寫下
                //1.資料校驗
              然後胸有成竹的寫一些有的沒的校驗,寫的過程中再想後面應該怎麼寫。
              寫註釋+周全的校驗是寫程式碼的基本素養)
         */
        String errMsg = check(str);
        if (errMsg != null) {
            throw new Exception(errMsg);
        }
        char[] c = str.toCharArray();
        int result = 0;
        for (int i = 0; i < c.length; i++) {
            result = result * 10 + (c[i] - '0');
        }
        return result;
    }

    /**
     * 
     * @param str 待校驗的資料
     * @return 錯誤資訊,沒有錯誤時返回null
     */
    private String check(String str) {
        if (str == null) {
            return "輸入不能為空!";
        }
        /*
            很少有人能一遍寫出不用除錯沒有bug的程式碼,更別說在面試那麼緊張的氣氛中了
            所以,在寫完能夠基本實現題目要求的程式碼以後,最好能想下程式碼中不足的地方,然後坦誠的告訴面試官:
                這個程式碼還有需要改進的地方,比如可以新增對 負號、正號、小數點的支援,比如沒有校驗int的閥值,
                如果是工作中遇到,我肯定能解決這些問題。
         */
        if (str.replaceAll("[0-9]","").length() > 0){
            return "包含非法字元";
        }
        return null;
    }

   本章中提到的一些面試回答技巧這裡再寫一下,我個人覺得挺有用的:

  1、 對於自己擅長的問題,可以在問題中留下一些點,吸引面試官繼續提問。

  2、 對於自己不擅長的問題,回答完自己會的部分以後,可以直接說再往下自己就不懂了,不要不懂裝懂,讓面試官繼續問下去後再回答不知道。

  3、 如果面試官已經問了你不知道的問題,儘量從效能和安全方面,說出一個答案。直接先坦誠的告訴面試官你不知道,只是根據個人經驗這個應該是這樣的,因為這樣對於安全/效能的優點是xxx。


   目錄如下:

   面試話癆(一)讓我們來熱切的討論這個養豬場吧

   面試話癆(二)C:JAVA String,別以為你穿個馬甲我就不認識你了