1.1.6.1 一维数组

109 阅读9分钟

[TOC]

6.1 一维数组

内容导视:

  • 数组介绍
  • 一维数组的声明
  • 一维数组的遍历
  • 数组元素默认值
  • 数组赋值机制

6.1.1 数组介绍

数组是一种数组结构,当成一个容器吧,只能存放同一类型的元素。如 int 数组只能存放 int 类型的元素。(包括自动转换成 int 的元素)

数组一旦创建,长度不可变。如:

int[] arr = new int[5];

此数组实例的长度被确定为 5 了,不可改变。

数组中的元素的内存地址是连续的,数组拿==首元素的内存地址==作为整个数组的内存地址。

数组对象都有 length 属性,可以获取数组的长度。int length = arr.length;

每个元素都有下标,从 0 开始,以 1 递增,如最后一个元素的下标是 length - 1,首元素的下标为 0。

下标也被称为索引、index。

为什么数组长度不可变

int[] arr = new int[3];
arr = new int[6];
/*
这并不是改变了原数组的长度,而是重新在堆中创建了一个新的数组实例
将地址重新赋给引用,原来的数组实例由于没有引用指向被回收
*/

堆是一块内存空间,用来存放实例(我有时候也称对象)。引用是指保存了实例的内存地址的变量,可以通过引用操纵实例,如同遥控器控制电视一样。

语言设计者对需要执行的任务分配给不同的结构,数组具有固定长度,因为它旨在成为开发人员可以构建的低级别、简单的实现。

计算机内存有限,如果可以延长数组,但旁边的内存已经存了另一个对象怎么办?数组各个元素是彼此相邻存储,延长不了。

固定长度后,如果数组没有足够的空间存放元素,那么可以找到更大的空白空间创建一个新的更大的数组存放原有数组的数据,同时也可以腾出原数组的空间。

如果不确定数组长度多大,可以使用 List 集合存放数据。

集合底层已经实现好了,当数组快装满时,会在更大的空白处创建一个长度更大的数组,将原有数据复制到新数组,不需要我们手动实现。

为什么数组下标从 0 开始

  1. 历史原因

    之前 C 语言数组下标也是从 0 开始,没有必要出一种语言就改一次下标,增加额外的学习和理解成本。

  2. 减少 CPU 指令运算

    1)下标从 0 开始,计算第 i 个元素的地址,arr[i] = 首地址 + i * 单个元素所占字节。

    2)下标从 1 开始,计算第 i 个元素的地址,arr[i] = 首地址 + (i - 1) * 单个元素所占字节。

    每次寻找地址时,多了一次 i - 1 即减法的指令运算,更加消耗CPU 资源。(把大脑想象成 CPU,每次寻址时多计算一次二进制的减法)

使用数组存放元素的优点:根据下标查询元素效率极高

  • 每个元素的内存地址在空间上是连续的。
  • 数组中每个元素类型相同,占用空间大小一样。
  • 知道首元素的内存地址、每个元素的占用空间大小,通过下标可以算出元素的内存地址,直接通过内存地址定位元素。

缺点

  • ==由于保证数组中每个元素的内存地址连续==,随机增删元素时,会涉及到后面元素统一向后或向前位移的操作,效率较低。
  • 数组不能存储大数据量,因为很难在内存中找一块特别的大的连续的空间。

简化版本如下

数组是什么

  • 存储同一类型元素的容器,引用类型。

数组的优缺点

  • 根据下标查询元素效率极高
  • 随机增删元素效率低

为什么数组下标从 0 开始

前提:

  • 数组的每个元素的内存地址是连续的

  • 每个元素占用空间一样

知道首元素内存地址,可以算出第 i 个下标的元素的内存地址。设元素是 int 类型:(首地址即第一个元素的内存地址)

  • 如果下标从 1 开始,arr[i] 的地址 = 首地址 + (i - 1) * 4
  • 如果下标从 0 开始,arr[i] 的地址 = 首地址 + i * 4

