数组的定义
首先引用维基百科对数组的定义如下:
在计算机科学中,阵列资料结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的资料结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的储存地址。
个人认为这个定义简洁又准确,我对重点作了加粗处理。
- 数组存储的是相同类型的数据;
数组在初始化时需要指定类型,在给每个位置赋值时,值类型必须相同。
如我们定义一个数组用于存储“人”,那么就可以放入“男人”/“女人”/“老人”/“小孩”等都可以。
但是如果定义一个数组存储“男人”,那么“女人”/“女孩”/“中年妇女”都是不可以存放的,只可以存放“男人”及其子类型,虽然都是从属于“人”这个大类。
- 数组在内存中的分配是连续的;
数组大小(元素个数)在初始定义时即确定了,同时分配了存储需要使用的内存。所以可以分配一块连续的内存,这提高了数组操作的效率。
为什么这样效率高?很好理解,一个旅行团入住一家酒店,是在连续的一排房间找人好找,还是让他们随机入住,东一个西一个好找?答案不言而喻。
- 通过索引可以直接查找到该位置的数据;
还是以旅行团为例,每个人都有自己的房间编号,我们通过编号就可以找到所有人。
Java 数组的补充:
- Java 数组的长度固定,且在初始化时就已经确定,不可更改;
一种奇怪的对象
Java 是一种面向对象的编程语言(虽然现在也添加面向函数的支持,但我依然认为 Java 首先是一种面向对象的语言),但又不是完全的面向对象,它还有些基本数据类型,如 int, byte, char, long 等等,这些基本数据类型都有提供包装类,用于兼容面向对象。这种语言设计上的取舍,方便了编码,同时又补上了非完全面向对象的遗憾。
数组特殊在哪?
它是对象,但又找不到对应的类定义。代码如下:
// 声明基本类型-整数
int n = 5;
// 声明包装类型-整数
Integer in = 5;
// 声明普通类型对象
ArrayList list = new ArrayList<String>();
// 声明数组
int[] array = new int[2];
按照 Java 声明对象的语法,数组的类型应该是 int[]
, 真是奇怪!我们尝试打印出它的类型看看,代码如下:
System.out.println(int.class.getName());
System.out.println(Integer.class.getName());
System.out.println(ArrayList.class.getName());
System.out.println(int[].class.getName());
System.out.println(String[].class.getName());
输出会是这样的:
int
java.lang.Integer
java.util.ArrayList
[I
[Ljava.lang.String;
所以数组的类型是 [
,这是个啥?虽然很奇怪,但在 JVM 的解释中,它确实是一种奇怪的对象。
另外根据对象的定义,我也可以推断它是一种对象。
对象的定义:
类是对事物的一种抽象,用于泛指某一些具体或抽象的具有同一共性的物体/行为/概念…
而对象,就是类的具体实例。
很抽象,还是举例:
- 我们定义"车"是一个类,这是一种具体事物类,有轮子,能行驶,就是"车"。那么号牌是"鲁ABC456"的汽车,就是该类的一个对象,我那辆破自行车也是该类的一个对象。
- 我们定义"发送"是一个类,这是一种行为类,把信息从一方传递给另一方,就是"发送"。那么"通过 Email(abc@456.com) 发送",就是该类的一个对象,"通过手机号(130xxxxxxxx)发送信息到手机号(150xxxxxxxx)"也是该类的一个对象。
数组呢?它有长度,有toString()
,equals()
等行为,还有它特有的寻值操作 []
,很明显,数组也是一种类型,每个具体的数组就是一个对象。
数组是对象,虽然它很奇怪!!!
数组在数据结构中的重要性
数组性能优秀
数组内存连续,这种天生概念上就造就了优秀性能,这点不再说了。只再单纯分析下字节码,看看它在 JVM 的地位有多高。对比下面两个方法的字节码:
public void objArray() {
Integer[] n = new Integer[10];
n[0] = 2;
n[9] = 21;
int m = n[0] + n[9];
}
public void list() {
var list = new ArrayList<Integer>(10);
list.set(0, 2);
list.set(9, 21);
int lm = list.get(0) + list.get(9);
}
代码任务一样,只是一个用数组实现,一个使用 ArrayList
实现(ArrayList
底层是使用数组实现的,在通常情况下,我们认为 ArrayList
性能也没有问题,具体实现以后再表)。每行代码对应的字节码我用空行格开。
objArray
的字节码:
0 bipush 10
2 anewarray #2 <java/lang/Integer>
5 astore_1
6 aload_1
7 iconst_0
8 iconst_2
9 invokestatic #3 <java/lang/Integer.valueOf>
12 aastore
13 aload_1
14 bipush 9
16 bipush 21
18 invokestatic #3 <java/lang/Integer.valueOf>
21 aastore
22 aload_1
23 iconst_0
24 aaload
25 invokevirtual #4 <java/lang/Integer.intValue>
28 aload_1
29 bipush 9
31 aaload
32 invokevirtual #4 <java/lang/Integer.intValue>
35 iadd
36 istore_2
37 return
list
的字节码:
0 new #5 <java/util/ArrayList>
3 dup
4 bipush 10
6 invokespecial #6 <java/util/ArrayList.<init>>
9 astore_1
10 aload_1
11 iconst_0
12 iconst_2
13 invokestatic #3 <java/lang/Integer.valueOf>
16 invokevirtual #7 <java/util/ArrayList.set>
19 pop
20 aload_1
21 bipush 9
23 bipush 21
25 invokestatic #3 <java/lang/Integer.valueOf>
28 invokevirtual #7 <java/util/ArrayList.set>
31 pop
32 aload_1
33 iconst_0
34 invokevirtual #8 <java/util/ArrayList.get>
37 checkcast #2 <java/lang/Integer>
40 invokevirtual #4 <java/lang/Integer.intValue>
43 aload_1
44 bipush 9
46 invokevirtual #8 <java/util/ArrayList.get>
49 checkcast #2 <java/lang/Integer>
52 invokevirtual #4 <java/lang/Integer.intValue>
55 iadd
56 istore_2
57 return
数组实现的虚拟机指令很明显再少,而且它有自己特殊的指令,如 anewarray
,对应普通对象的创建指令 new
,同时也省去了对象内方法的调用。如数组使用 aastore
指令存值,而list要使用 invokevirtual
指令调用 set
方法存值。
数组是其他数据结构的基础
数组虽然是对象,但在使用时,我们可以将它看作是如同基本类型一样,它是组成 Java 世界的基石之一。如同上面提到的 ArrayList
的底层实现就是数组,另外还有 ArrayDeque
(双端队列),Vector
(动态数组),Stack
(栈)等,很多数据结构都可以使用数组实现,我们以后会经常用到。所以熟悉数组的特性很重要。
关于数组需记住的要点
- 数组内存是连续的;
- 数组通过下标(索引)可以快速定位元素;
- 数组不可变,这也是数组最大的劣势,所以 Java 又实现了
ArrayList
来替代它。
这三点有因果关系。因为需要数组内存连续,所以数组不可变;因为数组不可变,才实现了通过下标快速查找元素。
怎么实现的快速定位?一个数组,记录它的起始点,如内存位置 32(只是举例),那么查找第 0 个元素,只需要返回 (32+0)位置的数据即可;查找第 10 个元素,只需要返回 (32+10)位置的数据即可。是不是很快速?