藏在Java數組的背後,你可能忽略的知識點

引言

概念

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

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

數組的定義和使用需要通過方括弧 []

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與胖君的回答

Tags: