(二十六)、Java數組在內存中如何存放與分配

Java中有兩種類型的數組:html

  基本數據類型數組;程序員

  對象數組;編程

當一個對象使用關鍵字「new」建立時,會在堆上分配內存空間,而後返回對象的引用,這對數組來講是同樣的,由於數組也是一個對象。數組

 

一維數組spa

int[] arr = new int[3];

在以上代碼中,arr變量存放了數組對象的引用;若是你建立了一個空間大小爲10的整型數組,狀況是同樣的,一個數組對象所佔的空間在堆上被分配,而後返回其引用。.net

 

二維數組指針

那麼二維數組是如何存儲的呢?事實上,在Java中只有一維數組,二維數組是一個存放了數組的數組,以下代碼及示意圖:code

int[][] arr = new int[3][]; arr[0] = new int[3]; arr[1] = new int[4]; arr[2] = new int[5];

對於多維數組,道理是同樣的。htm

 

 

Java數組是靜態的對象

  Java 語言是典型的靜態語言,所以 Java 數組是靜態的,即當數組被初始化以後,該數組 所佔的內存空間、數組長度都是不可變的。Java 程序中的數組必須通過初始化纔可以使用。所謂初始化,即建立實際的數組對象,也就是在內存中爲數組對象分配內存空間,併爲每一個數組 元素指定初始值。

數組的初始化有如下兩種方式:

  靜態初始化:初始化時由程序員顯式指定每一個數組元素的初始值,由系統決定數組長度。

  動態初始化:初始化時程序員只指定數組長度,由系統爲數組元素分配初始值。

無論採用哪一種方式初始化Java數組,一旦初始化完成,該數組的長度就不可改變,Java語言容許經過數組的length屬性來訪問數組的長度。示例以下:

public class ArrayTest{ public static void main(String[] args) { //採用靜態初始化方式初始化第一個數組
        String[] books = new String[]{"1", "2", "3", "4"}; //採用靜態初始化的簡化形式初始化第二個數組
        String[] names = {"孫悟空", "豬八戒", "白骨精"}; //採用動態初始化的語法初始化第三個數組
        String[] strArr = new String[5]; //訪問三個數組的長度
        System.out.println("第一個數組的長度: " + books.length); System.out.println("第二個數組的長度: " + names.length); System.out.println("第三個數組的長度: " + strArr.length); } }

上面代碼聲明並初始化了三個數組,這三個數組的長度將會始終不變。

 

前面已經指出,Java 語言的數組變量是引用類型的變量。

books、names 、strArr 這三個變量,以及各自引用的數組在內存中的分配示意圖如圖所示:

從圖1.1能夠看出,對於靜態初始化方式而言,程序員無須指定數組長度,指定該數組的 數組元素,由系統來決定該數組的長度便可。例如 books 數組,爲它指定了四個數組元素,它 的長度就是4 ;對於names 數組,爲它指定了三個元素,它的長度就是3 。

執行動態初始化時,程序員只需指定數組的長度,即爲每一個數組元素指定所需的內存空間, 系統將負責爲這些數組元素分配初始值。指定初始值時,系統將按以下規則分配初始值。

  • 數組元素的類型是基本類型中的整數類型(byte 、short、int 和long ),則數組元素的值是0 。
  • 數組元素的類型是基本類型中的浮點類型(float 、double ),則數組元素的值是0.0。
  • 數組元素的類型是基本類型中的字符類型(char ),則數組元素的值是'\u0000'。
  • 數組元素的類型是基本類型中的布爾類型(boolean),則數組元素的值是false 。
  • 數組元素的類型是引用類型(類、接口和數組),則數組元素的值是null 。

    Java 數組是靜態的,一旦數組初始化完成,數組元素的內存空間分配即結束,程序只能改變數組元素的值,而沒法改變數組的長度。

 

須要指出的是,Java 的數組變量是一種引用類型的變量,數組變量並非數組自己,它 只是指向堆內存中的數組對象。所以,能夠改變一個數組變量所引用的數組,這樣能夠形成數 組長度可變的假象。

假設,在上面程序的後面增長以下幾行。

public class test{ public static void main(String[] args) { //採用靜態初始化方式初始化第一個數組
        String[] books = new String[]{"1", "2", "3", "4"}; //採用靜態初始化的簡化形式初始化第二個數組
        String[] names = {"孫悟空", "豬八戒", "白骨精"}; //採用動態初始化的語法初始化第三個數組
        String[] strArr = new String[5]; //讓books數組變量、strArr 數組變量指向names 所引用的數組
        books = names; strArr = names; System.out.println("--------------"); System.out.println("books 數組的長度:" + books.length); System.out.println("strArr 數組的長度:" + strArr.length); // 改變books 數組變量所引用的數組的第二個元素值
        books[1] = "唐僧"; System.out.println("names 數組的第二個元素是:" + books[1]);
     System.out.println("names 數組的第二個元素是:" + strArr[1]); } }

 

