2021-Java后端工程師面試指南-(Java基礎(chǔ)篇)

前言

文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/bin392328206/six-finger
種一棵樹最好的時(shí)間是十年前,其次是現(xiàn)在

Tips

面試指南系列,很多情況下不會(huì)去深挖細(xì)節(jié),是小六六以被面試者的角色去回顧知識的一種方式,所以我默認(rèn)大部分的東西,作為面試官的你,肯定是懂的。

https://www.processon.com/view/link/600ed9e9637689349038b0e4

上面的是腦圖地址

叨絮

可能大家覺得有點(diǎn)老生常談了,確實(shí)也是。面試題,面試寶典,隨便一搜,根本看不完,也看不過來,那我寫這個(gè)的意義又何在呢?其實(shí)嘛我寫這個(gè)的有以下的目的

  • 第一就是通過一個(gè)體系的復(fù)習(xí),讓自己前面的寫的文章再重新的過一遍,總結(jié)升華嘛
  • 第二就是通過寫文章幫助大家建立一個(gè)復(fù)習(xí)體系,我會(huì)將大部分會(huì)問的的知識點(diǎn)以點(diǎn)帶面的形式給大家做一個(gè)導(dǎo)論

然后下面是前面的文章匯總

最后就是以面試題的形式來回顧所有的知識點(diǎn),會(huì)整理一些比較常見的面試題和自己實(shí)際開發(fā)碰到的問題等題目。

Java基本功

Java字符型常量和字符串常量的區(qū)別?

我們在開發(fā)的過程中,用的比較多的應(yīng)該是字符串,所以要熟悉下字符常量,我們可以回答

  • 形式上: 字符常量是單引號引起的一個(gè)字符; 字符串常量是雙引號引起的 0 個(gè)或若干個(gè)字符
  • 含義上: 字符常量相當(dāng)于一個(gè)整型值( ASCII 值),可以參加表達(dá)式運(yùn)算; 字符串常量代表一個(gè)地址值(該字符串在內(nèi)存中存放位置)
  • 占內(nèi)存大小 字符常量只占 2 個(gè)字節(jié); 字符串常量占若干個(gè)字節(jié)

String和StringBuilder、StringBuffer的區(qū)別?

  • Java 平臺(tái)提供了兩種類型的字符串:String 和 StringBuffer/StringBuilder,它們可以儲(chǔ)存和操作字符串。
  • 其中 String 是只讀字符串,也就意味著 String 引用的字符串內(nèi)容是不能被改變的。
  • 而 StringBuffer/StringBuilder 類表示的字符串對象可以直接進(jìn)行修改。StringBuilder 是 Java 5 中引入的,它和 StringBuffer 的方法完全相同,區(qū)別在于它是在單線程環(huán)境下使用的,因?yàn)樗乃蟹矫娑紱]有被 synchronized 修飾,因此它的效率也比 StringBuffer 要高。

小六六多嘴下,其實(shí)也是在告誡自己,其實(shí)我相信這個(gè)題目的答案大部分的人都是會(huì)的,也背的滾瓜爛熟,但是我們真正在開發(fā)的過程中確是沒有遵守的,比如有時(shí)候我們處理一些邏輯的時(shí)候,需要拼接些字段的時(shí)候我們會(huì)習(xí)慣的用+,不知道有沒有同款開發(fā)。哈哈,小六六和大家一起盡量養(yǎng)成好的開發(fā)習(xí)慣哈

說說反射的用途及實(shí)現(xiàn)

Java 反射機(jī)制是一個(gè)非常強(qiáng)大的功能,在很多的項(xiàng)目比如 Spring,MyBatis 都都可以看到反射的身影。通過反射機(jī)制,我們可以在運(yùn)行期間獲取對象的類型信息。利用這一點(diǎn)我們可以實(shí)現(xiàn)工廠模式和代理模式等設(shè)計(jì)模式,同時(shí)也可以解決 Java 泛型擦除等令人苦惱的問題。

獲取一個(gè)對象對應(yīng)的反射類,在 Java 中有下列方法可以獲取一個(gè)對象的反射類

  • new 一個(gè)對象,然后對象.getClass()方法
  • 通過 Class.forName() 方法
  • 使用 類.class

說說有沒有碰到BigDecimal的坑,或者說需要注意的點(diǎn)

  • 我們在使用BigDecimal時(shí),為了防止精度丟失,推薦使用它的 BigDecimal(String) 構(gòu)造方法來創(chuàng)建對象。
    其實(shí)就是想知道我們的實(shí)際開發(fā)是否有注意到這些點(diǎn),

說說你平時(shí)是怎么把一個(gè)逗號隔開的字符串變成一個(gè)集合的,類似于("1,2,3")這種

  • List myList = Arrays.stream(myArray).collect(Collectors.toList()),建議使用這種方式,而不是List myList = Arrays.asList(1, 2, 3);
  • 至于原因就是Arrays.asList()將數(shù)組轉(zhuǎn)換為集合后,底層其實(shí)還是數(shù)組

說說String s="abc"和String s=new String("abc")區(qū)別;

  • 相信大家對這道題并不陌生,答案也是眾所周知的,1個(gè)或2個(gè)。
  • 首先在堆中(不是常量池)創(chuàng)建一個(gè)指定的對象"abc",并讓str引用指向該對象
  • 在字符串常量池中查看,是否存在內(nèi)容為"abc"字符串對象
  • 若存在,則將new出來的字符串對象與字符串常量池中的對象聯(lián)系起來
  • 若不存在,則在字符串常量池中創(chuàng)建一個(gè)內(nèi)容為"abc"的字符串對象,并將堆中的對象與之聯(lián)系起來

聊聊Java中的SPI

系統(tǒng)設(shè)計(jì)的各個(gè)抽象,往往有很多不同的實(shí)現(xiàn)方案,在面向的對象的設(shè)計(jì)里,一般推薦模塊之間基于接口編程,模塊之間不對實(shí)現(xiàn)類進(jìn)行硬編碼。一旦代碼里涉及具體的實(shí)現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實(shí)現(xiàn),就需要修改代碼。為了實(shí)現(xiàn)在模塊裝配的時(shí)候能不在程序里動(dòng)態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機(jī)制。
Java SPI就是提供這樣的一個(gè)機(jī)制:為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)的機(jī)制。有點(diǎn)類似IOC的思想,就是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要。所以SPI的核心思想就是解耦。

聊聊Java 8有哪些特性

  • 函數(shù)式接口
  • 接口可以有實(shí)現(xiàn)方法,而且不需要實(shí)現(xiàn)類去實(shí)現(xiàn)其方法。
  • lambda表達(dá)式
  • stream流
  • 日期時(shí)間 API LocalDateTime年月日十分秒;LocalDate日期;LocalTime時(shí)間
  • Optional 類
    雖然說目前最新的版本已經(jīng)是15了,但是大部分企業(yè)還是用的8,所以就聊聊這個(gè)拉。

說說成員變量與局部變量的區(qū)別有哪些?

  • 成員變量可以被 public,private,static 等修飾符所修飾,而局部變量不能被訪問控制修飾符及 static 所修飾;但是,成員變量和局部變量都能被 final 所修飾。
  • 從變量在內(nèi)存中的存儲(chǔ)方式來看:如果成員變量是使用static修飾的,那么這個(gè)成員變量是屬于類的,如果沒有使用static修飾,這個(gè)成員變量是屬于實(shí)例的。而對象存在于堆內(nèi)存,局部變量則存在于棧內(nèi)存
  • 從變量在內(nèi)存中的生存時(shí)間上看:成員變量是對象的一部分,它隨著對象的創(chuàng)建而存在,而局部變量隨著方法的調(diào)用而自動(dòng)消失。
  • 成員變量如果沒有被賦初值:則會(huì)自動(dòng)以類型的默認(rèn)值而賦值(一種情況例外:被 final 修飾的成員變量也必須顯式地賦值),而局部變量則不會(huì)自動(dòng)賦值。

說說構(gòu)造方法有哪些特性?

  • 名字與類名相同。
  • 沒有返回值,但不能用 void 聲明構(gòu)造函數(shù)。
  • 生成類的對象時(shí)自動(dòng)執(zhí)行,無需調(diào)用。