很明显从 1 开始多算了一次减法,消耗的 CPU 更多。

为什么数组长度不可变

  • 数组是连续不断的

    • 如果数组可以延长,如果旁边恰好存了一个对象怎么办?
    • 如果数组可以缩减,那么突然空出来了一小块空间,别人如何利用?
  • 解决

    固定数组长度,如果数组满了:

    另寻一个更大的空间创建更大的数组,把原有数据填入新数组。旧数组无引用指向被当作垃圾回收,释放空间。

6.1.2 一维数组的声明

静态初始化

在声明时,同时确定了元素的值。

int[] array = {1, 2, 92, 64, 90};
/*
也可以这么写:
int[] array = new int[]{1, 2, 92, 64, 90};
有时候传参,或者先声明了 array,需要用到
*/

创建了一个 int 类型、长度为 5 的数组,引用名为 array,存放了 5 个 int 类型的元素,分别为 1、2、92、64、90。

上面图的元素,下标从左到右,依次为 0、1、2、3、4。

通过引用访问下标对应的元素:

// 访问第 1 个元素
int a1 = array[0];
// 访问第 5 个元素
int a5 = array[4];
// 获取数组的长度
int length = array.length;

System.out.println(a1);// 1
System.out.println(a5);// 90
System.out.println(length);// 5

动态初始化

int[] array2 = new int[3];
/*
可以先声明,后分配空间
int[] array2;
array2 = new int[3];
*/

创建了一个 int 类型、长度为 3 的数组,引用名为 array2,存放了 3 个 int 类型的元素,值都默认为 0。

System.out.println(array2[0]);// 0

赋值

// 把 53 赋给下标为 0 的元素
array2[0] = 53;
array2[1] = 2;
array2[2] = 44;

System.out.println(array2[0]);// 53

int[] arr1;// 此变量没有保存任何值,必须赋值(初始化)才能够访问
int[] arr2 = null;// 此变量保存了 null,代表空
int[] arr3 = {};// 创建了一个长度为 0 的数组(数组无元素),地址赋给了 arr3
int[] arr4 = new int[0];// 同上

6.1.3 一维数组的遍历

数组中的元素下标从 0 到 length - 1,可以使用 for 循环访问每个元素。

int[] arr = {6, 2, 9};

for (int i = 0; i < arr.length; i++) {
    int num = arr[i];
    System.out.println(num);
}

注意:当访问不存在的下标时,会报数组索引越界异常。

int i = arr[3];
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
        at Hello.main(Hello.java:5)

6.1.4 数组元素默认值

使用动态初始化时,不同类型的元素会有默认值。

数据类型默认值
short、byte、int、long0
float、double0.0
booleanfalse
char'\u0000'
引用类型包括 String、数组null
long[] arr = new long[3];
byte b = arr[0];// 报错:从 long 转换到 byte 可能会有损失

特别说明一下:

char 的 '\u0000' 并不是空格,而是空字符,代表什么都没有。

char[] arr = new char[3];
arr[1] = ' ';
System.out.println(arr[0] + 0);// 0
System.out.println(arr[1] + 0);// 32

'\u0000' 对应整数 0,另一种写法 '\0',是字符的八进制表示。打个比方,'a' 对应的 8、10、16 进制分别为 0141、97、0x0061,如下都是一个意思。

char c1 = 97;
char c2 = 0141;
char c3 = 0x0061;
char c4 = '\141';
char c5 = 'a';

6.1.5 数组赋值机制

int[] arr1 = {56, 6, 2};
// 将 arr1 保存的值拷贝一份,赋给 arr2
int[] arr2 = arr1;

arr1 保存的值为数组实例的内存地址,假设为 0x1111,然后拷贝一份地址(创建副本)给 arr2,本质还是值传递。

修改 arr2 保存的值,arr1 不会受到影响。

