Java数组与内存控制

来源:互联网 发布:高清解码矩阵 编辑:程序博客网 时间:2024/04/27 07:21

一、Java数组初始化

           Java数组是静态的,即当数组被初始化之后,该数组的长度是不可变的。Java数组使用之前必须先对数组对象进行初始化,所谓初始化,就是为数组的所有元素分配内存空间,并为每个数组元素指定初始值。(文章来源于李刚老师的《突破java程序员的16课》)

1:基本类型数组的两种初始化方式

  • 静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度。
  • 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。

        不要同时使用静态初始化和动态初始化,也就是说,不要在进行数组初始化时,既指定数组的长度,也为每个数组元素分配初始值。

Java的数组是静态的,一旦数组初始化完成,数组元素的内存空间分配即结束,程序只能改变数组元素的值,而无法改变数组的长度。Java的数组变量是一种引用类型的变量,数组变量并不是数组本身,它只是指向堆内存中的数组对象。因此,可以改变一个数组变量所引用的数组,这样可以造成数组长度可变的假象。

下面由一段程序来说明:

复制代码
public class ArrayTest { public static void main(String[] args) { //采用静态初始化方式初始化第1个数组 String[] books = newString[]{ "疯狂java讲义""轻量级Java EE企业级应用实战""疯狂Ajax讲义""疯狂XML讲义" } ; //采用静态初始化的简化方式初始化第2个数组String[] names = { "孙悟空""猪八戒""白骨精" }; //采用动态初始化方式初始化第3个数组 String[] strArr = new String[5]; //测试System.out.println("第一个数组长度为:"+books.length); System.out.println("第二个数组长度为:"+names.length); System.out.println("第三个数组长度为:"+strArr.length); //让books数组变量,strArr数组变量指向names所引用的数组 books = names; strArr = names; System.out.println("------------------"); System.out.println("books数组长度为:"+books.length); System.out.println("strArr数组长度为:"+strArr.length); //改变books数组变量所引用的数组的第二个元素值 books[1] = "唐僧"; System.out.println("names数组的第二个元素为:"+books[1]); }}
复制代码

打印结果为:

复制代码
第一个数组长度为:4第二个数组长度为:3第三个数组长度为:5------------------books数组长度为:3strArr数组长度为:3names数组的第二个元素为:唐僧
复制代码

下图表示的是3个引用变量和数组对象在内存中的分配情况:

从图中可以看出,原来books变量所引用的数组长度依然为4,但不再有任何引用变量引用该数组,因此它将会变成垃圾,等待回收机制回收。程序中3个变量同时引用同一个数组对象,因此当执行books[1] = "唐僧";时names数组的第二个数组的值也会改变。

二:数组一定要初始化吗? 

复制代码
...int[] nums = new int[]{12,32,34};//定义另一个数组变量int[] prices;//让prices数组指向nums所引用的数组prices = nums;...
复制代码

执行了上面的代码后,prices变量已经指向有效的内存及一个长度为4的数组对象,程序可以使用prices变量了

三:基本类型数组的初始化

对于基本类型数组而言,数组元素的值直接存储在对应的数组元素中,因此基本类型数组的初始化比较简单:程序直接先为数组分配内存空间,再将数组元素的值存入对应内存里。

所有局部变量都是存放在栈内存里保存的,不管其是基本类型的变量,还是引用类型的变量,都是存储在各自的方法栈区中;但引用类型变量所引用的对象(数组、普通Java对象)则总是存储在堆内存中。

对于很多Java程序员而言,他们最容易混淆的是:引用类型变量何时只是栈内存中的变量本身,何时又变为引用实际的Java对象。其实规则很简单:引用变量本质上只是一个指针,只要程序通过引用变量访问属性,或者通过调用引用变量来调用方法,该引用变量将会由它所引用的对象代替。 

四:引用类型数组的初始化

引用类型数组的数组元素依然是引用类型的,因此数组元素里存储的还是引用,它指向另一块内存,这块内存里存储了该引用变量所引用的对象(包括数组和Java数组)。

五:使用数组

当数组引用变量指向一个有效的数组对象之后,程序就可以通过该数组引用变量来访问数组对象。Java语言不允许直接访问堆内存中的数据,因此无法直接访问堆内存中的数组对象,程序将通过数组引用变量来访问数组。







引用类型数组的数组元素是引用,因此情况变得更加复杂:每个数组元素里存储还是引用,它指向另一块内存,这块内存里存储了有效数据。


为了更好地说明引用类型数组的运行过程,下面先定义一个Person类(所有类都是引用类型),关于定义类、对象和引用的详细介绍请参考第5章的介绍。Person类的代码如下:

[java] view plaincopy
  1. 程序清单:codes/04/4-6/Person.java  
  2. public class Person  
  3. {  
  4. //年龄  
  5. public int age;  
  6. //身高  
  7. public double height;  
  8. //定义一个info方法  
  9. public void info()  
  10. {  
  11. System.out.println("我的年龄是:" + age + ",我的身高是:" + height);  
  12. }  
  13. }  


下面程序将定义一个Person[]数组,接着动态初始化这个Person[]数组,并为这个数组的每个数组元素指定值。程序代码如下:

[java] view plaincopy
  1. 程序清单:codes/04/4-6/TestReferenceArray.java  
  2. public class TestReferenceArray  
  3. {  
  4. public static void main(String[] args)   
  5. {  
  6. //定义一个students数组变量,其类型是Person[]  
  7. Person[] students;  
  8. //执行动态初始化  
  9. students = new Person[2];  
  10. //创建一个Person实例,并将这个Person实例赋给zhang变量  
  11. Person zhang = new Person();  
  12. //为zhang所引用的Person对象的属性赋值  
  13. zhang.age = 15;  
  14. zhang.height = 158;  
  15. //创建一个Person实例,并将这个Person实例赋给lee变量  
  16. Person lee = new Person();  
  17. //为lee所引用的Person对象的属性赋值  
  18. lee.age = 16;  
  19. lee.height = 161;  
  20. //将zhang变量的值赋给第一个数组元素  
  21. students[0] = zhang;  
  22. //将lee变量的值赋给第二个数组元素  
  23. students[1] = lee;  
  24. //下面两行代码的结果完全一样,因为lee和students[1]指向的是同一个Person实例。  
  25. lee.info();  
  26. students[1].info();  
  27. }  
  28. }  


上面代码的执行过程代表了引用类型数组的初始化的典型过程,下面将结合示意图详细介绍这段代码的执行过程。


执行Person[] students;代码时,这行代码仅仅在栈内存中定义了一个引用变量,也就是一个指针,这个指针并未指向任何有效的内存区。此时内存中存储示意如下图所示:


在上图中的栈内存中定义了一个students变量,它仅仅是一个引用,并未指向任何有效的内存。直到执行初始化,本程序对students数组执行动态初始化,动态初始化由系统为数组元素分配默认的初始值:null,即每个数组元素的值都是null,执行动态初始化后的存储示意如下图所示:


从上图中可以看出,students数组的两个数组元素都是引用,而且这个引用并未指向任何有效的内存,因此每个数组元素的值都是null。这意味着依然不能直接使用students数组元素,因为每个数组元素都是null,这相当于定义了两个连续的Person变量,但这个变量还未指向任何有效的内存区,所以这两个连续的Person变量(students数组的数组元素)还不能使用。


接着的代码定义了zhang和lee两个Person实例,定义这两个实例实际上分配了4块内存,在栈内存中存储了zhang和lee两个引用变量,还在堆内存中存储了两个Person实例。此时的内存存储示意如下图所示:


此时students数组的两个数组元素依然是null,直到程序依次将zhang赋给students数组的第一个元素,把lee赋给students数组的第二个元素,students数组的两个数组元素将会指向有效的内存区,此时的内存存储示意如下图所示:


从 上图中可以看出:此时zhang和students[0]指向同一个内存区,而且它们都是引用类型变量,因此通过zhang和students[0]来访问Person实例的属性和方法的效果完全一样,不论修改students[0]所指向的Person实例的属性,还是修改zhang变量所指向的Person实例的属性,所修改的其实是同一个内存区,所以必然互相影响。同理,lee和students[1]也是引用到同一个Person对象,也有相同的效果。

 







三.java中数组到底是什么?

1)不管在其他语言中是什么,数组在Java中可得看作一个对象,它有一些值得探讨的特性。

Java中的数组其实是一个对象,但是确实是一个特殊的对象,实在是太特殊了,以致我们都不好把它多做对象处理。

java是纯面向对象的语言,数组也是一个对象。


首先我们看一下表面现象,数组创建的时候采用的是如下语句:
MyClass[] arr = new MyClass[9];


而普通类采用的是如下语句:
MyClass obj = new MyClass();


就是说,创建数组的时候不使用小括号传参。使得数组和普通类看起来就有很多不同,因为小括号里的参数是传递给构造方法的,进而让人感觉数组类是没有构造方法的。

2)java中数组是对象的依据:

数组的父类是Object,
  new Object[0].getClass().getSuperClass()  是Object.class

数组没有对应的类文件,String对应String.class.数组却没有,而且他们的 类名字很古怪,可以这样获得 new int[2].getClass().getName();
这是和其他对象最大的不同点,因为数组类是在运行时生成的。

java.lang.reflect.Array是final的,所以数组肯定不是它的子类,这个类用来动态生成数组或者操作数组(获得长度等)

再往深了想,还有很多让人感觉不自然的东西。可以肯定的是,java确实将数组作为了一个类来处理。还是用上面的例子说明:
可以通过以下方法得到MyClass[]的Class实例:arr.getClass()或MyClass[].class。这样,我就可以向数组类里面“窥探”了。
Class clazz = MyClass[].class;
System.out.println(clazz.getConstructors().length);
打印出来的结果是0;证明数组类确实没有构造方法。


数组类的“庐山真面目”:
System.out.println(clazz);
输出是:
[Larraytest.MyClass

对Java Class文件结构稍有了解就知道,这个字符串的意思就是一个元素类型为arraytest.MyClass的一维数组。也就是说,数组类型不是和普通类一样,以一个全限定路径名+类名来作为自己的唯一标示的,而是以[+一个或者多个L+数组元素类全限定路径+类来最为唯一标示的。这个()也是数组和普通类的区别。而这个区别似乎在某种程度上说明数组和普通java类在实现上有很大区别。因为java虚拟机(java指令集)在处理数组类和普通类的时候,肯定会做出区分。我猜想,可能会有专门的java虚拟机指令来处理数组。

分析到这里,基本上可以肯定:java对数组对象化的操作的支持是指令级的,也就是说java虚拟机有专门针对数组的指令。数组的Class类实例是java虚拟机动态创建动态加载的,其结构与普通java类的Class实例有一些不同。

JDK API中有一个java.lang.reflect.Array类,这个类提供了很多方法(绝大多数是native方法,这在另一个方面证明了java对数组的支持是专用指令支持的,否则用本地方去干嘛,用来弥补我们对数组操作的局限性。


下面这句话用来创建一个一维的、长度为10的、类型为arraytest.MyClass的数组:
arraytest.MyClass[] arr = (arraytest.MyClass[]) Array.newInstance(arraytest.MyClass, 10);

下面这句话用来创建一个二维的、3乘5的、类型为arraytest.MyClass的数组:
int[] arrModel = new int[]{3,5};
Object arrObj = Array.newInstance(Sub.class, arrModel);
当然你可以用一个数组的引用指向上面的二维数组,这里我们用一个Object的引用指向他。
使用的时候,我们也是可以利用Array类提供的方法来实现:

System.out.println(Array.getLength(arrObj);//第一维长度为3
System.out.println(Array.getLength(Array.get(arrObj, 2)));//第二维长度为5,这里如果写3,就会得到你意想之中的java.lang.ArrayIndexOutOfBoundsException

打印结果是如我所想的:
3
5

对于数组的Class类实例,还有一些奇怪的现象:
在运行代码 java.lang.reflect.Field fieldarr = clazz.getField("length");的时候,会抛出异常:java.lang.NoSuchFieldException: length,这似乎在说数组类没有length这个域,而这个域其实是我们用的最多的一个(也就是说这个域是肯定存在的)。我想关于数组的Class类实例、数组的实现等,还有很多“猫腻”在里面。

顺便说一句,java数组最多只能是255维的。这个让人看到了C的影子,嘿嘿。

“Java把数组当作一个java类来处理”说起来容易,用起来自然,但是细细想来,还是有很多不简单的地方呀。 


从对数组对象的进一步探讨,可以稍微了解Java对对象处理的一些方法。首先来看看一维数组的引用名称的定义:

int[] arr = null;

在这个定义中,arr表示一个可以参考引用自一维数组对象的变量名称,但是目前将这个名称参考引用自null,表示还没有指定这个名称参考引用自实际的对象。在Java中,=运算用于基本数据类型时,是将值复制给变量,但当它用于对象时,则是将对象指定给参考引用名称来参考引用。也可以将同一个对象指定给两个参考引用名称,当对象的值由其中一个参考引用名称进行操作而变更时,另一个参考引用名称所参考引用到的值也会变动。下面来看看范例5.8的示范。

ü 范例5.8  AdvancedArray.java

public class AdvancedArray {

    public static void main(String[] args) {

        int[] arr1 = {1, 2, 3, 4, 5};

        int[] tmp1 = arr1;

        int[] tmp2 = arr1;



        System.out.print("通过tmp1取出数组值:");

        for(int i = 0; i < tmp1.length; i++)

            System.out.print(tmp1[i] + " ");



        System.out.print("\n通过tmp2取出数组值:");

        for(int i = 0; i < tmp2.length; i++)

            System.out.print(tmp2[i] + " ");



        tmp1[2] = 9;

        System.out.print("\n\n通过tmp1取出数组值:");

        for(int i = 0; i < tmp1.length; i++)

            System.out.print(tmp1[i] + " ");



        System.out.print("\n通过tmp2取出数组值:");

        for(int i = 0; i < tmp2.length; i++)

            System.out.print(tmp2[i] + " ");

        System.out.println();

    }

}

执行结果:



通过tmp1取出数组值:1 2 3 4 5

通过tmp2取出数组值:1 2 3 4 5



通过tmp1取出数组值:1 2 9 4 5

通过tmp2取出数组值:1 2 9 4 5



在这个范例中,通过tmp1名称改变了索引2的元素值,由于tmp2也引用自同一数组对象,所以tmp2取出索引2的元素值是改变后的值。事实上在范例5.8中,有三个引用名称引用自同一个数组对象,也就是arr1、tmp1与tmp2,所以,如果取出arr1索引2的元素,元素值也会是9。


了解到在Java中数组是一个对象,而使用=指定时是将对象指定给数组名来引用,而不是将数组进行复制。如果想将整个数组的值复制给另一个数组该如何作呢?可以使用循环,将整个数组的元素值遍历一遍,并指定给另一个数组相对应的索引位置。范例5.10示范了进行数组复制的方法。

Ü范例5.10  ArrayCopy.java

public class ArrayCopy {

    public static void main(String[] args) {

        int[] arr1 = {1, 2, 3, 4, 5};

        int[] arr2 = new int[5];

        for(int i = 0; i < arr1.length; i++)

            arr2[i] = arr1[i];

        for(int i = 0; i < arr2.length; i++)

            System.out.print(arr2[i] + " ");

        System.out.println();

    }

}

执行结果:

1 2 3 4 5

另一个进行数组复制的方法是使用System类提供的arraycopy()方法。其语法如下:

System.arraycopy(来源, 起始索引, 目的, 起始索引, 复制长度);

范例5.11改写了范例5.10,使用System.arraycopy()进行数组复制,执行结果与范例5.10是相同的。

Ü范例5.11  ArrayCopy2.java

public class ArrayCopy2 {

    public static void main(String[] args) {

        int[] arr1 = {1, 2, 3, 4, 5};

        int[] arr2 = new int[5];

        System.arraycopy(arr1, 0, arr2, 0, arr1.length);

        for(int i = 0; i < arr2.length; i++)

            System.out.print(arr2[i] + " ");

        System.out.println();

    }

}


四、 Java中的数组作为对象带来的好处
1)越界检查

2)length field:与传统的C++中的数组相比,length字段可以方便的得到数组的大小;但要注意,仅仅可以得到数组的大小,不能得到数组中实际包含多少个元素,因为length 只会告诉我们最多可将多少元素置入那个数组。

3) 初始化:对象数组在创建之初会自动初始化成null,由原始数据类型构成的数组会自动初始化成零(针对数值类型),(Char)0 (针对字符类型)或者false (针对布尔类型)。

4) 数组作为返回值:首先,既然数组是对象,那么就可以把这个对象作为返回值;而且,不必担心那个数组的是否可用只要需要它就会自动存在而且垃圾收集器会在我们完成后自动将其清除

0 0
原创粉丝点击