微信搜尋【程式設計師囧輝】,關注這個堅持分享技術乾貨的程式設計師。

我的最新面試題文章:全網最硬核 Redis 高頻面試題解析(2021年最新版)

前言

網上的 Java 基礎面試題文章有非常多,但是大部分都比較老了。

很多題目早已不是當前的熱門題目,沒有必要在這些題目上花太多時間。

很多答案放現在已經不準確,可能會誤導新人。

因此,我花了幾天時間整理了一些時下高頻的 Java 基礎題目,並反覆斟酌,給出符合當前版本的解析。

面試系列

我自己前前後後加起來總共應該參加了不下四五十次的面試,拿到過幾乎所有一線大廠的 offer:阿里、位元組、美團、快手、拼多多等等。

每次面試後我都會將面試的題目進行記錄,並整理成自己的題庫,最近我將這些題目整理出來,並按大廠的標準給出自己的解析,希望在這金三銀四的季節裡,能助你一臂之力。

面試文章持續更新中。。。

1、面試經驗分享

921天,從小廠到入職阿里

兩年Java開發工作經驗面試總結

4 年 Java 經驗面試總結、心得體會

5 年 Java 經驗,位元組、美團、快手核心部門面試總結(真題解析)

複習2個月拿下美團offer,我都做了些啥

如何準備好一場大廠面試

2、簡歷

如何寫一份讓 HR 眼前一亮的簡歷(附模板)

3、Offer選擇

跳槽,如何選擇一家公司

4、Java基礎

一道有意思的“初始化”面試題

5、集合

問遍了身邊的面試官朋友,我整理出這份 Java 集合高頻面試題(2021年最新版)

面試阿里,HashMap 這一篇就夠了

6、併發程式設計

面試必問的執行緒池,你懂了嗎?

面試必問的CAS,你懂了嗎?

7、MySQL

面試必問的 MySQL,你懂了嗎?

MySQL 8。0 MVCC 核心原理解析(核心原始碼)

8、Spring

面試必問的 Spring,你懂了嗎?

9、MyBatis

面試題:mybatis 中的 DAO 介面和 XML 檔案裡的 SQL 是如何建立關係的?

10、Redis

全網最硬核 Redis 高頻面試題解析(2021年最新版)

11、JVM

Java虛擬機器面試題精選(二)

Java虛擬機器面試題精選(一)

12、分散式

面試必問的分散式鎖,你懂了嗎?

正文

1、面向物件的三個基本特徵?

面向物件的三個基本特徵是:封裝、繼承和多型。

繼承:讓某個型別的物件獲得另一個型別的物件的屬性的方法。繼承就是子類繼承父類的特徵和行為,使得子類物件(例項)具有父類的例項域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。

封裝:隱藏部分物件的屬性和實現細節,對資料的訪問只能透過外公開的介面。透過這種方式,物件對內部資料提供了不同級別的保護,以防止程式中無關的部分意外的改變或錯誤的使用了物件的私有部分。

多型:對於同一個行為,不同的子類物件具有不同的表現形式。多型存在的3個條件:1)繼承;2)重寫;3)父類引用指向子類物件。

舉個簡單的例子:英雄聯盟裡面我們按下 Q 鍵這個動作:

對於亞索,就是斬鋼閃

對於提莫,就是致盲吹箭

對於劍聖,就是阿爾法突襲

同一個事件發生在不同的物件上會產生不同的結果。

我再舉一個簡單的例子幫助大家理解,這個例子可能不是完全準確,但是我認為是有利於理解的。

public

class

Animal

{

// 動物

public

void

sleep

()

{

System

out

println

“躺著睡”

);

}

}

class

Horse

extends

Animal

{

// 馬 是一種動物

public

void

sleep

()

{

System

out

println

“站著睡”

);

}

}

class

Cat

extends

Animal

{

// 貓 是一種動物

private

int

age

public

int

getAge

()

{

return

age

+

1

}

@Override

public

void

sleep

()

{

System

out

println

“四腳朝天的睡”

);

}

}

在這個例子中:

House 和 Cat 都是 Animal,所以他們都繼承了 Animal,同時也從 Animal 繼承了 sleep 這個行為。

但是針對 sleep 這個行為,House 和 Cat 進行了重寫,有了不同的表現形式(實現),這個我們稱為多型。

在 Cat 裡,將 age 屬性定義為 private,外界無法直接訪問,要獲取 Cat 的 age 資訊只能透過 getAge 方法,從而對外隱藏了 age 屬性,這個就叫做封裝。當然,這邊 age 只是個例子,實際使用中可能是一個複雜很多的物件。

2、訪問修飾符public,private,protected,以及不寫時的區別?

Java 基礎高頻面試題(2021年最新版)

3、下面兩個程式碼塊能正常編譯和執行嗎?

// 程式碼塊1

short

s1

=

1

s1

=

s1

+

1

// 程式碼塊2

short

s1

=

1

s1

+=

1

程式碼塊1編譯報錯,錯誤原因是:不相容的型別: 從int轉換到short可能會有損失”。

程式碼塊2正常編譯和執行。

我們將程式碼塊2進行編譯,位元組碼如下:

public

class

com

joonwhee

open

demo

Convert

{

public

com

joonwhee

open

demo

Convert

();

Code

0

aload_0

1

invokespecial

#

1

// Method java/lang/Object。“”:()V

4

return

public

static

void

main

java

lang

String

[]);

Code

0

iconst_1

// 將int型別值1入(運算元)棧

1

istore_1

// 將棧頂int型別值儲存到區域性變數1中

2

iload_1

// 從區域性變數1中裝載int型別值入棧

3

iconst_1

