Core Java 52 問(含答案)

秉心說發表於2019-04-08

上篇文章 4.9k Star 安卓面試知識點,請收下! 翻譯了 Mindorks 的一份超強面試題,今天帶來的是其中 Core Java 部分 52 道題目的答案。題目的質量還是比較高的,基本涵蓋了 Java 基礎知識點,物件導向、集合、基本資料型別、併發、Java 記憶體模型、GC、異常等等都有涉及。整理答案的過程中才發現自己也有一些知識點記不太清了,一邊回憶學習,一邊整理答案。52 道題,可以程式碼驗證的都經過我的驗證,保證答案准確。

文章比較長,翻到文末可以直接獲取 Core Java 52 問 pdf 文件。

下面就進入提問!

Core Java

物件導向

1. 什麼是 OOP ?

別說我還真的被問到過這個問題,記得當時我第一句話就是 “萬物皆物件”。當然答案很開放,說說你對物件導向的理解就行了。下面是從 維基百科 總結的答案:

Object-oriented programming ,物件導向程式設計,是種具有物件概念的程式程式設計典範,同時也是一種程式開發的抽象方針。

它可能包含資料 、屬性 、程式碼與方法。物件則指的是類的例項。它將物件作為程式的基本單元,將程式和資料封裝其中,以提高軟體的重用性、靈活性和擴充套件性,

物件裡的程式可以訪問及經常修改物件相關連的資料。在物件導向程式程式設計裡,計算機程式會被設計成彼此相關的物件。

OOP 一般具有以下特徵:

  • 類與物件

類定義了一件事物的抽象特點。類的定義包含了資料的形式以及對資料的操作。舉例來說, 這個類會包含狗的一切基礎特徵,即所有 都共有的特徵或行為,例如它的孕育、毛皮顏色和吠叫的能力。 物件就是類的例項。

  • 封裝

封裝(Encapsulation)是指 OOP 隱藏了某一方法的具體執行步驟和實現細節,限制只有特定類的物件可以訪問這一特定類的成員,通常暴露介面來供呼叫。每個人都知道怎麼訪問它,但卻不必考慮它的內部實現細節。

舉例來說, 這個類有 吠叫() 的方法,這一方法定義了狗具體該通過什麼方法吠叫。但是,呼叫者並不知道它到底是如何吠叫的。

  • 繼承

繼承性(Inheritance)是指,在某種情況下,一個類會有 子類。子類比原本的類(稱為父類)要更加具體化。例如, 這個類可能會有它的子類 牧羊犬吉娃娃犬

子類會繼承父類的屬性和行為,並且也可包含它們自己的。這意味著程式設計師只需要將相同的程式碼寫一次。

  • 多型

多型(Polymorphism)是指由繼承而產生的相關的不同的類,其物件對同一訊息會做出不同的響應。例如,狗和雞都有 叫() 這一方法,但是呼叫狗的 叫(),狗會吠叫;呼叫雞的 叫(),雞則會啼叫。

除了繼承,介面實現,同一類中進行方法過載也是多型的體現。

2. 抽象類和介面的區別 ?

  • 抽象類可以有預設的方法實現。介面在 jdk1.8 之前沒有方法實現,1.8 之後可以使用 default 關鍵字定義方法實現
  • 抽象類可以有建構函式,介面不可以
  • 子類使用 extends 關鍵字來繼承抽象類。如果子類不是抽象類的話,它需要提供抽象類中所有宣告的方法的實現。子類使用關鍵字 implements 來實現介面。它需要提供介面中所有宣告的方法的實現
  • 抽象方法可以有 publicprotecteddefault 這些修飾符,介面方法預設是 public 的,可以預設
  • 抽象類的欄位可以使用任何修飾符。介面中欄位預設是 public final
  • 單繼承 多實現
  • 抽象類:is-a 的關係,體現的是一種關係的延續。 介面: like-a 體現的是一種功能的擴充套件關係

3. Iterator 和 Enumeration 的區別 ?

  • 函式介面不同 Enumeration 只有 2 個函式介面。通過 Enumeration,我們只能讀取集合的資料,而不能對資料進行修改。

      Iterator 有 3 個函式介面。Iterator 除了能讀取集合的資料之外,也能資料進行刪除操作。
    複製程式碼
  • Iterator 支援 fail-fast 機制,而 Enumeration 不支援。 Enumeration 是 JDK 1.0 新增的介面。使用到它的函式包括 Vector 、Hashtable 等類,這些類都是 JDK 1.0 中加入的,Enumeration 存在的目的就是為它們提供遍歷介面。Enumeration 本身並沒有支援同步,而在 Vector 、Hashtable 實現 Enumeration 時,新增了同步。

      而 Iterator 是 JDK 1.2 才新增的介面,它也是為了 HashMap 、ArrayList 等集合提供遍歷介面。Iterator 是支援 fail-fast 機制的:當多個執行緒對同一個集合的內容進行操作時,就可能會產生 fail-fast 事件。
      
      所以 Enumeration 比 Iterator 的遍歷速度更快。
    複製程式碼

4. 你同意 組合優先於繼承 嗎 ?

繼承的功能非常強大,但是也存在諸多問題,因為它違背了封裝原則 。 只 有當子類和超類之間確實存在子型別關係時,使用繼承才是恰當的 。 即使如此,如果子 類和超類處在不同的包中,並且超類並不是為了繼承而設計的,那麼繼承將會導致脆弱性 ( fragility ) 。 為了避免這種脆弱性,可以用複合和轉發機制來代替繼承,尤其是當存在適當 的介面可以實現包裝類的時候 。 包裝類不僅比子類更加健壯,而且功能也更加強大。(也就是裝飾者模式)。

具體見 Effective Java 18條 複合優先於繼承

5. 方法過載和方法重寫的區別 ?

同一個類中,方法名稱相同但是引數型別不同,稱為方法過載。 過載的方法在編譯過程中即可完成識別。具體到每一個方法呼叫,Java 編譯器會根據所傳入引數的宣告型別(注意與實際型別區分)來選取過載方法。

如果子類中定義了與父類中非私有方法同名的方法,而且這兩個方法引數型別不同,那麼在子類中,這兩個方法同樣構成了過載。反之,如果方法引數型別相同, 這時候要區分是否是靜態方法。如果是靜態方法,那麼子類中的方法會隱藏父類的方法。如果不是靜態方法,就是子類重寫了父類的方法、

對過載方法的區分在編譯階段已經完成,過載也被稱為靜態繫結,或者編譯時多型。重寫被稱為為動態繫結。

6. 你知道哪些訪問修飾符 ? 它們分別的作用 ?

訪問級別 訪問控制修飾符 同類 同包 子類 不同的包
公開 public
受保護 protected --
預設 沒有訪問控制修飾符 -- --
私有 private -- -- --

7. 一個介面可以實現另一個介面嗎 ?

可以,但是不是 implements , 而是 extends 。一個介面可以繼承一個或多個介面。

8. 什麼是多型 ?什麼是繼承 ?

在 java 中多型有編譯期多型(靜態繫結)和執行時多型(動態繫結)。方法過載是編譯期多型的一種形式。方法重寫是執行時多型的一種形式。

多型的另一個重要例子是父類引用子類例項。事實上,滿足 is-a 關係的物件都可以看出多型。 例如,Cat 類 是 Animal 類的子類,所以 Cat is Animal,這就滿足了 is-a 關係。

繼承性(Inheritance)是指,在某種情況下,一個類會有“子類”。子類比原本的類(稱為父類)要更加具體化。例如,“狗”這個類可能會有它的子類“牧羊犬”和“吉娃娃犬”。 子類會繼承父類的屬性和行為,並且也可包含它們自己的。這意味著程式設計師只需要將相同的程式碼寫一次。

9. Java 中類和介面的多繼承

在 java 中一個類不可以繼承多個類,但是介面可以繼承多個介面。

10. 什麼是設計模式?

設計模式就不在這裡展開說了。推薦一個 github 專案 java-design-patterns。 後面有機會單獨寫一寫設計模式。

集合和泛型

11. Arrays vs ArrayLists

Arrays 是一個工具類,提供了許多操作,排序,查詢陣列的靜態方法。

ArrayList 是一個動態陣列佇列,實現了 Collection 和 List 介面,提供了資料的增加,刪除,獲取等方法。

12. HashSet vs TreeSet

HashSetTreeSet 都是基於 Set 介面的實現類。其中 TreeSetSet 的子介面 SortedSet 的實現類。

HashSet 基於雜湊表實現,它不保證集合的迭代順序,特別是它不保證該順序恆久不變。允許 null 值。不支援同步。

TreeSet 基於二叉樹實現,它的元素自動排序,按照自然順序或者提供的比較器進行排序,所以 TreeSet 中元素要實現 Comparable 介面。不允許 null 值。

13. HashMap vs HashSet

HashMap HashSet
實現了 Map 介面 實現了 Set 介面
儲存鍵值對 僅儲存物件
呼叫 put() 向 map 中新增元素 呼叫 add() 方法向 Set中 新增元素
使用鍵物件來計算 hashcode 值 使用成員物件來計算 hashcode 值,對於兩個物件來說 hashcode 可能相同,所以 equals() 方法用來判斷物件的相等性,如果兩個物件不同的話,那麼返回false
HashMap 相對於 HashSet 較快,因為它是使用唯一的鍵獲取物件 HashSet 較 HashMap 來說比較慢

14. Stack vs Queue

佇列是一種基於先進先出(FIFO)策略的集合型別。佇列在儲存元素的同時儲存它們的相對順序:使它們入列順序和出列順序相同。佇列在生活和程式設計中極其常見,就像排隊,先進入隊伍的總是先出去。

棧是一種基於後進先出(LIFO)策略的集合型別,當使用 foreach 語句遍歷棧中的元素時,元素的處理順序和它們被壓入的順序正好相反。就像我們的郵箱,後進來的郵件總是會先看到。

15. 解釋 java 中的泛型

16. String 類是如何實現的?它為什麼被設計成不可變類 ?

String 類是使用 char 陣列實現的,jdk 9 中改為使用 byte 陣列實現。 不可變類好處:

  • 不可變類比較簡單。
  • 不可變物件本質上是執行緒安全的,它們不要求同步。不可變物件可以被自由地共享。
  • 不僅可以共享不可變物件,甚至可以共享它們的內部資訊。
  • 不可變物件為其他物件提供了大量的構建。
  • 不可變類真正唯一的缺點是,對於每個不同的值都需要一個單獨的物件。

走進 JDK 之 String

物件和基本型別

17. 為什麼說 String 不可變 ?

  • Stringfinal 類,不可以被擴充套件
  • private final char value[],不可變
  • 沒有對外提供任何修改 value[] 的方法

參見我的文章 String 為什麼不可變 ?

18. 什麼是 String.intern() ? 何時使用? 為什麼使用 ?

如果常量池中存在當前字串, 就會直接返回當前字串. 如果常量池中沒有此字串, 會將此字串放入常量池中後, 再返回。

將執行時需要大量使用的字串放入常量池。

深入解析 String.intern()

19. 列舉 8 種基本型別

基本型別 大小 最大值 最小值 包裝類 虛擬機器中符號
boolean - - - Boolean Z
char 16 bits 65536 0 Character C
byte 8 bits 127 -128 Byte B
short 16 bits 215-1 - 215 Short S
int 32 bits 231-1 231 Integer I
long 64 bits 263-1 -263 Long J
float 32 bits 3.4028235e+38f -3.4028235e+38f Float F
double 64 bits 1.7976931348623157e+308 -1.7976931348623157e+308 Double D

20. int 和 Integer 區別

int 是基本資料型別,一般直接儲存在棧中,更加高效

Integer 是包裝型別,new 出來的物件儲存在堆中,比較耗費資源

21. 什麼是自動裝箱拆箱 ?

把基本資料型別轉換成包裝類的過程叫做裝箱。

把包裝類轉換成基本資料型別的過程叫做拆箱。

在Java 1.5之前,要手動進行裝箱,

Integer i = new Integer(10);
複製程式碼

java 1.5 中,提供了自動拆箱與自動裝箱功能。需要拆箱和裝箱的時候,會自動進行轉換。

Integer i =10;  //自動裝箱
int b= i;     //自動拆箱
複製程式碼

自動裝箱都是通過Integer.valueOf()方法來實現的,Integer的自動拆箱都是通過integer.intValue來實現的。

關於 Java 基本型別可以看我的一篇總結文章:走進 JDK 之談談基本型別

22. Java 中的型別轉換

賦值和方法呼叫轉換規則:從低位型別到高位型別自動轉換;從高位型別到低位型別需要強制型別轉換:

  1. 布林型和其它基本資料型別之間不能相互轉換;
  2. byte 型可以轉換為 shortintlongfloatdouble
  3. short 可轉換為 intlongfloatdouble
  4. char 可轉換為 intlongfloatdouble
  5. int 可轉換為 longfloatdouble
  6. long 可轉換為 floatdouble
  7. float 可轉換為 double

基本型別 與 對應包裝類 可自動轉換,這是自動裝箱和折箱的原理。 

兩個引用型別間轉換:

  1. 子類能直接轉換為父類 或 介面型別

  2. 父類轉換為子類要強制型別轉換,且在執行時若實際不是對應的物件,會丟擲 ClassCastException 執行時異常;

23. Java 值傳遞還是引用傳遞 ?

值傳遞。

值傳遞(pass by value)是指在呼叫函式時將實際引數複製一份傳遞到函式中,這樣在函式中如果對引數進行修改,將不會影響到實際引數。

引用傳遞(pass by reference)是指在呼叫函式時將實際引數的地址直接傳遞到函式中,那麼在函式中對引數所進行的修改,將影響到實際引數。

Java 呼叫方法傳遞的是實參引用的副本。

為什麼說Java中只有值傳遞。

24. 物件例項化和初始化之間的區別 ?

Initialization(例項化) 是建立新物件並且分配記憶體的過程。新建立的變數必須顯示賦值,否則它將使用儲存在該記憶體區域上的上一個變數包含的值。為了避免這個問題,Java 會給不同的資料型別賦予預設值:

  • boolean defaults to false;
  • byte defaults to 0;
  • short defaults to 0;
  • int defaults to 0;
  • long defaults to 0L;
  • char defaults to \u0000;
  • float defaults to 0.0f;
  • double defaults to 0.0d;
  • object defaults to null.

Instantiation(初始化)是給已經宣告的變數顯示賦值的過程。

int j;  // Initialized variable (int defaults to 0 right after)
j = 10; // Instantiated variable
複製程式碼

25. 區域性變數、例項變數以及類變數之間的區別?

區域性變數僅僅存在於建立它的方法中,他們被儲存在棧記憶體,在方法外無法獲得它們的引用。Java 的方法執行不是依賴暫存器的,而是棧幀,每個方法的執行和結束都伴隨著棧幀的入棧和出棧,也伴隨著區域性變數的建立和釋放。

例項變數也就是成員變數,宣告在類中,依賴類例項而存在,不同類例項中變數值也可能不同。

類變數也就是靜態變數,在所有類例項中只有一個值,在一個地方改變它的值將會改變所有類例項中的值。

Java 記憶體模型和垃圾收集器

26. 什麼是垃圾收集器 ? 它是如何工作的 ?

Java 和 C++ 之前有一堵由記憶體動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裡面的人想出去。

垃圾收集器主要用來回收堆上的無用物件,Java 開發者只管建立和使用物件,JVM 來為你自動分配和回收記憶體。

JVM 通過可達性分析演算法來判定物件是否存活。這個演算法的基本思路就是通過一系列的稱為 GC Roots 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到 GC Roots 沒有任何引用鏈相連(用圖論的話來說,就是從 GC Roots 到這個物件不可達)時,則證明此物件是不可用的。

即使在可達性分析演算法中不可達的物件,也並非是 非死不可 的,這時候他們暫時處於緩刑階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程。

更多詳細內容可以閱讀 《深入理解 Java 虛擬機器》 第三章 垃圾收集器與記憶體分配策略

27. 什麼是 java 記憶體模型? 它遵循了什麼原則?它的堆疊是如何組織的 ?

Java 虛擬機器規範中試圖定義一種 Java 記憶體模型(Java Memory Model,JMM)來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓 Java 程式在各種平臺下都能達到一致的記憶體訪問效果。JMM 是語言級的記憶體模型,它確保在不同的編譯器和不同的處理器平臺上,通過禁止特定型別的編譯器重排序和處理器重排序,為程式設計師提供一致的記憶體可見性保證。

JMM 記憶體模型的抽象表示如下:

Core Java 52 問(含答案)

結合上圖,在 Java 中,所有例項域、靜態域和陣列元素都儲存在堆記憶體中,堆記憶體線上程之間共享。區域性變數、方法定義引數和異常處理器引數在棧中,不會線上程之間共享,它們不會有記憶體可見性問題,也不會受記憶體模型影響。

Java 執行緒之間的通訊由 JMM 控制,JMM 決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。JMM 通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為 Java 程式設計師提供記憶體可見性保證。

更多詳細內容可以閱讀 《Java 併發程式設計的藝術》

28. 什麼是 記憶體洩漏,java 如何處理它 ?

記憶體洩露就是不會再被使用的物件無法被 GC 回收,即這些物件在可達性分析中是可達的,但在程式中的確不會再被使用。比如長生命週期的物件引用了短生命週期的物件,導致短生命週期物件不能被回收。

Java 應該不會處理記憶體洩漏,我們能做的更多是防患於未然,以及使用合理手段監測,比如 Android 裡常用的 LeakCanary,詳細原理可以看我之前的一篇文章 LeakCanary 原始碼解析

29. 什麼是 強引用,軟引用,弱引用,虛引用 ?

在 JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分為 強引用、軟引用、弱引用、虛引用 4 種,這 4 種引用強度依次逐漸減弱。

  • 強引用就是指程式程式碼之中普遍存在的,類似 Object obj = new Object() 這類的引用,只要強引用還存在,GC 永遠不會回收掉被引用的物件。

  • 軟引用是用來描述一些還有用但並非必須的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在 JDK 1.2 之後,提供了 SoftReference 類來實現軟引用。

  • 弱引用也是用來描述非必須物件的,但它的強度比軟引用要弱一些,被弱引用關聯的物件只能生存到下一次 GC 發生之前。當 GC 工作時,無法當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在 JDK 1.2 之後,提供了 WeakReference 類來實現弱引用。

  • 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被 GC 回收時收到一個系統通知。在 JDK 1.2 之後,提供了 PhantomReference 類來實現虛引用。

併發

30. 關鍵字 synchronized 的作用 ?

關鍵字 synchronized 可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一時刻,只能有一個執行緒處於方法或同步塊中,它保證了執行緒對變數訪問的可見性和排他性。

  • 對於普通同步方法,鎖是當前例項物件
  • 對於靜態同步方法,鎖是當前類的 Class 物件
  • 對於同步方法塊,鎖是 Synchonized 括號裡配置的物件

31. ThreadPoolExecutor 作用 ?

在 Java 中,使用執行緒來執行非同步任務。Java 執行緒的建立與銷燬需要一定的開銷,如果我們為每一個任務都建立一個新執行緒來執行,這些執行緒的建立與銷燬將消耗大量的計算資源。同時,為每一個任務建立一個新執行緒來執行,這種策略可能會使處於高負荷的應用最終崩潰。

關於執行緒池的詳細介紹,推薦一篇文章 Java併發程式設計:執行緒池的使用

32. 關鍵字 volatile 的作用 ?

volatile 是輕量級的 synchronized,它在多處理器開發中保證了共享變數的 可見性。volatile 用來修飾欄位(成員變數),就是告知程式任何對該變數的訪問均需從共享記憶體中獲取,而對它的改變必須同步重新整理回共享記憶體,它能保證所有執行緒對變數訪問的可見性。

但是,過多的使用 volatile 是不必要的,因為它會降低程式執行的效率。

異常

33. try{} catch{} finally{} 是如何工作的 ?

try 程式碼塊用來標記需要進行異常監控的程式碼

catch 程式碼塊跟在 try 程式碼塊之後,用來捕獲 try 程式碼塊中觸發的某種指定型別的異常。除了宣告所捕獲的異常型別之外,catch 程式碼塊還定義了針對該異常型別的異常處理器。在 Java 中,try 程式碼塊後面可以跟著多個 catch 程式碼塊,來捕獲不同的異常。Java 虛擬機器會從上至下匹配異常處理器。因此,前面的 catch 程式碼塊所捕獲的異常型別不能覆蓋後面的,否則編譯器會報錯。

finally 程式碼塊跟在 try 程式碼塊和 catch 程式碼塊之後,用來宣告一段必定執行的程式碼。它的設計初衷是為了避免跳過某些關鍵的清理程式碼,例如關閉已開啟的系統資源。

在編譯生成的位元組碼中,每個方法都附帶一個異常表。異常表中的每一個條目代表一個異常處理器,並且由 from 指標,to 指標, target 指標以及所捕獲的異常型別構成。這些指標的值是位元組碼索引(bytecode index,bci),用以定位位元組碼。

其中,from 指標和 to 指標標示了該異常處理器所監控的範圍,例如 try 程式碼塊所覆蓋的範圍。target 指標則指向異常處理器的起始位置,比如 catch 程式碼塊的起始位置。

finally 程式碼塊的編譯比較複雜。當前版本 Java 編譯器的做法,是複製 finally 程式碼塊的內容,分別放在try-catch 程式碼塊所有正常執行路徑以及異常執行路徑的出口中。

以上內容來自極客時間專欄 深入拆解 Java 虛擬機器

34. Checked Exception 和 Un-Checked Exception 區別 ?

在 Java 中,所有異常都是 Throwable 類或者其子類的例項。Throwable 有兩大直接子類。一個是 Error,涵蓋程式不應捕獲的異常。當 Error 發生時,它的執行狀態已經無法恢復,需要終止執行緒甚至虛擬機器。第二個子類是 Exception,涵蓋程式可能需要捕獲並且處理的異常。

Exception 有一個特殊的子類 RuntimeException,執行時異常,用來表示 “程式雖然無法繼續執行,但還能搶救一下” 的情況。

RuntimeException 和 Error 屬於 Java 裡的非檢查異常(unchecked exception)。其他異常則屬於檢查異常(checked exception)。在 Java 語法中,所有的檢查異常都需要程式顯式地捕獲,或者在方法宣告中用 throws 關鍵字標註。通常情況下,程式中自定義的異常應為檢查異常,以便最大化利用 Java 編譯器的編譯時檢查。

以上內容來自極客時間專欄 深入拆解 Java 虛擬機器

其他

35. 什麼是序列化?如何實現 ?

序列化是將物件轉換成位元組流以便持久化儲存的過程。它可以儲存物件的狀態和資料,方便在特定時刻重新構建該物件。在 Android 中,一般使用 Serializable , Externalizable (implements Serializable) 或者 Parcelable 介面。

Serializable 最容易實現,直接實現介面即可。Externalizable 可以在序列化的過程中插入一些自己的邏輯程式碼,考慮到它是 Java 早期版本的遺留物,現在基本已經沒人再使用它。在 Android 中推薦使用 Parcelable ,它就是為 Android 而實現,效能是 Serializable 的十倍,因為 Serializable 使用了反射。反射不僅慢,還會建立大量臨時物件,導致頻繁 GC。

例子:

/**
*  Implementing the Serializeable interface is all that is required
*/
public class User implements Serializable {

    private String name;
    private String email;

        public User() {
        }

        public String getName() {
            return name;
        }

        public void setName(final String name) {
            this.name = name;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(final String email) {
            this.email = email;
        }
    }
複製程式碼

Parcelable 需要多一些工作:

public class User implements Parcelable {

        private String name;
        private String email;

        /**
         * Interface that must be implemented and provided as a public CREATOR field
         * that generates instances of your Parcelable class from a Parcel.
         */
        public static final Creator<User> CREATOR = new Creator<User>() {

            /**
             * Creates a new USer object from the Parcel. This is the reason why
             * the constructor that takes a Parcel is needed.
             */
            @Override
            public User createFromParcel(Parcel in) {
                return new User(in);
            }

            /**
             * Create a new array of the Parcelable class.
             * @return an array of the Parcelable class,
             * with every entry initialized to null.
             */
            @Override
            public User[] newArray(int size) {
                return new User[size];
            }
        };

        public User() {
        }

        /**
         * Parcel overloaded constructor required for
         * Parcelable implementation used in the CREATOR
         */
        private User(Parcel in) {
            name = in.readString();
            email = in.readString();
        }

        public String getName() {
            return name;
        }

        public void setName(final String name) {
            this.name = name;
        }

        public String getEmail() {
            return email;
        }

        public void setEmail(final String email) {
            this.email = email;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        /**
         * This is where the parcel is performed.
         */
        @Override
        public void writeToParcel(final Parcel parcel, final int i) {
            parcel.writeString(name);
            parcel.writeString(email);
        }
    }
複製程式碼

36. 關鍵字 transient 的作用 ?

transient 很簡單,它的作用就是讓被其修飾的成員變數在序列化的過程中不被序列化。

37. 什麼是匿名內部類 ?

匿名內部類是唯一一種沒有構造器的類。正因為其沒有構造器,所以匿名內部類的使用範圍非常有限,大部分匿名內部類用於介面回撥。匿名內部類在編譯的時候由系統自動起名為 Outter$1.class。一般來說,匿名內部類用於繼承其他類或是實現介面,並不需要增加額外的方法,只是對繼承方法的實現或是重寫。

Android 中應用最常見的就是各種點選事件。

38. 物件的 == 和 .equals 區別 ?

對於物件而言,== 永遠比較的都是其記憶體地址。而 equals() 則要看該物件是否重寫了 equals() 方法,如果沒有則會呼叫父類的 equals() 方法,如果父類也沒有實現的話,就不斷向上追溯,直至 Object 類。看一下 Object.java 中的 equals() 方法:

public boolean equals(Object obj) {
    return (this == obj);
}
複製程式碼

Object 中,equals 等同於 ==,都是比較記憶體地址。再看一下不是比較記憶體地址的,String.equals()

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
複製程式碼

這裡比較的就不再是記憶體地址,而是其實際的值。

39. hashCode() 和 equals() 用處 ?

equals() 用於判斷兩個物件是否相等,未被重寫的話就是判斷記憶體地址,和 == 語義一致。重寫了的話,就按照重寫的邏輯進行判斷。

hashCode() 用於計算物件的雜湊碼,預設實現是將物件的記憶體地址作為雜湊碼返回,可以保證不同物件的返回值不同。理論上,hashCode 也可以用來比較物件是否相等。hashCode() 主要用在雜湊表中,比如 HashMapHashSet 等。

當我們向雜湊表(如HashSet、HashMap等)中新增物件object時,首先呼叫hashCode()方法計算object的雜湊碼,通過雜湊碼可以直接定位object在雜湊表中的位置(一般是雜湊碼對雜湊表大小取餘)。如果該位置沒有物件,可以直接將object插入該位置;如果該位置有物件(可能有多個,通過連結串列實現),則呼叫equals()方法比較這些物件與object是否相等,如果相等,則不需要儲存object;如果不相等,則將該物件加入到連結串列中。

equals() 相等,hashCode 必然相等。反之則不然,hashCode 相等,equals() 不能保證一定相等。

40. 建構函式中為什麼不能呼叫抽象方法 ?

建構函式中不能呼叫抽象方法,說的更嚴謹一點,建構函式中不能呼叫可被覆蓋的方法

先看這樣一個例子:

public abstract class Super {

    Super(){
        overrideMe();
    }

    abstract void overrideMe();
}

public class Sub extends Super {

    private final Instant instant;

    public Sub() {
        instant = Instant.now();
    }

    @Override
    void overrideMe() {
        System.out.println(instant);
    }

    public static void main(String[] args) {
        Sub sub=new Sub();
        sub.overrideMe();
    }

}
複製程式碼

最後的列印結果:

null
2019-04-01T02:42:13.947Z
複製程式碼

第一次列印出的是 null,因為 overrideMe 方法被 Super 構造器呼叫的時候,構造器 Sub 還沒有機會初始化 instant 域 。 注意,這個程式觀察到的 final 域處於兩種不同的狀態 。

超類的構造器在子類的構造器之前執行,所以,子類中覆蓋版本的方法將會在子類的構造器執行之前先被 呼叫 。 如果該覆蓋版本的方法依賴於子類構造器所執行的任何初始化工作,該方法將不會如預期般執行 。

41. 你什麼時候會使用 final 關鍵字 ?

對於一個 final 變數,如果是基本資料型別的變數,則其數值一旦在初始化之後便不能更改;如果是引用型別的變數,則在對其初始化之後便不能再讓其指向另一個物件。

另外,匿名內部類中使用的外部區域性變數只能是 final 變數。

final 變數是基本資料型別以及 String 型別時,如果在編譯期間能知道它的確切值,則編譯器會把它當做編譯期常量使用。

final 修飾方法引數也是為了強調引數不可改變。

final 修飾類表示類不可被繼承。

淺析 Java 的 final 關鍵字

42. final, finally 和 finalize 的區別 ?

final 和 finally 就不再說了。重點看看 finalize。

如果類中重寫了 finalize 方法,當該類物件被回收時,finalize 方法有可能會被觸發。 Effective Java 中明確說明 終結方法(finalize)通常是不可預測的,也是很危險的,一般情況下是不必要的。

JVM 不僅不保證 finalize 方法可以被及時執行,而且根本就不保證它們會被執行。所以不要依賴 finalize 方法來做一些例如 釋放資源的操作。可能會延時物件的回收,造成效能損失。

43. Java 中 static 關鍵字的含義 ?

static 就是為了方便在沒有建立物件的情況下來進行呼叫(方法/變數)。

  • static 方法一般稱作靜態方法,由於靜態方法不依賴於任何物件就可以進行訪問,因此對於靜態方法來說,是沒有 this 的,因為它不依附於任何物件,既然都沒有物件,就談不上 this 了。並且由於這個特性,在靜態方法中不能訪問類的非靜態成員變數和非靜態成員方法,因為非靜態成員方法/變數都是必須依賴具體的物件才能夠被呼叫。

  • static 變數也稱作靜態變數,靜態變數和非靜態變數的區別是:靜態變數被所有的物件所共享,在記憶體中只有一個副本,它當且僅當在類初次載入時會被初始化。而非靜態變數是物件所擁有的,在建立物件的時候被初始化,存在多個副本,各個物件擁有的副本互不影響。

