数组的初始化
数组是大多数编程语言都提供的一种复合结构,如果程序需要多个类型相同的变量时,就可以考虑定义一个数组。Java 语言的数组变量是引用类型的变量,因此具有 Java 独有的特性。
Java 数组是静态的
Java 语言是典型的静态语言,因此Java的数组是静态的,即当数组被初始化之后,该数组的长度是不可变的。Java 程序中的数组必须经过初始化才可使用。所谓初始化,就是为数组对象的元素分配内存空间,并为每个数组元素指定初始值。
数组的初始化有以下两种方式:
- 静态初始化:初始化时由程序员显式指定每个数组元素的初始值。由系统决定数组的长度。
- 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。
不管采用哪种方式初始化 Java 数组,一旦初始化完成,该数组的长度就不可改变,Java 语言允许通过数组的 length
属性来访问数组的长度。
public class ArrayTest {
public static void main(String[] args) {
//采用静态初始化方式初始化第一个数组
String[] books = new String[]{"疯狂 Java 讲义", "轻量级 Java EE 企业应用实战",
"疯狂 Ajax 讲义", "疯狂 XML 讲义"};
//采用静态初始化的简化形式初始化第二个数组
String[] names = {"孙悟空", "猪八戒", "白骨精"};
//采用动态初始化的语法初始化第三个数组
String[] strArr = new String[5];
//访问三个数组的长度
System.out.println("第一个数组的长度:" + books.length);
System.out.println("第二个数组的长度:" + names.length);
System.out.println("第三个数组的长度:" + strArr.length);
}
}
在上面的代码中初始化了3个数组。这3个数组的长度将会始终不变,程序输出3个数组的长度依次为4、3、5。
数组在内存中的示意图
执行动态初始化时,程序员之需指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些数组元素分配初始值。指定初始值时,系统将按如下规则分配初始值。
- 数组元素的类型是基本类型中的整数类型(byte、short、int 和 long),则数组元素的值是 0。
- 数组元素的类型是基本类型中的浮点类型(float、double),则数组元素的值是 0.0。
- 数组元素的类型是基本类型中的字符类型(char),则数组元素的值是 '\u0000'。
- 数组元素的类型是基本类型中的布尔类型(boolean),则数组元素的值是 false。
- 数组元素的类型是引用类型(类、接口和数组),则数组元素的值是 null。
不要同时使用静态初始化和动态初始化。也就是说,不要在进行数组初始化时,既指定数组的长度,也为每个数组元素分配初始值。
Java 的数组是静态的,一旦数组初始化完成,数组元素的内存空间分配即结束,程序只能改变数组元素的值,而无法改变数组的长度。
需要指出的是,Java 的数组变量是一种引用类型的变量,数组变量并不是数组本身,它只是指向堆内存中的数组对象。因此,可以改变一个数组变量所引用的数组,这样可以造成数组长度可变的假象。在上面的程序中增加以下几行代码:
books = names;
strArr = names;
System.out.println("books 数组的长度:" + books.length);
System.out.println("strArr 数组的长度:" + books.length);
//改变 books 数组变量所引用的数组的第二个元素值
books[1] = "唐僧";
System.out.println("names 数组的第二个元素是:" + books[1]);
在上面的程序中,将 books 数组变量、strArr 数组变量都指向 names 数组变量所有引用的数组,这样做的结果就是 books、strArr、names 这3个变量引用同一个数组对象。此时。3个引用变量和数组对象在内存中的分配如下图所示:
数组在内存中的示意图
从上图看出,此时 strArr、names 和 book 数组变量实际上引用同一个数组对象。因此,当访问 books 数组、strArr 数组的长度时,将看到3。这很容易造成一个假想:books 数组的长度从4变成了3。实际上,数组对象本身的长度并没有发生改变,变的是 books 数组变量。books 数组变量原本指向堆内存下面的数组,当执行了books = names;
语句之后,books 数组将改为指向堆内存中间的数组,而原来 books 变量所引用的数组的长度仍然是4。而且不会有任何引用变量引用堆内存下面的数组,因此它将成为垃圾,等着垃圾回收机制来回收。
数组一定要初始化吗
使用 Java 数组之前必须先初始化数组,也就是为数组元素分配内存空间,并指定初始值。实际上,如果真正掌握了 Java 数组在内存中的分配机制,那么完全可以换一个方式来初始化数组,或者说,数组无需经过初始化。
始终记住:Java 的数组变量是引用类型的变量,它并不是数组对象本身,只要让数组变量指向有效的数组对象,程序中即可使用该数组变量。
public class ArrayTest2 {
public static void main(String[] args) {
//定义并初始化 nums 数组
int[] nums = new int[]{3, 5, 20, 12};
//定义一个 prices 数组变量
int[] prices;
//让 prices 数组指向 nums 所引用的数组
prices = nums;
Arrays.stream(prices).forEach(num -> System.out.println(num));
//将 prices 数组的第三个元素赋值为34
prices[2] = 34;
//访问 nums 数组的第三个元素,将看到输出34
System.out.println("nums 数组的第三个元素的值是:" + nums[2]);
}
}
从上面的代码中可以看出,程序定义了 prices 之后,并未对 prices 数组进行初始化。当执行 init[] prices
之后,程序的内存分配如下图:
数组在内存中的示意图
从上图可以看出,此时 prices 数组变量还未指向任何有效的内存。未指向任何数组对象,此时程序还不可使用 prices 数组变量。
当程序执行 prices = nums;
之后,prices 变量将指向 nums 变量所引用的数组,此时 prices 变量和 nums 变量引用同一个数组对象。执行这条语句以后,prices 变量已经指向有效的内存及一个长度为4的数组对象,因此程序完全可以正常使用 prices 变量了。
对于数组变量来说,它并不需要所谓的初始化,只要让数组变量指向一个有效的数组对象,程序即可正常使用该数组变量。
对于 Java 程序中所有的引用变量,它们都不需要经过所谓的初始化操作,需要进行初始化操作的是该变量所引用的对象,比如,数组变量不需要进行初始化操作,而数组对象本身需要进行初始化;对象的引用变量也不需要进行初始化,而对象本身才需要进行初始化。
基本类型数组的初始化
对于基本类型数组而言,数组元素的值直接储存在对应的数组元素中,因此基本类型数组的初始化也比较简单:程序直接先为数组分配内存空间,再将数组元素的值存入对应内存里。
当静态初始化完成后,数组变量所引用的数组所占用的空间内存被固定下来,程序员只能改变各数组元素内的值,但不能移动该数组所占用的内存空间————既不能扩大该数组对象所占用的内存,也不能缩减该数组对象所占用的内存。
所有的局部变量都是放在栈内存里保存的,不管其是基本类型的变量,还是引用类型的变量,都是存储在各自的方法栈区中,但引用类型变量所引用的类型(包括数组、普通 Java 对象)则总是储存在堆内存中。
对于 Java 语言而言,堆内存中的对象(不管是数组对象、还是普通 Java 对象)通常不允许直接访问,为了访问堆内存中的对象,通常只能通过引用变量。引用变量本质上只是一个指针,只要程序通过引用变量访问属性,或者通过调用引用变量来调用方法,该引用变量将会由它所引用的对象代替。
public class ArrayTest3 {
public static void main(String[] args) {
//定义一个 int[] 类型数组变量,此时先让 iArr 指向了 null
//如果不指向如何对象或者 null 下面的语句在编译器编译的时候就会报错。
int[] iArr = null;
//只要不访问 iArr 的属性和方法,程序完全可以使用该数组变量
System.out.println(iArr);
//动态初始化数组,数组长度为5
iArr = new int[5];
//只有当 iArr 指向有效的数组对象后,才可访问 iArr 的属性
System.out.println(iArr.length);
}
}
NullPointerException
:当通过引用变量来访问实例属性,或者调用非静态方法时,如果该引用对象变量还未引用一个有效的对象,程序就会引发NullPointerException
运行时异常。
引用类型数组的初始化
引用类型数组的数组元素依然是引用类型,因此数组元素里储存的还是引用,它指向另一块内存,这块内存里储存了该引用变量所引用的对象(包括数组和 Java 对象)。
对于引用类型的数组而言,它的数组元素其实就是一个引用类型的变量,因此可以指向任何有效的内存————此处“有效”的意思指强类型的约束。比如对 String[] 类型的数组而言,它的每个元素都相当于 String 类型的变量,因此它的数组元素只能指向 String 对象。
使用数组
当数组引用变量指向一个有效的数组对象之后,程序就可通过该数组引用变量来访问数组对象。Java 语言不允许直接访问堆内存中的数据,因此无法直接访问堆内存的数组对象,程序将通过数组引用变量来访问数组。
Java 语言避免直接访问堆内存的数据可以保证程序更加健壮,如果程序直接访问并修改堆内存中的数据,可能破坏内存中的数据完整性,从而导致程序 Carsh。
数组元素就是变量
只要在已有数据类型之后增加方括号,就会产生一个新的数组类型,实例如下:
- int -> int[]
- String -> String[]
- int[] -> int[][]
当程序需要多个类型相同的变量来保存程序状态时,可以考虑使用数组来保存这些变量。当一个数组初始化完成,就相当于定义了多个类型相同的变量。
无论哪种类型的数组,其数组元素其实相当于一个普通变量,把数组类型之后的方括号去掉后得到的类型就是该数组元素的类型,实例如下:
- int[] -> int
- String[] -> String
- int[][] -> int[]
需要指出的是,main 方法声明的变量都属于局部变量,因此它们总是被保存在 main 方法栈中;但数组元素则作为数组对象的一部分,总是保存在堆内存中,不管它们是基本类型的数组元素,还是引用类型的数组元素。
没有多维数组
前面已经指出:只要在已有数据类型之后增加方括号,就会产生一个新的数组类型。如果已有的类型是 int,增加方括号是 int[]类型,这是一个i额数组类型;如果再以 int[] 类型为已有类型,增加方括号就得到 int[][] 类型,这依然是数组类型。
所谓的多维数组,其实只是数组元素依然是数组的1维数组;2维数组是数组元素是1维数组的数组;3维数组是数组元素是2维数组的数组,4维数组是数组元素是3维数组的数组 …… N 维数组是数组元素是 N-1 维数组的数组。
Java 允许将多维数组当成1维数组处理。初始化多维数组时可以先只初始化最左边的维数,此时该数组的每个元素都相当于一个数组引用变量,这些数组元素还需要进一步初始化。
public class ArrayTest4 {
public static void main(String[] args) {
//定义一个2维数组
int[][] a;
//把 a 当成1维数组进行初始化,初始化 a为一个长度为4的数组
//a数组的数组元素又是引用类型
a = new int[4][];
//把a数组当成1维数组,变量a数组的每个数组元素
Arrays.stream(a).forEach(ia -> System.out.println(ia));
//初始化a数组的第1个元素;
a[0] = new int[2];
//访问a数组的第1个元素所引指数组的第2个元素
a[0][1] = 6;
//a数组的第1个元素是1个1维数组,遍历这个1维数组
Arrays.stream(a[0]).forEach(ia -> System.out.println(ia));
}
}
数组的 length 属性应该返回系统为该数组所分配的连续内存空间的长度。
本文部分转载自:《疯狂Java 突破程序员基本功的16课》第一章