深入理解 Java 数组
虽然在平常开发中,使用集合(容器)的频率比数组高得多,不过集合的底层也是通过数组来实现的。而且,尽管集合相比数组来说强大得多,但是其执行效率远不及数组。所以在讲集合之前,非常有必要深入了解一下数组。全文脉络思维导图如下:
1. 一维数组详解
所谓数组,就是「相同数据类型的元素按一定顺序排列而成的集合」。先来看看一维数组的三种声明和赋值方式:
第一种:
int[] a = {1, 2, 3};
第二种:
int[] b = new int[] {1, 2, 3};
第三种:
int[] c = new int[3];
c[0] = 1;
c[1] = 2;
c[2] = 3;
以上这三种方式的效果都是一样的,创建了一个存储 1、2、3 这三个整数的数组。
❝可以使用下面两种形式声明数组 :
int[] a;
或int a[];
通常都会使用第一种风格, 因为它将类型
❞int[]
( 整型数组)与变量名分开了。
我们来反编译一下这三段代码的 .class
文件,你就会发现其实「在底层它们的创建方式都是一样的」,编译器自动的给我们加上了 new 关键字,甚至还把 c 的声明和赋值一体化了。
另外,需要注意的是:new int[3];
这条语句会创建一个能够存储 3 个元素的数组,不过该数组的最后一个元素的下标是 2(因为下标从 0 开始计数,相信我,刷算法题的时候,这个鬼东西经常会让你脑子短路)。并且这条语句会自动的初始化所有元素,比如对于 int 数组来说就是全部初始化为 0,对于 boolean 数组来说就会全部初始化为 false, 对象数组就会初始化为 null 等。
从上面这些代码和分析中,我们也不难看出,「数组创建之后是无法改变其存储空间大小的」(存储能力),尽管它可以改变每一个数组元素。
我们通过 IDEA 的联想功能来看看数组能够调用什么东西:
可以发现,数组拥有 Object
类的所有方法,并且还会新增一个属性 length
(注意是属性,而不是方法),用来表示这个数组的长度,我们可以这样调用:a.length
。
❝注意区别于
❞String
类的length()
「方法」,数组拥有的是length
「属性」,而非方法。
综上,数组不仅能够封装数据,还能调用属性和方法,那这和对象有啥区别?没错,这也就是为什么说「数组的本质是对象」了。回顾一下我们之前总结的 Java 中方法参数的使用情况(按值调用):
一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 一个方法可以改变一个对象参数的状态。 一个方法不能让对象参数引用一个新的对象。
而因为数组的本质是对象,因此,将数组作为参数传递给方法,这个数组是可以被改变的。
OK,接下来,以下面这段代码为例,我们来看看一维数组在内存中的存储方式:
int[] b = new int[] {1, 2, 3};
int 数组对象 b 存储在 栈中,而数组元素既然是 new 出来的,那当然是存储在堆中。只有当 JVM 执行 new int[]
时,才会在堆中开辟相应的内存区域。
2. 多维数组详解
我们再来看看多维数组,就以二维数组为例,同样的三种声明与赋值方式:
第一种:
double[][] a = {
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
};
第二种:
// 构造一个 4 行 4 列的二维数组
double[][] b = new double[4][4] {
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
};
第三种:
double[][] c = new double[4][4];
c[0][0] = 16; // 第一行第一列值为 16
c[0][1] = 3; // 第一行第二列值为 3
c[0][2] = 2;
c[0][3] = 13;
c[1][0] = 5; // 第二行第一列值为 5
c[1][1] = 10;
c[1][2] = 1;
c[1][3] = 8;
......
同样的,我们来反编译一下这三段代码的 .class
文件,底层它们的创建方式基本也都是一样的,不过有些细微的差别。编译器还是自动的给我们加上了 new 关键字,不过没有像一维数组那样把 c 的声明和赋值一体化了。
到目前为止,我们所看到的数组与其他程序设计语言中提供的数组没有多大区别。但实际存在着一些细微的差异, 而这正是 Java 的优势所在:「Java 实际上没有多维数组,只有一维数组」。多维数组被解释为「数组的数组」。请看下图:
由于可以单独地存取数组的某一行, 所以可以让两行交换。
int[] temp = b[1];
b[1] = b[2];
b[2] = temp;
3. for each 循环
Java 有一种功能很强的循环结构, 可以用来依次处理数组中的每个元素而不必为指定下标值而分心。这种增强的 for 循环的语句格式为:
for(variable : collection){
// todo
}
「collection
这一集合表达式必须是一个数组或者是一个实现了 Iterable
接口的类对象」,例如 ArrayList
。
下面我们来对比一下使用下标遍历数组和使用 for each 循环遍历数组这两种方式:
// 使用下标遍历数组
int[] a = new int[100];
for(int i = 0; i < 100; i++) {
a[i] = i;
}
// 使用 for each 循环遍历数组
for(int element: a){
System.out.println(element);
}
for each 循环语句的循环变量将会遍历数组中的每个元素, 而不需要使用下标值。
不过,需要注意的是,「for each 循环语句不能自动处理多维数组的每一个元素,它是按照行, 也就是一维数组处理的」。以二维数组为例,要想访问二维数组的所有元素, 需要使用两个嵌套的循环, 如下所示:
int[][] a = {
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
};
for(int[] row : a) { // 遍历每一行
for(int value : row) { // 遍历每一列
System.out.println(value);
}
}
4. 可变参数
在 「JDK 1.5」 之后,如果我们定义一个方法需要接受多个参数,并且「多个参数类型一致」,我们可以对其简化成如下格式:
修饰符 返回值类型 方法名 (参数类型... 形参名){ }
...
用在参数上,称之为「可变参数」,它「表明这个方法可以接收任意数量的参数」。其实这个写法完全等价与
修饰符 返回值类型 方法名 (参数类型[] 形参名){ }
虽然同样是代表数组,但是在调用这个带有可变参数的方法时,不用创建数组,直接将数组中的元素作为实际参数进行传递,这就是简单之处。当然,其实这种方式的底层实现也是将这些元素先封装到一个数组中,在进行传递,不过这些动作都在编译 .class
文件时就自动完成了。
代码演示:
public class ChangeArgs {
//可变参数写法
public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
public static void main(String[] args) {
int[] arr = { 1, 4, 62, 431, 2 };
int sum = getSum(arr);
System.out.println(sum);
int sum2 = getSum(6, 7, 2, 12, 2121);
System.out.println(sum2);
}
}
❝需要注意的是:如果在方法书写时,这个方法拥有多个参数,并且参数中包含可变参数,「可变参数一定要写在参数列表的末尾」。
❞
5. Arrays 类
Java 中,提供了一个很有用的数组工具类:java.util.Arrays
。它提供的主要操作有:
1)Arrays.toString
- 将一维数组转成字符串类型(打印一维数组的所有元素)
2)Arrays.deepToString
- 将二维数组转成字符串类型(打印二维数组的所有元素)
3)Arrays.copyOf
- 数组拷贝。举个例子,将 a 数组中的元素全部拷贝给 c 数组:
int[] c = Arrays.copyOf(a, 2 * a.length());
第 2 个参数是新数组的长度。这个方法通常用来增加新数组的大小:如果数组元素是数值型,那么多余的元素将被赋值为 0 ; 如果数组元素是布尔型,则将赋值为 false 等。相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素。
4)Arrays.sort
- 对数组中的元素进行排序
5)Arrays.equals
- Arrays
类提供了重载后的 equals
方法,用来基于内容比较数组,数组相等的条件是元素个数和对应位置的元素都相等。
6. 总结
不可否认,在 Java 中,数组是一种效率最高的存储和随机访问对象引用序列的方式。数组就是一个简单的线性序列,在内存中采用「连续空间分配」的存储方式,这使得通过下标访问元素非常快速。但是代价就是「一旦创建了数组, 就不能再改变它的大小」(尽管可以改变每一个数组元素)。
如果「经常需要在运行过程中扩展数组的大小, 可以使用集合 ArrayList
」 。它可以通过创建一个新实例,然后把旧实例中所有的引用移到新实例中,从而实现更多空间的自动分配。但是这种弹性需要开销,因此,ArrayList 的效率比数组低很多。当然,无论数组还是集合,如果越界,都会得到一个 RuntimeException
异常。
关于集合会写成一个系列,下篇文章就会陆续开更,内容没啥难度,不过要记的东西非常多,啃完集合后面还有个硬骨头多线程,这俩学完 Java 基础部分基本就没啥了。
参考资料
《Java 核心技术 - 卷 1 基础知识 - 第 10 版》 《Thinking In Java(Java 编程思想)- 第 4 版》 《On Java 8》中文版(《Java 编程思想》- 第 5 版) 清浅池塘 - Java 中的数组-Java那些事儿:https://juejin.cn/post/6844903498207756295
😁 点击下方卡片关注公众号「飞天小牛肉」(专注于分享计算机基础、Java 基础和面试指南的相关原创技术好文,帮助读者快速掌握高频重点知识,有的放矢),与小牛肉一起成长、共同进步
🎉 并向大家强烈推荐我维护的 Gitee 仓库 「CS-Wiki」(Gitee 推荐项目,目前已 1.0k+ star。致力打造完善的后端知识体系,在技术的路上少走弯路。相比公众号,该仓库拥有更健全的知识体系,欢迎给位小伙伴前来交流学习,仓库地址 https://gitee.com/veal98/CS-Wiki。也可直接下方扫码访问
原创不易,读完有收获不妨点赞|分享|在看支持