// 將int型別值1入棧

4

iadd

// 將棧頂兩int型別數相加,結果入棧

5

i2s

// 將棧頂int型別值截斷成short型別值,後帶符號擴充套件成int型別值入棧。

6

istore_1

// 將棧頂int型別值儲存到區域性變數1中

7

return

}

可以看到位元組碼中包含了 i2s 指令,該指令用於將 int 轉成 short。i2s 是 int to short 的縮寫。

其實,s1 += 1 相當於 s1 = (short)(s1 + 1),有興趣的可以自己編譯下這兩行程式碼的位元組碼,你會發現是一摸一樣的。

說好的 Java 基礎題,怎麼又開始變態起來了???

Java 基礎高頻面試題(2021年最新版)

4、基礎考察,指出下題的輸出結果

public

static

void

main

String

[]

args

{

Integer

a

=

128

b

=

128

c

=

127

d

=

127

System

out

println

a

==

b

);

System

out

println

c

==

d

);

}

答案是:false,true。

執行 Integer a = 128,相當於執行:Integer a = Integer。valueOf(128),基本型別自動轉換為包裝類的過程稱為

自動裝箱

(autoboxing)。

public

static

Integer

valueOf

int

i

{

if

i

>=

IntegerCache

low

&&

i

<=

IntegerCache

high

return

IntegerCache

cache

i

+

(-

IntegerCache

low

)];

return

new

Integer

i

);

}

在 Integer 中引入了 IntegerCache 來快取一定範圍的值,IntegerCache 預設情況下範圍為:-128~127。

本題中的 127 命中了 IntegerCache,所以 c 和 d 是相同物件,而 128 則沒有命中,所以 a 和 b 是不同物件。

但是這個快取範圍時可以修改的,可能有些人不知道。可以透過JVM啟動引數:-XX:AutoBoxCacheMax= 來修改上限值,如下圖所示:

Java 基礎高頻面試題(2021年最新版)

5、用最有效率的方法計算2乘以8?

2 << 3。

進階:通常情況下,可以認為位運算是效能最高的。但是,其實編譯器現在已經“非常聰明瞭”,很多指令編譯器都能自己做最佳化。所以在實際實用中,我們無需特意去追求實用位運算,這樣不僅會導致程式碼可讀性很差,而且某些自作聰明的最佳化反而會誤導編譯器,使得編譯器無法進行更好的最佳化。

這可能就是所謂的“豬隊友”吧。

Java 基礎高頻面試題(2021年最新版)

6、&和&&的區別?

&&:

邏輯與運算子

。當運算子左右兩邊的表示式都為 true,才返回 true。同時具有短路性,如果第一個表示式為 false,則直接返回 false。

&:邏輯與運算子、按位與運算子。

按位與運算子:用於二進位制的計算,只有對應的兩個

二進位

均為1時,結果位才為1 ,否則為0。

邏輯與運算子:& 在用於邏輯與時,和 && 的區別是不具有短路性。所在通常使用邏輯與運算子都會使用 &&,而 & 更多的適用於位運算。

7、String 是 Java 基本資料型別嗎?

答:不是。Java 中的基本資料型別只有8個:byte、short、int、long、float、double、char、boolean;除了基本型別(primitive type),剩下的都是引用型別(reference type)。

基本資料型別:資料直接儲存在棧上

引用資料型別區別:資料儲存在堆上,棧上只儲存引用地址

8、String 類可以繼承嗎?

不行。String 類使用 final 修飾,無法被繼承。

9、String和StringBuilder、StringBuffer的區別?

String:String 的值被建立後不能修改,任何對 String 的修改都會引發新的 String 物件的生成。

StringBuffer:跟 String 類似,但是值可以被修改,使用 synchronized 來保證執行緒安全。

StringBuilder:StringBuffer 的非執行緒安全版本,沒有使用 synchronized,具有更高的效能,推薦優先使用。

10、String s = new String(“xyz”) 建立了幾個字串物件?

一個或兩個。如果字串常量池已經有“xyz”,則是一個;否則,兩個。

當字元創常量池沒有 “xyz”,此時會建立如下兩個物件:

一個是字串字面量 “xyz” 所對應的、駐留(intern)在一個全域性共享的字串常量池中的例項,此時該例項也是在堆中,字串常量池只放引用。

另一個是透過 new String() 建立並初始化的,內容與“xyz”相同的例項,也是在堆中。

11、String s = “xyz” 和 String s = new String(“xyz”) 區別?

兩個語句都會先去字串常量池中檢查是否已經存在 “xyz”,如果有則直接使用,如果沒有則會在常量池中建立 “xyz” 物件。

另外,String s = new String(“xyz”) 還會透過 new String() 在堆裡建立一個內容與 “xyz” 相同的物件例項。

所以前者其實理解為被後者的所包含。

12、== 和 equals 的區別是什麼?

==:運算子,用於比較基礎型別變數和引用型別變數。

對於

基礎型別變數

,比較的變數儲存的值是否相同,型別不一定要相同。

short

s1

=

1

long

l1

=

1

// 結果:true。型別不同,但是值相同

System

out

println

s1

==

l1

);

對於引用型別變數,比較的是兩個物件的地址是否相同。

Integer

i1

=

new

Integer

1

);

Integer

i2

=

new

Integer

1

);

// 結果:false。透過new建立,在記憶體中指向兩個不同的物件

System

out

println

i1

==

i2

);

equals:Object 類中定義的方法,通常用於比較兩個物件的值是否相等。

equals 在 Object 方法中其實等同於 ==,但是在實際的使用中,equals 通常被重寫用於比較兩個物件的值是否相同。

Integer

