Java 基礎常見知識點&面試題總結(上),2022 最新版!| JavaGuide

你好,我是 Guide。秋招即將到來,我對 JavaGuide 的內容進行了重構完善,公眾號同步一下最新更新,希望能夠幫助你。

基礎概念與常識

Java 語言有哪些特點?

  1. 簡單易學;
  2. 面向對象(封裝,繼承,多態);
  3. 平台無關性( Java 虛擬機實現平台無關性);
  4. 支持多線程( C++ 語言沒有內置的多線程機制,因此必須調用操作系統的多線程功能來進行多線程程序設計,而 Java 語言卻提供了多線程支持);
  5. 可靠性;
  6. 安全性;
  7. 支持網絡編程並且很方便( Java 語言誕生本身就是為簡化網絡編程設計的,因此 Java 語言不僅支持網絡編程而且很方便);
  8. 編譯與解釋並存;

🐛 修正(參見: issue#544 :C++11 開始(2011 年的時候),C++就引入了多線程庫,在 windows、linux、macos 都可以使用std::threadstd::async來創建線程。參考鏈接://www.cplusplus.com/reference/thread/thread/?kw=thread

🌈 拓展一下:

「Write Once, Run Anywhere(一次編寫,隨處運行)」這句宣傳口號,真心經典,流傳了好多年!以至於,直到今天,依然有很多人覺得跨平台是 Java 語言最大的優勢。實際上,跨平台已經不是 Java 最大的賣點了,各種 JDK 新特性也不是。目前市面上虛擬化技術已經非常成熟,比如你通過 Docker 就很容易實現跨平台了。在我看來,Java 強大的生態才是!

JVM vs JDK vs JRE

JVM

Java 虛擬機(JVM)是運行 Java 位元組碼的虛擬機。JVM 有針對不同系統的特定實現(Windows,Linux,macOS),目的是使用相同的位元組碼,它們都會給出相同的結果。位元組碼和不同系統的 JVM 實現是 Java 語言「一次編譯,隨處可以運行」的關鍵所在。

JVM 並不是只有一種!只要滿足 JVM 規範,每個公司、組織或者個人都可以開發自己的專屬 JVM。 也就是說我們平時接觸到的 HotSpot VM 僅僅是是 JVM 規範的一種實現而已。

除了我們平時最常用的 HotSpot VM 外,還有 J9 VM、Zing VM、JRockit VM 等 JVM 。維基百科上就有常見 JVM 的對比:Comparison of Java virtual machines ,感興趣的可以去看看。並且,你可以在 Java SE Specifications 上找到各個版本的 JDK 對應的 JVM 規範。

Java SE Specifications

JDK 和 JRE

JDK 是 Java Development Kit 縮寫,它是功能齊全的 Java SDK。它擁有 JRE 所擁有的一切,還有編譯器(javac)和工具(如 javadoc 和 jdb)。它能夠創建和編譯程序。

JRE 是 Java 運行時環境。它是運行已編譯 Java 程序所需的所有內容的集合,包括 Java 虛擬機(JVM),Java 類庫,java 命令和其他的一些基礎構件。但是,它不能用於創建新程序。

如果你只是為了運行一下 Java 程序的話,那麼你只需要安裝 JRE 就可以了。如果你需要進行一些 Java 編程方面的工作,那麼你就需要安裝 JDK 了。但是,這不是絕對的。有時,即使您不打算在計算機上進行任何 Java 開發,仍然需要安裝 JDK。例如,如果要使用 JSP 部署 Web 應用程序,那麼從技術上講,您只是在應用程序服務器中運行 Java 程序。那你為什麼需要 JDK 呢?因為應用程序服務器會將 JSP 轉換為 Java servlet,並且需要使用 JDK 來編譯 servlet。

什麼是位元組碼?採用位元組碼的好處是什麼?

在 Java 中,JVM 可以理解的代碼就叫做位元組碼(即擴展名為 .class 的文件),它不面向任何特定的處理器,只面向虛擬機。Java 語言通過位元組碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以, Java 程序運行時相對來說還是高效的(不過,和 C++,Rust,Go 等語言還是有一定差距的),而且,由於位元組碼並不針對一種特定的機器,因此,Java 程序無須重新編譯便可在多種不同操作系統的計算機上運行。

Java 程序從源代碼到運行的過程如下圖所示:

我們需要格外注意的是 .class->機器碼 這一步。在這一步 JVM 類加載器首先加載位元組碼文件,然後通過解釋器逐行解釋執行,這種方式的執行速度會相對比較慢。而且,有些方法和代碼塊是經常需要被調用的(也就是所謂的熱點代碼),所以後面引進了 JIT(just-in-time compilation) 編譯器,而 JIT 屬於運行時編譯。當 JIT 編譯器完成第一次編譯後,其會將位元組碼對應的機器碼保存下來,下次可以直接使用。而我們知道,機器碼的運行效率肯定是高於 Java 解釋器的。這也解釋了我們為什麼經常會說 Java 是編譯與解釋共存的語言

HotSpot 採用了惰性評估(Lazy Evaluation)的做法,根據二八定律,消耗大部分系統資源的只有那一小部分的代碼(熱點代碼),而這也就是 JIT 所需要編譯的部分。JVM 會根據代碼每次被執行的情況收集信息並相應地做出一些優化,因此執行的次數越多,它的速度就越快。JDK 9 引入了一種新的編譯模式 AOT(Ahead of Time Compilation),它是直接將位元組碼編譯成機器碼,這樣就避免了 JIT 預熱等各方面的開銷。JDK 支持分層編譯和 AOT 協作使用。但是 ,AOT 編譯器的編譯質量是肯定比不上 JIT 編譯器的。

《Java 面試指北》來啦!這是一份教你如何更高效地準備面試的小冊,涵蓋常見八股文(系統設計、常見框架、分佈式、高並發 ……)、優質面經等內容。

為什麼說 Java 語言「編譯與解釋並存」?

其實這個問題我們講位元組碼的時候已經提到過,因為比較重要,所以我們這裡再提一下。

我們可以將高級編程語言按照程序的執行方式分為兩種:

  • 編譯型編譯型語言 會通過編譯器將源代碼一次性翻譯成可被該平台執行的機器碼。一般情況下,編譯語言的執行速度比較快,開發效率比較低。常見的編譯性語言有 C、C++、Go、Rust 等等。
  • 解釋型解釋型語言會通過解釋器一句一句的將代碼解釋(interpret)為機器代碼後再執行。解釋型語言開發效率比較快,執行速度比較慢。常見的解釋性語言有 Python、JavaScript、PHP 等等。

編譯型語言和解釋型語言

根據維基百科介紹:

為了改善編譯語言的效率而發展出的即時編譯技術,已經縮小了這兩種語言間的差距。這種技術混合了編譯語言與解釋型語言的優點,它像編譯語言一樣,先把程序源代碼編譯成位元組碼。到執行期時,再將位元組碼直譯,之後執行。JavaLLVM是這種技術的代表產物。

相關閱讀:基本功 | Java 即時編譯器原理解析及實踐

為什麼說 Java 語言「編譯與解釋並存」?

這是因為 Java 語言既具有編譯型語言的特徵,也具有解釋型語言的特徵。因為 Java 程序要經過先編譯,後解釋兩個步驟,由 Java 編寫的程序需要先經過編譯步驟,生成位元組碼(.class 文件),這種位元組碼必須由 Java 解釋器來解釋執行。

Oracle JDK vs OpenJDK

可能在看這個問題之前很多人和我一樣並沒有接觸和使用過 OpenJDK 。那麼 Oracle JDK 和 OpenJDK 之間是否存在重大差異?下面我通過收集到的一些資料,為你解答這個被很多人忽視的問題。

對於 Java 7,沒什麼關鍵的地方。OpenJDK 項目主要基於 Sun 捐贈的 HotSpot 源代碼。此外,OpenJDK 被選為 Java 7 的參考實現,由 Oracle 工程師維護。關於 JVM,JDK,JRE 和 OpenJDK 之間的區別,Oracle 博客帖子在 2012 年有一個更詳細的答案:

問:OpenJDK 存儲庫中的源代碼與用於構建 Oracle JDK 的代碼之間有什麼區別?

答:非常接近 – 我們的 Oracle JDK 版本構建過程基於 OpenJDK 7 構建,只添加了幾個部分,例如部署代碼,其中包括 Oracle 的 Java 插件和 Java WebStart 的實現,以及一些閉源的第三方組件,如圖形光柵化器,一些開源的第三方組件,如 Rhino,以及一些零碎的東西,如附加文檔或第三方字體。展望未來,我們的目的是開源 Oracle JDK 的所有部分,除了我們考慮商業功能的部分。

總結:(提示:下面括號內的內容是基於原文補充說明的,因為原文太過於晦澀難懂,用人話重新解釋了下,如果你看得懂裏面的術語,可以忽略括號解釋的內容)

  1. Oracle JDK 大概每 6 個月發一次主要版本(從 2014 年 3 月 JDK 8 LTS 發佈到 2017 年 9 月 JDK 9 發佈經歷了長達 3 年多的時間,所以並不總是 6 個月),而 OpenJDK 版本大概每三個月發佈一次。但這不是固定的,我覺得了解這個沒啥用處。詳情參見://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence

  2. OpenJDK 是一個參考模型並且是完全開源的,而 Oracle JDK 是 OpenJDK 的一個實現,並不是完全開源的;(個人觀點:眾所周知,JDK 原來是 SUN 公司開發的,後來 SUN 公司又賣給了 Oracle 公司,Oracle 公司以 Oracle 數據庫而著名,而 Oracle 數據庫又是閉源的,這個時候 Oracle 公司就不想完全開源了,但是原來的 SUN 公司又把 JDK 給開源了,如果這個時候 Oracle 收購回來之後就把他給閉源,必然會引其很多 Java 開發者的不滿,導致大家對 Java 失去信心,那 Oracle 公司收購回來不就把 Java 爛在手裡了嗎!然後,Oracle 公司就想了個騷操作,這樣吧,我把一部分核心代碼開源出來給你們玩,並且我要和你們自己搞的 JDK 區分下,你們叫 OpenJDK,我叫 Oracle JDK,我發佈我的,你們繼續玩你們的,要是你們搞出來什麼好玩的東西,我後續發佈 Oracle JDK 也會拿來用一下,一舉兩得!)OpenJDK 開源項目://github.com/openjdk/jdk

  3. Oracle JDK 比 OpenJDK 更穩定(肯定啦,Oracle JDK 由 Oracle 內部團隊進行單獨研發的,而且發佈時間不 OpenJDK 更長,質量更有保障)。OpenJDK 和 Oracle JDK 的代碼幾乎相同(OpenJDK 的代碼是從 Oracle JDK 代碼派生出來的,可以理解為在 Oracle JDK 分支上拉了一條新的分支叫 OpenJDK,所以大部分代碼相同),但 Oracle JDK 有更多的類和一些錯誤修復。因此,如果您想開發企業/商業軟件,我建議您選擇 Oracle JDK,因為它經過了徹底的測試和穩定。某些情況下,有些人提到在使用 OpenJDK 可能會遇到了許多應用程序崩潰的問題,但是,只需切換到 Oracle JDK 就可以解決問題;

  4. 在響應性和 JVM 性能方面,Oracle JDK 與 OpenJDK 相比提供了更好的性能;

  5. Oracle JDK 不會為即將發佈的版本提供長期支持(如果是 LTS 長期支持版本的話也會,比如 JDK 8,但並不是每個版本都是 LTS 版本),用戶每次都必須通過更新到最新版本獲得支持來獲取最新版本;

  6. Oracle JDK 使用 BCL/OTN 協議獲得許可,而 OpenJDK 根據 GPL v2 許可獲得許可。

既然 Oracle JDK 這麼好,那為什麼還要有 OpenJDK?

答:

  1. OpenJDK 是開源的,開源意味着你可以對它根據你自己的需要進行修改、優化,比如 Alibaba 基於 OpenJDK 開發了 Dragonwell8://github.com/alibaba/dragonwell8

  2. OpenJDK 是商業免費的(這也是為什麼通過 yum 包管理器上默認安裝的 JDK 是 OpenJDK 而不是 Oracle JDK)。雖然 Oracle JDK 也是商業免費(比如 JDK 8),但並不是所有版本都是免費的。

  3. OpenJDK 更新頻率更快。Oracle JDK 一般是每 6 個月發佈一個新版本,而 OpenJDK 一般是每 3 個月發佈一個新版本。(現在你知道為啥 Oracle JDK 更穩定了吧,先在 OpenJDK 試試水,把大部分問題都解決掉了才在 Oracle JDK 上發佈)

基於以上這些原因,OpenJDK 還是有存在的必要的!

oracle jdk release cadence

🌈 拓展一下:

  • BCL 協議(Oracle Binary Code License Agreement): 可以使用 JDK(支持商用),但是不能進行修改。
  • OTN 協議(Oracle Technology Network License Agreement): 11 及之後新發佈的 JDK 用的都是這個協議,可以自己私下用,但是商用需要付費。

相關閱讀 👍:《Differences Between Oracle JDK and OpenJDK》

《Java 面試指北》來啦!這是一份教你如何更高效地準備面試的小冊,涵蓋常見八股文(系統設計、常見框架、分佈式、高並發 ……)、優質面經等內容。

Java 和 C++ 的區別?

我知道很多人沒學過 C++,但是面試官就是沒事喜歡拿咱們 Java 和 C++ 比呀!沒辦法!!!就算沒學過 C++,也要記下來。

雖然,Java 和 C++ 都是面向對象的語言,都支持封裝、繼承和多態,但是,它們還是有挺多不相同的地方:

  • Java 不提供指針來直接訪問內存,程序內存更加安全
  • Java 的類是單繼承的,C++ 支持多重繼承;雖然 Java 的類不可以多繼承,但是接口可以多繼承。
  • Java 有自動內存管理垃圾回收機制(GC),不需要程序員手動釋放無用內存。
  • C ++同時支持方法重載和操作符重載,但是 Java 只支持方法重載(操作符重載增加了複雜性,這與 Java 最初的設計思想不符)。
  • ……

基本語法

字符型常量和字符串常量的區別?

  1. 形式 : 字符常量是單引號引起的一個字符,字符串常量是雙引號引起的 0 個或若干個字符。
  2. 含義 : 字符常量相當於一個整型值( ASCII 值),可以參加表達式運算; 字符串常量代表一個地址值(該字符串在內存中存放位置)。
  3. 占內存大小 : 字符常量只佔 2 個位元組; 字符串常量占若干個位元組。

(注意: char 在 Java 中佔兩個位元組)

注釋有哪幾種形式?

Java 中的注釋有三種:

  1. 單行注釋

  2. 多行注釋

  3. 文檔注釋。

在我們編寫代碼的時候,如果代碼量比較少,我們自己或者團隊其他成員還可以很輕易地看懂代碼,但是當項目結構一旦複雜起來,我們就需要用到注釋了。注釋並不會執行(編譯器在編譯代碼之前會把代碼中的所有注釋抹掉,位元組碼中不保留注釋),是我們程序員寫給自己看的,注釋是你的代碼說明書,能夠幫助看代碼的人快速地理清代碼之間的邏輯關係。因此,在寫程序的時候隨手加上注釋是一個非常好的習慣。

《Clean Code》這本書明確指出:

代碼的注釋不是越詳細越好。實際上好的代碼本身就是注釋,我們要盡量規範和美化自己的代碼來減少不必要的注釋。

若編程語言足夠有表達力,就不需要注釋,盡量通過代碼來闡述。

舉個例子:

去掉下面複雜的注釋,只需要創建一個與注釋所言同一事物的函數即可

// check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

應替換為

if (employee.isEligibleForFullBenefits())

標識符和關鍵字的區別是什麼?

在我們編寫程序的時候,需要大量地為程序、類、變量、方法等取名字,於是就有了 標識符 。簡單來說, 標識符就是一個名字

有一些標識符,Java 語言已經賦予了其特殊的含義,只能用於特定的地方,這些特殊的標識符就是 關鍵字 。簡單來說,關鍵字是被賦予特殊含義的標識符 。比如,在我們的日常生活中,如果我們想要開一家店,則要給這個店起一個名字,起的這個「名字」就叫標識符。但是我們店的名字不能叫「警察局」,因為「警察局」這個名字已經被賦予了特殊的含義,而「警察局」就是我們日常生活中的關鍵字。

Java 語言關鍵字有哪些?

分類 關鍵字
訪問控制 private protected public
類,方法和變量修飾符 abstract class extends final implements interface native
new static strictfp synchronized transient volatile enum
程序控制 break continue return do while if else
for instanceof switch case default assert
錯誤處理 try catch throw throws finally
包相關 import package
基本類型 boolean byte char double float int long
short
變量引用 super this void
保留字 goto const

Tips:所有的關鍵字都是小寫的,在 IDE 中會以特殊顏色顯示。

default 這個關鍵字很特殊,既屬於程序控制,也屬於類,方法和變量修飾符,還屬於訪問控制。

  • 在程序控制中,當在 switch 中匹配不到任何情況時,可以使用 default 來編寫默認匹配的情況。
  • 在類,方法和變量修飾符中,從 JDK8 開始引入了默認方法,可以使用 default 關鍵字來定義一個方法的默認實現。
  • 在訪問控制中,如果一個方法前沒有任何修飾符,則默認會有一個修飾符 default,但是這個修飾符加上了就會報錯。

⚠️ 注意 :雖然 true, false, 和 null 看起來像關鍵字但實際上他們是字面值,同時你也不可以作為標識符來使用。

官方文檔://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html

自增自減運算符

在寫代碼的過程中,常見的一種情況是需要某個整數類型變量增加 1 或減少 1,Java 提供了一種特殊的運算符,用於這種表達式,叫做自增運算符(++)和自減運算符(–)。

++ 和 — 運算符可以放在變量之前,也可以放在變量之後,當運算符放在變量之前時(前綴),先自增/減,再賦值;當運算符放在變量之後時(後綴),先賦值,再自增/減。例如,當 b = ++a 時,先自增(自己增加 1),再賦值(賦值給 b);當 b = a++ 時,先賦值(賦值給 b),再自增(自己增加 1)。也就是,++a 輸出的是 a+1 的值,a++輸出的是 a 值。用一句口訣就是:「符號在前就先加/減,符號在後就後加/減」。

《Java 面試指北》來啦!這是一份教你如何更高效地準備面試的小冊,涵蓋常見八股文(系統設計、常見框架、分佈式、高並發 ……)、優質面經等內容。

continue、break 和 return 的區別是什麼?

在循環結構中,當循環條件不滿足或者循環次數達到要求時,循環會正常結束。但是,有時候可能需要在循環的過程中,當發生了某種條件之後 ,提前終止循環,這就需要用到下面幾個關鍵詞:

  1. continue :指跳出當前的這一次循環,繼續下一次循環。
  2. break :指跳出整個循環體,繼續執行循環下面的語句。

return 用於跳出所在方法,結束該方法的運行。return 一般有兩種用法:

  1. return; :直接使用 return 結束方法執行,用於沒有返回值函數的方法
  2. return value; :return 一個特定值,用於有返回值函數的方法

思考一下:下列語句的運行結果是什麼?

    public static void main(String[] args) {
        boolean flag = false;
        for (int i = 0; i <= 3; i++) {
            if (i == 0) {
                System.out.println("0");
            } else if (i == 1) {
                System.out.println("1");
                continue;
            } else if (i == 2) {
                System.out.println("2");
                flag = true;
            } else if (i == 3) {
                System.out.println("3");
                break;
            } else if (i == 4) {
                System.out.println("4");
            }
            System.out.println("xixi");
        }
        if (flag) {
            System.out.println("haha");
            return;
        }
        System.out.println("heihei");
    }

運行結果:

0
xixi
1
2
xixi
3
haha

方法

什麼是方法的返回值?方法有哪幾種類型?

方法的返回值 是指我們獲取到的某個方法體中的代碼執行後產生的結果!(前提是該方法可能產生結果)。返回值的作用是接收出結果,使得它可以用於其他的操作!

我們可以按照方法的返回值和參數類型將方法分為下面這幾種:

1.無參數無返回值的方法

public void f1() {
    //......
}
// 下面這個方法也沒有返回值,雖然用到了 return
public void f(int a) {
    if (...) {
        // 表示結束方法的執行,下方的輸出語句不會執行
        return;
    }
    System.out.println(a);
}

2.有參數無返回值的方法

public void f2(Parameter 1, ..., Parameter n) {
    //......
}

3.有返回值無參數的方法

public int f3() {
    //......
    return x;
}

4.有返回值有參數的方法

public int f4(int a, int b) {
    return a * b;
}

靜態方法為什麼不能調用非靜態成員?

這個需要結合 JVM 的相關知識,主要原因如下:

  1. 靜態方法是屬於類的,在類加載的時候就會分配內存,可以通過類名直接訪問。而非靜態成員屬於實例對象,只有在對象實例化之後才存在,需要通過類的實例對象去訪問。
  2. 在類的非靜態成員不存在的時候靜態成員就已經存在了,此時調用在內存中還不存在的非靜態成員,屬於非法操作。

靜態方法和實例方法有何不同?

1、調用方式

在外部調用靜態方法時,可以使用 類名.方法名 的方式,也可以使用 對象.方法名 的方式,而實例方法只有後面這種方式。也就是說,調用靜態方法可以無需創建對象

不過,需要注意的是一般不建議使用 對象.方法名 的方式來調用靜態方法。這種方式非常容易造成混淆,靜態方法不屬於類的某個對象而是屬於這個類。

因此,一般建議使用 類名.方法名 的方式來調用靜態方法。

public class Person {
    public void method() {
      //......
    }

    public static void staicMethod(){
      //......
    }
    public static void main(String[] args) {
        Person person = new Person();
        // 調用實例方法
        person.method();
        // 調用靜態方法
        Person.staicMethod()
    }
}

2、訪問類成員是否存在限制

靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變量和靜態方法),不允許訪問實例成員(即實例成員變量和實例方法),而實例方法不存在這個限制。

重載和重寫的區別

重載就是同樣的一個方法能夠根據輸入數據的不同,做出不同的處理

重寫就是當子類繼承自父類的相同方法,輸入數據一樣,但要做出有別於父類的響應時,你就要覆蓋父類方法

重載

發生在同一個類中(或者父類和子類之間),方法名必須相同,參數類型不同、個數不同、順序不同,方法返回值和訪問修飾符可以不同。

《Java 核心技術》這本書是這樣介紹重載的:

如果多個方法(比如 StringBuilder 的構造方法)有相同的名字、不同的參數, 便產生了重載。

StringBuilder sb = new StringBuilder();
StringBuilder sb2 = new StringBuilder("HelloWorld");

編譯器必須挑選出具體執行哪個方法,它通過用各個方法給出的參數類型與特定方法調用所使用的值類型進行匹配來挑選出相應的方法。 如果編譯器找不到匹配的參數, 就會產生編譯時錯誤, 因為根本不存在匹配, 或者沒有一個比其他的更好(這個過程被稱為重載解析(overloading resolution))。

Java 允許重載任何方法, 而不只是構造器方法。

綜上:重載就是同一個類中多個同名方法根據不同的傳參來執行不同的邏輯處理。

重寫

重寫發生在運行期,是子類對父類的允許訪問的方法的實現過程進行重新編寫。

  1. 方法名、參數列表必須相同,子類方法返回值類型應比父類方法返回值類型更小或相等,拋出的異常範圍小於等於父類,訪問修飾符範圍大於等於父類。
  2. 如果父類方法訪問修飾符為 private/final/static 則子類就不能重寫該方法,但是被 static 修飾的方法能夠被再次聲明。
  3. 構造方法無法被重寫

綜上:重寫就是子類對父類方法的重新改造,外部樣子不能改變,內部邏輯可以改變。

區別點 重載方法 重寫方法
發生範圍 同一個類 子類
參數列表 必須修改 一定不能修改
返回類型 可修改 子類方法返回值類型應比父類方法返回值類型更小或相等
異常 可修改 子類方法聲明拋出的異常類應比父類方法聲明拋出的異常類更小或相等;
訪問修飾符 可修改 一定不能做更嚴格的限制(可以降低限制)
發生階段 編譯期 運行期

方法的重寫要遵循「兩同兩小一大」(以下內容摘錄自《瘋狂 Java 講義》,issue#892 ):

  • 「兩同」即方法名相同、形參列表相同;
  • 「兩小」指的是子類方法返回值類型應比父類方法返回值類型更小或相等,子類方法聲明拋出的異常類應比父類方法聲明拋出的異常類更小或相等;
  • 「一大」指的是子類方法的訪問權限應比父類方法的訪問權限更大或相等。

《Java 面試指北》來啦!這是一份教你如何更高效地準備面試的小冊,涵蓋常見八股文(系統設計、常見框架、分佈式、高並發 ……)、優質面經等內容。

⭐️ 關於 重寫的返回值類型 這裡需要額外多說明一下,上面的表述不太清晰準確:如果方法的返回類型是 void 和基本數據類型,則返回值重寫時不可修改。但是如果方法的返回值是引用類型,重寫時是可以返回該引用類型的子類的。

public class Hero {
    public String name() {
        return "超級英雄";
    }
}
public class SuperMan extends Hero{
    @Override
    public String name() {
        return "超人";
    }
    public Hero hero() {
        return new Hero();
    }
}

public class SuperSuperMan extends SuperMan {
    public String name() {
        return "超級超級英雄";
    }

    @Override
    public SuperMan hero() {
        return new SuperMan();
    }
}

什麼是可變長參數?

從 Java5 開始,Java 支持定義可變長參數,所謂可變長參數就是允許在調用方法時傳入不定長度的參數。就比如下面的這個 printVariable 方法就可以接受 0 個或者多個參數。

public static void method1(String... args) {
   //......
}

另外,可變參數只能作為函數的最後一個參數,但其前面可以有也可以沒有任何其他參數。

public static void method2(String arg1, String... args) {
   //......
}

遇到方法重載的情況怎麼辦呢?會優先匹配固定參數還是可變參數的方法呢?

答案是會優先匹配固定參數的方法,因為固定參數的方法匹配度更高。

我們通過下面這個例子來證明一下。

/**
 * 微信搜 JavaGuide 回復"面試突擊"即可免費領取個人原創的 Java 面試手冊
 *
 * @author Guide哥
 * @date 2021/12/13 16:52
 **/
public class VariableLengthArgument {

    public static void printVariable(String... args) {
        for (String s : args) {
            System.out.println(s);
        }
    }

    public static void printVariable(String arg1, String arg2) {
        System.out.println(arg1 + arg2);
    }

    public static void main(String[] args) {
        printVariable("a", "b");
        printVariable("a", "b", "c", "d");
    }
}

輸出:

ab
a
b
c
d

另外,Java 的可變參數編譯後實際會被轉換成一個數組,我們看編譯後生成的 class文件就可以看出來了。

public class VariableLengthArgument {

    public static void printVariable(String... args) {
        String[] var1 = args;
        int var2 = args.length;

        for(int var3 = 0; var3 < var2; ++var3) {
            String s = var1[var3];
            System.out.println(s);
        }

    }
    // ......
}

基本數據類型

Java 中的幾種基本數據類型了解么?

Java 中有 8 種基本數據類型,分別為:

  • 6 種數字類型:
    • 4 種整數型:byteshortintlong
    • 2 種浮點型:floatdouble
  • 1 種字符類型:char
  • 1 種布爾型:boolean

這 8 種基本數據類型的默認值以及所佔空間的大小如下:

基本類型 位數 位元組 默認值 取值範圍
byte 8 1 0 -128 ~ 127
short 16 2 0 -32768 ~ 32767
int 32 4 0 -2147483648 ~ 2147483647
long 64 8 0L -9223372036854775808 ~ 9223372036854775807
char 16 2 ‘u0000’ 0 ~ 65535
float 32 4 0f 1.4E-45 ~ 3.4028235E38
double 64 8 0d 4.9E-324 ~ 1.7976931348623157E308
boolean 1 false true、false

對於 boolean,官方文檔未明確定義,它依賴於 JVM 廠商的具體實現。邏輯上理解是佔用 1 位,但是實際中會考慮計算機高效存儲因素。

另外,Java 的每種基本類型所佔存儲空間的大小不會像其他大多數語言那樣隨機器硬件架構的變化而變化。這種所佔存儲空間大小的不變性是 Java 程序比用其他大多數語言編寫的程序更具可移植性的原因之一(《Java 編程思想》2.2 節有提到)。

注意:

  1. Java 里使用 long 類型的數據一定要在數值後面加上 L,否則將作為整型解析。
  2. char a = 'h'char :單引號,String a = "hello" :雙引號。

這八種基本類型都有對應的包裝類分別為:ByteShortIntegerLongFloatDoubleCharacterBoolean

基本類型和包裝類型的區別?

  • 成員變量包裝類型不賦值就是 null ,而基本類型有默認值且不是 null
  • 包裝類型可用於泛型,而基本類型不可以。
  • 基本數據類型的局部變量存放在 Java 虛擬機棧中的局部變量表中,基本數據類型的成員變量(未被 static 修飾 )存放在 Java 虛擬機的堆中。包裝類型屬於對象類型,我們知道幾乎所有對象實例都存在於堆中。
  • 相比於對象類型, 基本數據類型佔用的空間非常小。

為什麼說是幾乎所有對象實例呢? 這是因為 HotSpot 虛擬機引入了 JIT 優化之後,會對對象進行逃逸分析,如果發現某一個對象並沒有逃逸到方法外部,那麼就可能通過標量替換來實現棧上分配,而避免堆上分配內存

⚠️ 注意 : 基本數據類型存放在棧中是一個常見的誤區! 基本數據類型的成員變量如果沒有被 static 修飾的話(不建議這麼使用,應該要使用基本數據類型對應的包裝類型),就存放在堆中。

class BasicTypeVar{
  private int x;
}

包裝類型的緩存機制了解么?

Java 基本數據類型的包裝類型的大部分都用到了緩存機制來提升性能。

Byte,Short,Integer,Long 這 4 種包裝類默認創建了數值 [-128,127] 的相應類型的緩存數據,Character 創建了數值在 [0,127] 範圍的緩存數據,Boolean 直接返回 True or False

Integer 緩存源碼:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static {
        // high value may be configured by property
        int h = 127;
    }
}

