2020最新大型互聯網企業Java後端技術面試題總結(含答案)

說明:以下所有答案均爲個人的理解和網上的一些資料的整合

List 和 Set 的區別

List , Set 都是繼承自 Collection 接口 List 特點:元素有放入順序,元素可重複 ,Set 特點:元素無放入順序,元素不可重複,重複元素會覆蓋掉,(元素雖然無放入順序,但是元素在set中的位置是有該元素的 HashCode 決定的,其位置其實是固定的,加入Set 的 Object 必須定義 equals ()方法 ,另外list支持for循環,也就是通過下標來遍歷,也可以用迭代器,但是set只能用迭代,因爲他無序,無法用下標來取得想要的值。) Set和List對比 Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變。

List:和數組類似,List可以動態增長,查找元素效率高,插入刪除元素效率低,因爲會引起其他元素位置改變

HashSet 是如何保證不重複的

向 HashSet 中 add ()元素時,判斷元素是否存在的依據,不僅要比較hash值,同時還要結合 equles 方法比較。

HashSet 中的 add ()方法會使用 HashMap 的 add ()方法。以下是 HashSet 部分源碼:

大型互聯網企業Java後端技術面試題總結(含答案)

 

HashMap 是線程安全的嗎,爲什麼不是線程安全的(最好畫圖說明多線程環境下不安全)?

不是線程安全的;如果有兩個線程A和B,都進行插入數據,剛好這兩條不同的數據經過哈希計算後得到的哈希碼是一樣的,且該位置還沒有其他的數據。所以這兩個線程都會進入我在上面標記爲1的代碼中。假設一種情況,線程A通過if判斷,該位置沒有哈希衝突,進入了if語句,還沒有進行數據插入,這時候 CPU 就把資源讓給了線程B,線程A停在了if語句裏面,線程B判斷該位置沒有哈希衝突(線程A的數據還沒插入),也進入了if語句,線程B執行完後,輪到線程A執行,現在線程A直接在該位置插入而不用再判斷。這時候,你會發現線程A把線程B插入的數據給覆蓋了。發生了線程不安全情況。本來在 HashMap 中,發生哈希衝突是可以用鏈表法或者紅黑樹來解決的,但是在多線程中,可能就直接給覆蓋了。

上面所說的是一個圖來解釋可能更加直觀。如下面所示,兩個線程在同一個位置添加數據,後面添加的數據就覆蓋住了前面添加的。

大型互聯網企業Java後端技術面試題總結(含答案)

 

如果上述插入是插入到鏈表上,如兩個線程都在遍歷到最後一個節點,都要在最後添加一個數據,那麼後面添加數劇的線程就會把前面添加的數據給覆蓋住。

大型互聯網企業Java後端技術面試題總結(含答案)

 

則在擴容的時候也可能會導致數據不一致,因爲擴容是從一個數組拷貝到另外一個數組。

HashMap 的擴容過程

當向容器添加元素的時候,會判斷當前容器的元素個數,如果大於等於閾值(知道這個閾字怎麼念嗎?不念 fa 值,念 yu 值四聲)---即當前數組的長度乘以加載因子的值的時候,就要自動擴容啦。

擴容( resize )就是重新計算容量,向 HashMap 對象裏不停的添加元素,而 HashMap 對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。當然 Java 裏的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

cap =3, hashMap 的容量爲4;
cap =4, hashMap 的容量爲4;
HashMap hashMap=new HashMap(cap);
cap =5, hashMap 的容量爲8;
cap =9, hashMap 的容量爲16;

如果 cap 是2的n次方,則容量爲 cap ,否則爲大於 cap 的第一個2的n次方的數。

HashMap 1.7 與 1.8 的 區別,說明 1.8 做了哪些優化,如何優化的?

HashMap結構圖

大型互聯網企業Java後端技術面試題總結(含答案)

 

在 JDK1.7 及之前的版本中, HashMap 又叫散列鏈表:基於一個數組以及多個鏈表的實現,hash值衝突的時候,就將對應節點以鏈表的形式存儲。

JDK1.8 中,當同一個hash值( Table 上元素)的鏈表節點數不小於8時,將不再以單鏈表的形式存儲了,會被調整成一顆紅黑樹。這就是 JDK7 與 JDK8 中 HashMap 實現的最大區別。

其下基於 JDK1.7.0_80 與 JDK1.8.0_66 做的分析

JDK1.7中

