前言
網(wǎng)上的 Java 基礎面試題文章有非常多,但是大部分都比較老了。很多題目早已不是當前的熱門題目,沒有必要在這些題目上花太多時間。很多答案放現(xiàn)在已經(jīng)不準確,可能會誤導新人。因此,我花了幾天時間整理了一些時下高頻的 Java 基礎題目,并反復斟酌,給出符合當前版本的解析。面試系列
我自己前前后后加起來總共應該參加了不下四五十次的面試,拿到過幾乎所有一線大廠的 offer:阿里、字節(jié)、美團、快手、拼多多等等。每次面試后我都會將面試的題目進行記錄,并整理成自己的題庫,最近我將這些題目整理出來,并按大廠的標準給出自己的解析,希望在這金三銀四的季節(jié)里,能助你一臂之力。正文
1、面向?qū)ο蟮娜齻€基本特征?
面向?qū)ο蟮娜齻€基本特征是:封裝、繼承和多態(tài)。繼承:讓某個類型的對象獲得另一個類型的對象的屬性的方法。繼承就是子類繼承父類的特征和行為,使得子類對象(實例)具有父類的實例域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為。封裝:隱藏部分對象的屬性和實現(xiàn)細節(jié),對數(shù)據(jù)的訪問只能通過外公開的接口。通過這種方式,對象對內(nèi)部數(shù)據(jù)提供了不同級別的保護,以防止程序中無關的部分意外的改變或錯誤的使用了對象的私有部分。多態(tài):對于同一個行為,不同的子類對象具有不同的表現(xiàn)形式。多態(tài)存在的3個條件:1)繼承;2)重寫;3)父類引用指向子類對象。舉個簡單的例子:英雄聯(lián)盟里面我們按下 Q 鍵這個動作:同一個事件發(fā)生在不同的對象上會產(chǎn)生不同的結果。我再舉一個簡單的例子幫助大家理解,這個例子可能不是完全準確,但是我認為是有利于理解的。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 進行了重寫,有了不同的表現(xiàn)形式(實現(xiàn)),這個我們稱為多態(tài)。在 Cat 里,將 age 屬性定義為 private,外界無法直接訪問,要獲取 Cat 的 age 信息只能通過 getAge 方法,從而對外隱藏了 age 屬性,這個就叫做封裝。當然,這邊 age 只是個例子,實際使用中可能是一個復雜很多的對象。2、訪問修飾符public,private,protected,以及不寫時的區(qū)別?
3、下面兩個代碼塊能正常編譯和執(zhí)行嗎?
short s1 = 1; s1 = s1 + 1;short s1 = 1; s1 += 1;
代碼塊1編譯報錯,錯誤原因是:不兼容的類型: 從int轉(zhuǎn)換到short可能會有損失”。public class com.joonwhee.open.demo.Convert { public com.joonwhee.open.demo.Convert(); Code: 0: aload_0 1: invokespecial #1 4: return
public static void main(java.lang.String[]); Code: 0: iconst_1 1: istore_1 2: iload_1 3: iconst_1 4: iadd 5: i2s 6: istore_1 7: return}
可以看到字節(jié)碼中包含了 i2s 指令,該指令用于將 int 轉(zhuǎn)成 short。i2s 是 int to short 的縮寫。
其實,s1 += 1 相當于 s1 = (short)(s1 + 1),有興趣的可以自己編譯下這兩行代碼的字節(jié)碼,你會發(fā)現(xiàn)是一摸一樣的。說好的 Java 基礎題,怎么又開始變態(tài)起來了???
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);}
執(zhí)行 Integer a = 128,相當于執(zhí)行:Integer a = Integer.valueOf(128),基本類型自動轉(zhuǎn)換為包裝類的過程稱為自動裝箱(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啟動參數(shù):-XX:AutoBoxCacheMax=<size> 來修改上限值,如下圖所示:5、用最有效率的方法計算2乘以8?
進階:通常情況下,可以認為位運算是性能最高的。但是,其實編譯器現(xiàn)在已經(jīng)“非常聰明了”,很多指令編譯器都能自己做優(yōu)化。所以在實際實用中,我們無需特意去追求實用位運算,這樣不僅會導致代碼可讀性很差,而且某些自作聰明的優(yōu)化反而會誤導編譯器,使得編譯器無法進行更好的優(yōu)化。6、&和&&的區(qū)別?
&&:邏輯與運算符。當運算符左右兩邊的表達式都為 true,才返回 true。同時具有短路性,如果第一個表達式為 false,則直接返回 false。按位與運算符:用于二進制的計算,只有對應的兩個二進位均為1時,結果位才為1 ,否則為0。邏輯與運算符:& 在用于邏輯與時,和 && 的區(qū)別是不具有短路性。所在通常使用邏輯與運算符都會使用 &&,而 & 更多的適用于位運算。7、String 是 Java 基本數(shù)據(jù)類型嗎?
答:不是。Java 中的基本數(shù)據(jù)類型只有8個:byte、short、int、long、float、double、char、boolean;除了基本類型(primitive type),剩下的都是引用類型(reference type)。基本數(shù)據(jù)類型:數(shù)據(jù)直接存儲在棧上引用數(shù)據(jù)類型區(qū)別:數(shù)據(jù)存儲在堆上,棧上只存儲引用地址8、String 類可以繼承嗎?
不行。String 類使用 final 修飾,無法被繼承。9、String和StringBuilder、StringBuffer的區(qū)別?
String:String 的值被創(chuàng)建后不能修改,任何對 String 的修改都會引發(fā)新的 String 對象的生成。StringBuffer:跟 String 類似,但是值可以被修改,使用 synchronized 來保證線程安全。StringBuilder:StringBuffer 的非線程安全版本,沒有使用 synchronized,具有更高的性能,推薦優(yōu)先使用。10、String s = new String("xyz") 創(chuàng)建了幾個字符串對象?
一個或兩個。如果字符串常量池已經(jīng)有“xyz”,則是一個;否則,兩個。當字符創(chuàng)常量池沒有 “xyz”,此時會創(chuàng)建如下兩個對象:一個是字符串字面量 "xyz" 所對應的、駐留(intern)在一個全局共享的字符串常量池中的實例,此時該實例也是在堆中,字符串常量池只放引用。另一個是通過 new String() 創(chuàng)建并初始化的,內(nèi)容與"xyz"相同的實例,也是在堆中。11、String s = "xyz" 和 String s = new String("xyz") 區(qū)別?
兩個語句都會先去字符串常量池中檢查是否已經(jīng)存在 “xyz”,如果有則直接使用,如果沒有則會在常量池中創(chuàng)建 “xyz” 對象。另外,String s = new String("xyz") 還會通過 new String() 在堆里創(chuàng)建一個內(nèi)容與 "xyz" 相同的對象實例。12、== 和 equals 的區(qū)別是什么?
==:運算符,用于比較基礎類型變量和引用類型變量。對于基礎類型變量,比較的變量保存的值是否相同,類型不一定要相同。short s1 = 1; long l1 = 1;System.out.println(s1 == l1);
對于引用類型變量,比較的是兩個對象的地址是否相同。
Integer i1 = new Integer(1);Integer i2 = new Integer(1);System.out.println(i1 == i2);
equals:Object 類中定義的方法,通常用于比較兩個對象的值是否相等。equals 在 Object 方法中其實等同于 ==,但是在實際的使用中,equals 通常被重寫用于比較兩個對象的值是否相同。Integer i1 = new Integer(1);Integer i2 = new Integer(1);System.out.println(i1.equals(i2));
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、什么是反射
反射是指在運行狀態(tài)中,對于任意一個類都能夠知道這個類所有的屬性和方法;并且對于任意一個對象,都能夠調(diào)用它的任意一個方法;這種動態(tài)獲取信息以及動態(tài)調(diào)用對象方法的功能稱為反射機制。15、深拷貝和淺拷貝區(qū)別是什么?
數(shù)據(jù)分為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型?;緮?shù)據(jù)類型:數(shù)據(jù)直接存儲在棧中;引用數(shù)據(jù)類型:存儲在棧中的是對象的引用地址,真實的對象數(shù)據(jù)存放在堆內(nèi)存里。淺拷貝:對于基礎數(shù)據(jù)類型:直接復制數(shù)據(jù)值;對于引用數(shù)據(jù)類型:只是復制了對象的引用地址,新舊對象指向同一個內(nèi)存地址,修改其中一個對象的值,另一個對象的值隨之改變。深拷貝:對于基礎數(shù)據(jù)類型:直接復制數(shù)據(jù)值;對于引用數(shù)據(jù)類型:開辟新的內(nèi)存空間,在新的內(nèi)存空間里復制一個一模一樣的對象,新老對象不共享內(nèi)存,修改其中一個對象的值,不會影響另一個對象。16、并發(fā)和并行有什么區(qū)別?
并發(fā):兩個或多個事件在同一時間間隔發(fā)生。并行是真正意義上,同一時刻做多件事情,而并發(fā)在同一時刻只會做一件事件,只是可以將時間切碎,交替做多件事情。你吃飯吃到一半,電話來了,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行。你吃飯吃到一半,電話來了,你停了下來接了電話,接完后繼續(xù)吃飯,這說明你支持并發(fā)。你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支持并行。17、構造器是否可被 重寫?
Constructor 不能被 override(重寫),但是可以 overload(重載),所以你可以看到?個類中有多個構造函數(shù)的情況。18、當一個對象被當作參數(shù)傳遞到一個方法后,此方法可改變這個對象的屬性,并可返回變化后的結果,那么這里到底是值傳遞還是引用傳遞?
值傳遞。Java 中只有值傳遞,對于對象參數(shù),值的內(nèi)容是對象的引用。19、Java 靜態(tài)變量和成員變量的區(qū)別。
public class Demo { public static String STATIC_VARIABLE = "靜態(tài)變量"; public String INSTANCE_VARIABLE = "實例變量";}
成員變量存在于堆內(nèi)存中。靜態(tài)變量存在于方法區(qū)中。
成員變量與對象共存亡,隨著對象創(chuàng)建而存在,隨著對象被回收而釋放。靜態(tài)變量與類共存亡,隨著類的加載而存在,隨著類的消失而消失。成員變量所屬于對象,所以也稱為實例變量。靜態(tài)變量所屬于類,所以也稱為類變量。成員變量只能被對象所調(diào)用 。靜態(tài)變量可以被對象調(diào)用,也可以被類名調(diào)用。20、是否可以從一個靜態(tài)(static)方法內(nèi)部發(fā)出對非靜態(tài)(non-static)方法的調(diào)用?
區(qū)分兩種情況,發(fā)出調(diào)用時是否顯示創(chuàng)建了對象實例。1)沒有顯示創(chuàng)建對象實例:不可以發(fā)起調(diào)用,非靜態(tài)方法只能被對象所調(diào)用,靜態(tài)方法可以通過對象調(diào)用,也可以通過類名調(diào)用,所以靜態(tài)方法被調(diào)用時,可能還沒有創(chuàng)建任何實例對象。因此通過靜態(tài)方法內(nèi)部發(fā)出對非靜態(tài)方法的調(diào)用,此時可能無法知道非靜態(tài)方法屬于哪個對象。public class Demo { public static void staticMethod() { instanceMethod(); } public void instanceMethod() { System.out.println("非靜態(tài)方法"); }}
2)顯示創(chuàng)建對象實例:可以發(fā)起調(diào)用,在靜態(tài)方法中顯示的創(chuàng)建對象實例,則可以正常的調(diào)用。public class Demo { public static void staticMethod() { Demo demo = new Demo(); demo.instanceMethod(); } public void instanceMethod() { System.out.println("非靜態(tài)方法"); }}
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"); }}
1)靜態(tài)變量只會初始化(執(zhí)行)一次。2)當有父類時,完整的初始化順序為:父類靜態(tài)變量(靜態(tài)代碼塊)->子類靜態(tài)變量(靜態(tài)代碼塊)->父類非靜態(tài)變量(非靜態(tài)代碼塊)->父類構造器 ->子類非靜態(tài)變量(非靜態(tài)代碼塊)->子類構造器 。關于初始化,這題算入門題,我之前還寫過一道有(fei)點(chang)意(bian)思(tai)的進階題目,有興趣的可以看看:一道有意思的“初始化”面試題22、重載(Overload)和重寫(Override)的區(qū)別?
方法的重載和重寫都是實現(xiàn)多態(tài)的方式,區(qū)別在于前者實現(xiàn)的是編譯時的多態(tài)性,而后者實現(xiàn)的是運行時的多態(tài)性。重載:一個類中有多個同名的方法,但是具有有不同的參數(shù)列表(參數(shù)類型不同、參數(shù)個數(shù)不同或者二者都不同)。重寫:發(fā)生在子類與父類之間,子類對父類的方法進行重寫,參數(shù)都不能改變,返回值類型可以不相同,但是必須是父類返回值的派生類。即外殼不變,核心重寫!重寫的好處在于子類可以根據(jù)需要,定義特定于自己的行為。23、為什么不能根據(jù)返回類型來區(qū)分重載?
如果我們有兩個方法如下,當我們調(diào)用:test(1) 時,編譯器無法確認要調(diào)用的是哪個。int test(int a);long test(int a);
方法的返回值只是作為方法運行之后的一個“狀態(tài)”,但是并不是所有調(diào)用都關注返回值,所以不能將返回值作為重載的唯一區(qū)分條件。
24、抽象類(abstract class)和接口(interface)有什么區(qū)別?
抽象類中可以有成員變量,接口中沒有成員變量,只能有常量(默認就是 public static final)抽象類中可以包含非抽象的方法,在 Java 7 之前接口中的所有方法都是抽象的,在 Java 8 之后,接口支持非抽象方法:default 方法、靜態(tài)方法等。Java 9 支持私有方法、私有靜態(tài)方法。抽象類中的抽象方法類型可以是任意修飾符,Java 8 之前接口中的方法只能是 public 類型,Java 9 支持 private 類型。接口是自上而下的抽象過程,接口規(guī)范了某些行為,是對某一行為的抽象。我需要這個行為,我就去實現(xiàn)某個接口,但是具體這個行為怎么實現(xiàn),完全由自己決定。抽象類是自下而上的抽象過程,抽象類提供了通用實現(xiàn),是對某一類事物的抽象。我們在寫實現(xiàn)類的時候,發(fā)現(xiàn)某些實現(xiàn)類具有幾乎相同的實現(xiàn),因此我們將這些相同的實現(xiàn)抽取出來成為抽象類,然后如果有一些差異點,則可以提供抽象方法來支持自定義實現(xiàn)。抽象類像叔伯,有一部分會給你,還能指導你做事的方法。接口像干爹,可以給你指引方法,但是做成啥樣得你自己努力實現(xiàn)。25、Error 和 Exception 有什么區(qū)別?
Error 和 Exception 都是 Throwable 的子類,用于表示程序出現(xiàn)了不正常的情況。區(qū)別在于:Error 表示系統(tǒng)級的錯誤和程序不必處理的異常,是恢復不是不可能但很困難的情況下的一種嚴重問題,比如內(nèi)存溢出,不可能指望程序能處理這樣的情況。Exception 表示需要捕捉或者需要程序進行處理的異常,是一種設計或?qū)崿F(xiàn)問題,也就是說,它表示如果程序運行正常,從不會發(fā)生的情況。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) { System.out.println(USER); USER.setName("test"); System.out.println(USER); }}
27、闡述 final、finally、finalize 的區(qū)別。
finally:finally 是對 Java 異常處理機制的最佳補充,通常配合 try、catch 使用,用于存放那些無論是否出現(xiàn)異常都一定會執(zhí)行的代碼。在實際使用中,通常用于釋放鎖、數(shù)據(jù)庫連接等資源,把資源釋放方法放到 finally 中,可以大大降低程序出錯的幾率。finalize:Object 中的方法,在垃圾收集器將對象從內(nèi)存中清除出去之前做必要的清理工作。finalize()方法僅作為了解即可,在 Java 9 中該方法已經(jīng)被標記為廢棄,并添加新的 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"); } }}
相信很多同學應該都做對了,try、catch。finally 的基礎用法,在 return 前會先執(zhí)行 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; } }}
這題有點先將,但也不難,try 返回前先執(zhí)行 finally,結果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。finally 里面使用 return 僅存在于面試題中,實際開發(fā)中千萬不要這么用。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; } }}
這邊估計有不少同學會以為結果應該是 3,因為我們知道在 return 前會執(zhí)行 finally,而 i 在 finally 中被修改為 3 了,那最終返回 i 不是應該為 3 嗎?確實很容易這么想,我最初也是這么想的,當初的自己還是太年輕了啊。這邊的根本原因是,在執(zhí)行 finally 之前,JVM 會先將 i 的結果暫存起來,然后 finally 執(zhí)行完畢后,會返回之前暫存的結果,而不是返回 i,所以即使這邊 i 已經(jīng)被修改為 3,最終返回的還是之前暫存起來的結果 2。這邊其實根據(jù)字節(jié)碼可以很容易看出來,在進入 finally 之前,JVM 會使用 iload、istore 兩個指令,將結果暫存,在最終返回時在通過 iload、ireturn 指令返回暫存的結果。為了避免氣氛再次變態(tài)起來,我這邊就不貼具體的字節(jié)碼程序了,有興趣的同學可以自己編譯查看下。31、JDK1.8之后有哪些新特性?
接口默認方法:Java 8允許我們給接口添加一個非抽象的方法實現(xiàn),只需要使用 default關鍵字即可Lambda 表達式和函數(shù)式接口:Lambda 表達式本質(zhì)上是一段匿名內(nèi)部類,也可以是一段可以傳遞的代碼。Lambda 允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞到方法中),使用 Lambda 表達式使代碼更加簡潔,但是也不要濫用,否則會有可讀性等問題,《Effective Java》作者 Josh Bloch 建議使用 Lambda 表達式最好不要超過3行。Stream API:用函數(shù)式編程方式在集合類上進行復雜操作的工具,配合Lambda表達式可以方便的對集合進行處理。Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執(zhí)行非常復雜的查找、過濾和映射數(shù)據(jù)等操作。使用Stream API 對集合數(shù)據(jù)進行操作,就類似于使用 SQL 執(zhí)行的數(shù)據(jù)庫查詢。也可以使用 Stream API 來并行執(zhí)行操作。簡而言之,Stream API 提供了一種高效且易于使用的處理數(shù)據(jù)的方式。方法引用:方法引用提供了非常有用的語法,可以直接引用已有Java類或?qū)ο螅▽嵗┑姆椒ɑ驑嬙炱?。與lambda聯(lián)合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。日期時間API:Java 8 引入了新的日期時間API改進了日期時間的管理。Optional 類:著名的 NullPointerException 是引起系統(tǒng)失敗最常見的原因。很久以前 Google Guava 項目引入了 Optional 作為解決空指針異常的一種方式,不贊成代碼被 null 檢查的代碼污染,期望程序員寫整潔的代碼。受Google Guava的鼓勵,Optional 現(xiàn)在是Java 8庫的一部分。新工具:新的編譯工具,如:Nashorn引擎 jjs、 類依賴分析器 jdeps。50、wait() 和 sleep() 方法的區(qū)別
來源不同:sleep() 來自 Thread 類,wait() 來自 Object 類。對于同步鎖的影響不同:sleep() 不會該表同步鎖的行為,如果當前線程持有同步鎖,那么 sleep 是不會讓線程釋放同步鎖的。wait() 會釋放同步鎖,讓其他線程進入 synchronized 代碼塊執(zhí)行。使用范圍不同:sleep() 可以在任何地方使用。wait() 只能在同步控制方法或者同步控制塊里面使用,否則會拋 IllegalMonitorStateException。恢復方式不同:兩者會暫停當前線程,但是在恢復上不太一樣。sleep() 在時間到了之后會重新恢復;wait() 則需要其他線程調(diào)用同一對象的 notify()/nofityAll() 才能重新恢復。51、線程的 sleep() 方法和 yield() 方法有什么區(qū)別?
線程執(zhí)行 sleep() 方法后進入超時等待(TIMED_WAITING)狀態(tài),而執(zhí)行 yield() 方法后進入就緒(READY)狀態(tài)。sleep() 方法給其他線程運行機會時不考慮線程的優(yōu)先級,因此會給低優(yōu)先級的線程運行的機會;yield() 方法只會給相同優(yōu)先級或更高優(yōu)先級的線程以運行的機會。52、線程的 join() 方法是干啥用的?
用于等待當前線程終止。如果一個線程A執(zhí)行了 threadB.join() 語句,其含義是:當前線程A等待 threadB 線程終止之后才從 threadB.join() 返回繼續(xù)往下執(zhí)行自己的代碼。53、編寫多線程程序有幾種實現(xiàn)方式?
通常來說,可以認為有三種方式:1)繼承 Thread 類;2)實現(xiàn) Runnable 接口;3)實現(xiàn) Callable 接口。其中,Thread 其實也是實現(xiàn)了 Runable 接口。Runnable 和 Callable 的主要區(qū)別在于是否有返回值。54、Thread 調(diào)用 start() 方法和調(diào)用 run() 方法的區(qū)別
run():普通的方法調(diào)用,在主線程中執(zhí)行,不會新建一個線程來執(zhí)行。start():新啟動一個線程,這時此線程處于就緒(可運行)狀態(tài),并沒有運行,一旦得到 CPU 時間片,就開始執(zhí)行 run() 方法。55、線程的狀態(tài)流轉(zhuǎn)
NEW:新建但是尚未啟動的線程處于此狀態(tài),沒有調(diào)用 start() 方法。RUNNABLE:包含就緒(READY)和運行中(RUNNING)兩種狀態(tài)。線程調(diào)用 start() 方法會會進入就緒(READY)狀態(tài),等待獲取 CPU 時間片。如果成功獲取到 CPU 時間片,則會進入運行中(RUNNING)狀態(tài)。BLOCKED:線程在進入同步方法/同步塊(synchronized)時被阻塞,等待同步鎖的線程處于此狀態(tài)。WAITING:無限期等待另一個線程執(zhí)行特定操作的線程處于此狀態(tài),需要被顯示的喚醒,否則會一直等待下去。例如對于 Object.wait(),需要等待另一個線程執(zhí)行 Object.notify() 或 Object.notifyAll();對于 Thread.join(),則需要等待指定的線程終止。TIMED_WAITING:在指定的時間內(nèi)等待另一個線程執(zhí)行某項操作的線程處于此狀態(tài)。跟 WAITING 類似,區(qū)別在于該狀態(tài)有超時時間參數(shù),在超時時間到了后會自動喚醒,避免了無期限的等待。TERMINATED:執(zhí)行完畢已經(jīng)退出的線程處于此狀態(tài)。線程在給定的時間點只能處于一種狀態(tài)。這些狀態(tài)是虛擬機狀態(tài),不反映任何操作系統(tǒng)線程狀態(tài)。56、synchronized 和 Lock 的區(qū)別
1)Lock 是一個接口;synchronized 是 Java 中的關鍵字,synchronized 是內(nèi)置的語言實現(xiàn);2)Lock 在發(fā)生異常時,如果沒有主動通過 unLock() 去釋放鎖,很可能會造成死鎖現(xiàn)象,因此使用 Lock 時需要在 finally 塊中釋放鎖;synchronized 不需要手動獲取鎖和釋放鎖,在發(fā)生異常時,會自動釋放鎖,因此不會導致死鎖現(xiàn)象發(fā)生;3)Lock 的使用更加靈活,可以有響應中斷、有超時時間等;而 synchronized 卻不行,使用 synchronized 時,等待的線程會一直等待下去,直到獲取到鎖;4)在性能上,隨著近些年 synchronized 的不斷優(yōu)化,Lock 和 synchronized 在性能上已經(jīng)沒有很明顯的差距了,所以性能不應該成為我們選擇兩者的主要原因。官方推薦盡量使用 synchronized,除非 synchronized 無法滿足需求時,則可以使用 Lock。57、synchronized 各種加鎖場景的作用范圍
1.作用于非靜態(tài)方法,鎖住的是對象實例(this),每一個對象實例有一個鎖。public synchronized void method() {}
2.作用于靜態(tài)方法,鎖住的是類的Class對象,因為Class的相關數(shù)據(jù)存儲在永久代元空間,元空間是全局共享的,因此靜態(tài)方法鎖相當于類的一個全局鎖,會鎖所有調(diào)用該方法的線程。public static synchronized void method() {}
3.作用于 Lock.class,鎖住的是 Lock 的Class對象,也是全局只有一個。synchronized (Lock.class) {}
4.作用于 this,鎖住的是對象實例,每一個對象實例有一個鎖。5.作用于靜態(tài)成員變量,鎖住的是該靜態(tài)成員變量對象,由于是靜態(tài)變量,因此全局只有一個。public static Object monitor = new Object(); synchronized (monitor) {}
58、如何檢測死鎖?
1)互斥條件:進程對所分配到的資源進行排他性控制,即在一段時間內(nèi)某資源僅為一個進程所占有。此時若有其他進程請求該資源,則請求進程只能等待。2)請求和保持條件:進程已經(jīng)獲得了至少一個資源,但又對其他資源發(fā)出請求,而該資源已被其他進程占有,此時該進程的請求被阻塞,但又對自己獲得的資源保持不放。3)不可剝奪條件:進程已獲得的資源在未使用完畢之前,不可被其他進程強行剝奪,只能由自己釋放。4)環(huán)路等待條件:存在一種進程資源的循環(huán)等待鏈,鏈中每一個進程已獲得的資源同時被 鏈中下一個進程所請求。即存在一個處于等待狀態(tài)的進程集合{Pl, P2, …, pn},其中 Pi 等待的資源被 P(i+1) 占有(i=0, 1, …, n-1),Pn 等待的資源被 P0占 有,如下圖所示。59、怎么預防死鎖?
預防死鎖的方式就是打破四個必要條件中的任意一個即可。1)打破互斥條件:在系統(tǒng)里取消互斥。若資源不被一個進程獨占使用,那么死鎖是肯定不會發(fā)生的。但一般來說在所列的四個條件中,“互斥”條件是無法破壞的。因此,在死鎖預防里主要是破壞其他幾個必要條件,而不去涉及破壞“互斥”條件。。2)打破請求和保持條件:1)采用資源預先分配策略,即進程運行前申請全部資源,滿足則運行,不然就等待。2)每個進程提出新的資源申請前,必須先釋放它先前所占有的資源。3)打破不可剝奪條件:當進程占有某些資源后又進一步申請其他資源而無法滿足,則該進程必須釋放它原來占有的資源。4)打破環(huán)路等待條件:實現(xiàn)資源有序分配策略,將系統(tǒng)的所有資源統(tǒng)一編號,所有進程只能采用按序號遞增的形式申請資源。60、為什么要使用線程池?直接new個線程不是很舒服?
如果我們在方法中直接new一個線程來處理,當這個方法被調(diào)用頻繁時就會創(chuàng)建很多線程,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,一不小心把系統(tǒng)搞崩了,就可以直接去財務那結帳了。如果我們合理的使用線程池,則可以避免把系統(tǒng)搞崩的窘境。總得來說,使用線程池可以帶來以下幾個好處:- 降低資源消耗。通過重復利用已創(chuàng)建的線程,降低線程創(chuàng)建和銷毀造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
- 增加線程的可管理型。線程是稀缺資源,使用線程池可以進行統(tǒng)一分配,調(diào)優(yōu)和監(jiān)控。
61、線程池的核心屬性有哪些?
threadFactory(線程工廠):用于創(chuàng)建工作線程的工廠。corePoolSize(核心線程數(shù)):當線程池運行的線程少于 corePoolSize 時,將創(chuàng)建一個新線程來處理請求,即使其他工作線程處于空閑狀態(tài)。workQueue(隊列):用于保留任務并移交給工作線程的阻塞隊列。maximumPoolSize(最大線程數(shù)):線程池允許開啟的最大線程數(shù)。handler(拒絕策略):往線程池添加任務時,將在下面兩種情況觸發(fā)拒絕策略:1)線程池運行狀態(tài)不是 RUNNING;2)線程池已經(jīng)達到最大線程數(shù),并且阻塞隊列已滿時。keepAliveTime(保持存活時間):如果線程池當前線程數(shù)超過 corePoolSize,則多余的線程空閑時間超過 keepAliveTime 時會被終止。
62、說下線程池的運作流程。
63、線程池有哪些拒絕策略?
AbortPolicy:中止策略。默認的拒絕策略,直接拋出 RejectedExecutionException。調(diào)用者可以捕獲這個異常,然后根據(jù)需求編寫自己的處理代碼。DiscardPolicy:拋棄策略。什么都不做,直接拋棄被拒絕的任務。DiscardOldestPolicy:拋棄最老策略。拋棄阻塞隊列中最老的任務,相當于就是隊列中下一個將要被執(zhí)行的任務,然后重新提交被拒絕的任務。如果阻塞隊列是一個優(yōu)先隊列,那么“拋棄最舊的”策略將導致拋棄優(yōu)先級最高的任務,因此最好不要將該策略和優(yōu)先級隊列放在一起使用。CallerRunsPolicy:調(diào)用者運行策略。在調(diào)用者線程中執(zhí)行該任務。該策略實現(xiàn)了一種調(diào)節(jié)機制,該策略既不會拋棄任務,也不會拋出異常,而是將任務回退到調(diào)用者(調(diào)用線程池執(zhí)行任務的主線程),由于執(zhí)行任務需要一定時間,因此主線程至少在一段時間內(nèi)不能提交任務,從而使得線程池有時間來處理完正在執(zhí)行的任務。
70、List、Set、Map三者的區(qū)別?
List(對付順序的好幫手):List 接口存儲一組不唯一(可以有多個元素引用相同的對象)、有序的對象。Set(注重獨一無二的性質(zhì)):不允許重復的集合,不會有多個元素引用相同的對象。Map(用Key來搜索的專業(yè)戶): 使用鍵值對存儲。Map 會維護與 Key 有關聯(lián)的值。兩個 Key可以引用相同的對象,但 Key 不能重復,典型的 Key 是String類型,但也可以是任何對象。71、ArrayList 和 LinkedList 的區(qū)別。
ArrayList 底層基于動態(tài)數(shù)組實現(xiàn),LinkedList 底層基于鏈表實現(xiàn)。對于按 index 索引數(shù)據(jù)(get/set方法):ArrayList 通過 index 直接定位到數(shù)組對應位置的節(jié)點,而 LinkedList需要從頭結點或尾節(jié)點開始遍歷,直到尋找到目標節(jié)點,因此在效率上 ArrayList 優(yōu)于 LinkedList。對于隨機插入和刪除:ArrayList 需要移動目標節(jié)點后面的節(jié)點(使用System.arraycopy 方法移動節(jié)點),而 LinkedList 只需修改目標節(jié)點前后節(jié)點的 next 或 prev 屬性即可,因此在效率上 LinkedList 優(yōu)于 ArrayList。對于順序插入和刪除:由于 ArrayList 不需要移動節(jié)點,因此在效率上比 LinkedList 更好。這也是為什么在實際使用中 ArrayList 更多,因為大部分情況下我們的使用都是順序插入。72、ArrayList 和 Vector 的區(qū)別。
Vector 和 ArrayList 幾乎一致,唯一的區(qū)別是 Vector 在方法上使用了 synchronized 來保證線程安全,因此在性能上 ArrayList 具有更好的表現(xiàn)。有類似關系的還有:StringBuilder 和 StringBuffer、HashMap 和 Hashtable。73、介紹下 HashMap 的底層數(shù)據(jù)結構
我們現(xiàn)在用的都是 JDK 1.8,底層是由“數(shù)組+鏈表+紅黑樹”組成,如下圖,而在 JDK 1.8 之前是由“數(shù)組+鏈表”組成。74、為什么要改成“數(shù)組+鏈表+紅黑樹”?
主要是為了提升在 hash 沖突嚴重時(鏈表過長)的查找性能,使用鏈表的查找性能是 O(n),而使用紅黑樹是 O(logn)。75、那在什么時候用鏈表?什么時候用紅黑樹?
對于插入,默認情況下是使用鏈表節(jié)點。當同一個索引位置的節(jié)點在新增后超過8個(閾值8):如果此時數(shù)組長度大于等于 64,則會觸發(fā)鏈表節(jié)點轉(zhuǎn)紅黑樹節(jié)點(treeifyBin);而如果數(shù)組長度小于64,則不會觸發(fā)鏈表轉(zhuǎn)紅黑樹,而是會進行擴容,因為此時的數(shù)據(jù)量還比較小。對于移除,當同一個索引位置的節(jié)點在移除后達到 6 個,并且該索引位置的節(jié)點為紅黑樹節(jié)點,會觸發(fā)紅黑樹節(jié)點轉(zhuǎn)鏈表節(jié)點(untreeify)。
76、HashMap 的默認初始容量是多少?HashMap 的容量有什么限制嗎?
默認初始容量是16。HashMap 的容量必須是2的N次方,HashMap 會根據(jù)我們傳入的容量計算一個大于等于該容量的最小的2的N次方,例如傳 9,容量為16。77、HashMap 的插入流程是怎么樣的?
78、HashMap 的擴容(resize)流程是怎么樣的?
79、除了 HashMap,還用過哪些 Map,在使用時怎么選擇?
80、HashMap 和Hashtable 的區(qū)別?
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 內(nèi)存結構(運行時數(shù)據(jù)區(qū))
程序計數(shù)器:線程私有。一塊較小的內(nèi)存空間,可以看作當前線程所執(zhí)行的字節(jié)碼的行號指示器。如果線程正在執(zhí)行的是一個Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果正在執(zhí)行的是Native方法,這個計數(shù)器值則為空。Java虛擬機棧:線程私有。它的生命周期與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。本地方法棧:線程私有。本地方法棧與虛擬機棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。Java堆:線程共享。對大多數(shù)應用來說,Java堆是Java虛擬機所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。方法區(qū):與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息(構造方法、接口定義)、常量、靜態(tài)變量、即時編譯器編譯后的代碼(字節(jié)碼)等數(shù)據(jù)。方法區(qū)是JVM規(guī)范中定義的一個概念,具體放在哪里,不同的實現(xiàn)可以放在不同的地方。運行時常量池:運行時常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。String str = new String("hello");
上面的語句中變量 str 放在棧上,用 new 創(chuàng)建出來的字符串對象放在堆上,而"hello"這個字面量是放在堆中。
91、什么是雙親委派模型?
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。92、Java虛擬機中有哪些類加載器?
啟動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內(nèi)存中。擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴展類加載器。應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher$AppClassLoader實現(xiàn)。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。93、類加載的過程
類加載的過程包括:加載、驗證、準備、解析、初始化,其中驗證、準備、解析統(tǒng)稱為連接。加載:通過一個類的全限定名來獲取定義此類的二進制字節(jié)流,在內(nèi)存中生成一個代表這個類的java.lang.Class對象。驗證:確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。準備:為靜態(tài)變量分配內(nèi)存并設置靜態(tài)變量初始值,這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值。解析:將常量池內(nèi)的符號引用替換為直接引用。初始化:到了初始化階段,才真正開始執(zhí)行類中定義的 Java 初始化程序代碼。主要是靜態(tài)變量賦值動作和靜態(tài)語句塊(static{})中的語句。94、介紹下垃圾收集機制(在什么時候,對什么,做了什么)?
在觸發(fā)GC的時候,具體如下,這里只說常見的 Young GC 和 Full GC。觸發(fā)Young GC:當新生代中的 Eden 區(qū)沒有足夠空間進行分配時會觸發(fā)Young GC。- 當準備要觸發(fā)一次Young GC時,如果發(fā)現(xiàn)統(tǒng)計數(shù)據(jù)說之前Young GC的平均晉升大小比目前老年代剩余的空間大,則不會觸發(fā)Young GC而是轉(zhuǎn)為觸發(fā)Full GC。(通常情況)
- 如果有永久代的話,在永久代需要分配空間但已經(jīng)沒有足夠空間時,也要觸發(fā)一次Full GC。
- System.gc()默認也是觸發(fā)Full GC。
- heap dump帶GC默認也是觸發(fā)Full GC。
- CMS GC時出現(xiàn)Concurrent Mode Failure會導致一次Full GC的產(chǎn)生。
對那些JVM認為已經(jīng)“死掉”的對象。即從GC Root開始搜索,搜索不到的,并且經(jīng)過一次篩選標記沒有復活的對象。對這些JVM認為已經(jīng)“死掉”的對象進行垃圾收集,新生代使用復制算法,老年代使用標記-清除和標記-整理算法。95、GC Root有哪些?
在Java語言中,可作為GC Roots的對象包括下面幾種:- 本地方法棧中JNI(即一般說的Native方法)引用的對象。
96、垃圾收集有哪些算法,各自的特點?
首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。為了解決效率問題,一種稱為“復制”(Copying)的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。只是這種算法的代價是將內(nèi)存縮小為了原來的一半,未免太高了一點。復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。根據(jù)老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。當前商業(yè)虛擬機的垃圾收集都采用“分代收集”(Generational Collection)算法,這種算法并沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?/span>在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。在老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清理或者標記—整理算法來進行回收。最后
金三銀四的季節(jié),相信有不少同學正準備跳槽。 我將我最近的原創(chuàng)的文章進行了匯總:原創(chuàng)匯總,其中有不少面試高頻題目解析,很多都是我自己在面試大廠時遇到的,我在對每個題目解析時都會按較高的標準進行深入剖析,可能只看一遍并不能完全明白,但是相信反復閱讀,定能有所收獲有興趣的同學可以在我公眾號底部的“原創(chuàng)匯總”自行查看。原創(chuàng)不易,如果你覺得本文寫的還不錯,對你有幫助,請通過【點贊】讓我知道,支持我寫出更好的文章。
推薦閱讀
921天,從小廠到入職阿里
兩年Java開發(fā)工作經(jīng)驗面試總結
4 年 Java 經(jīng)驗面試總結、心得體會
5 年 Java 經(jīng)驗字節(jié)、美團、快手核心部門面試總結(真題解析)
面試必問的 Spring,你懂了嗎?
如何寫一份讓 HR 眼前一亮的簡歷(附模板)
面試阿里,HashMap 這一篇就夠了
面試必問的線程池,你懂了嗎?
BATJTMD 面試必問的 MySQL,你懂了嗎?
如何準備好一場大廠面試
跳槽,如何選擇一家公司