概述

为什么需要数组

容器

  • 生活中的容器:水杯(装水等液体),衣柜(装衣服等物品),集装箱(装货物等)。
  • 程序中的容器:将多个数据存储到一起,每个数据称为该容器的元素。

数组的概念

数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。

数组中的概念:

  • 数组名
  • 索引(或下标)
  • 元素
  • 数组的长度

数组的特点:

  • 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型
  • 创建数组对象会在内存中开辟一整块连续的空间。占据的空间的大小,取决于数组的长度和数组中元素的类型
  • 数组中的元素在内存中是依次紧密排列的,有序的
  • 数组,一旦初始化完成,其长度就是确定的。数组的长度一旦确定,就不能修改
  • 可以直接通过索引调用指定位置的元素,速度很快
  • 数组名中引用的是这块连续空间的首地址

数组的分类

按照元素类型分:

  • 基本数据类型元素的数组:每个元素位置存储基本数据类型的值
  • 引用数据类型元素的数组:每个元素位置存储对象(本质是存储对象的首地址)

按照维度分:

  • 一维数组:存储一组数据
  • 二维数组:存储多组数据,相当于二维表,一行代表一组数据,只是这里的二维表每一行长度不要求一样
  • 多维数组

一维数组

声明与初始化

声明

格式:

// 推荐
元素的数据类型[] 一维数组的名称;
 
// 不推荐
元素的数据类型  一维数组名[];
 
// 例如
int[] arr;
double[] arr2;
String[] arr3;  // 引用类型变量数组

数组的声明,需要明确:

  1. 数组的维度:在 Java 中数组的符号是 [][] 表示一维,[][] 表示二维。
  2. 数组的元素类型:即创建的数组容器可以存储什么数据类型的数据。元素的类型可以是任意的 Java 的数据类型。例如:intStringStudent 等。
  3. 数组名:就是代表某个数组的标识符,数组名其实也是变量名,按照变量的命名规范来命名。数组名是个引用数据类型的变量,因为它代表一组数据。

注意:Java 语言中声明数组时不能指定其长度(数组中元素的个数),例如:int a[5];,非法

静态初始化

如果数组变量的初始化和数组元素的赋值操作同时进行,那就称为静态初始化。

静态初始化,本质是用静态数据(编译时已知)为数组初始化。此时数组的长度由静态数据的个数决定。

格式 1:new 关键字

数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3, ...};
// 或
数据类型[] 数组名;
数组名 = new 数据类型[]{元素1, 元素2, 元素3, ...};
 
// 例如
int[] arr = new int[]{1, 2, 3, 4, 5};
// 或
int[] arr;
arr = new int[]{1, 2, 3, 4, 5};

new:关键字,创建数组使用的关键字。因为数组本身是引用数据类型,所以要用 new 创建数组实体。

格式 2:字面量

数据类型[] 数组名 = {元素1, 元素2, 元素3, ...}; // 必须在一个语句中完成,不能分成两个语句写
 
// 例如
int[] arr = {1, 2, 3, 4, 5}; // 正确
 
int[] arr;
arr = {1, 2, 3, 4, 5}; // 错误

动态初始化

数组变量的初始化和数组元素的赋值操作分开进行,即为动态初始化。

动态初始化中,只确定了元素的个数(即数组的长度),而元素值此时只是默认值,还并未真正赋自己期望的值。真正期望的数据需要后续单独一个一个赋值。

格式:

数组存储的元素的数据类型[] 数组名字 = new 数组存储的元素的数据类型[长度];
// 或
数组存储的数据类型[] 数组名字;
数组名字 = new 数组存储的数据类型[长度];
 
// 例如
int[] arr = new int[5];
// 或
int[] arr;
arr = new int[5];
 
// 错误写法
int[] arr = new int[5]{1, 2, 3, 4, 5};
// 后面有 {} 指定元素列表,就不需要在 [] 中指定元素个数了

数组的使用

数组的长度

数组的元素总个数,即数组的长度

每个数组都有一个属性 length 指明它的长度,例如:arr.length 指明数组 arr 的长度(即元素个数)

每个数组都具有长度,而且一旦初始化,其长度就是确定,且是不可变的。

数组元素的索引

如何表示数组中的一个元素?

每一个存储到数组的元素,都会自动的拥有一个编号,从 0 开始,这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。

格式:数组名[索引]

数组的索引范围?

Java 中数组的索引从 0 开始,下标范围是 [0, 数组的长度-1],即 [0, 数组名.length-1]

数组元素下标可以是整型常量或整型表达式,如:a[3]b[i]c[6 * i]

数组的遍历

将数组中的每个元素分别获取出来,就是遍历。for 循环与数组的遍历是绝配。

public class ArrayTest4 {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        System.out.println("数组的长度:" + arr.length); // 5
 
