藏在Java陣列的背後,你可能忽略的知識點

衍方 發表於 2020-09-12

引言

概念

陣列是資料呈線性排列的一種資料結構,它用一組連續的記憶體空間,來儲存一組相同資料型別的資料,表示一組相同型別的資料的集合,具有固定的長度,並且在記憶體中佔據連續的空間。

陣列是基本上所有語言都會有的一種資料型別,是我們在開發過程中經常會接觸到的,所以我們很有必要了解陣列的相關特性

陣列的定義和使用需要通過方括號 []

Java 中,陣列是一種引用型別。

Java 中,陣列是用來儲存固定大小的同型別元素。

區別於C/C++陣列

儲存結構區別:

C陣列:陣列空間是一次性給定的,優先訪問低地址,自底向上而放元素。

在記憶體中是連續儲存的,並且所有陣列都是連續的,都可作為一維陣列看待。

同時,C陣列是可以動態申請記憶體空間的,也就是可以動態擴容的,而Java陣列是不行的,當然Java也提供了ArrayList動態陣列類

如下圖,一個二維陣列就可以看成一個一維陣列,只是裡面存放的元素為一維陣列。所以C中的陣列是呈線性結構

在這裡插入圖片描述

Java中的陣列就不一樣,在Java中,陣列都是引用實體變數,呈樹形結構,每一個葉子節點之間毫無關係,只有引用關係,每一個引用變數只引用一個實體。

Java陣列是會做邊界檢查的,所以當你越界訪問時,會丟擲 RuntimeException,而在C或C++是不做邊界檢查的

如圖,上面的例子是這樣表示的。在堆記憶體中,各個一維陣列的元素是連續的,但各個一維陣列之間不是連續存放的。

在這裡插入圖片描述

陣列是物件嗎?

C語言是程式導向的語言,在這裡不討論

C++中的陣列不是物件,只是一個資料的集合,而Java中的陣列是物件,這一點在後面會講到和驗證

區別於容器

Java 中,容器是用來儲存多個物件的東西.嚴格來說是儲存物件的引用.因為物件實際的資料是放在另外的地方的.放在容器中的只是指向那塊記憶體區域的一個標識

Java 中,既然有了強大的容器,是不是就不需要陣列了?答案是不

誠然,大多數情況下,應該選擇容器儲存資料。

陣列和容器的區別有:效率型別識別以及存放基本型別的能力

