一、内存中的数组
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可以通过该数组变量来访问数组元素。
实际的数组对象被存储在堆(heap)内存中;如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈(stack)内存中。数组在内存中的存储示意图:
如果需要访问如图所示的堆内存中的数组元素,则程序只能通过p[index]的形式实现。即数组引用变量是访问堆内存中数组元素的根本方式。
堆内存和栈内存:
堆(heap)内存:Java虚拟机启动时分配一块永久的、很大的内存区。堆内存只有一块。
栈(stack)内存:每个方法运行时分配一块临时的、很小的内存区。每个方法都有自己的栈区,当方法结束时,对于的栈区就会被销毁。
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放到这个栈内存中,随着方法的执行结束,这个方法的内存栈也将随之销毁。因此,所有在方法中定义的局部变量都是在栈内存中的;在程序创建一个对象时,这个对象将被保存在运行时的数据区中,以便重复利用(因为对象的创建成本比较大),这个运行时数据区就是堆内存。堆内存的对象不会随着方法的结束而消失,即使方法结束后,这个对象还可以被其他另外一个引用变量所引用(在方法的参数传递中很常见),则这个对象依然不会被销毁。只有当一个对象没有任何变量引用它时,系统的垃圾回收器才会在合适的时候回收它。
如果堆内存中的数组不在有任何引用变量指向自己,则这个数组将会成为垃圾,该数组所占的内存也会被系统的垃圾回收器回收。因此为了让垃圾回收器回收一个数组所占的内存空间,可以将该数组便便赋值为null,也就切断数组引用变量和实际数组之间的引用关系,实际的数组也就成为垃圾。
只要类型相互兼容,就可以让一个数组变量指向另一个实际的数组,这样会让人产生数组长度可变的错觉。
1 class ArrayTest 2 { 3 public static void main(String[] args) 4 { 5 //静态方法定义数组 6 int[] a={1,3,5}; 7 //动态初始化定义数组 8 var b=new int[4]; 9 10 System.out.println("数组b的长度为:"+b.length); 11 //循环输出数组a,b 12 for(int i:a) 13 { 14 System.out.print(" "+i); 15 } 16 System.out.print(" "); 17 for(int i=0;i<b.length;i++) 18 { 19 System.out.print(" "+b[i]); 20 if(i==b.length-1) 21 System.out.print(" "); 22 } 23 //因为a、b都是int[]引用类型,所以可以让b指向a引用指向的数组 24 b=a; 25 System.out.println("指向a指向的数组后引用变量b指向的数组长度:"+b.length); 26 } 27 } 28 ---------- 运行java(捕获窗口) ---------- 29 数组b的长度为:4 30 1 3 5 31 0 0 0 0 32 指向a指向的数组后引用变量b指向的数组长度:3 33 34 输出完成 (耗时 0 秒) - 正常终止
在变量与b变量都引用了第一个数组后,此时在堆内存的原数组元素失去了引用,变成了垃圾,等待垃圾回收器来回收它。
二、基本类型的数组的初始化
对于基本类型的数组而言,数组元素的值直接存储在对应的数组元素中,因此,初始化数组时,先为该数组分配内存空间,然后直接将数组元素的值存入对应数组元素中。
class PrimitiveArrayTest { public static void main(String[] args) { //定义一个int[]类型的数组变量 int[] array; //动态初始化数组,数组长度为5 array=new int[5]; //采用循环方式为数组每个元素赋值 for(var i=0;i<array.length;i++) { array[i]=i+10; } //循环输出数组 for(int i:array) { System.out.println(i); } } } ---------- 运行java(捕获窗口) ---------- 10 11 12 13 14 输出完成 (耗时 1 秒) - 正常终止
执行int[] array,此时分配一个栈内存和一个堆内存(java虚拟机启动的时候就有),定义了一个空引用,这个引用变量并未指向任何有效的内存,也就无法知道数组的长度
array=new int[5],动态初始化后,系统将为该数组分配内存存储空间,并分配默认初始化值:所有数组元素赋值为0。
for(var i=0;i<array.length;i++)
{
array[i]=i+10;
}
循环为数组赋值
三、引用类型的初始化
基本类型的赋值:直接将该值存入变量的内存中。
引用类型的赋值:将该引用对象的第一个内存单元的编号(首地址)存入变量。
引用类型数组的数组元素时引用,因此情况更加复杂。每个数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了有效数据。
定义一个Person类
1 class Person 2 { 3 public int age; 4 public double height; 5 //定义一个方法 6 public void info() 7 { 8 System.out.println("年龄:"+age+",身高:"+height); 9 } 10 }
下面程序定义一个Person[]数组,接着初始化这个Person[]数组,并为这个数组的每个元素指定值。
1 public class ReferenceArrayTest 2 { 3 public static void mian(Srting[] args) 4 { 5 //定义一个sudentd数组,类型Person[] 6 Person[] students; 7 //执行初始化 8 students=new Person[2]; 9 10 //创建两个Person实例 11 var zhang=new Person(); 12 zhang.age=15; 13 zhang.height=158; 14 15 var lee=new Person(); 16 lee.age=16; 17 lee.height=168; 18 19 students[0]=zhang; 20 students[1]=lee; 21 22 //下面代码的执行结果完全一样 23 lee.info(); 24 students[0].info(); 25 } 26 }
执行Person[] students;代码时,这行代码仅仅在栈内存中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效内存区。
执行动态初始化,默认由系统指定初始值null。
students=new Person[2];
然后代码定义了zhang和lee两个Person实例,实际上分配了四个内存,在栈内存储zhang和lee两个引用变量,还在堆内存储两个Person实例,如下所示:
students[0]=zhang;
students[1]=lee;
从图可以看出zhang和students[0]同时指向一个内存区,且都为引用变量,因此通过zhang和students[0]来访问Person实例的方法和变量效果是一样的,无论修改students[0]指向的实例还是zhang指向的实例,所修改的其实是一个内存区。
四、没有多维数组
java语言的数组类型还是引用类型,这个引用指向真实的数组内存。数组元素的类型也可以是引用,如果数组元素的引用再次指向真实的数组内存,这种情况看上去就很像多维数组。
定义一个变量的用法:type varName,type是变量类型。如果定义一个引用变量的数组类型,只需要将这个type具体成int[]。
二维数组的定义:
type[][] arrayName;
从采用上面的语法格式来定义二维数组,但它的实质还是一维数组,只是数组元素还是引用,数组元素里保存的引用指向一位数组。
对二维数组的初始化,同样可以将数组看成以为数组初始化,把这个“二维数组”当成一一维数组,其元素类型是type[]类型,可以采用以下语法进行初始化
arrayName=new type[length][]
上面的语法相当于初始化一个一维数组,这个一维数组的长度为length,同样这个一维数组的数组元素是引用类型(数组类型),所以系统为每个元素分配的初始值为null。这个二维数组完全可以当成一维数组:使用new type[length]相当于定义了length个type类型变量;类似的,使用new type[length][]初始化这个数组后,相当于定义了length个type[]类型的变量,当然这个type[]类型的变量都是数组类型,因此需要再一次进行初始化。
1 class 二维数组 2 { 3 public static void main(String[] args) 4 { 5 //定义一个二维数组 6 int[][] a; 7 //把a当成一维数组进行初始化,初始化a为一个长度为4的数组 8 //a数组的数组元素有是引用类型 9 a=new int[4][]; 10 //把a当成一维数组,遍历a数组的每个元素 11 for(int i=0;i<a.length;i++) 12 { 13 System.out.println(a[i]); 14 } 15 //初始化a数组的第一个元素 16 a[0]=new int[2]; 17 a[0][0]=1; 18 a[0][1]=2; 19 20 //遍历二维数组a的第一个元素 21 for(int i=0;i<a[0].length;i++) 22 { 23 System.out.println(a[0][i]); 24 } 25 } 26 }
[][] a;这一行代码,将在栈里定义一个引用变量,这个变量并未指向任何有效的内存空间,此时堆内存还未为这行代码分配任何存储区。
a=new int[4][];程序对数组进行初始化,这行代码让a变量指向一块长度为4的内存空间,这个长度为4的数组里每个数组元素都是引用类型(数组类型),系统为这些数组元素分配初始值:null。此时数组a在内存中存储的示意图:
a[0]=new int[2];
a[0][0]=1;
a[0][1]=2;
对一个数组原素a[0]进行初始化
初始化多维数组时,可以只指定左边维的大小;当然也可以一次指定每一维的大小。
1 int[][] b=new int[3[4];
上面的代码定义一个b数组变量,这个变量指向一个长度为3的数组,这个数组的每个元素又是一个数组类型,他们各自指向长度为4的int[]数组,每个数组的元素为0。示意图为:
还可以使用静态初始化方式来初始化二维数组,使用静态初始化方式来初始化二维数组,二维数组的每个数组元素都是一维数组,因此必须指定多个一维数组作为二维数组的初始值。代码如下:
//使用静态初始化方法来初始化一个二维数组 String[][] str1=new String[][]{new String[3],new String[]{"hello"}}
结论:二维数组是一维数组,其数组元素是一维数组;三维数组其数组元素是二维数组。