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

JavaGuide發表於2022-06-09

你好,我是 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來建立執行緒。參考連結:http://www.cplusplus.com/refe...

? 擴充一下:

“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 語言“編譯與解釋並存”?

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

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

  • 編譯型編譯型語言 會通過編譯器將原始碼一次性翻譯成可被該平臺執行的機器碼。一般情況下,編譯語言的執行速度比較快,開發效率比較低。常見的編譯性語言有 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 版本大概每三個月釋出一次。但這不是固定的,我覺得了解這個沒啥用處。詳情參見:https://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 開源專案:https://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:https://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 和 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 語言關鍵字有哪些?

分類關鍵字
訪問控制privateprotectedpublic
類,方法和變數修飾符abstractclassextendsfinalimplementsinterfacenative
newstaticstrictfpsynchronizedtransientvolatileenum
程式控制breakcontinuereturndowhileifelse
forinstanceofswitchcasedefaultassert
錯誤處理trycatchthrowthrowsfinally
包相關importpackage
基本型別booleanbytechardoublefloatintlong
short
變數引用superthisvoid
保留字gotoconst

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

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

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

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

官方文件:https://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 值。用一句口訣就是:“符號在前就先加/減,符號在後就後加/減”。

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 ):

  • “兩同”即方法名相同、形參列表相同;
  • “兩小”指的是子類方法返回值型別應比父類方法返回值型別更小或相等,子類方法宣告丟擲的異常類應比父類方法宣告丟擲的異常類更小或相等;
  • “一大”指的是子類方法的訪問許可權應比父類方法的訪問許可權更大或相等。

⭐️ 關於 重寫的返回值型別 這裡需要額外多說明一下,上面的表述不太清晰準確:如果方法的返回型別是 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 種基本資料型別的預設值以及所佔空間的大小如下:

基本型別位數位元組預設值取值範圍
byte810-128 ~ 127
short1620-32768 ~ 32767
int3240-2147483648 ~ 2147483647
long6480L-9223372036854775808 ~ 9223372036854775807
char162'u0000'0 ~ 65535
float3240f1.4E-45 ~ 3.4028235E38
double6480d4.9E-324 ~ 1.7976931348623157E308
boolean1 falsetrue、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 方法比較

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

什麼是自動拆裝箱?

  • 裝箱:將基本型別用它們對應的引用型別包裝起來;
  • 拆箱:將包裝型別轉換為基本資料型別;

舉例:

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;
}

參考

相關文章