40道一線互聯網公司高頻面試題(附答案!)


Java 基礎 40
語言特性 12
Q1:Java 語言的優點?
① 平台無關性,擺脫硬件束縛,"一次編寫,到處運行"。
② 相對安全的內存管理和訪問機制,避免大部分內存泄漏和指針越界。
③ 熱點代碼檢測和運行時編譯及優化,使程序隨運行時間增長獲得更高性能。
④ 完善的應用程序接口,支持第三方類庫。
Q2:Java 如何實現平台無關?
JVM: Java 編譯器可生成與計算機體系結構無關的字節碼指令,字節碼文件不僅可以輕易地在任何機器上解釋執行,還可以動態地轉換成本地機器代碼,轉換是由 JVM 實現的,JVM 是平台相關的,屏蔽了不同操作系統的差異。
語言規范: 基本數據類型大小有明確規定,例如 int 永遠為 32 位,而 C/C++ 中可能是 16 位、32 位,也可能是編譯器開發商指定的其他大小。Java 中數值類型有固定字節數,二進制數據以固定格式存儲和傳輸,字符串采用標准的 Unicode 格式存儲。
Q3:JDK 和 JRE 的區別?
JDK: Java Development Kit,開發工具包。提供了編譯運行 Java 程序的各種工具,包括編譯器、JRE 及常用類庫,是 JAVA 核心。
JRE: Java Runtime Environment,運行時環境,運行 Java 程序的必要環境,包括 JVM、核心類庫、核心配置工具。
Q4:Java 按值調用還是引用調用?
按值調用指方法接收調用者提供的值,按引用調用指方法接收調用者提供的變量地址。
Java 總是按值調用,方法得到的是所有參數值的副本,傳遞對象時實際上方法接收的是對象引用的副本。方法不能修改基本數據類型的參數,如果傳遞了一個 int 值 ,改變值不會影響實參,因為改變的是值的一個副本。
可以改變對象參數的狀態,但不能讓對象參數引用一個新的對象。如果傳遞了一個 int 數組,改變數組的內容會影響實參,而改變這個參數的引用並不會讓實參引用新的數組對象。
Q5:淺拷貝和深拷貝的區別?
淺拷貝: 只復制當前對象的基本數據類型及引用變量,沒有復制引用變量指向的實際對象。修改克隆對象可能影響原對象,不安全。
深拷貝: 完全拷貝基本數據類型和引用數據類型,安全。
Q6:什么是反射?
在運行狀態中,對於任意一個類都能知道它的所有屬性和方法,對於任意一個對象都能調用它的任意方法和屬性,這種動態獲取信息及調用對象方法的功能稱為反射。缺點是破壞了封裝性以及泛型約束。反射是框架的核心,Spring 大量使用反射。
Q7:Class 類的作用?如何獲取一個 Class 對象?
在程序運行期間,Java 運行時系統為所有對象維護一個運行時類型標識,這個信息會跟蹤每個對象所屬的類,虛擬機利用運行時類型信息選擇要執行的正確方法,保存這些信息的類就是 Class,這是一個泛型類。
獲取 Class 對象:① 類名.class 。②對象的 getClass方法。③ Class.forName(類的全限定名)。
Q8:什么是注解?什么是元注解?
注解是一種標記,使類或接口附加額外信息,幫助編譯器和 JVM 完成一些特定功能,例如 @Override 標識一個方法是重寫方法。
元注解是自定義注解的注解,例如:
@Target:約束作用位置,值是 ElementType 枚舉常量,包括 METHOD 方法、VARIABLE 變量、TYPE 類/接口、PARAMETER 方法參數、CONSTRUCTORS 構造方法和 LOACL_VARIABLE 局部變量等。
@Rentention:約束生命周期,值是 RetentionPolicy 枚舉常量,包括 SOURCE 源碼、CLASS 字節碼和 RUNTIME 運行時。
@Documented:表明這個注解應該被 javadoc 記錄。
Q9:什么是泛型,有什么作用?
泛型本質是參數化類型,解決不確定對象具體類型的問題。泛型在定義處只具備執行 Object 方法的能力。
泛型的好處:① 類型安全,放置什么出來就是什么,不存在 ClassCastException。② 提升可讀性,編碼階段就顯式知道泛型集合、泛型方法等處理的對象類型。③ 代碼重用,合並了同類型的處理代碼。
Q10:泛型擦除是什么?
泛型用於編譯階段,編譯后的字節碼文件不包含泛型類型信息,因為虛擬機沒有泛型類型對象,所有對象都屬於普通類。例如定義 List 或 List,在編譯后都會變成 List 。
定義一個泛型類型,會自動提供一個對應原始類型,類型變量會被擦除。如果沒有限定類型就會替換為 Object,如果有限定類型就會替換為第一個限定類型,例如 `` 會使用 A 類型替換 T。
Q11: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 應用。
Q12:異常有哪些分類?
所有異常都是 Throwable 的子類,分為 Error 和 Exception。Error 是 Java 運行時系統的內部錯誤和資源耗盡錯誤,例如 StackOverFlowError 和 OutOfMemoryError,這種異常程序無法處理。
Exception 分為受檢異常和非受檢異常,受檢異常需要在代碼中顯式處理,否則會編譯出錯,非受檢異常是運行時異常,繼承自 RuntimeException。
受檢異常:① 無能為力型,如字段超長導致的 SQLException。② 力所能及型,如未授權異常 UnAuthorizedException,程序可跳轉權限申請頁面。常見受檢異常還有 FileNotFoundException、ClassNotFoundException、IOException等。
非受檢異常:① 可預測異常,例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,這類異常應該提前處理。② 需捕捉異常,例如進行 RPC 調用時的遠程服務超時,這類異常客戶端必須顯式處理。③ 可透出異常,指框架或系統產生的且會自行處理的異常,例如 Spring 的 NoSuchRequestHandingMethodException,Spring 會自動完成異常處理,將異常自動映射到合適的狀態碼。
數據類型 5
Q1:Java 有哪些基本數據類型?
數據類型 內存大小 默認值 取值范圍
字節 1個 (字節)0 -128 ~ 127
短 2個 (短)0 -215 ~ 215-1
整型 4個 0 -231 ~ 231-1
long 8層 0升 -263 ~ 263-1
浮動 4個 0.0F ±3.4E+38(有效位數 6~7 位)
雙 8層 0.0D ±1.7E+308(有效位數 15 位)
燒焦 英文 1B,中文 UTF-8 占 3B,GBK 占 2B。 '\ u0000' '\ u0000'〜'\ uFFFF'
布爾值 單個變量 4B / 數組 1B 假 真假
JVM 沒有 boolean 賦值的專用字節碼指令,boolean f = false 就是使用 ICONST_0 即常數 0 賦值。單個 boolean 變量用 int 代替,boolean 數組會編碼成 byte 數組。
Q2:自動裝箱/拆箱是什么?
每個基本數據類型都對應一個包裝類,除了 int 和 char 對應 Integer 和 Character 外,其余基本數據類型的包裝類都是首字母大寫即可。
自動裝箱: 將基本數據類型包裝為一個包裝類對象,例如向一個泛型為 Integer 的集合添加 int 元素。
自動拆箱: 將一個包裝類對象轉換為一個基本數據類型,例如將一個包裝類對象賦值給一個基本數據類型的變量。
比較兩個包裝類數值要用 equals ,而不能用 == 。
Q3:String 是不可變類為什么值可以修改?
String 類和其存儲數據的成員變量 value 字節數組都是 final 修飾的。對一個 String 對象的任何修改實際上都是創建一個新 String 對象,再引用該對象。只是修改 String 變量引用的對象,沒有修改原 String 對象的內容。
Q4:字符串拼接的方式有哪些?
① 直接用 + ,底層用 StringBuilder 實現。只適用小數量,如果在循環中使用 + 拼接,相當於不斷創建新的 StringBuilder 對象再轉換成 String 對象,效率極差。
② 使用 String 的 concat 方法,該方法中使用 Arrays.copyOf 創建一個新的字符數組 buf 並將當前字符串 value 數組的值拷貝到 buf 中,buf 長度 = 當前字符串長度 + 拼接字符串長度。之后調用 getChars 方法使用 System.arraycopy 將拼接字符串的值也拷貝到 buf 數組,最后用 buf 作為構造參數 new 一個新的 String 對象返回。效率稍高於直接使用 +。
③ 使用 StringBuilder 或 StringBuffer,兩者的 append 方法都繼承自 AbstractStringBuilder,該方法首先使用 Arrays.copyOf 確定新的字符數組容量,再調用 getChars 方法使用 System.arraycopy 將新的值追加到數組中。StringBuilder 是 JDK5 引入的,效率高但線程不安全。StringBuffer 使用 synchronized 保證線程安全。
Q5:String a = "a" + new String("b") 創建了幾個對象?
常量和常量拼接仍是常量,結果在常量池,只要有變量參與拼接結果就是變量,存在堆。
使用字面量時只創建一個常量池中的常量,使用 new 時如果常量池中沒有該值就會在常量池中新創建,再在堆中創建一個對象引用常量池中常量。因此 String a = "a" + new String("b") 會創建四個對象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。
面向對象 10
Q1:談一談你對面向對象的理解
面向過程讓計算機有步驟地順序做一件事,是過程化思維,使用面向過程語言開發大型項目,軟件復用和維護存在很大問題,模塊之間耦合嚴重。面向對象相對面向過程更適合解決規模較大的問題,可以拆解問題復雜度,對現實事物進行抽象並映射為開發對象,更接近人的思維。
例如開門這個動作,面向過程是 open(Door door),動賓結構,door 作為操作對象的參數傳入方法,方法內定義開門的具體步驟。面向對象的方式首先會定義一個類 Door,抽象出門的屬性(如尺寸、顏色)和行為(如 open 和 close),主謂結構。
面向過程代碼松散,強調流程化解決問題。面向對象代碼強調高內聚、低耦合,先抽象模型定義共性行為,再解決實際問題。
Q2:面向對象的三大特性?
封裝是對象功能內聚的表現形式,在抽象基礎上決定信息是否公開及公開等級,核心問題是以什么方式暴漏哪些信息。主要任務是對屬性、數據、敏感行為實現隱藏,對屬性的訪問和修改必須通過公共接口實現。封裝使對象關系變得簡單,降低了代碼耦合度,方便維護。
迪米特原則就是對封裝的要求,即 A 模塊使用 B 模塊的某接口行為,對 B 模塊中除此行為外的其他信息知道得應盡可能少。不直接對 public 屬性進行讀取和修改而使用 getter/setter 方法是因為假設想在修改屬性時進行權限控制、日志記錄等操作,在直接訪問屬性的情況下無法實現。如果將 public 的屬性和行為修改為 private 一般依賴模塊都會報錯,因此不知道使用哪種權限時應優先使用 private。
繼承用來擴展一個類,子類可繼承父類的部分屬性和行為使模塊具有復用性。繼承是"is-a"關系,可使用里氏替換原則判斷是否滿足"is-a"關系,即任何父類出現的地方子類都可以出現。如果父類引用直接使用子類引用來代替且可以正確編譯並執行,輸出結果符合子類場景預期,那么說明兩個類符合里氏替換原則。
多態以封裝和繼承為基礎,根據運行時對象實際類型使同一行為具有不同表現形式。多態指在編譯層面無法確定最終調用的方法體,在運行期由 JVM 動態綁定,調用合適的重寫方法。由於重載屬於靜態綁定,本質上重載結果是完全不同的方法,因此多態一般專指重寫。
Q3:重載和重寫的區別?
重載指方法名稱相同,但參數類型個數不同,是行為水平方向不同實現。對編譯器來說,方法名稱和參數列表組成了一個唯一鍵,稱為方法簽名,JVM 通過方法簽名決定調用哪種重載方法。不管繼承關系如何復雜,重載在編譯時可以根據規則知道調用哪種目標方法,因此屬於靜態綁定。
JVM 在重載方法中選擇合適方法的順序:① 精確匹配。② 基本數據類型自動轉換成更大表示范圍。③ 自動拆箱與裝箱。④ 子類向上轉型。⑤ 可變參數。
重寫指子類實現接口或繼承父類時,保持方法簽名完全相同,實現不同方法體,是行為垂直方向不同實現。
元空間有一個方法表保存方法信息,如果子類重寫了父類的方法,則方法表中的方法引用會指向子類實現。父類引用執行子類方法時無法調用子類存在而父類不存在的方法。
重寫方法訪問權限不能變小,返回類型和拋出的異常類型不能變大,必須加 @Override 。
Q4:類之間有哪些關系?
類關系 描述 權力強側 舉例
繼承 父子類之間的關系:is-a 父類 小狗繼承於動物
實現 接口和實現類之間的關系:can-do 接口 小狗實現了狗叫接口
組合 比聚合更強的關系:contains-a 整體 頭是身體的一部分
聚合 暫時組裝的關系:has-a 組裝方 小狗和繩子是暫時的聚合關系
依賴 一個類用到另一個:depends-a 被依賴方 人養小狗,人依賴於小狗
關聯 平等的使用關系:links-a 平等 人使用卡消費,卡可以提取人的信息
Q5: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:阻塞或喚醒持有該對象鎖的線程。
Q6:內部類的作用是什么,有哪些分類?
內部類可對同一包中其他類隱藏,內部類方法可以訪問定義這個內部類的作用域中的數據,包括 private 數據。
內部類是一個編譯器現象,與虛擬機無關。編譯器會把內部類轉換成常規的類文件,用 $ 分隔外部類名與內部類名,其中匿名內部類使用數字編號,虛擬機對此一無所知。
靜態內部類: 屬於外部類,只加載一次。作用域僅在包內,可通過 外部類名.內部類名 直接訪問,類內只能訪問外部類所有靜態屬性和方法。HashMap 的 Node 節點,ReentrantLock 中的 Sync 類,ArrayList 的 SubList 都是靜態內部類。內部類中還可以定義內部類,如 ThreadLoacl 靜態內部類 ThreadLoaclMap 中定義了內部類 Entry。
成員內部類: 屬於外部類的每個對象,隨對象一起加載。不可以定義靜態成員和方法,可訪問外部類的所有內容。
局部內部類: 定義在方法內,不能聲明訪問修飾符,只能定義實例成員變量和實例方法,作用范圍僅在聲明類的代碼塊中。
匿名內部類: 只用一次的沒有名字的類,可以簡化代碼,創建的對象類型相當於 new 的類的子類類型。用於實現事件監聽和其他回調。
Q7:訪問權限控制符有哪些?
訪問權限控制符 本類 封裝形式 包外子類 任何地方
上市 √ √ √ √
受保護的 √ √ √ ×
無 √ √ × ×
私人的 √ × × ×
Q8:接口和抽象類的異同?
接口和抽象類對實體類進行更高層次的抽象,僅定義公共行為和特征。
語法維度 抽象類 接口
成員變量 無特殊要求 默認 public static final 常量
構造方法 有構造方法,不能實例化 沒有構造方法,不能實例化
方法 抽象類可以沒有抽象方法,但有抽象方法一定是抽象類。 默認 public abstract,JDK8 支持默認/靜態方法,JDK9 支持私有方法。
繼承 單繼承 多繼承
Q9:接口和抽象類應該怎么選擇?
抽象類體現 is-a 關系,接口體現 can-do 關系。與接口相比,抽象類通常是對同類事物相對具體的抽象。
抽象類是模板式設計,包含一組具體特征,例如某汽車,底盤、控制電路等是抽象出來的共同特征,但內飾、顯示屏、座椅材質可以根據不同級別配置存在不同實現。
接口是契約式設計,是開放的,定義了方法名、參數、返回值、拋出的異常類型,誰都可以實現它,但必須遵守接口的約定。例如所有車輛都必須實現剎車這種強制規范。
接口是頂級類,抽象類在接口下面的第二層,對接口進行了組合,然后實現部分接口。當糾結定義接口和抽象類時,推薦定義為接口,遵循接口隔離原則,按維度划分成多個接口,再利用抽象類去實現這些,方便后續的擴展和重構。
例如 Plane 和 Bird 都有 fly 方法,應把 fly 定義為接口,而不是抽象類的抽象方法再繼承,因為除了 fly 行為外 Plane 和 Bird 間很難再找到其他共同特征。
Q10:子類初始化的順序
① 父類靜態代碼塊和靜態變量。② 子類靜態代碼塊和靜態變量。③ 父類普通代碼塊和普通變量。④ 父類構造方法。⑤ 子類普通代碼塊和普通變量。⑥ 子類構造方法。
集合 7
Q1:說一說 ArrayList
ArrayList 是容量可變的非線程安全列表,使用數組實現,集合擴容時會創建更大的數組,把原有數組復制到新數組。支持對元素的快速隨機訪問,但插入與刪除速度很慢。ArrayList 實現了 RandomAcess 標記接口,如果一個類實現了該接口,那么表示使用索引遍歷比迭代器更快。
elementData是 ArrayList 的數據域,被 transient 修飾,序列化時會調用 writeObject 寫入流,反序列化時調用 readObject 重新賦值到新對象的 elementData。原因是 elementData 容量通常大於實際存儲元素的數量,所以只需發送真正有實際值的數組元素。
size 是當前實際大小,elementData 大小大於等於 size。
**modCount **記錄了 ArrayList 結構性變化的次數,繼承自 AbstractList。所有涉及結構變化的方法都會增加該值。expectedModCount 是迭代器初始化時記錄的 modCount 值,每次訪問新元素時都會檢查 modCount 和 expectedModCount 是否相等,不相等就會拋出異常。這種機制叫做 fail-fast,所有集合類都有這種機制。
Q2:說一說 LinkedList
LinkedList 本質是雙向鏈表,與 ArrayList 相比插入和刪除速度更快,但隨機訪問元素很慢。除繼承 AbstractList 外還實現了 Deque 接口,這個接口具有隊列和棧的性質。成員變量被 transient 修飾,原理和 ArrayList 類似。
LinkedList 包含三個重要的成員:size、first 和 last。size 是雙向鏈表中節點的個數,first 和 last 分別指向首尾節點的引用。
LinkedList 的優點在於可以將零散的內存單元通過附加引用的方式關聯起來,形成按鏈路順序查找的線性結構,內存利用率較高。
Q3: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 實現的,添加元素到集合時按照比較規則將其插入合適的位置,保證插入后的集合仍然有序。
Q4:TreeMap 有什么特點?
TreeMap 基於紅黑樹實現,增刪改查的平均和最差時間復雜度均為 O(logñ) ,最大特點是 Key 有序。Key 必須實現 Comparable 接口或提供的 Comparator 比較器,所以 Key 不允許為 null。
HashMap 依靠 hashCode 和 equals 去重,而 TreeMap 依靠 Comparable 或 Comparator。TreeMap 排序時,如果比較器不為空就會優先使用比較器的 compare 方法,否則使用 Key 實現的 Comparable 的 compareTo 方法,兩者都不滿足會拋出異常。
TreeMap 通過 put 和 deleteEntry 實現增加和刪除樹節點。插入新節點的規則有三個:① 需要調整的新節點總是紅色的。② 如果插入新節點的父節點是黑色的,不需要調整。③ 如果插入新節點的父節點是紅色的,由於紅黑樹不能出現相鄰紅色,進入循環判斷,通過重新着色或左右旋轉來調整。TreeMap 的插入操作就是按照 Key 的對比往下遍歷,大於節點值向右查找,小於向左查找,先按照二叉查找樹的特性操作,后續會重新着色和旋轉,保持紅黑樹的特性。
Q5: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。
Q6:HashMap 相關方法的源碼?
JDK8 之前
hash:計算元素 key 的散列值
① 處理 String 類型時,調用 stringHash32 方法獲取 hash 值。
② 處理其他類型數據時,提供一個相對於 HashMap 實例唯一不變的隨機值 hashSeed 作為計算初始量。
③ 執行異或和無符號右移使 hash 值更加離散,減小哈希沖突概率。
indexFor:計算元素下標
將 hash 值和數組長度-1 進行與操作,保證結果不會超過 table 數組范圍。
get:獲取元素的 value 值
① 如果 key 為 null,調用 getForNullKey 方法,如果 size 為 0 表示鏈表為空,返回 null。如果 size 不為 0 說明存在鏈表,遍歷 table[0] 鏈表,如果找到了 key 為 null 的節點則返回其 value,否則返回 null。
② 如果 key 為 不為 null,調用 getEntry 方法,如果 size 為 0 表示鏈表為空,返回 null 值。如果 size 不為 0,首先計算 key 的 hash 值,然后遍歷該鏈表的所有節點,如果節點的 key 和 hash 值都和要查找的元素相同則返回其 Entry 節點。
③ 如果找到了對應的 Entry 節點,調用 getValue 方法獲取其 value 並返回,否則返回 null。
put:添加元素
① 如果 key 為 null,直接存入 table[0]。
② 如果 key 不為 null,計算 key 的 hash 值。
③ 調用 indexFor 計算元素存放的下標 i。
④ 遍歷 table[i] 對應的鏈表,如果 key 已存在,就更新 value 然后返回舊 value。
⑤ 如果 key 不存在,將 modCount 值加 1,使用 addEntry 方法增加一個節點並返回 null。
resize:擴容數組
① 如果當前容量達到了最大容量,將閾值設置為 Integer 最大值,之后擴容不再觸發。
② 否則計算新的容量,將閾值設為 newCapacity x loadFactor 和 最大容量 + 1 的較小值。
③ 創建一個容量為 newCapacity 的 Entry 數組,調用 transfer 方法將舊數組的元素轉移到新數組。
transfer:轉移元素
① 遍歷舊數組的所有元素,調用 rehash 方法判斷是否需要哈希重構,如果需要就重新計算元素 key 的 hash 值。
② 調用 indexFor 方法計算元素存放的下標 i,利用頭插法將舊數組的元素轉移到新數組。
JDK8
hash:計算元素 key 的散列值
如果 key 為 null 返回 0,否則就將 key 的 hashCode 方法返回值高低16位異或,讓盡可能多的位參與運算,讓結果的 0 和 1 分布更加均勻,降低哈希沖突概率。
put:添加元素
① 調用 putVal 方法添加元素。
② 如果 table 為空或長度為 0 就進行擴容,否則計算元素下標位置,不存在就調用 newNode 創建一個節點。
③ 如果存在且是鏈表,如果首節點和待插入元素的 hash 和 key 都一樣,更新節點的 value。
④ 如果首節點是 TreeNode 類型,調用 putTreeVal 方法增加一個樹節點,每一次都比較插入節點和當前節點的大小,待插入節點小就往左子樹查找,否則往右子樹查找,找到空位后執行兩個方法:balanceInsert 方法,插入節點並調整平衡、moveRootToFront 方法,由於調整平衡后根節點可能變化,需要重置根節點。
⑤ 如果都不滿足,遍歷鏈表,根據 hash 和 key 判斷是否重復,決定更新 value 還是新增節點。如果遍歷到了鏈表末尾則添加節點,如果達到建樹閾值 7,還需要調用 treeifyBin 把鏈表重構為紅黑樹。
⑥ 存放元素后將 modCount 加 1,如果 ++size > threshold ,調用 resize 擴容。
get :獲取元素的 value 值
① 調用 getNode 方法獲取 Node 節點,如果不是 null 就返回其 value 值,否則返回 null。
② getNode 方法中如果數組不為空且存在元素,先比較第一個節點和要查找元素的 hash 和 key ,如果都相同則直接返回。
③ 如果第二個節點是 TreeNode 類型則調用 getTreeNode 方法進行查找,否則遍歷鏈表根據 hash 和 key 查找,如果沒有找到就返回 null。
resize:擴容數組
重新規划長度和閾值,如果長度發生了變化,部分數據節點也要重新排列。
重新規划長度
① 如果當前容量 oldCap > 0 且達到最大容量,將閾值設為 Integer 最大值,return 終止擴容。
② 如果未達到最大容量,當 oldCap << 1 不超過最大容量就擴大為 2 倍。
③ 如果都不滿足且當前擴容閾值 oldThr > 0,使用當前擴容閾值作為新容量。
④ 否則將新容量置為默認初始容量 16,新擴容閾值置為 12。
重新排列數據節點
① 如果節點為 null 不進行處理。
② 如果節點不為 null 且沒有next節點,那么通過節點的 hash 值和 新容量-1 進行與運算計算下標存入新的 table 數組。
③ 如果節點為 TreeNode 類型,調用 split 方法處理,如果節點數 hc 達到6 會調用 untreeify 方法轉回鏈表。
④ 如果是鏈表節點,需要將鏈表拆分為 hash 值超出舊容量的鏈表和未超出容量的鏈表。對於hash & oldCap == 0 的部分不需要做處理,否則需要放到新的下標位置上,新下標 = 舊下標 + 舊容量。
Q7: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風格6
Q1:同步/異步/阻塞/非阻塞 IO 的區別?
同步和異步是通信機制,阻塞和非阻塞是調用狀態。
同步 IO 是用戶線程發起 IO 請求后需要等待或輪詢內核 IO 操作完成后才能繼續執行。異步 IO 是用戶線程發起 IO 請求后可以繼續執行,當內核 IO 操作完成后會通知用戶線程,或調用用戶線程注冊的回調函數。
阻塞 IO 是 IO 操作需要徹底完成后才能返回用戶空間 。非阻塞 IO 是 IO 操作調用后立即返回一個狀態值,無需等 IO 操作徹底完成。
Q2:什么是 BIO?
BIO 是同步阻塞式 IO,JDK1.4 之前的 IO 模型。服務器實現模式為一個連接請求對應一個線程,服務器需要為每一個客戶端請求創建一個線程,如果這個連接不做任何事會造成不必要的線程開銷。可以通過線程池改善,這種 IO 稱為偽異步 IO。適用連接數目少且服務器資源多的場景。
Q3:什么是 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 最大容量。
使用步驟:向 Buffer 寫數據,調用 flip 方法轉為讀模式,從 Buffer 中讀數據,調用 clear 或 compact 方法清空緩沖區。
flip 將寫轉為讀,底層實現原理把 position 置 0,並把 limit 設為當前的 position 值。
clear 將讀轉為寫模式(用於讀完全部數據的情況,把 position 置 0,limit 設為 capacity)。
compact 將讀轉為寫模式(用於存在未讀數據的情況,讓 position 指向未讀數據的下一個)。
通道方向和 Buffer 方向相反,讀數據相當於向 Buffer 寫,寫數據相當於從 Buffer 讀。
Q4:什么是 AIO?
AIO 是 JDK7 引入的異步非阻塞 IO。服務器實現模式為一個有效請求對應一個線程,客戶端的 IO 請求都是由操作系統先完成 IO 操作后再通知服務器應用來直接使用准備好的數據。適用連接數目多且連接時間長的場景。
異步是指服務端線程接收到客戶端管道后就交給底層處理IO通信,自己可以做其他事情,非阻塞是指客戶端有數據才會處理,處理好再通知服務器。
實現方式包括通過 Future 的 get 方法進行阻塞式調用以及實現 CompletionHandler 接口,重寫請求成功的回調方法 completed 和請求失敗回調方法 failed。
Q5:java.io 包下有哪些流?
主要分為字符流和字節流,字符流一般用於文本文件,字節流一般用於圖像或其他文件。
字符流包括了字符輸入流 Reader 和字符輸出流 Writer,字節流包括了字節輸入流 InputStream 和字節輸出流 OutputStream。字符流和字節流都有對應的緩沖流,字節流也可以包裝為字符流,緩沖流帶有一個 8KB 的緩沖數組,可以提高流的讀寫效率。除了緩沖流外還有過濾流 FilterReader、字符數組流 CharArrayReader、字節數組流 ByteArrayInputStream、文件流 FileInputStream 等。
Q6:序列化和反序列化是什么?
Java 對象 JVM 退出時會全部銷毀,如果需要將對象及狀態持久化,就要通過序列化實現,將內存中的對象保存在二進制流中,需要時再將二進制流反序列化為對象。對象序列化保存的是對象的狀態,因此屬於類屬性的靜態變量不會被序列化。
常見的序列化有三種:
Java 原生序列化
實現 Serializabale 標記接口,Java 序列化保留了對象類的元數據(如類、成員變量、繼承類信息)以及對象數據,兼容性最好,但不支持跨語言,性能一般。序列化和反序列化必須保持序列化 ID 的一致,一般使用 private static final long serialVersionUID 定義序列化 ID,如果不設置編譯器會根據類的內部實現自動生成該值。如果是兼容升級不應該修改序列化 ID,防止出錯,如果是不兼容升級則需要修改。
Hessian 序列化
Hessian 序列化是一種支持動態類型、跨語言、基於對象傳輸的網絡協議。Java 對象序列化的二進制流可以被其它語言反序列化。Hessian 協議的特性:① 自描述序列化類型,不依賴外部描述文件,用一個字節表示常用基礎類型,極大縮短二進制流。② 語言無關,支持腳本語言。③ 協議簡單,比 Java 原生序列化高效。Hessian 會把復雜對象所有屬性存儲在一個 Map 中序列化,當父類和子類存在同名成員變量時會先序列化子類再序列化父類,因此子類值會被父類覆蓋。
JSON 序列化
JSON 序列化就是將數據對象轉換為 JSON 字符串,在序列化過程中拋棄了類型信息,所以反序列化時只有提供類型信息才能准確進行。相比前兩種方式可讀性更好,方便調試。
序列化通常會使用網絡傳輸對象,而對象中往往有敏感數據,容易遭受攻擊,Jackson 和 fastjson 等都出現過反序列化漏洞,因此不需要進行序列化的敏感屬性傳輸時應加上 transient 關鍵字。transient 的作用就是把變量生命周期僅限於內存而不會寫到磁盤里持久化,變量會被設為對應數據類型的零值。

總結了一些2020年的面試題,這份面試題的包含的模塊分為19個模塊,分別是: Java基礎、容器、多線程、反射、對象拷貝、JavaWeb異常、網絡、設計模式、Spring/SpringMVC、SpringBoot/SpringCloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM。
獲取以下資料,關注公眾號:【有故事的程序員】。
記得點個關注+評論哦~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM