目录
1、数组的基本概念
1.1为什么要使用数组
如果我们要定义三个整型变量并赋值,我们可以一个一个写,但是如果我们要定义10个或100个变量呢?我们如果再一个一个定义就会显得过于麻烦,也会让代码显的冗余!
而如果我们想定义一组数据,同时这组数据都是相同数据类型的,那我们就可以用“数组”来定义这书数据
1.2什么是数组
数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。
- 数组中存放的元素类型形同
- 数组的空间是连在一起的
- 每个空间有自己的编号,起始位置的编号为0,即数组的下标
1.3数组的创建及初始化
1.3.1 数组的创建和介绍
int[] array = new int[5];
组成 | int | int[ ] | array | new | 5 |
含义 | 表示数组中存放的元素的类型为整数型 | 表示数组的类型为整数型 | 数组的名字 | 创建对象关键字 | 表示数组的长度 |
“array”是一个变量,其中存储的是数组的地址
其中 '[' 表示数组类型,'I' 表示整数,'14ae5a5' 是地址的哈希值,也可以理解为地址。
在Java中,我们对于这种存储地址的变量,通常称为 “引用变量”【引用】
而‘new’是创建对象的关键字,所以我们创建的数组也是‘对象’!(这就和C语言中数字区分开了,Java中的数组其实就是对象)
所以,在引用变量当中,存储了对象的地址,一般叫做这个引用/指向了一个对象。
1.3.2 数组的初始化
数组的初始化主要分为动态初始化和静态初始化
1.动态初始化:在创建数组时,直接指定数组中元素的个数
int[] array = new int[5];
2. 静态初始化:在创建数组时,不直接指定数据元素个数,而直接将具体的数据内容进行指定
int[] array1 = new int[]{1,2,3,4,5,6,7,8};
double[] array2 = new double[]{1.0,2.0,3.0,4.0,5.0};
String[] array3 = new String[]{"hello","world"};
【注意事项】
- 静态初始化虽然没有指定数组长度,但编译器在编译时,会根据{}中元素个数来确定数组的长度。
- 静态初始化时,{}中数据必须与 [ ] 前的数据类型一样。
- 静态初始化可以简写,省去new T[]
例如:
int[] array1 = {1,2,3,4,5,6,7,8}; double[] array2 = {1.0,2.0,3.0,4.0,5.0}; String[] array3 = {"hello","world"};
(注意:虽然省去了new T[ ],但是编译器编译代码时还是会还原)
- 如果不确定数组当中内容时,使用动态初始化,否则建议使用静态初始化。
- 静态和动态初始化也可以分为两步,但是省略格式不可以
//动态初始化 int[] array1; array1 = new int[10]; //静态初始化 int[] array2; array2 = new int[]{1,2,3,4};
注意:省略格式不可以拆分,否则编译失败
- 如果没有对数组进行初始化,数组中元素被这设置为默认值
1、如果数组中存储的元素为基类类型,默认值为基类类型对于的默认值
类型 默认值 byte 0 short 0 int 0 long 0 float 0.0f double 0.0 char /u0000 boolean false 2、如果数组中存储元素类型为引用类型,默认值为null
1.4数组的使用
1.4.1数组中元素访问
数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,该编号称为数组的下标。
数组可以通过下标访问数组中任意位置的元素,同时还能通过 [ ] 对数组中的元素进行修改!
int[] array = new int[]{1,2,3,4,5};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
array[2] = 30;
System.out.println(array[2]);
【注意事项】
- 数组是一段连续的内存空间,因此支持随机访问,即通过下标快速访问数组中任意位置的元素
- 下标从0开始,介于[0,N) 之间不包含N,N为元素个数,数组访问不能越界,否则编译器会报出下标越界异常
1.4.2 遍历数组 【3种方式】
什么是“遍历”? 所谓遍历就是指将数组中的所有元素都访问一遍!
例如:
int[] array = {1,2,3,4,5};
System.out.println(array[0]);
System.out.println(array[1]);
System.out.println(array[2]);
System.out.println(array[3]);
System.out.println(array[4]);
上述代码虽然可以起到对数组中元素的遍历,但是如果数组中元素增加到100个呢?我们难道要写100条打印语句吗?
第一种遍历方式
为了解决上述问题,用循环来进行遍历打印。
int[] array = {1,2,3,4,5,6,7};
for (int i = 0; i < 7; i++) {
System.out.print(array[i]+" ");
}
现在又有一个问题,如果数组增加了一个元素,那么我就应该把for循环的条件改为“ i < 8”,这样就显得会很麻烦。我们能否直接获取数组的长度呢?
【注意】:在数组中,可以通过 ‘数组对象.length’ 来获取数组的长度
第二种遍历方式
同时,也可以使用 for-each 遍历数组
int[] array = {10,20,30,40};
for(int x:array){
System.out.print(x+" ");
}
for-each 是 for 循环的另外一种使用方式,能够更方便的完成对数组的遍历,可以避免循环条件和更新语句写错。
第三种遍历方式
在Java中,为了让我们更好的去操作数组,提供了一系列的方法,这些方法在工具类Arrays当中
如下
在使用上述方法时,我们需要到入Arrays的包!
import java.util.Arrays;
如果你不想手动添加,当你在写Arrays时,编译器会提示,按enter键编译器就能直接导入
而toString方法其实就是把我们传入的数组,以字符串的形式进行输出。
我们查看toString源代码就能看出来,返回值为String类型,并且在数组前后加上了‘[’‘]’。
2.数组是引用类型
2.1基本类型变量与引用类型变量的区别
在上文我们提到,引用类型的变量,是对对象的引用,而与基本类型变量最大的不同就是,
基本变量中存储的是本身的值,而引用类型的变量存储的是对象的地址。
基本类型变量:用基本数据类型创建的变量
我们来看下面这个例子:
在讲解下面这个例子之前先扩展两个小知识
- 虚拟机栈(JVM Stack) : 其中存储的是与方法调用相关的信息,每个方法执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他一些信息,保存的都是与方法执行时相关的一些信息。
- 堆(Heap): JVM管理的最大内存区域,使用 new 创建的对象都是在堆上保存(例如下面的 new int[]{1,2,3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
public static void main(String[] args) {
int a = 10;
int[] array = new int[]{1,2,3};
}
在上面代码中,a和array都是main方法内部的变量,其空间都在main方法对应的栈帧都分配
a 是内置类型的变量,因此空间中保存的就是给该变量初始化的值
array 是数组类型的引用变量,其中保存的值是所引用对象在堆中存放的地址
我们可以看到,引用变量并不直接存储对象本身,而是存储对象在堆中所在的地址。通过该地址,引用变量便可以去操作对象。(类似于C语言中的指针)
2.2细讲引用变量
我们来看一下下面这段代码运行的结果
public static void main(String[] args) {
int[] array1 = new int[3];
array1[0] = 1;
array1[1] = 2;
array1[2] = 3;
System.out.println("array1 :"+Arrays.toString(array1));
int[] array2 = new int[]{100,200,3,4,5};
System.out.println("array2 :"+Arrays.toString(array2));
array1 = array2;
array1[2] = 300;
array1[3] = 400;
array2[4] = 500;
System.out.println("array1 :"+Arrays.toString(array1));
}
我们不难发现,通过array1所打印出数组变了,也就是说引用变量array1指向的对象变了,同时array2和array1通过下标去对象进行了修改。
下面我通过画图的形式分三步具体讲解这一过程:
第一步:对象的创建
1.创数组array1 ,但是没有给数组元素设置初始值,因此每个位置都是0
2.array1通过下标的方式将数组元素分别修改为1、2、3
3.创建数组array2
第二步:改变引用对象 array1 = array2
array1 = array2 这段代码的意思是让array1去引用array2 引用的数组空间,也就是说array1和array2可以同时调用同一个数组。此时array1中的地址改变为0x002412
第三步:array1和array2通过下标修改同一个数组对象
array1通过下标将数组2中第三个和第四个元素分别修改为300和400,此时array2也能看到数组中修改后的值,这里是因为array1和array2此时引用的是同一个数组对象!
然后array2通过下标把数组中第五个元素修改为500,此时array1也能看到数组中修改的值。
通过打印我们能看到最终的结果
2.3 引用变量的空引用
在实际使用中,我们可能只想创建一个引用变量,并不想有所指向。这样我们就可以把null赋值给该引用变量
【引出】 null 在Java中表示“空引用”,也就是不指向对象的引用
public static void main(String[] args) {
int[] arr = null;
}
在这里要强调一点,在C语言语言中NULL和0等价的,但是在Java中null就是null跟0没有任何关系
比如我们将0赋值给引用变量时就会报错!
【注意】
null 的作用类似于C语言中的 NULL(空指针),都是表示一个无效的内存位置。因此不能对这个内存进行任何读写操作!
所以当给引用变量赋值 null 时,该引用变量既不能通过'.'访问length,也不能通过下标进行访问。如果进行以上错误的操作的话,编译器就会报出空指针引用异常警报 ‘NullPointerException’
以下是报错信息截图:
3.数组的应用场景
3.1作为函数的参数
当我们需要传递一堆相同类型的数据时,我们就可以把数组作为方法的参数进行传递。如下
public static void func(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
func(array);
}
在上述代码中,main方法中的array是实参,而funct方法中arr是形参。而把array传递给arr的过程,其实就是地址的传递,也可以理解为把array和arr指向了同一个对象。
这时就出现了一个问题,如果修改形参arr,那么实参array会改变吗?
我们来看下面的代码
public static void func1(int[] arr1){
arr1 = new int[]{10,20,30,40,50};
}
public static void func2(int[] arr2){
arr2[0] = 10;
}
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
func1(array);
func2(array);
System.out.println(Arrays.toString(array));
}
大家猜一下打印的结果是什么?结果如下
我们发现array指向的对象中,只有0号下标的元素改变了。而在func1和func2中都对形参进行了就该,结果只有方法func2修改对形参造成了影响!是不是有点迷惑,别急让我慢慢来解答
首先main先在虚拟机栈里面创建栈帧,也可以理解为创建一个空间。然后在main所述的空间中,声明一个引用变量array同时在堆中创建数组对象,array中存储对象的地址‘0x99’(array指向对象)。
接着,在main上面创建了func1的栈帧,并在其中声明了引用变量arr1。
array作为实参传递给方法func1中,也就是把array中的地址传递给arr1,所以arr1也指向‘0x99’。
然后,在中func1(),又创建了一个数组对象,在堆中的地址为‘0x56’,并赋值给了arr1。
arr1 = new int[]{10,20,30,40} ;
此时,arr1指向的对象变了,但并不影响array中存储的地址,也不影响array所指向的对象。
所以当方法func1()执行完,其在虚拟机栈中开辟空间也会被回收,同时因为地址为‘0x56’对象没有人引用,也会被回收。
到此方法func1()执行完了,array中存储的地址还是“0x99”!
接下来轮到执行方法func2()了,还是先创建空间,然后创建临时变量等一系列操作。
array作为实参把所引用对象的地址传递过去,然后在方法func2()中arr2接收,所以array和arr2所指的对象一致。
arr2[0] = 10;
接下来,arr2通过下标把所指对象的第一个元素改为10,这个修改就影响到了array,因为他俩引用的是同一个对象。当array通过下标访问第一个元素的值也就更改为了‘10’ !
所以,形参只能通过相同地址的对象时,实参才能受到影响。而如果形参所指向对象改变了,那么实参将不受影响!
总结:所谓的‘引用’本质上只是存了一个地址,Java将数组设定成引用类型,这样的话后续进行数组参数传参,其实就是将数组的地址传入到函数形参中,这样可以避免对整个数组的拷贝!
3.2作为函数的返回值
数组不仅可以作为参数传递,也可以作为返回值
当我们想返回多个相同类型的数时,我们就可以把它们定义为数组在进行返回
比如:获取斐波那契数列的前N项
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 < array.length; i++) {
array[i] = array[i-1] + array[i-2];
}
return array;
}
public static void main(String[] args) {
int[] arr = fib(10);
System.out.println(Arrays.toString(arr));
}
4.二维数组
二维数组本质上也就是一维数组,只不过每个元素都是一个一维数组
4.1二维数组初始化
同时二维数组初始化可分为动态初始化和静态初始化
1.二维数组动态初始化
int[][] arr = new int[2][3];
在Java中的二维数组跟C语言中的二维数组有一点不同。C语言中可以省略行,而Java中可以省略列!
这种二维数组叫‘不规则数组’
在不规则数组中,因为省略了列,所以编译器无法判断什么时候换行,所以我们要给每一行进行初始化。如下
int[][] arr = new int[2][];
arr[0] = new int[3];
arr[1] = new int[5];
这样代表着第一行有3个元素,第二行有5个元素
2.二维数组静态初始化
int[][] arr = new int[][]{{1,2,3},{4,5,6}};
int[][] arr2 = {{1,2,3},{4,5,6}};
4.2二维数组遍历
在讲数组遍历之前,我来讲一下二维数组的存储结构,以下面代码为例
int[][] array = new int[][]{{1,2,3},{4,5,6}};
如果单看二维数组的行而不看列的话,二维数组中就有两个元素,而这两个元素可以看成两个单独的引用变量,每个引用变量可以具体指向每个对象
如下,拆解
我们可以只打印每一行的数据看看结果是什么
如图打印出的是每一行元素指向对象的地址,通俗点就是每一行都是一个一维数组,所以打印出的都是地址。
从这我们可以看出,想要遍历二维数组每一个元素单层for循环是做不到的,所以便有了一下遍历方式
1.两层for循环嵌套遍历打印
public static void main(String[] args) {
int[][] array = new int[][]{{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();//打印完一行,换行
}
}
打印结果:
这里面需要注意的是外层循环和内层循环的条件,就那上述代码来说,外层循环遍历的是着二维数组行数,为2,所以条件为i<2。在以为数组中数组名加length可以得到数组中的元素个数,所以单独引用二维数组的数组名array可以看做为一维数组。
array.length就代表着array中有两个元素,分别为两个一维数组!
了解了外层,内存就清晰了不少,内存循环的条件为j<array[i].length。因为二维数组每行都是一维数组,所以就可以把array[i]看成每个一维数组的数组名!
2.两层for-each遍历打印二维数组
public static void main(String[] args) {
int[][] array = new int[][]{{1,2,3},{4,5,6}};
for (int[] tmp:array) {
for (int x:tmp) {
System.out.print(x+" ");
}
System.out.println();//打印一行完换行
}
}
打印结果:
3.通过Arrays.deepToString()类打印
在Java中提供了一个让我们打印二维数组的类,如下
public static void main(String[] args) {
int[][] array = new int[][]{{1,2,3},{4,5,6}};
System.out.println(Arrays.deepToString(array));
}
打印结果:
跟打印一维的类一样,我们需要导入Arrays包才能调用该类
import java.util.Arrays;
最后,二维数组的用法跟一维数组没有明显的区别,因此就不再赘述了。
同理,还存在‘三维数组’‘四位数组’等更复杂的数组,只不过出现概率很低