1、Java 中,陣列是一種效率最高的儲存和隨機訪問物件引用序列的方式。陣列的效率要高於容器(如 ArrayList

2、型別識別方面,Java容器ListSetMap在處理物件的時候就好像這些物件都沒有自己的型別一樣,容器將它所含的元素都看根類Object型別,這樣我們只需建立一種容器,就能把所有的型別的物件全部放進去。但是當取出資料時,需要我們自己進行型別轉換,這個問題在Java引入泛型進行型別檢查後,與容器類一起使用就可以解決型別轉換的問題

3、陣列可以持有值型別,而容器則不能(必須用到包裝類)

陣列特性

隨機訪問

非隨機訪問:就是存取第N個資料時,必須先訪問前(N-1)個資料 (連結串列)

隨機訪問:就是存取第N個資料時,不需要訪問前(N-1)個資料,直接就可以對第N個資料操作(陣列)

陣列是如何做到隨機訪問的?

事實上,陣列的資料是按順序儲存在記憶體的連續空間內的,從上面的圖我們看出來,即便Java二維陣列是呈樹形結構,但是各個一維陣列的元素是連續的,通過arr[0],arr[1]等陣列物件指向一維陣列,所以每個資料的記憶體地址(在記憶體上的位置)都可以通過陣列下標算出,我們也就可以藉此直接訪問目標資料,也就是隨機訪問

Java陣列與記憶體

上面這麼說還是有點懵懵懂懂的,可以畫圖解看看Java 陣列在記憶體中的儲存是怎麼樣的?

陣列物件(類比看作指標)儲存在棧中,陣列元素儲存在堆中

一維陣列:
在這裡插入圖片描述

二維陣列:

在這裡插入圖片描述

精彩點評:一維陣列在堆上連續的記憶體空間直接儲存值,二維陣列在連續的地址上儲存一維陣列的引用地址,一維陣列與一維陣列並不一定靠在一起,但是這些一維陣列內部的值是在連續地址上的。更高維的陣列繼續以此類推,只有最後一維陣列在連續地址上儲存值,其他緯度均在連續地址上儲存下一維度的引用地址。同維度的例項不一定靠在一起。

解惑

陣列下標為什麼是從0開始?

前面說到陣列訪問資料時使用的是隨機訪問(通過下標可計算出記憶體地址),從陣列儲存的記憶體模型上來看,“下標”最確切的定義應該是“偏移(offset)”。如果用 a 來表示陣列的首地址,a[0] 就是偏移為 0 的位置,也就是首地址,a[k] 就表示偏移 k 個 type_size 的位置,所以計算 a[k] 的記憶體地址只需要用這個公式:

a[k]_address = base_address + k * type_size

但是,如果陣列從 1 開始計數,那我們計算陣列元素 a[k] 的記憶體地址就會變為:

a[k]_address = base_address + (k-1)*type_size

對比兩個公式,可以發現,從 0 開始編號,每次隨機訪問陣列元素都少了一次減法運算,對於 CPU 來說,就是少了一次減法指令, 提高了訪問的效率

陣列的本質

Java中的陣列是物件嗎?

Java和C++都是物件導向的語言。在使用這些語言的時候,我們可以直接使用標準的類庫,也可以使用組合和繼承等物件導向的特性構建自己的類,並且根據自己構建的類建立物件。那麼,我們是不是應該考慮這樣一個問題:在物件導向的語言中,陣列是物件嗎?

判斷陣列是不是物件,那麼首先明確什麼是物件,也就是物件的定義。在較高的層面上,物件是根據某個類建立出來的一個例項,表示某類事物中一個具體的個體。物件具有各種屬性,並且具有一些特定的行為。而在較低的層面上,站在計算機的角度,物件就是記憶體中的一個記憶體塊,在這個記憶體塊封裝了一些資料,也就是類中定義的各個屬性,所以,物件是用來封裝資料的。以下為一個Person物件在記憶體中的表示:

在這裡插入圖片描述

注意

1、紅色矩形表示一個引用(地址)或一個基本型別的資料,綠色矩形表示一個物件,多個紅色矩形組合在一塊,可組成一個物件。

2、name在物件中只表示一個引用, 也就是一個地址值,它指向一個真實存在的字串物件。在這裡嚴格區分了引用和物件。

那麼在Java中,陣列滿足以上的條件嗎?在較高的層面上,陣列不是某類事物中的一個具體的個體,而是多個個體的集合。那麼它應該不是物件。而在計算機的角度,陣列也是一個記憶體塊,也封裝了一些資料,這樣的話也可以稱之為物件。以下是一個陣列在記憶體中的表示:

在這裡插入圖片描述

這樣的話, 陣列既可以是物件, 也可以不是物件。至於到底是不是把陣列當做物件,全憑Java的設計者決定。陣列到底是不是物件, 通過程式碼驗證:

int[] arr = new int[4];
int len = arr.length;  //陣列中儲存一個欄位, 表示陣列的長度

//以下方法說明陣列可以呼叫方法,Java陣列是物件.這些方法是Object中的方法,所以可以肯定,陣列的最頂層父類也是Object
arr.clone();
arr.toString();

從上面的程式碼來看,在陣列arr上, 可以訪問它的屬性,也可以呼叫一些方法。這基本上可以認定,Java中的陣列也是物件,它具有java中其他物件的一些基本特點:封裝了一些資料,可以訪問屬性,也可以呼叫方法。所以答案是肯定的,陣列是物件。

同時權威的Java Language Specification是這麼說的:

In the Java programming language, arrays are objects (§4.3.1), are dynamically created, and may be assigned to variables of type Object (§4.3.2). All methods of class Object may be invoked on an array.

這裡我就不給大家翻譯了,看不懂的有道翻譯一下

補充:Java Language Specification 裡關於Array還有這麼一段:

Every array has an associated Class object, shared with all other arrays with the same component type. [ This] acts as if: the direct superclass of an array type is Object [ and] every array type implements the interfaces Cloneable and java. io. Serializable.

陣列物件不是從某個類例項化來的,而是由JVM直接建立的。實際上也沒有Array這個類(有是有,但只是java.lang.reflect包裡的一個反射類)。但每個陣列都對應一個Class物件。通過RTTI(Run-Time Type Information)可以直接檢查Array的執行時型別,以及它的簽名,它的基類,還有其他很多事。在C++中,陣列雖然封裝了資料,但陣列名只是一個指標,指向陣列中的首個元素,既沒有屬性,也沒有方法可以呼叫。如下程式碼所示:

int main(){
	int a[] = {1, 2, 3, 4};
	int* pa = a;
	//無法訪問屬性,也不能呼叫方法。
	return 0;
}

所以C++中的陣列不是物件,只是一個資料的集合,而不能當做物件來使用。

Java中陣列的型別

Java是一種強型別的語言。既然是物件, 那麼就必須屬於一個型別,比如根據Person類建立一個物件,這個物件的型別就是Person。那麼陣列的型別是什麼呢?看下面的程式碼:

int[] arrI = {1, 2, 3, 4};
System.out.println(arrI.getClass().getName());

String[] arrS = new String[2];
System.out.println(arrS.getClass().getName());

String[][] arrsS = new String[2][3];
System.out.println(arrsS.getClass().getName());

OutPut:
[I
[Ljava.lang.String;
[[Ljava.lang.String;

arrI的型別為[ IarrS的型別是[Ljava.lang.String; , arrsS的型別是[[Ljava.lang.String;

所以,陣列也是有型別的。只是這個型別顯得比較奇怪。你可以說arrI的型別是int[],這也無可厚非。但是我們沒有自己建立這個類,也沒有在Java的標準庫中找到這個類。也就是說不管是我們自己的程式碼,還是在JDK中,都沒有如下定義:

public class int[] {
	
	// ...
}

這隻能有一個解釋,那就是這個陣列物件並不是從某個類例項化來的,而是由JVM直接建立的,同時這個直接建立的物件的父類就是Object,所以可以呼叫Object中的所有方法,包括你用到的toString()。

我們可以把陣列型別和8種基本資料型別一樣, 當做Java的內建型別,這種型別的命名規則是這樣的:

每一維度用一個[表示;開頭兩個[,就代表是二維陣列。
[後面是陣列中元素的型別(包括基本資料型別和引用資料型別)

在Java語言層面上,arrS是陣列,也是一個物件,那麼它的型別應該是String[],這樣說是合理的。但是在JVM中,他的型別為[java.lang.String。順便說一句普通的類在JVM裡的型別為 包名+類名,也就是全限定名。同一個型別在Java語言中和在虛擬機器中的表示可能是不一樣的。

Java中陣列的繼承關係

上面已經驗證了,陣列是物件,也就是說可以以操作物件的方式來運算元組。並且陣列在虛擬機器中有它特別的型別。既然是物件,遵循Java語言中的規則 -- Object是上帝, 也就是說所有類的頂層父類都是Object。陣列的頂層父類也必須是Object,這就說明陣列物件可以向上直接轉型到Object,也可以向下強制型別轉換,也可以使用instanceof關鍵字做型別判定。 這一切都和普通物件一樣。如下程式碼所示:

//1		在test1()中已經測試得到以下結論: 陣列也是物件, 陣列的頂層父類是Object, 所以可以向上轉型
int[] a = new int[8];
Object obj = a ; //陣列的父類也是Object,可以將a向上轉型到Object

//2		那麼能向下轉型嗎?
int[] b = (int[])obj;  //可以進行向下轉型

//3		能使用instanceof關鍵字判定嗎?
if(obj instanceof int[]){  //可以用instanceof關鍵字進行型別判定
    System.out.println("obj的真實型別是int[]");
}

參考資料

什麼是陣列?

Java和C的陣列區別

Java中陣列的特性

Java中的陣列是物件嗎? —— 看Sunny與胖君的回答