Java Case Interview

WhaleFall541發表於2021-04-30

什麼是物件導向?

物件導向和麵向過程的區別:

  • 程式導向更注重每一個步驟以及其順訊,物件導向更注重哪些物件,他們具有哪些能力
  • 程式導向比較直接,而物件導向更易於複用、擴充套件和維護
    三大特性:
    封裝:內部細節隱藏 只提供對外的介面
  1. 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 關鍵字

  1. 修飾成員變數
    如果final修飾的是類變數,只能在靜態初始化塊中指定初始化值或者宣告該類變數時指定初始值。
    如果final修飾的是成員變數,可以在非靜態塊初始化,宣告該變數或者構造器中執行初始化值。

  2. 修飾區域性變數
    一定要賦值且只賦值一次,變數地址不能再次賦值

  3. 為什麼內部類只能訪問帶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容器

  1. 配置檔案配置、註解配置包掃描路徑
  2. 遞迴包掃描獲取.class檔案,將所有被特定註解(@component)標記的類全路徑名放到一個set集合中
  3. 遍歷set集合,獲取類上有指定註解的類,並將其交給IOC容器,定義一個安全的Map用來儲存這些物件
  4. 遍歷這個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區別

  1. sleep是Thread靜態本地方法;wait是Ojbect類的本地方法
  2. sleep不釋放鎖;wait釋放鎖,並加入到等待佇列中
  3. sleep不需要被喚醒;wait需要喚醒
  4. sleep方法不依賴於synchronized關鍵字;但是wait需要依賴
  5. 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;
    }

使用場景

  1. 當一些屬性需要誇很多層方法傳遞時 可以使用 避免一直傳遞引數
  2. 資料線上程之間的安全線,每個執行緒持有一個ThreadLocalMap物件
  3. 進行事務操作時嗎,用於儲存事務資訊
  4. 資料庫連線,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

使用原則

  1. 使用完ThreadLocal之後,及時清除value值
  2. 定義ThreadLocal變數為private static變數,這樣就一直存在ThreadLocal的強引用,方便使用和清除操作

一個執行緒只有一個ThreadLocalMap,那為什麼ThreadLocalMap中要維護一個Entry[]
因為一個Thread中可以有多個ThreadLocal,Entry在陣列中的位置由key.threadLocalHashCode & (table.length - 1)
ThreadLcoal的hash值和陣列的長度(陣列長度超過threshold,並且沒有清理掉過期的Entry,陣列中的資料會轉移到另
一個長度更大的陣列中)來確定

參考資料

  1. B站視訊地址
  2. JDK 8 ThreadLocal 部分原始碼
  3. Oracle 官方網站地址

歡迎關注微信公眾號哦~ ~

相關文章