這篇 Java 基礎,我吹不動了

程式設計師cxuan發表於2021-09-13

Hey guys,這裡是程式設計師cxuan,歡迎你收看我最新一期的文章,這篇文章我補充了一些關於《Java基礎核心總結》的內容,修改了部分錯別字和語句不通順的地方,並且對內部類、泛型等內容進行了一定的補充,並且我在文章有些地方給出了一些連結,這些連結都是我自己寫的硬核文章,能夠更好的幫助你理解 Java 這門語言,那麼廢話不多說,下面開始正文。

Java 概述

什麼是 Java?

Java 是 Sun Microsystems 於1995 年首次釋出的一種程式語言和計算平臺。程式語言還比較好理解,那麼什麼是 計算平臺 呢?

計算平臺是在電腦中執行應用程式(軟體)的環境,包括硬體環境軟體環境。一般系統平臺包括一臺電腦的硬體體系結構、作業系統、執行時庫。

Java 是快速,安全和可靠的。 從膝上型電腦到資料中心,從遊戲機到科學超級計算機,從手機到網際網路,Java 無處不在!Java 主要分為三個版本:

  • JavaSE(J2SE)(Java2 Platform Standard Edition,java平臺標準版)

JavaSE 是 JavaEE 和 JavaME 的基礎,JavaSE 就是基於 JDK 和 JRE,然後還包含了一些支援 Java Web 服務開發的類

  • JavaEE(J2EE)(Java 2 Platform,Enterprise Edition,java平臺企業版)

JavaEE 一開始叫 J2EE,後來改名為 JavaEE,它是 JavaSE 的一個擴充套件,這是我們企業級開發使用的一個版本,包括一些 Java Web 工具包。

  • JavaME(J2ME)(Java 2 Platform Micro Edition,java平臺微型版)。

JavaME 一般用於移動裝置和嵌入式裝置(比如手機、PDA、電視機頂盒和印表機)上執行的應用程式提供一個健壯且靈活的環境。

Java 的特點

  • Java 是一門物件導向的程式語言

什麼是物件導向?物件導向(Object Oriented) 是一種軟體開發思想。它是對現實世界的一種抽象,物件導向會把相關的資料和方法組織為一個整體來看待。

相對的另外一種開發思想就是程式導向的開發思想,什麼程式導向?程式導向(Procedure Oriented) 是一種以過程為中心的程式設計思想。

再舉個例子:比如你是個學生,你每天去上學需要做幾件事情?

起床、穿衣服、洗臉刷牙,吃飯,去學校。一般是順序性的完成一系列動作。

class student {
		void student_wakeUp(){...}
  	void student_cloth(){...}
  	void student_wash(){...}
  	void student_eating(){...}
  	void student_gotoSchool(){...}
}

而物件導向可以把學生進行抽象,所以這個例子就會變為

class student(){
  	void wakeUp(){...}
  	void cloth(){...}
  	void wash(){...}
  	void eating(){...}
  	void gotoSchool(){...}
}

可以不用嚴格按照順序來執行每個動作。這是特點一。

  • Java 摒棄了 C++ 中難以理解的多繼承、指標、記憶體管理等概念;不用手動管理物件的生命週期,這是特徵二。
  • Java 語言具有功能強大和簡單易用兩個特徵,現在企業級開發,快速敏捷開發,尤其是各種框架的出現,使 Java 成為越來越火的一門語言。這是特點三。
  • Java 是一門靜態語言,靜態語言指的就是在編譯期間就能夠知道資料型別的語言,在執行前就能夠檢查型別的正確性,一旦型別確定後就不能再更改,比如下面這個例子。
public void foo() {
    int x = 5;
    boolean b = x;
}

靜態語言主要有 Pascal, Perl, C/C++, JAVA, C#, Scala 等。

相對應的,動態語言沒有任何特定的情況需要指定變數的型別,在執行時確定的資料型別。比如有**Lisp, Perl, Python、Ruby、JavaScript **等。

從設計的角度上來說,所有語言的設計目的都是用來把人類可讀的程式碼轉換為機器指令。下面是一幅語言分類圖。

image-20210907222622439

動態語言是為了能夠讓程式設計師提高編碼效率,因此你可以使用更少的程式碼來實現功能。靜態語言設計是用來讓硬體執行的更高效,因此需要程式設計師編寫準確無誤的程式碼,以此來讓你的程式碼儘快的執行。從這個角度來說,靜態語言的執行效率要比動態語言高,速度更快。這是特點四。

  • Java 具有平臺獨立性和可移植性

Java 有一句非常著名的口號: Write once, run anywhere,也就是一次編寫、到處執行。為什麼 Java 能夠吹出這種牛批的口號來?核心就是 JVM。我們知道,計算機應用程式和硬體之間會遮蔽很多細節,它們之間依靠作業系統完成排程和協調,大致的體系結構如下

image-20210907222642565

那麼加上 Java 應用、JVM 的體系結構會變為如下

image-20210907222654021

Java 是跨平臺的,已編譯的 Java 程式可以在任何帶有 JVM 的平臺上執行。你可以在 Windows 平臺下編寫程式碼,然後拿到 Linux 平臺下執行,該如何實現呢?

首先你需要在應用中編寫 Java 程式碼;

Eclipse 或者 javac 把 Java 程式碼編譯為 .class 檔案;

然後把你的 .class 檔案打成 .jar 檔案;

然後你的 .jar 檔案就能夠在 Windows 、Mac OS X、Linux 系統下執行了。不同的作業系統有不同的 JVM 實現,切換平臺時,不需要再次編譯你的 Java 程式碼了。這是特點五。

  • Java 能夠容易實現多執行緒

Java 是一門高階語言,高階語言會對使用者遮蔽很多底層實現細節。比如 Java 是如何實現多執行緒的。從作業系統的角度來說,實現多執行緒的方式主要有下面這幾種

在使用者空間中實現多執行緒

在核心空間中實現多執行緒

在使用者和核心空間中混合實現執行緒

而我認為 Java 應該是在 使用者空間 實現的多執行緒,核心是感知不到 Java 存在多執行緒機制的。這是特點六。

  • Java 具有高效能

我們編寫的程式碼,經過 javac 編譯器編譯稱為 位元組碼(bytecode),經過 JVM 內嵌的直譯器將位元組碼轉換為機器程式碼,這是解釋執行,這種轉換過程效率較低。但是部分 JVM 的實現比如 Hotspot JVM 都提供了 JIT(Just-In-Time) 編譯器,也就是通常所說的動態編譯?器,JIT 能夠在執行時將?熱點程式碼編譯機器碼,這種方式執行效率比較高,這是編譯執行。所以 Java 不僅僅只是一種解釋執行的語言。這是特點七。

  • Java 語言具有健壯性

Java 的強型別機制、異常處理、垃圾的自動收集等是 Java 程式健壯性的重要保證。這也是 Java 與 C 語言的重要區別。這是特點八。

  • Java 很容易開發分散式專案

Java 語言支援 Internet 應用的開發,Java 中有 net api,它提供了用於網路應用程式設計的類庫,包括URL、URLConnection、Socket、ServerSocket等。Java的 RMI(遠端方法啟用)機制也是開發分散式應用的重要手段。這是特點九。

一個小例子說明一下面相過程和麵向物件的區別

一、程式導向

為了把大象裝進冰箱,需要3個過程。

思路:

1、把冰箱門開啟(得到開啟門的冰箱)。

2、把大象裝進去(開啟門後,得到裡面裝著大象的冰箱)。

3、把冰箱門關上(開啟門、裝好大象後,獲得關好門的冰箱)。

根據上面的思路,可以看到,每個過程都有一個階段性的目標,依次完成這些過程,就能把大象裝進冰箱。

二、物件導向

為了把大象裝進冰箱,需要做三個動作(或者叫行為)。每個動作有一個執行者,它就是物件。

思路:

1、冰箱,你給我把門開啟。

2、冰箱,你給我把大象裝進去(或者說,大象,你給我鑽到冰箱裡去)。

3、冰箱,你給我把門關上。

依次完成這些動作,你就可以把大象裝進去。

Java 開發環境

JDK

JDK(Java Development Kit)稱為 Java 開發包或 Java 開發工具,是一個編寫 Java 的 Applet 小程式和應用程式的程式開發環境。JDK是整個Java的核心,包括了Java執行環境(Java Runtime Environment),一些Java 工具Java 的核心類庫(Java API)

image-20210907222710633

我們可以認真研究一下這張圖,它幾乎包括了 Java 中所有的概念,我使用的是 jdk1.8,可以點進去 Description of Java Conceptual Diagram, 可以發現這裡麵包括了所有關於 Java 的描述。

Oracle 提供了兩種 Java 平臺的實現,一種是我們上面說的 JDK,Java 開發標準工具包,一種是 JRE,叫做Java Runtime Environment,Java 執行時環境。JDK 的功能要比 JRE 全很多。

JRE

JRE 是個執行環境,JDK 是個開發環境。因此寫 Java 程式的時候需要 JDK,而執行 Java 程式的時候就需要JRE。而 JDK 裡面已經包含了JRE,因此只要安裝了JDK,就可以編輯 Java 程式,也可以正常執行 Java 程式。但由於 JDK 包含了許多與執行無關的內容,佔用的空間較大,因此執行普通的 Java 程式無須安裝 JDK,而只需要安裝 JRE 即可。

Java 開發環境配置

這裡給大家推薦幾個 JDK 安裝和配置的部落格:

Windows 版本 JDK 的下載和安裝

mac 版本 JDK 的下載和安裝

Java 基本語法

在配置完 Java 開發環境,並下載 Java 開發工具(Eclipse、IDEA 等)後,就可以編寫 Java 程式了,因為這個教程是從頭梳理 Java 體系,所以有必要從基礎的概念開始談起。

資料型別

在 Java 中,資料型別只有四類八種

  • 整數型:byte、short、int、long

byte 也就是位元組,1 byte = 8 bits,byte 的預設值是 0 ;

short 佔用兩個位元組,也就是 16 位,1 short = 16 bits,它的預設值也是 0 ;

int 佔用四個位元組,也就是 32 位,1 int = 32 bits,預設值是 0 ;

long 佔用八個位元組,也就是 64 位,1 long = 64 bits,預設值是 0L;

所以整數型的佔用位元組大小空間為 long > int > short > byte

  • 浮點型

浮點型有兩種資料型別:float 和 double

float 是單精度浮點型,佔用 4 位,1 float = 32 bits,預設值是 0.0f;

double 是雙精度浮點型,佔用 8 位,1 double = 64 bits,預設值是 0.0d;

  • 字元型

字元型就是 char,char 型別是一個單一的 16 位 Unicode 字元,最小值是 \u0000 (也就是 0 ),最大值是 \uffff (即為 65535),char 資料型別可以儲存任何字元,例如 char a = 'A'。

  • 布林型

布林型指的就是 boolean,boolean 只有兩種值,true 或者是 false,只表示 1 位,預設值是 false。

以上 x 位都指的是在記憶體中的佔用。

image-20210907222746854

基礎語法

  • 大小寫敏感:Java 是對大小寫敏感的語言,例如 Hello 與 hello 是不同的,這其實就是 Java 的字串表示方式。
  • 類名:對於所有的類來說,首字母應該大寫,例如 MyFirstClass
  • 包名:包名應該儘量保證小寫,例如 my.first.package
  • 方法名:方法名首字母需要小寫,後面每個單詞字母都需要大寫,例如 myFirstMethod()

運算子

運算子不只 Java 中有,其他語言也有運算子,運算子是一些特殊的符號,主要用於數學函式、一些型別的賦值語句和邏輯比較方面,我們就以 Java 為例,來看一下運算子。

  • 賦值運算子

賦值運算子使用操作符 = 來表示,它的意思是把 = 號右邊的值複製給左邊,右邊的值可以是任何常數、變數或者表示式,但左邊的值必須是一個明確的,已經定義的變數。比如 int a = 4

但是對於物件來說,複製的不是物件的值,而是物件的引用,所以如果說將一個物件複製給另一個物件,實際上是將一個物件的引用賦值給另一個物件

  • 算數運算子

算數運算子就和數學中的數值計算差不多,主要有

image-20210907222804980

算數運算子需要注意的就是優先順序問題,當一個表示式中存在多個操作符時,操作符的優先順序順序就決定了計算順序,最簡單的規則就是先乘除後加減,() 的優先順序最高,沒必要記住所有的優先順序順序,不確定的直接用 () 就可以了。

  • 自增、自減運算子

這個就不文字解釋了,解釋不如直接看例子明白

int a = 5;
b = ++a;
c = a++;
  • 比較運算子

比較運算子用於程式中的變數之間,變數和自變數之間以及其他型別的資訊之間的比較。

比較運算子的運算結果是 boolean 型。當運算子對應的關係成立時,運算的結果為 true,否則為 false。比較運算子共有 6 個,通常作為判斷的依據用於條件語句中。

image-20210907222824045

  • 邏輯運算子

邏輯運算子主要有三種,與、或、非

image-20210907222837435

下面是邏輯運算子對應的 true/false 符號表

image-20210907222850103

  • 按位運算子

按位運算子用來操作整數基本型別中的每個位元位,也就是二進位制位。按位操作符會對兩個引數中對應的位執行布林代數運算,並最終生成一個結果。

image-20210907222905936

如果進行比較的雙方是數字的話,那麼進行比較就會變為按位運算。

按位與:按位進行與運算(AND),兩個運算元中位都為1,結果才為1,否則結果為0。需要首先把比較雙方轉換成二進位制再按每個位進行比較

按位或:按位進行或運算(OR),兩個位只要有一個為1,那麼結果就是1,否則就為0。

按位異或:按位進行異或運算(XOR),如果位為0,結果是1,如果位為1,結果是0。

按位非:按位進行取反運算(NOT),兩個運算元的位中,相同則結果為0,不同則結果為1。

  • 移位運算子

移位運算子用來將運算元向某個方向(向左或者右)移動指定的二進位制位數。

image-20210907222926218

  • 三元運算子

三元運算子是類似 if...else... 這種的操作符,語法為:條件表示式?表示式 1:表示式 2。問號前面的位置是判斷的條件,判斷結果為布林型,為 true 時呼叫表示式 1,為 false 時呼叫表示式 2。

Java 執行控制流程

Java 中的控制流程其實和 C 一樣,在 Java 中,流程控制會涉及到包括 if-else、while、do-while、for、return、break 以及選擇語句 switch。下面以此進行分析。

條件語句

條件語句可根據不同的條件執行不同的語句。包括 if 條件語句與 switch 多分支語句。

if 條件語句

if 語句可以單獨判斷表示式的結果,表示表達的執行結果,例如:

int a = 10;
if(a > 10){
  return true;
}
return false;