使用一個 Entry 數組來存儲數據,用key的 hashcode 取模來決定key會被放到數組裏的位置,如果 hashcode 相同,或者 hashcode 取模後的結果相同( hash collision ),那麼這些 key 會被定位到 Entry 數組的同一個格子裏,這些 key 會形成一個鏈表。

在 hashcode 特別差的情況下,比方說所有key的 hashcode 都相同,這個鏈表可能會很長,那麼 put/get 操作都可能需要遍歷這個鏈表,也就是說時間複雜度在最差情況下會退化到 O(n)

JDK1.8中

使用一個 Node 數組來存儲數據,但這個 Node 可能是鏈表結構,也可能是紅黑樹結構

  • 如果插入的 key 的 hashcode 相同,那麼這些key也會被定位到 Node 數組的同一個格子裏。
  • 如果同一個格子裏的key不超過8個,使用鏈表結構存儲。
  • 如果超過了8個,那麼會調用 treeifyBin 函數,將鏈表轉換爲紅黑樹。

那麼即使 hashcode 完全相同,由於紅黑樹的特點,查找某個特定元素,也只需要O(log n)的開銷也就是說put/get的操作的時間複雜度最差只有 O(log n)聽起來挺不錯,但是真正想要利用 JDK1.8 的好處,有一個限制:

key的對象,必須正確的實現了 Compare 接口如果沒有實現 Compare 接口,或者實現得不正確(比方說所有 Compare 方法都返回0)那 JDK1.8 的 HashMap 其實還是慢於 JDK1.7 的

簡單的測試數據如下:

向 HashMap 中 put/get 1w 條 hashcode 相同的對象

JDK1.7: put 0.26s , get 0.55s
JDK1.8 (未實現 Compare 接口): put 0.92s , get 2.1s

但是如果正確的實現了 Compare 接口,那麼 JDK1.8 中的 HashMap 的性能有巨大提升,這次 put/get 100W條hashcode 相同的對象

JDK1.8 (正確實現 Compare 接口,): put/get 大概開銷都在320ms 左右

final finally finalizefinal
  • 可以修飾類、變量、方法,修飾類表示該類不能被繼承、修飾方法表示該方法不能被重寫、修飾變量表示該變量是一個常量不能被重新賦值。
  • finally一般作用在try-catch代碼塊中,在處理異常的時候,通常我們將一定要執行的代碼方法finally代碼塊中,表示不管是否出現異常,該代碼塊都會執行,一般用來存放一些關閉資源的代碼。
  • finalize是一個方法,屬於Object類的一個方法,而Object類是所有類的父類,該方法一般由垃圾回收器來調用,當我們調用 System.gc() 方法的時候,由垃圾回收器調用finalize(),回收垃圾,一個對象是否可回收的最後判斷。

對象的四種引用

強引用 只要引用存在,垃圾回收器永遠不會回收

Object obj = new Object();
User user=new User();

可直接通過obj取得對應的對象 如 obj.equels(new Object()); 而這樣 obj 對象對後面 new Object 的一個強引用,只有當 obj 這個引用被釋放之後,對象纔會被釋放掉,這也是我們經常所用到的編碼形式。

軟引用 非必須引用,內存溢出之前進行回收,可以通過以下代碼實現

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有時候會返回null

這時候sf是對obj的一個軟引用,通過sf.get()方法可以取到這個對象,當然,當這個對象被標記爲需要回收的對象時,則返回null; 軟引用主要用戶實現類似緩存的功能,在內存足夠的情況下直接通過軟引用取值,無需從繁忙的真實來源查詢數據,提升速度;當內存不足時,自動刪除這部分緩存數據,從真正的來源查詢這些數據。

弱引用 第二次垃圾回收時回收,可以通過如下代碼實現

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有時候會返回null
wf.isEnQueued();//返回是否被垃圾回收器標記爲即將回收的垃圾

弱引用是在第二次垃圾回收時回收,短時間內通過弱引用取對應的數據,可以取到,當執行過第二次垃圾回收時,將返回null。弱引用主要用於監控對象是否已經被垃圾回收器標記爲即將回收的垃圾,可以通過弱引用的isEnQueued 方法返回對象是否被垃圾回收器標記。

ThreadLocal 中有使用到弱引用,
public class ThreadLocal<T> {
static class ThreadLocalMap { 
 static class Entry extends WeakReference<ThreadLocal<?>> {
 /** The value associated with this ThreadLocal. */
 Object value;
 Entry(ThreadLocal<?> k, Object v) {
 super(k);
 value = v;
 }
 }
 //....
 }
 //.....
}