        // 遍历输出数组中的元素
        System.out.println("数组的元素有:");
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

数组元素的默认值

数组是引用类型,当我们使用动态初始化方式创建数组时,元素值只是默认值。例如:

public class ArrayTest6 {
	public static void main(String argv[]){
		int a[]= new int[5]; 
		System.out.println(a[3]); // a[3] 的默认值为 0
	}
} 
  • 对于基本数据类型而言,默认初始化值各有不同
  • 对于引用数据类型而言,默认初始化值为 null

一维数组内存分析

Java 虚拟机的内存划分

为了提高运算效率,对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

区域名称作用
虚拟机栈用于存储正在执行的每个 Java 方法的局部变量表等。局部变量表存放了编译期可知长度 的各种基本数据类型、对象引用,方法执行完,自动释放。。
堆内存存储对象(包括数组对象),new 来创建的,都存储在堆内存。
方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
本地方法栈当程序中调用了 native 的本地方法时,本地方法执行期间的内存区域域
程序计数器程序计数器是 CPU 中的寄存器,它包含每一个线程下一条要执行的指令的地址址

一维数组在内存中的存储

一个一维数组

public static void main(String[] args) {
  	int[] arr = new int[3];
  	System.out.println(arr); // [I@5f150435
}

数组下标为什么是 0 开始?因为第一个元素距离数组首地址间隔 0 个单元格

两个独立的一维数组

public static void main(String[] args) {
    int[] arr = new int[3];
    int[] arr2 = new int[2];
    System.out.println(arr);
    System.out.println(arr2);
}

两个变量指向一个一维数组

public static void main(String[] args) {
    // 定义数组,存储3个元素
    int[] arr = new int[3];
    // 数组索引进行赋值
    arr[0] = 5;
    arr[1] = 6;
    arr[2] = 7;
    // 输出 3 个索引上的元素值
    System.out.println(arr[0]);
    System.out.println(arr[1]);
    System.out.println(arr[2]);
    // 定义数组变量 arr2,将 arr 的地址赋值给 arr2
    int[] arr2 = arr;
    arr2[1] = 9;
    System.out.println(arr[1]);
}

多维数组

Java 语言里提供了支持多维数组的语法。

如果说可以把一维数组当成几何中的线性图形,那么二维数组就相当于是一个表格,像 Excel 中的表格、围棋棋盘一样。

对于二维数组的理解,可以看成是一维数组 array1 又作为另一个一维数组 array2 的元素而存在。

其实,从数组底层的运行机制来看,其实没有多维数组。

以下以二维举例,更高维同理

声明与初始化

声明

二维数组声明的语法格式:

// 推荐
元素的数据类型[][] 二维数组的名称;
 
// 不推荐
元素的数据类型  二维数组名[][];
// 不推荐
元素的数据类型[]  二维数组名[];

面试题:

int[] x, y[];
// x 是一维数组,y 是二维数组

静态初始化

格式:

int[][] arr = new int[][]{{3, 8, 2}, {2, 7}, {9, 0, 1, 6}};

定义一个名称为 arr 的二维数组,二维数组中有三个一维数组

每一个一维数组中具体元素也都已初始化

  • 第一个一维数组:arr[0] = {3, 8, 2};
  • 第二个一维数组:arr[1] = {2, 7};
  • 第三个一维数组:arr[2] = {9, 0, 1, 6};

第三个一维数组的长度表示方式:arr[2].length

// 字面量:声明与初始化必须在一句完成
int[][] arr = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9, 10}};
 
// new:声明与初始化可以分开
int[][] arr = new int[][]{{1, 2, 3}, {4, 5, 6}, {7, 8, 9, 10}};
 
int[][] arr;
arr = new int[][]{{1, 2, 3}, {4, 5, 6}, {7, 8, 9, 10}};
 
// 错误:静态初始化和动态初始化不能合在一起写
arr = new int[3][3]{{1, 2, 3}, {4, 5, 6}, {7, 8, 9, 10}};

动态初始化

如果二维数组的每一个数据,甚至是每一行的列数,需要后期单独确定,那么就只能使用动态初始化方式了。

格式 1:规则二维表(每一行的列数是相同的)

// 1. 确定行数和列数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[m][n];
// m: 表示这个二维数组有多少个一维数组。或者说一共二维表有几行
// n: 表示每一个一维数组的元素有多少个。或者说每一行共有一个单元格
 
// 此时创建完数组,行数、列数确定,而且元素也都有默认值
 
// 2. 再为元素赋新值
二维数组名[行下标][列下标] = 值;

举例:

int[][] arr = new int[3][2];
  • 定义了名称为 arr 的二维数组
  • 二维数组中有 3 个一维数组
  • 每一个一维数组中有 2 个元素
  • 一维数组的名称分别为 arr[0]arr[1]arr[2]
  • 给第一个一维数组 1 下标位赋值为 78 写法是:arr[0][1] = 78;