arr2 = null;
System.out.println(arr1);// [I@15db9742(等同于实例的内存地址)

本来应该就此打住,但是有人可能还接触过引用传递这个概念:

值传递(pass by value):赋值时将值拷贝一份赋给另一个变量,这样在另一个变量中修改自己保存的值,不会影响到最初的变量;

类似 Ctrl + C、Ctrl + V。

引用传递(pass by reference):赋值时将值的地址赋给另一个变量,另一个变量通过内存地址定位到此值,如果修改此值,会影响原来变量保存的值;

类似 Windows 系统的创建桌面快捷方式。

值传递引用传递
会创建副本不会创建副本
无法改变原变量保存的值可以改变原变量保存的值

有人就问了:

int[] arr1 = {6, 62, 2};
int[] arr2 = arr1;

// 使用 arr2 改变了数组中第一个元素的值
arr2[0] = 99;
// 你看看,这不影响了 arr1 吗?
System.out.println(arr1[0]);// 99

不不不,不是这个意思,把 arr1、arr2 当成两个独立的遥控器吧,电视只有一个,arr2 换台,受影响的是电视,而不是 arr1。

这里修改的是数组中的元素,而不是 arr1 保存的值,arr1 保存的值还是这个实例的地址,没受影响。

结论:arr1 把自己保存的值拷贝一份赋给了arr2,只不过这个值恰好是地址,就当作是值传递吧。

数组实例 {6, 62, 2} 把地址赋给 arr1,才有点引用传递的味道。

由于不能直接通过实例访问到元素,必须借助引用;当没有引用时,再也不可能访问到此实例,等同于垃圾。

有人说,不对啊?必须借助引用访问元素这我知道,如 arr[0];可当没有引用时,我可以重新把这个实例的地址赋给另一个变量,又来了一个引用,这不就可以访问到了吗?

int[] arr1 = {5, 6, 2};// 保存实例的地址的变量称为引用
// 现在 arr1 不是 {5, 6, 2} 的引用了
arr1 = null;

// 重新将此实例的地址赋给 arr2
int[] arr2 = {5, 6, 2};
// 我访问到了!
System.out.println(arr2[1]);// 6

可是,你确定第 1 行与第 6 行的 {5, 6, 2} 是同一个实例吗?

要想确认是否是同一个实例,需要借助引用修改这个实例的值,再看看实例是否被修改了。如果被修改了,说明是同一个实例。

int[] arr1 = {5, 6, 2};
int[] arr2 = {5, 6, 2};

// 修改 arr1 对应的实例第一个元素为 9
arr1[0] = 9;

// 访问 arr2 对应的实例第一个元素,还是 5
System.out.println(arr2[0]);// 5

然而并没有被修改,说明不是同一个实例。所以第 1 行的 {5, 6, 2} 永远不可访问到了,这种访问不到的实例已经没有用处,需要及时被垃圾回收器清理。

也可通过直接输出引用、双等号比较,查看是否是同一个实例:

int[] arr1 = {5, 6, 2};
int[] arr2 = arr1;
int[] arr3 = {5, 6, 2};

// arr1 与 arr2 保存的值一样
System.out.println(arr1 == arr2);// true
// arr1 与 arr3 保存的值不一样,说明保存的不是同一个实例的内存地址
System.out.println(arr1 == arr3);// false

System.out.println(arr1);// [I@15db9742
System.out.println(arr2);// [I@15db9742
System.out.println(arr3);// [I@6d06d69c

于是我们通常说:基本数据类型使用双等号比较的是值,引用类型使用双等号比较的是内存地址。

那如果有人只认为保存的内容相等就行,不在乎是否为不同实例,在源代码最上面加入 import java.util.Arrays;

int[] arr1 = {5, 6, 2};
int[] arr2 = arr1;
int[] arr3 = {5, 6, 2};

// equals 比较内容
System.out.println(Arrays.equals(arr1, arr3));// true
System.out.println(arr1 == arr3);// false