更好的阅读体验:点这里 ( www.doubibiji.com
)
更好的阅读体验:点这里 ( www.doubibiji.com
)
更好的阅读体验:点这里 ( www.doubibiji.com
)
5 数组
前面我们保存数据的时候,是将数据保存在变量中,如果要保存2个数据,就声明2个变量。
如果要保存100个数据呢,1000个数据…呢?
所以就需要保存数据的集合来保存批量的数据,下面介绍一下数组。
数组的主要特点:
- 有序,顺序排列;
- 只能存储相同类型的元素;
- 大小在创建的时候就确定了,大小不可变;
5.1 一维数组
1 初始化
同样,数组和变量一样,在使用之前也要进行声明和初始化,数组变量是引用类型的。
// 数组声明
// 声明方式1
int[] numbers1;
// 声明方式2
int numbers2[];
// 对上面声明的数组进行初始化
// 初始化方式1:动态初始化
numbers1 = new int[5]; // 表示定义了长度为5的数组,此时数组中的值都为0
// 初始化方式2:静态初始化,直接初始化各个元素的值
numbers2 = new int[]{1, 2, 3, 4, 5, 6}; // 表示定义了长度为5的数组,内容为1,2,3,4,5
// 初始化方式3:静态初始化,直接初始化各个元素的值
int[] number3 = {1, 2, 3, 4, 5}; // 初始化列表
// 声明并初始化
String[] names = new String[5]; // 表示定义了长度为5的数组,内容为null
String[] names2 = {"Hello", "Doubi"};
定义数组就是类型加上 []
,[]
可以在类型后,也可以在变量名后,Java 中推荐放在类型后面。
int[] numbers1
中,[]
表示是数组,int
表示是整形的数组,里面只能放 int
类型的数据。
2 基础操作
获取长度
使用 .length
属性可以获取到数组的长度。
int [] numbers = new int[]{1, 2, 3, 4, 5};
System.out.println("数组长度:" + numbers.length); // 输出:5
访问元素
访问数组元素是使用下标来访问的,从0开始。
注意不要越界,下标越界会报错。
public static void main(String[] args) {
int [] numbers = new int[]{1, 2, 3, 4, 5};
int firstNum = numbers[0]; // 获取第一个元素
System.out.println(firstNum); // 输出:1
int lastNum = numbers[numbers.length - 1]; // 获取最后一个元素
System.out.println(lastNum); // 输出:5
numbers[0] = 100; // 修改第一个元素的值
System.out.println(numbers[0]); // 100
}
因为数组的长度创建后就无法更改了,所以我们无法删除和添加更多到元素到数组中,只能访问和修改数组中的元素。
例如现在创建了一个长度为20的数组,添加学生姓名,现在发现长度不够了,像扩大数组的长度,放更多的元素,这种做法你只能想想。实际上你只能重新创建一个更大的数组,将这个数组的数据复制到更大的数组中。
访问元素的时候一定要注意,不要超越数组的长度,例如:
int [] numbers = new int[]{1, 2, 3, 4, 5}; // 数组的长度为5
int num = numbers[numbers.length]; // 报错,数组越界
长度为 5 的数组,最后一个元素的下标 index 为 4,所以不能大于4,否则会越界报错。
3 遍历数组
遍历就是依次访问数组中的元素。
使用 for
循环遍历
int[] numbers = new int[10];
// 遍历数组并进行赋值
for (int i = 0; i < numbers.length; i++) {
numbers[i] = i;
}
// 遍历进行输出
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]); // 依次输出0 1 2 3 4 5 6 7 8 9
}
注意编辑结束的条件是: i < numbers.length
,是用 for 循环,可以对数据进行遍历赋值和访问。
使用 for-each
循环遍历
for-each
循环也叫 增强型 for 循环
,使用起来更加简洁,但是不能使用 for-each
循环进行赋值。
int[] numbers = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (int num : numbers) { // num 表示每次取出的元素
System.out.println(num); // 依次输出0, 1 2 3 4 5 6 7 8 9
}
for-each
循环,冒号前面是依次取出的数组中的每个元素。
4 数组元素的默认值
动态初始化方式定义的数组,例如 new int[5]
,没有设置数组中元素的值,元素是有默认值的。对于基本数据类型,byte、short、int、long、char 类型的数组,默认值为 0
,float、double 类型的数组,默认值为 0.0
,boolean 类型数组,默认值为 false
。对于引用类型的数组,元素的默认值都是 null
。
举个栗子:
int [] numbers = new int[3];
System.out.println(Arrays.toString(numbers)); // [0, 0, 0]
boolean [] bools = new boolean[3];
System.out.println(Arrays.toString(bools)); // [false, false, false]
String [] strs = new String[3];
System.out.println(Arrays.toString(strs)); // [null, null, null]
Arrays.toString()
方法可以以字符串的形式打印数组中的内容。
5 内存结构
数组是引用类型的,和普通的数据类型有一些不同。
int [] numbers = new int[]{1, 2, 3, 4, 5};
对于上面的代码,在内存中的结构是这样的:
内存中会有不同的存储数据的区域,这里使用的有栈和堆,栈中存放的是局部变量,也就是方法中定义的变量,现在我们定义的变量都是在方法中的,也就是 main 方法,所以 numbers 变量是存储在栈中的,但是右边的数组值是存储在堆中的,numbers 变量中存储的是一个地址值,是数组的首地址(第一个元素的起始地址,也就是1的起始地址,上面的地址值是胡诌的)。
再看一段代码:
int [] numbers = new int[]{1, 2, 3, 4, 5};
numbers = new int[]{1, 2, 3};
上面的代码,重新将一个数组赋值给 numbers,内存结构如下:
numbers 被赋值为一个新的数组,那么存储的是新的数组的首地址值,numbers 与新的数组建立关系,与原来的数组断开联系了。
5.2 二维数组
一维数组看上去只是一行或一列数据:
二维数组可以存储类似一个表格的数据:
二维数组其实是:一维数组的每一个元素也是一个数组。
也就是一个一维数组中的每个元素,存储的都是一维数组的地址,地址指向的都是第二层一维数组,如下图:
1 初始化
// 静态初始化,数组3*4
int[][] numbers1 = new int[][]{{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
// 静态初始化,数组的长度是3,数组中的各个数组元素的长度是不同的
int[][] numbers2 = new int[][]{{1,2,3,4}, {5,6}, {7,8,9}};
//动态初始化,长度3*2
String[][] strs1 = new String[3][2];
//动态初始化,数组的长度是3,数组中的元素是空的,但是数组中的元素只能是数组类型
int[][] number3 = new int[3][];
现在看一下上面定义二维数组的内存结构:
int[][] numbers1 = new int[][]{{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
内存结构如下:
首先在栈中定义了一个变量 numbers1,numbers1 存储的是第一层数组的首地址,第一层中的各个元素存储的是第二层各个数组的首地址。
int[][] numbers2 = new int[][]{{1,2,3,4}, {5,6}, {7,8,9}};
内存结构如下:
String[][] strs1 = new String[3][2];
内存结构如下:
int[][] number3 = new int[3][];
内存结构如下:
在上面第一层数组的值是空的,还没有指向第二层数组,后面可以创建第二层数组,将地址赋值给第一层数组。
2 基础操作
获取长度
也是使用 .length
属性可以获取到数组的长度。
但是数组中第二层数组元素的长度需要获取到第一层数组元素再获取长度。
int[][] numbers = new int[][]{{1,2,3,4}, {5,6}, {7,8,9}};
System.out.println(numbers.length); // 3
System.out.println(numbers[0].length); // 4
System.out.println(numbers[1].length); // 2
String[][] strs = new String[3][];
System.out.println(strs.length); // 3
//System.out.println(strs[0].length); // 报错,因为strs[0]为null
访问元素
访问二维数组中的元素,需要通过两个 []
获取,第一个表示获取到的是第一层的元素,第二个才是获取到二维数组中的元素。
int[][] numbers = new int[][]{{1,2,3,4}, {5,6}, {7,8,9}};
System.out.println(numbers[0][0]); // 1
System.out.println(numbers[1][1]); // 6
numbers[2][1] = 1; // 修改值
numbers[1] = new int[]{1,2,3,4,5,6,7,8,9}; // 修改值
String[][] strs = new String[3][];
strs[0] = new String[]{"red", "yellow"}; // 元素就是数组
System.out.println(strs[0][1]); // yellow
不是数组的长度无法更改吗,为什么可以这样操作 numbers[1] = new int[]{1,2,3,4,5,6,7,8,9};
,这是将第一层数组指向了一个新的第二层数组而已。
3 遍历数组
使用 for
循环遍历
遍历二维数组,需要两层 for 循环。
// 创建一个3x3的二维数组
int[][] numbers = new int[3][3];
// 赋值
for (int i = 0; i < numbers.length; i++) {
for (int j = 0; j < numbers[i].length; j++) {
numbers[i][j] = i * 10 + j;
}
}
// 遍历并打印数组元素
System.out.println("二维数组的内容:");
for (int i = 0; i < numbers.length; i++) {
for (int j = 0; j < numbers[i].length; j++) {
System.out.print(numbers[i][j] + " ");
}
System.out.println(); // 换行
}
上面使用 for 循环对二维数组进行了遍历赋值和打印。
注意:第一层 for 循环取的是第一层数组的长度,第二层数组取的是第二层数组的长度。
执行结果:
二维数组的内容:
0 1 2
10 11 12
20 21 22
使用 for-each
循环遍历
// 创建一个3x3的二维数组
int[][] numbers = new int[3][3];
// 赋值
for (int i = 0; i < numbers.length; i++) {
for (int j = 0; j < numbers[i].length; j++) {
numbers[i][j] = i * 10 + j;
}
}
// 遍历并打印数组元素
System.out.println("二维数组的内容:");
for (int[] nums : numbers) {
for (int i : nums) {
System.out.print(i + " ");
}
System.out.println(); // 换行
}
使用 for-each
循环看上去要简洁很多,第一层 for-each
循环获取到的每个元素其实是一维数组,然后第二层 for-each
循环遍历每个一维数组。
5.3 数组的其他操作
1 数组复制
将一个数组的值赋值给另一个数组,该如何操作。
先看下面的代码,看看能否将 numbers1
的值复制给 numbers2
:
int[] numbers1 = {1, 2, 3, 4, 5, 6};
int[] numbers2 = new int[6];
numbers2 = numbers1;
System.out.println(Arrays.toString(numbers2)); // [1, 2, 3, 4, 5, 6]
numbers1[0] = 8; // 修改numbers1的元素
System.out.println(Arrays.toString(numbers2)); // [8, 2, 3, 4, 5, 6] numbers2的值也变了
很明显上面并不能将 numbers1
的值复制给 numbers2
,因为后来修改 numbers1
的值, numbers2
的值也发生了变化,说明不是独立的两份数据。
其实内存结果是下面这样的:
初始化两个数组的时候, numbers1
和 numbers2
还是独立的数据,后来 numbers2 = numbers1;
则将 number1 中存储的地址值赋给了 number2
,那么 number2
也之下那个了 number1
指向的堆中的内存地址。
所以我们在复制数组的时候,需要遍历数组,一个一个元素的去复制:
int[] numbers1 = {1, 2, 3, 4, 5, 6};
int[] numbers2 = new int[6];
// 将数组numbers1的值赋值给number2,这里要注意numbers1和numbers2的长度,不要越界
for (int i = 0; i < numbers1.length; i++) {
numbers2[i] = numbers1[i];
}
System.out.println(Arrays.toString(numbers2)); // [1, 2, 3, 4, 5, 6]
numbers1[0] = 8; // 修改numbers1的元素
System.out.println(Arrays.toString(numbers2)); // [1, 2, 3, 4, 5, 6],不影响numbers2
Java API 给我们提供了很多可以用的类,可以帮我们完成很多功能,不用需要我们编写代码去实现。
所谓的 Java API 就是为 Java 编程语言提供的可以直接使用的类库和方法。
例如上面的数组复制功能,也可以调用 Java 提供的 API :System.arraycopy
int[] numbers1 = {1, 2, 3, 4, 5, 6};
int[] numbers2 = new int[6];
// 将numbers1的值复制给numbers2
System.arraycopy(numbers1, 0, numbers2, 0, numbers1.length);
System.out.println(Arrays.toString(numbers2));
numbers1[0] = 8; // 修改numbers1的元素
System.out.println(Arrays.toString(numbers2)); // [1, 2, 3, 4, 5, 6],不影响numbers2
System.arraycopy
参数说明:
- 第一个参数:源数组,数据从哪里来;
- 第二个参数:从源数组的哪个位置开始复制;
- 第三个参数:目标数组,数据复制给谁;
- 第四个参数:放到目标数组哪个位置;
- 第五个参数:要复制的数据的长度;
还有另外的API : Arrays.copyOf
和 Arrays.copyOfRange
,也可以完成数组的复制,不过是重新开辟了内存空间。
int[] numbers1 = {1, 2, 3, 4, 5, 6};
int[] numbers2 = new int[6];
numbers2 = Arrays.copyOf(numbers1, 4);
System.out.println(Arrays.toString(numbers2)); // [1, 2, 3, 4]
int[] numbers3 = Arrays.copyOfRange(numbers1, 2, 5);
System.out.println(Arrays.toString(numbers3)); // [3, 4, 5]
在上面的代码中,并没有修改原来 numbers2
的内存空间的数据,而是重新开辟了内存控制,然后将内存地址给了变量 number2
。
Arrays.copyOf
的第一个参数是源数组,第二个是复制的数据长度,复制是从索引0开始复制的。
Arrays.copyOfRange
的第一个参数是源数组,第二个参数是复制的起始位置(包含该位置),第三个参数是复制的结束位置(不包含该位置)。
2 冒泡排序
思考:
一个 int[]
类型的数组,其中的元素大小没有顺序,该如何实现将其中的元素由大到小排列呢?
原理
冒泡排序是数组排序中一个非常经典的算法。
原理是:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。
首先取出第一个数,依次和后面的元素比较,如果比后面的元素大,就交换,否则继续用后面的数比较。
用图示例一下:
上面只是第一轮的比较,会找到最大的数,排到数组末尾;第二轮需要拿4进行比较,最终找到第二大的数6,6会沉到7前面,就不用和7比较了,也就是每比较一次,在最后面就会少比较一次,因为大的都沉到底部了,后面都是排序好的,自然就不用比较了。
冒泡算法因为在排序的过程中,较大(或较小)的元素会像气泡一样从数组的一端“浮”到另一端,像冒泡一样。
Java代码实现
下面使用 Java 代码实现冒泡排序。
public static void main(String[] args) {
int[] num = {6, 4, 7, 1, 2, 5, 3};
// 总共比较数组长度-1次,i就是当前比较的次数
for (int i = 0; i < num.length - 1; i++) {
// 每一次比较,在最后面都会少比较已经比较的次数
for (int j = 0; j < num.length - i - 1; j++) {
// 如果当前元素大于下一个元素,则交换它们
if (num[j] > num[j + 1]) {
// 交换两个位置的值
int temp = num[j];
num[j] = num[j + 1];
num[j + 1] = temp;
}
}
}
System.out.println(Arrays.toString(num)); // [1, 2, 3, 4, 5, 6, 7]
}
同样,在实际开发中也不需要我们手写排序算法, Java API 也提供了排序的方法,可以直接调用排序方法来对数组进行排序,只是 Java API 底层不是用冒泡排序实现的,冒泡排序的效率不是很高。
int[] num = {6, 4, 7, 1, 2, 5, 3};
// 从小到大对数组进行排序
Arrays.sort(num);
System.out.println(Arrays.toString(num)); // [1, 2, 3, 4, 5, 6, 7]
所有当我们要实现一个功能的时候,可以先查一下 Java API 有没有提供现成的功能。