面試話癆(二)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,別以為你穿個馬甲我就不認識你了