if...else 條件語句

if 語句還可以與 else 連用,通常表現為 如果滿足某種條件,就進行某種處理,否則就進行另一種處理

int a = 10;
int b = 11;
if(a >= b){
  System.out.println("a >= b");
}else{
  System.out.println("a < b");
}

if 後的 () 內的表示式必須是 boolean 型的。如果為 true,則執行 if 後的複合語句;如果為 false,則執行 else 後的複合語句。

if...else if 多分支語句

上面中的 if...else 是單分支和兩個分支的判斷,如果有多個判斷條件,就需要使用 if...else if

int x = 40;
if(x > 60) {
  System.out.println("x的值大於60");
} else if (x > 30) {
  System.out.println("x的值大於30但小於60");
} else if (x > 0) {
  System.out.println("x的值大於0但小於30");
} else {
  System.out.println("x的值小於等於0");
}

switch case多分支語句

一種比 **if...else if ** 語句更優雅的方式是使用 switch 多分支語句,它的示例如下:

switch (week) {
  case 1:
    System.out.println("Monday");
    break;
  case 2:
    System.out.println("Tuesday");
    break;
  case 3:
    System.out.println("Wednesday");
    break;
  case 4:
    System.out.println("Thursday");
    break;
  case 5:
    System.out.println("Friday");
    break;
  case 6:
    System.out.println("Saturday");
    break;
  case 7:
    System.out.println("Sunday");
    break;
  default:
    System.out.println("No Else");
    break;
}

迴圈語句

迴圈語句就是在滿足一定的條件下反覆執行某一表示式的操作,直到滿足迴圈語句的要求。使用的迴圈語句主要有 **for、do...while() 、 while **。

while 迴圈語句

while 迴圈語句的迴圈方式為利用一個條件來控制是否要繼續反覆執行這個語句。while 迴圈語句的格式如下:

while(布林值){
  表示式
}

它的含義是,當 (布林值) 為 true 的時候,執行下面的表示式,布林值為 false 的時候,結束迴圈,布林值其實也是一個表示式,比如:

int a = 10;
while(a > 5){
  a--;
}

do...while 迴圈

while 與 do...while 迴圈的唯一區別是 do...while 語句至少執行一次,即使第一次的表示式為 false。而在 while 迴圈中,如果第一次條件為 false,那麼其中的語句根本不會執行。在實際應用中,while 要比 do...while 應用的更廣。它的一般形式如下:

int b = 10;
// do···while迴圈語句
do {
  System.out.println("b == " + b);
  b--;
} while(b == 1);

for 迴圈語句

for 迴圈是我們經常使用的迴圈方式,這種形式會在第一次迭代前進行初始化。它的形式如下:

for(初始化; 布林表示式; 步進){}

每次迭代前會測試布林表示式。如果獲得的結果是 false,就會執行 for 語句後面的程式碼;每次迴圈結束,會按照步進的值執行下一次迴圈。

逗號操作符

這裡不可忽略的一個就是逗號操作符,Java 裡唯一用到逗號操作符的就是 for 迴圈控制語句。在表示式的初始化部分,可以使用一系列的逗號分隔的語句;通過逗號操作符,可以在 for 語句內定義多個變數,但它們必須具有相同的型別。

for(int i = 1,j = i + 10;i < 5;i++, j = j * 2){}

for-each 語句

在 Java JDK 1.5 中還引入了一種更加簡潔的、方便對陣列和集合進行遍歷的方法,即 for-each 語句,例子如下:

int array[] = {7, 8, 9};

for (int arr : array) {
     System.out.println(arr);
}

跳轉語句

Java 語言中,有三種跳轉語句: break、continue 和 return

break 語句

break 語句我們在 switch 中已經見到了,它是用於終止迴圈的操作,實際上 break 語句在for、while、do···while迴圈語句中,用於強行退出當前迴圈,例如:

for(int i = 0;i < 10;i++){
	if(i == 5){
    break;
  }
}

continue 語句

continue 也可以放在迴圈語句中,它與 break 語句具有相反的效果,它的作用是用於執行下一次迴圈,而不是退出當前迴圈,還以上面的例子為主:

for(int i = 0;i < 10;i++){
  
  System.out.printl(" i = " + i );
	if(i == 5){
    System.out.printl("continue ... ");
    continue;
  }
}

return 語句

return 語句可以從一個方法返回,並把控制權交給呼叫它的語句。

public void getName() {
    return name;
}

物件導向

物件導向是學習 Java 一種非常重要的開發思想,但是物件導向並不是 Java 所特有的思想,這裡大家不要搞混了。

下面我們來探討物件導向的思想,物件導向的思想已經逐步取代了過程化的思想 --- 程式導向,Java 是物件導向的高階程式語言,面嚮物件語言具有如下特徵

  • 物件導向是一種常見的思想,比較符合人們的思考習慣;

  • 物件導向可以將複雜的業務邏輯簡單化,增強程式碼複用性;

  • 物件導向具有抽象、封裝、繼承、多型等特性。

物件導向的程式語言主要有:C++、Java、C#等。

所以必須熟悉物件導向的思想才能編寫出 Java 程式。

類也是一種物件

現在我們來認識一個物件導向的新的概念 --- 類,什麼是類,它就相當於是一系列物件的抽象,就比如書籍一樣,類相當於是書的封面,大多數物件導向的語言都使用 class 來定義類,它告訴你它裡面定義的物件都是什麼樣的,我們一般使用下面來定義類

class ClassName {
	// body;
}

程式碼段中涉及一個新的概念 // ,這個我們後面會說。上面,你宣告瞭一個 class 類,現在,你就可以使用 new 來建立這個物件

ClassName classname = new ClassName();

一般,類的命名遵循駝峰原則,它的定義如下:

駱駝式命名法(Camel-Case)又稱駝峰式命名法,是電腦程式編寫時的一套命名規則(慣例)。正如它的名稱 CamelCase 所表示的那樣,是指混合使用大小寫字母來構成變數和函式的名字。程式設計師們為了自己的程式碼能更容易的在同行之間交流,所以多采取統一的可讀性比較好的命名方式。

物件的建立

在 Java 中,萬事萬物都是物件。這句話相信你一定不陌生,儘管一切都看作是物件,但是你操縱的卻是一個物件的 引用(reference)。在這裡有一個很形象的比喻:你可以把車鑰匙和車看作是一組物件引用和物件的組合。當你想要開車的時候,你首先需要拿出車鑰匙點選開鎖的選項,停車時,你需要點選加鎖來鎖車。車鑰匙相當於就是引用,車就是物件,由車鑰匙來驅動車的加鎖和開鎖。並且,即使沒有車的存在,車鑰匙也是一個獨立存在的實體,也就是說,你有一個物件引用,但你不一定需要一個物件與之關聯,也就是

Car carKey;

這裡建立的只是引用,而並非物件,但是如果你想要使用 s 這個引用時,會返回一個異常,告訴你需要一個物件來和這個引用進行關聯。一種安全的做法是,在建立物件引用時同時把一個物件賦給它。

Car carKey = new Car();

在 Java 中,一旦建立了一個引用,就希望它能與一個新的物件進行關聯,通常使用 new 操作符來實現這一目的。new 的意思是,給我一個新物件,如果你不想相親,自己 new 一個物件就好了。祝你下輩子幸福。

屬性和方法

類一個最基本的要素就是有屬性和方法。

屬性也被稱為欄位,它是類的重要組成部分,屬性可以是任意型別的物件,也可以是基本資料型別。例如下

class A{
  int a;
  Apple apple;
}

類中還應該包括方法,方法表示的是 做某些事情的方式。方法其實就是函式,只不過 Java 習慣把函式稱為方法。這種叫法也體現了物件導向的概念。

方法的基本組成包括 方法名稱、引數、返回值和方法體, 下面是它的示例:

public int getResult(){
  // ...
  return 1;
}

其中,getResult 就是方法名稱、() 裡面表示方法接收的引數、return 表示方法的返回值。有一種特殊的引數型別 --- void 表示方法無返回值。{} 包含的程式碼段被稱為方法體。

構造方法

在 Java 中,有一種特殊的方法被稱為 構造方法,也被稱為建構函式、構造器等。在 Java 中,通過提供這個構造器,來確保每個物件都被初始化。構造方法只能在物件的建立時期呼叫一次,保證了物件初始化的進行。構造方法比較特殊,它沒有引數型別和返回值,它的名稱要和類名保持一致,並且構造方法可以有多個,下面是一個構造方法的示例:

class Apple {
  
  int sum;
  String color;
  
  public Apple(){}
  public Apple(int sum){}
  public Apple(String color){}
  public Apple(int sum,String color){}
  
}

上面定義了一個 Apple 類,你會發現這個 Apple 類沒有引數型別和返回值,並且有多個以 Apple 同名的方法,而且各個 Apple 的引數列表都不一樣,這其實是一種多型的體現,我們後面會說。在定義完成構造方法後,我們就能夠建立 Apple 物件了。

class createApple {

    public static void main(String[] args) {
        Apple apple1 = new Apple();
        Apple apple2 = new Apple(1);
        Apple apple3 = new Apple("red");
        Apple apple4 = new Apple(2,"color");

    }
}

如上面所示,我們定義了四個 Apple 物件,並呼叫了 Apple 的四種不同的構造方法,其中,不加任何引數的構造方法被稱為預設的構造方法,也就是

Apple apple1 = new Apple();

如果類中沒有定義任何構造方法,那麼 JVM 會為你自動生成一個構造方法,如下:

class Apple {    int sum;  String color;  }class createApple {    public static void main(String[] args) {        Apple apple1 = new Apple();    }}

上面程式碼不會發生編譯錯誤,因為 Apple 物件包含了一個預設的構造方法。

預設的構造方法也被稱為預設構造器或者無參構造器。

這裡需要注意一點的是,即使 JVM 會為你預設新增一個無參的構造器,但是如果你手動定義了任何一個構造方法,JVM 就不再為你提供預設的構造器,你必須手動指定,否則會出現編譯錯誤

image-20210907222946943

顯示的錯誤是,必須提供 Apple 帶有 int 引數的建構函式,而預設的無參建構函式沒有被允許使用。

方法過載

在 Java 中一個很重要的概念是方法的過載,它是類名的不同表現形式。我們上面說到了建構函式,其實建構函式也是過載的一種。另外一種就是方法的過載

public class Apple {    int sum;    String color;    public Apple(){}    public Apple(int sum){}        public int getApple(int num){        return 1;    }        public String getApple(String color){        return "color";    }}

如上面所示,就有兩種過載的方式,一種是 Apple 建構函式的過載,一種是 getApple 方法的過載。

但是這樣就涉及到一個問題,要是有幾個相同的名字,Java 如何知道你呼叫的是哪個方法呢?這裡記住一點即可,每個過載的方法都有獨一無二的引數列表。其中包括引數的型別、順序、引數數量等,滿足一種一個因素就構成了過載的必要條件。

請記住下面過載的條件

  • 方法名稱必須相同。

  • 引數列表必須不同(個數不同、或型別不同、引數型別排列順序不同等)。

  • 方法的返回型別可以相同也可以不相同。

  • 僅僅返回型別不同不足以成為方法的過載。

  • 過載是發生在編譯時的,因為編譯器可以根據引數的型別來選擇使用哪個方法。

方法的重寫

方法的重寫與過載雖然名字很相似,但卻完全是不同的東西。方法重寫的描述是對子類和父類之間的。而過載指的是同一類中的。例如如下程式碼

class Fruit { 	public void eat(){    System.out.printl('eat fruit');  }}class Apple extends Fruit{    @Override  public void eat(){    System.out.printl('eat apple');  }}

上面這段程式碼描述的就是重寫的程式碼,你可以看到,子類 Apple 中的方法和父類 Fruit 中的方法同名,所以,我們能夠推斷出重寫的原則

  • 重寫的方法必須要和父類保持一致,包括返回值型別,方法名,引數列表 也都一樣。
  • 重寫的方法可以使用 @Override 註解來標識
  • 子類中重寫方法的訪問許可權不能低於父類中方法的訪問許可權。

初始化

類的初始化

上面我們建立出來了一個 Car 這個物件,其實在使用 new 關鍵字建立一個物件的時候,其實是呼叫了這個物件無引數的構造方法進行的初始化,也就是如下這段程式碼

class Car{  public Car(){}}

這個無引數的建構函式可以隱藏,由 JVM 自動新增。也就是說,建構函式能夠確保類的初始化。

成員初始化

Java 會盡量保證每個變數在使用前都會獲得初始化,初始化涉及兩種初始化。

  • 一種是編譯器預設指定的欄位初始化,基本資料型別的初始化

    image-20210907223003059

    一種是其他物件型別的初始化,String 也是一種物件,物件的初始值都為 null ,其中也包括基本型別的包裝類。

  • 一種是指定數值的初始化,例如:

int a = 11

也就是說, 指定 a 的初始化值不是 0 ,而是 11。其他基本型別和物件型別也是一樣的。

構造器初始化

可以利用構造器來對某些方法和某些動作進行初始化,確定初始值,例如

public class Counter{
  int i;
  public Counter(){
    i = 11;
  }
}

利用建構函式,能夠把 i 的值初始化為 11。

初始化順序

首先先來看一下有哪些需要探討的初始化順序

  • 靜態屬性:static 開頭定義的屬性

  • 靜態方法塊: static {} 包起來的程式碼塊

  • 普通屬性: 非 static 定義的屬性

  • 普通方法塊: {} 包起來的程式碼塊

  • 建構函式: 類名相同的方法

  • 方法: 普通方法

public class LifeCycle {
    // 靜態屬性
    private static String staticField = getStaticField();
    // 靜態方法塊
    static {
        System.out.println(staticField);
        System.out.println("靜態方法塊初始化");
    }
    // 普通屬性
    private String field = getField();
    // 普通方法塊
    {
        System.out.println(field);
    }
    // 建構函式
    public LifeCycle() {
        System.out.println("建構函式初始化");
    }

    public static String getStaticField() {
        String statiFiled = "Static Field Initial";
        return statiFiled;
    }

    public static String getField() {
        String filed = "Field Initial";
        return filed;
    }   
    // 主函式
    public static void main(String[] argc) {
        new LifeCycle();
    }
}

這段程式碼的執行結果就反應了它的初始化順序

輸出結果: 靜態屬性初始化、靜態方法塊初始化、普通屬性初始化、普通方法塊初始化、建構函式初始化

陣列初始化

陣列是相同型別的、用一個識別符號名稱封裝到一起的一個物件序列或基本型別資料序列。陣列是通過方括號下標操作符 [] 來定義使用。

一般陣列是這麼定義的

int[] a1;

//或者

int a1[];

