31道Java核心面試題,一次性打包送給你
先看再點贊,給自己一點思考的時間,微信搜尋【沉默王二】關注這個靠才華苟且的程式設計師。
本文 GitHub github.com/itwanger 已收錄,裡面還有一線大廠整理的面試題,以及我的系列文章。二哥,你好,找工作找了仨月,還沒有找到,很焦慮,我該怎麼辦呢?你那有沒有 Java 方面的面試題可以分享一波啊?
以上是讀者田田給我發的私信,看完後於我心有慼慼焉啊,最近境況確實不容樂觀,並非是個人的原因造成的。那,既然需要面試題,二哥就義不容辭,必須得準備一波。
這次我花了一週的時間,準備了 31 道 Java 核心面試題,希望能夠幫助到田田,以及其他和田田類似情況的讀者朋友。
(後續我打算再花一週時間,更新第二波,同樣有 31 道,敬請期待)
01、請說出 Java 14 版本中更新的重要功能
Java 14 釋出於 2020 年 3 月 17 日,更新的重要功能有:
- switch 表示式
- instanceof 增強表示式,預覽功能
- 文字塊,第二次預覽
- Records,預覽功能
剛好我之前寫過一篇文章,關於 Java 14 的開箱體驗,很香,讀者朋友需要的話,可以點下面的連結看一看。
Java 14 開箱,它真香香香香
02、請說出 Java 13 版本中更新的重要功能
Java 13 釋出於 2019 年 9 月 17 日,更新的重要功能有:
- 文字塊,預覽功能
- switch 表示式,預覽功能
- Java Socket 重新實現
FileSystems.newFileSystem()
- 支援 Unicode 12.1
- 可伸縮、低延遲的垃圾收集器改進,用於返回未使用的記憶體
03、請說出 Java 12 版本中更新的重要功能
Java 12 釋出於 2019 年 3 月 19 日,更新的重要功能有:
- JVM 更新
File.mismatch()
方法- 緊湊型數字格式
- String 類新增了一些方法,比如說
indent()
04、請說出 Java 11 版本中更新的重要功能
Java 11 是繼 Java 8 之後的第二個商用版本,如果你下載的是 Oracle JDK,則需要進行付費;如果想繼續使用免費版本,需要下載 Open JDK。
Oracle JDK 中會有一些 Open JDK 沒有的、商用閉源的功能。
Java 11 更新的重要功能有:
- 可以直接使用
java
命令執行 Java 程式,原始碼將會隱式編譯和執行。 - String 類新增了一些方法,比如說
isBlank()
、lines()
、strip()
等等。 - Files 類新增了兩個讀寫方法,
readString()
和writeString()
。 - 可以在 Lambda 表示式中使用 var 作為變數型別。
05、請說出 Java 10 版本中更新的重要功能
Java 10 更新的重要功能有:
- 區域性變數型別推斷,舉個例子,
var list = new ArrayList
,可以使用 var 來作為變數型別,Java 編譯器知道 list 的型別為字串的 ArrayList。(); - 增強
java.util.Locale
。 - 提供了一組預設的根證書頒發機構(CA)。
06、請說出 Java 9 版本中更新的重要功能
Java 9 更新的重要功能有:
- 模組系統
- 不可變的 List、Set、Map 的工廠方法
- 介面中可以有私有方法
- 垃圾收集器改進
07、請說出 Java 8 版本中更新的重要功能
Java 8 釋出於 2014 年 3 月份,可以說是 Java 6 之後最重要的版本更新,深受開發者的喜愛。
- 函數語言程式設計和 Lambda 表示式
- Stream 流
- Java Date Time API
- 介面中可以使用預設方法和靜態方法
我強烈建議點開上面的連結閱讀以下,以正確理解這些概念。
08、請說出 Java 面向物件程式設計中的一些重要概念
- 抽象
- 封裝
- 多型
- 繼承
09、Java 聲稱的平臺獨立性指的是什麼?
常見的作業系統有 Windows、Linux、OS-X,那麼平臺獨立性意味著我們可以在任何作業系統中執行相同原始碼的 Java 程式,比如說我們可以在 Windows 上編寫 Java 程式,然後在 Linux 上執行它。
10、什麼是 JVM?
JVM(Java Virtual Machine)俗稱 Java 虛擬機器。之所以稱為虛擬機器,是因為它實際上並不存在。它提供了一種執行環境,可供 Java 位元組碼在上面執行。
JVM 提供了以下操作:
- 載入位元組碼
- 驗證位元組碼
- 執行位元組碼
- 提供執行時環境
JVM 定義了以下內容:
- 儲存區
- 類檔案格式
- 暫存器組
- 垃圾回收堆
- 致命錯誤報告等
我們來嘗試理解一下 JVM 的內部結構,它包含了類載入器(Class Loader)、執行時資料區(Runtime Data Areas)和執行引擎(Excution Engine)。
1)類載入器
類載入器是 JVM 的一個子系統,用於載入類檔案。每當我們執行一個 Java 程式,它都會由類載入器首先載入。Java 中有三個內建的類載入器:
啟動類載入器(Bootstrap Class-Loader),載入
jre/lib
包下面的 jar 檔案,比如說常見的 rt.jar(包含了 Java 標準庫下的所有類檔案,比如說java.lang
包下的類,java.net
包下的類,java.util
包下的類,java.io
包下的類,java.sql
包下的類)。擴充套件類載入器(Extension or Ext Class-Loader),載入
jre/lib/ext
包下面的 jar 檔案。應用類載入器(Application or App Clas-Loader),根據程式的類路徑(classpath)來載入 Java 類。
一般來說,Java 程式設計師並不需要直接同類載入器進行互動。JVM 預設的行為就已經足夠滿足大多數情況的需求了。不過,如果遇到了需要和類載入器進行互動的情況,而對類載入器的機制又不是很瞭解的話,就不得不花大量的時間去除錯
ClassNotFoundException
和 NoClassDefFoundError
等異常。
對於任意一個類,都需要由它的類載入器和這個類本身一同確定其在 JVM 中的唯一性。也就是說,如果兩個類的載入器不同,即使兩個類來源於同一個位元組碼檔案,那這兩個類就必定不相等(比如兩個類的 Class 物件不 equals
)。
是不是有點暈,來來來,通過一段簡單的程式碼瞭解下。
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
每個 Java 類都維護著一個指向定義它的類載入器的引用,通過 類名.class.getClassLoader()
可以獲取到此引用;然後通過 loader.getParent()
可以獲取類載入器的上層類載入器。
上面這段程式碼的輸出結果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4617c264
第一行輸出為 Test 的類載入器,即應用類載入器,它是 sun.misc.Launcher$AppClassLoader
類的例項;第二行輸出為擴充套件類載入器,是 sun.misc.Launcher$ExtClassLoader
類的例項。那啟動類載入器呢?
按理說,擴充套件類載入器的上層類載入器是啟動類載入器,但在我這個版本的 JDK 中, 擴充套件類載入器的 getParent()
返回 null
。所以沒有輸出。
2)執行時資料區
執行時資料區又包含以下內容。
PC暫存器(PC Register),也叫程式計數器(Program Counter Register),是一塊較小的記憶體空間,它的作用可以看做是當前執行緒所執行的位元組碼的訊號指示器。
JVM 棧(Java Virtual Machine Stack),與 PC 暫存器一樣,JVM 棧也是執行緒私有的。每一個 JVM 執行緒都有自己的 JVM 棧,這個棧與執行緒同時建立,它的生命週期與執行緒相同。
本地方法棧(Native Method Stack),JVM 可能會使用到傳統的棧來支援 Native 方法(使用 Java 語言以外的其它語言[C語言]編寫的方法)的執行,這個棧就是本地方法棧。
堆(Heap),在 JVM 中,堆是可供各條執行緒共享的執行時記憶體區域,也是供所有類例項和資料物件分配記憶體的區域。
方法區(Method area),在 JVM 中,被載入型別的資訊都儲存在方法區中。包括型別資訊(Type Information)和方法列表(Method Tables)。方法區是所有執行緒共享的,所以訪問方法區資訊的方法必須是執行緒安全的。
執行時常量池(Runtime Constant Pool),執行時常量池是每一個類或介面的常量池在執行時的表現形式,它包括了編譯器可知的數值字面量,以及執行期解析後才能獲得的方法或欄位的引用。簡而言之,當一個方法或者變數被引用時,JVM 通過執行時常量區來查詢方法或者變數在記憶體裡的實際地址。
3)執行引擎
執行引擎包含了:
直譯器:讀取位元組碼流,然後執行指令。因為它一條一條地解釋和執行指令,所以它可以很快地解釋位元組碼,但是執行起來會比較慢。
即時(Just-In-Time,JIT)編譯器:即時編譯器用來彌補直譯器的缺點,提高效能。執行引擎首先按照解釋執行的方式來執行,然後在合適的時候,即時編譯器把整段位元組碼編譯成原生代碼。然後,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過原生代碼去執行。執行原生代碼比一條一條進行解釋執行的速度快很多。編譯後的程式碼可以執行的很快,因為原生代碼是儲存在快取裡的。
11、JDK 和 JVM 有什麼區別?
JDK 是 Java Development Kit 的首字母縮寫,是提供給 Java 開發人員的軟體環境,包含 JRE 和一組開發工具。可分為以下版本:
- 標準版(大多數開發人員用的就是這個)
- 企業版
- 微型版
JDK 包含了一個私有的 JVM 和一些其他資源,比如說編譯器(javac 命令)、直譯器(java 命令)等,幫助 Java 程式設計師完成開發工作。
12、JVM 和 JRE 有什麼區別?
Java Runtime Environment(JRE)是 JVM 的實現。JRE 由 JVM 和 Java 二進位制檔案以及其他類組成,可以執行任何程式。JRE 不包含 Java 編譯器,偵錯程式等任何開發工具。
13、哪個類是所有類的超類?
java.lang.Object
是所有 Java 類的超類,我們不需要繼承它,因為是隱式繼承的。
14、為什麼 Java 不支援多重繼承?
如果有兩個類共同繼承(extends)一個有特定方法的父類,那麼該方法會被兩個子類重寫。然後,如果你決定同時繼承這兩個子類,那麼在你呼叫該重寫方法時,編譯器不能識別你要呼叫哪個子類的方法。這也正是著名的菱形問題,見下圖。
ClassC 同時繼承了 ClassA 和 ClassB,ClassC 的物件在呼叫 ClassA 和 ClassB 中過載的方法時,就不知道該呼叫 ClassA 的方法,還是 ClassB 的方法。
15、為什麼 Java 不是純粹的面向物件程式語言?
之所以不能說 Java 是純粹的面向物件程式語言,是因為 Java 支援基本資料型別,比如說 int、short、long、double 等,儘管它們有自己的包裝器型別,但它們的確不能算是物件。
16、path 和 classpath 之間有什麼區別?
path 是作業系統用來查詢可執行檔案的環境變數,我的電腦上就定義了下圖這些 path 變數,比如 Java 和 Maven 的。
classpath 是針對 Java 而言的,用於指定 Java 虛擬機器載入的位元組碼檔案路徑。
17、Java 中 `main()` 方法的重要性是什麼?
每個程式都需要一個入口,對於 Java 程式來說,入口就是 main 方法。
public static void main(String[] args) {}
public 關鍵字是另外一個訪問修飾符,除了可以宣告方法和變數(所有類可見),還可以宣告類。
main()
方法必須宣告為 public。static 關鍵字表示該變數或方法是靜態變數或靜態方法,可以直接通過類訪問,不需要例項化物件來訪問。
void 關鍵字用於指定方法沒有返回值。
另外,main 關鍵字為方法的名字,Java 虛擬機器在執行程式時會尋找這個識別符號;args 為 main()
方法的引數名,它的型別為一個 String 陣列,也就是說,在使用 java 命令執行程式的時候,可以給 main()
方法傳遞字串陣列作為引數。
java HelloWorld 沉默王二 沉默王三
javac 命令用來編譯程式,java 命令用來執行程式,HelloWorld 為這段程式的類名,沉默王二和沉默王三為字串陣列,中間通過空格隔開,然後就可以在 main()
方法中通過 args[0]
和 args[1]
獲取傳遞的引數值了。
public class HelloWorld {
public static void main(String[] args) {
if ("沉默王二".equals(args[0])) {
}
if ("沉默王三".equals(args[1])) {
}
}
}
main()
方法的寫法並不是唯一的,還有其他幾種變體,儘管它們可能並不常見,可以簡單來了解一下。
第二種,把方括號 []
往 args 靠近而不是 String 靠近:
public static void main(String []args) { }
第三種,把方括號 []
放在 args 的右側:
public static void main(String args[]) { }
第四種,還可以把陣列形式換成可變引數的形式:
public static void main(String...args) { }
第五種,在 main()
方法上新增另外一個修飾符 strictfp
,用於強調在處理浮點數時的相容性:
public strictfp static void main(String[] args) { }
也可以在 main()
方法上新增 final 關鍵字或者 synchronized 關鍵字。
第六種,還可以為 args 引數新增 final 關鍵字:
public static void main(final String[] args) { }
第七種,最複雜的一種,所有可以新增的關鍵字統統新增上:
final static synchronized strictfp void main(final String[] args) { }
當然了,並不需要為了裝逼特意把 main()
方法寫成上面提到的這些形式,使用 IDE 提供的預設形式就可以了。
18、Java 的重寫(Override)和過載(Overload)有什麼區別?
先來看一段重寫的程式碼吧。
class LaoWang{
public void write() {
System.out.println("老王寫了一本《基督山伯爵》");
}
}
public class XiaoWang extends LaoWang {
@Override
public void write() {
System.out.println("小王寫了一本《茶花女》");
}
}
重寫的兩個方法名相同,方法引數的個數也相同;不過一個方法在父類中,另外一個在子類中。就好像父類 LaoWang 有一個 write()
方法(無參),方法體是寫一本《基督山伯爵》;子類 XiaoWang 重寫了父類的 write()
方法(無參),但方法體是寫一本《茶花女》。
來寫一段測試程式碼。
public class OverridingTest {
public static void main(String[] args) {
LaoWang wang = new XiaoWang();
wang.write();
}
}
大家猜結果是什麼?