物件導向的三個基本特徵是:封裝、繼承和多型。
繼承:讓某個型別的物件獲得另一個型別的物件的屬性的方法。繼承就是子類繼承父類別的特徵和行為,使得子類物件(範例)具有父類別的範例域和方法,或子類從父類別繼承方法,使得子類具有父類別相同的行為。(推薦教學:)
封裝:隱藏部分物件的屬性和實現細節,對資料的存取只能通過外公開的介面。通過這種方式,物件對內部資料提供了不同級別的保護,以防止程式中無關的部分意外的改變或錯誤的使用了物件的私有部分。
多型:對於同一個行為,不同的子類物件具有不同的表現形式。多型存在的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 只是個例子,實際使用中可能是一個複雜很多的物件。
// 程式碼塊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."<init>":()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 基礎題,怎麼又開始變態起來了???
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=<size> 來修改上限值,如下圖所示:
2 << 3。
進階:通常情況下,可以認為位運算是效能最高的。但是,其實編譯器現在已經「非常聰明瞭」,很多指令編譯器都能自己做優化。所以在實際實用中,我們無需特意去追求實用位運算,這樣不僅會導致程式碼可讀性很差,而且某些自作聰明的優化反而會誤導編譯器,使得編譯器無法進行更好的優化。
這可能就是所謂的「豬隊友」吧。
&&:邏輯與運運算元。當運運算元左右兩邊的表示式都為 true,才返回 true。同時具有短路性,如果第一個表示式為 false,則直接返回 false。
&:邏輯與運運算元、按位元與運運算元。
按位元與運運算元:用於二進位制的計算,只有對應的兩個二進位均為1時,結果位才為1 ,否則為0。
邏輯與運運算元:& 在用於邏輯與時,和 && 的區別是不具有短路性。所在通常使用邏輯與運運算元都會使用 &&,而 & 更多的適用於位運算。
答:不是。Java 中的基本資料型別只有8個:byte、short、int、long、float、double、char、boolean;除了基本型別(primitive type),剩下的都是參照型別(reference type)。
基本資料型別:資料直接儲存在棧上
參照資料型別區別:資料儲存在堆上,棧上只儲存參照地址
不行。String 類使用 final 修飾,無法被繼承。
String:String 的值被建立後不能修改,任何對 String 的修改都會引發新的 String 物件的生成。
StringBuffer:跟 String 類似,但是值可以被修改,使用 synchronized 來保證執行緒安全。
StringBuilder:StringBuffer 的非執行緒安全版本,沒有使用 synchronized,具有更高的效能,推薦優先使用。
一個或兩個。如果字串常數池已經有「xyz」,則是一個;否則,兩個。
當字元創常數池沒有 「xyz」,此時會建立如下兩個物件:
一個是字串字面量 "xyz" 所對應的、駐留(intern)在一個全域性共用的字串常數池中的範例,此時該範例也是在堆中,字串常數池只放參照。
另一個是通過 new String() 建立並初始化的,內容與"xyz"相同的範例,也是在堆中。
兩個語句都會先去字串常數池中檢查是否已經存在 「xyz」,如果有則直接使用,如果沒有則會在常數池中建立 「xyz」 物件。
另外,String s = new String("xyz") 還會通過 new String() 在堆裡建立一個內容與 "xyz" 相同的物件範例。
所以前者其實理解為被後者的所包含。
==:運運算元,用於比較基礎型別變數和參照型別變數。
對於基礎型別變數,比較的變數儲存的值是否相同,型別不一定要相同。
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; }
不對。hashCode() 和 equals() 之間的關係如下:
當有 a.equals(b) == true 時,則 a.hashCode() == b.hashCode() 必然成立,
反過來,當 a.hashCode() == b.hashCode() 時,a.equals(b) 不一定為 true。
反射是指在執行狀態中,對於任意一個類都能夠知道這個類所有的屬性和方法;並且對於任意一個物件,都能夠呼叫它的任意一個方法;這種動態獲取資訊以及動態呼叫物件方法的功能稱為反射機制。
資料分為基本資料型別和參照資料型別。基本資料型別:資料直接儲存在棧中;參照資料型別:儲存在棧中的是物件的參照地址,真實的物件資料存放在堆記憶體裡。
淺拷貝:對於基礎資料型別:直接複製資料值;對於參照資料型別:只是複製了物件的參照地址,新舊物件指向同一個記憶體地址,修改其中一個物件的值,另一個物件的值隨之改變。
深拷貝:對於基礎資料型別:直接複製資料值;對於參照資料型別:開闢新的記憶體空間,在新的記憶體空間裡複製一個一模一樣的物件,新老物件不共用記憶體,修改其中一個物件的值,不會影響另一個物件。
深拷貝相比於淺拷貝速度較慢並且花銷較大。
並行:兩個或多個事件在同一時間間隔發生。
並行:兩個或者多個事件在同一時刻發生。
並行是真正意義上,同一時刻做多件事情,而並行在同一時刻只會做一件事件,只是可以將時間切碎,交替做多件事情。
網上有個例子挺形象的:
你吃飯吃到一半,電話來了,你一直到吃完了以後才去接,這就說明你不支援並行也不支援並行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完後繼續吃飯,這說明你支援並行。
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支援並行。
Constructor 不能被 override(重寫),但是可以 overload(過載),所以你可以看到⼀個類中有多個建構函式的情況。
值傳遞。Java 中只有值傳遞,對於物件引數,值的內容是物件的參照。
public class Demo { /** * 靜態變數:又稱類變數,static修飾 */ public static String STATIC_VARIABLE = "靜態變數"; /** * 範例變數:又稱成員變數,沒有static修飾 */ public String INSTANCE_VARIABLE = "範例變數"; }
成員變數存在於堆記憶體中。靜態變數存在於方法區中。
成員變數與物件共存亡,隨著物件建立而存在,隨著物件被回收而釋放。靜態變數與類共存亡,隨著類的載入而存在,隨著類的消失而消失。
成員變數所屬於物件,所以也稱為範例變數。靜態變數所屬於類,所以也稱為類變數。
成員變數只能被物件所呼叫 。靜態變數可以被物件呼叫,也可以被類名呼叫。
區分兩種情況,發出呼叫時是否顯示建立了物件範例。
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("非靜態方法"); } }
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)的進階題目,有興趣的可以看看:一道有意思的「初始化」面試題
方法的過載和重寫都是實現多型的方式,區別在於前者實現的是編譯時的多型性,而後者實現的是執行時的多型性。
過載:一個類中有多個同名的方法,但是具有有不同的參數列(引數型別不同、引數個數不同或者二者都不同)。
重寫:發生在子類與父類別之間,子類對父類別的方法進行重寫,引數都不能改變,返回值型別可以不相同,但是必須是父類別返回值的派生類。即外殼不變,核心重寫!重寫的好處在於子類可以根據需要,定義特定於自己的行為。
如果我們有兩個方法如下,當我們呼叫:test(1) 時,編譯器無法確認要呼叫的是哪個。
// 方法1 int test(int a); // 方法2 long test(int a);
方法的返回值只是作為方法執行之後的一個「狀態」,但是並不是所有呼叫都關注返回值,所以不能將返回值作為過載的唯一區分條件。
抽象類只能單繼承,介面可以多實現。
抽象類可以有構造方法,介面中不能有構造方法。
抽象類中可以有成員變數,介面中沒有成員變數,只能有常數(預設就是 public static final)
抽象類中可以包含非抽象的方法,在 Java 7 之前介面中的所有方法都是抽象的,在 Java 8 之後,介面支援非抽象方法:default 方法、靜態方法等。Java 9 支援私有方法、私有靜態方法。
抽象類中的方法型別可以是任意修飾符,Java 8 之前介面中的方法只能是 public 型別,Java 9 支援 private 型別。
設計思想的區別:
介面是自上而下的抽象過程,介面規範了某些行為,是對某一行為的抽象。我需要這個行為,我就去實現某個介面,但是具體這個行為怎麼實現,完全由自己決定。
抽象類是自下而上的抽象過程,抽象類提供了通用實現,是對某一類事物的抽象。我們在寫實現類的時候,發現某些實現類具有幾乎相同的實現,因此我們將這些相同的實現抽取出來成為抽象類,然後如果有一些差異點,則可以提供抽象方法來支援自定義實現。
我在網上看到有個說法,挺形象的:
普通類像親爹 ,他有啥都是你的。
抽象類像叔伯,有一部分會給你,還能指導你做事的方法。
介面像乾爹,可以給你指引方法,但是做成啥樣得你自己努力實現。
Error 和 Exception 都是 Throwable 的子類,用於表示程式出現了不正常的情況。區別在於:
Error 表示系統級的錯誤和程式不必處理的異常,是恢復不是不可能但很困難的情況下的一種嚴重問題,比如記憶體溢位,不可能指望程式能處理這樣的情況。
Exception 表示需要捕捉或者需要程式進行處理的異常,是一種設計或實現問題,也就是說,它表示如果程式執行正常,從不會發生的情況。
修飾類:該類不能再派生出新的子類,不能作為父類別被繼承。因此,一個類不能同時被宣告為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); } }
其實是三個完全不相關的東西,只是長的有點像。。
final 如上所示。
finally:finally 是對 Java 例外處理機制的最佳補充,通常配合 try、catch 使用,用於存放那些無論是否出現異常都一定會執行的程式碼。在實際使用中,通常用於釋放鎖、資料庫連線等資源,把資源釋放方法放到 finally 中,可以大大降低程式出錯的機率。
finalize:Object 中的方法,在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。finalize()方法僅作為了解即可,在 Java 9 中該方法已經被標記為廢棄,並新增新的 java.lang.ref.Cleaner,提供了更靈活和有效的方法來釋放資源。這也側面說明了,這個方法的設計是失敗的,因此更加不能去使用它。
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。
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 僅存在於面試題中,實際開發中千萬不要這麼用。
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 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 引入了新的日期時間API改進了日期時間的管理。
Optional 類:著名的 NullPointerException 是引起系統失敗最常見的原因。很久以前 Google Guava 專案引入了 Optional 作為解決空指標異常的一種方式,不贊成程式碼被 null 檢查的程式碼汙染,期望程式設計師寫整潔的程式碼。受Google Guava的鼓勵,Optional 現在是Java 8庫的一部分。
新工具:新的編譯工具,如:Nashorn引擎 jjs、 類依賴分析器 jdeps。
來源不同:sleep() 來自 Thread 類,wait() 來自 Object 類。
對於同步鎖的影響不同:sleep() 不會該表同步鎖的行為,如果當前執行緒持有同步鎖,那麼 sleep 是不會讓執行緒釋放同步鎖的。wait() 會釋放同步鎖,讓其他執行緒進入 synchronized 程式碼塊執行。
使用範圍不同:sleep() 可以在任何地方使用。wait() 只能在同步控制方法或者同步控制塊裡面使用,否則會拋 IllegalMonitorStateException。
恢復方式不同:兩者會暫停當前執行緒,但是在恢復上不太一樣。sleep() 在時間到了之後會重新恢復;wait() 則需要其他執行緒呼叫同一物件的 notify()/nofityAll() 才能重新恢復。
執行緒執行 sleep() 方法後進入超時等待(TIMED_WAITING)狀態,而執行 yield() 方法後進入就緒(READY)狀態。
sleep() 方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒執行的機會;yield() 方法只會給相同優先順序或更高優先順序的執行緒以執行的機會。
用於等待當前執行緒終止。如果一個執行緒A執行了 threadB.join() 語句,其含義是:當前執行緒A等待 threadB 執行緒終止之後才從 threadB.join() 返回繼續往下執行自己的程式碼。
通常來說,可以認為有三種方式:1)繼承 Thread 類;2)實現 Runnable 介面;3)實現 Callable 介面。
其中,Thread 其實也是實現了 Runable 介面。Runnable 和 Callable 的主要區別在於是否有返回值。
run():普通的方法呼叫,在主執行緒中執行,不會新建一個執行緒來執行。
start():新啟動一個執行緒,這時此執行緒處於就緒(可執行)狀態,並沒有執行,一旦得到 CPU 時間片,就開始執行 run() 方法。
一個執行緒可以處於以下狀態之一:
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:執行完畢已經退出的執行緒處於此狀態。
執行緒在給定的時間點只能處於一種狀態。這些狀態是虛擬機器器狀態,不反映任何作業系統執行緒狀態。
1)Lock 是一個介面;synchronized 是 Java 中的關鍵字,synchronized 是內建的語言實現;
2)Lock 在發生異常時,如果沒有主動通過 unLock() 去釋放鎖,很可能會造成死鎖現象,因此使用 Lock 時需要在 finally 塊中釋放鎖;synchronized 不需要手動獲取鎖和釋放鎖,在發生異常時,會自動釋放鎖,因此不會導致死鎖現象發生;
3)Lock 的使用更加靈活,可以有響應中斷、有超時時間等;而 synchronized 卻不行,使用 synchronized 時,等待的執行緒會一直等待下去,直到獲取到鎖;
4)在效能上,隨著近些年 synchronized 的不斷優化,Lock 和 synchronized 在效能上已經沒有很明顯的差距了,所以效能不應該成為我們選擇兩者的主要原因。官方推薦儘量使用 synchronized,除非 synchronized 無法滿足需求時,則可以使用 Lock。
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) {}
死鎖的四個必要條件:
1)互斥條件:程序對所分配到的資源進行排他性控制,即在一段時間內某資源僅為一個程序所佔有。此時若有其他程序請求該資源,則請求程序只能等待。
2)請求和保持條件:程序已經獲得了至少一個資源,但又對其他資源發出請求,而該資源已被其他程序佔有,此時該程序的請求被阻塞,但又對自己獲得的資源保持不放。
3)不可剝奪條件:程序已獲得的資源在未使用完畢之前,不可被其他程序強行剝奪,只能由自己釋放。
4)環路等待條件:存在一種程序資源的迴圈等待鏈,鏈中每一個程序已獲得的資源同時被 鏈中下一個程序所請求。即存在一個處於等待狀態的程序集合{Pl, P2, …, pn},其中 Pi 等待的資源被 P(i+1) 佔有(i=0, 1, …, n-1),Pn 等待的資源被 P0佔 有,如下圖所示。
預防死鎖的方式就是打破四個必要條件中的任意一個即可。
1)打破互斥條件:在系統裡取消互斥。若資源不被一個程序獨佔使用,那麼死鎖是肯定不會發生的。但一般來說在所列的四個條件中,「互斥」條件是無法破壞的。因此,在死鎖預防裡主要是破壞其他幾個必要條件,而不去涉及破壞「互斥」條件。。
2)打破請求和保持條件:1)採用資源預先分配策略,即程序執行前申請全部資源,滿足則執行,不然就等待。 2)每個程序提出新的資源申請前,必須先釋放它先前所佔有的資源。
3)打破不可剝奪條件:當程序佔有某些資源後又進一步申請其他資源而無法滿足,則該程序必須釋放它原來佔有的資源。
4)打破環路等待條件:實現資源有序分配策略,將系統的所有資源統一編號,所有程序只能採用按序號遞增的形式申請資源。
如果我們在方法中直接new一個執行緒來處理,當這個方法被呼叫頻繁時就會建立很多執行緒,不僅會消耗系統資源,還會降低系統的穩定性,一不小心把系統搞崩了,就可以直接去財務那結帳了。
如果我們合理的使用執行緒池,則可以避免把系統搞崩的窘境。總得來說,使用執行緒池可以帶來以下幾個好處:
threadFactory(執行緒工廠):用於建立工作執行緒的工廠。
corePoolSize(核心執行緒數):當執行緒池執行的執行緒少於 corePoolSize 時,將建立一個新執行緒來處理請求,即使其他工作執行緒處於空閒狀態。
workQueue(佇列):用於保留任務並移交給工作執行緒的阻塞佇列。
maximumPoolSize(最大執行緒數):執行緒池允許開啟的最大執行緒數。
handler(拒絕策略):往執行緒池新增任務時,將在下面兩種情況觸發拒絕策略:1)執行緒池執行狀態不是 RUNNING;2)執行緒池已經達到最大執行緒數,並且阻塞佇列已滿時。
keepAliveTime(保持存活時間):如果執行緒池當前執行緒數超過 corePoolSize,則多餘的執行緒空閒時間超過 keepAliveTime 時會被終止。
AbortPolicy:中止策略。預設的拒絕策略,直接丟擲 RejectedExecutionException。呼叫者可以捕獲這個異常,然後根據需求編寫自己的處理程式碼。
DiscardPolicy:拋棄策略。什麼都不做,直接拋棄被拒絕的任務。
DiscardOldestPolicy:拋棄最老策略。拋棄阻塞佇列中最老的任務,相當於就是佇列中下一個將要被執行的任務,然後重新提交被拒絕的任務。如果阻塞佇列是一個優先佇列,那麼「拋棄最舊的」策略將導致拋棄優先順序最高的任務,因此最好不要將該策略和優先順序佇列放在一起使用。
CallerRunsPolicy:呼叫者執行策略。在呼叫者執行緒中執行該任務。該策略實現了一種調節機制,該策略既不會拋棄任務,也不會丟擲異常,而是將任務回退到呼叫者(呼叫執行緒池執行任務的主執行緒),由於執行任務需要一定時間,因此主執行緒至少在一段時間內不能提交任務,從而使得執行緒池有時間來處理完正在執行的任務。
List(對付順序的好幫手): List 介面儲存一組不唯一(可以有多個元素參照相同的物件)、有序的物件。
Set(注重獨一無二的性質):不允許重複的集合,不會有多個元素參照相同的物件。
Map(用Key來搜尋的專業戶): 使用鍵值對儲存。Map 會維護與 Key 有關聯的值。兩個 Key可以參照相同的物件,但 Key 不能重複,典型的 Key 是String型別,但也可以是任何物件。
ArrayList 底層基於動態陣列實現,LinkedList 底層基於連結串列實現。
對於按 index 索引資料(get/set方法):ArrayList 通過 index 直接定位到陣列對應位置的節點,而 LinkedList需要從頭結點或尾節點開始遍歷,直到尋找到目標節點,因此在效率上 ArrayList 優於 LinkedList。
對於隨機插入和刪除:ArrayList 需要移動目標節點後面的節點(使用System.arraycopy 方法移動節點),而 LinkedList 只需修改目標節點前後節點的 next 或 prev 屬性即可,因此在效率上 LinkedList 優於 ArrayList。
對於順序插入和刪除:由於 ArrayList 不需要移動節點,因此在效率上比 LinkedList 更好。這也是為什麼在實際使用中 ArrayList 更多,因為大部分情況下我們的使用都是順序插入。
Vector 和 ArrayList 幾乎一致,唯一的區別是 Vector 在方法上使用了 synchronized 來保證執行緒安全,因此在效能上 ArrayList 具有更好的表現。
有類似關係的還有:StringBuilder 和 StringBuffer、HashMap 和 Hashtable。
我們現在用的都是 JDK 1.8,底層是由「陣列+連結串列+紅黑樹」組成,如下圖,而在 JDK 1.8 之前是由「陣列+連結串列」組成。
主要是為了提升在 hash 衝突嚴重時(連結串列過長)的查詢效能,使用連結串列的查詢效能是 O(n),而使用紅黑樹是 O(logn)。
對於插入,預設情況下是使用連結串列節點。當同一個索引位置的節點在新增後超過8個(閾值8):如果此時陣列長度大於等於 64,則會觸發連結串列節點轉紅黑樹節點(treeifyBin);而如果陣列長度小於64,則不會觸發連結串列轉紅黑樹,而是會進行擴容,因為此時的資料量還比較小。
對於移除,當同一個索引位置的節點在移除後達到 6 個,並且該索引位置的節點為紅黑樹節點,會觸發紅黑樹節點轉連結串列節點(untreeify)。
預設初始容量是16。HashMap 的容量必須是2的N次方,HashMap 會根據我們傳入的容量計算一個大於等於該容量的最小的2的N次方,例如傳 9,容量為16。
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 類。
程式計數器:執行緒私有。一塊較小的記憶體空間,可以看作當前執行緒所執行的位元組碼的行號指示器。如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器值則為空。
Java虛擬機器器棧:執行緒私有。它的生命週期與執行緒相同。虛擬機器器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器器棧中入棧到出棧的過程。
本地方法棧:執行緒私有。本地方法棧與虛擬機器器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器器棧為虛擬機器器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器器使用到的Native方法服務。
Java堆:執行緒共用。對大多數應用來說,Java堆是Java虛擬機器器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共用的一塊記憶體區域,在虛擬機器器啟動時建立。此記憶體區域的唯一目的就是存放物件範例,幾乎所有的物件範例都在這裡分配記憶體。
方法區:與Java堆一樣,是各個執行緒共用的記憶體區域,它用於儲存已被虛擬機器器載入的類資訊(構造方法、介面定義)、常數、靜態變數、即時編譯器編譯後的程式碼(位元組碼)等資料。方法區是JVM規範中定義的一個概念,具體放在哪裡,不同的實現可以放在不同的地方。
執行時常數池:執行時常數池是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常數池,用於存放編譯期生成的各種字面量和符號參照,這部分內容將在類載入後進入方法區的執行時常數池中存放。
String str = new String("hello");
上面的語句中變數 str 放在棧上,用 new 建立出來的字串物件放在堆上,而"hello"這個字面量是放在堆中。
如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類別載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。
啟動類載入器(Bootstrap ClassLoader):
這個類載入器負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器器識別的(僅按照檔名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器器記憶體中。
擴充套件類載入器(Extension ClassLoader):
這個載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
應用程式類載入器(Application ClassLoader):
這個類載入器由sun.misc.Launcher$AppClassLoader實現。由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類載入器,如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
自定義類載入器:
使用者自定義的類載入器。
類載入的過程包括:載入、驗證、準備、解析、初始化,其中驗證、準備、解析統稱為連線。
載入:通過一個類的全限定名來獲取定義此類的二進位制位元組流,在記憶體中生成一個代表這個類的java.lang.Class物件。
驗證:確保Class檔案的位元組流中包含的資訊符合當前虛擬機器器的要求,並且不會危害虛擬機器器自身的安全。
準備:為靜態變數分配記憶體並設定靜態變數初始值,這裡所說的初始值「通常情況」下是資料型別的零值。
解析:將常數池內的符號參照替換為直接參照。
初始化:到了初始化階段,才真正開始執行類中定義的 Java 初始化程式程式碼。主要是靜態變數賦值動作和靜態語句塊(static{})中的語句。
在什麼時候?
在觸發GC的時候,具體如下,這裡只說常見的 Young GC 和 Full GC。
觸發Young GC:當新生代中的 Eden 區沒有足夠空間進行分配時會觸發Young GC。
觸發Full GC:
對什麼?
對那些JVM認為已經「死掉」的物件。即從GC Root開始搜尋,搜尋不到的,並且經過一次篩選標記沒有復活的物件。
做了什麼?
對這些JVM認為已經「死掉」的物件進行垃圾收集,新生代使用複製演演算法,老年代使用標記-清除和標記-整理演演算法。
在Java語言中,可作為GC Roots的物件包括下面幾種:
標記 - 清除演演算法
首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
複製演演算法
為了解決效率問題,一種稱為「複製」(Copying)的收集演演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。
標記 - 整理演演算法
複製收集演演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演演算法。
根據老年代的特點,有人提出了另外一種「標記-整理」(Mark-Compact)演演算法,標記過程仍然與「標記-清除」演演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
分代收集演演算法
當前商業虛擬機器器的垃圾收集都採用「分代收集」(Generational Collection)演演算法,這種演演算法並沒有什麼新的思想,只是根據物件存活週期的不同將記憶體劃分為幾塊。
一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演演算法。
在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演演算法,只需要付出少量存活物件的複製成本就可以完成收集。
在老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清理或者標記—整理演演算法來進行回收。
金三銀四的季節,相信有不少同學正準備跳槽。
我將我最近的原創的文章進行了彙總:原創匯總,其中有不少面試高頻題目解析,很多都是我自己在面試大廠時遇到的,我在對每個題目解析時都會按較高的標準進行深入剖析,可能只看一遍並不能完全明白,但是相信反覆閱讀,定能有所收穫。
更多程式設計相關知識,請存取:!!
以上就是【吐血整理】2022年Java 基礎高頻面試題及答案(收藏)的詳細內容,更多請關注TW511.COM其它相關文章!