李刚java笔记___深入数组

4.6深入数组
数组是一种引用数据类型,数组引用变量只是一个引用,数组元素和数组变量在内存里是分开存
放的。下面将深入介绍数组在内存中的运行机制。
> >4.6.1内存中的数组
    数组引用变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内
存后,才可通过该数组变量来访问数组元素。
    与所有引用变量相同的是,引用变量访问真实对象的根本方式。也就是说,如果我们希望在程
序中访问数组,则只能通过这个数组的引用变量来访问它。
    实际的数组元素被存储在堆(heap)内存中;数组引用变量是一个引用类型的变量,被存储在栈
( stack)内存中。数组在内存中的存储示意图如图4.2所示:
    如果需要访问图4.2中堆内存中的数组元素,程序中只能通过p[index]的形式实现.也就是说,数
组引用变量是访问堆内存中数组元素的唯一方式。
李刚java笔记___深入数组_第1张图片
学生提问:为什么有栈内存和堆内存之分?
答:当一个方法执行时,每个方法都会建立自己的内存找,在这个方挂内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方挂的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能披另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在合适的时候回收它。
   
      如果堆内存中数组不再有任何引用变量指向自己,则这个数组将成为垃圾,该数组所占的内存将会被系统的垃圾回收机制回收。因此,为了让垃圾回收机制回收一个数组所占的内存空间,则可以将该数组变量赋为null,也就切断了数组引用变量和实际数组之间的引用关系,实际数组也就成了垃圾。
      只要类型相互兼容,可以让一个数组变量指向另一个实际的数组,这种操作会产生数组的长度可变的错觉,如下代码所示:
    ArrayInRam.java
public class ArrayInRam {
	public static void main(String args[]){
		//定义并初始化数组,使用静态初始化
		int[] a = {5, 7, 20};
		//定义并初始化数组,使用动态初始化
		int[] b = new int[4];
		//输出b数组的长度
		System.out.println("b数组的长度为:" + b.length);
		//循环输出a数组的元素
		for(int i = 0;i <a.length;i++){
			System.out.println(a[i]);
		}
		//循环输出b数组的
		for(int i = 0; i<b.length; i++){
			System.out.println(b[i]);
		}
		//英文a是int[]类型,b也是int[]类型,所以可以将a的值赋给b
		//也就是让b引用指向a引用指向的数组
		b = a;
		//再次输出b数组的长度
		System.out.println("b数组的长度为:" + b.length);
	}	
}
输出:
b数组的长度为:4
5
7
20
0
0
0
0
b数组的长度为:3  
  运行上面代码后,将可以看到先输出b数组的长度为4,然后依次输出a数组和b数组的每个数组元素,接着会输出b数组的长度为3。看起来似乎数组的长度是可变的,但这只是一个假象。必须牢记:定义并初始化一个数组后,在内存里分配了两个空间,一个用于存放数组的引用变量,一个用于存放数组本身。下面将结合示意图来说明上面程序的运行过程。
   当程序定义并初始化了a, b两个数组后,系统内存中实际上产生了4块内存区,其中栈内存中有两个引用变量:a和b;堆内存中也有两块内存区,分别用于存储a和b引用所指向的数组本身。此时计算机内存的存储示意如图4.3所示:
李刚java笔记___深入数组_第2张图片

  从图4.3中可以非常清楚地看出,a引用和b引用所指向数组里数组元素的值,并可以很明白地看出b数组的长度是4.
    当执行上面粗体字标识代码:b=a代码时,系统将会把a的值赋给b, a和b都是引用类型变量,存储的是地址。因此把a的值赋给b后,就是让b指向a所指向的地址。此时计算机内存的存储示意如图4.4所示:
李刚java笔记___深入数组_第3张图片


从图4.4中可以看出,当执行了b=a之后,堆内存中第一个数组具有了两个引用:a变量和b变量都指向了第一个数组。此时第二个数组失去了引用,变成垃圾,只有等待垃圾回收来回收它------但它的长度依然不会改变,直到它彻底消失。
    程序员进行程序开发时,不要仅仅停留在代码表面,而要深入底层的运行机制,才可以对程序的运行机制有更准确的把握。当我们看一个数组时,一样要把数组看成两个部分:一个是数组引用,也就是在代码中定义的数组引用变量;还有一个是实际数组本身,这个部分是运行在系统内存里的,通常无法直接访问它,只能通过数组引用变量来访问它。