說說==和equals

  • == : 它的作用是判斷兩個(gè)對象的地址是不是相等。即,判斷兩個(gè)對象是不是同一個(gè)對象(基本數(shù)據(jù)類型==比較的是值,引用數(shù)據(jù)類型==比較的是內(nèi)存地址)。
  • equals() : 它的作用也是判斷兩個(gè)對象是否相等。但它一般有兩種使用情況:
    • 情況 1:類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個(gè)對象時(shí),等價(jià)于通過“==”比較這兩個(gè)對象。
    • 情況 2:類覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來比較兩個(gè)對象的內(nèi)容是否相等;若它們的內(nèi)容相等,則返回 true (即,認(rèn)為這兩個(gè)對象相等)。

聊聊Java的四種引用,強(qiáng)弱軟虛

小六六這邊說下,其實(shí)我們作為一個(gè)crud仔,平時(shí)真正接觸到的也許就是強(qiáng)引用了,但是也不是說其他的引用方式?jīng)]有用,存在即合理,我們來看看他們具體的作用吧!感覺應(yīng)該放到JVM模塊的,算了,就這個(gè)吧

  • 強(qiáng)引用:最普遍的一種引用方式,如String s = "abc",變量s就是字符串“abc”的強(qiáng)引用,只要強(qiáng)引用存在,則垃圾回收器就不會(huì)回收這個(gè)對象。
  • 軟引用:用于描述還有用但非必須的對象,如果內(nèi)存足夠,不回收,如果內(nèi)存不足,則回收。一般用于實(shí)現(xiàn)內(nèi)存敏感的高速緩存(自定義內(nèi)存cached的時(shí)候用),軟引用可以和引用隊(duì)列ReferenceQueue聯(lián)合使用,如果軟引用的對象被垃圾回收,JVM就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。
  • 弱引用:弱引用和軟引用大致相同,弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。(這個(gè)就比較垃圾了)
  • 虛引用:虛引用不會(huì)改變對象的生命周期,如果一個(gè)對象僅持有虛引用,那么它就和沒有任何引用一樣。(其實(shí)小六六自己對于虛引用的理解還是因?yàn)門hreadLocal,防止我們忘記remove)

Java容器集合

聊聊數(shù)組的特點(diǎn)唄

  • 一致性:數(shù)組只能保存相同數(shù)據(jù)類型元素,元素的數(shù)據(jù)類型可以是任何相同的數(shù)據(jù)類型。
  • 有序性:數(shù)組中的元素是有序的,通過下標(biāo)訪問。
  • 不可變性:數(shù)組一旦初始化,則長度(數(shù)組中元素的個(gè)數(shù))不可變。 數(shù)組是一種連續(xù)存儲(chǔ)線性結(jié)構(gòu),元素類型相同,大小相等

說說Java常見的集合有哪些

嗯,我覺得這題會(huì)經(jīng)常問,算是一個(gè)對集合考查的引入吧

  • Map接口和Collection接口是所有集合框架的父接口
  • Collection接口的子接口包括:Set接口和List接口
  • Map接口的實(shí)現(xiàn)類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  • Set接口的實(shí)現(xiàn)類主要有:HashSet、TreeSet、LinkedHashSet等
  • List接口的實(shí)現(xiàn)類主要有:ArrayList、LinkedList、Stack以及Vector等

簡單聊聊 Set、List、Map的區(qū)別

List

  • 可以允許重復(fù)的對象。
  • 可以插入多個(gè)null元素。
  • 是一個(gè)有序容器,保持了每個(gè)元素的插入順序,輸出的順序就是插入的順序。

Set

  • 不允許重復(fù)對象
  • 無序容器,你無法保證每個(gè)元素的存儲(chǔ)順序,TreeSet通過 Comparator 或者 Comparable 維護(hù)了一個(gè)排序順序。
  • 只允許一個(gè) null 元素

Map

  • Map不是collection的子接口或者實(shí)現(xiàn)類。Map是一個(gè)接口。
  • Map 的 每個(gè) Entry 都持有兩個(gè)對象,也就是一個(gè)鍵一個(gè)值,Map 可能會(huì)持有相同的值對象但鍵對象必須是唯一的。
  • Map 里你可以擁有隨意個(gè) null 值但最多只能有一個(gè) null 鍵。