兩種格式的含義是一樣的。

  • 直接給每個元素賦值 : int array[4] = {1,2,3,4};
  • 給一部分賦值,後面的都為 0 : int array[4] = {1,2};
  • 由賦值引數個數決定陣列的個數 : int array[] = {1,2};

可變引數列表

Java 中一種陣列冷門的用法就是可變引數 ,可變引數的定義如下

public int add(int... numbers){
  int sum = 0;
  for(int num : numbers){
    sum += num;
  }
  return sum;
}

然後,你可以使用下面這幾種方式進行可變引數的呼叫

add();  // 不傳引數
add(1);  // 傳遞一個引數
add(2,1);  // 傳遞多個引數
add(new int[] {1, 3, 2});  // 傳遞陣列

物件的銷燬

雖然 Java 語言是基於 C++ 的,但是它和 C/C++ 一個重要的特徵就是不需要手動管理物件的銷燬工作。在著名的一書 《深入理解 Java 虛擬機器》中提到一個觀點

image-20210907223020842

在 Java 中,我們不再需要手動管理物件的銷燬,它是由 Java 虛擬機器(JVM)進行管理和銷燬的。雖然我們不需要手動管理物件,但是你需要知道 物件作用域 這個概念。

物件作用域

許多數語言都有作用域(scope) 這個概念。作用域決定了其內部定義的變數名的可見性和生命週期。在 C、C++ 和 Java 中,作用域通常由 {} 的位置來決定,這也是我們常說的程式碼塊。例如:

{  int a = 11;  {    int b = 12;  }}

a 變數會在兩個 {} 作用域內有效,而 b 變數的值只能在它自己的 {} 內有效。

雖然存在作用域,但是不允許這樣寫

{
  int x = 11;
  {
    int x = 12;
  }
}

這種寫法在 C/C++ 中是可以的,但是在 Java 中不允許這樣寫,因為 Java 設計者認為這樣寫會導致程式混亂。

this 和 super

this 和 super 都是 Java 中的關鍵字。

this 表示的當前物件,this 可以呼叫方法、呼叫屬性和指向物件本身。this 在 Java 中的使用一般有三種:指向當前物件

public class Apple {

    int i = 0;

    Apple eatApple(){
        i++;
        return this;
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        apple.eatApple().eatApple();
    }
}

這段程式碼比較精妙,精妙在哪呢?我一個 eatApple() 方法竟然可以呼叫多次,你在後面還可以繼續呼叫,這就很神奇了,為啥呢?其實就是 this 在作祟了,我在 eatApple 方法中加了一個 return this 的返回值,也就是說哪個物件呼叫 eatApple 方法都能返回物件的自身。

this 還可以修飾屬性,最常見的就是在構造方法中使用 this ,如下所示

public class Apple {

    private int num;
    
    public Apple(int num){
        this.num = num;
    }

    public static void main(String[] args) {
        new Apple(10);
    }
}

main 方法中傳遞了一個 int 值為 10 的引數,它表示的就是蘋果的數量,並把這個數量賦給了 num 全域性變數。所以 num 的值現在就是 10。

this 還可以和建構函式一起使用,充當一個全域性關鍵字的效果

public class Apple {

    private int num;
    private String color;

    public Apple(int num){
        this(num,"紅色");
    }
    
    public Apple(String color){
        this(1,color);
    }

    public Apple(int num, String color) {
        this.num = num;
        this.color = color;
    }
    
}

你會發現上面這段程式碼使用的不是 this, 而是 this(引數)。它相當於呼叫了其他構造方法,然後傳遞引數進去。這裡注意一點:this() 必須放在構造方法的第一行,否則編譯不通過。

image-20210907223035882

如果你把 this 理解為指向自身的一個引用,那麼 super 就是指向父類的一個引用。super 關鍵字和 this 一樣,你可以使用 super.物件 來引用父類的成員,如下:

public class Fruit {

    int num;
    String color;

    public void eat(){
        System.out.println("eat Fruit");
    }
}

public class Apple extends Fruit{

    @Override
    public void eat() {
        super.num = 10;
        System.out.println("eat " + num + " Apple");
    }

}

你也可以使用 super(引數) 來呼叫父類的建構函式,這裡不再舉例子了。

下面為你彙總了 this 關鍵字和 super 關鍵字的比較。

image-20210907223047478

訪問控制許可權

訪問控制許可權又稱為封裝,它是物件導向三大特性中的一種,我之前在學習過程中經常會忽略封裝,心想這不就是一個訪問修飾符麼,怎麼就是三大特性的必要條件了?後來我才知道,如果你信任的下屬對你隱瞞 bug,你是根本不知道的

訪問控制許可權其實最核心就是一點:只對需要的類可見。

Java中成員的訪問許可權共有四種,分別是 public、protected、default、private,它們的可見性如下

image-20210907223100206

繼承

繼承是所有 OOP(Object Oriented Programming) 語言和 Java 語言都不可或缺的一部分。只要我們建立了一個類,就隱式的繼承自 Object 父類,只不過沒有指定。如果你顯示指定了父類,那麼你繼承於父類,而你的父類繼承於 Object 類。

image-20210908223815666

繼承的關鍵字是 extends ,如上圖所示,如果使用了 extends 顯示指定了繼承,那麼我們可以說 Father 是父類,而 Son 是子類,用程式碼表示如下

class Father{}class Son extends Father{}

繼承雙方擁有某種共性的特徵

class Father{    public void feature(){    System.out.println("父親的特徵");  }}class Son extends Father {}

如果 Son 沒有實現自己的方法的話,那麼預設就是用的是父類的 feature 方法。如果子類實現了自己的 feature 方法,那麼就相當於是重寫了父類的 feature 方法,這也是我們上面提到的重寫了。

多型

多型指的是同一個行為具有多個不同表現形式。是指一個類例項(物件)的相同方法在不同情形下具有不同表現形式。封裝和繼承是多型的基礎,也就是說,多型只是一種表現形式而已。

如何實現多型?多型的實現具有三種充要條件

  • 繼承
  • 重寫父類方法
  • 父類引用指向子類物件

比如下面這段程式碼

public class Fruit {

    int num;

    public void eat(){
        System.out.println("eat Fruit");
    }
}

public class Apple extends Fruit{

    @Override
    public void eat() {
        super.num = 10;
        System.out.println("eat " + num + " Apple");
    }

    public static void main(String[] args) {
        Fruit fruit = new Apple();
        fruit.eat();
    }
}

你可以發現 main 方法中有一個很神奇的地方,Fruit fruit = new Apple(),Fruit 型別的物件竟然指向了 Apple 物件的引用,這其實就是多型 -> 父類引用指向子類物件,因為 Apple 繼承於 Fruit,並且重寫了 eat 方法,所以能夠表現出來多種狀態的形式。

組合

組合其實不難理解,就是將物件引用置於新類中即可。組合也是一種提高類的複用性的一種方式。如果你想讓類具有更多的擴充套件功能,你需要記住一句話多用組合,少用繼承

public class SoccerPlayer {
    
    private String name;
    private Soccer soccer;
    
}

public class Soccer {
    
    private String soccerName;    
}

程式碼中 SoccerPlayer 引用了 Soccer 類,通過引用 Soccer 類,來達到呼叫 soccer 中的屬性和方法。

組合和繼承是有區別的,它們的主要區別如下。

image-20210907223116239

關於繼承和組合孰優孰劣的爭論沒有結果,只要發揮各自的長處和優點即可,一般情況下,組合和繼承也是一對可以連用的好兄弟。

代理

除了繼承和組合外,另外一種值得探討的關係模型稱為 代理。代理的大致描述是,A 想要呼叫 B 類的方法,A 不直接呼叫,A 會在自己的類中建立一個 B 物件的代理,再由代理呼叫 B 的方法。例如下面程式碼:

public class Destination {

    public void todo(){
        System.out.println("control...");
    }
}

public class Device {

    private String name;
    private Destination destination;
    private DeviceController deviceController;

    public void control(Destination destination){
        destination.todo();
    }

}

public class DeviceController {

    private Device name;
    private Destination destination;

    public void control(Destination destination){
        destination.todo();
    }
}

關於深入理解代理的文章,可以參考

動態代理竟然如此簡單!

深入理解代理

向上轉型

向上轉型代表了父類與子類之間的關係,其實父類和子類之間不僅僅有向上轉型,還有向下轉型,它們的轉型後的範圍不一樣

  • 向上轉型:通過子類物件(小範圍)轉化為父類物件(大範圍),這種轉換是自動完成的,不用強制。
  • 向下轉型 : 通過父類物件(大範圍)例項化子類物件(小範圍),這種轉換不是自動完成的,需要強制指定。

static

static 是 Java 中的關鍵字,它的意思是 靜態的,static 可以用來修飾成員變數和方法,static 用在沒有建立物件的情況下呼叫 方法/變數。

  • 用 static 宣告的成員變數為靜態成員變數,也成為類變數。類變數的生命週期和類相同,在整個應用程式執行期間都有效。
static String name = "cxuan";
  • 使用 static 修飾的方法稱為靜態方法,靜態方法能夠直接使用類名.方法名 進行呼叫。由於靜態方法不依賴於任何物件就可以直接訪問,因此對於靜態方法來說,是沒有 this 關鍵字的,例項變數都會有 this 關鍵字。在靜態方法中不能訪問類的非靜態成員變數和非靜態方法,
static void printMessage(){  System.out.println("cxuan is writing the article");}

static 除了修飾屬性和方法外,還有靜態程式碼塊 的功能,可用於類的初始化操作。進而提升程式的效能。

public class StaicBlock {    static{        System.out.println("I'm A static code block");    }}

由於靜態程式碼塊隨著類的載入而執行,因此,很多時候會將只需要進行一次的初始化操作放在 static 程式碼塊中進行。

關於 static 關鍵字的深入理解用法,可以參考筆者的這篇文章 一個 static 還能難得住我?強烈建議學完 Java 基礎之後閱讀。

final

final 的意思是最後的、最終的,它可以修飾類、屬性和方法

  • final 修飾類時,表明這個類不能被繼承。final 類中的成員變數可以根據需要設為 final,但是要注意 final 類中的所有成員方法都會被隱式地指定為 final 方法。
class Parent {}
final class Person extends Parent{} //可以繼承Parent類
class Child extends Person{} //不能繼承Person類
  • final 修飾方法時,表明這個方法不能被任何子類重寫,因此,如果只有在想明確禁止該方法在子類中被覆蓋的情況下才將方法設定為 final。
class Parent {
	// final修飾的方法,不可以被覆蓋,但可以繼承使用
    public final void method1(){}  //這個方法不可以重寫
    public void method2(){}
}
class Child extends Parent {
	//可以重寫method2方法
	public final void method2(){}
}
  • final 修飾變數分為兩種情況,一種是修飾基本資料型別,表示資料型別的值不能被修改;一種是修飾引用型別,表示對其初始化之後便不能再讓其指向另一個物件。
final int i = 20;
i = 30; //賦值報錯,final修飾的變數只能賦值一次

在 Java 中,與 final 、finally 和 finalize 併成為最後的三兄弟。關於這三個關鍵字的詳細用法,你可以參考閱讀作者的這篇文章 看完這篇 final、finally 和 finalize 和麵試官扯皮就沒問題了

介面和抽象類

介面

介面相當於就是對外的一種約定和標準,這裡拿作業系統舉例子,為什麼會有作業系統?就會為了遮蔽軟體的複雜性和硬體的簡單性之間的差異,為軟體提供統一的標準。

在 Java 語言中,介面是由 interface 關鍵字來表示的,比如我們可以向下面這樣定義一個介面

public interface CxuanGoodJob {}

比如我們定義了一個 CxuanGoodJob 的介面,然後你就可以在其內部定義 cxuan 做的好的那些事情,比如 cxuan 寫的文章不錯。

public interface CxuanGoodJob {

    void writeWell();
}

這裡隱含了一些介面的特徵:

  • interface 介面是一個完全抽象的類,他不會提供任何方法的實現,只是會進行方法的定義。
  • 介面中只能使用兩種訪問修飾符,一種是 public,它對整個專案可見;一種是 default 預設值,它只具有包訪問許可權。
  • 介面只提供方法的定義,介面沒有實現,但是介面可以被其他類實現。也就是說,實現介面的類需要提供方法的實現,實現介面使用 implements 關鍵字來表示,一個介面可以有多個實現。
class CXuanWriteWell implements CxuanGoodJob{

    @Override
    public void writeWell() {
        System.out.println("Cxuan write Java is vary well");
    }
}
  • 介面不能被例項化,所以介面中不能有任何構造方法,你定義構造方法編譯會出錯。
  • 介面的實現比如實現介面的全部方法,否則必須定義為抽象類,這就是我們下面要說的內容

抽象類

抽象類是一種抽象能力弱於介面的類,在 Java 中,抽象類使用 abstract 關鍵字來表示。如果把介面形容為狗這個物種,那麼抽象類可以說是毛髮是白色、小體的品種,而實現類可以是具體的類,比如說是博美、泰迪等。你可以像下面這樣定義抽象類

public interface Dog {

    void FurColor();

}

abstract class WhiteDog implements Dog{

    public void FurColor(){
        System.out.println("Fur is white");
    }

    abstract void SmallBody();
}

在抽象類中,具有如下特徵

  • 如果一個類中有抽象方法,那麼這個類一定是抽象類,也就是說,使用關鍵字 abstract 修飾的方法一定是抽象方法,具有抽象方法的類一定是抽象類。實現類方法中只有方法具體的實現。

  • 抽象類中不一定只有抽象方法,抽象類中也可以有具體的方法,你可以自己去選擇是否實現這些方法。

  • 抽象類中的約束不像介面那麼嚴格,你可以在抽象類中定義 構造方法、抽象方法、普通屬性、方法、靜態屬性和靜態方法

  • 抽象類和介面一樣不能被例項化,例項化只能例項化具體的類

異常

異常是程式經常會出現的,發現錯誤的最佳時機是在編譯階段,也就是你試圖在執行程式之前。但是,在編譯期間並不能找到所有的錯誤,有一些 NullPointerExceptionClassNotFoundException 異常在編譯期找不到,這些異常是 RuntimeException 執行時異常,這些異常往往在執行時才能被發現。

我們寫 Java 程式經常會出現兩種問題,一種是 java.lang.Exception ,一種是 java.lang.Error,都用來表示出現了異常情況,下面就針對這兩種概念進行理解。

認識 Exception

Exception 位於 java.lang 包下,它是一種頂級介面,繼承於 Throwable 類,Exception 類及其子類都是 Throwable 的組成條件,是程式出現的合理情況。

在認識 Exception 之前,有必要先了解一下什麼是 Throwable

什麼是 Throwable

