深入数组(引用类型)
数组是一种引用数据类型,数据引用变量只是一个引用,数组元素和数组变量在内存里是分开存放的。在我们看待一个数组时,一定要把他看待成两部分:一部分是数组引用,也就是在代码中定义的数组引用变量;还有一部分是实际的数组对象,这部分是在堆内存中运行的,通常无法直接访问它,只能通过数组引用变量来访问。
-
内存中的数组
数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可以通过该数组变量来访问数组元素。
与所有引用变量相同的是,引用变量是访问真实对象的根本方式。也就是说,如果希望在程序中访问数组对象本身,则只能通过这个数组的引用变量来访问它。
实际的数组对象被存储在堆内存中;如果引用该数组对象的数组引用变量是一个局部变量,那么它被存储在栈内存中。
也就是说,数组引用变量是访问堆内存的根本方式。
如果堆内存中数组不再有引用变量指向自己,则这个数组将变为垃圾。该数组所占的内存将会被系统的垃圾回收器回收。因此为了让垃圾回收器回收一个数组所占的内存空间,可以将该数组变量赋为null,也就切断了数组引用变量和实际变量之间的引用关系,实际的数组也就变成了垃圾。
-
只要类型互相兼容,就可以让一个数组变量指向另一个实际的数组,这样的操作会让人产生数组的长度可变的错觉。
public class ArrayInRam { public static void main(String[] args) { int[] a={5,7,20}; int [] b=new int[4]; System.out.println("b数组的长度为:" + b.length);//输出b数组的长度 System.out.println("a数组的长度为:" + a.length); for(int i=0;i<a.length;i++) { System.out.println(a[i]); } for(int i=0,len=b.length;i<len;i++)· { System.out.println(b[i]); } //a数组是int类型,b数组也是int类型,可以将a的值赋给b //也就是让b指向a引用指向的数组 b=a; System.out.println("b数组的长度为:"+b.length); System.out.println("a数组的长度为:" + a.length);//这里变化的是引用变量 } }
可见两个同类型的数组可直接进行赋值,看起数组的长度是可变的,但是不要忘记一个数组有两个内存,一个是在栈内存中的引用变量,一个是对内存引用变量所指向的数组本身。实际上在堆内存的数组长度仍然没有发生改变。
我们在程序区域可操作的是栈内存,底层的堆内存我们无法直接对他们进行操作,但是可通过栈内存的引用变量来实现间接操作。
如上程序可见。引用变量a赋给了引用变量b,也就是说在之前引用变量a指向堆内存的a数组有两个栈内存的引用变量,即:a和b。
此时堆内存的b数组没有指向他的引用变量,变成了垃圾,等待被回收,但它的长度仍然没有发生改变,直到他彻底消失。
-
基本类型数组的初始化
对于基本类型数组而言,数组元素的值直接储存在对应的数组中,因此,初始化数组时,先为该数组分配空间,然后直接将数组元素的值存入对于的数组元素中。
public class PremitiveArrayTest { public static void main(String[] args) { int [] iArr; iArr=new int [5]; for(var i=0;i<iArr.length;i++) { iArr[i]=i+10; } } }
执行第一行int声明iArr数组后,仅在栈内存中定义了一个空引用(就是iArr数组变量),这个引用并未指向任何有效的内存,当然无法指向数组的长度。
当执行iArr=new int [5];动态初始化后,系统将负责为该数组的每个数组元素依次赋值为0;此时堆内存中的数组是有值的,每个数组元素的值都是0,
当循环为每个数组元素赋值后,此时每个数组元素的值都变成程序显式指定的值。显示指定每个数组元素值后方的存储。
因此可以看出每个数组元素的值直接存储在对应的内存中。操作基本类型数组的数组元素时,实际上相当于操作基本类型的变量。
-
引用类型数组的初始化
引用类型数组的数组元素是引用,因此情况变得更加复杂。每个数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了有效数据。
为了更好低说明引用类型数组的运行过程,下面先定义一个Person类(所有的类都是引用类型)。
class Person { public int age; public double height; public void info() { System.out.println("我的年龄是:" + age +"我的身高是:" +height); } }
下面程序将定义一个Person[]数组,接着动态舒适化这个Person[]数组,并为这个数组的每个数组元素指定值。程序代码如下
public class ReferenceArrayTest { public static void main(String[] args) { Person[] students;//定义一个students数组,其类型为Person students = new Person[2];//执行动态初始化 var zhang= new Person();//创建一个Person实例,并将这个实例赋给zhang变量 zhang.age=15; zhang.height=158; var lee =new Person();//创建一个Person实例,并将这个实例赋给lee变量 lee.age=16;//为lee所引用的Person对象的age、height赋值 lee.height=161; students[0]=zhang;//将zhang、lee等变量的值赋给数组元素 students[1]=lee; lee.info();//这两行代码的值完全一样,因为lee和students[1]指向的是用一个Person实例 students[1].info(); } }
上述代码的执行过程代表了引用类型数组舒适化的典型过程。
执行Person[]students;代码时,这行代码仅仅在栈内存中定义了一个引用变量,这就是一个指针,这个指针并为指向任何有效的内存区。
栈内存中定义了一个students变量。它仅仅是一个引用,并未指向任何有效的内存,直到执行初始化,本程序对students数组执行动态初始化,动态初始化由系统为数组元素分配默认的初始值,:null。即每个数组元素的值都是null。
students数组的两个数组元素都是引用,而且这个引用并未指向任何有效的内存,因此每个数组元素的值都是null。这相当于定义了两个连续的Person变量,但这个变量还未指向任何有效的内存区,所以这两个连续的Person变量(students数组的数组元素)还不能使用。
接着代码定义了lee和zhang两个Person的实例,定义在这两个实例实际上分配了4块内存,在栈内存中存储了zhang和lee两个引用变量,还在堆内存中存储了两个Person实例。
此时students数组的两个元素依然是null,直到程序以此将zhang和lee赋给数组的第一、二个元素,students数组的两个数组元素将会指向有效的内存区。
-
数组的名称
直接打印数组名称:得到的是数组对应的内存地址哈希值。
格式[数据类型首字母+@+16进制地址值。