虛引用 垃圾回收時回收,無法通過引用取到對象值,可以通過如下代碼實現

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永遠返回null
pf.isEnQueued();//返回是否從內存中已經刪除

虛引用是每次垃圾回收的時候都會被回收,通過虛引用的get方法永遠獲取到的數據爲null,因此也被成爲幽靈引用。虛引用主要用於檢測對象是否已經從內存中刪除。

Java獲取反射的三種方法

1.通過new對象實現反射機制 2.通過路徑實現反射機制 3.通過類名實現反射機制

public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get {
//獲取反射機制三種方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通過建立對象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通過路徑-相對路徑)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通過類名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}

Java反射機制

Java 反射機制是在運行狀態中,對於任意一個類,都能夠獲得這個類的所有屬性和方法,對於任意一個對象都能夠調用它的任意一個屬性和方法。這種在運行時動態的獲取信息以及動態調用對象的方法的功能稱爲 Java 的反射機制。

Class 類與 java.lang.reflect 類庫一起對反射的概念進行了支持,該類庫包含了 Field,Method,Constructor 類 (每個類都實現了 Member 接口)。這些類型的對象時由 JVM 在運行時創建的,用以表示未知類裏對應的成員。

這樣你就可以使用 Constructor 創建新的對象,用 get() 和 set() 方法讀取和修改與 Field 對象關聯的字段,用invoke() 方法調用與 Method 對象關聯的方法。另外,還可以調用 getFields() getMethods() 和getConstructors() 等很便利的方法,以返回表示字段,方法,以及構造器的對象的數組。這樣匿名對象的信息就能在運行時被完全確定下來,而在編譯時不需要知道任何事情。

public class Get {
//獲取反射機制三種方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通過建立對象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通過路徑-相對路徑)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通過類名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}

運行結果: 無參構造器 Run……….. 有參構造器 Run………..Apple

Arrays.sort 和 Collections.sort 實現原理 和區別

Collection和Collections區別

java.util.Collection 是一個集合接口。它提供了對集合對象進行基本操作的通用接口方法。

java.util.Collections 是針對集合類的一個幫助類,他提供一系列靜態方法實現對各種集合的搜索、排序、線程安全等操作。 然後還有混排(Shuffling)、反轉(Reverse)、替換所有的元素(fill)、拷貝(copy)、返回Collections中最小元素(min)、返回Collections中最大元素(max)、返回指定源列表中最後一次出現指定目標列表的起始位置(lastIndexOfSubList )、返回指定源列表中第一次出現指定目標列表的起始位置( IndexOfSubList )、根據指定的距離循環移動指定列表中的元素(Rotate);

事實上Collections.sort方法底層就是調用的array.sort方法,

public static void sort(Object[] a) {
 if (LegacyMergeSort.userRequested)
 legacyMergeSort(a);
 else
 ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
 }
//void java.util.ComparableTimSort.sort()
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen)
{
 assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
 int nRemaining = hi - lo;
 if (nRemaining < 2)
 return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
 if (nRemaining < MIN_MERGE) {
 int initRunLen = countRunAndMakeAscending(a, lo, hi);
 binarySort(a, lo, hi, lo + initRunLen);
 return;
 }
}

legacyMergeSort (a):歸併排序 ComparableTimSort.sort() : Timsort 排序

Timsort 排序是結合了合併排序(merge sort)和插入排序(insertion sort)而得出的排序算法Timsort的核心過程TimSort 算法爲了減少對升序部分的回溯和對降序部分的性能倒退,將輸入按其升序和降序特點進行了分區。排序的輸入的單位不是一個個單獨的數字,而是一個個的塊-分區。其中每一個分區叫一個run。針對這些 run 序列,每次拿一個 run 出來按規則進行合併。每次合併會將兩個 run合併成一個 run。合併的結果保存到棧中。合併直到消耗掉所有的 run,這時將棧上剩餘的 run合併到只剩一個 run 爲止。這時這個僅剩的run 便是排好序的結果。

綜上述過程,Timsort算法的過程包括

(0)如何數組長度小於某個值,直接用二分插入排序算法

(1)找到各個run,併入棧

(2)按規則合併run

LinkedHashMap 的應用

基於 LinkedHashMap 的訪問順序的特點,可構造一個 LRU(Least Recently Used) 最近最少使用簡單緩存。

也有一些開源的緩存產品如 ehcache 的淘汰策略( LRU )就是在 LinkedHashMap 上擴展的。

Cloneable 接口實現原理

Cloneable接口是Java開發中常用的一個接口, 它的作用是使一個類的實例能夠將自身拷貝到另一個新的實例中,注意,這裏所說的“拷貝”拷的是對象實例,而不是類的定義,進一步說,拷貝的是一個類的實例中各字段的值。

在開發過程中,拷貝實例是常見的一種操作,如果一個類中的字段較多,而我們又採用在客戶端中逐字段複製的方法進行拷貝操作的話,將不可避免的造成客戶端代碼繁雜冗長,而且也無法對類中的私有成員進行復制,而如果讓需要具備拷貝功能的類實現Cloneable接口,並重寫clone()方法,就可以通過調用clone()方法的方式簡潔地實現實例拷貝功能

深拷貝(深複製)和淺拷貝(淺複製)是兩個比較通用的概念,尤其在C++語言中,若不弄懂,則會在delete的時候出問題,但是我們在這幸好用的是Java。雖然Java自動管理對象的回收,但對於深拷貝(深複製)和淺拷貝(淺複製),我們還是要給予足夠的重視,因爲有時這兩個概念往往會給我們帶來不小的困惑。

淺拷貝是指拷貝對象時僅僅拷貝對象本身(包括對象中的基本變量),而不拷貝對象包含的引用指向的對象。深拷貝不僅拷貝對象本身,而且拷貝對象包含的引用指向的所有對象。舉例來說更加清楚:對象 A1 中包含對 B1 的引用, B1 中包含對 C1 的引用。淺拷貝 A1 得到 A2 , A2 中依然包含對 B1 的引用, B1 中依然包含對 C1 的引用。深拷貝則是對淺拷貝的遞歸,深拷貝 A1 得到 A2 , A2 中包含對 B2 ( B1 的 copy )的引用, B2 中包含對 C2 ( C1 的 copy )的引用。

若不對clone()方法進行改寫,則調用此方法得到的對象即爲淺拷貝

異常分類以及處理機制

大型互聯網企業Java後端技術面試題總結(含答案)

 

Java標準庫內建了一些通用的異常,這些類以Throwable爲頂層父類。

Throwable又派生出Error類和Exception類。

錯誤:Error類以及他的子類的實例,代表了JVM本身的錯誤。錯誤不能被程序員通過代碼處理,Error很少出現。

因此,程序員應該關注Exception爲父類的分支下的各種異常類。

異常:Exception以及他的子類,代表程序運行時發送的各種不期望發生的事件。可以被Java異常處理機制使用,是異常處理的核心。

大型互聯網企業Java後端技術面試題總結(含答案)

 

總體上我們根據 Javac 對異常的處理要求,將異常類分爲二類。

非檢查異常( unckecked exception ): Error 和 RuntimeException 以及他們的子類。 javac 在編譯時,不會提示和發現這樣的異常,不要求在程序處理這些異常。所以如果願意,我們可以編寫代碼處理(使用 try…catch…finally )這樣的異常,也可以不處理。對於這些異常,我們應該修正代碼,而不是去通過異常處理器處理 。這樣的異常發生的原因多半是代碼寫的有問題。如除0錯誤 ArithmeticException ,錯誤的強制類型轉換錯誤ClassCastException ,數組索引越界
ArrayIndexOutOfBoundsException ,使用了空對象NullPointerException 等等。

檢查異常( checked exception ):除了 Error 和 RuntimeException 的其它異常。 javac 強制要求程序員爲這樣的異常做預備處理工作(使用 try…catch…finally 或者 throws )。在方法中要麼用 try-catch 語句捕獲它並處理,要麼用throws子句聲明拋出它,否則編譯不會通過。這樣的異常一般是由程序的運行環境導致的。因爲程序可能被運行在各種未知的環境下,而程序員無法干預用戶如何使用他編寫的程序,於是程序員就應該爲這樣的異常時刻準備着。如 SQLException , IOException , ClassNotFoundException 等。

需要明確的是:檢查和非檢查是對於 javac 來說的,這樣就很好理解和區分了。

wait 和 sleep 的區別

源碼如下

public class Thread implements Runnable { 
public static native void sleep(long millis) throws InterruptedException;
 public static void sleep(long millis, int nanos) throws InterruptedException {
 if (millis < 0) {
 throw new IllegalArgumentException("timeout value is negative");
 }
 if (nanos < 0 || nanos > 999999) {
 throw new IllegalArgumentException(
 "nanosecond timeout value out of range");
 }
 if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
 millis++;
 }
 sleep(millis);
 }
 //...
} 