> > 4.6.2基本类型数组的初始化
    对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此,初始化数组时,先为该数组分配内存空间,然后直接将数组元素的值存入对应数组元素中。
    下面程序定义了一个int[]类型的数组变量,采用动态初始化的方式初始化了该数组,并显式为每个数组元素赋值,程序如下:
public class TestPrimitiveArray {
public static void main(String args[]){
//定义一个int[]类型的数组变量
int [] iArr;
//动态初始化数组,数组长度为5
iArr = new int[5];
//采用循环方式为每个数组元素赋值.
for(int i =0; i<iArr.length; i++){
iArr[i] = i + 10;
}
}
}
上面代码的执行过程代表了基本类型数组初始化的典型过程,下面将结合示意图详细介绍这段代码的执行过程。
    执行第一行代码int[] iArr;时,仅定义一个数组变量,此时内存中的存储示意如图4.5所示:
    执行了int[] iArr;代码后,仅在栈内存中定义了一个空引用(就是iArr数组变量),这个引用并未指向任何有效的内存,当然无法指定数组的长度
李刚java笔记___深入数组_第4张图片
当执行iArr = new int[5];动态初始化后,系统将负责为该数组分配内存空间,并分配默认的初始值:所有数组元素都被赋为0,此时内存中的存储示意如图4.6所示:
李刚java笔记___深入数组_第5张图片
   此时iArr数组的每个数组元素的值都是0,当循环为该数组的每个数组元素依次赋值后,此时每个数组元素的值都变成程序指定的值。显式指定数组元素值后存储示意如图4.7所示:
李刚java笔记___深入数组_第6张图片
   从图4.7中可以看到基本类型数组的存储示意图,每个数组元素的值直接存储在对应的内存里。操作基本类型数组的数组元素时,实际上就是操作基本类型的变量。
>>4.6.3引用类型数组的初始化
    引用类型数组的数组元素是引用,因此情况变得更加复杂:每个数组元素里存储还是引用,它指向另一块内存,这块内存里存储了有效数据。
    为了更好地说明引用类型数组的运行过程,下面先定义一个Person类(所有类都是引用类型),关于定义类、对象引用的详细介绍请参考第5章的介绍。Person类的代码如下:
   程序清单:codes/04/4-6/Person.java
public class Person {
public int age;
public double height;
//定义一个info
public void info(){
System.out.println("我的年龄是" + age + "我的身高是" + "height");
}
}

    下面程序将定义一个Person[]数组,接着动态初始化这个Person[]数组,并为这个数组的每个数组元素指定值。程序代码如下:
程序清单:codes/04/4-6/TestReferenceArray. java
public class TestReferenceArray {
public static void main(String args[]){
//定义一个students数组变量,其类型是Person[]
Person[] students;
//执行动态初始化
students = new Person[2];
//创建一个Person实例,并将这个Person实例赋给zhang变量
Person zhang = new Person();
//为zhang所引用的person对象的属性赋值
zhang.age = 15;
zhang.height = 158;
//创建一个Person实例,并将这个Person实例给lee变量
Person lee = new Person();
//为lee所引用的Person对象的属性赋值
lee.age = 16;
lee.height = 161;
//将zhang变量的值赋给第一个数组元素
students[0] = zhang;
//将lee变量的值赋给第二个数组元素
students[1] = lee;
//下面两行代码的结果完全一样,因为lee和students[1]指向的是同一个Person实例
lee.info();
students[1].info();
}
}
输出:
我的年龄是16我的身高是height
我的年龄是16我的身高是height

    上面代码的执行过程代表了引用类型数组的初始化的典型过程,下面将结合示意图详细介绍这段代码的执行过程。
    执行Person[] students;代码时,这行代码仅仅在栈内存中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效的内存区。此时内存中存储示意如图4.8所示:
李刚java笔记___深入数组_第7张图片
  在图4.8中的栈内存中定义了一个students变量,它仅仅是一个引用,并未指向任何有效的内存。直到执行初始化,本程序对students数组执行动态初始化,动态初始化由系统为数组元素分配默认的.
初始值:null,即每个数组元素的值都是null,执行动态初始化后的存储示意如图4.9所示:
李刚java笔记___深入数组_第8张图片
   从图4.9中可以看出,students数组的两个数组元素都是引用,而且这个引用并未指向任何有效的内存,因此每个数组元素的值都是null。这意味着依然不能直接使用students数组元素,因为每个数组元素都是null,这相当于定义了两个连续的Person变量,但这个变量还未指向任何有效的内存区,所以这两个连续的Person变量(students数组的数组元素)还不能使用。
    接着的代码定义了zhang和lee两个Person实例,定义这两个实例实际上分配了4块内存,在栈内存中存储了zhang和lee两个引用变量,还在堆内存中存储了两个Person实例。此时的内存存储示意如图4.10所示:
李刚java笔记___深入数组_第9张图片
此时students数组的两个数组元素依然是null,直到程序依次将zhang赋给students数组的第一个元素,把lee赋给students数组的第二个元素,students数组的两个数组元素将会指向有效的内存区,此时的内存存储示意如图4.11所示:
李刚java笔记___深入数组_第10张图片

从图4.11中可以看出:此时zhang和students[0]指向同一个内存区,而且它们都是引用类型变量,因此通过zhang和students[O]来访问Person实例的属性和方法的效果完全一样,不论修改students[O]所指向的Person实例的属性,还是修改zhang变量所指向的Person实例的属性,所修改的其实是同一个内存区,所以必然互相影响。同理,lee和students[l]也是引用到同一个Person对象,也有相同的效果。
> > 4.6.4没有多维数组
    Java语言里提供了支持多维数组的语法。但笔者还是想说,没有多维数组—如果从数组底层的运行机制上来看。
    Java语言里的数组类型是引用类型,因此,数组变量其实是一个引用,这个引用指向真实的数组内存。数组元素的类型也可以是引用,如果数组元素的引用再次指向真实的数组内存,这种情形看上去很像多维数组。
    回到前面定义数组的语法:type[] arrName;,这是典型的一维数组的定义语法,其中type是数组元素的类型。如果希望数组元素也是一个引用,而且是指向int数组的引用,那可以把上面type具体成为int[](前面已经指出:int[]就是一种类型,int[]类型的用法与普通类型并无任何区别),那么上面定义数组的语法就是int[][] arrName。
    如果把int这个类型扩大到Java的所有类型(不包括数组类型),则出现了定义二维的数组的语法:
    type[][] arrName;
    Java语言采用上面的语法格式来定义二维数组,但它的实质还是一维数组,只是其数组元素也是引用,数组元素里保存的引用指向一维数组。
接着对这个“二维数组”执行初始化,执行初始化同样可以把这个数组当成一维数组来初始化,把这个“二维数组”当成一个一维数组,其元素的类型是type[]类型,则可以采用如下语法进行初始化:
arrName=new type[length][]
    上面的初始化语法相当于初始化了一个一维数组,这个一维数组的长度是length。同样,因为这个一维数组的数组元素是引用类型(数组类型)的,所以系统为每个数组元素都分配初始值:null.
    这个二维数组实际上完全可以当成一维数组使用:使用new type[length]初始化一维数组后,相当于定义了length个type类型的变量;类似的,使用new type[length]口初始化这个数组后,相当于定义了length个type[]类型的变量,当然,这些type[]类型的变量都是数组类型,因此必须再次初始化这些数组。
    下面程序示范了如何把二维数组当成一维数组处理:
    程序清单:codes/04/4-6/ TestTwoDimension Java