Throwable 類是 Java 語言中所有錯誤(errors)異常(exceptions)的父類。只有繼承於 Throwable 的類或者其子類才能夠被丟擲,還有一種方式是帶有 Java 中的 @throw 註解的類也可以丟擲。

Java規範中,對非受查異常和受查異常的定義是這樣的:

The unchecked exception classes are the run-time exception classes and the error classes.

The checked exception classes are all exception classes other than the unchecked exception classes. That is, the checked exception classes are Throwable and all its subclasses other than RuntimeException and its subclasses and Errorand its subclasses.

也就是說,除了 RuntimeException 和其子類,以及error和其子類,其它的所有異常都是 checkedException

那麼,按照這種邏輯關係,我們可以對 Throwable 及其子類進行歸類分析

image-20210907223136780

可以看到,Throwable 位於異常和錯誤的最頂層,我們檢視 Throwable 類中發現它的方法和屬性有很多,我們只討論其中幾個比較常用的

// 返回丟擲異常的詳細資訊
public string getMessage();
public string getLocalizedMessage();

//返回異常發生時的簡要描述
public public String toString();
  
// 列印異常資訊到標準輸出流上
public void printStackTrace();
public void printStackTrace(PrintStream s);
public void printStackTrace(PrintWriter s)

// 記錄棧幀的的當前狀態
public synchronized Throwable fillInStackTrace();

此外,因為 Throwable 的父類也是 Object,所以常用的方法還有繼承其父類的getClass()getName() 方法。

常見的 Exception

下面我們回到 Exception 的探討上來,現在你知道了 Exception 的父類是 Throwable,並且 Exception 有兩種異常,一種是 RuntimeException ;一種是 CheckedException,這兩種異常都應該去捕獲

下面列出了一些 Java 中常見的異常及其分類,這塊面試官也可能讓你舉出幾個常見的異常情況並將其分類

RuntimeException

image-20210907223603308

UncheckedException

image-20210907223632991

與 Exception 有關的 Java 關鍵字

那麼 Java 中是如何處理這些異常的呢?在 Java 中有這幾個關鍵字 throws、throw、try、finally、catch 下面我們分別來探討一下

throws 和 throw

在 Java 中,異常也就是一個物件,它能夠被程式設計師自定義丟擲或者應用程式丟擲,必須藉助於 throwsthrow 語句來定義丟擲異常。

throws 和 throw 通常是成對出現的,例如

static void cacheException() throws Exception{  throw new Exception();}

throw 語句用在方法體內,表示丟擲異常,由方法體內的語句處理。
throws 語句用在方法宣告後面,表示再丟擲異常,由該方法的呼叫者來處理。

throws 主要是宣告這個方法會丟擲這種型別的異常,使它的呼叫者知道要捕獲這個異常。
throw 是具體向外拋異常的動作,所以它是丟擲一個異常例項。

try 、finally 、catch

這三個關鍵字主要有下面幾種組合方式 try...catch 、try...finally、try...catch...finally

try...catch 表示對某一段程式碼可能丟擲異常進行的捕獲,如下

static void cacheException() throws Exception{

  try {
    System.out.println("1");
  }catch (Exception e){
    e.printStackTrace();
  }

}

try...finally 表示對一段程式碼不管執行情況如何,都會走 finally 中的程式碼

static void cacheException() throws Exception{
  for (int i = 0; i < 5; i++) {
    System.out.println("enter: i=" + i);
    try {
      System.out.println("execute: i=" + i);
      continue;
    } finally {
      System.out.println("leave: i=" + i);
    }
  }
}

try...catch...finally 也是一樣的,表示對異常捕獲後,再走 finally 中的程式碼邏輯。

什麼是 Error

Error 是程式無法處理的錯誤,表示執行應用程式中較嚴重問題。大多數錯誤與程式碼編寫者執行的操作無關,而表示程式碼執行時 JVM(Java 虛擬機器)出現的問題。這些錯誤是不可檢查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程式執行時不允許出現的狀況,比如 OutOfMemoryErrorStackOverflowError異常的出現會有幾種情況,這裡需要先介紹一下 Java 記憶體模型 JDK1.7。

image-20210907223649891

其中包括兩部分,由所有執行緒共享的資料區和執行緒隔離的資料區組成,在上面的 Java 記憶體模型中,只有程式計數器是不會發生 OutOfMemoryError 情況的區域,程式計數器控制著計算機指令的分支、迴圈、跳轉、異常處理和執行緒恢復,並且程式計數器是每個執行緒私有的。

什麼是執行緒私有:表示的就是各條執行緒之間互不影響,獨立儲存的記憶體區域。

如果應用程式執行的是 Java 方法,那麼這個計數器記錄的就是虛擬機器位元組碼指令的地址;如果正在執行的是 Native 方法,這個計數器值則為空(Undefined)

除了程式計數器外,其他區域:方法區(Method Area)虛擬機器棧(VM Stack)本地方法棧(Native Method Stack)堆(Heap) 都是可能發生 OutOfMemoryError 的區域。

  • 虛擬機器棧:如果執行緒請求的棧深度大於虛擬機器棧所允許的深度,將會出現 StackOverflowError 異常;如果虛擬機器動態擴充套件無法申請到足夠的記憶體,將出現 OutOfMemoryError

  • 本地方法棧和虛擬機器棧一樣

  • 堆:Java 堆可以處於物理上不連續,邏輯上連續,就像我們的磁碟空間一樣,如果堆中沒有記憶體完成例項分配,並且堆無法擴充套件時,將會丟擲 OutOfMemoryError。

  • 方法區:方法區無法滿足記憶體分配需求時,將丟擲 OutOfMemoryError 異常。

在 Java 中,你可以把異常理解為是一種能夠提高你程式健壯性的機制,它能夠讓你在編寫程式碼中注意這些問題,也可以說,如果你寫程式碼不會注意這些異常情況,你是無法成為一位硬核程式設計師的。

內部類

距今為止,我們瞭解的都是普通類的定義,那就是直接在 IDEA 中直接新建一個 class 。

image-20210907223703017

新建完成後,你就會擁有一個 class 檔案的定義,這種操作太簡單了,時間長了就會枯燥,我們年輕人多需要更新潮和騷氣的寫法,好吧,既然你提到了那就使用 內部類吧,這是一種有用而且騷氣的定義類的方式,內部類的定義非常簡單:可以將一個類的定義放在另一個類的內部,這就是內部類

內部類是一種非常有用的特性,定義在類內部的類,持有外部類的引用,但卻對其他外部類不可見,看起來就像是一種隱藏程式碼的機制,就和 弗蘭奇將軍 似的,弗蘭奇可以和弗蘭奇將軍進行通訊,但是外面的敵人卻無法直接攻擊到弗蘭奇本體。

image-20210907223713681

下面我們就來聊一聊建立內部類的方式。

如何定義內部類

下面是一種最簡單的內部類定義方式:

public class Parcel1 {
    public class Contents{
        private int value = 0;
    
        public int getValue(){
            return value;
        }
    }
}

這是一個很簡單的內部類定義方式,你可以直接把一個類至於另一個類的內部,這種定義 Contents 類的方式被稱為內部類。

那麼,就像上面程式碼所展示的,程式設計師該如何訪問 Contents 中的內容呢?

public class Parcel1 {

    public class Contents{
        private int value = 0;

        public int getValue(){
            return value;
        }
    }

    public Contents contents(){
        return new Contents();
    }

    public static void main(String[] args) {
        Parcel1 p1 = new Parcel1();
        Parcel1.Contents pc1 = p1.contents();
        System.out.println(pc1.getValue());
    }
}

就像上面程式碼看到的那樣,你可以寫一個方法來訪問 Contents,相當於指向了一個對 Contents 的引用,可以用外部類.內部類這種定義方式來建立一個對於內部類的引用,就像 Parcel1.Contents pc1 = p1.contents() 所展示的,而 pc1 相當於持有了對於內部類 Contents 的訪問許可權。

現在,我就有一個疑問,如果上面程式碼中的 contents 方法變為靜態方法,pc1 還能訪問到嗎?

編譯就過不去,那麼為什麼會訪問不到呢?請看接下來的分析。

連結到外部類

看到這裡,你還不明白為什麼要採用這種方式來編寫程式碼,好像只是為了裝 B ?或者你覺得重新定義一個類很麻煩,乾脆直接定義一個內部類得了,好像到現在並沒有看到這種定義內部類的方式為我們帶來的好處。請看下面這個例子

public class Parcel2 {

    private static int i = 11;

    public class Parcel2Inner {

        public Parcel2Inner(){
            i++;
        }

        public int getValue(){
            return i;
        }

    }

    public Parcel2Inner parcel2Inner(){
        return new Parcel2Inner();
    }

    public static void main(String[] args) {
        Parcel2 p2 = new Parcel2();
        for(int i = 0;i < 5;i++){
            p2.parcel2Inner();
        }
        System.out.println("p2.i = " + p2.i);
    }
}

輸出結果: 16

當你建立了一個內部類物件的時候,此物件就與它的外圍物件產生了某種聯絡,如上面程式碼所示,內部類Parcel2Inner 是可以訪問到 Parcel2 中的 i 的值,也可以對這個值進行修改。

那麼,問題來了,如何建立一個內部類的物件呢?程式設計師不能每次都寫一個方法返回外部類的物件吧?看下面程式碼:

public class Parcel3 {

    public class Contents {

        public Parcel3 dotThis(){
            return Parcel3.this;
        }

        public String toString(){
            return "Contents";
        }
    }

    public Parcel3 contents(){
        return new Contents().dotThis();
    }

    public String toString(){
        return "Parcel3";
    }

    public static void main(String[] args) {
        Parcel3 pc3 = new Parcel3();
        Contents c = pc3.new Contents();
        Parcel3 parcel3 = pc3.contents();
        System.out.println(pc3);
        System.out.println(c);
        System.out.println(parcel3);
    }
}

輸出:
Parcel3
Contents
Parcel3

如上面程式碼所示,Parcel3 內定義了一個內部類 Contents,內部類中定義了一個方法 dotThis(),這個方法的返回值為外部類的物件,在外部類中有一個 contents() 方法,這個方法返回的還是外部類的引用。

內部類與向上轉型

本文到現在所展示的都是本類持有內部類的訪問許可權,那麼,與此類無關的類是如何持有此類內部類的訪問許可權呢?而且內部類與向上轉型到底有什麼關係呢?

public interface Animal {

    void eat();
}

public class Parcel4 {

    private class Dog implements Animal {

        @Override
        public void eat() {
            System.out.println("啃骨頭");
        }
    }

    public Animal getDog(){
        return new Dog();
    }

    public static void main(String[] args) {
        Parcel4 p4 = new Parcel4();
        //Animal dog = p4.new Dog();
        Animal dog = p4.getDog();
        dog.eat();
    }
}

輸出: 啃骨頭

這個輸出大家肯定都知道了,Dog 是由 private 修飾的,按說非本類的任何一個類都是訪問不到,那麼為什麼能夠訪問到呢? 仔細想一下便知,因為 Parcel4 是 public 的,而 Parcel4 是可以訪問自己的內部類的,那麼 Animal 也可以訪問到 Parcel4 的內部類也就是 Dog 類,並且 Dog 類是實現了 Animal 介面,所以 getDog() 方法返回的也是 Animal 類的子類,從而達到了向上轉型的目的,讓程式碼更美妙。

定義在方法中和任意作用域內部的類

上面所展示的一些內部類的定義都是普通內部類的定義,如果我想在一個方法中或者某個作用域內定義一個內部類該如何編寫呢?

你可能會考慮這幾種定義的思路:

  1. 我想定義一個內部類,它實現了某個介面,我定義內部類是為了返回介面的引用
  2. 我想解決某個問題,並且這個類又不希望它是公共可用的,顧名思義就是封裝起來,不讓別人用
  3. 因為懶...

以下是幾種定義內部類的方式:

  • 一個在方法中定義的類(區域性內部類)
  • 一個定義在作用域內的類,這個作用域在方法的內部(成員內部類)
  • 一個實現了介面的匿名類(匿名內部類)
  • 一個匿名類,它擴充套件了非預設構造器的類
  • 一個匿名類,執行欄位初始化操作
  • 一個匿名類,它通過例項初始化實現構造
  • 定義在方法內部的類又被稱為區域性內部類
public class Parcel5 {  private Destination destination(String s){    class PDestination implements Destination{      String label;      public PDestination(String whereTo){        label = whereTo;      }      @Override      public String readLabel() {        return label;      }    }    return new PDestination(s);  }  public static void main(String[] args) {    Parcel5 p5 = new Parcel5();    Destination destination = p5.destination("China");    System.out.println(destination.readLabel());  }}

輸出 : China

如上面程式碼所示,你可以在編寫一個方法的時候,在方法中插入一個類的定義,而內部類中的屬性是歸類所有的,我在寫這段程式碼的時候很好奇,內部類的執行過程是怎樣的,Debugger走了一下發現當執行到p5.destination("China") 的時候,先會執行 return new PDestination(s),然後才會走 PDestination 的初始化操作,這與我們對其外部類的初始化方式是一樣的,只不過這個方法提供了一個訪問內部類的入口而已。

區域性內部類的定義不能有訪問修飾符

  • 一個定義在作用域內的類,這個作用域在方法的內部
public class Parcel6 {
  // 吃椰子的方法
  private void eatCoconut(boolean flag){
    // 如果可以吃椰子的話
    if(flag){
      class Coconut {
        private String pipe;

        public Coconut(String pipe){
          this.pipe = pipe;
        }

        // 喝椰子汁的方法
        String drinkCoconutJuice(){
          System.out.println("喝椰子汁");
          return pipe;
        }
      }
      // 提供一個吸管,可以喝椰子汁
      Coconut coconut = new Coconut("用吸管喝");
      coconut.drinkCoconutJuice();
    }

    /**
             * 如果可以吃椰子的話,你才可以用吸管喝椰子汁
             * 如果不能接到喝椰子汁的指令的話,那麼你就不能喝椰子汁
             */
    // Coconut coconut = new Coconut("用吸管喝");
    // coconut.drinkCoconutJuice();
  }

  public static void main(String[] args) {
    Parcel6 p6 = new Parcel6();
    p6.eatCoconut(true);
  }
}

輸出: 喝椰子汁

如上面程式碼所示,只有程式設計師告訴程式,現在我想吃一個椰子,當程式接收到這條命令的時候,它回答好的,馬上為您準備一個椰子,並提供一個吸管讓您可以喝到新鮮的椰子汁。程式設計師如果不想吃椰子的話,那麼程式就不會為你準備椰子,更別說讓你喝椰子汁了。

  • 一個實現了匿名介面的類

我們都知道介面是不能被例項化的,也就是說你不能 return 一個介面的物件,你只能是返回這個介面子類的物件,但是如果像下面這樣定義,你會不會表示懷疑呢?