讓books 數組變量、strArr 數組變量都指向names 數組變量所引 用的數組,這樣作的結果就是books、strArr、names 這三個變量引用同一個數組對象。此時, 三個引用變量和數組對象在內存中的分配示意圖如圖 所示。

從圖1.2能夠看出,此時 strArr、names 和books 數組變量實際上引用了同一個數組對象。

所以,當訪問 books 數組、strArr 數組的長度時,將看到輸出 3。這很容易形成一個假象:books 數組的長度從4 變成了3。實際上,數組對象自己的長度並無發生改變,只是 books 數組變 量發生了改變。books 數組變量本來指向圖 1.2下面的數組,當執行了books = names;語句以後,books 數組將改成指向圖1.2 中間的數組,而原來books 變量所引用的數組的長度依然是4 。

從圖1.2 還能夠看出,原來 books 變量所引用的數組的長度依然是 4 ,但再也不有任何引用 變量引用該數組,所以它將會變成垃圾,等着垃圾回收機制來回收。此時,程序使用books、 names 和strArr 這三個變量時,將會訪問同一個數組對象,所以把 books 數組的第二個元素賦 值爲「唐僧」時,names 數組的第二個元素的值也會隨之改變。

 

基本數據類型數組的初始化

對於基本類型數組而言,數組元素的值直接存儲在對應的數組元素中,所以基本類型 數組的初始化比較簡單:

  程序直接先爲數組分配內存空間,再將數組元素的值存入對應內 存裏。

下面程序採用靜態初始化方式初始化了一個基本類型的數組對象。

public class PrimitiveArrayTest{ public static void main(String[] args) { //定義一個int[]類型的數組變量
        int[] iArr; //靜態初始化數組,數組長度爲4
        iArr = new int[]{2, 5, -12, 50}; } }

上面代碼的執行過程表明了基本類型數組初始化的典型過程。下面將結合示意圖詳細介紹這段代碼的執行過程。

執行第一行代碼int[] iArr;時,僅定義一個數組變量,此時內存中的存儲示意圖如圖所示。

執行了int[] iArr; 代碼後,僅在 main 方法棧中定義了一個 iArr 數組變量,它是一個引用類 型的變量,並未指向任何有效的內存,沒有真正指向實際的數組對象。此時還不能使用該數組 對象。

當執行iArr = new int[]{2,5,-12,20}; 靜態初始化後,系統會根據程序員指定的數組元素來決 定數組的長度。此時指定了四個數組元素,系統將建立一個長度爲4 的數組對象,一旦該數組 對象建立成功,該數組的長度將不可改變,程序只能改變數組元素的值。此時內存中的存儲示 意圖如圖所示。

靜態初始化完成後,iArr 數組變量引用的數組所佔用的內存空間被固定下來,程序員只能 改變各數組元素內的值。既不能移動該數組所佔用的內存空間,也不能擴大該數組對象所佔用 的內存,或縮減該數組對象所佔用的內存。

 

全部局部變量都是放在棧內存裏保存的,無論其是基本類型的變量,還 是引用類型的變量,都是存儲在各自的方法棧內存中的;

但引用類型的變量所引用的對象(包 括數組、普通的Java 對象)則老是存儲在堆內存中。

對於Java 語言而言,堆內存中的對象(不論是數組對象,仍是普通的 Java 對象)一般不 容許直接訪問,爲了訪問堆內存中的對象,一般只能經過引用變量。這也是很容易混淆的地方。 例如,iArr 本質上只是main 棧區的引用變量,但使用 iArr.length 、iArr[2] 時,系統將會自動變 爲訪問堆內存中的數組對象。

對於不少Java 程序員而言,他們最容易混淆的是:引用類型的變量什麼時候只是棧內存中的 變量自己,什麼時候又變爲引用實際的Java 對象。其實規則很簡單:引用變量本質上只是一個指 針,只要程序經過引用變量訪問屬性,或者經過引用變量來調用方法,該引用變量就會由它所 引用的對象代替。

public class PrimitiveArrayTest{ public static void main(String[] args) { //定義一個int[]類型的數組變量
        int[] iArr = null; //只要不訪問iArr 的屬性和方法,程序徹底可使用該數組變量
        System.out.println(iArr); //// 動態初始化數組,數組長度爲5
        iArr = new int[5]; //只有當iArr 指向有效的數組對象後,下面纔可訪問iArr 的屬性
        System.out.println(iArr.length); //
 } }

上面程序中兩行粗體字代碼兩次訪問iArr 變量。

對於①行代碼而言,雖然此時的iArr 數 組變量並未引用到有效的數組對象,但程序在①行代碼處並不會出現任何問題,由於此時並未 經過iArr 訪問屬性或調用方法,所以程序只是訪問iArr 引用變量自己,並不會去訪問iArr 所 引用的數組對象。

對於②行代碼而言,此時程序經過iArr 訪問了length 屬性,程序將自動變 爲訪問iArr 所引用的數組對象,這就要求iArr 必須引用一個有效的對象。

 

