一、Java中数组对象的存储方式

理解:Java中的每一个数组,都是一个对象。不同于基本数据类型,比如在main函数中创建一个int变量,则此变量是存储在栈区的,包括八大基本数据类型。而数组不是基本数据类型,每一个数组都是一个对象。无论是静态开辟的数组,还是动态开辟的数组,都属于对象,都是存储在堆区的。Java中的在堆区开辟的对象,不需要我们手动释放,当对象没有引用变量指向它时,就会由Java虚拟机中的垃圾回收器帮助我们回收已经变成垃圾的对象,从而达到避免内存浪费的目的。

Java的垃圾回收是Java语言的重要功能之一。当程序创建对象、数组等引用类型实体时,系统都会在堆内存中为之分配一块内存区,对象就保存在这块内存区,当这块内存区不在被任何引用变量所引用时,这块内存就变成了垃圾,等待垃圾回收机制进行回收。

1. 垃圾回收机制只负责回收堆内存中的对象,不回收任何物理资源;
2. 程序无法精确控制垃圾回收运行,垃圾回收会在合适的时候进行。当对象永久性地失去引用后,系统会在合适的时候回收它所占的内存。
3. 在垃圾回收机制回收任何对象之前,总先会调用它的finalize()方法,该方法可能使该对象重新复活,从而呆滞垃圾回收机制取消。

Java 数组中存放数组 java数组存放对象_开发语言

如图,arr引用变量存储在栈区中,类似于基本数据接类型变量,而数组对象开辟存储在堆区,随着程序块的结束,arr引用变量被销毁,数组对象没有了引用变量引用它,从而这块内存就无法被访问了。所以就是一块垃圾内存,就会由Java的垃圾回收器自动回收。

java基本数据类型存放在哪?

基本数据类型变量存放在哪不是靠常规说的基本数据类型存放在栈中,引用类型存放在堆中这样简单的话语定义的,而是需要利用场景来帮助理解,不然可能会被误导。
举个例子:

class Text{
 int a=1;
 }



在Text类中基本数据类型的变量a是随对象存储在堆中的而

public void text(){
 int b=1;
 }

在text局部方法中基本数据类型的变量b是存储在栈中的,从更深层次理解为什么基本类型的实例变量在堆上创建,局部变量在栈上创建,这样做有什么好处?
如果你将一个实例变量放在栈内,那么就不存在多个线程访问同一个对象资源了,这显然是不对的,所以实例变量要在堆上创建,但是对于局部变量,是在栈上创建的,调用一次方法创建一个帧,独享一份内存区域,其他的线程是不会访问到该线程的资源,在栈上创建也会减轻GC的压力,随着该方法的结束,帧出栈,相对应的内存消除,这种局部变量占用的内存自然就消失了。

二、数组的创建

1.创建数组对象

// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };

静态初始化与动态初始化,初始化出的都是数组对象,都存储在堆区。

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

Java中数组大小支持常量,变量,表达式。

其中,int[] 表示的是arr1的类型,表示arr1是一个引用类型,指向一个存储int的数组。不同于C/C++里面不可以写数字,否则就是破坏了数组的类型。

2. 默认值

arr3的元素有默认值,int类型数组元素默认为0

Java 数组中存放数组 java数组存放对象_开发语言_02

 (图中的Arrays是一个标准类,存放在Java的标准类包中:java.util文件中。里面的成员方法是一些对于数组的常用操作,比如toString()   可以将数组转化为字符串形式,方便打印。)

  如果没有对数组进行初始化,数组中的元素有默认值。不同的数据类型有不同的默认值,如果是整型,大多是默认值为0。float是0.0f,double是0.0,char是/u0000,boolean是false;引用类型比如字符串类型则是null。

三、数组的使用

Java中的数组同样,当然,也应当支持下标操作,并且返回值是一个左值。也就是可以用下标运算符获取数组中的元素,同时也可以改变它。略了。

1. 数组的遍历

public static void main(String[] args) {
        int[] array = new int[]{1,2,3,4,5};
   //  1
        for(int i = 0; i < array.length; ++i) {
            System.out.print(array[i]+" ");
        }
        System.out.println();
        System.out.println("====================");
   //  2
        for(int i : array) {
            System.out.print(i+" ");
        }
        System.out.println();
        System.out.println("====================");
    }

值得一说的是array.length  如上所说,Java中的数组是一个对象,其实就类似于C++中一个类的对象,那么这个类肯定有某些成员函数, 某些数据成员,如果按照C++的语法来说,那么这个length就是一个公有数据成员,存储数组的元素个数。

其次,第二种是增强for循环,在C++中同样存在,这是数组遍历的最简单方法,缺点是无法获取元素的下标。

2. 数组越界异常

Java 数组中存放数组 java数组存放对象_开发语言_03

Array Index Out Of Bounds Exception  数组下标超出界限异常。

四、数组在内存中的分布

其实在第一部分已经讲了一些了,这里做进一步讲解。

1. JVM-Java虚拟机内存分布简介:

JVM虚拟机里面的本地方法栈里面运行。为了高效管理内存,JVM对内存进行了划分。JVM是一个软件,由C/C++编写的软件。这是因为系统之类的由C/C++代码编写比较高效。

 

Java 数组中存放数组 java数组存放对象_System_04

说明:

(1)Java虚拟机栈:也是我们在Java中常说的栈。一般用来存放方法内创建的局部变量。比如引用一个数组对象的引用变量,int类型变量,for循环中的int变量等等

(2)本地方法栈:运行由c/c++编写的代码。java文件经过编译产生的.class文件被加载到这里运行。

(3)堆:是Java中最大的一块内存空间。只要是对象,都是在堆上开辟的。堆上的内存是不需要进行手动释放的,Java有自己的垃圾回收机制。

(4)程序计数器:保存一些指令和信息等。比如,一个方法调用另一个方法,需要保存那个主调指令的位置。

(5)方法区:存放常量,静态变量等。

2. 引用类型变量

引用到底是什么:

如果只是想要理解引用的作用,C++中说过引用其实可以理解为一个别名,即一个对象的别名。也可以理解为一个指针,保存着对象的存储地址。

所谓的 "引用" 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实 只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).

方法中的引用类型局部变量是开辟在栈区的,这在第一部分说过。存储的是引用所指向的对象的地址。 

引用与对象的内存存储:

int[] array = new int[]{1,2,3,4};
String s = new String("haha");

 如上,s和array都是引用类型的变量,存储在栈区。

引用变量存的是对象的地址。

Java 数组中存放数组 java数组存放对象_数组_05

接下来如果我们直接打印此array  System.out.println(array);

Java 数组中存放数组 java数组存放对象_数组_06

 

 打印出了这么一个地址(可以理解为数组首元素地址)。这里的地址为了安全,经过了特殊的处理,是这个引用对象的哈希地址。@之后的是哈希地址,@前的I表示这是一个int型,[表示是一个数组。arr这个引用,引用(指向)了一个数组对象。

又比如接下来的代码:

int[] array1 = new int[]{1,2,3,4,5};
int[] array2 = array1;
System.out.println(array1);
System.out.println(array2);

Java 数组中存放数组 java数组存放对象_数组_07

 这里打印出的地址是一样的,表示array2这个引用变量引用了array1引用变量所引用的对象。

Java 数组中存放数组 java数组存放对象_Java_08

 接下来,改变array2引用的对象,可以发现。

Java 数组中存放数组 java数组存放对象_java_09

注意的几点:

(1)引用不能指向引用,引用指向了对象(引用的对象)。

(2)一个引用,不能同时引用多个对象。

(3)引用不一定在栈上。还有可能在堆上,比如实例成员变量。

(4)引用赋值为null表示不指向任何对象。

例题

public static void func1(int[] arr) {
        array = new int[]{2,2,2,2,2,2};
    }
    public static void func2(int[] arr) {
        array[0] = 666;
    }
    public static void main(String[] args) {
        int[] array = new int[]{1,2,3,4,5};
        System.out.println(Arrays.toString(array));
        func1(array);
        System.out.println(Arrays.toString(array));
        func2(array);
        System.out.println(Arrays.toString(array));
    }

引用变量array开辟在栈区,指向堆区的一个对象数组,而调用func1作为实参传递时,array存储的这个数组的地址,将传递给形参arr,arr也会得到这个地址,从而指向同一个堆区的数组对象。在func1的函数栈帧内,形参arr存储在这块函数栈帧内,而函数体中,array = new int[]{2,2,2,2,2,2}; 表示在堆区创建一个新的数组对象,让形参array指向此对象。而这个操作是没有影响实参array的。因为arr和array存储在栈区的不同内存处。

但是在func2函数运行时,形参arr得到实参array保存的那个地址,函数体中,array[0] = 666; 表示利用下标运算符,找到形参array所指向数组对象的0下标的元素,修改为666

如图,func1函数体语句实行之前:

Java 数组中存放数组 java数组存放对象_Java_10

 func1函数体语句执行之后

Java 数组中存放数组 java数组存放对象_System_11

五、二维数组:一种特殊的一维数组

Java中的二维数组完美诠释了那句话:二维数组就是一个特殊的一维数组,只是数组元素是一个一维数组而已。

定义二维数组方式:

int[][] array1 = {{1,2,3},{4},{5,6}};
int[][] array2 = new int[][]{{1,2,3},{4,5},{6,7,8}};
int[][] array3 = new int[3][];
int[][] array4 = new int[3][4];

array1和array2就是一种常规的开辟数组的方式,数组有三个元素,每个元素都是一个一维数组。

array3 表示数组存有三个元素,每个元素都是一个数组引用类型。且都存储着null

array4 表示三个元素,每个元素是一个数组,数组有四个默认初始化为0的元素。

Java 数组中存放数组 java数组存放对象_System_12

三种遍历特殊一维数组(二维数组)的方式:

int[][] array = {{1,2,3},{4},{5,6}};
        // 1
        for(int i = 0; i < array.length; ++i) {
            for(int j = 0; j < array[i].length; ++j) {
                System.out.print(array[i][j]+" ");
            }
            System.out.println();
        }
        // 2
        for(int[] arr : array) {
            for(int j : arr) {
                System.out.print(j+" ");
            }
            System.out.println();
        }
        // 3
        System.out.println(Arrays.deepToString(array));

 注意第一种的j的for循环是< array[i].length  而不是array[0].length

一种可能的二维数组的存储方式

Java 数组中存放数组 java数组存放对象_System_13