public interface Contents {

    int getValue();
}

public class Parcel7 {

    private Contents contents(){
        return new Contents() {

            private int value = 11;

            @Override
            public int getValue() {
                return value;
            }
        };
    }

    public static void main(String[] args) {
        Parcel7 p7 = new Parcel7();
        System.out.println(p7.contents().getValue());
    }
}

輸出 : 11

為什麼能夠返回一個介面的定義?而且還有 {},這到底是什麼鬼? 這其實是一種匿名內部類的寫法,其實和上面所講的內部類和向上轉型是相似的。也就是說匿名內部類返回的 new Contents() 其實也是屬於 Contents 的一個實現類,只不過這個實現類的名字被隱藏掉了,能用如下的程式碼示例來進行轉換:

public class Parcel7b {

    private class MyContents implements Contents {

        private int value = 11;

        @Override
        public int getValue() {
            return 11;
        }
    }

    public Contents contents(){
        return new MyContents();
    }

    public static void main(String[] args) {
        Parcel7b parcel7b = new Parcel7b();
        System.out.println(parcel7b.contents().getValue());
    }
}

輸出的結果你應該知道了吧~! 你是不是覺得這段程式碼和 10.3 章節所表示的程式碼很一致呢?

  • 一個匿名類,它擴充套件了非預設構造器的類

如果你想返回一個帶有引數的構造器(非預設的構造器),該怎麼表示呢?

public class WithArgsConstructor {

    private int sum;

    public WithArgsConstructor(int sum){
        this.sum = sum;
    }

    public int sumAll(){
        return sum;
    }
}

public class Parcel8 {

    private WithArgsConstructor withArgsConstructor(int x){

        // 返回WithArgsConstructor帶引數的構造器,執行欄位初始化
        return new WithArgsConstructor(x){

            // 重寫sumAll方法,實現子類的執行邏輯
            @Override
            public int sumAll(){
                return super.sumAll() * 2;
            }
        };
    }

    public static void main(String[] args) {
        Parcel8 p8 = new Parcel8();
        System.out.println(p8.withArgsConstructor(10).sumAll());
    }
}

以上 WithArgsConstructor 中的程式碼很簡單,定義一個 sum 欄位,構造器進行初始化,sumAll 方法返回 sum 的值,Parcel8 中的 withArgsConstructor 方法直接返回 x 的值,但是在這個時候,你想在返回值上做一些特殊的處理,比如你想定義一個類,重寫 sumAll 方法,來實現子類的業務邏輯。 Java 程式設計思想198頁中說 程式碼中的“;”並不是表示內部類結束,而是表示式的結束,只不過這個表示式正巧包含了匿名內部類而已。

  • 一個匿名類,它能夠執行欄位初始化

上面程式碼確實可以進行初始化操作,不過是通過構造器執行欄位的初始化,如果沒有帶引數的構造器,還能執行初始化操作嗎? 這樣也是可以的。

public class Parcel9 {

    private Destination destination(String dest){
        return new Destination() {

            // 初始化賦值操作
            private String label = dest;

            @Override
            public String readLabel() {
                return label;
            }
        };
    }

    public static void main(String[] args) {
        Parcel9 p9 = new Parcel9();
        System.out.println(p9.destination("pen").readLabel());
    }
}

Java 程式設計思想 p198 中說如果給欄位進行初始化操作,那麼形參必須是 final 的,如果不是 final,編譯器會報錯,這部分提出來質疑,因為我不定義為final,編譯器也沒有報錯。我考慮過是不是 private 的問題,當我把 private 改為 public,也沒有任何問題。

我不清楚是中文版作者翻譯有問題,還是經過這麼多 Java 版本的升級排除了這個問題,我沒有考證原版是怎樣寫的,這裡還希望有知道的大牛幫忙解釋一下這個問題。

  • 一個匿名類,它通過例項初始化實現構造
public abstract class Base {

    public Base(int i){
        System.out.println("Base Constructor = " + i);
    }

    abstract void f();
}

public class AnonymousConstructor {

    private static Base getBase(int i){

        return new Base(i){
            {
                System.out.println("Base Initialization" + i);
            }

            @Override
            public void f(){
                System.out.println("AnonymousConstructor.f()方法被呼叫了");
            }
        };
    }

    public static void main(String[] args) {
        Base base = getBase(57);
        base.f();
    }
}

輸出:
Base Constructor = 57
Base Initialization 57
AnonymousConstructor.f()方法被呼叫了

這段程式碼和 "一個匿名類,它擴充套件了非預設構造器的類" 中屬於相同的範疇,都是通過構造器實現初始化的過程。

巢狀類

上面我們介紹了 6 種內部類定義的方式,現在我們來解決一下剛開始提出的疑問,為什麼 contents() 方法變成靜態的,會編譯出錯的原因:

Java程式設計思想 p201 頁講到:如果不需要內部類與其外圍類之前產生關係的話,就把內部類宣告為 static。這通常稱為巢狀類,也就是說巢狀類的內部類與其外圍類之前不會產生某種聯絡,也就是說內部類雖然定義在外圍類中,但是確實可以獨立存在的。巢狀類也被稱為靜態內部類。

靜態內部類意味著:

  1. 要建立巢狀類的物件,並不需要其外圍類的物件
  2. 不能從巢狀類的物件中訪問非靜態的外圍類物件

看下面程式碼

public class Parcel10 {

    private int value = 11;

    static int bValue = 12;

    // 靜態內部類
    private static class PContents implements Contents {

        // 編譯報錯,靜態內部類PContents中沒有叫value的欄位
        @Override
        public int getValue() {
            return value;
        }

        // 編譯不報錯,靜態內部類PContents可以訪問靜態屬性bValue
        public int f(){
            return bValue;
        }
    }

    // 普通內部類
    private class PDestination implements Destination {

        @Override
        public String readLabel() {
            return "label";
        }
    }

    // 編譯不報錯,因為靜態方法可以訪問靜態內部類
    public static Contents contents(){
        return new PContents();
    }

    // 編譯報錯,因為非靜態方法不能訪問靜態內部類
    public Contents contents2(){
        Parcel10 p10 = new Parcel10();
        return p10.new PContents();
    }

    // 編譯不報錯,靜態方法可以訪問非靜態內部類
    public static Destination destination(){
        Parcel10 p10 = new Parcel10();
        return p10.new PDestination();
    }

    // 編譯不報錯,非靜態方法可以訪問非靜態內部類
    public Destination destination2(){
        return new PDestination();
    }
}

由上面程式碼可以解釋,編譯出錯的原因是靜態方法不能直接訪問非靜態內部類,而需要通過建立外圍類的物件來訪問普通內部類

介面內部的類

納尼?介面內部只能定義方法,難道介面內部還能放一個類嗎?可以!

正常情況下,不能在介面內部放置任何程式碼,但是巢狀類作為介面的一部分,你放在介面中的任何類預設都是public 和 static 的。因為類是 static 的,只是將巢狀類置於介面的名稱空間內,這並不違反介面的規則,你甚至可以在內部類實現外部類的介面,不過一般我們不提倡這麼寫。

public interface InnerInterface {

    void f();

    class InnerClass implements InnerInterface {

        @Override
        public void f() {
            System.out.println("實現了介面的方法");
        }

        public static void main(String[] args) {
            new InnerClass().f();
        }
    }

    // 不能在介面中使用main方法,你必須把它定義在介面的內部類中
//    public static void main(String[] args) {}
}

輸出: 實現了介面的方法

內部類實現多重繼承

在 Java 中,類與類之間的關係通常是一對一的,也就是單項繼承原則,那麼在介面中,類與介面之間的關係是一對多的,也就是說一個類可以實現多個介面,而介面和內部類結合可以實現"多重繼承",並不是說用 extends 關鍵字來實現,而是介面和內部類的對多重繼承的模擬實現。

參考 chenssy 的文章 http://www.cnblogs.com/chenssy/p/3389027.html 已經寫的很不錯了。

public class Food {

    private class InnerFruit implements Fruit{
        void meakFruit(){
            System.out.println("種一個水果");
        }
    }

    private class InnerMeat implements Meat{
        void makeMeat(){
            System.out.println("煮一塊肉");
        }
    }

    public Fruit fruit(){
        return new InnerFruit();
    }

    public Meat meat(){
        return new InnerMeat();
    }

    public static void main(String[] args) {
        Food food = new Food();
        InnerFruit innerFruit = (InnerFruit)food.fruit();
        innerFruit.meakFruit();
        InnerMeat innerMeat = (InnerMeat) food.meat();
        innerMeat.makeMeat();
    }
}

輸出:
種一個水果
煮一塊肉

內部類的繼承

內部類之間也可以實現繼承,與普通類之間的繼承相似,不過不完全一樣。

public class BaseClass {

    class BaseInnerClass {

        public void f(){
            System.out.println("BaseInnerClass.f()");
        }
    }

    private void g(){
        System.out.println("BaseClass.g()");
    }
}
/**
 *  可以看到,InheritInner只是繼承自內部類BaseInnerClass,而不是外圍類
 *  但是預設的構造方式會報編譯錯誤,
 *  必須使用類似enclosingClassReference.super()才能編譯通過
 *  用來來說明內部類與外部類物件引用之間的關聯。
 *
 */
public class InheritInner extends BaseClass.BaseInnerClass{

    // 編譯出錯
//    public InheritInner(){}

    public InheritInner(BaseClass bc){
        bc.super();
    }

    @Override
    public void f() {
        System.out.println("InheritInner.f()");
    }

    /*
    * 加上@Override 會報錯,因為BaseInnerClass 中沒有g()方法
    * 這也是為什麼覆寫一定要加上Override註解的原因,否則預設是本類
    * 中持有的方法,會造成誤解,程式設計師以為g()方法是重寫過後的。
    @Override
    public void g(){
        System.out.println("InheritInner.g()");
    }*/

    public static void main(String[] args) {
        BaseClass baseClass = new BaseClass();
        InheritInner inheritInner = new InheritInner(baseClass);
        inheritInner.f();
    }
}

輸出:InheritInner.f()

內部類的覆蓋

關於內部類的覆蓋先來看一段程式碼:

public class Man {

    private ManWithKnowledge man;

    protected class ManWithKnowledge {

        public void haveKnowledge(){
            System.out.println("當今社會是需要知識的");
        }
    }

    // 我們想讓它輸出子類的haveKnowledge()方法
    public Man(){
        System.out.println("當我們有了一個孩子,我們更希望他可以當一個科學家,而不是網紅");
        new ManWithKnowledge().haveKnowledge();
    }
}

// 網紅
public class InternetCelebrity extends Man {

    protected class ManWithKnowledge {

        public void haveKnowledge(){
            System.out.println("網紅是當今社會的一種病態");
        }
    }

    public static void main(String[] args) {
        new InternetCelebrity();
    }
}

輸出:當我們有了一個孩子,我們更希望他可以當一個科學家,而不是網紅
當今社會是需要知識的

我們預設內部類是可以覆蓋的。所以我們想讓他輸出 InternetCelebrity.haveKnowledge() , 來實現我們的猜想,但是卻輸出了 ManWithKnowledge.haveKnowledge() 方法。

這個例子說明當繼承了某個外圍類的時候,內部類並沒有發生特別神奇的變化,兩個內部類各自獨立,都在各自的名稱空間內。

關於原始碼中內部類的表示

由於每個類都會產生一個.class 檔案,包含了建立該型別物件的全部資訊。

同樣的,內部類也會生成一個.class 檔案,表示方法為: OneClass$OneInnerClass

內部類的優點

下面總結一下內部類的優點:

1、封裝部分程式碼,當你建立一個內部類的時候,該內部類預設持有外部類的引用;

2、內部類具有一定的靈活性,無論外圍類是否繼承某個介面的實現,對於內部類都沒有影響;

3、內部類能夠有效的解決多重繼承的問題。

集合

集合在我們的日常開發中所使用的次數簡直太多了,你已經把它們都用的熟透了,但是作為一名合格的程式設計師,你不僅要了解它的基本用法,你還要了解它的原始碼;存在即合理,你還要了解它是如何設計和實現的,你還要了解它的衍生過程。

這篇部落格就來詳細介紹一下 Collection 這個龐大集合框架的家族體系和成員,讓你瞭解它的設計與實現。

是時候祭出這張神圖了

image-20210907223747116

首先來介紹的就是列表爺爺輩兒的介面- Iterator

Iterable 介面

實現此介面允許物件成為 for-each 迴圈的目標,也就是增強 for 迴圈,它是 Java 中的一種語法糖

List<Object> list = new ArrayList();
for (Object obj: list){}

除了實現此介面的物件外,陣列也可以用 for-each 迴圈遍歷,如下:

Object[] list = new Object[10];
for (Object obj: list){}

其他遍歷方式

jdk 1.8之前Iterator只有 iterator 一個方法,就是

Iterator<T> iterator();

實現次介面的方法能夠建立一個輕量級的迭代器,用於安全的遍歷元素,移除元素,新增元素。這裡面涉及到一個 fail-fast 機制。

總之一點就是能建立迭代器進行元素的新增和刪除的話,就儘量使用迭代器進行新增和刪除。

也可以使用迭代器的方式進行遍歷

for(Iterator it = coll.iterator(); it.hasNext(); ){
    System.out.println(it.next());
}

頂層介面

Collection 是一個頂層介面,它主要用來定義集合的約定

List 介面也是一個頂層介面,它繼承了 Collection 介面 ,同時也是 ArrayList、LinkedList 等集合元素的父類

Set 介面位於與 List 介面同級的層次上,它同時也繼承了 Collection 介面。Set 介面提供了額外的規定。它對add、equals、hashCode 方法提供了額外的標準。

Queue 是和 List、Set 介面並列的 Collection 的三大介面之一。Queue 的設計用來在處理之前保持元素的訪問次序。除了 Collection 基礎的操作之外,佇列提供了額外的插入,讀取,檢查操作。

SortedSet 介面直接繼承於 Set 介面,使用 Comparable 對元素進行自然排序或者使用 Comparator 在建立時對元素提供定製的排序規則。set 的迭代器將按升序元素順序遍歷集合。

Map 是一個支援 key-value 儲存的物件,Map 不能包含重複的 key,每個鍵最多對映一個值。這個介面代替了Dictionary 類,Dictionary 是一個抽象類而不是介面。

ArrayList

ArrayList 是實現了 List 介面的可擴容陣列(動態陣列),它的內部是基於陣列實現的。它的具體定義如下:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {...}
  • ArrayList 可以實現所有可選擇的列表操作,允許所有的元素,包括空值。ArrayList 還提供了內部儲存 list 的方法,它能夠完全替代 Vector,只有一點例外,ArrayList 不是執行緒安全的容器。
  • ArrayList 有一個容量的概念,這個陣列的容量就是 List 用來儲存元素的容量。
  • ArrayList 不是執行緒安全的容器,如果多個執行緒中至少有兩個執行緒修改了 ArrayList 的結構的話就會導致執行緒安全問題,作為替代條件可以使用執行緒安全的 List,應使用 Collections.synchronizedList