public class TestTwoDimension {
	public static void main(String[] args)
	{
	    //定义一个二维数组
	    int[][]a;
	    //把a当成一维数组进行初始化,初始化a是一个长度为3的数组
	    //a数组的数组元素又是引用类型
	    a = new int[3][];
	    //把a数组当成一维数组,遍历a数组的每个数组元素
	    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][1]);
	}
	}
}
 上面程序中粗体字标识部分把a这个二维数组当成一维数组处理,只是每个数组元素都是null,所以我们看到输出结果都是null。下面结合示意图来说明这个程序的运行过程。
    程序的第一行int[][] a;,将在栈内存中定义一个引用变量,这个变量并未指向任何有效的内存空间,此时的堆内存中还未为这行代码分配任何存储区。程序对a数组执行初始化:a = new int[3][];,这行代码为让a变量指向一块长度为3的数组内存,这个长度为3数组里每个数组元素都是引用类型(数组类型),系统为这些数组元素分配默认初始值:null。此时a数组在内存中的存储示意如图4.12所示:
    从4.12来看,虽然声明a是一个二维数组,但这里丝毫看不出它是一个二维数组的样子,完全是一维数组的样子。这个一维数组的长度是3个,只是这三个数组元素都是引用类型,它们的默认值是null.所以程序中可以把a数组当成一维数组处理,依此遍历a数组的每个元素,将看到每个数组元素的值都是null.
李刚java笔记___深入数组_第11张图片

    因为a数组的元素必须是int[]数组,所以接下来的程序对a[O]元素执行初始化,也就是让图4.12右边堆内存的中第一个数组元素指向一个有效的数组内存,指向一个长度为2的int数组。因为程序采用动态初始化a[O]数组,因此系统将为a[O]的每个数组元素分配默认初始值:0,然后程序显式为a[0]数组的第二个元素赋值为6。此时在内存中的存储示意如图4.13所示:
李刚java笔记___深入数组_第12张图片
图4.13中灰色覆盖的数组元素就是程序显式指定的数组元素值。TestTwoDimension.java接着迭代输出a[O]数组的每个数组元素,将看到输出0和6.

$学生:我是否可以让图4.13中灰色覆盖的数组元素再次指向另一个数组?这样不可以扩展成三维数组吗?甚至扩展到更多维数组
答:不能!至少在这个程序中不能。因为Java是强类型的语言,当我们定义a数组时,已经确定了a数组的数组元素是int[]类型,则a[0]
数组的数组元素只能int类型,所以灰色覆盖的数组元素里只能存储int类型的变量。对于其他弱类型的语言,例JavaScript和Ruby等,确
实可以把一维数组无限扩展,扩展成二维数组、三维数组……如果想在Java语言中实现这种可无限扩展的数组,则可以定义一个Object[]类型的数祖,这个数祖的元素是Object类型,因此可以再次指向一个Object[]类型的数祖,这样就可以从一维数组扩展到二维数组、三维数组....一直下去。

    从上面程序中可以看出,初始化多维数组时,可以只指定最左边维的大小;当然,也可以一次指定每一维的大小,例如下面代码(程序清单同上):
//同时初始化二维数组的两个维数
int [][]b =new int[3][4]
    上面代码将定义一个b数组变量,这个数组变量指向一个长度为3的数组,这个数组的每个数组元素又是一个数组类型,它们各指向对应的长度为4的int[]数组,每个数组元素的值为0。这行代码执行后在内存中存储示意如图4.14所示:
李刚java笔记___深入数组_第13张图片
   还可以使用静态初始化的方式来初始化二维数组,使用静态初始化方式来初始化二维数组时,二维数组的每个数组元素都是一维数组,因此必须指定多个一维数组作为二维数组的初始化值,如下代码所示(程序清单同上):
//使用静态初始化的语法来初始化一个二维数组
String[][] str1 = new String[][]{new String[3], new String[]{"hello"}};
上面代码的执行后内存中的存储示意如图4.15所示:
李刚java笔记___深入数组_第14张图片
  通过上面讲解,我们可以得到一个结论:二维数组是一维数组,其数组元素是一维数组;三维数组也是一维数组,其数组元素是二维数组;四维数组还是一维数组,其数组元素是三维数组……从这个角度来看,Java语言里没有多维数组。








你可能感兴趣的:(李刚java笔记___深入数组)