面经——Java基础
Java基礎
注:題目從???Java部門面經整理而來。
2020秋招面經大匯總!(崗位劃分)
ArrayList 和 LinkedList 區別
1. ArrayList概覽
因為 ArrayList 是基于數組實現的,所以支持快速隨機訪問。RandomAccess 接口標識著該類支持快速隨機訪問。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable數組的默認大小為 10。
private static final int DEFAULT_CAPACITY = 10;2. 擴容
添加元素時使用 ensureCapacityInternal() 方法來保證容量足夠,如果不夠時,需要使用 grow() 方法進行擴容,新容量的大小為 oldCapacity + (oldCapacity >> 1) ,也就是舊容量的 1.5 倍。
擴容操作需要調用 Arrays.copyOf() 把原數組整個復制到新數組中,這個操作代價很高,因此最好在創建ArrayList 對象時就指定大概的容量大小,減少擴容操作的次數。
3. 刪除元素
需要調用 System.arraycopy() 將 index+1 后面的元素都復制到 index 位置上,該操作的時間復雜度為 O(N),可以看出 ArrayList 刪除元素的代價是非常高的。
1. LinkedList 概覽
LinkedList 基于雙向鏈表實現,使用 Node 存儲鏈表節點信息。
private static class Node<E> { E item; Node<E> next; Node<E> prev; }每個鏈表存儲了 first 和 last 指針:
transient Node<E> first; transient Node<E> lastArrayList 和 LinkedList 區別
ArrayList 和 LinkedList 實現線程安全
ArrayList 和 LinkedList 是線程不安全的。
線程安全解決辦法——換成線程安全的集合:ArrayList -> CopyOnWriteArrayList,LinkedList -> ConcurrentLinkedQueue,HashMap -> ConcurrentHashMap,HashSet -> CopyOnWriteArraySet。
線程安全集合類特點:
2. 雙親委派模型以及優點
類加載器分類
從 Java 虛擬機的角度來講,只存在以下兩種不同的類加載器:
- 啟動類加載器(Bootstrap ClassLoader),使用 C++ 實現,是虛擬機自身的一部分;
- 所有其它類的加載器,使用 Java 實現,獨立于虛擬機,繼承自抽象類 java.lang.ClassLoader。
從 Java 開發人員的角度看,類加載器可以劃分得更細致一些:
- 啟動類加載器(Bootstrap ClassLoader)此類加載器負責將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啟動類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給啟動類加載器,直接使用 null 代替即可。
- 擴展類加載器(Extension ClassLoader)這個類加載器是由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被java.ext.dir 系統變量所指定路徑中的所有類庫加載到內存中,開發者可以直接使用擴展類加載器。
- 應用程序類加載器(Application ClassLoader)這個類加載器是由AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由于這個類加載器是ClassLoader 中的getSystemClassLoader() 方法的返回值,因此一般稱為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
雙親委派模型
應用程序是由三種類加載器互相配合從而實現類加載,除此之外還可以加入自己定義的類加載器。
類加載器之間的層次關系,稱為雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類加載器外,其它的類加載器都要有自己的父類加載器。這里的父子關系一般通過組合關系(Composition)來實現,而不是繼承關系(Inheritance)。
1. 工作過程
一個類加載器首先將類加載請求轉發到父類加載器,只有當父類加載器無法完成時才嘗試自己加載。
2. 好處
使得 Java 類隨著它的類加載器一起具有一種帶有優先級的層次關系,從而使得基礎類得到統一。
例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 并放到 ClassPath 中,程序可以編譯通過。由于雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先級更高,這是因為 rt.jar 中的 Object 使用的是啟動類加載器,而 ClassPath 中的 Object 使用的是應用程序類加載器。rt.jar 中的 Object 優先級更高,那么程序中所有的 Object 都是這個 Object。
3. String是否可以被繼承及相關原因
String 被聲明為 final,因此它不可被繼承。
在 Java 8 中,String 內部使用 char 數組存儲數據。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; }在 Java 9 之后,String 類的實現改用 byte 數組存儲字符串,同時使用 coder 來標識使用了哪種編碼。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final byte[] value; /** The identifier of the encoding used to encode the bytes in {@code value}. */ private final byte coder; }value 數組被聲明為 final,這意味著 value 數組初始化之后就不能再引用其它數組。并且 String 內部沒有改變 value數組的方法,因此可以保證 String 不可變。
不可變的好處
因為 String 的 hash 值經常被使用,例如 String 用做 HashMap 的 key。不可變的特性可以使得 hash 值也不可變,因此只需要進行一次計算。
如果一個 String 對象已經被創建過了,那么就會從 String Pool 中取得引用。只有 String 是不可變的,才可能使用String Pool。
String 經常作為參數,String 不可變性可以保證參數不可變。例如在作為網絡連接參數的情況下如果 String 是可變的,那么在網絡連接過程中,String 被改變,改變 String 對象的那一方以為現在連接的是其它主機,而實際情況卻不一定是。
String 不可變性天生具備線程安全,可以在多個線程中安全地使用
4. String 和 StringBuffer、StringBuilder 的區別是什么?String 為什么是不可變的?
可變性
簡單的來說:String 類中使用 final 關鍵字字符數組保存字符串, private final char value[] ,所以 String對象是不可變的。而StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在AbstractStringBuilder中也是使用字符數組保存字符串 char[]value 但是沒有用 final 關鍵字修飾,所以這兩種對象都是可變的。
StringBuilder 與 StringBuffer 的構造方法都是調用父類構造方法也就是 AbstractStringBuilder 實現的,大家可以自
行查閱源碼。
線程安全性
String 中的對象是不可變的,也就可以理解為常量,線程安全。AbstractStringBuilder 是 StringBuilder 與 StringBuffer 的公共父類,定義了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。StringBuilder 并沒有對方法進行加同步鎖,所以是非線程安全的。
性能
每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,然后將指針指向新的 String 對象。
StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象并改變對象引用。相同情況下使用StirngBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。
對于三者使用的總結:
5. 接口和抽象類的區別
備注:在JDK8中,接口也可以定義靜態方法,可以直接用接口名調用。實現類和實現是不可以調用的。如果同時實現兩個接口,接口中定義了一樣的默認方法,必須重寫,不然會報錯。
Java 中的異常體系
Java異常類層次結構圖
在 Java 中,所有的異常都有一個共同的祖先java.lang包中的 Throwable 類。Throwable: 有兩個重要的子類:Exception(異常) 和 Error(錯誤) ,二者都是 Java 異常處理的重要子類,各自都包含大量子類。
Error(錯誤):
-
是程序無法處理的錯誤,表示運行應用程序中較嚴重問題。大多數錯誤與代碼編寫者執行的操作無關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。例如,Java虛擬機運行錯誤(Virtual MachineError),當JVM 不再有繼續執行操作所需的內存資源時,將出現OutOfMemoryError。這些異常發生時,Java虛擬機(JVM)一般會選擇線程終止。
-
這些錯誤表示故障發生于虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(VirtualMachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因為它們在應用程序的控制和處理能力之外,而且絕大多數是程序運行時不允許出現的狀況。對于設計合理的應用程序來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java中,錯誤通過Error的子類描述。
Exception(異常):
- 是程序本身可以處理的異常。Exception 類有一個重要的子類 RuntimeException。
RuntimeException 異常由Java虛擬機拋出。NullPointerException(要訪問的變量沒有引用任何對象時,拋出該異常)、ArithmeticException(算術運算異常,一個整數除以0時,拋出該異常)和
ArrayIndexOutOfBoundsException (下標越界異常)。
注意:異常和錯誤的區別:異常能被程序本身可以處理,錯誤是無法處理。
Throwable類常用方法
- public string getMessage():返回異常發生時的詳細信息
- public string toString():返回異常發生時的簡要描述
- public string getLocalizedMessage():返回異常對象的本地化信息。使用Throwable的子類覆蓋這個方法,可以聲稱本地化信息。如果子類沒有覆蓋該方法,則該方法返回的信息與getMessage()返回的結果相同
- public void printStackTrace():在控制臺上打印Throwable對象封裝的異常信息
異常處理總結
- try 塊:用于捕獲異常。其后可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。
- catch 塊:用于處理try捕獲到的異常。
- finally 塊:無論是否捕獲或處理異常,finally塊里的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。
在以下4種特殊情況下,finally塊不會被執行:
7. synchronized 底層實現
synchronized 關鍵字底層原理屬于 JVM 層面。
① synchronized 同步語句塊的情況
public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("synchronized 代碼塊");}} }通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執行 javap -c -s -v -l
SynchronizedDemo.class 。
從上面我們可以看出:
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。 當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因) 的持有權。當計數器為0則可以成功獲取,獲取后將鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令后,將鎖計數器設為0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
② synchronized 修飾方法的的情況
public class SynchronizedDemo2 {public synchronized void method() {System.out.println("synchronized 方法");} }
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的是ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。
更多內容請看:synchronized 面試五連擊
8. final關鍵字
final關鍵字主要用在三個地方:變量、方法、類。
9. 重載和重寫的區別
- 重載: 發生在同一個類中,方法名必須相同,參數類型不同、個數不同、順序不同,方法返回值和訪問修飾符可以不同,發生在編譯時。
- 重寫: 發生在父子類中,方法名、參數列表必須相同,返回值范圍小于等于父類,拋出的異常范圍小于等于父類,訪問修飾符范圍大于等于父類;如果父類方法訪問修飾符為 private 則子類就不能重寫該方法。
10. 淺拷貝和深拷貝的區別
深拷貝和淺拷貝是只針對Object和Array這樣的引用數據類型的。
深拷貝和淺拷貝的示意圖大致如下:
- 淺拷貝只是復制了對象的引用地址,兩個對象指向同一個內存地址,所以修改其中任意的值,另一個值都會隨之變化。
- 深拷貝是將對象及值復制過來,兩個對象修改其中任意的值另一個值不會改變。
補充:數據類型
數據分為基本數據類型(String, Number, Boolean, Null, Undefined,Symbol)和引用數據類型。
基本數據類型的特點:直接存儲在棧(stack)中的數據
引用數據類型的特點:存儲的是該對象在棧中引用,真實的數據存放在堆內存里。
引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址后從堆中獲得實體。
11. static 關鍵字
1. 靜態變量
靜態變量:又稱為類變量,也就是說這個變量屬于類的,類所有的實例都共享靜態變量,可以直接通過類名來訪問它。靜態變量在內存中只存在一份。
實例變量:每創建一個實例就會產生一個實例變量,它與該實例同生共死。
public class A {private int x; // 實例變量private static int y; // 靜態變量public static void main(String[] args) {// int x = A.x; // Non-static field 'x' cannot be referenced from a static contextA a = new A();int x = a.x;int y = A.y;}}2. 靜態方法
靜態方法在類加載的時候就存在了,它不依賴于任何實例。所以靜態方法必須有實現,也就是說它不能是抽象方法。
只能訪問所屬類的靜態字段和靜態方法,方法中不能有 this 和 super 關鍵字。
public class A {private static int x;private int y;public static void func1() {int a = x; // int b = y; // Non-static field 'y' cannot be referenced from a static context // int b = this.y; // 'A.this' cannot be referenced from a static context} }3. 靜態語句塊
靜態語句塊在類初始化時運行一次。
4. 靜態內部類
非靜態內部類依賴于外部類的實例,而靜態內部類不需要。
靜態內部類不能訪問外部類的非靜態的變量和方法。
5. 靜態導包
在使用靜態變量和方法時不用再指明 ClassName,從而簡化代碼,但可讀性大大降低。
6. 初始化順序
靜態變量和靜態語句塊優先于實例變量和普通語句塊,靜態變量和靜態語句塊的初始化順序取決于它們在代碼中的順
序。
最后才是構造函數的初始化。
public InitialOrderTest() {System.out.println("構造函數"); }存在繼承的情況下,初始化順序為:
12. wait和sleep區別
sleep()方法是Thread的靜態方法,而wait是Object實例方法
wait()方法必須要在同步方法或者同步塊中調用,也就是必須已經獲得對象鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放占有的對象鎖,使得該線程進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU并不會釋放掉對象鎖;
sleep()方法在休眠時間達到后如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知后,才會離開等待池,并且再次獲得CPU時間片才會繼續執行。
sleep方法有可能會拋出異常,所以需要進行異常處理;wait方法不需要處理
13. 反射
反射是在運行狀態中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為 Java 語言的反射機制。
每個類都有一個 Class 對象,包含了與類有關的信息。當編譯一個新類時,會產生一個同名的 .class 文件,該文件內
容保存著 Class 對象。
類加載相當于 Class 對象的加載,類在第一次使用時才動態加載到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver") 這種方式來控制類的加載,該方法會返回一個 Class 對象。反射可以提供運行時的類信息,并且這個類可以在運行時才加載進來,甚至在編譯時期該類的 .class 不存在也可以加載進來。
Class 和 java.lang.reflect 一起對反射提供了支持,java.lang.reflect 類庫主要包含了以下三個類:
- Field :可以使用 get() 和 set() 方法讀取和修改 Field 對象關聯的字段;
- Method :可以使用 invoke() 方法調用與 Method 對象關聯的方法;
- Constructor :可以用 Constructor 創建新的對象。
反射的優點:
反射的缺點:
盡管反射非常強大,但也不能濫用。如果一個功能可以不用反射完成,那么最好就不用。在我們使用反射技術時,下面幾條內容應該牢記于心。
14. 為什么java是跨平臺的
java程序的運行本質上是先編譯成二進制字節碼的class文件,再由JVM解釋執行class文件。而不同平臺上的擁有不同的JVM,都能夠運行字節碼文件,從而實現不同平臺的運行,實現跨平臺。
15. int和Integer區別
Java是一個面向對象編程語言,但是為了編程的方便還是引入不是對象的基本數據類型,為了能夠將這些基本數據類型當成對象操作,Java為每一個基本數據類型都引入了對應的包裝類型(wrapper class),int 的包裝類型就是 Integer,從JDK 1.5開始引入了自動裝箱/拆箱機制,使得二者可以相互轉換。
Java 為每個原始類型提供了包裝類型:
- 原始類型: boolean,char,byte,short,int,long,float,double
- 包裝類型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
首先需要注意的是 f1、f2、f3、f4 四個變量都是 Integer 對象,所以上面的 == 運算比較的不是值而是引用。裝箱的本質是什么呢?當我們給一個Integer對象賦一個int值的時候,會調用Integer類的靜態方法valueOf。
通過看valueOf源碼可知:字面量的值在-128到127之間,那么不會new新的Integer對象,而是直接引用常量池中的Integer對象,所以上面的面試題中f1 == f2的結果是true,而f3 == f4的結果是false。
int和Integer區別:
16. HashMap詳解
1. HashMap 的數據結構?
JDK1.8 之前 HashMap 底層是 數組和鏈表 結合在一起使用也就是 鏈表散列。JDK1.8后 變化為數組+鏈表+紅黑樹的存儲方式,當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹。
HashMap 通過 key 的 hashCode 經過擾動函數處理過后得到 hash 值,然后通過 (n - 1) & hash 判斷當前元素存放的位置(這里的 n 指的是數組的長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鏈法解決沖突。
所謂 “拉鏈法” 就是:將鏈表和數組相結合。也就是說創建一個鏈表數組,數組中每一格就是一個鏈表。若遇到哈希沖突,則將沖突的值加到鏈表中即可。
TreeMap、TreeSet以及JDK1.8之后的HashMap底層都用到了紅黑樹。紅黑樹就是為了解決二叉查找樹的缺陷,因為二叉查找樹在某些情況下會退化成一個線性結構。
2. HashMap 的工作原理?
A:HashMap 底層是 hash 數組和單向鏈表實現,數組中的每個元素都是鏈表,由 Node 內部類(實現 Map.Entry<K,V>接口)實現,HashMap 通過 put & get 方法存儲和獲取。
存儲對象時,將 K/V 鍵值傳給 put() 方法:
①、調用 hash(K) 方法計算 K 的 hash 值,然后結合數組長度,計算得數組下標;
②、調整數組大小(當容器中的元素個數大于 capacity * loadfactor 時,容器會進行擴容resize 為 2n);
③、i.如果 K 的 hash 值在 HashMap 中不存在,則執行插入,若存在,則發生碰撞;
ii.如果 K 的 hash 值在 HashMap 中存在,且它們兩者 equals 返回 true,則更新鍵值對;
iii. 如果 K 的 hash 值在 HashMap 中存在,且它們兩者 equals 返回 false,則插入鏈表的尾部(尾插法)或者紅黑樹中(樹的添加方式)。
(JDK 1.7 之前使用頭插法、JDK 1.8 使用尾插法)
(注意:當碰撞導致鏈表大于 TREEIFY_THRESHOLD = 8 時,就把鏈表轉換成紅黑樹)
獲取對象時,將 K 傳給 get() 方法:
①、調用 hash(K) 方法(計算 K 的 hash 值)從而獲取該鍵值所在鏈表的數組下標;
②、順序遍歷鏈表,equals()方法查找相同 Node 鏈表中 K 值對應的 V 值。
hashCode 是定位的,存儲位置;equals是定性的,比較兩者是否相等
3. 當兩個對象的 hashCode 相同會發生什么?
因為 hashCode 相同,不一定就是相等的(equals方法比較),所以兩個對象所在數組的下標相同,"碰撞"就此發生。又因為 HashMap 使用鏈表存儲對象,這個 Node 會存儲到鏈表中。
4. HashMap 的 table 的容量如何確定?loadFactor 是什么? 該容量如何變化?這種變化會帶來什么問題?
①、table 數組大小是由 capacity 這個參數確定的,默認是16,也可以構造時傳入,最大限制是1<<30;
②、loadFactor 是裝載因子,主要目的是用來確認table 數組是否需要動態擴展,默認值是0.75,比如table 數組大小為 16,裝載因子為 0.75 時,threshold 就是12,當 table 的實際大小超過 12 時,table就需要動態擴容;
③、擴容時,調用 resize() 方法,將 table 長度變為原來的兩倍(注意是 table 長度,而不是 threshold)
④、如果數據很大的情況下,擴展時將會帶來性能的損失,在性能要求很高的地方,這種損失很可能很致命。
5. HashMap JDK1.7和1.8 的區別?
出現哈希沖突時,1.7把數據存放在鏈表,1.8是先放在鏈表,鏈表長度超過8就轉成紅黑樹。
1.7擴容條件是數組長度大于閾值且存在哈希沖突,1.8擴容條件是數組長度大于閾值或鏈表轉為紅黑樹且數組元素小于64時。
6. HashMap 的長度為什么是2的冪次方
為了能讓 HashMap 存取高效,盡量較少碰撞,也就是要盡量把數據分配均勻。我們上面也講到了過了,Hash 值的范圍值-2147483648到2147483647,前后加起來大概40億的映射空間,只要哈希函數映射得比較均勻松散,一般應用是很難出現碰撞的。但問題是一個40億長度的數組,內存是放不下的。所以這個散列值是不能直接拿來用的。用之前還要先做對數組的長度取模運算,得到的余數才能用來要存放的位置也就是對應的數組下標。這個數組下標的計算方法是“ hash & (n - 1) ”。(n代表數組長度)。這也就解釋了 HashMap 的長度為什么是2的冪次方。
這個算法應該如何設計呢?
我們首先可能會想到采用%取余的操作來實現。但是,重點來了:“取余(%)操作中如果除數是2的冪次則等價于與其除數減一的與(&)操作(也就是說 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二進制位操作 &,相對于%能夠提高運算效率,這就解釋了 HashMap 的長度為什么是2的冪次方。
7. 擾動函數(hash方法)
所謂擾動函數指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函數是為了防止一些實現比較差的hashCode() 方法,換句話說使用擾動函數之后可以減少碰撞。
JDK 1.8 HashMap 的 hash 方法源碼:
static final int hash(Object key) {int h;// key.hashCode():返回散列值也就是hashcode// ^ :按位異或// >>>:無符號右移,忽略符號位,空位都以0補齊return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }由源碼注釋可知,(數組長度-1)正好相當于一個“低位掩碼”?!?amp;”操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數組下標訪問。以初始長度16為例,16-1=15。2進制表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結果就是截取了最低的四位值。
10100101 11000100 00100101 & 00000000 00000000 00001111 ----------------------------------00000000 00000000 00000101 //高位全部歸零,只保留末四位但這時候問題就來了,這樣就算我的散列值分布再松散,要是只取最后幾位的話,碰撞也會很嚴重。更要命的是如果散列本身做得不好,分布上成等差數列的漏洞,恰好使最后幾個低位呈現規律性重復,就很不好了。
這時候“擾動函數”的價值就體現出來了。
右位移16位,正好是32bit的一半,自己的高半區和低半區做異或,就是為了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合后的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。
8. HashMap擴容
| capacity | table 的容量大小,默認為 16。需要注意的是 capacity 必須保證為 2 的 n 次方。 |
| size | 鍵值對數量。 |
| threshold | size 的臨界值,當 size 大于等于 threshold 就必須進行擴容操作。 |
| loadFactor | 裝載因子,table 能夠使用的比例,threshold = capacity * loadFactor。 |
擴容使用 resize() 實現,當需要擴容時,令 capacity 為原來的兩倍。需要注意的是,擴容操作同樣需要把 oldTable 的所有鍵值對重新插入 newTable 中,因此這一步是很費時的。
在進行擴容時,需要把鍵值對重新放到對應的桶上。HashMap 使用了一個特殊的機制,可以降低重新計算桶下標的操作。
假設原數組長度 capacity 為 16,擴容之后 new capacity 為 32:
capacity : 00010000 new capacity : 00100000對于一個 Key,它的哈希值如果在第 5 位上為 0,那么取模得到的結果和之前一樣。如果為 1,那么得到的結果為原來的結果 +16。
17. HashSet 和 HashMap 區別
如果你看過 HashSet 源碼的話就應該知道:HashSet 底層就是基于 HashMap 實現的。(HashSet 的源碼非常非常少,因為除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不實現之外,其他方法都是直接調用 HashMap 中的方法。)
18. HashMap 和 Hashtable 的區別
①創建時如果不指定容量初始值,Hashtable 默認的初始大小為11,之后每次擴充,容量變為原來的2n+1。HashMap 默認的初始化大小為16。之后每次擴充,容量變為原來的2倍。
②創建時如果給定了容量初始值,那么 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充為2的冪次方大小,也就是說 HashMap 總是使用2的冪作為哈希表的大小。
19. ConcurrentHashMap解析
1. ConcurrentHashMap概述
ConcurrentHashMap 是 Java并發包 java.util.concurrent 中提供的一個線程安全且高效的 HashMap 實現。ConcurrentHashMap的數據結構已經接近HashMap,相對而言,ConcurrentHashMap只是增加了同步的操作來控制并發,從JDK1.7 版本的 ReentrantLock+Segment+HashEntry,到JDK1.8版本中的synchronized + CAS + HashEntry+紅黑樹。
2. ConcurrentHashMap線程安全的具體實現方式/底層具體實現
JDK1.7(上面有示意圖)
首先將數據分為一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據時,其他段的數據也能被其他線程訪問。
ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。
Segment 實現了 ReentrantLock,所以 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用于存儲鍵值對數據。
一個 ConcurrentHashMap 里包含一個 Segment 數組。Segment 的結構和HashMap類似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護著一個HashEntry數組里的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment的鎖。
JDK1.8 (上面有示意圖)
ConcurrentHashMap取消了Segment分段鎖,采用CAS和synchronized來保證并發安全。數據結構跟HashMap1.8的結構類似,數組+鏈表/紅黑二叉樹。
synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不沖突,就不會產生并發,效率又提升N倍。
3. ConcurrentHashMap 的并發度是什么?
程序運行時能夠同時更新 ConccurentHashMap 且不產生鎖競爭的最大線程數。默認為 16,且可以在構造函數中設置。當用戶設置并發度時,ConcurrentHashMap 會使用大于等于該值的最小2冪指數作為實際并發度(假如用戶設置并發度為17,實際并發度則為32)
4. ConcurrentHashMap 在 JDK 1.8 中,為什么要使用內置鎖 synchronized 來代替重入鎖 ReentrantLock?
20. ConcurrentHashMap 和 Hashtable 的區別
ConcurrentHashMap 和 Hashtable 的區別主要體現在實現線程安全的方式上不同。
21. == 和 equals 區別
1. == 和 equals 區別
- == : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象。(基本數據類型 == 比較的是值,引用數據類型 == 比較的是內存地址)
- equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:
情況1:類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價于通過“==”比較這兩個對象。
情況2:類覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來兩個對象的內容相等;若它們的內容相等,則返回 true (即,認為這兩個對象相等)。
2. 為什么要同時重寫equals()和hashCode()
當改寫equals()的時候,總是要改寫hashCode(),根據一個類的equals方法(改寫后),兩個截然不同的實例有可能在邏輯上是相等的,但是,根據Object.hashCode方法,它們僅僅是兩個對象。違反了“相等的對象必須具有相等的散列碼”原則。所以要同時重寫equals()和hashCode()。
3. 為什么要重寫 equals 方法
因為不重寫 equals 方法,執行 user1.equals(user2) 比較的就是兩個對象的地址(即 user1 == user2),肯定是不相等的,見 Object 源碼:
4. 為什么要重寫 hashCode 方法
當 equals 方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。
hashCode 是用于散列數據的快速存取,如利用 HashSet/HashMap/Hashtable 類來存儲數據時,都會根據存儲對象的 hashCode 值來進行判斷是否相同的。如果只重寫 equals() 而不重寫 hashCode() 那么 HashSet 等集合會判定為兩個對象,而不是同一個對象。
22. Java 序列化和反序列化
- 把對象轉換為字節序列的過程稱為對象的序列化。
- 把字節序列恢復為對象的過程稱為對象的反序列化。
Java 的序列化是為了保存各種對象在內存中的狀態,并且可以把保存的對象狀態再讀取出來。
以下情況需要使用 Java 序列化:
總結
以上是生活随笔為你收集整理的面经——Java基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 滑动窗口问题
- 下一篇: 二叉树的遍历(递归,非递归,Morris