List list = Collections.synchronizedList(new ArrayList(...))
  • ArrayList 具有 fail-fast 快速失敗機制,能夠對 ArrayList 作出失敗檢測。當在迭代集合的過程中該集合在結構上發生改變的時候,就有可能會發生 fail-fast,即丟擲 ConcurrentModificationException 異常。

Vector

Vector 同 ArrayList 一樣,都是基於陣列實現的,只不過 Vector 是一個執行緒安全的容器,它對內部的每個方法都簡單粗暴的上鎖,避免多執行緒引起的安全性問題,但是通常這種同步方式需要的開銷比較大,因此,訪問元素的效率要遠遠低於 ArrayList。

還有一點在於擴容上,ArrayList 擴容後的陣列長度會增加 50%,而 Vector 的擴容長度後陣列會增加一倍。

LinkedList 類

LinkedList 是一個雙向連結串列,允許儲存任何元素(包括 null )。它的主要特性如下:

  • LinkedList 所有的操作都可以表現為雙向性的,索引到連結串列的操作將遍歷從頭到尾,視哪個距離近為遍歷順序。
  • 注意這個實現也不是執行緒安全的,如果多個執行緒併發訪問連結串列,並且至少其中的一個執行緒修改了連結串列的結構,那麼這個連結串列必須進行外部加鎖。或者使用
List list = Collections.synchronizedList(new LinkedList(...))

Stack

堆疊是我們常說的後入先出(吃了吐)的容器 。它繼承了 Vector 類,提供了通常用的 push 和 pop 操作,以及在棧頂的 peek 方法,測試 stack 是否為空的 empty 方法,和一個尋找與棧頂距離的 search 方法。

第一次建立棧,不包含任何元素。一個更完善,可靠性更強的 LIFO 棧操作由 Deque 介面和他的實現提供,應該優先使用這個類

Deque<Integer> stack = new ArrayDeque<Integer>()

HashSet

HashSet 是 Set 介面的實現類,由雜湊表支援(實際上 HashSet 是 HashMap 的一個例項)。它不能保證集合的迭代順序。這個類允許 null 元素。

  • 注意這個實現不是執行緒安全的。如果多執行緒併發訪問 HashSet,並且至少一個執行緒修改了set,必須進行外部加鎖。或者使用 Collections.synchronizedSet() 方法重寫。
  • 這個實現支援 fail-fast 機制。

TreeSet

TreeSet 是一個基於 TreeMap 的 NavigableSet 實現。這些元素使用他們的自然排序或者在建立時提供的Comparator 進行排序,具體取決於使用的建構函式。

  • 此實現為基本操作 add,remove 和 contains 提供了 log(n) 的時間成本。
  • 注意這個實現不是執行緒安全的。如果多執行緒併發訪問 TreeSet,並且至少一個執行緒修改了 set,必須進行外部加鎖。或者使用
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...))
  • 這個實現持有 fail-fast 機制。

LinkedHashSet 類

LinkedHashSet 繼承於 Set,先來看一下 LinkedHashSet 的繼承體系:

image-20210907223758284

LinkedHashSet 是 Set 介面的 Hash 表和 LinkedList 的實現。這個實現不同於 HashSet 的是它維護著一個貫穿所有條目的雙向連結串列。此連結串列定義了元素插入集合的順序。注意:如果元素重新插入,則插入順序不會受到影響。

  • LinkedHashSet 有兩個影響其構成的引數: 初始容量和載入因子。它們的定義與 HashSet 完全相同。但請注意:對於 LinkedHashSet,選擇過高的初始容量值的開銷要比 HashSet 小,因為 LinkedHashSet 的迭代次數不受容量影響。
  • 注意 LinkedHashSet 也不是執行緒安全的,如果多執行緒同時訪問 LinkedHashSet,必須加鎖,或者通過使用
Collections.synchronizedSet
  • 該類也支援fail-fast機制

PriorityQueue

PriorityQueue 是 AbstractQueue 的實現類,優先順序佇列的元素根據自然排序或者通過在建構函式時期提供Comparator 來排序,具體根據構造器判斷。PriorityQueue 不允許 null 元素。

  • 佇列的頭在某種意義上是指定順序的最後一個元素。佇列查詢操作 poll,remove,peek 和 element 訪問佇列頭部元素。
  • 優先順序佇列是無限制的,但具有內部 capacity,用於控制用於在佇列中儲存元素的陣列大小。
  • 該類以及迭代器實現了 Collection、Iterator 介面的所有可選方法。這個迭代器提供了 iterator() 方法不能保證以任何特定順序遍歷優先順序佇列的元素。如果你需要有序遍歷,考慮使用 Arrays.sort(pq.toArray())
  • 注意這個實現不是執行緒安全的,多執行緒不應該併發訪問 PriorityQueue 例項如果有某個執行緒修改了佇列的話,使用執行緒安全的類 PriorityBlockingQueue

HashMap

HashMap 是一個利用雜湊表原理來儲存元素的集合,並且允許空的 key-value 鍵值對。HashMap 是非執行緒安全的,也就是說在多執行緒的環境下,可能會存在問題,而 Hashtable 是執行緒安全的容器。HashMap 也支援 fail-fast 機制。HashMap 的例項有兩個引數影響其效能:初始容量 和載入因子。可以使用 Collections.synchronizedMap(new HashMap(...)) 來構造一個執行緒安全的 HashMap。

TreeMap 類

一個基於 NavigableMap 實現的紅黑樹。這個 map 根據 key 自然排序儲存,或者通過 Comparator 進行定製排序。

  • TreeMap 為 containsKey,get,put 和remove方法提供了 log(n) 的時間開銷。

  • 注意這個實現不是執行緒安全的。如果多執行緒併發訪問 TreeMap,並且至少一個執行緒修改了 map,必須進行外部加鎖。這通常通過在自然封裝集合的某個物件上進行同步來實現,或者使用 SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...))

  • 這個實現持有fail-fast機制。

LinkedHashMap 類

LinkedHashMap 是 Map 介面的雜湊表和連結串列的實現。這個實現與 HashMap 不同之處在於它維護了一個貫穿其所有條目的雙向連結串列。這個連結串列定義了遍歷順序,通常是插入 map 中的順序。

  • 它提供一個特殊的 LinkedHashMap(int,float,boolean) 構造器來建立 LinkedHashMap,其遍歷順序是其最後一次訪問的順序。

  • 可以重寫 removeEldestEntry(Map.Entry) 方法,以便在將新對映新增到 map 時強制刪除過期對映的策略。

  • 這個類提供了所有可選擇的 map 操作,並且允許 null 元素。由於維護連結串列的額外開銷,效能可能會低於HashMap,有一條除外:遍歷 LinkedHashMap 中的 collection-views 需要與 map.size 成正比,無論其容量如何。HashMap 的迭代看起來開銷更大,因為還要求時間與其容量成正比。

  • LinkedHashMap 有兩個因素影響了它的構成:初始容量和載入因子。

  • 注意這個實現不是執行緒安全的。如果多執行緒併發訪問LinkedHashMap,並且至少一個執行緒修改了map,必須進行外部加鎖。這通常通過在自然封裝集合的某個物件上進行同步來實現 Map m = Collections.synchronizedMap(new LinkedHashMap(...))

  • 這個實現持有fail-fast機制。

Hashtable 類

Hashtable 類實現了一個雜湊表,能夠將鍵對映到值。任何非空物件都可以用作鍵或值。

  • 此實現類支援 fail-fast 機制
  • 與新的集合實現不同,Hashtable 是執行緒安全的。如果不需要執行緒安全的容器,推薦使用 HashMap,如果需要多執行緒高併發,推薦使用 ConcurrentHashMap

IdentityHashMap 類

IdentityHashMap 是比較小眾的 Map 實現了。

  • 這個類不是一個通用的 Map 實現!雖然這個類實現了 Map 介面,但它故意違反了 Map 的約定,該約定要求在比較物件時使用 equals 方法,此類僅適用於需要引用相等語義的極少數情況。
  • 同 HashMap,IdentityHashMap 也是無序的,並且該類不是執行緒安全的,如果要使之執行緒安全,可以呼叫Collections.synchronizedMap(new IdentityHashMap(...))方法來實現。
  • 支援 fail-fast 機制

WeakHashMap 類

WeakHashMap 類基於雜湊表的 Map 基礎實現,帶有弱鍵。WeakHashMap 中的 entry 當不再使用時還會自動移除。更準確的說,給定key的對映的存在將不會阻止 key 被垃圾收集器丟棄。

  • 基於 map 介面,是一種弱鍵相連,WeakHashMap 裡面的鍵會自動回收
  • 支援 null 值和 null 鍵。
  • fast-fail 機制
  • 不允許重複
  • WeakHashMap 經常用作快取

Collections 類

Collections 不屬於 Java 框架繼承樹上的內容,它屬於單獨的分支,Collections 是一個包裝類,它的作用就是為集合框架提供某些功能實現,此類只包括靜態方法操作或者返回 collections。

同步包裝

同步包裝器將自動同步(執行緒安全性)新增到任意集合。 六個核心集合介面(Collection,Set,List,Map,SortedSet 和 SortedMap)中的每一個都有一個靜態工廠方法。

public static  Collection synchronizedCollection(Collection c);
public static  Set synchronizedSet(Set s);
public static  List synchronizedList(List list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static  SortedSet synchronizedSortedSet(SortedSet s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

不可修改的包裝

不可修改的包裝器通過攔截修改集合的操作並丟擲 UnsupportedOperationException,主要用在下面兩個情景:

  • 構建集合後使其不可變。在這種情況下,最好不要去獲取返回 collection 的引用,這樣有利於保證不變性
  • 允許某些客戶端以只讀方式訪問你的資料結構。 你保留對返回的 collection 的引用,但分發對包裝器的引用。 通過這種方式,客戶可以檢視但不能修改,同時保持完全訪問許可權。

這些方法是:

public static  Collection unmodifiableCollection(Collection<? extends T> c);public static  Set unmodifiableSet(Set<? extends T> s);public static  List unmodifiableList(List<? extends T> list);public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);public static  SortedSet unmodifiableSortedSet(SortedSet<? extends T> s);public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);

執行緒安全的Collections

Java1.5 併發包 (java.util.concurrent) 提供了執行緒安全的 collections 允許遍歷的時候進行修改,通過設計iterator 為 fail-fast 並丟擲 ConcurrentModificationException。一些實現類是CopyOnWriteArrayListConcurrentHashMapCopyOnWriteArraySet

Collections 演算法

此類包含用於集合框架演算法的方法,例如二進位制搜尋,排序,重排,反向等。

集合實現類特徵圖

下圖彙總了部分集合框架的主要實現類的特徵圖,讓你能有清晰明瞭看出每個實現類之間的差異性

image-20210907223813609

泛型

在 Jdk1.5 中,提出了一種新的概念:泛型,那麼什麼是泛型呢?

泛型其實就是一種引數化的集合,它限制了你新增進集合的型別。泛型的本質就是一種引數化型別。多型也可以看作是泛型的機制。一個類繼承了父類,那麼就能通過它的父類找到對應的子類,但是不能通過其他類來找到具體要找的這個類。泛型的設計之處就是希望物件或方法具有最廣泛的表達能力。

下面來看一個例子說明沒有泛型的用法

List arrayList = new ArrayList();
arrayList.add("cxuan");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
		System.out.println("test === ", item);
}

這段程式不能正常執行,原因是 Integer 型別不能直接強制轉換為 String 型別

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

如果我們用泛型進行改寫後,示例程式碼如下

List<String> arrayList = new ArrayList<String>();arrayList.add(100);

這段程式碼在編譯期間就會報錯,編譯器會在編譯階段就能夠幫我們發現類似這樣的問題。

泛型的使用

泛型有三種使用方式,分別為:泛型類、泛型介面、泛型方法,下面我們就來一起探討一下。

用泛型表示類

泛型可以加到類上面,來表示這個類的型別

//此處 T 可以隨便寫為任意標識,常見的如T、E、K、V等形式的引數常用於表示泛型
public class GenericDemo<T>{ 
    //value 這個成員變數的型別為T,T的型別由外部指定  
    private T value;

    public GenericDemo(T value) {
        this.value = value;
    }

    public T getValue(){ //泛型方法getKey的返回值型別為T,T的型別由外部指定
        return value;
    }
 
 		public void setValue(T value){
	      this.value = value
    }
}

用泛型表示介面

泛型介面與泛型類的定義及使用基本相同。

//定義一個泛型介面
public interface Generator<T> {
    public T next();
}

一般泛型介面常用於 生成器(generator) 中,生成器相當於物件工廠,是一種專門用來建立物件的類。

泛型方法

可以使用泛型來表示方法

public class GenericMethods {
  public <T> void f(T x){
    System.out.println(x.getClass().getName());
  }
}

泛型萬用字元

無限制萬用字元<?>

List 是泛型類,為了 表示各種泛型 List 的父類,可以使用型別萬用字元,型別萬用字元使用問號(?)表示,它的元素型別可以匹配任何型別。例如

public static void main(String[] args) {
    List<String> name = new ArrayList<String>();
    List<Integer> age = new ArrayList<Integer>();
    List<Number> number = new ArrayList<Number>();
    name.add("cxuan");
    age.add(18);
    number.add(314);
    generic(name);
    generic(age);
    generic(number);   
}

public static void generic(List<?> data) {
    System.out.println("Test cxuan :" + data.get(0));
}

上界萬用字元

在型別引數中使用 extends 表示這個泛型中的引數必須是 E 或者 E 的子類,這樣有兩個好處:

  • 如果傳入的型別不是 E 或者 E 的子類,編輯不成功
  • 泛型中可以使用 E 的方法,要不然還得強轉成 E 才能使用

舉個例子:

private <K extends ChildBookBean, E extends BookBean> E test2(K arg1, E arg2){
    E result = arg2;
    arg2.compareTo(arg1);
    //.....
    return result;
}

下界萬用字元

在型別引數中使用 super 表示這個泛型中的引數必須是 E 或者 E 的父類。

private <E> void add(List<? super E> dst, List<E> src){
    for (E e : src) {
        dst.add(e);
    }
}

可以看到,上面的 dst 型別 “大於等於” src 的型別,這裡的“大於等於”是指 dst 表示的範圍比 src 要大,因此裝得下 dst 的容器也就能裝 src。

萬用字元比較