有過一些編程經驗,應該常常看到一個Runtime 異常: NullPointerException (空指針異常)。當經過引用變量來訪問實例屬性,或者調 用非靜態方法時,若是該引用變量還未引用一個有效的對象,程序就會引起 NullPointerException 運行時異常。

 

引用類型數組的初始化

引用類型數組的數組元素依然是引用類型的,所以數組元素裏存儲的仍是引用,它指向另外一塊內存,這塊內存裏存儲了該引用變量所引用的對象(包括數組和Java 對象)。

 

爲了說明引用類型數組的運行過程,下面程序先定義一個Person 類,而後定義一個 Person[]數組,並動態初始化該Person[]數組,再顯式地爲數組的不一樣數組元素指定值。該程序代碼以下。

class Person{ public int age; public double height; public void info(){ System.out.println("個人年齡是:" + age + ",個人身高是:" + height); } } public class ReferenceArrayTest{ public static void main(String[] args) { // 定義一個students 數組變量,其類型是Person[]
 Person[] students; // 執行動態初始化
        students = new Person[2]; System.out.println("students所引用的數組的長度是:" + students.length); //// 建立一個Person 實例,並將這個Person 實例賦給 leslie 變量
        Person leslie = new Person(); // 爲leslie 所引用的Person 對象的屬性賦值
        leslie.age = 22; leslie.hight = 180; // 建立一個Person 實例,並將這個Person 實例賦給lee 變量
        Person lee = new Person(); lee.age = 21; lee.hight = 178; // 將leslie 變量的值賦給第一個數組元素
        students[0] = leslie; // 將lee 變量的值賦給第二個數組元素
        students[1] = lee; // 下面兩行代碼的結果徹底同樣, // 由於lee 和students[1]指向的是同一個Person 實例
 lee.info(); students[1].info(); } }

上面代碼的執行過程表明了引用類型數組的初始化的典型過程。下面將結合示意圖詳細介紹這段代碼的執行過程。

執行Person[] students;代碼時,這行代碼僅僅在棧內存中定義了一個引用變量,也就是一個指針,這個指針並未指向任何有效的內存區。此時內存中的存儲示意圖如圖所示。

在圖1.6中的棧內存中定義了一個 students 變量,它僅僅是一個空引用,並未指向任何有 效的內存,直到執行初始化,本程序對 students 數組執行動態初始化。動態初始化由系統爲數 組元素分配默認的初始值null ,即每一個數組元素的值都是 null 。執行動態初始化後的存儲示意 圖如圖所示。

從圖1.7 中能夠看出,students 數組的兩個數組元素都是引用,並且這兩個引用並未指 向任何有效的內存,所以,每一個數組元素的值都是 null 。此時,程序能夠經過students 來 訪問它所引用的數組的屬性,所以在①行代碼處經過 students 訪問了該數組的長度,此時 將輸出2 。

students 數組是引用類型的數組,所以 students[0] 、students[1] 兩個數組元素至關於兩個引 用類型的變量。若是程序只是直接輸出這兩個引用類型的變量,那麼程序徹底正常。但程序依 然不能經過students[0] 、students[1] 來調用屬性或方法,所以它們還未指向任何有效的內存區, 因此這兩個連續的Person 變量(students 數組的數組元素)還不能被使用。

接着,程序定義了leslie 和lee 兩個引用變量,並讓它們指向堆內存中的兩個Person 對象,此時的leslie、lee 兩個引用變量存儲在 main 方法棧區中,而兩個 Person 對象則存儲在堆內存中。此時的內存存儲示意圖如圖所示。

對於leslie、lee 兩個引用變量來講,它們能夠指向任何有效的Person 對象,而students[0] 、 students[1] 也能夠指向任何有效的Person 對象。從本質上來看,leslie、lee、students[0] 、students[1] 可以存儲的內容徹底相同。接着,程序執行students[0] = leslie;和students[1] = lee; 兩行代碼, 也就是讓leslie 和students[0] 指向同一個 Person 對象,讓 lee 和students[1] 指向同一個Person 對象。此時的內存存儲示意圖如圖 所示。

從圖1.9 中能夠看出,此時 leslie 和students[0] 指向同一個內存區,並且它們都是引用類 型的變量,所以經過 leslie 和students[0] 來訪問Person 實例的屬性和方法的效果徹底同樣。不 論修改students[0] 所指向的 Person 實例的屬性,仍是修改 leslie 變量所指向的 Person 實例的 屬性,所修改的實際上是同一個內存區,因此必然互相影響。同理,lee 和students[1] 也是引用 到同一個Person 對象,也有相同的效果。

前面已經提到,對於引用類型的數組而言,它的數組元素其實就是一個引用類型的變量, 所以能夠指向任何有效的內存——此處「有效」的意思是指強類型的約束。好比,對 Person[] 類型的數組而言,它的每一個數組元素都至關於Person 類型的變量,所以它的數組元素只能指 向Person 對象。

 

引自:https://blog.csdn.net/nioutian/article/details/81609714

https://www.cnblogs.com/chenpi/p/5489732.html

相關文章
相關標籤/搜索