Java Case Interview
- 2021 年 4 月 30 日
- 筆記
什麼是面向對象?
面向對象和面向過程的區別:
- 面向過程更注重每一個步驟以及其順訊,面向對象更注重哪些對象,他們具有哪些能力
- 面向過程比較直接,而面向對象更易於復用、擴展和維護
三大特性:
封裝:內部細節隱藏 只提供對外的介面
- javabean屬性只能通過set方法賦值,不能使用Classname.filed直接賦值。
繼承:子類共性的方法和屬性在父類中體現出來,子類只需要做出特性的擴展即可。
多態:繼承,方法重寫,父類引用指向子類
JVM 虛擬機棧
java 棧
Oracle frame interpretation
每一個方法被調用時,就有一個新的棧幀被創建。當方法調用完成時,不管是拋出異常還是正常返回棧幀都會被銷毀。
棧幀由java虛擬機棧中創建該棧幀的執行緒來分配。每個棧幀都有自己的本地變數,操作數棧,動態鏈接(返回方法的值或者拋出的異常)。
局部變數表(Local Variables):每個棧幀都有一個局部變數表(一個數組),可以存放類型為boolean, byte,
char, short, int, float, reference, or returnAddress。在32位JVM中long,double類型佔用連續兩個變數位置。
每一個棧中的變數表從0號位開始,0號位位當前方法的調用者(this),任何局部變數都是從變數表 1號位開始。
操作數棧(Operand Stacks):JVM提供指令載入常量或者值從本地方法列表或者屬性到操作數棧。其他Java JVM
可以對操作數棧中的值進行操作(計算),然後彈棧返回結果到操作數棧。操作數棧也用作方法參數的傳遞以及
接收方法的返回值。 任何時候每個操作數棧都有自己的深度,long、double都要佔用兩個單元深度,其他的類型的值佔用一個操作數單元。
動態鏈接(Dynamic Linking):每個棧幀都會引用一個支援動態鏈接到當前方法區方法的運行時常量池。被引用到的位元組碼方法會被調用,
變數將可以通過符號引用進行訪問。動態鏈接將這些符號鏈接翻譯為具體的方法引用,載入還沒有符號引用的的類,翻譯變數的記憶體地址與運行時的記憶體地址將關聯。
如何判斷對象是否成為垃圾?
引用計數法:當有一個地方使用計數值+1,失效時-1,為0時是不可再被引用的對象
缺點:循環引用時,某些對象將無法被回收掉
final 關鍵字
-
修飾成員變數
如果final修飾的是類變數,只能在靜態初始化塊中指定初始化值或者聲明該類變數時指定初始值。
如果final修飾的是成員變數,可以在非靜態塊初始化,聲明該變數或者構造器中執行初始化值。 -
修飾局部變數
一定要賦值且只賦值一次,變數地址不能再次賦值 -
為什麼內部類只能訪問帶final的外部變數?
原因一:如果內部類的方法執行完成,但是內部類對象還存在,並且引用了一個無效的成員變數。
原因二:局部變數修改,和內部類的變數值在內部改變,那麼也會出問題。
所以只能訪問帶final的外部變數。
StringBuilder StringBuffer String 區別
String是final修飾的,不可變,每次操作都會產生新的對象。
StringBuffer 和 StringBuilder 都是在原對象上操作,StringBuilder從JDK 5 開始
優先使用StringBuilder,多執行緒使用共享變數時用StringBuffer
重載和重寫
重載:發生在同一個類中,方法名必須相同,參數類型不同、個數不同、順序不同,方法的返回值和訪問修飾符可以不同。發生在編譯時。
重寫:發生在父子類中,方法名,(一同兩小一大)參數列表必須相同。返回值類型小於父類,拋出異常小於父類,訪問修飾符大於父類;
如果附列訪問的修飾符位private則子類就不能重寫該方法。靜態的方法不能被重寫,只能被隱藏。發生在運行時。
介面和抽象類的區別
單繼承多實現
抽象類可以由具體方法,介面不可以有
介面都是靜態類屬性public static final.
抽象類可以集中實現公共的方法,這樣寫子類時只需要擴展特定的方法,提高了程式碼的復用性。
介面是定義行為,不關心子類怎麼實現。
抽象類只能繼承一個類,需要寫出所有子類的所有共性,難度較高。
而介面在功能上就會弱化很多,他們只是針對一個動作的描述,在設計時會降低難度。
List 和 Set 區別
List: 有序可重複 允許多個null元素對象,可以使用iterator迭代器遍曆元素,還可以使用下標遍歷。
Set: 無序,不可重複,最多允許一個null元素對象,取元素時只能使用迭代器進行遍歷。
HashCode 和 equals
hashcode 的作用是獲取哈希碼,可以用來確認該對象在哈希表中的索引位置。HashCode()定義在JDK的Object中,
Java中的任何類都包含有HashCode()函數。散列表存儲的是鍵值對,能根據鍵快速檢索出對應的值。比較兩個對
象是否為同一對象,HashCode相同時,還會調用equals方法。
注意:hashCode是對象在堆上產生的獨特的值,如果沒有重寫hashCode(),則該class的兩個對象始終不會相等。
ArrayList 和 LinkedList
ArrayList 動態數組,連續記憶體存儲,查詢快,刪除效率較低,但是在初始容量給得夠的情況下尾部追加元素的的效率也是極高的。
LinkedList鏈表,可以分散存儲在記憶體中。適合做數據插入刪除操作,不適合查詢。
使用for循環遍歷,或者indexOf返回索引都是效率極低的,一般使用迭代器iterator進行遍歷。
HashMap 和 Hashtable
HashMap 執行緒不安全,HashTable 執行緒安全(方法都被sychronized加鎖)
HashMap 允許一個null鍵和多個null 值,而Hashtable則不允許。
底層數據結構 數組+鏈表
JDK8 開始鏈表高度為8 數組長度超過64 時鏈表會扭轉為紅黑樹,元素以內部類Node節點存在。數組長度低於6時紅黑樹扭轉為鏈表
-
計算key的Hash值,二次hash然後對數組長度取模,對應到數組下標
-
如果沒有Hash衝突,創建Node存入數組
-
如果產生Hash衝突,先進行equals比較,相同則取代該元素;不同,則判斷鏈表的高度插入鏈表。
-
key為null值,存在下標為0的位置。
ConcurrentHashMap jdk7 和 jdk8區別
jdk7
數據結構:ReentrantLock + segment + hashEntry, 一個Segment中包含一個HashEntry數組,每個HashEtry又是一個鏈表結構
元素查找:二次Hash,第一次Hash定位到Segment位置,第二次Hash定位到元素所在的鏈表頭部
鎖:Segment分段鎖,Segment繼承了Reentrantlock,鎖定操作的Segment,其他的Segment 不受影響,
並發度為Segment個數,可以通過構造函數指定,數組擴容不會影響其他的Segment。
get方法無需加鎖,volitile保證寫都在主記憶體中。
jdk8
數據結構sychronized+CAS+紅黑樹 Node的val和next都用volatile修飾,保證對其他執行緒的可見性
查找替換賦值都是用CAS
鎖:鎖鏈表的head節點,不影響其他元素的讀寫,鎖力度更細,效率更高,擴容時,阻塞所有的讀寫操作,並發擴容。
讀操作無鎖:
Node的val和next都用volatile修飾,保證對其他執行緒的可見性。
數組採用volatile修飾是為了保證擴容時,對其他執行緒可見。
如何實現一個IOC容器
- 配置文件配置、註解配置包掃描路徑
- 遞歸包掃描獲取.class文件,將所有被特定註解(@component)標記的類全路徑名放到一個set集合中
- 遍歷set集合,獲取類上有指定註解的類,並將其交給IOC容器,定義一個安全的Map用來存儲這些對象
- 遍歷這個IOC容器,後去到每一個類的實例,判斷裡面是否有依賴注入的對象還沒有注入,然後進行依賴注入。
雙親委派模型
三種類載入器
BootStrapClassLoader 默認載入%JAVA_HOME%/lib 下jar包和class文件
ExtClassLoader 負責載入%JAVA_HOME%/lib/ext 下jar包和class文件
AppClassLoader 是自定義類載入器的父類(parent屬性指向),負責載入classpath下的類文件
向上委派 查找快取
向下查找 查找載入路徑 該路徑下有該類則載入 否則向下查找
安全性:雙親委派 保證了類只會被載入一次 ,避免用戶編寫核心java類被載入。
相同的類被不同的載入器載入就是不同的兩個類。
java中的異常體系
Error 異常是程式無法處理的會造成程式停止;Exception則不會造成程式停止
RuntimeException 發生在程式運行過程中,會導致程式當前執行緒執行失敗。
CheckedException 發生在程式編譯的過程中,會導致程式編譯不通過。
GC如何判斷對象可以被回收
引用計數法: 每個對象有一個引用計數屬性,新增一個引用計數加1,釋放一個引用計數鍵減1,計數為0時可以回收。
可達性分析:通過一系列的稱為GC ROOTS
的對象作為起點,往下搜索(路徑為引用鏈),
當對象不與GC任何引用鏈相連時,則這些對象是不可達的。
GC ROOTS對象包括:
1.虛擬機棧中引用的對象
2.方法區中靜態屬性或者常量引用的對象
3.本地方法引用的對象
可達性演算法中不可達對象不是立即死亡的,對象擁有一次自我拯救的機會。
對象被系統宣告死亡至少經理兩次標記過程。第一次經過可達性分析發現沒有與GC ROOTS 引用鏈相連,
第二次是在虛擬機自動創建的Finalizer隊列中判斷是否需要執行finalize()方法。
當對象變成不可達狀態時,GC會判斷該對象是否覆蓋了finalize方法,未覆蓋則直接將其回收,否則
若對象未執行finalize方法,將其放入F-Queue隊列,由低級優先執行緒執行該隊列中對象的finalize方法。
執行方法完畢後,GC會再次判斷該對象是否可達,若不可達,則進行回收,否則,對象復活。
finalize()方法運行的代價大,每個對象只能觸發一次。一般被用來釋放資源。
執行緒的狀態
創建、就緒、運行、阻塞、死亡
阻塞:
- wait阻塞 通過notify(Object方法) 或者 notifyAll喚醒
- sychronize阻塞
- other 阻塞 : sleep(Thread方法)或者join 或者發生了IO請求時,JVM會把該吸納城置為阻塞狀態。
sleep wait join yield區別
- sleep是Thread靜態本地方法;wait是Ojbect類的本地方法
- sleep不釋放鎖;wait釋放鎖,並加入到等待隊列中
- sleep不需要被喚醒;wait需要喚醒
- sleep方法不依賴於synchronized關鍵字;但是wait需要依賴
- sleep會讓出CPU執行時間且強制切換上下文;而wait不一定,被notify之後還是有機會競爭到鎖
yield 執行後執行緒進入就緒狀態,馬上釋放了CPU的執行權,但是依然保留了CPU的性執行這個。
join執行後執行緒進入阻塞狀態,在A執行緒中B.join則在執行完B之後在執行A。
ThreadLocal 的原因和使用場景
每一個Thread對象均含有一個ThreadLocalMap類型的成員變數threadLocals變數
ThreadLocalMap
由一個Entry對象構成,Entry對象繼承自WeakReference<ThreadLocal<?>>
一個Entry由ThreadLocal對象和Object構成; 當沒有對象強引用ThreadLocal對象後,該key就會被垃圾
收集器收集.
ThreadLocal 內部類-> ThreadLocalMap 內部類-> Entry
set方法
// ThreadLocal 方法
public void set(T value) {
// 獲取當前執行緒對象
Thread t = Thread.currentThread();
// 獲取t執行緒ThreadLocal對象的內部類對象ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 以ThreadLocal對象為鍵 入參為值
map.set(this, value);
else
createMap(t, value);
}
// ThreadLocalMap 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 從計算後的i位置開始,逐個比較引用如果相同則替換掉value值
// 如果e的引用為空,則會替換掉相應的值,並且刪除未被引用的值
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法
// ThreadLocal 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 獲取Entry內部類
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果當前執行緒ThreadLocalMap屬性值為空
// 則獲取初始化值,並創建ThreadLocalMap
return setInitialValue();
}
// ThreadLocalMap 方法
private Entry getEntry(ThreadLocal<?> key) {
// 使用key的Hashcode & Entry的數組長度
// 得到key在數組中的可能位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 如果未命中 則從下面方法去獲取
return getEntryAfterMiss(key, i, e);
}
// ThreadLocalMap 方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
// e的引用和調用get()方法的執行緒為同一執行緒時返回該Entry
if (k == key)
return e;
// 如果e的引用為空 則觸發刪除不被引用的Entry對象,
// 包括之前不被引用的其他Entry
if (k == null)
expungeStaleEntry(i);
else
// ((i + 1 < len) ? i + 1 : 0)
// 一定會有一個i命中i可能不是從0開始,所以上面超出數組長度時置位0
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
使用場景
- 當一些屬性需要誇很多層方法傳遞時 可以使用 避免一直傳遞參數
- 數據在執行緒之間的安全線,每個執行緒持有一個ThreadLocalMap對象
- 進行事務操作時嗎,用於存儲事務資訊
- 資料庫連接,Session會話管理
四種引用類型 看下面的文章足以
參考博文出處
ThreadLocal記憶體泄漏如何避免
ThreadLcoalMap中Entry的key置位null,被GC回收後,
如果執行緒還持有對Entry中的value的引用就造成記憶體泄漏。
key可以通過手動置位null或者使用弱引用;value可以調用set方法將value置為null
或者remove方法將Entry置為null(還是會調用expungeStaleEntry())
如果有get,set的時候有調用到ThreadLocalMap上expungeStaleEntry(),會將value和Entry置位null
使用原則
- 使用完ThreadLocal之後,及時清除value值
- 定義ThreadLocal變數為private static變數,這樣就一直存在ThreadLocal的強引用,方便使用和清除操作
一個執行緒只有一個ThreadLocalMap,那為什麼ThreadLocalMap中要維護一個Entry[]
因為一個Thread中可以有多個ThreadLocal,Entry在數組中的位置由key.threadLocalHashCode & (table.length - 1)
ThreadLcoal的hash值和數組的長度(數組長度超過threshold,並且沒有清理掉過期的Entry,數組中的數據會轉移到另
一個長度更大的數組中)來確定
參考資料
歡迎關注微信公眾號哦~ ~