通過上面的例子我們可以知道,無限制萬用字元 < ?> 和 Object 有些相似,用於表示無限制或者不確定範圍的場景。

兩種有限制通配形式 < ? super E> 和 < ? extends E> 也比較容易混淆,我們再來比較下。

它們的目的都是為了使方法介面更為靈活,可以接受更為廣泛的型別。

< ? super E> 用於靈活寫入或比較,使得物件可以寫入父型別的容器,使得父型別的比較方法可以應用於子類物件。
< ? extends E> 用於靈活讀取,使得方法可以讀取 E 或 E 的任意子型別的容器物件。

泛型的型別擦除

Java 中的泛型和 C++ 中的模板有一個很大的不同:

C++ 中模板的例項化會為每一種型別都產生一套不同的程式碼,這就是所謂的程式碼膨脹。Java 中並不會產生這個問題。虛擬機器中並沒有泛型型別物件,所有的物件都是普通類。
(摘自:http://blog.csdn.net/fw0124/article/details/42295463)

在 Java 中,泛型是 Java 編譯器的概念,用泛型編寫的 Java 程式和普通的 Java 程式基本相同,只是多了一些引數化的型別同時少了一些型別轉換。

實際上泛型程式也是首先被轉化成一般的、不帶泛型的 Java 程式後再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯,Java 虛擬機器執行時對泛型基本一無所知。

當編譯器對帶有泛型的 Java 程式碼進行編譯時,它會去執行型別檢查和型別推斷,然後生成普通的不帶泛型的位元組碼,這種普通的位元組碼可以被一般的 Java 虛擬機器接收並執行,這在就叫做 型別擦除(type erasure)。

實際上無論你是否使用泛型,集合框架中存放物件的資料型別都是 Object,這一點不僅僅從原始碼中可以看到,通過反射也可以看到。

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());//true

上面程式碼輸出結果並不是預期的 false,而是 true。其原因就是泛型的擦除。

反射

反射是 Java 中一個非常重要同時也是一個高階特性,基本上 Spring 等一系列框架都是基於反射的思想寫成的。我們首先來認識一下什麼反射。

Java 反射機制是在程式的執行過程中,對於任何一個類,都能夠知道它的所有屬性和方法;對於任意一個物件,都能夠知道呼叫它的任意屬性和方法,這種動態獲取資訊以及動態呼叫物件方法的功能稱為 Java 語言的反射機制

要想解剖一個類,必須先要獲取到該類的位元組碼檔案物件。而解剖使用的就是 Class 類中的方法,所以先要獲取到每一個位元組碼檔案對應的 Class 型別的物件.

所謂反射其實是獲取類的位元組碼檔案,也就是. class 檔案,那麼我們就可以通過 Class 這個物件進行獲取。

Java 反射機制主要提供了以下這幾個功能

  • 在執行時判斷任意一個物件所屬的類
  • 在執行時構造任意一個類的物件
  • 在執行時判斷任意一個類所有的成員變數和方法
  • 在執行時呼叫任意一個物件的方法

這麼一看,反射就像是一個掌控全域性的角色,不管你程式怎麼執行,我都能夠知道你這個類有哪些屬性和方法,你這個物件是由誰呼叫的,嗯,很屌。

在 Java 中,使用 Java.lang.reflect包實現了反射機制。Java.lang.reflect 所設計的類如下

image-20210907223829738

下面是一個簡單的反射類

public class Person {
    public String name;// 姓名
    public int age;// 年齡
 
    public Person() {
        super();
    }
 
    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
 
    public String showInfo() {
        return "name=" + name + ", age=" + age;
    }
}

public class Student extends Person implements Study {
    public String className;// 班級
    private String address;// 住址
 
    public Student() {
        super();
    }
 
    public Student(String name, int age, String className, String address) {
        super(name, age);
        this.className = className;
        this.address = address;
    }
 
    public Student(String className) {
        this.className = className;
    }
 
    public String toString() {
        return "姓名:" + name + ",年齡:" + age + ",班級:" + className + ",住址:"
                + address;
    }
 
    public String getAddress() {
        return address;
    }
 
    public void setAddress(String address) {
        this.address = address;
    }
}

public class TestRelect {
 
    public static void main(String[] args) {
        Class student = null;
        try {
            student = Class.forName("com.cxuan.reflection.Student");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
 
        // 獲取物件的所有公有屬性。
        Field[] fields = student.getFields();
        for (Field f : fields) {
            System.out.println(f);
        }
        System.out.println("---------------------");
        // 獲取物件所有屬性,但不包含繼承的。
        Field[] declaredFields = student.getDeclaredFields();
        for (Field df : declaredFields) {
            System.out.println(df);
        }
      
      	// 獲取物件的所有公共方法
        Method[] methods = student.getMethods();
        for (Method m : methods) {
            System.out.println(m);
        }
        System.out.println("---------------------");
        // 獲取物件所有方法,但不包含繼承的
        Method[] declaredMethods = student.getDeclaredMethods();
        for (Method dm : declaredMethods) {
            System.out.println(dm);
        }
				
      	// 獲取物件所有的公共構造方法
        Constructor[] constructors = student.getConstructors();
        for (Constructor c : constructors) {
            System.out.println(c);
        }
        System.out.println("---------------------");
        // 獲取物件所有的構造方法
        Constructor[] declaredConstructors = student.getDeclaredConstructors();
        for (Constructor dc : declaredConstructors) {
            System.out.println(dc);
        }
      
      	Class c = Class.forName("com.cxuan.reflection.Student");
      	Student stu1 = (Student) c.newInstance();
      	// 第一種方法,例項化預設構造方法,呼叫set賦值
        stu1.setAddress("河北石家莊");
        System.out.println(stu1);

        // 第二種方法 取得全部的建構函式 使用建構函式賦值
        Constructor<Student> constructor = c.getConstructor(String.class, 
                                                            int.class, String.class, String.class);
        Student student2 = (Student) constructor.newInstance("cxuan", 24, "六班", "石家莊");
        System.out.println(student2);

        /**
        * 獲取方法並執行方法
        */
        Method show = c.getMethod("showInfo");//獲取showInfo()方法
        Object object = show.invoke(stu2);//呼叫showInfo()方法
      	
 
    }
}

有一些是比較常用的,有一些是我至今都沒見過怎麼用的,下面進行一個歸類。

與 Java 反射有關的類主要有

Class 類

在 Java 中,你每定義一個 java class 實體都會產生一個 Class 物件。也就是說,當我們編寫一個類,編譯完成後,在生成的 .class 檔案中,就會產生一個 Class 物件,這個 Class 物件用於表示這個類的型別資訊。Class 中沒有公共的構造器,也就是說 Class 物件不能被例項化。下面來簡單看一下 Class 類都包括了哪些方法

toString()

public String toString() {
  return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
    + getName();
}

toString() 方法能夠將物件轉換為字串,toString() 首先會判斷 Class 型別是否是介面型別,也就是說,普通類和介面都能夠用 Class 物件來表示,然後再判斷是否是基本資料型別,這裡判斷的都是基本資料型別和包裝類,還有 void 型別。

所有的型別如下

  • java.lang.Boolean : 代表 boolean 資料型別的包裝類
  • java.lang.Character: 代表 char 資料型別的包裝類
  • java.lang.Byte: 代表 byte 資料型別的包裝類
  • java.lang.Short: 代表 short 資料型別的包裝類
  • java.lang.Integer: 代表 int 資料型別的包裝類
  • java.lang.Long: 代表 long 資料型別的包裝類
  • java.lang.Float: 代表 float 資料型別的包裝類
  • java.lang.Double: 代表 double 資料型別的包裝類
  • java.lang.Void: 代表 void 資料型別的包裝類

然後是 getName() 方法,這個方法返回類的全限定名稱。

