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 对象。