格式2:不规则二维表(每一行的列数不一样)

// 1. 先确定总行数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[总行数][];
 
// 此时只是确定了总行数,每一行里面现在是 null
 
// 2. 再确定每一行的列数,创建每一行的一维数组
二维数组名[行下标] = new 元素的数据类型[该行的总列数];
 
// 此时已经 new 完的行的元素就有默认值了,没有 new 的行还是 null
 
// 3. 再为元素赋值
二维数组名[行下标][列下标] = 值;

举例:

int[][] arr = new int[3][];
  • 二维数组中有 3 个一维数组
  • 每个一维数组都是默认初始化值 null
  • 可以对这个三个一维数组分别进行初始化
    • arr[0] = new int[3];
    • arr[1] = new int[1];
    • arr[2] = new int[2];
  • 注意:int[][]arr = new int[][3]; 是非法的

数组的长度和索引

二维数组的长度/行数:二维数组名.length

二维数组的某一行:二维数组名[行下标],此时相当于获取其中一组数据,它是一个一维数组。行下标的范围:[0, 二维数组名.length-1]。此时把二维数组看成一维数组的话,元素是行对象。

某一行的列数:二维数组名[行下标].length,因为二维数组的每一行是一个一维数组

某一个元素:二维数组名[行下标][列下标],即先确定行/组,再确定列

二维数组的遍历

双层嵌套 for 循环

int[][] arr = {
	{85, 96, 85, 75},
	{99, 96, 74, 72, 75},
	{52, 42}
};
 
for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j]);
    }
    System.out.println();
}

内存解析

Java 不同于 C/C++:

数组元素的赋值与数组复制

注意引用数据类型存储的是指针(内存地址),赋值是将内存地址复制了一份,指向的是同一个数组

要复制数组,需要重新 new 一个,并一个一个元素值进行复制

Arrays 工具类

java.util.Arrays 类是操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。 比如:

  • 数组转字符串
    • static String toString(int[] a):形式为:[元素1, 元素2, 元素3]
    • 元素将自动调用自己从 Object 继承的 toString 方法将对象转为字符串进行拼接,如果没有重写,则返回类型 @hash 值,如果重写则按重写返回的字符串进行拼接
  • 数组排序
    • static void sort(int[] a):将 a 数组按照从小到大进行排序
    • static void sort(int[] a, int fromIndex, int toIndex):将 a 数组的 [fromIndex, toIndex) 部分按照升序排列
    • static void sort(Object[] a):根据元素的自然顺序对指定对象数组按升序进行排序
    • static <T> void sort(T[] a, Comparator<? super T> c):根据指定比较器产生的顺序对指定对象数组进行排序
  • 数组元素的二分查找
    • static int binarySearch(int[] a, int key)static int binarySearch(Object[] a, Object key):要求数组有序,在数组中查找 key 是否存在,如果存在返回第一次找到的下标,不存在返回负数。
  • 数组的复制
    • static int[] copyOf(int[] original, int newLength):根据 original 原数组复制一个长度为 newLength 的新数组,并返回新数组
    • static <T> T[] copyOf(T[] original, int newLength):根据 original 原数组复制一个长度为 newLength 的新数组,并返回新数组
    • static int[] copyOfRange(int[] original, int from, int to):复制 original 原数组的 [from, to) 构成新数组,并返回新数组
    • static <T> T[] copyOfRange(T[] original, int from, int to):复制 original 原数组的 [from, to) 构成新数组,并返回新数组
  • 比较两个数组是否相等
    • static boolean equals(int[] a, int[] a2):比较两个数组的长度、元素是否完全相同
    • static boolean equals(Object[] a, Object[] a2):比较两个数组的长度、元素是否完全相同
  • 填充数组
    • static void fill(int[] a, int val):用 val 值填充整个 a 数组
    • static void fill(Object[] a, Object val):用 val 对象填充整个 a 数组
    • static void fill(int[] a, int fromIndex, int toIndex, int val):将 a 数组 [fromIndex, toIndex) 部分填充为 val 值
    • static void fill(Object[] a, int fromIndex, int toIndex, Object val):将 a 数组 [fromIndex, toIndex) 部分填充为 val 对象

数组中的常见异常

数组索引越界异常

public class TestArrayIndexOutOfBoundsException {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        System.out.println(arr[arr.length]); // ArrayIndexOutOfBoundsException
    }
}

当访问数组元素时,下标指定超出 [0, 数组名.length-1] 的范围时,就会报数组索引越界异常:ArrayIndexOutOfBoundsException

空指针异常

public class TestNullPointerException {
    public static void main(String[] args) {
        int[][] arr = new int[3][];
        System.out.println(arr[0][0]); // NullPointerException
    }
}

因为此时数组的每一行还未分配具体存储元素的空间,此时 arr[0]null,此时访问 arr[0][0] 会抛出 NullPointerException 空指针异常