一、数组的基本概念
1、引入
举例
例如当我们需要保存多个学生的成绩时,如果没有数组我们就需要创建多个变量,分别存储数据。
代码示例
public class Test {
public static void main(String[] args) {
int score1 = 60;
int score2 = 70;
int score3 = 80;
int score4 = 90;
System.out.println(score1);
System.out.println(score2);
System.out.println(score3);
System.out.println(score4);
}
}
缺点:如果有100个数据需要存储,难道我们要创建100个变量吗?这样就太过于麻烦了。
但这些数据是相同类型的,所以我们可以使用数组来存储数据。
代码示例
public static void main(String[] args) {
int[] score = {60, 70, 80, 90};
for (int i = 0; i < score.length; i++) {
System.out.println(score[i]);
}
}
需要创建多个相同类型变量存放数据时,可以使用数组,因为数组可以存放相同数据类型的变量
2、什么是数组
概念
可以看成是相同类型元素的一个集合
特点
- 数组中存放的元素类型相同
- 在内存中是连续存放的
- 每个空间有自己的编号,起始位置的编号为0,即数组的下标
举例
就像车库里的停车位,一排车位,车辆停在车位上就像数据存储在数组里,车位有编号从 0 开始依次递增。
3、数组创建及初始化
创建
语法
T[] 数组名 = new T[N];
解释:
T:表示数组中存放元素的类型
T[]:表示数组的类型
N:表示数组的长度
代码示例
public static void main(String[] args) {
int[] arr1 = new int[10]; //创建一个可以存放10个 int 类型元素的数组
double[] arr2 = new double[5]; //创建一个可以存放5个 double 类型元素的数组
}
初始化
动态初始化
在创建数组时,直接指定数组中元素的个数。
代码示例:int[] array = new int[10];
静态初始化
在创建数组时不直接指定数据元素个数,而直接将具体的数据内容进行指定。
代码示例:public static void main(String[] args) { //静态初始化数组 int[] arr1 = new int[] {1, 2, 3, 4, 5}; String[] arr2 = new String[] {"xx", "yy", "zz"}; }
注意事项
- 静态初始化没有指定数组长度,但是编译器在编译时会根据{}中元素个数来确定数组的长度。
- 静态初始化时, {} 中数据类型必须与 [] 前数据类型一致
- 静态初始化可以简写,省去后面的 new T[]。
代码示例:
int[] arr1 = {1, 2, 3, 4, 5};
- 数组也可以按照C语言个数创建
int arr[] = {1, 2, 3}; //不推荐,数据类型与 [ ],意思更清晰,不容易看走眼
- 静态和动态初始化也可以分为两步,但是省略格式不可以
代码示例:
public static void main(String[] args) { //动态初始化 int[] arr1; arr1 = new int[10]; //静态初始化 int[] arr2; arr2 = new int[] {1, 2, 3, 4, 5}; //省略new 数据类型[] //省略格式不能拆分,不然编译失败 /* int[] arr3; arr3 = {1, 1, 1, 1, 1};*/ }
- 数组若没有初始化,其元素有 默认值
(1)、元素为 基本数据类型
数据类型 默认值 byte 0 short 0 long 0 int 0 char \u0000 flaot 0.0f double 0.0 boolean false (2)、元素为 引用数据类型
默认值为 null
4、数组的使用
数组中元素访问
数组在内存中,是一块连续的空间,从下标为 0 的位置开始,依次递增,我们可以通过 下标 随机访问数组中的元素
代码示例
public static void main(String[] args) {
//下标访问数组
int[] arr = {1, 2, 3};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
arr[2] = 100;
System.out.println(arr[2]);
}
注意事项
- 数组是一段连续的内存空间,因此支持随机访问,即通过下标访问快速访问数组中任意位置的元素。
- 下标从0开始,介于[0, N)之间不包含N,N为元素个数,不能越界,否则会报出下标越界异常。
代码示例:
public static void main(String[] args) { //下标访问数组越界 int[] arr = {1, 2, 3}; System.out.println(arr[3]); }
执行结果:抛出了 java.lang.ArrayIndexOutOfBoundsException 异常. 使用数组一定要下标谨防越界
遍历数组
概念
遍历:将数组中的所有元素都访问一遍, 访问 是指对数组中的元素进行某种操作
举例
代码示例
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
System.out.println(arr[4]);
}
以上代码有几个问题:
- 问题一:如果数组中增加了一个元素,就需要增加一条打印语句。
- 问题二:如果数组中有100个元素,就需要写100个打印语句。
- 问题三:如果现在要把打印修改为给数组中每个元素加1,修改起来非常麻烦。
解决方法:
- 问题二、三可以使用 循环 来解决。
代码示例public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5}; for (int i = 0; i < 5; i++) { System.out.println(arr[i]); } }
- 问题一有两个解决办法
方法一: 使用 for - each 遍历数组。
- 认识 for - each
for 循环的另外一种使用方式.,能够更方便的完成对数组的遍历,可以避免循环条件和更新语句写错。
- 代码示例
public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5}; //for - each for (int x : arr) { System.out.println(x + ""); } }
方法二: 使用 数组对象 . length 获取数组长度。
- 代码示例
public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5}; //数组对象.length遍历数组 for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } }
- for循环 和 for-each 的区别
- for循环依赖下标来遍历数组,而for-each不依赖下标。
- for-each 不需要写循环条件和更新语句。
二、数组是引用类型
1. JVM内存分布
内存概念
内存:一段连续的存储空间,主要用来 存储程序运行时数据的
例如:
- 程序运行时代码 需要加载到内存。
- 程序运行产生的中间数据 要存放在内存。
- 程序中的常量 也要保存。
- 有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁。
JVM内存分布
JVM 对所使用的内存按照 功能的不同 进行了划分
- 程序计数器(PC Register)
只是一个很小的空间, 保存下一条执行的指令的地址
- 虚拟机栈(JVM Stack)
- 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧。
栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。
- 当方法结束,栈帧销毁,栈帧中的数据也被销毁。
- 平时说的栈,就是虚拟机栈。
- 本地方法栈(Native Method Stack)
与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的。
- 堆(heap)
- JVM所管理的最大内存区域。
- 使用 new 创建的对象 都是在堆上保存。
代码示例
public static void main(String[] args) { int[] arr = new int[] {1, 2, 3, 4, 5}; System.out.println(arr[0]); }
所以,我们创建的数组其实是存储在堆上的。
- 堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
- 方法区(Method Area)
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。例如:方法编译出的字节码就是保存在这个区域。
总结
每块内存,都有自己的使命,不同内存储存的数据就不同。
2. 基本数据类型变量和引用数据类型变量的区别
概念
- 基本数据类型创建的变量
称为 基本变量,该变量空间中直接存放的是其所对应的值。
- 引用数据类型创建的变量
称为 对象的引用,其空间中存储的是对象所在空间的地址。
代码示例
public static void main(String[] args) {
int a = 0;
int b = 100;
int[] arr = new int[] {1, 2, 3, 4, 5};
}
图解
- a,b是内置类型的变量:空间中存储的就是变量初始化的值。
- arr是数组类型的引用变量:内部保存的内容可以简单理解成是 数组在堆空间中的首地址,可以说,arr 这个引用 指向了 数组对象
3. 再谈引用变量
两个引用指向同一个对象
代码示例
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 4};
int[] arr2 = new int[4];
arr2[0] = 11;
arr2[1] = 22;
arr2[2] = 33;
arr2[3] = 44;
arr1 = arr2;
for (int i = 0; i < arr1.length; i++) {
System.out.println(arr1[i]);
}
}
图解
注意:不能说引用指向引用,只能说引用指向引用指向的对象
4. 认识null
概念
在 Java 中表示 “空引用” , 也就是一个不指向任何对象的引用。
作用
类似C语言中的空指针,指向一块无效的内存位置
所以不能对该内存进行任何读写操作,一旦进行操作就会报错。
代码示例
public static void main(String[] args) { int[] arr = null; System.out.println(arr[0]); }
执行结果:报 空指针异常 (NullPointerException)。
注意
Java 中并没有约定 null 和 0 号地址的内存有任何关联
三、数组的应用场景
1. 保存数据
代码示例
public static void main(String[] args) {
//保存数据
int[] arr = {1, 2, 3, 4};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
2. 作为方法参数
参数传递基本数据类型
- 代码示例
public static void func(int x) {
System.out.println("修改前x = " + x);
x = 99;
System.out.println("修改后x = " + x); // x 已经被修改为99
}
public static void main(String[] args) {
int a = 100;
System.out.println("修改前a = " + a);
func(a);
System.out.println("修改后a = " + a);
}
- 执行结果
- 图解
- 结论
形参是在func方法中的变量,用来接收实参的值,实参是在main方法中的变量,本质是两个实体,所以对形参的修改不会影响实参。
参数传数组类型(引用数据类型)
- 代码示例
public static void func2(int[] a) {
a[0] = 100;
}
public static void main(String[] args) {
int[] arr = {1, 2 ,3, 4};
System.out.println("修改前:arr[0] = " + arr[0]);
func2(arr);
System.out.println("修改后:arr[0] = " + arr[0]);
System.out.println("成功修改!!!");
}
- 执行结果
- 图解
- 结论
本质就是,把引用作为实参传递给形参,形参是一个在func方法中创建的数组类型的引用,它们是两个变量,这个形参用来接收实参的值,而实参的值储存的是对象在堆中的起始地址。所以这两个引用才同时指向一个对象。
- 引用的本质
- 本质就是存了本质上只是存了一个地址。
- Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大)。
3. 作为方法返回值
举例:获取斐波那契数列的前N项
- 代码示例
public static int[] fib(int n) {
int[] arr1 = new int[n];
arr1[0] = arr1[1] = 1;
for (int i = 2; i < n; i++) {
arr1[i] = arr1[i-1] + arr1[i-2];
}
return arr1;
}
public static void main(String[] args) {
int[] arr = fib(10);
System.out.println(Arrays.toString(arr));
}
- 执行结果
四、二维数组
1. 本质
是一个一维数组,只不过每个元素又是一个一维数组
- 代码示例
public static void main(String[] args) { int[][] arr1 = new int[][] {{1, 2, 3}, {4, 5, 6, 7}}; System.out.println(arr1[0].length); System.out.println(arr1[1].length); }
- 执行结果
- 图解
2. 基本语法
数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
3. 不规整的二维数组
1.省略行数的二维数组
1.代码示例
public static void main(String[] args) { int[][] arr = new int[2][]; arr[0] = new int[] {1, 2 ,3}; arr[1] = new int[] {2, 4, 6, 8, 10}; System.out.println(Arrays.deepToString(arr)); }
- 执行结果
- 图解
2. 对没有初始化 列数 的二维数组进行操作
- 代码示例
public static void main(String[] args) { //对没有初始化列数的二维数组进行操作 int[][] arr = new int[2][]; for (int i = 0; i < arr.length; i++) { for (int j = 0; j < arr[i].length; j++) { System.out.println(arr[i][j]); } System.out.println(); } }
- 执行结果
会报 空指针 错误。
- 图解
- 总结
- 没有设定列数时,二维数组中的元素(引用),没有指向任何数组,所以为null,对null,进行操作时,就会报错。
- 引用类型,未初始化,默认值为null。