1. 程式人生 > >藏在Java陣列的背後,你可能忽略的知識點

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

[TOC] ## 引言 ### 概念 **陣列**是資料呈線性排列的一種資料結構,它用一組連續的記憶體空間,來儲存一組相同資料型別的資料,表示一組相同型別的資料的集合,具有固定的長度,並且在記憶體中佔據連續的空間。 陣列是基本上所有語言都會有的一種資料型別,是我們在開發過程中經常會接觸到的,所以我們很有必要了解陣列的相關特性 **陣列的定義和使用需要通過方括號 `[]`。** > **Java 中,陣列是一種引用型別。** > > **Java 中,陣列是用來儲存固定大小的同類型元素。** ### 區別於C/C++陣列 **儲存結構區別:** **C陣列**:陣列空間是一次性給定的,優先訪問低地址,自底向上而放元素。 在記憶體中是連續儲存的,並且所有陣列都是連續的,都可作為一維陣列看待。 同時,C陣列是可以動態申請記憶體空間的,也就是可以動態擴容的,而Java陣列是不行的,當然Java也提供了`ArrayList`動態陣列類 如下圖,一個二維陣列就可以看成一個一維陣列,只是裡面存放的元素為一維陣列。所以C中的陣列是呈線性結構 ![在這裡插入圖片描述](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4dd19e6bf14436590737b24fedcc51e~tplv-k3u1fbpfcp-zoom-1.image) **Java**中的**陣列**就不一樣,在Java中,陣列都是引用實體變數,呈樹形結構,每一個葉子節點之間毫無關係,只有引用關係,每一個引用變數只引用一個實體。 `Java陣列`是會做邊界檢查的,所以當你越界訪問時,會丟擲 RuntimeException,而在C或C++是不做邊界檢查的 如圖,上面的例子是這樣表示的。在堆記憶體中,各個一維陣列的元素是連續的,但各個一維陣列之間不是連續存放的。 ![在這裡插入圖片描述](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/afecd218996d49c495c302aa4dd5533f~tplv-k3u1fbpfcp-zoom-1.image) **陣列是物件嗎?** C語言是面向過程的語言,在這裡不討論 C++中的陣列不是物件,只是一個數據的集合,而Java中的陣列是物件,這一點在後面會講到和驗證 ### 區別於容器 Java 中,容器是用來儲存多個物件的東西.嚴格來說是儲存物件的引用.因為物件實際的資料是放在另外的地方的.放在容器中的只是指向那塊記憶體區域的一個標識 Java 中,既然有了強大的容器,是不是就不需要陣列了?答案是不 誠然,大多數情況下,應該選擇容器儲存資料。 陣列和容器的區別有:`效率`、`型別識別` 、`以及存放基本型別的能力` 1、Java 中,陣列是一種效率最高的儲存和隨機訪問物件引用序列的方式。陣列的效率要高於容器(如 `ArrayList`) 2、型別識別方面,Java容器`List`、`Set`和`Map`在處理物件的時候就好像這些物件都沒有自己的型別一樣,容器將它所含的元素都看根類`Object`型別,這樣我們只需建立一種容器,就能把所有的型別的物件全部放進去。但是當取出資料時,需要我們自己進行型別轉換,這個問題在`Java`引入**泛型**進行型別檢查後,與容器類一起使用就可以解決型別轉換的問題 3、陣列可以持有值型別,而容器則不能(必須用到包裝類) ## 陣列特性 ### 隨機訪問 **非隨機訪問**:就是存取第N個數據時,必須先訪問前(N-1)個數據 (連結串列) **隨機訪問**:就是存取第N個數據時,不需要訪問前(N-1)個數據,直接就可以對第N個數據操作(陣列) **陣列**是如何做到隨機訪問的? 事實上,陣列的資料是按**順序儲存**在記憶體的連續空間內的,從上面的圖我們看出來,即便`Java`二維陣列是呈樹形結構,但是各個一維陣列的元素是連續的,通過arr[0],arr[1]等陣列物件指向一維陣列,所以每個資料的記憶體地址(在記憶體上的位置)都可以通過陣列下標算出,我們也就可以藉此直接訪問目標資料,也就是**隨機訪問** ### Java陣列與記憶體 上面這麼說還是有點懵懵懂懂的,可以畫圖解看看Java 陣列在記憶體中的儲存是怎麼樣的? 陣列物件(類比看作指標)儲存在棧中,陣列元素儲存在堆中 一維陣列: ![在這裡插入圖片描述](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a95225c2a960496d985156d829db5a07~tplv-k3u1fbpfcp-zoom-1.image) 二維陣列: ![在這裡插入圖片描述](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aea2983a36a1430a8cd4137653a5890e~tplv-k3u1fbpfcp-zoom-1.image) **精彩點評**:一維陣列在堆上連續的記憶體空間直接儲存值,二維陣列在連續的地址上儲存一維陣列的引用地址,一維陣列與一維陣列並不一定靠在一起,但是這些一維陣列內部的值是在連續地址上的。更高維的陣列繼續以此類推,只有最後一維陣列在連續地址上儲存值,其他緯度均在連續地址上儲存下一維度的引用地址。同維度的例項不一定靠在一起。 ### **解惑** **陣列下標為什麼是從0開始?** 前面說到陣列訪問資料時使用的是隨機訪問(通過下標可計算出記憶體地址),從陣列儲存的記憶體模型上來看,“下標”最確切的定義應該是“偏移(offset)”。如果用 a 來表示陣列的首地址,a[0] 就是偏移為 0 的位置,也就是首地址,a[k] 就表示偏移 k 個 type_size 的位置,所以計算 a[k] 的記憶體地址只需要用這個公式: ```text a[k]_address = base_address + k * type_size ``` 但是,如果陣列從 1 開始計數,那我們計算陣列元素 a[k] 的記憶體地址就會變為: ```text a[k]_address = base_address + (k-1)*type_size ``` 對比兩個公式,可以發現,從 0 開始編號,每次隨機訪問陣列元素都少了一次減法運算,對於 CPU 來說,就是少了一次減法指令, 提高了訪問的效率 ## 陣列的本質 ### Java中的陣列是物件嗎? Java和C++都是面向物件的語言。在使用這些語言的時候,我們可以直接使用標準的類庫,也可以使用組合和繼承等面向物件的特性構建自己的類,並且根據自己構建的類建立物件。那麼,我們是不是應該考慮這樣一個問題:在面向物件的語言中,陣列是物件嗎? 判斷陣列是不是物件,那麼首先明確什麼是物件,也就是物件的定義。在較高的層面上,物件是根據某個類創建出來的一個例項,表示某類事物中一個具體的個體。物件具有各種屬性,並且具有一些特定的行為。而在較低的層面上,站在計算機的角度,物件就是記憶體中的一個記憶體塊,在這個記憶體塊封裝了一些資料,也就是類中定義的各個屬性,所以,物件是用來封裝資料的。以下為一個Person物件在記憶體中的表示: ![在這裡插入圖片描述](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df9362cb3ab54d3fb3a6c4caae4e7744~tplv-k3u1fbpfcp-zoom-1.image) **注意**: 1、紅色矩形表示一個引用(地址)或一個基本型別的資料,綠色矩形表示一個物件,多個紅色矩形組合在一塊,可組成一個物件。 2、name在物件中只表示一個引用, 也就是一個地址值,它指向一個真實存在的字串物件。在這裡嚴格區分了引用和物件。 那麼在Java中,陣列滿足以上的條件嗎?在較高的層面上,陣列不是某類事物中的一個具體的個體,而是多個個體的集合。那麼它應該不是物件。而在計算機的角度,陣列也是一個記憶體塊,也封裝了一些資料,這樣的話也可以稱之為物件。以下是一個數組在記憶體中的表示: ![在這裡插入圖片描述](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c764e91478c9493bb080de1c5fd4139b~tplv-k3u1fbpfcp-zoom-1.image) 這樣的話, 陣列既可以是物件, 也可以不是物件。至於到底是不是把陣列當做物件,全憑Java的設計者決定。陣列到底是不是物件, 通過程式碼驗證: ```java int[] arr = new int[4]; int len = arr.length; //陣列中儲存一個欄位, 表示陣列的長度 //以下方法說明陣列可以呼叫方法,Java陣列是物件.這些方法是Object中的方法,所以可以肯定,陣列的最頂層父類也是Object arr.clone(); arr.toString(); ``` 從上面的程式碼來看,在陣列arr上, 可以訪問它的屬性,也可以呼叫一些方法。**這基本上可以認定,Java中的陣列也是物件,它具有java中其他物件的一些基本特點:封裝了一些資料,可以訪問屬性,也可以呼叫方法。所以答案是肯定的,陣列是物件。** 同時權威的[Java Language Specification](https://link.zhihu.com/?target=http%3A//docs.oracle.com/javase/specs/jls/se7/html/index.html)是這麼說的: > In the Java programming language, *arrays* are objects ([§4.3.1](https://link.zhihu.com/?target=http%3A//docs.oracle.com/javase/specs/jls/se7/html/jls-4.html%23jls-4.3.1)), are dynamically created, and may be assigned to variables of type Object ([§4.3.2](https://link.zhihu.com/?target=http%3A//docs.oracle.com/javase/specs/jls/se7/html/jls-4.html%23jls-4.3.2)). All methods of class Object may be invoked on an array. 這裡我就不給大家翻譯了,看不懂的有道翻譯一下 補充:[Java Language Specification](https://link.zhihu.com/?target=http%3A//docs.oracle.com/javase/specs/jls/se7/html/index.html) 裡關於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++中,陣列雖然封裝了資料,但陣列名只是一個指標,指向陣列中的首個元素,既沒有屬性,也沒有方法可以呼叫。如下程式碼所示: ```cpp int main(){ int a[] = {1, 2, 3, 4}; int* pa = a; //無法訪問屬性,也不能呼叫方法。 return 0; } ``` 所以C++中的陣列不是物件,只是一個數據的集合,而不能當做物件來使用。 ### Java中陣列的型別 Java是一種強型別的語言。既然是物件, 那麼就必須屬於一個型別,比如根據Person類建立一個物件,這個物件的型別就是Person。那麼陣列的型別是什麼呢?看下面的程式碼: ```java 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`的型別為`[ I` ,`arrS`的型別是`[Ljava.lang.String;` , `arrsS`的型別是`[[Ljava.lang.String`; 所以,**陣列也是有型別的。**只是這個型別顯得比較奇怪。你可以說`arrI`的型別是`int[]`,這也無可厚非。但是我們沒有自己建立這個類,也沒有在`Java`的標準庫中找到這個類。也就是說不管是我們自己的程式碼,還是在`JDK`中,都沒有如下定義: ```java 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關鍵字做型別判定。 這一切都和普通物件一樣。如下程式碼所示: ```java //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[]"); } ``` ## 參考資料 > [什麼是陣列?](https://zhuanlan.zhihu.com/p/105962783) > > [Java和C的陣列區別](https://blog.csdn.net/qq_42913794/article/details/89077825) > > [Java中陣列的特性](https://blog.csdn.net/zhangjg_blog/article/details/16116613#t1) > > [Java中的陣列是物件嗎?](https://www.zhihu.com/question/26297216/answer/32507772) —— 看Sunny與胖君