i1

=

new

Integer

1

);

Integer

i2

=

new

Integer

1

);

// 結果:true。兩個不同的物件,但是具有相同的值

System

out

println

i1

equals

i2

));

// Integer的equals重寫方法

public

boolean

equals

Object

obj

{

if

obj

instanceof

Integer

{

// 比較物件中儲存的值是否相同

return

value

==

((

Integer

obj

)。

intValue

();

}

return

false

}

13、兩個物件的 hashCode() 相同,則 equals() 也一定為 true,對嗎?

不對。hashCode() 和 equals() 之間的關係如下:

當有 a。equals(b) == true 時,則 a。hashCode() == b。hashCode() 必然成立,

反過來,當 a。hashCode() == b。hashCode() 時,a。equals(b) 不一定為 true。

14、什麼是反射

反射是指在執行狀態中,對於任意一個類都能夠知道這個類所有的屬性和方法;並且對於任意一個物件,都能夠呼叫它的任意一個方法;這種動態獲取資訊以及動態呼叫物件方法的功能稱為反射機制。

15、深複製和淺複製區別是什麼?

資料分為基本資料型別和引用資料型別。基本資料型別:資料直接儲存在棧中;引用資料型別:儲存在棧中的是物件的引用地址,真實的物件資料存放在堆記憶體裡。

淺複製:對於基礎資料型別:直接複製資料值;對於引用資料型別:只是複製了物件的引用地址,新舊物件指向同一個記憶體地址,修改其中一個物件的值,另一個物件的值隨之改變。

深複製:對於基礎資料型別:直接複製資料值;對於引用資料型別:開闢新的記憶體空間,在新的記憶體空間裡複製一個一模一樣的物件,新老物件不共享記憶體,修改其中一個物件的值,不會影響另一個物件。

深複製相比於淺複製速度較慢並且花銷較大。

16、併發和並行有什麼區別?

併發:兩個或多個事件在同一時間間隔發生。

並行:兩個或者多個事件在同一時刻發生。

並行是真正意義上,同一時刻做多件事情,而併發在同一時刻只會做一件事件,只是可以將時間切碎,交替做多件事情。

網上有個例子挺形象的:

你吃飯吃到一半,電話來了,你一直到吃完了以後才去接,這就說明你不支援併發也不支援並行。

你吃飯吃到一半,電話來了,你停了下來接了電話,接完後繼續吃飯,這說明你支援併發。

你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支援並行。

17、

構造器

是否可被 重寫?

Constructor 不能被 override(重寫),但是可以 overload(過載),所以你可以看到⼀個類中有多個建構函式的情況。

18、當一個物件被當作引數傳遞到一個方法後,此方法可改變這個物件的屬性,並可返回變化後的結果,那麼這裡到底是值傳遞還是引用傳遞?

值傳遞。Java 中只有值傳遞,對於物件引數,值的內容是物件的引用。

19、Java 靜態變數和成員變數的區別。

public

class

Demo

{

/**

* 靜態變數:又稱類變數,static修飾

*/

public

static

String

STATIC_VARIABLE

=

“靜態變數”

/**

* 例項變數:又稱成員變數,沒有static修飾

*/

public

String

INSTANCE_VARIABLE

=

“例項變數”

}

成員變數存在於堆記憶體中。靜態變數存在於方法區中。

成員變數與物件共存亡,隨著物件建立而存在,隨著物件被回收而釋放。靜態變數與類共存亡,隨著類的載入而存在,隨著類的消失而消失。

成員變數所屬於物件,所以也稱為例項變數。靜態變數所屬於類,所以也稱為類變數。

成員變數只能被物件所呼叫 。靜態變數可以被物件呼叫,也可以被類名呼叫。

20、是否可以從一個靜態(static)方法內部發出對非靜態(non-static)方法的呼叫?

區分兩種情況,發出呼叫時是否顯示建立了物件例項。

1)沒有顯示建立物件例項:不可以發起呼叫,非靜態方法只能被物件所呼叫,靜態方法可以透過物件呼叫,也可以透過類名呼叫,所以靜態方法被呼叫時,可能還沒有建立任何例項物件。因此透過靜態方法內部發出對非靜態方法的呼叫,此時可能無法知道非靜態方法屬於哪個物件。

public

class

Demo

{

public

static

void

staticMethod

()

{

// 直接呼叫非靜態方法:編譯報錯

instanceMethod

();

}

public

void

instanceMethod

()

{

System

out

println

“非靜態方法”

);

}

}

2)顯示建立物件例項:可以發起呼叫,在靜態方法中顯示的建立物件例項,則可以正常的呼叫。

public

class

Demo

{

public

static

void

staticMethod

()

{

// 先建立例項物件,再呼叫非靜態方法:成功執行

Demo

demo

=

new

Demo

();

demo

instanceMethod

();

}

public

void

instanceMethod

()

{

System

out

println

“非靜態方法”

);

}

}

21、初始化考察,請指出下面程式的執行結果。

public

class

InitialTest

{

public

static

void

main

String

[]

args

{

A

ab

=

new

B

();

ab

=

new

B

();

}

}

class

A

{

static

{

// 父類靜態程式碼塊

System

out

print

“A”

);

}

public

A

()

{

// 父類構造器

System

out

print

“a”

);

}

}

class

B

extends

A

{

static

{

//

子類靜態程式碼

System

out

print

“B”

);

}

public

B

()

{

// 子類構造器

System

out

print

“b”

);

}

}

執行結果:ABabab,兩個考察點:

1)靜態變數只會初始化(執行)一次。

2)當有父類時,完整的初始化順序為:父類靜態變數(靜態程式碼塊)->