public class Object {
 public final native void wait(long timeout) throws InterruptedException;
 public final void wait(long timeout, int nanos) throws InterruptedException {
 if (timeout < 0) {
 throw new IllegalArgumentException("timeout value is negative");
 }
 if (nanos < 0 || nanos > 999999) {
 throw new IllegalArgumentException(
 "nanosecond timeout value out of range");
 }
 if (nanos > 0) {
 timeout++;
 }
 wait(timeout);
 }
 //...
}

1、 sleep 來自 Thread 類,和 wait 來自 Object 類。

2、最主要是sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。

3、wait,notify和 notifyAll 只能在同步控制方法或者同步控制塊裏面使用,而 sleep 可以在任何地方使用(使用範圍)

4、 sleep 必須捕獲異常,而 wait , notify 和 notifyAll 不需要捕獲異常

(1) sleep 方法屬於 Thread 類中方法,表示讓一個線程進入睡眠狀態,等待一定的時間之後,自動醒來進入到可運行狀態,不會馬上進入運行狀態,因爲線程調度機制恢復線程的運行也需要時間,一個線程對象調用了 sleep方法之後,並不會釋放他所持有的所有對象鎖,所以也就不會影響其他進程對象的運行。但在 sleep 的過程中過程中有可能被其他對象調用它的 interrupt() ,產生 InterruptedException 異常,如果你的程序不捕獲這個異常,線程就會異常終止,進入 TERMINATED 狀態,如果你的程序捕獲了這個異常,那麼程序就會繼續執行catch語句塊(可能還有 finally 語句塊)以及以後的代碼。