  • static 關鍵字還有一個比較關鍵的作用就是 用來形成靜態程式碼塊以優化程式效能。static 塊可以置於類中的任何地方,類中可以有多個 static 塊。在類初次被載入的時候,會按照 static 塊的順序來執行每個 static 塊,只會在類載入的時候執行一次。

static 成員變數的初始化順序按照定義的順序進行初始化。

44. 靜態方法可以重寫嗎 ?

你可以重寫,但這並不是多型的體現,並不是真正意義上的重寫。子類的靜態方法會隱藏父類的靜態方法,這兩個方法並沒有什麼關係,具體呼叫哪一個方法是看呼叫者是哪個物件的引用,並不存在多型。只有普通的方法呼叫才可以是多型的。

45. 靜態程式碼塊如何執行 ?

靜態程式碼塊隨著類的載入而執行,而且只執行一次。

靜態程式碼塊經過編譯後是放在 <clinit> 中, <clinit> 在jvm第一次載入class檔案時呼叫,包括靜態變數初始化語句和靜態塊的執行。

46. 什麼是反射 ?

反射 (Reflection) 是 Java 的特徵之一,它允許執行中的 Java 程式獲取自身的資訊,並且可以操作類或物件的內部屬性。

Oracle 官方對反射的解釋是:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

簡而言之,通過反射,我們可以在執行時獲得程式或程式集中每一個型別的成員和成員的資訊。程式中一般的物件的型別都是在編譯期就確定下來的,而 Java 反射機制可以動態地建立物件並呼叫其屬性,這樣的物件的型別在編譯期是未知的。所以我們可以通過反射機制直接建立物件,即使這個物件的型別在編譯期是未知的。

反射的核心是 JVM 在執行時才動態載入類或呼叫方法/訪問屬性,它不需要事先(寫程式碼的時候或編譯期)知道執行物件是誰。

Java 反射主要提供以下功能:

在執行時判斷任意一個物件所屬的類; 在執行時構造任意一個類的物件; 在執行時判斷任意一個類所具有的成員變數和方法(通過反射甚至可以呼叫private方法); 在執行時呼叫任意一個物件的方法

深入 Java 反射

IDE 的智慧提示就是利用反射。

47. 什麼是依賴注入 ?列舉幾個庫 ?你使用過嗎 ?

理解的還不夠透徹,放上來一篇網上的寫的不錯的文章: 輕鬆理解 Java開發中的依賴注入(DI)和控制反轉(IOC)

48. StringBuilder 如何避免不可變類 String 的分配問題?

StringBuilder 內部維護了一個可變長的 char[],用來儲存和拼接字串,從而避免了因 String 是不可變類帶來的頻繁建立 String 物件的問題。

49. StringBuffer 和 StringBuilder 區別 ?

StringBufferStringBuilder 在使用上基本沒有區別。StringBuffer 通過 synchronized 關鍵字保證了執行緒安全,而 StringBuilder 沒有任何同步操作。所以在確定無執行緒同步問題時,使用 StringBuilder 效率更高。

50. Enumeration and an Iterator 區別 ?

重複了,見第 3 題。

51. fail-fast and fail-safe 區別 ?

fail-fast 機制在遍歷一個集合時,當集合結構被修改,會丟擲 Concurrent Modification Exception。迭代器在遍歷過程中是直接訪問內部資料的,因此內部的資料在遍歷的過程中無法被修。 為了保證不被修改,迭代器內部維護了一個標記 “mode” ,當集合結構改變(新增刪除或者修改),標記 "mode" 會被修改, 而迭代器每次的 hasNext()next() 方法都會檢查該 "mode" 是否被改變,當檢測到被修改時,丟擲 Concurrent Modification Exception

fail-safe 任何對集合結構的修改都會在一個複製的集合上進行修改,因此不會丟擲 ConcurrentModificationException

fail-safe 機制有兩個問題:

  1. 需要複製集合,產生大量的無效物件,開銷大

  2. 無法保證讀取的資料是目前原始資料結構中的資料

52. 什麼是 NIO ?

在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 類, 引入了一種基於通道和緩衝區的 I/O 方式, 它可以使用 Native 函式庫直接分配堆外記憶體,然後通過一個儲存在 Java 堆的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作, 避免了在 Java 堆和 Native 堆中來回複製資料。

NIO 是一種同步非阻塞的 IO 模型。同步是指執行緒不斷輪詢 IO 事件是否就緒,非阻塞是指執行緒在等待 IO 的時候,可以同時做其他任務。 同步的核心就是 Selector,Selector 代替了執行緒本身輪詢 IO 事件,避免了阻塞同時減少了不必要的執行緒消耗;非阻塞的核心就是通道和緩衝區, 當 IO 事件就緒時,可以通過寫道緩衝區,保證 IO 的成功,而無需執行緒阻塞式地等待。

深入理解 Java NIO

End

文章首發於微信公眾號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

Core Java 52 問(含答案)

微信搜尋 秉心說, 或者掃碼關注,回覆 Core Java 即可領取所有回答 pdf 文件 。

相關文章