Character 緩存源碼:

public static Character valueOf(char c) {
    if (c <= 127) { // must cache
      return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

private static class CharacterCache {
    private CharacterCache(){}
    static final Character cache[] = new Character[127 + 1];
    static {
        for (int i = 0; i < cache.length; i++)
            cache[i] = new Character((char)i);
    }

}

Boolean 緩存源碼:

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

如果超出對應範圍仍然會去創建新的對象,緩存的範圍區間的大小只是在性能和資源之間的權衡。

兩種浮點數類型的包裝類 Float,Double 並沒有實現緩存機制。

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 輸出 true

Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 輸出 false

Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 輸出 false

下面我們來看一下問題。下面的代碼的輸出結果是 true 還是 false 呢?

Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);

Integer i1=40 這一行代碼會發生裝箱,也就是說這行代碼等價於 Integer i1=Integer.valueOf(40) 。因此,i1 直接使用的是緩存中的對象。而Integer i2 = new Integer(40) 會直接創建新的對象。

因此,答案是 false 。你答對了嗎?

記住:所有整型包裝類對象之間值的比較,全部使用 equals 方法比較

自動裝箱與拆箱了解嗎?原理是什麼?

什麼是自動拆裝箱?

  • 裝箱:將基本類型用它們對應的引用類型包裝起來;
  • 拆箱:將包裝類型轉換為基本數據類型;

《Java 面試指北》來啦!這是一份教你如何更高效地準備面試的小冊,涵蓋常見八股文(系統設計、常見框架、分佈式、高並發 ……)、優質面經等內容。

舉例:

Integer i = 10;  //裝箱
int n = i;   //拆箱

上面這兩行代碼對應的位元組碼為:

   L1

    LINENUMBER 8 L1

    ALOAD 0

    BIPUSH 10

    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

    PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;

   L2

    LINENUMBER 9 L2

    ALOAD 0

    ALOAD 0

    GETFIELD AutoBoxTest.i : Ljava/lang/Integer;

    INVOKEVIRTUAL java/lang/Integer.intValue ()I

    PUTFIELD AutoBoxTest.n : I

    RETURN

從位元組碼中,我們發現裝箱其實就是調用了 包裝類的valueOf()方法,拆箱其實就是調用了 xxxValue()方法。

因此,

  • Integer i = 10 等價於 Integer i = Integer.valueOf(10)
  • int n = i 等價於 int n = i.intValue();

注意:如果頻繁拆裝箱的話,也會嚴重影響系統的性能。我們應該盡量避免不必要的拆裝箱操作。

private static long sum() {
    // 應該使用 long 而不是 Long
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

《Java 面試指北》來啦!這是一份教你如何更高效地準備面試的小冊,涵蓋常見八股文(系統設計、常見框架、分佈式、高並發 ……)、優質面經等內容。

參考