注意 sleep() 方法是一個靜態方法,也就是說他只對當前對象有效,通過 t.sleep() 讓t對象進入 sleep ,這樣的做法是錯誤的,它只會是使當前線程被 sleep 而不是 t 線程

(2) wait 屬於 Object 的成員方法,一旦一個對象調用了wait方法,必須要採用 notify() 和 notifyAll() 方法喚醒該進程;如果線程擁有某個或某些對象的同步鎖,那麼在調用了 wait() 後,這個線程就會釋放它持有的所有同步資源,而不限於這個被調用了 wait() 方法的對象。 wait() 方法也同樣會在 wait 的過程中有可能被其他對象調用 interrupt() 方法而產生 。

數組在內存中如何分配

對於 Java 數組的初始化,有以下兩種方式,這也是面試中經常考到的經典題目:

靜態初始化:初始化時由程序員顯式指定每個數組元素的初始值,由系統決定數組長度,如:

//只是指定初始值,並沒有指定數組的長度,但是系統爲自動決定該數組的長度爲4
String[] computers = {"Dell", "Lenovo", "Apple", "Acer"}; //①
//只是指定初始值,並沒有指定數組的長度,但是系統爲自動決定該數組的長度爲3
String[] names = new String[]{"多啦A夢", "大雄", "靜香"}; //②

動態初始化:初始化時由程序員顯示的指定數組的長度,由系統爲數據每個元素分配初始值,如:

//只是指定了數組的長度,並沒有顯示的爲數組指定初始值,但是系統會默認給數組數組元素分配初始值爲null
String[] cars = new String[4]; //③

因爲 Java 數組變量是引用類型的變量,所以上述幾行初始化語句執行後,三個數組在內存中的分配情況如下圖所示:

大型互聯網企業Java後端技術面試題總結(含答案)

 

由上圖可知,靜態初始化方式,程序員雖然沒有指定數組長度,但是系統已經自動幫我們給分配了,而動態初始化方式,程序員雖然沒有顯示的指定初始化值,但是因爲 Java 數組是引用類型的變量,所以系統也爲每個元素分配了初始化值 null ,當然不同類型的初始化值也是不一樣的,假設是基本類型int類型,那麼爲系統分配的初始化值也是對應的默認值0。

福利來啦!!!

針對粉絲個人總結了Java後端其它全面的面試經驗以及技術面試題(含答案)的文檔資料想要刷面試題提升自己面試經驗的關注我私信回覆【架構面試】領取面試資料

大型互聯網企業Java後端技術面試題總結(含答案)

 

大型互聯網企業Java後端技術面試題總結(含答案)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章