面试必会系列 - 1.1 Java SE 基础
本文已收錄至 github,完整圖文:https://github.com/HanquanHq/MD-Notes
Java SE 基礎
面向對象
Java 按值調用還是引用調用?
按值調用指方法接收調用者提供的值,按引用調用指方法接收調用者提供的變量地址。
Java 總是 按值 調用,方法得到的是所有參數值的副本,傳遞對象時,實際上方法接收的是 對象引用的副本。方法不能修改基本數據類型的參數,如果傳遞了一個 int 值 ,改變值不會影響實參,因為改變的是值的一個副本。
可以改變對象參數的狀態,但不能讓對象參數引用一個新的對象。如果傳遞了一個 int 數組,改變數組的內容會影響實參,而改變這個參數的引用并不會讓實參引用新的數組對象。
什么是反射?
在運行狀態中,對于任意一個類都能知道它的所有屬性和方法,對于任意一個對象都能調用它的任意方法和屬性,這種動態獲取信息及調用對象方法的功能稱為反射。缺點是破壞了封裝性以及泛型約束。反射是框架的核心,Spring 大量使用反射。
Class 類的作用?如何獲取一個 Class 對象?
在程序運行期間,Java 運行時系統為所有對象維護一個 運行時類型標識,這個信息會跟蹤每個對象所屬的類,虛擬機利用運行時類型信息選擇要執行的正確方法,保存這些信息的類就是 Class,這是一個泛型類。
獲取 Class 對象:① 類名.class 。②對象的 getClass方法。③ Class.forName(類的全限定名)。
什么是注解?什么是元注解?
注解是一種標記,使類或接口附加額外信息,幫助編譯器和 JVM 完成一些特定功能,例如 @Override 標識一個方法是重寫方法。
元注解是自定義注解的注解,例如:
@Target:約束作用位置,值是 ElementType 枚舉常量,包括 METHOD 方法、VARIABLE 變量、TYPE 類/接口、PARAMETER 方法參數、CONSTRUCTORS 構造方法和 LOACL_VARIABLE 局部變量等。
@Rentention:約束生命周期,值是 RetentionPolicy 枚舉常量,包括 SOURCE 源碼、CLASS 字節碼和 RUNTIME 運行時。
@Documented:表明這個注解應該被 javadoc 記錄。
什么是泛型,有什么作用?
泛型本質是參數化類型,解決不確定對象具體類型的問題。泛型在定義處只具備執行 Object 方法的能力。
泛型的好處:① 類型安全,放置什么出來就是什么,不存在 ClassCastException。② 提升可讀性,編碼階段就顯式知道泛型集合、泛型方法等處理的對象類型。③ 代碼重用,合并了同類型的處理代碼。
泛型擦除是什么?
泛型用于編譯階段,編譯后的字節碼文件不包含泛型類型信息,因為虛擬機沒有泛型類型對象,所有對象都屬于普通類。例如定義 List<Object> 或 List<String>,在編譯后都會變成 List 。
定義一個泛型類型,會自動提供一個對應原始類型,類型變量會被擦除。如果沒有限定類型就會替換為 Object,如果有限定類型就會替換為第一個限定類型,例如 <T extends A & B> 會使用 A 類型替換 T。
JDK8 新特性有哪些?
**lambda 表達式:**允許把函數作為參數傳遞到方法,簡化匿名內部類代碼。
**函數式接口:**使用 @FunctionalInterface 標識,有且僅有一個抽象方法,可被隱式轉換為 lambda 表達式。
**方法引用:**可以引用已有類或對象的方法和構造方法,進一步簡化 lambda 表達式。
**接口:**接口可以定義 default 修飾的默認方法,降低了接口升級的復雜性,還可以定義靜態方法。
**注解:**引入重復注解機制,相同注解在同地方可以聲明多次。注解作用范圍也進行了擴展,可作用于局部變量、泛型、方法異常等。
**類型推測:**加強了類型推測機制,使代碼更加簡潔。
**Optional 類:**處理空指針異常,提高代碼可讀性。
**Stream 類:**引入函數式編程風格,提供了很多功能,使代碼更加簡潔。方法包括 forEach 遍歷、count 統計個數、filter 按條件過濾、limit 取前 n 個元素、skip 跳過前 n 個元素、map 映射加工、concat 合并 stream 流等。
**日期:**增強了日期和時間 API,新的 java.time 包主要包含了處理日期、時間、日期/時間、時區、時刻和時鐘等操作。
**JavaScript:**提供了一個新的 JavaScript 引擎,允許在 JVM上運行特定 JavaScript 應用。
異常有哪些分類?
所有異常都是 Throwable 的子類,分為 Error 和 Exception。Error 是 Java 運行時系統的內部錯誤和資源耗盡錯誤,例如 StackOverFlowError 和 OutOfMemoryError,這種異常程序無法處理。
Exception 分為受檢異常和非受檢異常,受檢異常需要在代碼中顯式處理,否則會編譯出錯,非受檢異常是運行時異常,繼承自 RuntimeException。
**受檢異常:**① 無能為力型,如字段超長導致的 SQLException。② 力所能及型,如未授權異常 UnAuthorizedException,程序可跳轉權限申請頁面。常見受檢異常還有 FileNotFoundException、ClassNotFoundException、IOException等。
**非受檢異常:**① 可預測異常,例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,這類異常應該提前處理。② 需捕捉異常,例如進行 RPC 調用時的遠程服務超時,這類異常客戶端必須顯式處理。③ 可透出異常,指框架或系統產生的且會自行處理的異常,例如 Spring 的 NoSuchRequestHandingMethodException,Spring 會自動完成異常處理,將異常自動映射到合適的狀態碼。
Java 有哪些基本數據類型?
| byte | 1 B | (byte)0 | -128 ~ 127 |
| short | 2 B | (short)0 | -2^15 ~ 2^15-1 |
| int | 4 B | 0 | -2^31 ~ 2^31-1 |
| long | 8 B | 0L | -2^63 ~ 2^63-1 |
| float | 4 B | 0.0F | ±3.4E+38(有效位數 6~7 位) |
| double | 8 B | 0.0D | ±1.7E+308(有效位數 15 位) |
| char | 英文 1B,中文 UTF-8 占 3B,GBK 占 2B | ‘\u0000’ | ‘\u0000’ ~ ‘\uFFFF’ |
| boolean | 單個變量 4B / 數組 1B | false | true、false |
JVM 沒有 boolean 賦值的專用字節碼指令,boolean f = false 就是使用 ICONST_0 即常數 0 賦值。單個 boolean 變量用 int 代替,boolean 數組會編碼成 byte 數組。
StringBuilder 或 StringBuffer 怎么實現的字符串拼接?
兩者的 append 方法都繼承自 AbstractStringBuilder,該方法首先使用 Arrays.copyOf 確定新的字符數組容量,再調用 getChars 方法使用 System.arraycopy 將新的值追加到數組中。StringBuilder 是 JDK5 引入的,效率高但線程不安全。StringBuffer 使用 synchronized 保證線程安全。
String a = “a” + new String(“b”) 創建了幾個對象?
常量和常量拼接仍是常量,結果在常量池,只要有變量參與拼接結果就是變量,存在堆。
使用字面量時只創建一個常量池中的常量,使用 new 時如果常量池中沒有該值就會在常量池中新創建,再在堆中創建一個對象引用常量池中常量。因此 String a = “a” + new String(“b”) 會創建四個對象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。
重載和重寫的區別?
重載指方法名稱相同,但參數類型個數不同,是行為水平方向不同實現。
重寫,@Override,指子類實現接口或繼承父類時,保持方法簽名完全相同,實現不同方法體,是行為垂直方向不同實現。
Object 類有哪些方法?
**equals:**檢測對象是否相等,默認使用 == 比較對象引用,可以重寫 equals 方法自定義比較規則。equals 方法規范:自反性、對稱性、傳遞性、一致性、對于任何非空引用 x,x.equals(null) 返回 false。
**hashCode:**散列碼是由對象導出的一個整型值,沒有規律,每個對象都有默認散列碼,值由對象存儲地址得出。字符串散列碼由內容導出,值可能相同。為了在集合中正確使用,一般需要同時重寫 equals 和 hashCode,要求 equals 相同 hashCode 必須相同,hashCode 相同 equals 未必相同,因此 hashCode 是對象相等的必要不充分條件。
**toString:**打印對象時默認的方法,如果沒有重寫打印的是表示對象值的一個字符串。
**clone:**clone 方法聲明為 protected,類只能通過該方法克隆它自己的對象,如果希望其他類也能調用該方法必須定義該方法為 public。如果一個對象的類沒有實現 Cloneable 接口,該對象調用 clone 方法會拋出一個 CloneNotSupport 異常。默認的 clone 方法是淺拷貝,一般重寫 clone 方法需要實現 Cloneable 接口并指定訪問修飾符為 public。
**finalize:**確定一個對象死亡至少要經過兩次標記,如果對象在可達性分析后發現沒有與 GC Roots 連接的引用鏈會被第一次標記,隨后進行一次篩選,條件是對象是否有必要執行 finalize 方法。假如對象沒有重寫該方法或方法已被虛擬機調用,都視為沒有必要執行。如果有必要執行,對象會被放置在 F-Queue 隊列,由一條低調度優先級的 Finalizer 線程去執行。虛擬機會觸發該方法但不保證會結束,這是為了防止某個對象的 finalize 方法執行緩慢或發生死循環。只要對象在 finalize 方法中重新與引用鏈上的對象建立關聯就會在第二次標記時被移出回收集合。由于運行代價高昂且無法保證調用順序,在 JDK 9 被標記為過時方法,并不適合釋放資源。
**getClass:**返回包含對象信息的類對象。
**wait / notify / notifyAll:**阻塞或喚醒持有該對象鎖的線程。
接口和抽象類的異同?
接口和抽象類對實體類進行更高層次的抽象,僅定義公共行為和特征。
| 成員變量 | 無特殊要求 | 默認 public static final 常量 |
| 構造方法 | 有構造方法,不能實例化 | 沒有構造方法,不能實例化 |
| 方法 | 抽象類可以沒有抽象方法,但有抽象方法一定是抽象類。 | 默認 public abstract,JDK8 支持默認/靜態方法,JDK9 支持私有方法。 |
| 繼承 | 單繼承 | 多繼承 |
抽象類體現 is-a 關系,接口體現 can-do 關系。與接口相比,抽象類通常是對同類事物相對具體的抽象。
抽象類是模板式設計,包含一組具體特征,例如某汽車,底盤、控制電路等是抽象出來的共同特征,但內飾、顯示屏、座椅材質可以根據不同級別配置存在不同實現。
接口是契約式設計,是開放的,定義了方法名、參數、返回值、拋出的異常類型,誰都可以實現它,但必須遵守接口的約定。例如所有車輛都必須實現剎車這種強制規范。
接口是頂級類,抽象類在接口下面的第二層,對接口進行了組合,然后實現部分接口。當糾結定義接口和抽象類時,推薦定義為接口,遵循接口隔離原則,按維度劃分成多個接口,再利用抽象類去實現這些,方便后續的擴展和重構。
例如 Plane 和 Bird 都有 fly 方法,應把 fly 定義為接口,而不是抽象類的抽象方法再繼承,因為除了 fly 行為外 Plane 和 Bird 間很難再找到其他共同特征。
子類初始化的順序
① 父類靜態代碼塊和靜態變量。
② 子類靜態代碼塊和靜態變量。
③ 父類普通代碼塊和普通變量。
④ 父類構造方法。
⑤ 子類普通代碼塊和普通變量。
⑥ 子類構造方法。
集合
說一說 ArrayList
ArrayList 是容量可變的非線程安全列表,使用數組實現,集合擴容時會創建更大的數組,把原有數組復制到新數組。支持對元素的快速隨機訪問,但插入與刪除速度很慢。ArrayList 實現了 RandomAcess 標記接口,如果一個類實現了該接口,那么表示使用索引遍歷比迭代器更快。
elementData 是 ArrayList 的數據域,被 transient 修飾,序列化時會調用 writeObject 寫入流,反序列化時調用 readObject 重新賦值到新對象的 elementData。原因是 elementData 容量通常大于實際存儲元素的數量,所以只需發送真正有實際值的數組元素。
size 是當前實際大小,elementData 大小大于等于 size。
**modCount **記錄了 ArrayList 結構性變化的次數,繼承自 AbstractList。所有涉及結構變化的方法都會增加該值。expectedModCount 是迭代器初始化時記錄的 modCount 值,每次訪問新元素時都會檢查 modCount 和 expectedModCount 是否相等,不相等就會拋出異常。這種機制叫做 fail-fast,所有集合類都有這種機制。
說一說 LinkedList
LinkedList 本質是雙向鏈表,與 ArrayList 相比插入和刪除速度更快,但隨機訪問元素很慢。除繼承 AbstractList 外還實現了 Deque 接口,這個接口具有隊列和棧的性質。成員變量被 transient 修飾,原理和 ArrayList 類似。
LinkedList 包含三個重要的成員:size、first 和 last。size 是雙向鏈表中節點的個數,first 和 last 分別指向首尾節點的引用。
LinkedList 的優點在于可以將零散的內存單元通過附加引用的方式關聯起來,形成按鏈路順序查找的線性結構,內存利用率較高。
Set 有什么特點,有哪些實現?
Set 不允許元素重復且無序,常用實現有 HashSet、LinkedHashSet 和 TreeSet。
HashSet 通過 HashMap 實現,HashMap 的 Key 即 HashSet 存儲的元素,所有 Key 都使用相同的 Value ,一個名為 PRESENT 的 Object 類型常量。使用 Key 保證元素唯一性,但不保證有序性。由于 HashSet 是 HashMap 實現的,因此線程不安全。
HashSet 判斷元素是否相同時,對于包裝類型直接按值比較。對于引用類型先比較 hashCode 是否相同,不同則代表不是同一個對象,相同則繼續比較 equals,都相同才是同一個對象。
LinkedHashSet 繼承自 HashSet,通過 LinkedHashMap 實現,使用雙向鏈表維護元素插入順序。
TreeSet 通過 TreeMap 實現的,添加元素到集合時按照比較規則將其插入合適的位置,保證插入后的集合仍然有序。
TreeMap 有什么特點?
TreeMap 基于紅黑樹實現,增刪改查的平均和最差時間復雜度均為 O(logn) ,最大特點是 Key 有序。Key 必須實現 Comparable 接口或提供的 Comparator 比較器,所以 Key 不允許為 null。
HashMap 依靠 hashCode 和 equals 去重,而 TreeMap 依靠 Comparable 或 Comparator。 TreeMap 排序時,如果比較器不為空就會優先使用比較器的 compare 方法,否則使用 Key 實現的 Comparable 的 compareTo 方法,兩者都不滿足會拋出異常。
TreeMap 通過 put 和 deleteEntry 實現增加和刪除樹節點。插入新節點的規則有三個:① 需要調整的新節點總是紅色的。② 如果插入新節點的父節點是黑色的,不需要調整。③ 如果插入新節點的父節點是紅色的,由于紅黑樹不能出現相鄰紅色,進入循環判斷,通過重新著色或左右旋轉來調整。TreeMap 的插入操作就是按照 Key 的對比往下遍歷,大于節點值向右查找,小于向左查找,先按照二叉查找樹的特性操作,后續會重新著色和旋轉,保持紅黑樹的特性。
HashMap 有什么特點?
JDK8 之前底層實現是數組 + 鏈表,JDK8 改為數組 + 鏈表/紅黑樹,節點類型從Entry 變更為 Node。主要成員變量包括存儲數據的 table 數組、元素數量 size、加載因子 loadFactor。
table 數組記錄 HashMap 的數據,每個下標對應一條鏈表,所有哈希沖突的數據都會被存放到同一條鏈表,Node/Entry 節點包含四個成員變量:key、value、next 指針和 hash 值。
HashMap 中數據以鍵值對的形式存在,鍵對應的 hash 值用來計算數組下標,如果兩個元素 key 的 hash 值一樣,就會發生哈希沖突,被放到同一個鏈表上,為使查詢效率盡可能高,鍵的 hash 值要盡可能分散。
HashMap 默認初始化容量為 16,擴容容量必須是 2 的冪次方、最大容量為 1<< 30 、默認加載因子為 0.75。
HashMap 為什么線程不安全?
JDK7 存在死循環和數據丟失問題。
數據丟失:
-
并發賦值被覆蓋: 在 createEntry 方法中,新添加的元素直接放在頭部,使元素之后可以被更快訪問,但如果兩個線程同時執行到此處,會導致其中一個線程的賦值被覆蓋。
-
已遍歷區間新增元素丟失: 當某個線程在 transfer 方法遷移時,其他線程新增的元素可能落在已遍歷過的哈希槽上。遍歷完成后,table 數組引用指向了 newTable,新增元素丟失。
-
新表被覆蓋: 如果 resize 完成,執行了 table = newTable,則后續元素就可以在新表上進行插入。但如果多線程同時 resize ,每個線程都會 new 一個數組,這是線程內的局部對象,線程之間不可見。遷移完成后resize 的線程會賦值給 table 線程共享變量,可能會覆蓋其他線程的操作,在新表中插入的對象都會被丟棄。
死循環:
擴容時 resize 調用 transfer 使用頭插法遷移元素,雖然 newTable 是局部變量,但原先 table 中的 Entry 鏈表是共享的,問題根源是 Entry 的 next 指針并發修改,某線程還沒有將 table 設為 newTable 時用完了 CPU 時間片,導致數據丟失或死循環。
JDK8 在 resize 方法中完成擴容,并改用尾插法,不會產生死循環,但并發下仍可能丟失數據。可用 ConcurrentHashMap 或 Collections.synchronizedMap 包裝成同步集合。
IO 流
同步/異步/阻塞/非阻塞 IO 的區別?
同步和異步是通信機制,阻塞和非阻塞是調用狀態。
同步 IO 是用戶線程發起 IO 請求后需要等待或輪詢內核 IO 操作完成后才能繼續執行。異步 IO 是用戶線程發起 IO 請求后可以繼續執行,當內核 IO 操作完成后會通知用戶線程,或調用用戶線程注冊的回調函數。
阻塞 IO 是 IO 操作需要徹底完成后才能返回用戶空間 。非阻塞 IO 是 IO 操作調用后立即返回一個狀態值,無需等 IO 操作徹底完成。
什么是 BIO?
BIO 是同步阻塞式 IO,JDK1.4 之前的 IO 模型。服務器實現模式為一個連接請求對應一個線程,服務器需要為每一個客戶端請求創建一個線程,如果這個連接不做任何事會造成不必要的線程開銷。可以通過線程池改善,這種 IO 稱為偽異步 IO。適用連接數目少且服務器資源多的場景。
什么是 NIO?
NIO 是 JDK1.4 引入的同步非阻塞 IO。服務器實現模式為多個連接請求對應一個線程,客戶端連接請求會注冊到一個多路復用器 Selector ,Selector 輪詢到連接有 IO 請求時才啟動一個線程處理。適用連接數目多且連接時間短的場景。
同步是指線程還是要不斷接收客戶端連接并處理數據,非阻塞是指如果一個管道沒有數據,不需要等待,可以輪詢下一個管道。
核心組件:
Selector: 多路復用器,輪詢檢查多個 Channel 的狀態,判斷注冊事件是否發生,即判斷 Channel 是否處于可讀或可寫狀態。使用前需要將 Channel 注冊到 Selector,注冊后會得到一個 SelectionKey,通過 SelectionKey 獲取 Channel 和 Selector 相關信息。
Channel: 雙向通道,替換了 BIO 中的 Stream 流,不能直接訪問數據,要通過 Buffer 來讀寫數據,也可以和其他 Channel 交互。
Buffer: 緩沖區,本質是一塊可讀寫數據的內存,用來簡化數據讀寫。Buffer 三個重要屬性:position 下次讀寫數據的位置,limit 本次讀寫的極限位置,capacity 最大容量。
- flip 將寫轉為讀,底層實現原理把 position 置 0,并把 limit 設為當前的 position 值。
- clear 將讀轉為寫模式(用于讀完全部數據的情況,把 position 置 0,limit 設為 capacity)。
- compact 將讀轉為寫模式(用于存在未讀數據的情況,讓 position 指向未讀數據的下一個)。
- 通道方向和 Buffer 方向相反,讀數據相當于向 Buffer 寫,寫數據相當于從 Buffer 讀。
使用步驟:向 Buffer 寫數據,調用 flip 方法轉為讀模式,從 Buffer 中讀數據,調用 clear 或 compact 方法清空緩沖區。
什么是 AIO?
AIO 是 JDK7 引入的異步非阻塞 IO。服務器實現模式為一個有效請求對應一個線程,客戶端的 IO 請求都是由操作系統先完成 IO 操作后再通知服務器應用來直接使用準備好的數據。適用連接數目多且連接時間長的場景。
異步是指服務端線程接收到客戶端管道后就交給底層處理IO通信,自己可以做其他事情,非阻塞是指客戶端有數據才會處理,處理好再通知服務器。
實現方式包括通過 Future 的 get 方法進行阻塞式調用以及實現 CompletionHandler 接口,重寫請求成功的回調方法 completed 和請求失敗回調方法 failed。
總結
以上是生活随笔為你收集整理的面试必会系列 - 1.1 Java SE 基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构与算法(二):堆,大根堆,小根堆
- 下一篇: 面试必会系列 - 1.2 Java 集合