  • 如果是引用型別,比如 String.class.getName() -> java.lang.String
  • 如果是基本資料型別,byte.class.getName() -> byte
  • 如果是陣列型別,new Object[3]).getClass().getName() -> [Ljava.lang.Object

toGenericString()

這個方法會返回類的全限定名稱,而且包括類的修飾符和型別引數資訊。

forName()

根據類名獲得一個 Class 物件的引用,這個方法會使類物件進行初始化。

例如 Class t = Class.forName("java.lang.Thread") 就能夠初始化一個 Thread 執行緒物件

在 Java 中,一共有三種獲取類例項的方式

  • Class.forName(java.lang.Thread)
  • Thread.class
  • thread.getClass()

newInstance()

建立一個類的例項,代表著這個類的物件。上面 forName() 方法對類進行初始化,newInstance 方法對類進行例項化。

getClassLoader()

獲取類載入器物件。

getTypeParameters()

按照宣告的順序獲取物件的引數型別資訊。

getPackage()

返回類的包

getInterfaces()

獲得當前類實現的類或是介面,可能是有多個,所以返回的是 Class 陣列。

Cast

把物件轉換成代表類或是介面的物件

asSubclass(Class clazz)

把傳遞的類的物件轉換成代表其子類的物件

getClasses()

返回一個陣列,陣列中包含該類中所有公共類和介面類的物件

getDeclaredClasses()

返回一個陣列,陣列中包含該類中所有類和介面類的物件

getSimpleName()

獲得類的名字

getFields()

獲得所有公有的屬性物件

getField(String name)

獲得某個公有的屬性物件

getDeclaredField(String name)

獲得某個屬性物件

getDeclaredFields()

獲得所有屬性物件

getAnnotation(Class annotationClass)

返回該類中與引數型別匹配的公有註解物件

getAnnotations()

返回該類所有的公有註解物件

getDeclaredAnnotation(Class annotationClass)

返回該類中與引數型別匹配的所有註解物件

getDeclaredAnnotations()

返回該類所有的註解物件

getConstructor(Class...<?> parameterTypes)

獲得該類中與引數型別匹配的公有構造方法

getConstructors()

獲得該類的所有公有構造方法

getDeclaredConstructor(Class...<?> parameterTypes)

獲得該類中與引數型別匹配的構造方法

getDeclaredConstructors()

獲得該類所有構造方法

getMethod(String name, Class...<?> parameterTypes)

獲得該類某個公有的方法

getMethods()

獲得該類所有公有的方法

getDeclaredMethod(String name, Class...<?> parameterTypes)

獲得該類某個方法

getDeclaredMethods()

獲得該類所有方法

Field 類

Field 類提供類或介面中單獨欄位的資訊,以及對單獨欄位的動態訪問。

這裡就不再對具體的方法進行介紹了,讀者有興趣可以參考官方 API

這裡只介紹幾個常用的方法

equals(Object obj)

屬性與obj相等則返回true

get(Object obj)

獲得obj中對應的屬性值

set(Object obj, Object value)

設定obj中對應屬性值

Method 類

invoke(Object obj, Object... args)

傳遞object物件及引數呼叫該物件對應的方法

ClassLoader 類

反射中,還有一個非常重要的類就是 ClassLoader 類,類裝載器是用來把類(class) 裝載進 JVM 的。ClassLoader 使用的是雙親委託模型來搜尋載入類的,這個模型也就是雙親委派模型。ClassLoader 的類繼承圖如下

image-20210907223844550

深入理解反射,可以閱讀作者的這篇文章 學會反射後,我被錄取了!(乾貨)

列舉

列舉可能是我們使用次數比較少的特性,在 Java 中,列舉使用 enum 關鍵字來表示,列舉其實是一項非常有用的特性,你可以把它理解為具有特定性質的類。enum 不僅僅 Java 有,C 和 C++ 也有列舉的概念。下面是一個列舉的例子。

public enum Family {

    FATHER,
    MOTHER,
    SON,
    Daughter;

}

上面我們建立了一個 Family的列舉類,它具有 4 個值,由於列舉型別都是常量,所以都用大寫字母來表示。那麼 enum 建立出來了,該如何引用呢?

public class EnumUse {

    public static void main(String[] args) {
        Family s = Family.FATHER;
    }
}

列舉特性

enum 列舉這個類比較有意思,當你建立完 enum 後,編譯器會自動為你的 enum 新增 toString() 方法,能夠讓你方便的顯示 enum 例項的具體名字是什麼。除了 toString() 方法外,編譯器還會新增 ordinal() 方法,這個方法用來表示 enum 常量的宣告順序,以及 values() 方法顯示順序的值。

public static void main(String[] args) {

  for(Family family : Family.values()){
    System.out.println(family + ", ordinal" + family.ordinal());
  }
}

enum 可以進行靜態匯入包,靜態匯入包可以做到不用輸入 列舉類名.常量,可以直接使用常量,神奇嗎? 使用 ennum 和 static 關鍵字可以做到靜態匯入包

image-20210907223907374

上面程式碼匯入的是 Family 中所有的常量,也可以單獨指定常量。

列舉和普通類一樣

列舉就和普通類一樣,除了列舉中能夠方便快捷的定義常量,我們日常開發使用的 public static final xxx 其實都可以用列舉來定義。在列舉中也能夠定義屬性和方法,千萬不要把它看作是異類,它和萬千的類一樣。

public enum OrdinalEnum {

    WEST("live in west"),
    EAST("live in east"),
    SOUTH("live in south"),
    NORTH("live in north");

    String description;

    OrdinalEnum(String description){
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public static void main(String[] args) {
        for(OrdinalEnum ordinalEnum : OrdinalEnum.values()){
            System.out.println(ordinalEnum.getDescription());
        }
    }
}

一般 switch 可以和 enum 一起連用,來構造一個小型的狀態轉換機。

enum Signal {
  GREEN, YELLOW, RED
}

public class TrafficLight {
    Signal color = Signal.RED;

    public void change() {
        switch (color) {
        case RED:
            color = Signal.GREEN;
            break;
        case YELLOW:
            color = Signal.RED;
            break;
        case GREEN:
            color = Signal.YELLOW;
            break;
        }
    }
}

是不是程式碼頓時覺得優雅整潔了些許呢?

列舉神祕之處

在 Java 中,萬事萬物都是物件,enum 雖然是個關鍵字,但是它卻隱式的繼承於 Enum 類。我們來看一下 Enum 類,此類位於 java.lang 包下,可以自動引用。

image-20210907223923592

此類的屬性和方法都比較少。你會發現這個類中沒有我們的 values 方法。前面剛說到,values() 方法是你使用列舉時被編譯器新增進來的 static 方法。可以使用反射來驗證一下

除此之外,enum 還和 Class 類有交集,在 Class 類中有三個關於 Enum 的方法

image-20210907223931510

前面兩個方法用於獲取 enum 常量,isEnum 用於判斷是否是列舉型別的。

列舉類

除了 Enum 外,還需要知道兩個關於列舉的工具類,一個是 EnumSet ,一個是 EnumMap

EnumSet 和 EnumMap

EnumSet 是 JDK1.5 引入的,EnumSet 的設計充分考慮到了速度因素,使用 EnumSet 可以作為 Enum 的替代者,因為它的效率比較高。

EnumMap 是一種特殊的 Map,它要求其中的 key 鍵值是來自一個 enum。因為 EnumMap 速度也很快,我們可以使用 EnumMap 作為 key 的快速查詢。

總的來說,列舉的使用不是很複雜,它也是 Java 中很小的一塊功能,但有時卻能夠因為這一個小技巧,能夠讓你的程式碼變得優雅和整潔。

I/O

建立一個良好的 I/O 程式是非常複雜的。JDK 開發人員編寫了大量的類只為了能夠建立一個良好的工具包,想必編寫 I/O 工具包很費勁吧。

IO 類設計出來,肯定是為了解決 IO 相關操作的,最常見的 I/O 讀寫就是網路、磁碟等。在 Java 中,對檔案的操作是一個典型的 I/O 操作。下面我們就對 I/O 進行一個分類。

image-20210907223943720

I/O 還可以根據操作物件來進行區分:主要分為

image-20210907224000191

除此之外,I/O 中還有其他比較重要的類

File 類

File 類是對檔案系統中檔案以及資料夾進行操作的類,可以通過物件導向的思想操作檔案和資料夾,是不是很神奇?

檔案建立操作如下,主要涉及 檔案建立、刪除檔案、獲取檔案描述符等

class FileDemo{
   public static void main(String[] args) {
       File file = new File("D:\\file.txt");
       try{
         f.createNewFile(); // 建立一個檔案
         
         // File類的兩個常量
         //路徑分隔符(與系統有關的)<windows裡面是 ; linux裡面是 : >
        System.out.println(File.pathSeparator);  //   ;
        //與系統有關的路徑名稱分隔符<windows裡面是 \ linux裡面是/ >
        System.out.println(File.separator);      //  \
         
         // 刪除檔案
         /*
         File file = new File(fileName);
         if(f.exists()){
             f.delete();
         }else{
             System.out.println("檔案不存在");
         }   
         */

         
       }catch (Exception e) {
           e.printStackTrace();
       }
    }
}

也可以對資料夾進行操作

class FileDemo{
  public static void main(String[] args) {
    String fileName = "D:"+ File.separator + "filepackage";
    File file = new File(fileName);
    f.mkdir();
    
		// 列出所有檔案
    /*
    String[] str = file.list();
    for (int i = 0; i < str.length; i++) {
      System.out.println(str[i]);
    }
    */
    
    // 使用 file.listFiles(); 列出所有檔案,包括隱藏檔案
    
    // 使用 file.isDirectory() 判斷指定路徑是否是目錄
  }
}

上面只是舉出來了兩個簡單的示例,實際上,還有一些其他對檔案的操作沒有使用。比如建立檔案,就可以使用三種方式來建立

File(String directoryPath);
File(String directoryPath, String filename);
File(File dirObj, String filename);

directoryPath 是檔案的路徑名,filename 是檔名,dirObj 是一個 File 物件。例如

File file = new File("D:\\java\\file1.txt");  //雙\\是轉義
System.out.println(file);
File file2 = new File("D:\\java","file2.txt");//父路徑、子路徑--可以適用於多個檔案的!
System.out.println(file2);
File parent = new File("D:\\java");
File file3 = new File(parent,"file3.txt");//File類的父路徑、子路徑
System.out.println(file3);

現在對 File 類進行總結

image-20210907224017172

基礎 IO 類和相關方法

雖然. IO 類有很多,但是最基本的是四個抽象類,InputStream、OutputStream、Reader、Writer。最基本的方法也就是 read()write() 方法,其他流都是上面這四類流的子類,方法也是通過這兩類方法衍生而成的。而且大部分的 IO 原始碼都是 native 標誌的,也就是說原始碼都是 C/C++ 寫的。這裡我們先來認識一下這些流類及其方法

InputStream

InputStream 是一個定義了 Java 流式位元組輸入模式的抽象類。該類的所有方法在出錯條件下引發一個IOException 異常。它的主要方法定義如下

image-20210907224027459

OutputStream

OutputStream 是定義了流式位元組輸出模式的抽象類。該類的所有方法返回一個void 值並且在出錯情況下引發一個IOException異常。它的主要方法定義如下

image-20210907224038211

Reader 類

Reader 是 Java 定義的流式字元輸入模式的抽象類。類中的方法在出錯時引發 IOException 異常。

image-20210907224047953

Writer 類

Writer 是定義流式字元輸出的抽象類。 所有該類的方法都返回一個 void 值並在出錯條件下引發 IOException 異常

image-20210907224100871

InputStream 及其子類

FileInputStream 檔案輸入流: FileInputStream 類建立一個能從檔案讀取位元組的 InputStream 類

ByteArrayInputStream 位元組陣列輸入流 : 把記憶體中的一個緩衝區作為 InputStream 使用

PipedInputStream 管道輸入流: 實現了pipe 管道的概念,主要線上程中使用

SequenceInputStream 順序輸入流:把多個 InputStream 合併為一個 InputStream

FilterOutputStream 過濾輸入流:其他輸入流的包裝。

ObjectInputStream 反序列化輸入流 : 將之前使用 ObjectOutputStream 序列化的原始資料恢復為物件,以流的方式讀取物件

DataInputStream : 資料輸入流允許應用程式以與機器無關方式從底層輸入流中讀取基本 Java 資料型別。

PushbackInputStream 推回輸入流: 緩衝的一個新穎的用法是實現推回 (pushback) 。 Pushback 用於輸入流允許位元組被讀取然後返回到流。

OutputStream 及其子類

FileOutputStream 檔案輸出流: 該類實現了一個輸出流,其資料寫入檔案。

ByteArrayOutputStream 位元組陣列輸出流 :該類實現了一個輸出流,其資料被寫入由 byte 陣列充當的緩衝區,緩衝區會隨著資料的不斷寫入而自動增長。

PipedOutputStream 管道輸出流 :管道的輸出流,是管道的傳送端。

ObjectOutputStream 基本型別輸出流 :該類將實現了序列化的物件序列化後寫入指定地方。

FilterOutputStream 過濾輸出流:其他輸出流的包裝。

PrintStream 列印流 通過 PrintStream 可以將文字列印到檔案或者網路中去。

DataOutputStream : 資料輸出流允許應用程式以與機器無關方式向底層輸出流中寫入基本 Java 資料型別。

Reader 及其子類

FileReader 檔案字元輸入流 : 把檔案轉換為字元流讀入

CharArrayReader 字元陣列輸入流 : 是一個把字元陣列作為源的輸入流的實現

BufferedReader 緩衝區輸入流 : BufferedReader 類從字元輸入流中讀取文字並緩衝字元,以便有效地讀取字元,陣列和行

PushbackReader: PushbackReader 類允許一個或多個字元被送回輸入流。

PipedReader 管道輸入流: 主要用途也是線上程間通訊,不過這個可以用來傳輸字元

Writer 及其子類

FileWriter 字元輸出流 : FileWriter 建立一個可以寫檔案的 Writer 類。

CharArrayWriter 字元陣列輸出流: CharArrayWriter 實現了以陣列作為目標的輸出流。

BufferedWriter 緩衝區輸出流 : BufferedWriter是一個增加了flush( ) 方法的Writer。 flush( )方法可以用來確保資料緩衝器確實被寫到實際的輸出流。

**PrintWriter ** : PrintWriter 本質上是 PrintStream 的字元形式的版本。

PipedWriter 管道輸出流: 主要用途也是線上程間通訊,不過這個可以用來傳輸字元

Java 的輸入輸出的流式介面為複雜而繁重的任務提供了一個簡潔的抽象。過濾流類的組合允許你動態建立客戶端流式介面來配合資料傳輸要求。繼承高階流類 InputStream、InputStreamReader、 Reader 和 Writer 類的 Java 程式在將來 (即使建立了新的和改進的具體類)也能得到合理運用。

深入理解 Java IO ,你可以閱讀作者的這篇文章 深入理解 Java IO

註解

Java 註解(Annotation) 又稱為後設資料 ,它為我們在程式碼中新增資訊提供了一種形式化的方法。它是 JDK1.5 引入的,Java 定義了一套註解,共有 7 個,3 個在 java.lang 中,剩下 4 個在 java.lang.annotation 中。

作用在程式碼中的註解有三個,它們分別是

  • @Override: 重寫標記,一般用在子類繼承父類後,標註在重寫過後的子類方法上。如果發現其父類,或者是引用的介面中並沒有該方法時,會報編譯錯誤。
  • @Deprecated :用此註解註釋的程式碼已經過時,不再推薦使用
  • @SuppressWarnings: 這個註解起到忽略編譯器的警告作用

元註解有四個,元註解就是用來標誌註解的註解。它們分別是

  • @Retention: 標識如何儲存,是隻在程式碼中,還是編入class檔案中,或者是在執行時可以通過反射訪問。

RetentionPolicy.SOURCE:註解只保留在原始檔,當 Java 檔案編譯成class檔案的時候,註解被遺棄;

RetentionPolicy.CLASS:註解被保留到 class 檔案,但 jvm 載入 class 檔案時候被遺棄,這是預設的生命週期;

RetentionPolicy.RUNTIME:註解不僅被儲存到 class 檔案中,jvm 載入 class 檔案之後,仍然存在;

  • @Documented: 標記這些註解是否包含在 JavaDoc 中。
  • @Target: 標記這個註解說明了 Annotation 所修飾的物件範圍,Annotation 可被用於 packages、types(類、介面、列舉、Annotation型別)、型別成員(方法、構造方法、成員變數、列舉值)、方法引數和本地變數(如迴圈變數、catch引數)。取值如下
public enum ElementType {
    TYPE, 						// 類、介面、註解、列舉
    FIELD,						// 欄位
    METHOD,						// 方法
    PARAMETER,				// 引數
    CONSTRUCTOR,			// 構造方法
    LOCAL_VARIABLE,		// 本地變數
    ANNOTATION_TYPE,	// 註解
    PACKAGE,					// 包
    TYPE_PARAMETER,		// 型別引數
    TYPE_USE					// 型別使用
  • @Inherited : 標記這個註解是繼承於哪個註解類的。

從 JDK1.7 開始,又新增了三個額外的註解,它們分別是

  • @SafeVarargs :在宣告可變引數的建構函式或方法時,Java 編譯器會報 unchecked 警告。使用 @SafeVarargs 可以忽略這些警告

  • @FunctionalInterface: 表明這個方法是一個函式式介面

  • @Repeatable: 標識某註解可以在同一個宣告上使用多次。

注意:註解是不支援繼承的。

註解的生命週期

註解也是有相應的宣告週期的,也是封裝在一個列舉類:RetentionPolicy 中:

  • SOURCE:原始碼期間,在編譯時會去除,所以這都是給編譯器使用的。
  • CLASS:會保留在類檔案中,但是執行時 JVM 不需要儲存,預設的生命週期。
  • RUNTIME:會持續儲存到 JVM 執行時,可以通過反射來獲取。

宣告週期配合 @Retention 來使用,使用方法如下:

@Retention(RetentionPolicy.RUNTIME)

一般來說對於編寫框架用的註解的生命週期都是RUNTIME。

關於 null 的幾種處理方式

對於 Java 程式設計師來說,空指標一直是惱人的問題,我們在開發中經常會受到 NullPointerException 的蹂躪和壁咚。Java 的發明者也承認這是一個巨大的設計錯誤。

那麼關於 null ,你應該知道下面這幾件事情來有效的瞭解 null ,從而避免很多由 null 引起的錯誤。

image-20210908220944817

大小寫敏感

首先,null 是 Java 中的關鍵字,像是 public、static、final。它是大小寫敏感的,你不能將 null 寫成 Null 或 NULL,編輯器將不能識別它們然後報錯。

image-20210908221001686

這個問題已經幾乎不會出現,因為 eclipse 和 Idea 編譯器已經給出了編譯器提示,所以你不用考慮這個問題。

null 是任何引用型別的初始值

null 是所有引用型別的預設值,Java 中的任何引用變數都將null作為預設值,也就是說所有 Object 類下的引用型別預設值都是 null。這對所有的引用變數都適用。就像是基本型別的預設值一樣,例如 int 的預設值是 0,boolean 的預設值是 false。

下面是基本資料型別的初始值

image-20210908221018989

null 只是一種特殊的值

null 既不是物件也不是一種型別,它僅是一種特殊的值,你可以將它賦予任何型別,你可以將 null 轉換為任何型別

public static void main(String[] args) {
  String str = null;
  Integer itr = null;
  Double dou = null;

  Integer integer = (Integer) null;
  String string = (String)null;

  System.out.println("integer = " + integer);
  System.out.println("string = " + string);
}

你可以看到在編譯期和執行期內,將 null 轉換成任何的引用型別都是可行的,並且不會丟擲空指標異常。

null 只能賦值給引用變數,不能賦值給基本型別變數

持有 null 的包裝類在進行自動拆箱的時候,不能完成轉換,會丟擲空指標異常,並且 null 也不能和基本資料型別進行對比

public static void main(String[] args) {
  int i = 0;
  Integer itr = null;
  System.out.println(itr == i);
}

使用了帶有 null 值的引用型別變數,instanceof 操作會返回 false

public static void main(String[] args) {
  Integer isNull = null;
  // instanceof = isInstance 方法
  if(isNull instanceof Integer){
    System.out.println("isNull is instanceof Integer");
  }else{
    System.out.println("isNull is not instanceof Integer");
  }
}

這是 instanceof 操作符一個很重要的特性,使得對型別強制轉換檢查很有用

靜態變數為 null 呼叫靜態方法不會丟擲 NullPointerException。因為靜態方法使用了靜態繫結

使用 Null-Safe 方法

你應該使用 null-safe 安全的方法,java 類庫中有很多工具類都提供了靜態方法,例如基本資料型別的包裝類,Integer , Double 等。例如

public class NullSafeMethod {

    private static String number;

    public static void main(String[] args) {
        String s = String.valueOf(number);
        String string = number.toString();
        System.out.println("s = " + s);
        System.out.println("string = " + string);
    }
}

number 沒有賦值,所以預設為null,使用String.value(number) 靜態方法沒有丟擲空指標異常,但是使用 toString() 卻丟擲了空指標異常。所以儘量使用物件的靜態方法。

null 判斷

你可以使用 == 或者 != 操作來比較 null 值,但是不能使用其他演算法或者邏輯操作,例如小於或者大於。跟SQL不一樣,在Java中 null == null 將返回 true,如下所示:

public class CompareNull {

    private static String str1;
    private static String str2;

    public static void main(String[] args) {
        System.out.println("str1 == str2 ? " + str1 == str2);
        System.out.println(null == null);
    }
}

相關文章