子類靜態變數

(靜態程式碼塊)->父類非靜態變數(非靜態程式碼塊)->父類構造器 ->子類非靜態變數(非靜態程式碼塊)->子類構造器 。

關於初始化,這題算入門題,我之前還寫過一道有(fei)點(chang)意(bian)思(tai)的進階題目,有興趣的可以看看:一道有意思的“初始化”面試題

22、過載(Overload)和重寫(Override)的區別?

方法的過載和重寫都是實現多型的方式,區別在於前者實現的是編譯時的

多型性

,而後者實現的是執行時的多型性。

過載:一個類中有多個同名的方法,但是具有有不同的引數列表(引數型別不同、引數個數不同或者二者都不同)。

重寫:發生在子類與父類之間,子類對父類的方法進行重寫,引數都不能改變,返回值型別可以不相同,但是必須是父類返回值的派生類。即外殼不變,核心重寫!重寫的好處在於子類可以根據需要,定義特定於自己的行為。

23、為什麼不能根據返回型別來區分過載?

如果我們有兩個方法如下,當我們呼叫:test(1) 時,編譯器無法確認要呼叫的是哪個。

// 方法1

int

test

int

a

);

// 方法2

long

test

int

a

);

方法的返回值只是作為方法執行之後的一個“狀態”,但是並不是所有呼叫都關注返回值,所以不能將返回值作為過載的唯一區分條件。

24、抽象類(abstract class)和介面(interface)有什麼區別?

抽象類只能單繼承,介面可以多實現。

抽象類可以有構造方法,介面中不能有構造方法。

抽象類中可以有成員變數,介面中沒有成員變數,只能有常量(預設就是 public static final)

抽象類中可以包含非抽象的方法,在 Java 7 之前介面中的所有方法都是抽象的,在 Java 8 之後,介面支援非抽象方法:default 方法、靜態方法等。Java 9 支援私有方法、私有靜態方法。

抽象類中的抽象方法型別可以是任意修飾符,Java 8 之前介面中的方法只能是 public 型別,Java 9 支援 private 型別。

設計思想的區別:

介面是自上而下的抽象過程,介面規範了某些行為,是對某一行為的抽象。我需要這個行為,我就去實現某個介面,但是具體這個行為怎麼實現,完全由自己決定。

抽象類是自下而上的抽象過程,抽象類提供了通用實現,是對某一類事物的抽象。我們在寫實現類的時候,發現某些實現類具有幾乎相同的實現,因此我們將這些相同的實現抽取出來成為抽象類,然後如果有一些差異點,則可以提供抽象方法來支援自定義實現。

我在網上看到有個說法,挺形象的:

普通類像親爹 ,他有啥都是你的。

抽象類像叔伯,有一部分會給你,還能指導你做事的方法。

介面像乾爹,可以給你指引方法,但是做成啥樣得你自己努力實現。

25、Error 和 Exception 有什麼區別?

Error 和 Exception 都是 Throwable 的子類,用於表示程式出現了不正常的情況。區別在於:

Error 表示系統級的錯誤和程式不必處理的異常,是恢復不是不可能但很困難的情況下的一種嚴重問題,比如記憶體溢位,不可能指望程式能處理這樣的情況。

Exception 表示需要捕捉或者需要程式進行處理的異常,是一種設計或實現問題,也就是說,它表示如果程式執行正常,從不會發生的情況。

26、Java 中的 final 關鍵字有哪些用法?

修飾類:該類不能再派生出新的子類,不能作為父類被繼承。因此,一個類不能同時被宣告為abstract 和 final。

修飾方法:該方法不能被子類重寫。

修飾變數:該變數必須在宣告時給定初值,而在以後只能讀取,不可修改。 如果變數是物件,則指的是引用不可修改,但是物件的屬性還是可以修改的。

public

class

FinalDemo

{

// 不可再修改該變數的值

public

static

final

int

FINAL_VARIABLE

=

0

// 不可再修改該變數的引用,但是可以直接修改屬性值

public

static

final

User

USER

=

new

User

();

public

static

void

main

String

[]

args

{

// 輸出:User(id=0, name=null, age=0)

System

out

println

USER

);

// 直接修改屬性值

USER

setName

“test”

);

// 輸出:User(id=0, name=test, age=0)

System

out

println

USER

);

}

}

27、闡述 final、finally、finalize 的區別。

其實是三個完全不相關的東西,只是長的有點像。。

final 如上所示。

finally:finally 是對 Java 異常處理機制的最佳補充,通常配合 try、catch 使用,用於存放那些無論是否出現異常都一定會執行的程式碼。在實際使用中,通常用於釋放鎖、資料庫連線等資源,把資源釋放方法放到 finally 中,可以大大降低程式出錯的機率。

finalize:Object 中的方法,在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。finalize()方法僅作為了解即可,在 Java 9 中該方法已經被標記為廢棄,並新增新的 java。lang。ref。Cleaner,提供了更靈活和有效的方法來釋放資源。這也側面說明了,這個方法的設計是失敗的,因此更加不能去使用它。

28、try、catch、finally 考察,請指出下面程式的執行結果。

public

class

TryDemo

{

public

static

void

main

String

[]

args

{

System

out

println

test

());

}

public

static

int

test

()

{

try

{

return

1

}

catch

Exception

e

{

return

2

}

finally

{

System

out

print

“3”

);

}

}

}

執行結果:31。

相信很多同學應該都做對了,try、catch。finally 的基礎用法,在 return 前會先執行 finally 語句塊,所以是先輸出 finally 裡的 3,再輸出 return 的 1。

29、try、catch、finally 考察2,請指出下面程式的執行結果。

public

class

TryDemo

{

public

static

void

main

String

[]

args

{

System

out

println

test1

());

}

public

static

int

test1

()

{

try

{

return

2

}

finally

{

return

3

}

}

}

執行結果:3。

這題有點先將,但也不難,try 返回前先執行 finally,結果 finally 裡不按套路出牌,直接 return 了,自然也就走不到 try 裡面的 return 了。

finally 裡面使用 return 僅存在於面試題中,實際開發中千萬不要這麼用。

30、try、catch、finally 考察3,請指出下面程式的執行結果。

public

class

TryDemo

{

public

static

void

main

String

[]

args

{

System

out

println

test1

());

}

public

static

int

test1

()

{

int

i

=

0

try

{

i

=

2

return

i

}

finally

{

i

=

3

}

}

}

執行結果:2。

這邊估計有不少同學會以為結果應該是 3,因為我們知道在 return 前會執行 finally,而 i 在 finally 中被修改為 3 了,那最終返回 i 不是應該為 3 嗎?確實很容易這麼想,我最初也是這麼想的,當初的自己還是太年輕了啊。

這邊的根本原因是,在執行 finally 之前,JVM 會先將 i 的結果暫存起來,然後 finally 執行完畢後,會返回之前暫存的結果,而不是返回 i,所以即使這邊 i 已經被修改為 3,最終返回的還是之前暫存起來的結果 2。

這邊其實根據位元組碼可以很容易看出來,在進入 finally 之前,JVM 會使用 iload、istore 兩個指令,將結果暫存,在最終返回時在透過 iload、ireturn 指令返回暫存的結果。

為了避免氣氛再次變態起來,我這邊就不貼具體的位元組碼程式了,有興趣的同學可以自己編譯檢視下。

Java 基礎高頻面試題(2021年最新版)

31、JDK1。8之後有哪些新特性?

介面預設方法:Java 8允許我們給介面新增一個非抽象的方法實現,只需要使用 default關鍵字即可

Lambda 表示式和函式式介面:Lambda 表示式本質上是一段匿名內部類,也可以是一段可以傳遞的程式碼。Lambda 允許把函式作為一個方法的引數(函式作為引數傳遞到方法中),使用 Lambda 表示式使程式碼更加簡潔,但是也不要濫用,否則會有可讀性等問題,《

Effective Java

》作者 Josh Bloch 建議使用 Lambda 表示式最好不要超過3行。

Stream API:用函數語言程式設計方式在集合類上進行復雜操作的工具,配合Lambda表示式可以方便的對集合進行處理。Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查詢、過濾和對映資料等操作。使用Stream API 對集合資料進行操作,就類似於使用 SQL 執行的資料庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之,Stream API 提供了一種高效且易於使用的處理資料的方式。

方法引用:方法引用提供了非常有用的語法,可以直接引用已有Java類或物件(例項)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗餘程式碼。

日期時間API

:Java 8 引入了新的

日期時間AP

I改進了日期時間的管理。

Optional 類:著名的 NullPointerException 是引起系統失敗最常見的原因。很久以前 Google Guava 專案引入了 Optional 作為解決空指標異常的一種方式,不贊成程式碼被 null 檢查的程式碼汙染,期望程式設計師寫整潔的程式碼。受Google Guava的鼓勵,Optional 現在是Java 8庫的一部分。

新工具:新的編譯工具,如:Nashorn引擎 jjs、 類依賴分析器 jdeps。

50、wait() 和 sleep() 方法的區別

對於同步鎖的影響不同:sleep() 不會該表同步鎖的行為,如果當前執行緒持有同步鎖,那麼 sleep 是不會讓執行緒釋放同步鎖的。wait() 會釋放同步鎖,讓其他執行緒進入 synchronized 程式碼塊執行。

使用範圍不同:sleep() 可以在任何地方使用。wait() 只能在同步控制方法或者同步控制塊裡面使用,否則會拋 IllegalMonitorStateException。

恢復方式不同:兩者會暫停當前執行緒,但是在恢復上不太一樣。sleep() 在時間到了之後會重新恢復;wait() 則需要其他執行緒呼叫同一物件的 notify()/nofityAll() 才能重新恢復。

51、執行緒的 sleep() 方法和 yield() 方法有什麼區別?

執行緒執行 sleep() 方法後進入超時等待(TIMED_WAITING)狀態,而執行 yield() 方法後進入就緒(READY)狀態。

sleep() 方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒執行的機會;yield() 方法只會給相同優先順序或更高優先順序的執行緒以執行的機會。

52、執行緒的 join() 方法是幹啥用的?

用於等待當前執行緒終止。如果一個執行緒A執行了 threadB。join() 語句,其含義是:當前執行緒A等待 threadB 執行緒終止之後才從 threadB。join() 返回繼續往下執行自己的程式碼。

53、編寫多執行緒程式有幾種實現方式?

通常來說,可以認為有三種方式:1)繼承 Thread 類;2)實現 Runnable 介面;3)實現 Callable 介面。

其中,Thread 其實也是實現了 Runable 介面。Runnable 和 Callable 的主要區別在於是否有返回值。

54、Thread 呼叫 start() 方法和呼叫 run() 方法的區別

run():普通的方法呼叫,在主執行緒中執行,不會新建一個執行緒來執行。

start():新啟動一個執行緒,這時此執行緒處於就緒(可執行)狀態,並沒有執行,一旦得到 CPU 時間片,就開始執行 run() 方法。

55、執行緒的狀態流轉

Java 基礎高頻面試題(2021年最新版)

​一個執行緒可以處於以下狀態之一:

NEW:新建但是尚未啟動的執行緒處於此狀態,沒有呼叫 start() 方法。

RUNNABLE:包含就緒(READY)和執行中(RUNNING)兩種狀態。執行緒呼叫 start() 方法會會進入就緒(READY)狀態,等待獲取 CPU 時間片。如果成功獲取到 CPU 時間片,則會進入執行中(RUNNING)狀態。

BLOCKED:執行緒在進入同步方法/同步塊(synchronized)時被阻塞,等待同步鎖的執行緒處於此狀態。

WAITING:無限期等待另一個執行緒執行特定操作的執行緒處於此狀態,需要被顯示的喚醒,否則會一直等待下去。例如對於 Object。wait(),需要等待另一個執行緒執行 Object。notify() 或 Object。notifyAll();對於 Thread。join(),則需要等待指定的執行緒終止。

TIMED_WAITING:在指定的時間內等待另一個執行緒執行某項操作的執行緒處於此狀態。跟 WAITING 類似,區別在於該狀態有超時時間引數,在超時時間到了後會自動喚醒,避免了無期限的等待。

TERMINATED:執行完畢已經退出的執行緒處於此狀態。

執行緒在給定的時間點只能處於一種狀態。這些狀態是虛擬機器狀態,不反映任何作業系統執行緒狀態。

56、synchronized 和 Lock 的區別

1)Lock 是一個介面;synchronized 是 Java 中的關鍵字,synchronized 是內建的語言實現;

2)Lock 在發生異常時,如果沒有主動透過 unLock() 去釋放鎖,很可能會造成死鎖現象,因此使用 Lock 時需要在 finally 塊中釋放鎖;synchronized 不需要手動獲取鎖和釋放鎖,在發生異常時,會自動釋放鎖,因此不會導致死鎖現象發生;

3)Lock 的使用更加靈活,可以有響應中斷、有超時時間等;而 synchronized 卻不行,使用 synchronized 時,等待的執行緒會一直等待下去,直到獲取到鎖;

4)在效能上,隨著近些年 synchronized 的不斷最佳化,Lock 和 synchronized 在效能上已經沒有很明顯的差距了,所以效能不應該成為我們選擇兩者的主要原因。官方推薦儘量使用 synchronized,除非 synchronized 無法滿足需求時,則可以使用 Lock。

57、synchronized 各種加鎖場景的作用範圍

1。作用於非靜態方法,鎖住的是物件例項(this),每一個物件例項有一個鎖。

public

synchronized

void

method

()

{}

2。作用於靜態方法,鎖住的是類的Class物件,因為Class的相關資料儲存在永久代元空間,元空間是全域性共享的,因此靜態方法鎖相當於類的一個全域性鎖,會鎖所有呼叫該方法的執行緒。

public

static

synchronized

void

method

()

{}

3。作用於 Lock。class,鎖住的是 Lock 的Class物件,也是全域性只有一個。

synchronized

Lock

class

{}

4。作用於 this,鎖住的是物件例項,每一個物件例項有一個鎖。

synchronized

this

{}

5。作用於

靜態成員變數

,鎖住的是該靜態成員變數物件,由於是靜態變數,因此全域性只有一個。

public

static

Object

monitor

=

new

Object

();

synchronized

monitor

{}

58、如何檢測死鎖?

死鎖的四個必要條件:

1)互斥條件:程序對所分配到的資源進行排他性控制,即在一段時間內某資源僅為一個程序所佔有。此時若有其他程序請求該資源,則請求程序只能等待。

2)請求和保持條件:程序已經獲得了至少一個資源,但又對其他資源發出請求,而該資源已被其他程序佔有,此時該程序的請求被阻塞,但又對自己獲得的資源保持不放。

3)不可剝奪條件:程序已獲得的資源在未使用完畢之前,不可被其他程序強行剝奪,只能由自己釋放。

4)環路等待條件:存在一種程序資源的迴圈等待鏈,鏈中每一個程序已獲得的資源同時被 鏈中下一個程序所請求。即存在一個處於等待狀態的程序集合{Pl, P2, …, pn},其中 Pi 等待的資源被 P(i+1) 佔有(i=0, 1, …, n-1),Pn 等待的資源被 P0佔 有,如下圖所示。

Java 基礎高頻面試題(2021年最新版)

59、怎麼預防死鎖?

預防死鎖的方式就是打破四個必要條件中的任意一個即可。

1)打破互斥條件:在系統裡取消互斥。若資源不被一個程序獨佔使用,那麼死鎖是肯定不會發生的。但一般來說在所列的四個條件中,“互斥”條件是無法破壞的。因此,在死鎖預防裡主要是破壞其他幾個必要條件,而不去涉及破壞“互斥”條件。。

2)打破請求和保持條件:1)採用資源預先分配策略,即程序執行前申請全部資源,滿足則執行,不然就等待。 2)每個程序提出新的資源申請前,必須先釋放它先前所佔有的資源。

3)打破不可剝奪條件:當程序佔有某些資源後又進一步申請其他資源而無法滿足,則該程序必須釋放它原來佔有的資源。

4)打破環路等待條件:實現資源有序分配策略,將系統的所有資源統一編號,所有程序只能採用按序號遞增的形式申請資源。

60、為什麼要使用執行緒池?直接new個執行緒不是很舒服?

如果我們在方法中直接new一個執行緒來處理,當這個方法被呼叫頻繁時就會建立很多執行緒,不僅會消耗系統資源,還會降低系統的穩定性,一不小心把系統搞崩了,就可以直接去財務那結帳了。

如果我們合理的使用執行緒池,則可以避免把系統搞崩的窘境。總得來說,使用執行緒池可以帶來以下幾個好處:

降低資源消耗。透過重複利用已建立的執行緒,降低執行緒建立和銷燬造成的消耗。

提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。

增加執行緒的可管理型。執行緒是稀缺資源,使用執行緒池可以進行統一分配,調優和監控。

61、執行緒池的核心屬性有哪些?

threadFactory(執行緒工廠):用於建立工作執行緒的工廠。

corePoolSize(核心執行緒數):當執行緒池執行的執行緒少於 corePoolSize 時,將建立一個新執行緒來處理請求,即使其他工作執行緒處於空閒狀態。

workQueue(佇列):用於保留任務並移交給工作執行緒的阻塞佇列。

maximumPoolSize(最大執行緒數):執行緒池允許開啟的最大執行緒數。

handler(拒絕策略):往執行緒池新增任務時,將在下面兩種情況觸發拒絕策略:1)執行緒池執行狀態不是 RUNNING;2)執行緒池已經達到最大執行緒數,並且阻塞佇列已滿時。

keepAliveTime(保持存活時間):如果執行緒池當前執行緒數超過 corePoolSize,則多餘的執行緒空閒時間超過 keepAliveTime 時會被終止。

62、說下執行緒池的運作流程。

Java 基礎高頻面試題(2021年最新版)

63、執行緒池有哪些拒絕策略?

AbortPolicy:中止策略。預設的拒絕策略,直接丟擲 RejectedExecutionException。呼叫者可以捕獲這個異常,然後根據需求編寫自己的處理程式碼。

DiscardPolicy:拋棄策略。什麼都不做,直接拋棄被拒絕的任務。

DiscardOldestPolicy:拋棄最老策略。拋棄阻塞佇列中最老的任務,相當於就是佇列中下一個將要被執行的任務,然後重新提交被拒絕的任務。如果阻塞佇列是一個

優先佇列

,那麼“拋棄最舊的”策略將導致拋棄優先順序最高的任務,因此最好不要將該策略和優先順序佇列放在一起使用。

CallerRunsPolicy:呼叫者執行策略。在呼叫者執行緒中執行該任務。該策略實現了一種調節機制,該策略既不會拋棄任務,也不會丟擲異常,而是將任務回退到呼叫者(呼叫執行緒池執行任務的主執行緒),由於執行任務需要一定時間,因此主執行緒至少在一段時間內不能提交任務,從而使得執行緒池有時間來處理完正在執行的任務。

70、List、Set、Map三者的區別?

List(對付順序的好幫手): List 介面儲存一組不唯一(可以有多個元素引用相同的物件)、有序的物件。

Set(注重獨一無二的性質):不允許重複的集合,不會有多個元素引用相同的物件。

Map(用Key來搜尋的專業戶): 使用鍵值對儲存。Map 會維護與 Key 有關聯的值。兩個 Key可以引用相同的物件,但 Key 不能重複,典型的 Key 是String型別,但也可以是任何物件。

71、ArrayList 和 LinkedList 的區別。

ArrayList 底層基於動態陣列實現,LinkedList 底層基於連結串列實現。

對於按 index 索引資料(get/set方法):ArrayList 透過 index 直接定位到陣列對應位置的節點,而 LinkedList需要從頭結點或尾節點開始遍歷,直到尋找到目標節點,因此在效率上 ArrayList 優於 LinkedList。

對於隨機插入和刪除:ArrayList 需要移動目標節點後面的節點(使用System。arraycopy 方法移動節點),而 LinkedList 只需修改目標節點前後節點的 next 或 prev 屬性即可,因此在效率上 LinkedList 優於 ArrayList。

對於順序插入和刪除:由於 ArrayList 不需要移動節點,因此在效率上比 LinkedList 更好。這也是為什麼在實際使用中 ArrayList 更多,因為大部分情況下我們的使用都是順序插入。

72、ArrayList 和 Vector 的區別。

Vector 和 ArrayList 幾乎一致,唯一的區別是 Vector 在方法上使用了 synchronized 來保證執行緒安全,因此在效能上 ArrayList 具有更好的表現。

有類似關係的還有:StringBuilder 和 StringBuffer、HashMap 和 Hashtable。

73、介紹下 HashMap 的底層資料結構

我們現在用的都是 JDK 1。8,底層是由“陣列+連結串列+紅黑樹”組成,如下圖,而在 JDK 1。8 之前是由“陣列+連結串列”組成。

Java 基礎高頻面試題(2021年最新版)

74、為什麼要改成“陣列+連結串列+紅黑樹”?

主要是為了提升在 hash 衝突嚴重時(連結串列過長)的查詢效能,使用連結串列的查詢效能是 O(n),而使用紅黑樹是 O(logn)。

75、那在什麼時候用連結串列?什麼時候用紅黑樹?

對於插入,預設情況下是使用連結串列節點。當同一個索引位置的節點在新增後超過8個(閾值8):如果此時陣列長度大於等於 64,則會觸發連結串列節點轉紅黑樹節點(treeifyBin);而如果陣列長度小於64,則不會觸發連結串列轉紅黑樹,而是會進行擴容,因為此時的資料量還比較小。

對於移除,當同一個索引位置的節點在移除後達到 6 個,並且該索引位置的節點為紅黑樹節點,會觸發紅黑樹節點轉連結串列節點(untreeify)。

76、HashMap 的預設初始容量是多少?HashMap 的容量有什麼限制嗎?

預設初始容量是16。HashMap 的容量必須是2的N次方,HashMap 會根據我們傳入的容量計算一個大於等於該容量的最小的2的N次方,例如傳 9,容量為16。

77、HashMap 的插入流程是怎麼樣的?

