目录
1. Java数组的基本概念
1.1 数组的定义
在C语言中我们也学过数组,数组是一组相同元素的集合,其内存储的都是相同类型的元素,数组在内存空间里面是连续的,这也就意味着我们只需要知道数组的首地址就可以找到后面的所有元素。
Java里面的数组定义和C语言的大相径庭,但是在Java里面同样也可以使用C语言的数组定义方式,只是有些别扭。
为什么说Java用C语言的数组定义方式有些别扭呢?
我们知道数组的内容是一组相同类型元素的集合,而数组的类型和内容的类型是息息相关的,如果内容是int型的,那么数组的类型就是int[ ]型的,那么看C语言的数组的定义方式,中括号[ ]在数组名的后面,数组的类型直接给数组名分割开来了
再看Java的数组定义方式,数组名在类型的后面,一般来说,我们平时定义变量的时候都是先写类型,然后变量名,然后再赋值。
public static void main(String[] args) {
int a = 100;
double d = 1.0;
String str = "rtx";
}
那这么一对比,是不是显得C语言的数组定义方式很别扭,所以在Java中数组定义方式都是先写内容类型然后加[ ] ,然后是数组名,再然后是内容,不过Java的数组定义方式有三种:
public static void main(String[] args) {
int[] array1 = {1,2,3,4}; // 方法1
int[] array2 = new int[]{1,2,3,4}; // 方法2
int[] array3 = new int[X]; // 方式3
}
方法1和方法2叫做静态初始化: 在创建数组的时候不直接指定数据元素的个数,而直接将具体的数据内容进行指定,方法1可以说是方法2 的简写,平时比较常用方法1,在编译器编译代码的时候,会将方法1 给还原成方法2 的格式。但是需要注意的是使用方法2的时候 [ ]内不可以写任何数值,否则报错。
方法3叫做动态初始化: 在创建数组的时候直接指定数组中元素的个数,当我们使用该方法定义数组时,我们只是为数组开辟了空间,并没有进行初始化,此时数组内的内容默认为0,不管是任何类型,都是其对应的默认0值。
public static void main(String[] args) {
int N = 10;
int[] array1 = {1,2,3,4}; // 方法1
int[] array2 = new int[]{1,2,3,4}; // 方法2
int[] array3 = new int[N]; // 方式3
System.out.println("array3[3] = " + array3[3]);
}
如果数组内存储的是基本数据类型,默认值为基本数据类型对应的0值。如果数组内存储的是引用型数据类型,默认值为null
1.2 数组存在的意义
在编程代码过程中,比如我们需要定义学生类,类里面有学生的年龄成员,此时我们需要把学生的年龄输入进去。并且需要将其输出,在没有数组的情况下,我们只能:
public static void main(String[] args) {
int a1 = 10;
int a2 = 10;
int a3 = 10;
int a4 = 10;
int a5 = 10;
int a6 = 10;
int a7 = 10;
int a8 = 10;
int a9 = 10;
int a10 = 10;
}
一旦数量特别大,我们需要创建大量的变量,占用大量的时间,占用大量的代码行,导致代码的可读性下降,得不偿失,这个时候数组的出现解决了这个问题,由于类型都是相同的,直接可以存储到数组中,方便快捷,同时代码的可读性也提高了。
1.3 数组的使用
数组是一段连续的内存空间,支持随机访问。 我们只需要通过数组的下标就可以将数组的内容提取出来,数组的每个空间都有自己的下标,起始空间的下标为0,只需要知道对应内容的下标,就可以将对应位的内容提取出来。
数组的下标从0开始,介于 [ 0 , 数组个数-1 ] 之间,不能越界,否则报出数组越界异常。
数组的存在就是方便数据的存储和数据的调用,当我们需要将数组的内容全部打印出来时,就需要用到数组的遍历,通常情况下遍历某个内容我们都是使用循环遍历的,数组也是如此。
方法1: 通过下标进行循环遍历。
int[] array = {1, 2, 3, 4, 5};
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
方法2:不通过下标,只是简单的遍历数组。
int[] array = {1, 2, 3, 4, 5};
for (int x : array) {
System.out.println(x);
}
两者没有太大的区别,唯一就是是否依靠下标
打印数组内容还有一个方法,就是使用Java提供的Arrays.toString()方法,只需要输入数组,直接将返回值进行打印即可。
int[] array = {1, 2, 3, 4, 5};
System.out.println(Arrays.toString(array));
1.4 二维数组
无论是C语言还是Java,二维数组都是由一维数组构成的,这也就意味着二维数组本质上是一个一维数组,只是有些特殊,二维数组的每个元素就是一个一维数组。
二维数组的定义语法:
和一维数组的定义方式相似,不管需要注意的是初始化数据需要将内容用{ }分割开来。
int[] array = {1, 2, 3, 4, 5};
System.out.println(Arrays.toString(array));
2. 引用类型+JVM的内存分布
在了解什么是引用类型之前我们需要先了解什么是JVM的内存分布。
2.1 JVM的内存分布
内存是一段连续的空间,是一段有限性的空间,并非无穷无尽,其主要用来存储程序运行时数据的。
- 程序运行时代码需要加载到内存
- 程序运行时产生的中间数据要存放在内存中
- 程序中定义的常量有需要保存
- 有些数据需要长时间存储,有些数据应该在方法运行结束后就要被销毁
内存中含有各式各样的数据,如果不加区分的随意存放,那么会有可能导致内存提前耗尽,从而导致程序异常终止,内存的管理也非常困难。在干净整洁的空间内查找事物和在脏乱差的空间内查找事物,那种环境查找更加方便?
所以需要对内存进行划分,是为了更好的归纳和整理我们的内存从而提高内存的利用率。
因此JVM也对使用的内存按照不同的功能进行了划分,当前只需要对虚拟机栈和堆进行了解即可。引用类型和两者之间有重要关系。
- 程序计数器:用于保存下一条执行的指令的地址。
- 虚拟机栈:每个方法在执行时,都会先创建一个栈帧,栈帧内包含:局部变量,操作数栈,动态链接,返回地址以及其他的一些信息,保存的都是和方法有关的信息。当方法运行结束后,栈帧会主动销毁,栈帧中保存的数据也随之销毁了,当下一次再调用该方法时,又会重新创建栈帧。
- 本地方法栈:本地方法栈的作用和虚拟机栈的类似,但是本地方法栈是用于保存被Native方法修饰的局部变量,被Native关键字修饰的方法是由C/C++的代码编写的,JVM本质上也是由C/C++代码编写的。
- 堆:JVM管理的最大内存区域,使用new创建的对象都是保存在堆上的,堆是随之程序的开始运行而创建,随着程序的退出而创建,堆中的数据只要还在使用,那么就不会被销毁。
- 方法区:用于存储已经被虚拟机加载的类信息,常量,静态常量,以及编译器编译后的代码等数据,方法的字节码文件就是保存在这个区域。
2.2 基本数据类型和引用型数据类型的区别
- 基本数据类型创建的变量,称之为变量,该变量空间内存储的是其所对应的值,该变量空间是栈。
- 引用型数据类型创建的变量,一般作为对象的对象的引用,称之为引用变量,其变量空间内存储的是对象在堆上的地址。
public static void main(String[] args) {
int a = 1;
int b = 2;
int[] array = {1,2,3};
System.out.println(a);
System.out.println(b);
System.out.println(array);
}
输出:
数组也是引用类型的,引用类型一般是:数组,类,接口,枚举。
变量a,b,array都是属于局部变量,并且都是存储在虚拟机栈上的,只是两者存储的内容不同,局部变量a和b存储的是实际值,而变量array也叫做引用变量,存储的是数组对象的地址。
上面输出中,“[ I @4eec7777”可以理解为数组在堆空间中的首地址,[ 表示的是该地址存储的是数组对象,I 表示的是该数组对象的类型是int,@是分隔符 ,4eeec7777表示地址,“[ I @4eec7777”并不是真正意义上的地址,但是可以将其当做地址使用,“[ I @4eec7777”就像是没有拆包装的面包,面包被外层的塑料包裹起来,保护里面可以食用的面包,“[ I @4eec7777”就是和面包一样,被封装保护起来了,但同时被塑料包裹起来的面包也叫面包,“[ I @4eec7777”也可以被称之为地址,同时也可以当真正的地址一样使用。
引用变量并不直接存储对象本身,可以简单理解为存储的是对象在堆空间中的起始地址,通过该地址便可以去操作对象,引用变量可以简称为引用。有点类似于C语言的指针,但是Java的引用使用起来比指针更加容易,引用的使用不需要频繁的解引用,可以直接使用。
public static void main(String[] args) {
int[] array; // 代码1
array = new int[]{1,2,3}; // 代码2
}
代码1中,仅是在虚拟机栈上开辟了一块空间给引用array,但该空间里面并没有实际内容,可以理解为真正意义上的空,此时array是没有引用任何对象的,也就是此时:array == null,此时不可以对引用array进行任何调用对象的操作,否则会报空指针异常。
代码2中,此时使用new在堆上开辟了一块空间,给int[ ]类这个对象进行存储,并且把对象的地址存储到array中,此时栈空间中array的空间内不再是空,而是存储数组对象的地址。
需要注意的是:代码2里面的等号“=”并不是真正意义上的赋值的意思,而是将对象的地址传递给变量,类似于返回值。
2.3 引用注意事项
看一段代码:
public static void main(String[] args) {
int[] array1 = {1,2,3};
int[] array2 = {1,2,3,4,5,6};
array2 = array1; // 代码1
System.out.println(Arrays.toString(array1)); // 代码2
System.out.println(Arrays.toString(array2)); // 代码3
}
代码1的意思并不是引用array1引用array2的意思,而是引用array1引用array2所指向的对象,原本在代码1之前,array1和array2两个引用指向的是不同对象,但是经过代码1之后,两个引用指向了同一个对象。
此时引用array2原本指向的对象没有任何引用指向它,我们就无法再找到该对象进行操作了,这也就意味着该对象的地址找不到了,JVM就会将该对象进行销毁回收。当运行代码2和3时,打印出来的结果就是1,2,3。
2.4 传值传递
在C语言中有传值和传参两种,但是在Java中就只有一种,那就是传值,只是这个值可以是引用,也可以是实际值。
public static void func1(int ii) {
ii = 2;
System.out.println(ii);
}
public static void func2(int[] array2) {
array2[0] = 10;
}
public static void func3(int[] array3) {
array3 = new int[]{11,22,33};
}
public static void main(String[] args) {
int i = 1;
int[] array = {1,2,3,4};
func1(i);
func2(array);
func3(array);
System.out.println("i = " + i);
System.out.println("array = " + Arrays.toString(array));
}
从学习C语言的角度来说,传值,是不会修改实参本身的,这个说法在Java中不全对。在Java中的传值传递,首先要看你传的值是什么类型的。
- 该值是基本数据类型的,那么传参绝对不会修改实参本身。
- 如果值是引用型数据类型的,那么传参就有可能会改变实参本身,因为值是引用型数据类型,此时传参就相当与array2 = array,这也就意味着array和array2同时是指向同一个对象的,通过array操作该对象等同于array2操作该对象。但还是具体要看传参后形参做了什么,并不是说一定会修改实参的内容。
在方法func3中,方法内部给形参重新new了一个对象,那么此时形参和实参是指向两个完全没有关系的对象,那么此时两者再怎么操作都不会再影响到对方了
在方法func2中,代码:array2[0] = 10,该代码的意思是:将数组下标为0的空间里面存储的数值修改为10,此时因为array2和array指向的是同一个对象,那也就是真正意义上的形参的修改影响到了实参,代码array2[0] = 10等同于array[0] = 10。
在方法func1中,形参和实参是两个完全互不影响的栈帧,在方法内部无论如何对形参修改,当方法运行结束了之后,都不会对实参造成任何影响。
3. 数组总结和应用场景
3.1 一维数组和二维数组的存储
一维数组的存储:
二维数组的存储:
int[][] array = {{1,2,3},{4,5,6}};该二维数组由两个一维数组组成,分别为:array[0]和array[1]组成。
为什么说二维数组本质上是一个特殊的一维数组呢?
其实可以把二维数组理解为,有X行,里面加存储有X个一维数组,二维数组的引用虽然类型是int[ ][ ],但实际上在堆空间里面建立的还是一维数组,只是这个一维数组里面存储的不是基本数据类型,而是引用型数据类型,也就意味着,这个一维数组里面存储的是引用,此时这个引用的类型是一维数组。
遍历二维数组也很简单,使用两次for循环,并且范围分别是二维数组的范围和一维数组的范围。
int[][] array = {{1,2,3},{4,5,6}};
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();
}
3.2 引用的概念
引用本质上是一个变量,但是引用本身并不存储相应的实际值,而是用于存储对象的地址,此时就可以同过引用去对对象进行操作,这也就意味着引用的作用并非存储实际值。当我们创建一个对象时,实际上是在堆上为对象开辟了一块空间,但此时我们没办法直接对对象进行操作,对象也根本没有名字,也就是只有内在没有外表。
此时我们就需要一个类似于指针的东西,能够通过操作对象,而唯一能够将两个空间给联系起来的方法就是一个空间能够找到另外一个空间,这很明显就是一个空间内存储有另外一个空间的地址,通过地址可以找到另外一个空间的内容并进行操作,此时引用型变量油然而生,而引用型变量也叫做引用。
并且因为形参是实参的临时拷贝,如果在调用方法的时候传递数组,在没有引用的情况下,陡然将实参数组内大量的数据拷贝给实参,拷贝过程中会创建大量的中间变量以及大量的栈帧,一旦数据过多,就会导致系统资源快速消耗,虚拟机栈空间提前消耗,运行效率也会大大降低。而引用的存在恰好解决了这一难题,直接将引用作为实参传递给形参,此时仅仅只是传递了一个数据,形参就可以直接对实参指向的对象直接进行操作,省去了拷贝大量数据的繁琐过程,运行效率大大提升。
3.3 数组的应用场景
3.3.1 保存数据
public static void main(String[] args) {
int[] array = {1, 2, 3};
for(int i = 0; i < array.length; ++i){
System.out.println(array[i] + " ");
}
}
3.2.2 参数传基本数据类型
public static void main(String[] args) {
int num = 0;
func(num);
System.out.println("num = " + num);
}
public static void func(int x) {
x = 10;
System.out.println("x = " + x);
}
发现在 func 方法中修改形参 x 的值 , 不影响实参的 num 值 。
3.2.3 参数传数组类型(引用数据类型)
public static void main(String[] args) {
int[] arr = {1, 2, 3};
func(arr);
System.out.println("arr[0] = " + arr[0]);
}
public static void func(int[] a) {
a[0] = 10;
System.out.println("a[0] = " + a[0]);
}
发现在 func 方法内部修改数组的内容 , 方法外部的数组内容也发生改变 .
因为数组是引用类型,按照引用类型来进行传递,是可以修改其中存放的内容的。
总结 : 所谓的 " 引用 " 本质上只是存了一个地址 . Java 将数组设定成引用类型 , 这样的话后续进行数组参数传参 , 其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝 ( 数组可能比较长 , 那么拷贝开销就会很大 )。
比如:获取斐波那契数列的前N项。
public class TestArray {
public static int[] fib(int n){
if(n <= 0) {
return null;
}
int[] array = new int[n];
array[0] = array[1] = 1;
for(int i = 2; i < n; ++i){
array[i] = array[i - 1] + array[i - 2];
}
return array;
}
public static void main(String[] args) {
int[] array = fib(10);
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}