参考资料
[1]. 疯狂Java讲义第三版,李刚
数组类型
Java数组的一些特点
- Java的数组要求所有的数组元素具有相同的数据类型。
- 因为类和类之间可以支持继承关系,这样可能产生一个数组里可以存放多种数据类型的假象。
- 数组一旦初始化完成,数组在内存中所占的空间就固定下来,因此数组的长度将不可改变,即使清空某个元素,但它所占的空间依然保留。
定义数组
定义数组的语法
// 常用的定义方法
type[] arrayName;
// 不常用的定义方法
type arrayName[];
数组是一种引用类型的变量,因为使用它定义一个变量时,仅仅表示定义了一个引用变量(也就是定义了一个指针),这个引用变量还未指定任何有效的内存,因此定义数组时不能指定数组的长度。而且由于定义数组只是定义了一个引用变量,并未指向任何有效的内存空间,所以还没有内存空间来存储数组元素,因此这个数组也不能使用,只有对数组进行初始化后才可以使用。
数组的初始化
Java语言中数组必须先初始化,然后才可以使用,所谓初始化,就是就数组的数组元素分配内存空间,并为每个数组元素赋初始值。
静态初始化
初始化时由程序员显示指定每个数组元素的初始值,由系统决定数组长度。
语法格式如下:
// 标准版
type arrayName = new type[] {element1, element2, element3, element4 ...};
// 简化版
type arrayName = {element1, element2, element3, element4 ...};
使用示例:
// 定义一个int数组类型的变量,变量名为intArr
int[] intArr;
// 使用静态初始化,初始化数组时只指定数组元素的初始值,不指定数组长度。
intArr = new int[]{5, 6, 8, 20};
// 定义一个Object数组类型的变量,变量名为objArr
Object[] objArr;
// 使用静态初始化,初始化数组时数组元素的类型是
// 定义数组时所指定的数组元素类型的子类
objArr = new String[] {"a", "b"};
Object[] objArr2;
objArr2 = new Object[] {"c", "d"};
// 简化版
int[] ints1 = {1, 2, 8, 10};
子类实例是一种特殊的父类实例,在上面的程序中,String类型是Object类型的子类,即字符串是一种特殊的Object实例,所以可以声明变量类型的子类赋值到父类。
动态初始化
初始化时程序员只指定数组长度,由系统为数组元素分配初始值。
动态初始化的语法格式:
arrayName = new type[length];
语法示例:
// 数组的定义和初始化同时完成,使用动态初始化语法
int[] prices = new int[5];
// 数组的定义和初始化同时完成,初始化数组时元素的类型是定义数组时元素类型的子类
Object[] books = new String[4];
执行动态初始化时,程序员只需要指定数组的长度,即为每个数组元素指定所需的内存空间,系统将负责为这些元素分配初始值。指定初始值时,系统将按如下规则分配初始值。
1. 基本类型
1.1 整数类型(byte、short、int、long),则数组元素的值为0。
1.2 浮点类型(folat、double),则数组元素的值是0.0。
1.3 字符类型(char),则数组元素的值是’\u0000’。
1.4 布尔类型(boolean),则数组元素的值是false。
2. 引用类型(类、接口、数组),则数组元素的值是null
使用数组
使用索引值访问到数组以后,就可以把它当成一个普通的变量使用,类型就是数组在声明时的类型。
// 数组的定义和初始化同时完成,使用动态初始化语法
int[] prices = new int[5];
System.out.println(prices[1]);
// 为prices的第二个元素赋值为110
prices[1] = 110;
System.out.println(prices[1]);
// 试图访问的数组元素索引值超过数组长度
// 将引发ArrayIndexOutOfBoundsException异常
System.out.println(prices[100]);
// 利用数组的length属性,来遍历数组
for (int i = 0 ; i < prices.length; i++)
{
System.out.println("prices: "+prices[i]);
}
foreach循环
foreach循环会自动迭代数组的每个元素,当每个元素都被迭代一次以后,foreach循环自动结束
foreach循环的语法格式如下:
for (type variableName : array | collection)
{
// variableName 自动迭代访问每个元素...
}
foreach循环遍历所有元素的使用示例:
String[] Letters = {"a", "b", "c", "d"};
// 使用foreach循环来遍历数组元素
// Letter会自动迭代每个数组元素
for (String Letter: Letters
) {
System.out.println(Letter);
}
当使用foreach循环来迭代输出数组元素或集合元素时,通常不要对循环变量赋值。
String[] Letters = {"a", "b", "c", "d"};
// 使用foreach循环来遍历数组元素
for (String Letter: Letters
) {
Letter = "e";
System.out.println("第一次:"+Letter);
}
// 再次遍历可以看到每个元素的值没有任何改变
// 很明显Letter只是一个临时变量,系统将元素的值赋给了它
// 所以不要再foreach循环中遍历
for(String Letter : Letters)
{
System.out.println("第二次:"+Letter);
}
深入数组
内存中的数组
实际的数组对象本存储在堆(heap)内存中,如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈(stack)内存中。数组在内存中的存储示意图如下:
如果堆内存中数组不再有任何引用变量指向自己,则这个数组将成为垃圾,这个数组所占的内存将会被系统的垃圾回收机制回收,可以将该数组变量赋值null
来达成资源回收的目的。
// 定义并初始化数组,使用静态初始化
int[] a = {1, 2, 20};
// 定义并初始化数组,使用动态初始化
int[] b = new int[5];
// 输出b数组的长度
System.out.println("a数组的长度是:" + a.length);
for (int i = 0; i < a.length; i++)
{
System.out.println(a[i]);
}
// 输出b数组的长度
System.out.println("b数组的长度是:"+ b.length);
for (int i = 0; i < b.length; i++)
{
System.out.println(b[i]);
}
// 将数组b的引用堆内存地址修改为a
b = a;
System.out.println("再次输出数组b的长度:" + b.length);
上面代码的运行原理如下:
通过上面的代码和图可以知道,变量放在栈内存中,数组的实体放在堆内存中,每次变动的只是栈内存中对堆内存中数组的引用。
基本类型数组的初始化
// 定义一个int[] 类型的数组变量
int[] iArr;
// 动态初始化数组,数组长度为5
iArr = new int[5];
// 采用循环方式为每个数组元素赋值
for (int i = 0; i < iArr.length; i++)
{
iArr[i] = i+10;
}
下面用图来说明执行过程:
当执行int[] iArr;
,仅在栈内存中定义了一个空引用,并未指向任何有效的内存,无法指定数组的长度。
当执行iArr = new int[5];
动态初始化后,系统将负责为该数组分配内存空间,并分配默认的初始值,所有的数组元素都被赋值为0
每个数组元素的值直接存储在对应的内存中,操作基本类型数组的数组元素时,实际上相当于操作基本类型的变量。循环赋值以后如图:
引用类型数组的初始化
下面定义一个Person
类(所有类都是引用类型)
public class Person {
public int age;
public double height;
public void info()
{
System.out.println("我的年龄是:" + age + ",我的身高是:" + height);
}
}
下面程序定义一个Person
数组,接着动态初始化这个Person
数组,并为这个数组的每个数组元素指定初始值,代码如下:
// 定义一个students数组变量,其类型是Person[]
Person[] students;
// 执行动态初始化
students = new Person[2];
// 创建一个Person实例,并将这个Person实例赋给zhang变量
Person zhang = new Person();
zhang.age = 15;
zhang.height = 158;
// 创建一个Person实例,并将这个Person实例赋给lee变量
Person lee = new Person();
// 为lee所引用的Person对象的age、height赋值
lee.age = 16;
lee.height = 161;
// 将张变量的值赋给第一个数组元素
students[0] = zhang;
// 将lee变量的值赋给第二个数组元素
students[1] = lee;
// 下面两行代码的结果完全一样,因为lee
// 和students[1]指向的是同一个Person实例
lee.info();
students[1].info();
执行Person[] students;代码时,只在栈内存中定义了一个引用变量。
执行动态初始化 students = new Person[2];
创建zhang和lee实例
// 创建一个Person实例,并将这个Person实例赋给zhang变量
Person zhang = new Person();
zhang.age = 15;
zhang.height = 158;
// 创建一个Person实例,并将这个Person实例赋给lee变量
Person lee = new Person();
// 为lee所引用的Person对象的age、height赋值
lee.age = 16;
lee.height = 161;
将变量引用给students变量
// 将张变量的值赋给第一个数组元素
students[0] = zhang;
// 将lee变量的值赋给第二个数组元素
students[1] = lee;
此时,zhang
和students[0]
指向同一个内存区,而且他们都是引用类型变量,因此通过zhang和students[0]
来访问Person
实例的实例变量和方法的效果完全一样,不论修改students[0]
所指向的Person
实例的实例变量,还是修改zhang
变量所执行的Person实例的实例变量,所修改的是同一个内存区,所以必然互相影响。
没有多维数组
Java语言里的数组类型是引用类型,因此数组变量其实就是一个引用,这个引用指向真实的数组内存。数组元素的类型也可以是引用,如果数组元素的引用再次指向真实的数组内存,这种情形看上去很像多维数组。
下面程序示范了如何把二维数组当成一维数组处理。
// 定义一个二维数组
int[][] a;
// 把a当成一维数组进行初始化,初始化a是一个长度为4的数组
// a数组的数组元素又是引用类型
a = new int[4][];
for (int i = 0; i < a.length; i++)
{
System.out.println(a[i]);
}
// 初始化a数组的第一个元素
a[0] = new int[2];
// 访问a数组的第一个元素所指数组的第二个元素
a[0][1] = 6;
// a数组的第一个元素是一个一维数组,遍历这个一维数组
for (int i = 0; i < a[0].length; i++)
{
System.out.println(a[0][i]);
}
当定义一个二维数组int[][] a;
的时候,只是在栈区生成了一个a引用变量
程序对a数组执行初始化:a = new int[4][];
,这行代码让a变量指向一块长度为4的数组内存,这个长度为4的数组里每个数组元素都是引用类型(数组类型),系统为这些数组元素分配默认的初始值null:
现在为a[0]
执行动态初始化,这样,所有的元素都是0,然后为a[0][1]
的元素赋值6
// 初始化a数组的第一个元素
a[0] = new int[2];
// 访问a数组的第一个元素所指数组的第二个元素
a[0][1] = 6;
如果一次指定每一维的大小,则代码如下:
// 同时初始化二维数组的两个维数
int[][] b = new int[3][4];
上面代码将定义一个b数组变量,这个数组变量指向一个长度为3的数组,这个数组的每个数组元素又是一个数组类型,它们各指向对应长度为4的int[]数组,每个数组元素的值为0。这行代码执行后在内存中的存储示意图如下图:
还可以使用静态初始化方式来初始化二维数组。使用静态初始化方式来初始化二维数组时,二维数组的每个数组元素都是一维数组,因此必须指定多个一维数组作为二维数组的初始化值,代码如下:
// 使用静态初始化语法来初始化一个二维数组
String[][] str1 = new String{new String[3], new String[]{"hello"}};
// 使用简化的静态初始化语法来初始化二维数组
String[][] str2 = new String{new String[3], new String[]{"hello"}};