Java 基礎高頻面試題(2021年最新版)

78、HashMap 的擴容(resize)流程是怎麼樣的?

Java 基礎高頻面試題(2021年最新版)

79、除了 HashMap,還用過哪些 Map,在使用時怎麼選擇?

Java 基礎高頻面試題(2021年最新版)

80、HashMap 和Hashtable 的區別?

HashMap 允許 key 和 value 為 null,Hashtable 不允許。

HashMap 的預設初始容量為 16,Hashtable 為 11。

HashMap 的擴容為原來的 2 倍,Hashtable 的擴容為原來的 2 倍加 1。

HashMap 是非執行緒安全的,Hashtable是執行緒安全的。

HashMap 的 hash 值重新計算過,Hashtable 直接使用 hashCode。

HashMap 去掉了 Hashtable 中的 contains 方法。

HashMap 繼承自 AbstractMap 類,Hashtable 繼承自 Dictionary 類。

90、Java 記憶體結構(執行時資料區)

程式計數器

:執行緒私有。一塊較小的記憶體空間,可以看作當前執行緒所執行的位元組碼的行號指示器。如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器值則為空。

Java虛擬機器棧:執行緒私有。它的生命週期與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

本地方法棧:執行緒私有。本地方法棧與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。

Java堆:執行緒共享。對大多數應用來說,Java堆是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。

方法區:與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊(構造方法、介面定義)、常量、靜態變數、即時編譯器編譯後的程式碼(位元組碼)等資料。方法區是JVM規範中定義的一個概念,具體放在哪裡,不同的實現可以放在不同的地方。

執行時常量池:執行時常量池是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。

String

str

=

new

String

“hello”

);

上面的語句中變數 str 放在棧上,用 new 創建出來的字串物件放在堆上,而“hello”這個字面量是放在堆中。

91、什麼是雙親委派模型?

如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

92、Java虛擬機器中有哪些類載入器?

啟動類載入器(Bootstrap ClassLoader):

這個類載入器負責將存放在\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔名識別,如rt。jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。

擴充套件類載入器(Extension ClassLoader):

這個載入器由sun。misc。Launcher$ExtClassLoader實現,它負責載入\lib\ext目錄中的,或者被java。ext。

dirs系統變數

所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。

應用程式類載入器(Application ClassLoader):

這個類載入器由sun。misc。Launcher$AppClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。

自定義類載入器:

使用者自定義的類載入器。

93、類載入的過程

類載入的過程包括:載入、驗證、準備、解析、初始化,其中驗證、準備、解析統稱為連線。

載入:透過一個類的全限定名來獲取定義此類的二進位制位元組流,在記憶體中生成一個代表這個類的java。lang。Class物件。

驗證:確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

準備:為靜態變數分配記憶體並設定靜態變數初始值,這裡所說的初始值“通常情況”下是資料型別的零值。

解析:將常量池內的符號引用替換為直接引用。

初始化:到了初始化階段,才真正開始執行類中定義的 Java 初始化程式程式碼。主要是靜態變數賦值動作和靜態語句塊(static{})中的語句。

94、介紹下垃圾收集機制(在什麼時候,對什麼,做了什麼)?

在什麼時候?

在觸發GC的時候,具體如下,這裡只說常見的 Young GC 和 Full GC。

觸發Young GC:當新生代中的 Eden 區沒有足夠空間進行分配時會觸發Young GC。

觸發Full GC:

當準備要觸發一次Young GC時,如果發現統計資料說之前Young GC的平均晉升大小比目前老年代剩餘的空間大,則不會觸發Young GC而是轉為觸發Full GC。(通常情況)

如果有永久代的話,在永久代需要分配空間但已經沒有足夠空間時,也要觸發一次Full GC。

System。gc()預設也是觸發Full GC。

heap dump帶GC預設也是觸發Full GC。

CMS GC時出現Concurrent Mode Failure會導致一次Full GC的產生。

對什麼?

對那些JVM認為已經“死掉”的物件。即從GC Root開始搜尋,搜尋不到的,並且經過一次篩選標記沒有復活的物件。

做了什麼?

對這些JVM認為已經“死掉”的物件進行垃圾收集,新生代使用複製演算法,老年代使用標記-清除和標記-整理演算法。

95、GC Root有哪些?

在Java語言中,可作為GC Roots的物件包括下面幾種:

虛擬機器棧(棧幀中的本地變量表)中引用的物件。

方法區中類靜態屬性引用的物件。

方法區中常量引用的物件。

本地方法棧中JNI(即一般說的Native方法)引用的物件。

96、垃圾收集有哪些演算法,各自的特點?

標記 - 清除演算法

首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

複製演算法

為了解決效率問題,一種稱為“複製”(Copying)的收集演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。

標記 - 整理演算法

複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。

根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

分代收集演算法

當前商業虛擬機器的垃圾收集都採用“分代收集”(Generational Collection)演算法,這種演算法並沒有什麼新的思想,只是根據物件存活週期的不同將記憶體劃分為幾塊。

一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。

在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。

在老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清理或者標記—整理演算法來進行回收。

最後

金三銀四的季節,相信有不少同學正準備跳槽。

我將我最近的原創的文章進行了彙總:囧輝文章彙總(自學教程、面試真題解析、技術專題深度解析),其中有不少面試高頻題目解析,很多都是我自己在面試大廠時遇到的,我在對每個題目解析時都會按較高的標準進行深入剖析,可能只看一遍並不能完全明白,但是相信反覆閱讀,定能有所收穫。

原創不易,如果你覺得本文寫的還不錯,對你有幫助,請透過【點贊】讓我知道,支援我寫出更好的文章。