JAVA面试整理之——JAVA基础
1.???? HashMap的源碼,實現原理,JDK8中對HashMap做了怎樣的優化。
在JDK1.6,JDK1.7中,HashMap采用位桶+鏈表實現,即使用鏈表處理沖突,同一hash值的鏈表都存儲在一個鏈表里。HashMap由鏈表+數組組成,他的底層結構是一個數組,而數組的元素是一個單向鏈表。但是當位于一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查找的效率較低。而JDK1.8中,HashMap采用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹,這樣大大減少了查找時間。
簡單說下HashMap的實現原理:
首先有一個每個元素都是鏈表(可能表述不準確)的數組,當添加一個元素(key-value)時,就首先計算元素key的hash值,以此確定插入數組中的位置,但是可能存在同一hash值的元素已經被放在數組同一位置了,這時就添加到同一hash值的元素的后面,他們在數組的同一位置,但是形成了鏈表,同一各鏈表上的Hash值是相同的,所以說數組存放的是鏈表。而當鏈表長度太長時,鏈表就轉換為紅黑樹,這樣大大提高了查找的效率。
當鏈表數組的容量超過初始容量的0.75時,再散列將鏈表數組擴大2倍,把原鏈表數組的搬移到新的數組中
存取機制:
HashMap如何getValue值,看源碼:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab;//Entry對象數組 Node<K,V> first,e; //在tab數組中經過散列的第一個位置 int n; K k; /*找到插入的第一個Node,方法是hash值和n-1相與,tab[(n - 1) & hash]*/ //也就是說在一條鏈上的hash值相同的 if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) { /*檢查第一個Node是不是要找的Node*/ if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k))))//判斷條件是hash值要相同,key值要相同 return first; /*檢查first后面的node*/ if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); /*遍歷后面的鏈表,找到key值和hash值都相同的Node*/ do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } View Codeget(key)方法時獲取key的hash值,計算hash&(n-1)得到在鏈表數組中的位置first=tab[hash&(n-1)],先判斷first的key是否與參數key相等,不等就遍歷后面的鏈表找到相同的key值返回對應的Value值即可
HashMap如何put(key,value);看源碼
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /*如果table的在(n-1)&hash的值是空,就新建一個節點插入在該位置*/ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); /*表示有沖突,開始處理沖突*/ else { Node<K,V> e; K k; /*檢查第一個Node,p是不是要找的值*/ if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { /*指針為空就掛在后面*/ if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //如果沖突的節點數已經達到8個,看是否需要改變沖突節點的存儲結構, //treeifyBin首先判斷當前hashMap的長度,如果不足64,只進行 //resize,擴容table,如果達到64,那么將沖突的存儲結構為紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } /*如果有相同的key值就結束遍歷*/ if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } /*就是鏈表上有相同的key值*/ if (e != null) { // existing mapping for key,就是key的Value存在 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;//返回存在的Value值 } } ++modCount; /*如果當前大小大于門限,門限原本是初始容量*0.75*/ if (++size > threshold) resize();//擴容兩倍 afterNodeInsertion(evict); return null; } View Code下面簡單說下添加鍵值對put(key,value)的過程:
1,判斷鍵值對數組tab[]是否為空或為null,否則以默認大小resize();
2,根據鍵值key計算hash值得到插入的數組索引i,如果tab[i]==null,直接新建節點添加,否則轉入3
3,判斷當前數組中處理hash沖突的方式為鏈表還是紅黑樹(check第一個節點類型即可),分別處理
?
2.???? HaspMap擴容是怎樣擴容的,為什么都是2的N次冪的大小。
HashMap使用的是懶加載,構造完HashMap對象后,只要不進行put 方法插入元素之前,HashMap并不會去初始化或者擴容table:
若threshold(閾值)不為空,table的首次初始化大小為閾值,否則初始化為缺省值大小16
當table需要擴容時,擴容后的table大小變為原來的兩倍,接下來就是進行擴容后table的調整:假設擴容前的table大小為2的N次方,有put方法解析可知,元素的table索引為其hash值的后N位確定,那么擴容后的table大小即為2的N+1次方,則其中元素的table索引為其hash值的后N+1位確定,比原來多了一位
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table; //創建一個oldTab數組用于保存之前的數組int oldCap = (oldTab == null) ? 0 : oldTab.length; //獲取原來數組的長度int oldThr = threshold; //原來數組擴容的臨界值int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) { //如果原來的數組長度大于最大值(2^30)threshold = Integer.MAX_VALUE; //擴容臨界值提高到正無窮return oldTab; //返回原來的數組,也就是系統已經管不了了,隨便你怎么玩吧 }//else if((新數組newCap)長度乘2) < 最大值(2^30) && (原來的數組長度)>= 初始長度(2^4))//這個else if 中實際上就是咋判斷新數組(此時剛創建還為空)和老數組的長度合法性,同時交代了,//我們擴容是以2^1為單位擴容的。下面的newThr(新數組的擴容臨界值)一樣,在原有臨界值的基礎上擴2^1else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold }else if (oldThr > 0)newCap = oldThr; //新數組的初始容量設置為老數組擴容的臨界值else { // 否則 oldThr == 0,零初始閾值表示使用默認值newCap = DEFAULT_INITIAL_CAPACITY; //新數組初始容量設置為默認值newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //計算默認容量下的閾值 }if (newThr == 0) { //如果newThr == 0,說明為上面 else if (oldThr > 0)//的情況(其他兩種情況都對newThr的值做了改變),此時newCap = oldThr;float ft = (float)newCap * loadFactor; //ft為臨時變量,用于判斷閾值的合法性newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE); //計算新的閾值 }threshold = newThr; //改變threshold值為新的閾值@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab; //改變table全局變量為,擴容后的newTableif (oldTab != null) {for (int j = 0; j < oldCap; ++j) { //遍歷數組,將老數組(或者原來的桶)遷移到新的數組(新的桶)中Node<K,V> e;if ((e = oldTab[j]) != null) { //新建一個Node<K,V>類對象,用它來遍歷整個數組oldTab[j] = null;if (e.next == null)//將e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置,newTab[e.hash & (newCap - 1)] = e; //這個我們之前講過,是一個取模操作else if (e instanceof TreeNode) //如果e已經是一個紅黑樹的元素,這個我們不展開講((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // 鏈表重排,這一段是最難理解的,也是ldk1.8做的一系列優化,我們在下面詳細講解Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;} View Code因此,table中的元素只有兩種情況:
元素hash值第N+1位為0:不需要進行位置調整
元素hash值第N+1位為1:調整至原索引的兩倍位置
在resize方法中,確定元素hashi值第N+1位是否為0:
若為0,則使用loHead與loTail,將元素移至新table的原索引處
若不為0,則使用hiHead與hiHead,將元素移至新table的兩倍索引處
擴容或初始化完成后,resize方法返回新的table
?
hashMap為啥初始化容量為2的次冪
hashMap源碼獲取元素的位置:
static int indexFor(int hashcode, int length) {
??? // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
??? return hashcode & (length-1);
}
如果length不為2的冪,比如15。那么length-1的2進制就會變成1110。在h為隨機數的情況下,和1110做&操作。尾數永遠為0。那么0001、1001、1101等尾數為1的位置就永遠不可能被entry占用。這樣會造成浪費,不隨機等問題
?
3.???? HashMap,HashTable,ConcurrentHashMap的區別。
HashTable
底層數組+鏈表實現,無論key還是value都不能為null,線程安全,實現線程安全的方式是在修改數據時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關優化
初始size為11,擴容:newsize = olesize*2+1
計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
底層數組+鏈表實現,可以存儲null鍵和null值,線程不安全
初始size為16,擴容:newsize = oldsize*2,size一定為2的n次冪
擴容針對整個Map,每次擴容時,原來數組中的元素依次重新計算存放位置,并重新插入
插入元素后才判斷該不該擴容,有可能無效擴容(插入后如果擴容,如果沒有再次插入,就會產生無效擴容)
當Map中元素總數超過Entry數組的75%,觸發擴容操作,為了減少鏈表長度,元素分配更均勻
計算index方法:index = hash & (tab.length – 1)
ConcurrentHashMap
底層采用分段的數組+鏈表實現,線程安全
通過把整個Map分為N個Segment,可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。(讀操作不加鎖,由于HashEntry的value變量是 volatile的,也能保證讀取到最新的值。)
Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨占,ConcurrentHashMap允許多個修改操作并發進行,其關鍵在于使用了鎖分離技術
有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢后,又按順序釋放所有段的鎖
擴容:段內擴容(段內元素超過該段對應Entry數組長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容
?
4.???? 極高并發下HashTable和ConcurrentHashMap哪個性能更好,為什么,如何實現的。
ConcurrentHashMap在多線程下效率更高
HashTable使用一把鎖處理并發問題,當有多個線程訪問時,需要多個線程競爭一把鎖,導致阻塞
ConcurrentHashMap則使用分段,相當于把一個HashMap分成多個,然后每個部分分配一把鎖,這樣就可以支持多線程訪問
推薦:https://blog.csdn.net/zhushuai1221/article/details/51706468
5.???? HashMap在高并發下如果沒有處理線程安全會有怎樣的安全隱患,具體表現是什么。
1、多線程put時可能會導致get無限循環,具體表現為CPU使用率100%;
原因:在向HashMap put元素時,會檢查HashMap的容量是否足夠,如果不足,則會新建一個比原來容量大兩倍的Hash表,然后把數組從老的Hash表中遷移到新的Hash表中,遷移的過程就是一個rehash()的過程,多個線程同時操作就有可能會形成循環鏈表,所以在使用get()時,就會出現Infinite Loop的情況
2、多線程put時可能導致元素丟失
原因:當多個線程同時執行addEntry(hash,key ,value,i)時,如果產生哈希碰撞,導致兩個線程得到同樣的bucketIndex去存儲,就可能會發生元素覆蓋丟失的情況
?
6.???? java中四種修飾符的限制范圍。
| 訪問權限 | 類 | 包 | 子類 | 其他包 |
| public???? | ∨ | ∨ | ∨ | ∨ |
| protect??? | ∨ | ∨ | ∨ | × |
| default??? | ∨ | ∨ | × | × |
| private??? | ∨ | × | × | × |
?
7.???? Object類中的方法。
equale()用于確認兩個對象是否相同。 hashCode()用于獲取對象的哈希值,用于檢索 finalize():這個函數在進行垃圾回收的時候會用到,匿名對象回收之前會調用到 toString()返回一個String對象,用來標識自己 getClass()返回一個Class對象 wait()用于讓當前線程失去操作權限,當前線程進入等待序列 notify()用于隨機通知一個持有對象的鎖的線程獲取操作權限 notifyAll()用于通知所有持有對象的鎖的線程獲取操作權限 wait(long) 和wait(long,int)用于設定下一次獲取鎖的距離當前釋放鎖的時間間隔 |
?
?
8.???? 接口和抽象類的區別,注意JDK8的接口可以有實現。
首先是相同的地方:
1. 接口和抽象類都能定義方法和屬性。
2. 接口和抽象類都是看作是一種特殊的類。大部分的時候,定義的方法要子類來實現
3. 抽象類和接口都可以不含有抽象方法。接口沒有方法就可以作為一個標志。比如可序列化的接口Serializable,沒有方法的接口稱為空接口
4. 抽象類和接口都不能創建對象。
5. 抽象類和接口都能利用多態性原理來使用抽象類引用指向子類對象。
6. 繼承和實現接口或抽象類的子類必須實現接口或抽象類的所有的方法,抽象類若有沒有實現的方法就繼續作為抽象類,要加abstract修飾。若接口的子類沒有實現的方法,也要變為抽象類。
?
下面是接口和抽象類的不同點:
1. 接口能夠多實現,而抽象類只能單獨被繼承,其本質就是,一個類能繼承多個接口,而只能繼承一個抽象類。
2. 方法上,抽象類的方法可以用abstract 和public或者protect修飾。而接口默認為public abttact 修飾。
3. 抽象類的方法可以有需要子類實現的抽象方法,也可以有具體的方法。而接口在老版本的jdk中,只能有抽象方法,但是Java8版本的接口中,接口可以帶有默認方法。
4. 屬性上,抽象類可以用各種各樣的修飾符修飾。而接口的屬性是默認的public static final
5. 抽象類中可以含有靜態代碼塊和靜態方法,而接口不能含有靜態方法和靜態代碼塊。
6. 抽象類可以含有構造方法,接口不能含有構造方法。
7. 既然說到Java 8 那么就來說明,Java8中的接口中的默認方法是可以被多重繼承的。而抽象類不行。
8. 另外,接口只能繼承接口。而抽象類可以繼承普通的類,也能繼承接口和抽象類。
?
9.???? 動態代理的兩種方式,以及區別。
jdk動態代理和cglib動態代理。兩種方法同時存在,各有優劣。
jdk動態代理是由java內部的反射機制來實現的,cglib動態代理底層則是借助asm來實現的。
總的來說,反射機制在生成類的過程中比較高效,而asm在生成類之后的相關執行過程中比較高效(可以通過將asm生成的類進行緩存,這樣解決asm生成類過程低效問題)。還有一點必須注意:jdk動態代理的應用前提,必須是目標類基于統一的接口。如果沒有上述前提,jdk動態代理不能應用。由此可以看出,jdk動態代理有一定的局限性,cglib這種第三方類庫實現的動態代理應用更加廣泛,且在效率上更有優勢。。
?
10.?? Java序列化的方式。
a.是相應的對象實現了序列化接口Serializable,這個使用的比較多,對于序列化接口Serializable接口是一個空的接口,它的主要作用就是標識這個對象時可序列化的,jre對象在傳輸對象的時候會進行相關的封裝
b.實現序列化的第二種方式為實現接口Externalizable
?
11.?? 傳值和傳引用的區別,Java是怎么樣的,有沒有傳值引用。
1. 在java中所有的參數都是傳值的,引用符號&的傳遞是C++中才有的;
2. 在java傳參中,基本類型(byte--short--int--long--float--double--boolean--char)的變量總是按值傳遞;
3. 對于對象來說,不是將對象本身傳遞給方法,而是將對象的的引用或者說對象的首地址傳遞給方法,引用本身是按值傳遞的;
4. 對于String、Integer、Long,數組等,這些都相當于對象,因此傳參時相當于是傳引用;
?
12.?? 一個ArrayList在循環過程中刪除,會不會出問題,為什么。
for(int i),這種遍歷的時候刪除,被刪除元素的后面那個元素會被跳過,除非在循環里面操作i,你想想是不是,因為當前對象被remove了,下一個對象的序號就減一了,但是并沒有對下一個對象做判斷,i照常加1,就跳過了下一個對象
轉載于:https://www.cnblogs.com/yfz1552800131/p/9430503.html
總結
以上是生活随笔為你收集整理的JAVA面试整理之——JAVA基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 编译原理中词法分析--部分实现
- 下一篇: Shell 当前路径下找出所有空文件夹