他們的使用場景

  • 如果你經(jīng)常會(huì)使用索引來對容器中的元素進(jìn)行訪問,那么 List 是你的正確的選擇。如果你已經(jīng)知道索引了的話,那么 List 的實(shí)現(xiàn)類比如 ArrayList 可以提供更快速的訪問,如果經(jīng)常添加刪除元素的,那么肯定要選擇LinkedList。
  • 如果你想容器中的元素能夠按照它們插入的次序進(jìn)行有序存儲(chǔ),那么還是 List,因?yàn)?List 是一個(gè)有序容器,它按照插入順序進(jìn)行存儲(chǔ)。
  • 如果你想保證插入元素的唯一性,也就是你不想有重復(fù)值的出現(xiàn),那么可以選擇一個(gè) Set 的實(shí)現(xiàn)類,比如 HashSet、LinkedHashSet 或者 TreeSet。所有 Set 的實(shí)現(xiàn)類都遵循了統(tǒng)一約束比如唯一性,而且還提供了額外的特性比如 TreeSet 還是一個(gè) SortedSet,所有存儲(chǔ)于 TreeSet 中的元素可以使用 Java 里的 Comparator 或者 Comparable 進(jìn)行排序。LinkedHashSet 也按照元素的插入順序?qū)λ鼈冞M(jìn)行存儲(chǔ)。
  • 如果你以鍵和值的形式進(jìn)行數(shù)據(jù)存儲(chǔ)那么 Map 是你正確的選擇。你可以根據(jù)你的后續(xù)需要從 Hashtable、HashMap、TreeMap 中進(jìn)行選擇。 `

來聊聊 ArrayList和LinkedList區(qū)別

  • ArrayList是實(shí)現(xiàn)了基于動(dòng)態(tài)數(shù)組的數(shù)據(jù)結(jié)構(gòu),LinkedList基于鏈表的數(shù)據(jù)結(jié)構(gòu)。
  • 對于隨機(jī)訪問get和set,ArrayList覺得優(yōu)于LinkedList,因?yàn)長inkedList要移動(dòng)指針。
  • 對于新增和刪除操作add和remove,LinedList比較占優(yōu)勢,因?yàn)锳rrayList要移動(dòng)數(shù)據(jù)。

說說Vector和ArrayList 區(qū)別:

  • Vector的方法都是同步的(Synchronized),是線程安全的(thread-safe),而ArrayList的方法不是,由于線程的同步必然要影響性能,因此,ArrayList的性能比Vector好。
  • 當(dāng)Vector或ArrayList中的元素超過它的初始大小時(shí),Vector會(huì)將它的容量翻倍,而ArrayList只增加50%的大小,這樣,ArrayList就有利于節(jié)約內(nèi)存空間。
  • 我們也可以用Collections.synchronizedList 來生成一個(gè)線程安全的List

說說HashSet和TreeSet的區(qū)別:

  • TreeSet 是二叉樹實(shí)現(xiàn)的,Treeset中的數(shù)據(jù)是自動(dòng)排好序的,不允許放入null值。
  • HashSet 是哈希表實(shí)現(xiàn)的,HashSet中的數(shù)據(jù)是無序的,可以放入null,但只能放入一個(gè)null,兩者中的值都不能重復(fù),就如數(shù)據(jù)庫中唯一約束。
  • HashSet要求放入的對象必須實(shí)現(xiàn)HashCode()方法,放入的對象,是以hashcode碼作為標(biāo)識的,而具有相同內(nèi)容的 String對象,hashcode是一樣,所以放入的內(nèi)容不能重復(fù)。但是同一個(gè)類的對象可以放入不同的實(shí)例 。

說說HashMap和HashTable的區(qū)別:

  • HashMap是非線程安全的,Hashtable是線程安全的,所以Hashtable重量級一些,因?yàn)槭褂昧藄ynchronized關(guān)鍵字來保證線程安全。
  • HashMap允許key和value都為null,而Hashtable都不能為null。
  • HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當(dāng)有其它線程改變了HashMap的結(jié)構(gòu)(增加或者移除元素),將會(huì)拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會(huì)拋出ConcurrentModificationException異常

上面的是我們集合的基礎(chǔ),下面來看看一些進(jìn)階面試題

能不能深度的總結(jié)下ArrayList

  • 默認(rèn)的無參構(gòu)造的數(shù)組大小為10
  • ArrayList用for循環(huán)遍歷比iterator迭代器遍歷快
  • 一起來總結(jié)下它的擴(kuò)容過程,第一步判斷是否需要擴(kuò)容(就是通過計(jì)算當(dāng)前我數(shù)組的長度加上我要添加到數(shù)組的長度的和minCapacity 去和當(dāng)前容量去比較,如果需要的話,那就進(jìn)行第一次擴(kuò)容,第一次擴(kuò)容是的容量大小是原來的1.5倍,擴(kuò)容之后再把擴(kuò)容之后的值和前面的那個(gè)minCapacity和比較 如果還小的話,就進(jìn)行第二次擴(kuò)容,把容量擴(kuò)成minCapacity,如果minCapacity大于最大容量,則就擴(kuò)容為最大容量21億左右

為什么ArrayList的elementData是用transient修飾的

  • ArrayList實(shí)現(xiàn)了Serializable接口,這意味著ArrayList是可以被序列化的,用transient修飾elementData意味著我不希望elementData數(shù)組被序列化
  • 序列化ArrayList的時(shí)候,ArrayList里面的elementData未必是滿的,比方說elementData有10的大小,但是我只用了其中的3個(gè),那么是否有必要序列化整個(gè)elementData呢?顯然沒有這個(gè)必要,因此ArrayList中重寫了writeObject方法。

你重寫過 hashcode 和 equals 么,為什么重寫 equals 時(shí)必須重寫 hashCode 方法?

  • 如果兩個(gè)對象相等,則 hashcode 一定也是相同的
  • 兩個(gè)對象相等,對兩個(gè)對象分別調(diào)用 equals 方法都返回 true
  • 兩個(gè)對象有相同的 hashcode 值,它們也不一定是相等的
  • 因此,equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋
  • hashCode() 的默認(rèn)行為是對堆上的對象產(chǎn)生獨(dú)特值。如果沒有重寫 hashCode(),則該 class 的兩個(gè)對象無論如何都不會(huì)相等(即使這兩個(gè)對象指向相同的數(shù)據(jù))

聊聊HashSet 如何檢查重復(fù)

當(dāng)你把對象加入HashSet時(shí),HashSet 會(huì)先計(jì)算對象的hashcode值來判斷對象加入的位置,同時(shí)也會(huì)與其他加入的對象的 hashcode 值作比較,如果沒有相符的 hashcode,HashSet 會(huì)假設(shè)對象沒有重復(fù)出現(xiàn)。但是如果發(fā)現(xiàn)有相同 hashcode 值的對象,這時(shí)會(huì)調(diào)用equals()方法來檢查 hashcode 相等的對象是否真的相同。如果兩者相同,HashSet 就不會(huì)讓加入操作成功。

說說HashMap 的底層實(shí)現(xiàn)

JDK1.8 之前
JDK1.8 之前 HashMap 底層是 數(shù)組和鏈表 結(jié)合在一起使用也就是 鏈表散列。HashMap 通過 key 的 hashCode 經(jīng)過擾動(dòng)函數(shù)處理過后得到 hash 值,然后通過 (n - 1) & hash 判斷當(dāng)前元素存放的位置(這里的 n 指的是數(shù)組的長度),如果當(dāng)前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鏈法解決沖突。

所謂擾動(dòng)函數(shù)指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動(dòng)函數(shù)是為了防止一些實(shí)現(xiàn)比較差的 hashCode() 方法 換句話說使用擾動(dòng)函數(shù)之后可以減少碰撞。

JDK1.8之后
相比于之前的版本, JDK1.8 之后在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長度大于閾值(默認(rèn)為 8)(將鏈表轉(zhuǎn)換成紅黑樹前會(huì)判斷,如果當(dāng)前數(shù)組的長度小于 64,那么會(huì)選擇先進(jìn)行數(shù)組擴(kuò)容,而不是轉(zhuǎn)換為紅黑樹)時(shí),將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時(shí)間。

那么你可以聊聊紅黑樹嗎

首先我們知道紅黑數(shù)是一個(gè)二叉查找樹, 它有以下的特點(diǎn)

  • 左子樹上所有結(jié)點(diǎn)的值均小于或等于它的根結(jié)點(diǎn)的值。
  • 右子樹上所有結(jié)點(diǎn)的值均大于或等于它的根結(jié)點(diǎn)的值。
  • 左、右子樹也分別為二叉排序樹。
    但是一般的二叉樹在極端情況下,可能變成線性查找了,那么就失去它的查找特性意義了,而紅黑樹是一個(gè)自平衡樹,它最重要的是增加了下面3個(gè)規(guī)則來確保它的自平衡
  • 每個(gè)葉子節(jié)點(diǎn)都是黑色的空節(jié)點(diǎn)(NIL節(jié)點(diǎn))。
  • 每個(gè)紅色節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)都是黑色。(從每個(gè)葉子到根的所有路徑上不能有兩個(gè)連續(xù)的紅色節(jié)點(diǎn))
  • 從任一節(jié)點(diǎn)到其每個(gè)葉子的所有路徑都包含相同數(shù)目的黑色節(jié)點(diǎn)。

這里小六六說下,紅黑樹還是很復(fù)雜的,所以一般往下不會(huì)問了,如果變態(tài)點(diǎn),就還會(huì)問問自平衡的過程,這邊我就不一一解釋了,大家自行去找。它的變色,它的左旋,右旋,哈哈確實(shí)掉頭發(fā)。。

HashMap 的長度為什么是 2 的冪次方

  • 首先考慮奇數(shù)行不行,在計(jì)算hash的時(shí)候,確定落在數(shù)組的位置的時(shí)候,計(jì)算方法是(n - 1) & hash ,奇數(shù)n-1為偶數(shù),偶數(shù)2進(jìn)制的結(jié)尾都是0,經(jīng)過&運(yùn)算末尾都是0,會(huì)增加hash沖突,并且有一半的空間不會(huì)有數(shù)據(jù)
  • 為啥要是2的冪,不能是2的倍數(shù)么,比如6,10?
    • hashmap 結(jié)構(gòu)是數(shù)組,每個(gè)數(shù)組里面的結(jié)構(gòu)是node(鏈表或紅黑樹),正常情況下,如果你想放數(shù)據(jù)到不同的位置,肯定會(huì)想到取余數(shù)確定放在那個(gè)數(shù)據(jù)里, 計(jì)算公式:
      hash % n,這個(gè)是十進(jìn)制計(jì)算。在計(jì)算機(jī)中, (n - 1) & hash,當(dāng)n為2次冪時(shí),會(huì)滿足一個(gè)公式:(n - 1) & hash = hash % n,計(jì)算更加高效。
    • 只有是2的冪數(shù)的數(shù)字經(jīng)過n-1之后,二進(jìn)制肯定是 ...11111111 這樣的格式,這種格式計(jì)算的位置的時(shí)候(&),完全是由產(chǎn)生的hash值類決定,而不受n-1(組數(shù)長度的二進(jìn)制) 影響。你可能會(huì)想,
      受影響不是更好么,又計(jì)算了一下,類似于擾動(dòng)函數(shù),hash沖突可能更低了,這里要考慮到擴(kuò)容了,2的冪次方*2,在二進(jìn)制中比如4和8,代表2的2次方和3次方,他們的2進(jìn)制結(jié)構(gòu)相 似,比如
      4和8 00000100 0000 1000 只是高位向前移了一位,這樣擴(kuò)容的時(shí)候,只需要判斷高位hash,移動(dòng)到之前位置的倍數(shù)就可以了,免去了重新計(jì)算位置的運(yùn)算。

HashMap 的擴(kuò)容伐值為什么是0.75

  • 當(dāng)負(fù)載因子是1.0的時(shí)候,也就意味著,只有當(dāng)數(shù)組的8個(gè)值(這個(gè)圖表示了8個(gè))全部填充了,才會(huì)發(fā)生擴(kuò)容。這就帶來了很大的問題,因?yàn)镠ash沖突時(shí)避免不了的。當(dāng)負(fù)載因子是1.0的時(shí)候,意味著會(huì)出現(xiàn)大量的Hash的沖突,底層的紅黑樹變得異常復(fù)雜。對于查詢效率極其不利。這種情況就是犧牲了時(shí)間來保證空間的利用率。
  • 負(fù)載因子是0.5的時(shí)候,這也就意味著,當(dāng)數(shù)組中的元素達(dá)到了一半就開始擴(kuò)容,既然填充的元素少了,Hash沖突也會(huì)減少,那么底層的鏈表長度或者是紅黑樹的高度就會(huì)降低。查詢效率就會(huì)增加。
  • 基本上為什么是0.75的答案也就出來了,這是時(shí)間和空間的權(quán)衡。

HashMap樹化的條件

這個(gè)小六六說下,如果沒看過源碼,肯定印象沒有那么深刻

  • 第一個(gè)肯定是發(fā)生了hash碰撞
  • 并且鏈表的長度大于8就會(huì)樹化,小于6之后會(huì)退化
  • 還有一個(gè)條件就是整個(gè)hashmap的容量要大于64

JDK1.7版本的hashmap死循環(huán)問題知道嗎

小六六也大致說下,就是1.7在多線程的情況下,擴(kuò)容的時(shí)候,假設(shè)2個(gè)線程同時(shí)擴(kuò)容導(dǎo)致的我們鏈表的相互引用,導(dǎo)致的死循環(huán),也就是我們所說的鏈表尾插,其實(shí)這也不算bug,因?yàn)楣俜矫鞔_說了 hashmap不應(yīng)該在多線程的環(huán)境下使用,具體大家自行百度

說說hashmap的put操作

  • 第一步當(dāng)然是先計(jì)算key的hash值(有過處理的 (h = key.hashCode()) ^ (h >>> 16))
  • 第二步調(diào)用putval方法,然后判斷是否容器中全部為空,如果是的話,就把容器的容量擴(kuò)容。
  • 第三步,把最大容量和hash值求&值(i = (n - 1) & hash),判斷這個(gè)數(shù)組下標(biāo)是否有數(shù)據(jù),如果沒有就把它放進(jìn)去。還要判斷key的equals方法,看是否需要覆蓋。
  • 第四步,如果有,說明發(fā)生了碰撞,那么繼續(xù)遍歷判斷鏈表的長度是否大于8,如果大于8,就繼續(xù)把當(dāng)前鏈表變成紅黑樹結(jié)構(gòu)。
  • 第五步,如果沒有到8,那么就直接把數(shù)據(jù)存在鏈表的尾部
  • 第六步,最后將容器的容量+1。

說說hashmap的resize的操作

  • 如果到達(dá)最大容量,那么返回當(dāng)前的桶,并不再進(jìn)行擴(kuò)容操作,否則的話擴(kuò)容為原來的兩倍,返回?cái)U(kuò)容后的桶;
  • 根據(jù)擴(kuò)容后的桶,修改其他的成員變量的屬性值;
  • 根據(jù)新的容量創(chuàng)建新的擴(kuò)建后的桶,并更新桶的引用;
  • 如果原來的桶里面有元素就需要進(jìn)行元素的轉(zhuǎn)移;
  • 在進(jìn)行元素轉(zhuǎn)移的時(shí)候需要考慮到元素碰撞和轉(zhuǎn)紅黑樹操作;
  • 在擴(kuò)容的過程中,按次從原來的桶中取出鏈表頭節(jié)點(diǎn),并對該鏈表上的所有元素重新計(jì)算hash值進(jìn)行分配;
  • 在發(fā)生碰撞的時(shí)候,將新加入的元素添加到末尾;
  • 在元素復(fù)制的時(shí)候需要同時(shí)對低位和高位進(jìn)行操作。

聊聊ConcurrentHashMap 1.7和1.8的比較

ConcurrentHashMap是conccurrent家族中的一個(gè)類,由于它可以高效地支持并發(fā)操作,以及被廣泛使用,經(jīng)典的開源框架Spring的底層數(shù)據(jù)結(jié)構(gòu)就是使用ConcurrentHashMap實(shí)現(xiàn)的。與同是線程安全的老大哥HashTable相比,它已經(jīng)更勝一籌,因此它的鎖更加細(xì)化,而不是像HashTable一樣為幾乎每個(gè)方法都添加了synchronized鎖,這樣的鎖無疑會(huì)影響到性能。

1.7和1.8實(shí)現(xiàn)線程安全的思想也已經(jīng)完全變了其中拋棄了原有的Segment 分段鎖,而采用了 CAS + synchronized 來保證并發(fā)安全性。它沿用了與它同時(shí)期的HashMap版本的思想,底層依然由“數(shù)組”+鏈表+紅黑樹的方式思想。

  • 不采用segment而采用node,鎖住node來實(shí)現(xiàn)減小鎖粒度。
  • 設(shè)計(jì)了MOVED狀態(tài) 當(dāng)resize的中過程中 線程2還在put數(shù)據(jù),線程2會(huì)幫助resize。
  • 使用3個(gè)CAS操作來確保node的一些操作的原子性,這種方式代替了鎖。
  • sizeCtl的不同值來代表不同含義,起到了控制的作用。

聊聊ConcurrentHashMap的sizeCtl

  • 負(fù)數(shù)代表正在進(jìn)行初始化或擴(kuò)容操作
  • -1代表正在初始化
  • -N 表示有N-1個(gè)線程正在進(jìn)行擴(kuò)容操作
  • 正數(shù)或0代表hash表還沒有被初始化,這個(gè)數(shù)值表示初始化或下一次進(jìn)行擴(kuò)容的大小,這一點(diǎn)類似于擴(kuò)容閾值的概念。還后面可以看到,它的值始終是當(dāng)前ConcurrentHashMap容量的0.75倍,這與loadfactor是對應(yīng)的。

說說ConcurrentHashMap的put方法

  • 第一步,一進(jìn)去肯定判斷key value是否為null 如果為null 拋出異常
  • 第二步,當(dāng)添加一對鍵值對的時(shí)候,首先會(huì)去判斷保存這些鍵值對的數(shù)組是不是初始化了,如果沒有就初始化數(shù)組。
  • 第三步, 通過計(jì)算hash值來確定放在數(shù)組的哪個(gè)位置如果這個(gè)位置為空則直接添加(CAS的加鎖方式),如果不為空的話,則取出這個(gè)節(jié)點(diǎn)來
  • 第四步,如果取出來的節(jié)點(diǎn)的hash值是MOVED(-1)的話,則表示當(dāng)前正在對這個(gè)數(shù)組進(jìn)行擴(kuò)容,復(fù)制到新的數(shù)組,則當(dāng)前線程也去幫助復(fù)制
  • 第五步,如果這個(gè)節(jié)點(diǎn),不為空,也不在擴(kuò)容,則通過synchronized來加鎖,進(jìn)行添加操作
  • 第六步,如果是鏈表的話,則遍歷整個(gè)鏈表,直到取出來的節(jié)點(diǎn)的key來個(gè)要放的key進(jìn)行比較,如果key相等,并且key的hash值也相等的話,則說明是同一個(gè)key,則覆蓋掉value,否則的話則添加到鏈表的末尾
  • 第七步,如果是樹的話,則調(diào)用putTreeVal方法把這個(gè)元素添加到樹中去
  • 第八步,最后在添加完成之后,會(huì)判斷在該節(jié)點(diǎn)處共有多少個(gè)節(jié)點(diǎn)(注意是添加前的個(gè)數(shù)),如果達(dá)到8個(gè)以上了的話,則調(diào)用treeifyBin方法來嘗試將處的鏈表轉(zhuǎn)為樹,或者擴(kuò)容數(shù)

結(jié)尾

這個(gè)就是一些基礎(chǔ)的面試題,當(dāng)然還是很多不一定那么全,但是如果把這些都能回答出來,其實(shí)Java基礎(chǔ)這塊也算是蠻扎實(shí)了。

日常求贊

好了各位,以上就是這篇文章的全部內(nèi)容了,能看到這里的人呀,都是真粉。

創(chuàng)作不易,各位的支持和認(rèn)可,就是我創(chuàng)作的最大動(dòng)力,我們下篇文章見

微信 搜 "六脈神劍的程序人生" 回復(fù)888 有我找的許多的資料送給大家

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容